Proxy

2020/7/7 ProxyES6

# Heading

# Proxy

Proxy 实际上重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义。

var obj = new Proxy({}, {
    get: function (target, propKey, receiver) {
        console.log("getting " + propKey + "!");
        return Reflect.get(target, propKey, receiver);
    },
    set: function (target, propKey, value, receiver) {
        console.log("setting " + propKey + "!");
        return Reflect.set(target, propKey, value, receiver);
    }
});
var reactive = function (obj) {
    return new Proxy(obj, {
        get: function (target, propKey, receiver) {
            console.log({ target: target, propKey: propKey, receiver: receiver });
            console.log("getting " + propKey);
            var ret = Reflect.get(target, propKey, receiver);
            return typeof ret !== 'object' || ret === null ? ret : reactive(obj);
        },
        set: function (target, propKey, value, receiver) {
            console.log({ target: target, propKey: propKey, value: value, receiver: receiver });
            console.log("setting " + propKey);
            return Reflect.set(target, propKey, value, receiver);
        }
    });
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 拦截操作

  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']。
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。
  • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一 个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。

# get

var ObjEx = function (obj) {
    return new Proxy(obj, {
        get: function (target, propKey, receiver) {
            if (Reflect.has(target, propKey)) {
                return target[propKey];
            }
            else {
                throw new Error("no prop[" + propKey + "]");
            }
        }
    });
};
1
2
3
4
5
6
7
8
9
10
11
12
  • get方法可以继承
let proto = new Proxy({}, {
  get(target, propertyKey, receiver) {
    console.log('GET ' + propertyKey);
    return target[propertyKey];
  }
});

let obj = Object.create(proto);
obj.foo // "GET foo"
1
2
3
4
5
6
7
8
9
  • 数组读取负数的索引
function createArray() {
    var elements = [];
    for (var _i = 0; _i < arguments.length; _i++) {
        elements[_i] = arguments[_i];
    }
    return new Proxy(__spreadArrays(elements), {
        get: function (target, propKey, receiver) {
            propKey = Number(propKey);
            if (propKey >= -target.length && propKey <= target.length - 1) {
                // return propKey < 0 ? target[target.length + +propKey] : target[propKey]
                var index = propKey < 0 ? target.length + +propKey : propKey;
                return Reflect.get(target, index, receiver);
            }
            else {
                throw new RangeError('out of Array Range');
            }
        }
    });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 利用 Proxy,可以将读取属性的操作(get),转变为执行某个函数,从而实现属性的链式操作。
var pipe = function (value) {
    var funcStack = [];
    var oproxy = new Proxy({}, {
        get: function (pipeObject, fnName) {
            if (fnName === 'get') {
                return funcStack.reduce(function (val, fn) { return fn(val); }, value);
            }
            funcStack.push(window[fnName]);
            return oproxy;
        }
    });
    return oproxy;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则通过 Proxy 对象访问该属性会报错。
VM3651:1 Uncaught TypeError: 'get' on proxy: property 'foo' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '123' but got '123')
    at <anonymous>:1:7
1
2

# set

  • 用于校验
function ageValidator(obj) {
    return new Proxy(obj, {
        set: function (target, p, value, receiver) {
            if (p === 'age') {
                if (!Number.isInteger(value)) {
                    throw new TypeError('age must be an Integer');
                }
                if (value < 0 || value > 200) {
                    throw new RangeError('age must between 0 and 200');
                }
            }
            return Reflect.set(target, p, value, receiver);
        }
    });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 用于防止私有变量访问
function privateGetSet(obj = {}) {
    const invariant = (key, action) => {
        if (key[0] === '_') {
            throw new Error(`Invalid attempt to ${action} private "${key}" property`);
        }
    }

    return new Proxy(obj, {
        get(target, key) {
            invariant(key, 'get');
            return target[key];
        },
        set(target, key, value) {
            invariant(key, 'set');
            target[key] = value;
            return true;
        }
    })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 第四个参数receiver,指的是原始的操作行为所在的那个对象,一般情况下是proxy实例本身
const proxy = new Proxy({}, {
    set: function (obj, prop, value, receiver) {
        obj[prop] = receiver;
    }
});
const myObj = {};
Object.setPrototypeOf(myObj, proxy);

myObj.foo = 'bar';
myObj.foo === myObj // true
1
2
3
4
5
6
7
8
9
10

# has

  1. 如果原对象不可配置或者禁止扩展,这时has拦截会报错。
Uncaught TypeError: 'has' on proxy: trap returned falsish for property 'a' but the proxy target is not extensible
    at <anonymous>:1:5
1
2
  1. has方法拦截的是HasProperty操作,而不是HasOwnProperty操作,即has方法不判断一个属性是对象自身的属性,还是继承的属性。
  2. 虽然for...in循环也用到了in运算符,但是has拦截对for...in循环不生效
  • 隐藏私有属性
function hidePrivate(obj){
    return new Proxy(obj,{
        has:(target,p) =>{
            if(p[0] === '_'){
                return false
            }
            return Reflect.has(target,p)
        }
    })
}
1
2
3
4
5
6
7
8
9
10

# deleteProperty

  • 目标对象自身的不可配置(configurable)的属性,不能被deleteProperty方法删除,否则报错。
function protectPrivate(obj){
    return new Proxy(obj, {
        deleteProperty: (target, key) => {
            if (key[0] === '_') {
                throw new Error(`Invalid attempt to delete private "${key}" property`)
            }
            return Reflect.deleteProperty(target, key)
        }
    })
}
1
2
3
4
5
6
7
8
9
10

# ownKeys

  • 拦截对象自身属性的读取操作。
    • Object.getOwnPropertyNames()
    • Object.getOwnPropertySymbols()
    • Object.keys()
    • for...in循环
  • 三类属性会被ownKeys()方法自动过滤,不会返回
    • 目标对象上不存在的属性
    • 属性名为 Symbol 值
    • 不可遍历(enumerable)的属性
  • ownKeys()方法返回的数组成员,只能是字符串或 Symbol 值。如果有其他类型的值,或者返回的根本不是数组,就会报错。
  • 如果目标对象自身包含不可配置的属性,则该属性必须被ownKeys()方法返回,否则报错。
  • 如果目标对象是不可扩展的(non-extensible),这时ownKeys()方法返回的数组之中,必须包含原对象的所有属性,且不能包含多余的属性,否则报错。

# getOwnPropertyDescriptor

  • 拦截Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者undefined

# defineProperty

  • 如果目标对象不可扩展(non-extensible),则defineProperty()不能增加目标对象上不存在的属性,否则会报错(尝试了一下没有报错)
  • 如果目标对象的某个属性不可写(writable)或不可配置(configurable),则defineProperty()方法不得改变这两个设置。
function preventAdd(obj) {
    return new Proxy(obj, {
        defineProperty: (target, key, desc) => {
            // return false
            desc = { writable: false }
            return Reflect.defineProperty(target, key, desc)
        }
    })
}
1
2
3
4
5
6
7
8
9

# preventExtensions

  • 必须返回一个布尔值,否则会被自动转为布尔值。
  • 只有目标对象不可扩展时(即Object.isExtensible(proxy)为false),proxy.preventExtensions才能返回true,否则会报错。 通常自己调用一下
var proxy = new Proxy({}, {
  preventExtensions: function(target) {
    Reflect.preventExtensions(target);
    return true;
  }
});
1
2
3
4
5
6

# getPrototypeOf

  • 拦截获取对象原型

    • Object.prototype.proto
    • Object.prototype.isPrototypeOf()
    • Object.getPrototypeOf()
    • Reflect.getPrototypeOf()
    • instanceof
  • getPrototypeOf()方法的返回值必须是对象或者null,否则报错

  • 如果目标对象不可扩展(non-extensible), getPrototypeOf()方法必须返回目标对象的原型对象

# isExtensible

  • 拦截Object.isExtensible()操作
  • 只能返回布尔值,否则返回值会被自动转为布尔值
  • 返回值必须与目标对象的isExtensible属性保持一致,否则就会抛出错误

# setPrototypeOf

  • 该方法只能返回布尔值,否则会被自动转为布尔值。
  • 如果目标对象不可扩展(non-extensible),setPrototypeOf()方法不得改变目标对象的原型
var handler = {
  setPrototypeOf (target, proto) {
      // proxy.__proto__ = {}
      // Object.setPrototypeOf(proxy, {});
    throw new Error('Changing the prototype is forbidden');
  }
};
1
2
3
4
5
6
7

# apply

var twice = {
    apply(target, ctx, args) {
        return Reflect.apply(...arguments) * 2;
    }
};
function sum(left, right) {
    return left + right;
};
var proxy = new Proxy(sum, twice);
proxy(1, 2) // 6
proxy.call(null, 5, 6) // 22
proxy.apply(null, [7, 8]) // 30
Reflect.apply(proxy, null, [9, 10]) // 38
1
2
3
4
5
6
7
8
9
10
11
12
13

# construct

  • 拦截new命令
  • 由于construct()拦截的是构造函数,所以它的目标对象必须是函数,否则就会报错
  • 必须返回对象,否则会报错
  • construct()方法中的this指向的是handler(普通函数),而不是实例对象
function constructObj(obj) {
    const handler = {
        construct: function (target, argArray, newTarget) {
            console.log(this !== target, this === handler, target === newTarget, this === window)
            //箭头函数: true false false true
            //普通函数: true true false false
            // return new target(...argArray)
            return {
                value: argArray.toString()
            }
        }
    }
    return new Proxy(obj, handler)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# Proxy.revocable()

  • Proxy.revocable()的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问
  • 调用revoke函数将报错VM2942:1 Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
p = Proxy.revocable({},{})
// {
//     proxy: Proxy {}
//     revoke: ƒ ()
// }
1
2
3
4
5

# this 问题

  • 正常情况下(箭头函数除外),Proxy代理的钩子函数中的this指向的是Proxy代理实例(construct钩子函数除外,该钩子函数中this指向的是handler)
  • 虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理
  • 有些原生对象的内部属性(Date.getDate),只有通过正确的this才能拿到,所以 Proxy 也无法代理这些原生对象的属性。this绑定原始对象,就可以解决这个问题。
const target = new Date('2015-01-01');
const handler = {
    //TODO  为什么是拦截get而不是apply?
  get(target, prop) {
    if (prop === 'getDate') {
      return target.getDate.bind(target);
    }
    return Reflect.get(target, prop);
  }
};
const proxy = new Proxy(target, handler);
proxy.getDate() // 1
//不绑定this - TypeError: this is not a Date object.
1
2
3
4
5
6
7
8
9
10
11
12
13