Proxy&Reflect官网笔记

297 阅读5分钟

Proxy

  • 1.get(target, propKey, receiver)

    拦截对象属性的读取, 比如proxy.fooproxy['foo']

    最后一个参数receiver 是一个对象, 可选, 参见下面Reflect.get部分.

    const _obj = { name: 'veloma' };
    const obj = new Proxy(_obj, {
        get: function(target, propKey, receiver) {
            if (propKey in target) return target[propKey];
            throw new Error(`Property ${propKey} does not exist.`);
        }
    });
    
    obj.name // 'veloma'
    obj.age // Error: Property age does not exist.
    

    我们来解释一下这段代码, 如果访问目标对象不存在的属性, 会抛出一个错误.如果没有这个拦截函数, 访问不存在的属性, 只会返回undefined

    get 方法可以继承

    const proto = new Proxy({}, {
        get(target, propertyKey, receiver) {
            console.log('GET' + propertyKey);
            return target[propertyKey];
        }
    });
    
    const obj = Object.create(proto);
    obj.name; // GET name
    

    但是如果这样写的话, get 方法就监听不到了

    const obj = Object.create(proto);
    obj.name = 'veloma';
    obj.name; // 这里就会执行get方法
    

    如果我们想要他执行, 该怎么办呢?

    const proto = new Proxy({}, {
        get() {...},
        set(target, propertyKey, value, receiver) {
            target[propertyKey] = value;
            return true;
        }
    });
    
    const obj = Object.create(proto);
    obj.name = 'veloma';
    obj.name; // GET name
    

    这里我们需要把proto给改造一下, 给他加上一个set方法才行.

    🌰: 使用get拦截, 实现数组读取负数的索引.

    function craeteArray(...elements) {
        let handler = {
            get(target, propKey, receiver) {
                let index = Number(propKey);
                if (index < 0) index = target.length + index;
                return Reflect.get(target, index, receiver);
            }
        };
    
        const target = [];
        target.push(...elements);
        return new Proxy(target, handler);
    }
    
    const arr = createArray('a', 'b', 'c');
    arr[-1]; // c
    

    我们来解释一下这段代码, 数组的位置参数是-1 , 就会输出数组的倒数最后一个成员, 其中有一句代码没有说过Reflect.get(target, propKey, receiver) , 看不懂没关系, 这里只要知道他是返回数组的某个索引值即可, 下面Reflect 部分会讲到.

    利用proxy, 可以将读取属性的操作(get), 转变为执行某个函数, 从而实现属性的链式操作.

    const global = {
      double: n => n * 2,
      pow: n => n * n,
      reverseInt: n => n.toString().split('').reverse().join('') | 0
    };
    
    const pipe = (function () {
        return function (value) {
            const funcStack = [];
            const proxy = new Proxy({}, {
                get(pipeObject, fnName) {
                    if (fnName === 'get') {
                        return funcStack.reduce((val, fn) => fn(val), value);
                    }
                    funcStack.push(global[fnName]);
                    return proxy;
                }
            });
    
            return proxy;
        }
    }());
    
    pipe(3).double.pow.reverseInt.get; // 63
    

    我们来解释一下这段代码

    1. 调用pipevalue=3传进去, 这时候不会执行proxy, 最后将proxy返回.
    2. 这时候pipe(3) 是一个proxy, 当调用double方法的时候, 会执行proxyget方法, 当执行get方法的时候, pipeObject一直为{}, 而这时候fnName则是double, 判断fnName不为'get', 则将该方法pushfuncStack中, 再返回proxy .
    3. 这时候pipe(3).double依旧是一个proxy , 当调用pow方法的时候也会执行proxyget方法, 而这时候fnNamepow, fnName依旧不是'get', 所以也会将pow方法pushfuncStack中, 然后返回proxy .
    4. 这时候pipe(3).double.pow依旧是proxy, 接着调用reverseInt方法, 执行结果与double方法和pow方法一致.
    5. 最后在调用get 方法的时候, 将funcStack中的方法, 一个一个拿出来执行, 并返回最终的执行结果(最后返回的结果不是proxy).

    上面的代码设置proxy后, 达到了奖函数名链式使用的结果.

    🌰: 利用get拦截, 实现一个生成各种DOM节点的通用函数dom.

    const dom = new Proxy({}, {
        get(target, property) {
            return function(attrs = {}, ...children) {
                const el = document.createElement(property);
                for (let prop of Object.keys(attrs)) {
                    el.setAttribute(prop, attrs[prop]);
                }
                for (let child of children) {
                    if (typeof child === 'string') {
                        child = document.createTextNode(child);
                    }
                    el.appendChild(child);
                }
                return el;
            }
        }
    });
    
    const el = dom.div({},
    	'Hello, my name is ',
    	dom.a({href: '//example.com'}, 'Mark'),
    	'. I like',
    	dom.ul({}, 
                dom.li({}, 'The web'),
                dom.li({}, 'Food'),
                dom.li({}, '...actually that\'s it')
    	)
    );
    

    上面这个方法就比较简单了, 其实就是相当于这样写

    function createElement(property, attrs = {}, ...children) {
    	const el = document.createElement(property);
    	for (let prop of Object.keys(attrs)) {
                el.setAttribute(prop, attrs[prop]);
    	}
    	for (let child of children) {
                if (typeof child === 'string') {
                        child = document.createTextNode(child);
                }
                el.appendChild(child);
    	}
    	return el;
    }
    
    const el = createElement('div', {}, 
    	'Hello, my name is ',
    	createElement('a', {hreft: '//example.com'}, 'Mark'),
    	createElement('ul', {}, 
                createElement('li', {}, 'The web'),
                createElement('li', {}, 'Food'),
                createElement('li', {}, '...actually that\'s it')
    	)
    );
    
  • 2.set(target, propKey, value, receiver)

    拦截对象属性的设置, 比如proxy.foo = v 或者 proxy['foo'] = v, 返回一个布尔值.

    🌰: 假设Person对象有一个age属性, 该属性应该是一个不大于150的整数, 那么可以使用Proxy保证age的属性之符合要求.

    const validator = {
        set(target, key, value) {
            if (key === 'age') {
                // isInteger方法, 看名字就知道, 他用来判断一个值是否为整数, 
                // 要注意, 值必须为number类型, 如果是string类型的数字是不行的
                if (!Number.isInteger(value)) {
                    throw new TypeError('The age is not an integer');
                }
                if (value > 150) {
                    throw new RangeError('The age seems invalid');
                }
            }
            target[key] = value;
            return true;
        }
    };
    
    const person = new Proxy({}, validator);
    
    person.age = 100;
    person.age; // 100
    person.age = 'young'; // TypeError: The age is not an integer
    person.age = 200; // RangeError: The age seems invalid
    

    我们来解释一下这段代码, 由于设置了存值函数set, 任何不符合要求的age属性赋值, 都会抛出一个错误. 利用set方法, 还可以数据绑定, 即当对象发生变化时, 会自动更新DOM.

    我们还可以结合getset方法来做私有属性

    const handler = {
        get(target, key) {
            invariant(key, 'get');
            return target[key];
        },
        set(target, key, value) {
            invariant(key, 'set');
            target[key] = value;
            return true;
        }
    }
    
    function invariant(key, action) {
        if (key[0] === '_') {
            throw new Error(`Invalid attempt to ${action} private "${key}" property.`);
        }
    }
    
    const target = { name: 'veloma', _name: 'timer' };
    const proxy = new Proxy(target, handler);
    proxy.name; // veloma
    proxy.name = 'timer';
    proxy._name; // Error: Invalid attempt to get private "_name" property
    proxy._name = 'veloma'; // Error: Invalid attempt to set private "_name" property
    

    我们来解释一下这段代码, 只要读写的属性名的第一个字符是下划线, 一律报错, 从而达到禁止读写内部属性的目的.

  • 3.has(target, propKey)

    has 方法用来拦截HasProperty操作, 即判断对象是否具有某个属性时, 对这个方法会生效. 典型的操作就是in运算符.(拦截propKey in proxy的操作, 以及对象的hasOwnProperty方法, 返回一个布尔值).

    来个🌰: 使用has方法隐藏某些属性, 不会被in运算符发现.

    const handler = {
        has(target, key) {
            if (key[0] === '_') return false;
            return key in target;
        }
    };
    
    const target = { _prop: 'foo', prop: 'foo' };
    
    const proxy = new Proxy(target, handler);
    
    '_prop' in proxy; // false
    'prop' in proxy; // true
    

    我们来解释一下这段代码, 如果属性名是以下划线_开头的, proxy.has就会返回false, 从而不被in运算符发现.

    如果原对象不可配置或者禁止扩展, 这是has 拦截会报错

    const obj = { a: 10 };
    Object.preventExtensions(obj);
    
    const p = new Proxy(obj, {
        has: (target, prop) => false
    });
    
    'a' in p; // TypeError is thrown
    

    上面代码中, obj对象禁止扩展, 结果使用has拦截就会报错.

    注意: has方法拦截的是HasProperty操作, 而不是HasOwnProperty操作, 即has方法不判断一个属性是对象自身属性, 还是继承的属性. 另外, 虽然 for...in 循环也用到了in 运算符, 但是has 拦截对for...in 循环不生效.

    const stu1 = { name: '张三', score: 59 };
    const stu2 = { name: '李四', score: 99 };
    
    const handler = {
        has(target, prop) {
            if (prop === 'score' && target[prop] < 60) {
                console.log(`${target.name} 不及格`);
                return false;
            }
            return prop in target;
        }
    }
    
    const proxy1 = new Proxy(stu1, handler);
    const proxy2 = new Proxy(stu2, handler);
    
    'score' in proxy1; // 张三 不及格 false
    'score' in proxy2; // true
    
    for (let a in proxy1) {
        console.log(proxy1[a]);
    }
    // 张三 
    // 59
    
    for (let b in proxy2) {
        console.log(proxy2[b]);
    }
    // 李四
    // 99
    

    上面代码中, has拦截只对in循环生效, 对for...in循环不生效, 导致不符合要求的属性没有被排除在for...in循环之外.

  • 4.deleteProperty(target, propKey)

    deleteProperty 方法用于拦截delete操作, 如果这个方法抛出错误或者返回false, 当前属性就无法被delete命令删除.(拦截delete proxy[propKey]的操作, 返回一个布尔值).

    const handler = {
        deleteProperty(target, key) {
            invariant(key, 'delete');
            return true;
        }
    }
    
    function invariant(key, action) {
        if (key[0] === '_') {
            throw new Error(`Invalid attempt to ${action} private "${key}" property`);
        }
    }
    
    const target = { _prop: 'foo' };
    const proxy = new Proxy(target, handler);
    	delete proxt._prop; // Invalid attempt to delete private "_prop" property
    
    

    上面代码中, deleteProperty 方法拦截了delete 操作符, 删除第一个字符为下划线的属性会报错.

  • 5.ownKeys(target)

    拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy), 返回一个数组.该方法返回对象所有自身的属性, 而Object.keys()仅返回对象可遍历的属性.

    const target = {};
    
    const handler = {
        ownKeys(target) {
            return ['hello', 'world'];
        }
    };
    
    const proxy = new Proxy(target, handler);
    Object.keys(proxy); // ['hello', 'world']
    

    上面代码拦截了对于target对象的Object.keys操作, 返回预先设定的数组.

    下面的例子是拦截第一个字符为下划线的属性名.

    const target = {
        _bar: 'foo',
        _prop: 'bar',
        prop: 'baz'
    };
    
    const handler = {
        ownKeys(target) {
            return Reflect.ownKeys(target).filter(key => key[0] !== '_');
        }
    }
    
    const proxy = new Proxy(target, handler);
    for (let key of Object.keys(proxy)) {
        console.log(target[key]);
    }
    // baz
    
  • 6.getOwnPropertyDescriptor(target, propKey)

    getOwnPropertyDescriptor 方法拦截Object.getOwnPropertyDescriptor, 返回一个属性描述对象或者undefined(拦截Object.getPropertyDescriptor(target, propKey)).

    const handler = {
        getOwnPropertyDescriptor(target, key) {
            if (key[0] === '_') {
                return;
            }
            return Object.getOwnPropertyDescriptor(target, key);
        }
    };
    
    const target = { _foo: 'bar', baz: 'tar' };
    const proxy = new Proxy(target, handler);
    Object.getOwnPropertyDescriptor(proxy, 'wat'); // undefined
    Object.getOwnPropertyDescriptor(proxy, '_foo'); // undefined
    Object.getOwnPropertyDescriptor(proxy, 'baz'); // { value: 'tar', writable: true, enumerable: true, configurable: true }
    

    上面代码中, handler.getOwnPropertyDescriptor方法对于第一个字符为下划线的属性名会返回undefined.

  • 7.defineProperty(target, propKey, propDesc)

    defineProperty 方法拦截了Object.defineProperty操作(拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs), 返回一个布尔值).

    const handler = {
        defineProperty(target, key, descriptor) {
            return false;
        }
    }
    
    const target = {};
    const proxy = new Proxy(target, handler);
    proxy.foo = 'bar'; // TypeError: proxy defineProperty handler returned false for property '"foo"'
    

    上面代码中, defineProperty 方法返回false , 导致添加新属性会抛出错误.

  • 8.preventExtensions(target)

    preventExtensions方法拦截Object.preventExtensions.该方法必须返回一个布尔值.这个方法有一个限制, 只有当Object.isExtensible(proxy)false(即不可扩展)时, proxy.preventExtensions才能返回true, 否则会报错.(拦截Object.preventExtensions(proxy), 返回一个布尔值).

    const proxy = new Proxy({}, {
        preventExtensions(target) {
            return true;
        }
    });
    
    Object.preventExtensions(proxy); // 报错
    

    上面代码中, proxy.preventExtensions方法返回true, 但这时Object.isExtensible(proxy)会返回true, 因此报错.

    为了防止出现这个问题, 通常要在proxy.preventExtensions方法里面, 调用一次Object.preventExtensions.

    const proxy = new Proxy({}, {
        preventExtensions(target) {
            console.log('called');
            Object.preventExtensions(target);
            return true;
        }
    });
    
    Object.preventExtensions(proxy); // called true
    
  • 9.getPrototypeOf(target)

    getPrototypeOf方法主要用来拦截Object.getPrototypeOf()运算符, 以及其他一些操作(拦截Object.getPrototypeOf(proxy), 返回一个对象).

    • Object.prototype.__proto__
    • Object.prototype.isPrototypeOf()
    • Object.getPrototypeOf()
    • Reflect.getPrototypeOf()
    • instance运算符

    来个🌰:

    const proto = {};
    const proxy = new Proxy({}, {
        getPrototypeOf(target) {
            return proto;
        }
    });
    
    Object.getPrototypeOf(proxy) === proto; // true
    

    上面代码中, getPrototypeOf方法拦截Object.getPrototypeOf(), 返回proto对象

  • 10.isExtensible(target)

    拦截Object.isExtensible(proxy), 返回一个布尔值.

    const proxy = new Proxy({}, {
        isExtensible(target) {
            console.log('calle');
            return true;
        }
    });
    
    Object.isExtensible(proxy); // called true
    

    上面代码设置了isExtensible方法, 在调用Object.isExtensible时会输出called.

    这个地方有一个强限制, 如果不能满足下面的条件, 就会抛出错误.

    Object.isExtensible(proxy) === Object.isExtensible(target)
    

    来个🌰:

    const proxy = new Proxy({}, {
        isExtensible(target) {
            return false;
        }
    });
    Object.isExtensible(proxy); // 报错
    
  • 11.setPrototypeOf(target, proto)

    拦截Object.setPrototypeOf(proxy, proto), 返回一个布尔值.

    如果目标对象是函数, 那么还有两种额外操作可以拦截.

    const handler = {
        setPropertyOf(target, proto) {
            throw new Error('Changing the prototype is forbidden');
        }
    }
    
    const proto = {};
    const target = function() {};
    const proxy = new Proxy(target, handler);
    proxy.setPrototypeOf(proxy, proto); // Error: Changing the prototype is forbidden
    

    上面代码中, 只要修改target的原型对象, 就会报错.

  • 12.apply(target, object, args)

    apply方法拦截proxy实例的函数调用、callapply操作, 比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)

    const handler = {
        apply(target, ctx, args) {
            return Reflect.apply(...arguments);
        }
    }
    

    apply 方法可以接受三个函数, 分别是目标对象(函数自己)、目标对象的上下文对象(this)和目标对象的参数数组

    来个🌰:

    const target = function () { return 'I am the veloma' };
    const handler = {
        apply: () => 'I am the timer'
    };
    
    const p = new Proxy(target, handler);
    
    p(); // I am the timer
    

    上面代码中, 变量p 是Proxy的实例, 当他作为函数调用时(p()), 就会被apply方法拦截, 返回一个字符串.

    再来举个🌰:

    const twice = {
        apply(target, ctx, args) {
            return Reflect.apply(...arguments) * 2;
        }
    };
    
    function sum(left, right) {
        return left + right;
    }
    
    const proxy = new Proxy(sum, twice);
    proxy(1, 2); // 6
    proxy.call(null, 5, 6); // 22
    proxy.apply(null, [7, 8]); // 30
    

    上面代码中, 每当执行proxy 函数(直接调用或callapply调用), 就会被apply方法拦截.

    另外, 直接调用Reflect.apply方法也会被拦截

    Reflect.apply(proxy, null, [9, 10]); // 38
    
  • 13.constructor(target, args)

    constructor 方法用于拦截new 命令, (拦截Proxy实例作为构造函数调用的操作, 比如new proxy(...args).)

    写法

    const handler = {
        constructor(target, args, newTarget) {
            return new target(...args);
        }
    }
    

    constructor 方法接受两个参数.

    • target : 目标对象
    • args : 构造函数的参数对象

    来个🌰:

    const proxy = new Proxy(function() {}, {
        constructor(target, args) {
            console.log(`called ${args.join(', ')}`);
            return { value: args[0] * 10 };
        }
    });
    
    new proxy(1).value; // called: 1 10
    

    constructor 方法返回的必须是一个对象, 否则会报错

    const proxy = new Proxy(function() {}, {
        constructor(target, argumentsList) {
            return 1;
        }
    });
    
    new proxy(); // 报错
    
  • 14.Proxy.revocable()

    Proxy.revocable方法返回一个可取消的Proxy实例

    const target = {};
    const handler = {};
    
    const { proxy, revoke } = Proxy.revocable(target, handler);
    
    proxy.foo = 123;
    proxy.foo; // 123
    
    revoke();
    proxy.foo; // TypeError: Revoked
    

    Proxy.revocable方法返回一个对象, 该对象的proxy属性是Proxy实例, revoke属性是一个函数, 可以取消Proxy实例. 上面代码中, 当执行revoke函数之后, 再访问Proxy实例, 就会抛出一个错误(如果访问原target对象的话可以).

  • this问题

    • 用一句话来概述, 原target与Proxy代理之后的proxy不相等.
    • 有一些原生对象的内部属性, 只有通过正确的this 才能拿到, 所以Proxy也无法代理这些原生对象的属性, 看下面的🌰
    const target = new Date();
    const handler = {};
    const proxy = new Proxy(target, handler);
    proxy.getDate(); // TypeError: this is not a Date object.
    

    上面代码中, getDate方法只能在Date对象实例上面拿到, 如果this不是Date对象实例就会报错.这时, this绑定原始对象, 就可以解决这个问题.

    const date = new Date('2020-01-01');
    const handler = {
        get(target, prop) {
            if (prop === 'getDate') {
                return target.getDate.bind(date);
            }
            return Reflect.get(target, prop);
        }
    };
    
    const proxy = new Proxy(date, handler);
    
    proxy.getDate(); // 1
    
    

Reflect

Reflect对象的设计目的

  1. Object对象的一些明显属于语言内部的方法(比如Object.defineProperty), 放到Reflect对象上. 现阶段, 某些方法同时在ObjectReflect对象上部署, 未来的新方法将只部署在Reflect对象上.
  2. 修改某些Object方法的返回结果, 让其变的更合理, 比如Object.defineProperty(obj, name, desc)在无法定义属性时, 会抛出一个错误, 而Reflect.defineProperty(obj, name, desc)则会返回false.
  3. Object操作都变成函数行为. 某些Object操作是命令式, 比如name in objdelete obj[name], 而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让他们变成了函数行为.
  4. Reflect对象的方法与Proxy对象的方法一一对应, 只要是Proxy对象的方法, 就能在Reflect对象上找到对应的方法.这就让Proxy对象可以方便的调用对应的Reflect方法, 完成默认行为, 作为修改行为的基础. 也就是说, 不管Proxy怎么修改默认行为, 你总可以在Reflect上获取默认行为.

Reflect对象的方法

Reflect对象的方法清单如下, 共13个.

  • Reflect.apply(fn, thisArg, args)

    等同于Function.prototype.apply.call(fn, thisArg, args).一般来说, 如果要绑定一个函数的this对象, 可以这样写fn.apply(obj, args), 但是如果函数定义了自己的apply方法, 就只能写成Function.prototype.apply.call(fn, obj, args),采用Reflect对象可以简化这种操作.另外, 需要注意的是, Reflect.set()Reflect.defineProperty()Reflect.freeze()Reflect.seal()Reflect.preventExtensions()返回一个布尔值, 表示操作是否成功.他们对应的Object方法, 失败时都会抛出错误.

    // 失败时抛出的错误
    Object.defineProperty(obj, name, desc);
    // 失败时返回false
    Reflect.defineProperty(obj, name, desc);
    

    上面代码中, Reflect.defineProperty方法的作用与Object.defineProperty是一样的, 都是为对象定义一个属性.但是, Reflect.defineProperty方法失败时, 不会抛出错误, 只会返回false.

  • Reflect.construct(target, args)

    等同于new target(...args), 这提供了一种不使用new, 来调用构造函数的方法

  • Reflect.get(target, name, receiver)

    查找并返回target对象的name属性, 如果没有该属性, 则返回undefined.

    如果name属性部署了读取函数, 则读取函数的this绑定receiver.

    const obj = {
        get foo() { 
                return this.bar();
        },
        bar: function() { console.log('bar') }
    }
    
    // 下面语句会让this.bar()
    // 变成调用wrapper.bar()
    Reflect.get(obj, 'foo', wrapper);
    
  • Reflect.set(target, name, value, receiver)

    设置target对象的name属性等于value.如果name属性设置了赋值函数, 则赋值函数的this绑定receiver.

  • Reflect.defineProperty(target, name, desc)

  • Reflect.deleteProperty(target, name)

    等同于delete obj[name]

  • Reflect.has(target, name)

    等同于name in obj

  • Reflect.ownKeys(target)

  • Reflect.isExtensible(target)

  • Reflect.preventExtensions(target)

  • Reflect.getOwnPropertyDescriptor(target, name)

  • Reflect.getPrototypeOf(target)

    读取对象的__proto__属性, 对应Object.setPrototypeOf(obj, newProto).

  • Reflect.setPrototypeOf(target, prototype)

  • 实例: 使用Proxy实现观察者模式

    观察者模式(Observer mode)指的是函数自动观察数据对象, 一旦对象有变化, 函数就会自动执行.

    const person = observable({
        name: 'veloma',
        age: 22
    });
    
    function print() {
        console.log(`${person.name}, ${person.age}`);
    }
    
    observe(print);
    person.name = 'timer'; // 李四, 20
    

    上面代码中, 数据对象person是观察目标, 函数print是观察者.一旦数据对象发生变化, print就会自动执行.

    下面, 使用Proxy写一个观察者模式的最简单实现, 即实现observableobserve这两个函数.思路是observable函数返回一个原始对象的Proxy代理, 拦截赋值操作, 触发充当观察者的各个函数.

    const queuedObservers = new Set();
    
    const observe = fn => queuedObservers.add(fn);
    const observable = obj => new Proxy(obj, {set});
    
    function set(target, key, value, receiver) {
        const result = Reflect.set(target, key, value, receiver);
        queuedObservers.forEach(observer => observer());
        return result;
    }
    

    上面代码中, 先定义了一个Set集合, 所有观察者函数都放进这个集合. 然后, observable函数返回原始对象的代理, 拦截赋值操作. 拦截函数Set中, 会自动执行所有观察者.