diff --git a/docs/proxy.md b/docs/proxy.md index 06562d0..83bbcad 100644 --- a/docs/proxy.md +++ b/docs/proxy.md @@ -1,10 +1,10 @@ -# Proxy和Reflect +# Proxy 和 Reflect -## Proxy概述 +## Proxy 概述 -Proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。 +Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。 -Proxy可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。 +Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。 ```javascript var obj = new Proxy({}, { @@ -30,15 +30,15 @@ obj.count = 1 // 2 ``` -上面代码说明,Proxy实际上重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义。 +上面代码说明,Proxy 实际上重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义。 -ES6原生提供Proxy构造函数,用来生成Proxy实例。 +ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。 ```javascript var proxy = new Proxy(target, handler); ``` -Proxy对象的所有用法,都是上面这种形式,不同的只是`handler`参数的写法。其中,`new Proxy()`表示生成一个Proxy实例,target参数表示所要拦截的目标对象,`handler`参数也是一个对象,用来定制拦截行为。 +Proxy 对象的所有用法,都是上面这种形式,不同的只是`handler`参数的写法。其中,`new Proxy()`表示生成一个`Proxy`实例,`target`参数表示所要拦截的目标对象,`handler`参数也是一个对象,用来定制拦截行为。 下面是另一个拦截读取属性行为的例子。 @@ -54,9 +54,9 @@ proxy.name // 35 proxy.title // 35 ``` -上面代码中,作为构造函数,Proxy接受两个参数。第一个参数是所要代理的目标对象(上例是一个空对象),即如果没有Proxy的介入,操作原来要访问的就是这个对象;第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。比如,上面代码中,配置对象有一个`get`方法,用来拦截对目标对象属性的访问请求。`get`方法的两个参数分别是目标对象和所要访问的属性。可以看到,由于拦截函数总是返回`35`,所以访问任何属性都得到`35`。 +上面代码中,作为构造函数,`Proxy`接受两个参数。第一个参数是所要代理的目标对象(上例是一个空对象),即如果没有`Proxy`的介入,操作原来要访问的就是这个对象;第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。比如,上面代码中,配置对象有一个`get`方法,用来拦截对目标对象属性的访问请求。`get`方法的两个参数分别是目标对象和所要访问的属性。可以看到,由于拦截函数总是返回`35`,所以访问任何属性都得到`35`。 -注意,要使得Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。 +注意,要使得`Proxy`起作用,必须针对`Proxy`实例(上例是`proxy`对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。 如果`handler`没有设置任何拦截,那就等同于直接通向原对象。 @@ -76,7 +76,7 @@ target.a // "b" var object = { proxy: new Proxy(target, handler) }; ``` -Proxy实例也可以作为其他对象的原型对象。 +Proxy 实例也可以作为其他对象的原型对象。 ```javascript var proxy = new Proxy({}, { @@ -121,7 +121,7 @@ fproxy.prototype === Object.prototype // true fproxy.foo // "Hello, foo" ``` -下面是Proxy支持的拦截操作一览。 +下面是 Proxy 支持的拦截操作一览。 对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。 @@ -175,11 +175,11 @@ fproxy.foo // "Hello, foo" **(12)apply(target, object, args)** -拦截Proxy实例作为函数调用的操作,比如`proxy(...args)`、`proxy.call(object, ...args)`、`proxy.apply(...)`。 +拦截 Proxy 实例作为函数调用的操作,比如`proxy(...args)`、`proxy.call(object, ...args)`、`proxy.apply(...)`。 **(13)construct(target, args)** -拦截Proxy实例作为构造函数调用的操作,比如`new proxy(...args)`。 +拦截 Proxy 实例作为构造函数调用的操作,比如`new proxy(...args)`。 ## Proxy实例的方法 @@ -224,7 +224,7 @@ let obj = Object.create(proto); obj.xxx // "GET xxx" ``` -上面代码中,拦截操作定义在Prototype对象上面,所以如果读取`obj`对象继承的属性时,拦截会生效。 +上面代码中,拦截操作定义在`Prototype`对象上面,所以如果读取`obj`对象继承的属性时,拦截会生效。 下面的例子使用`get`拦截,实现数组读取负数的索引。 @@ -251,7 +251,7 @@ arr[-1] // c 上面代码中,数组的位置参数是`-1`,就会输出数组的倒数最后一个成员。 -利用Proxy,可以将读取属性的操作(`get`),转变为执行某个函数,从而实现属性的链式操作。 +利用 Proxy,可以将读取属性的操作(`get`),转变为执行某个函数,从而实现属性的链式操作。 ```javascript var pipe = (function () { @@ -280,7 +280,7 @@ var reverseInt = n => n.toString().split("").reverse().join("") | 0; pipe(3).double.pow.reverseInt.get; // 63 ``` -上面代码设置Proxy以后,达到了将函数名链式使用的效果。 +上面代码设置 Proxy 以后,达到了将函数名链式使用的效果。 下面的例子则是利用`get`拦截,实现一个生成各种DOM节点的通用函数`dom`。 @@ -808,6 +808,77 @@ proxy.foo // TypeError: Revoked `Proxy.revocable`方法返回一个对象,该对象的`proxy`属性是`Proxy`实例,`revoke`属性是一个函数,可以取消`Proxy`实例。上面代码中,当执行`revoke`函数之后,再访问`Proxy`实例,就会抛出一个错误。 +## this 问题 + +虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的`this`关键字会指向 Proxy 代理。 + +```javascript +const target = { + m: function () { + console.log(this === proxy); + } +}; +const handler = {}; + +const proxy = new Proxy(target, handler); + +target.m() // false +proxy.m() // true +``` + +上面代码中,一旦`proxy`代理`target.m`,后者内部的`this`就是指向`proxy`,而不是`target`。 + +下面是一个例子,由于`this`指向的变化,导致 Proxy 无法代理目标对象。 + +```javascript +const _name = new WeakMap(); + +class Person { + constructor(name) { + _name.set(this, name); + } + get name() { + return _name.get(this); + } +} + +const jane = new Person('Jane'); +jane.name // 'Jane' + +const proxy = new Proxy(jane, {}); +proxy.name // undefined +``` + +上面代码中,目标对象`jane`的`name`属性,实际保存在外部`WeakMap`对象`_name`上面,通过`this`键区分。由于通过`proxy.name`访问时,`this`指向`proxy`,导致无法取到值,所以返回`undefined`。 + +此外,有些原生对象的内部属性,只有通过正确的`this`才能拿到,所以 Proxy 也无法代理这些原生对象的属性。 + +```javascript +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`绑定原始对象,就可以解决这个问题。 + +```javascript +const target = new Date('2015-01-01'); +const handler = { + 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 +``` + ## Reflect概述 `Reflect`对象与`Proxy`对象一样,也是ES6为了操作对象而提供的新API。`Reflect`对象的设计目的有这样几个。 @@ -914,7 +985,7 @@ Reflect.apply(Math.floor, undefined, [1.75]) // 1 查找并返回`target`对象的`name`属性,如果没有该属性,则返回`undefined`。 -如果`name`属性部署了读取函数,则读取函数的this绑定`receiver`。 +如果`name`属性部署了读取函数,则读取函数的`this`绑定`receiver`。 ```javascript var obj = { diff --git a/docs/reference.md b/docs/reference.md index 38b843d..2b5c5f4 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -106,6 +106,7 @@ - Nicolas Bevacqua, [ES6 Proxies in Depth](http://ponyfoo.com/articles/es6-proxies-in-depth) - Nicolas Bevacqua, [ES6 Proxy Traps in Depth](http://ponyfoo.com/articles/es6-proxy-traps-in-depth) - Nicolas Bevacqua, [More ES6 Proxy Traps in Depth](http://ponyfoo.com/articles/more-es6-proxy-traps-in-depth) +- Axel Rauschmayer, [Pitfall: not all objects can be wrapped transparently by proxies](http://www.2ality.com/2016/11/proxying-builtins.html) ## Symbol