愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

《ECMAScript6入门》学习笔记之Proxy

Proxy用于修改某些操作的默认行为,相当于在语言层面做出修改。可以认为是在目标对象之前架设了一层拦截,外界对目标对象的访问,都需要首先经过拦截层

一、示例

我们可以使用Proxy来拦截一个对象的属性的读写操作:

const realObj = {};
const obj = new Proxy(realObj, {
    get(target, key, receiver) {
        console.log(`getting ${key}`);
        return Reflect.get(target, key, receiver);
    },
    set(target, key, receiver) {
        console.log(`setting ${key}`);
        return Reflect.set(target, key, receiver);
    }
});

obj.name = 'RuphiLau'; // 输出:setting name
obj.name; // 输出:getting name


二、基本用法

ES6原生提供Proxy构造函数,用来生成Proxy实例,其语法形式为:

let proxy = new Proxy(target, handler)

其中,target是目标对象,而handler则是一个对象,用来定义拦截的行为。要使得拦截起作用,访问的必须是Proxy的实例,而非直接访问目标对象。
Proxy实例是可以作为其他对象的原型对象的,如:

const proxy = new Proxy({}, {
    get(target, key) {
        return 'Hello';
    }
});
const obj = Object.create(proxy);
obj.name; // 'Hello';

一个拦截器对象(handler)中可以包含多个拦截操作,目前可以拦截的操作有:
1)get(target, key, receiver) 拦截属性的读取操作,如:p.foop['foo']
2)set(target, key, value, receiver) 拦截属性的赋值操作,如:p.foo = 123
3)has(target, key) 拦截key in obj操作,返回布尔值
4)deleteProperty(target, key) 拦截delete obj[key]操作,返回布尔值
5)ownKeys(target) 拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)。返回目标对象所有自身的属性的属性名。而在相应操作的时候,则会自动根据枚举性、类型来返回正确的结果
6)getOwnPropertyDescriptor(target) 拦截Object.getOwnPropertyDescriptor(proxy, key)操作,返回属性的描述对象
7)defineProperty(target, key, desc) 拦截Object.defineProperty(proxy, key, desc)Object.defineProperties(proxy, desc) 返回一个布尔值
8)preventExtensions(target) 拦截Object.preventExtensions(proxy),返回一个布尔值
9)getPrototypeOf(target) 拦截Object.getPrototypeOf(proxy),返回一个对象
10)isExtensible(target) 拦截Object.isExtensible(proxy),返回一个布尔值
11)setPrototypeOf(target, proto) 拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值
如果拦截对象是函数,那么还有以下的拦截操作:
1)apply(target, ctx, args) 拦截proxy(..args)proxy.call(ctx, ...args)proxy.apply(ctx, args)
2)construct(target, args) 拦截Proxy实例作为构造函数调用时的操作,如:new proxy(...args)


三、用法详解

1、get()

我们可以借此实现一个生成各种DOM节点的函数dom

const dom = new Proxy({}, {
    get(target, key, receiver) {
        return function(attrs, ...children) {
            const el = document.createElement(key);
            for (let attr of Object.keys(attrs)) {
                el.setAttribute(attr, attrs[attr]);
            }
            for (let child of children) {
                if (typeof child === 'string') {
                    child = document.createTextNode(child);
                }
                el.appendChild(child);
            }
            return el;
        }
    }
});

const el = dom.div(
    { style: 'width:200px; height:200px; background:#000; color: #FFF;' },
    'Hello, wanna go:',
    dom.ul(
        {},
        dom.li({}, 'Zhihu'),
        dom.li({}, 'StackOverflow')
    )
);

document.body.appendChild(el);

需要注意的是:如果一个属性 不可写,那么该属性就不能被代理:

const target = Object.defineProperties({}, {
    foo: {
        configurable: false,
        writable: false,
        value: 123
    }
});
const proxy = new Proxy(target, {
    get(target, key, receiver) {
        return 'abc';
    }
})

proxy.foo; // 报错

2、set()

可以拦截属性的赋值操作,可以借此实现很多功能如:数据正确性保证、双向绑定等,还可以实现使_开头的属性、方法不可访问,如下是一个简单的示例:

function detectPrivate(key, action) {
    if (key[0] === '_') {
        throw new Error(`Error: Invalid attempt to ${action} private "${key}" property`);
    }
}
const target = {
    _age: 21,
    _name: 'RuphiLau'
}
const proxy = new Proxy(target, {
    get(target, key, receiver) {
        detectPrivate(key, 'get');
        return target[key];
    },
    set(target, key, value, receiver) {
        detectPrivate(key, 'set');
        target[key] = value;
        return true;
    }
});

proxy._age; // Uncaught Error: Error: Invalid attempt to get private "_age" property

注意:如果对象自身的某个属性,不可写也不可配置,那么set不能改变这个属性的值,只能返回同样的值,否则报错

3、apply()

apply(target, ctx, args)拦截函数的调用、call和apply操作。其中,ctx表示目标对象的上下文(this),args表示目标对象的参数数组。

const target = function() {
    return 'I am the target';
}
const proxy = new Proxy(target, {
    apply(target, ctx, args) {
        return 'I am the proxy'
    }
});

proxy(); // 'I am the proxy'

此外,如果直接调用Reflect.apply方法,也会被拦截:

Reflect.apply(proxy, null, [1, 2, 3]); // 'I am the proxy'

4、has()

has方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符:

const handler = {
    has(target, key) {
        if (key[0] === '_') {
            return false;
        }
        return key in target;
    }
}
const target = {
    _age: 21,
    name: 'RuphiLau'
};
const proxy = new Proxy(target, handler);
'_age' in proxy; // false
'name' in proxy; // true

注意:
1)如果原对象禁止扩展(Object.preventExtensions(obj)),这时has拦截会报错:

const obj = { a: 123 }
Object.preventExtensions(obj);

const p = new Proxy(obj, {
    has(target, key) {
        return false;
    }
});

'a' in p; // 报错

2)has方法拦截的是HasProperty操作,而非HasOwnProperty操作,所以has方法不判断一个属性是自身属性,还是继承的属性。

const parent = { b: 456 };
const obj = Object.create(parent);
obj.a = 123;
const p = new Proxy(obj, {
    has(target, key) {
        return false;
    }
});
'a' in p; // false
'b' in p; // false

3)虽然for-in中也有in,但是has对此不生效

const handler = {
    has(target, key) {
        if (key[0] === '_') {
            return false;
        }
        return key in target;
    }
}
const target = {
    _age: 21,
    name: 'RuphiLau'
};
const proxy = new Proxy(target, handler);
'_age' in proxy; // false
for (let k in proxy) {
    console.log(`${k}=${proxy[k]}`);
}
/*
输出:
_age=21
name=RuphiLau
*/

5、construct()

construct方法用于拦截new命令,下面是拦截对象的写法:

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

其中,参数target表示目标对象,参数args表示构建函数的参数对象:

const handler = {
    construct(target, args, newTarget) {
        return {
            sum: args.reduce((total, curr) => total + curr)
        }
    }
};
const Fn = function(){}
const Pfn = new Proxy(Fn, handler);
const p = new Pfn(1, 2, 3, 4);
p.sum; // 10

注意:construct的返回值必须是一个对象,否则会报错

6、deleteProperty()

deleteProperty()用于拦截delete操作,如果方法抛出错误或者返回false,则当前属性就没有办法使用delete删除:

const handler = {
    deleteProperty(target, key) {
        return false;
    }
}
const obj = { a: 123 };
const p = new Proxy(obj, handler);
delete p.a; // false
p.a; // 123

如果属性的configurable是false,则不能被deleteProperty方法删除,否则报错

7、defineProperty()

defineProperty方法拦截了Object.defineProperty()操作,当返回false的时候,添加新属性会报错。
注意:如果目标对象不可扩展(extensible),则defineProperty不能增加目标对象上不存在的属性,否则会报错。此外,如果目标对象的某个属性的writable为false或者configurable为false,则defineProperty方法不能改变这两个的设置

const obj = {}
const handler = {
    defineProperty(target, key, descriptor) {
        return false;
    }
}
obj.proxy = new Proxy(obj, handler);
Object.defineProperty(obj.proxy, 'name', {
    value: 'Ruphi'
}); // 报错

8、getOwnPropertyDescriptor()

拦截Object.getOwnPropertyDescriptor(),并返回一个属性描述对象或者undefined。
示例:_开头的表示私有属性,不能获取descriptor

const handler = {
    getOwnPropertyDescriptor(target, key) {
        if (key[0] === '_') {
            return;
        }
        return Object.getOwnPropertyDescriptor(target, key);
    }
}

const obj = { _age: 21, name: 'RuphiLau' }
const proxy = new Proxy(obj, handler);
Object.getOwnPropertyDescriptor(proxy, '_age'); // undefined;
Object.getOwnPropertyDescriptor(proxy, 'name'); // 描述对象

9、getPrototypeOf()

这个方法主要用来拦截获取对象的原型,即会拦截如下的操作:

  • Object.prototype.__proto__
  • Object.prototype.isPrototypeOf()
  • Object.getPrototypeOf()
  • Reflect.getPrototypeOf()
  • instanceof

例子如:

const proto = { a: 123 }
const proxy = new Proxy({}, {
    getPrototypeOf(target) {
        return proto;
    }
});
Object.getPrototypeOf(proxy) === proto; // true
proxy.__proto__ === proto; // true
proto.isPrototypeOf(proxy); // true

注意:返回值必须是对象或者null,否则会报错。如果目标对象不可扩展,则getPrototypeOf方法必须返回目标对象的原型对象

10、isExtensible()

拦截Object.isExtensible()操作(返回值只能返回Boolean值,否则会被自动转为Boolean)
使用限制:返回值必须和目标对象的isExtensible属性保持一致,否则会报错:

const p = new Proxy({}, {
    isExtensible(target) {
        return false;
    }
});
Object.isExtensible(p); // 报错

所以这个方法通常用来输出记录一些信息

11、ownKeys()

拦截对象自身属性的读取操作,即:

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()

拦截Object.keys()

const target = { a:1, c:3 };
Object.defineProperty(target, 'b', {
    enumerable: false,
    value: 2
});
const p = new Proxy(target, {
    ownKeys(target) {
        return ['a', 'b', 'd', Symbol()];
    }
});
Object.keys(p); // ['a']

对于ownKeys()返回数组中的元素,Object.keys()会过滤以下情况的元素:

  • 目标对象中不存在的属性(如d
  • 属性名为Symbol类型的值
  • 不可遍历的属性(enumerable为false)

需要注意的是:ownKeys()返回的数组,其元素类型只能为string或者symbol类型(因为对象的key只能为这两种类型,如果是其他类型,就会报错)
对于Object.getOwnPropertyNames(),它会返回ownKeys()返回值里除了symbol类型外的值:

Object.getOwnPropertyNames(p); // ['a', 'b', 'd']

如果目标对象中有不可配置的属性,那么ownKeys()中就必须返回,否则会报错:

const target = { a:1, c:3 };
Object.defineProperty(target, 'b', {
    configurable: false,
    value: 2
});
const p = new Proxy(target, {
    ownKeys(target) {
        return ['a', 'd'];
    }
});
Object.keys(p); // 报错:'ownKeys' on proxy: trap result did not include 'b'
Object.getOwnPropertyNames(p); // 一样报错

此外,如果目标对象是 不可扩展 的,那么ownKeys方法返回的数组之中,必须 包含原对象的所有属性,且 不能包含多余属性,否则报错

12、preventExtensions()

拦截Object.preventExtensions(),方法必须返回一个Boolean值,否则会被自动转为Boolean值。
使用注意:只有target对象不可扩展时,才能返回true,否则报错

13、setPrototypeOf()

拦截Object.setPrototypeOf()方法,返回Boolean值。此外,如果target对象不可扩展,则setPrototypeOf方法不得改变target对象的原型


四、Proxy.revocable()

该方法返回一个可取消的Proxy实例,其运用场景在于:不允许直接访问目标对象,必须通过代理访问,但是一旦访问结束,就收回代理权:

const target = {};
const handler = {};

const { proxy, revoke } = Proxy.revocable(target, handler);

proxy.foo = 123;
proxy.foo; // 123
revoke();
proxy.foo; // Cannot perform 'get' on a proxy that has been revoked

其中:返回的对象里的proxy属性表示Proxy实例,而revoke()方法则用来取消Proxy实例


五、this问题

Proxy所做的代理,不是透明代理。主要原因在于:在Proxy代理的情况下,target对象内部的this会指向Proxy代理,如:

const target = {
    foo() {
        console.log(this === proxy);
    }
};
const handler = {};
const proxy = new Proxy(target, handler);

target.foo(); // false
proxy.foo(); // true

另外就比如此前我们实现_开头的属性表示私有属性问题:

const person = {
    _age: 21,
    _name: 'Tom',
    desc() {
        console.log(`${this._name} is ${this._age}`);
    }
}
const limitPrivate = function(key) {
    if (key[0] === '_') {
        throw new Error(`Invalid access: ${key} is private`);
    }
};
const proxy = new Proxy(person, {
    get(target, key, reciver) {
        limitPrivate(key);
        return target[key];
    },
    set(target, key, value, receiver) {
        limitPrivate(key);
        target[key] = value;
    }
});

proxy._age; // 报错:Invalid access: _age is private
proxy._name; // 报错:Invalid access: _age is private

但是,存在以下的问题:

proxy.desc(); // 报错:Invalid access: _name is private

这是因为,this此时绑定的是proxy实例,所以在desc()内部,对_age_name的访问相当于proxy._ageproxy._name,所以修改如下:

const proxy = new Proxy(person, {
    get(target, key, reciver) {
        limitPrivate(key);
        return typeof target[key] === 'function'
            ? target[key].bind(target)
            : target[key];
    },
    set(target, key, value, receiver) {
        limitPrivate(key);
        target[key] = value;
    }
});

此时有:

proxy._age; // Invalid access: _age is private
proxy._name; // Invalid access: _name is private
proxy.desc(); // 输出:Tom is 21