Proxy & Reflect

extends的出现,使用内建对象的继承得以实现。Proxy可以拦截JS引擎内部目标的底层对象操作,这些底层操作被拦截后会触发响应特定操作的陷阱函数(traps),对于别人封装好的对象或内建对象,都可以自定义操作。
而反射(Reflect)API是以Reflect对象的形式出现的。API中的默认特征与相同的底层操作是一致的,而通过代理可以重写这些操作,每个代理Trap,对应一个命名和参数值都相同的Reflect方法。
Reflect 中所有方法及说明--MDN

const target = {};
const proxy = new Proxy(target, {});
proxy.title = 'proxy';
console.log(proxy.title);
console.log(target.title);

target.title = 'target';
console.log(target.title);
console.log(proxy.title);

非常简单的Proxy,没有实现任何自定义的Trap 方法,都是默认实现。

GET Trap

let handler = {
    get: function(target, name){
        return name in target ? target[name] : 37;
    }
};

let p = new Proxy({}, handler);

p.a = 1;
p.b = undefined;

console.log(p.a, p.b);    // 1, undefined

console.log('c' in p, p.c);    // false, 37

来自于MDN的一个小例子,语法极其简单。get trap function 有3个参数:

  • TrapTarget 被读取属性的原对象,即代理的目标。
  • key 要读取的属性(字符串或Symbol)
  • receiver 发生操作的对象,通常为代理。

而get trap 能做什么用呢?或者说,我们在什么时候使用get呢?一个最主要的功能,验证对象结构

对象结构:对象中所有可用的属性和方法集合。JS引擎通过对象结构来优化代码,通常会创建类来表示对象。
let proxy = new Proxy({}, {
    get(target, key, receiver) {
        if (!(key in receiver )) {
            throw new TypeError(`属性 ${key} 不存在`);
        }

        return Reflect.get(target, key, receiver);
    }
});

proxy.name = 'proxy';
console.log(proxy.name);  // proxy

console.log(proxy.age);  // 抛出 TypeError 异常

SET Trap

get是取值操作,而set就是赋值操作,可以对属性值进行验证。

let target = {
    name: 'target'
}

let proxy = new Proxy(target, {
    set(target, key, value, receiver) {
        if (!(key in receiver )) {
            if (isNaN(value)) {
                throw new TypeError('属性必须为数字');
            }
        }

        return Reflect.set(target, key, value, receiver);
    }
});

proxy.count = 10;
console.log(proxy.count);  // 10
console.log(proxy.count);  // 10

proxy.name = 'tom';
console.log(proxy.name);  // tom
console.log(target.name);  // tom

proxy.secondName = "Lee"; // 抛出 TypeError 异常

Has Trap

has trap function 接受两个参数

  • TrapTarget 被读取属性的原对象,即代理的目标。
  • key 要读取的属性(字符串或Symbol)

可以用来隐藏已有属性。

// has trap
let target = {
    name: 'target',
    age: 24
}

let proxy = new Proxy(target, {
    has(target, key) {
        if (key === 'age'){
            return false;
        } else {
            return Reflect.has(target, key);
        }
    }
});

console.log('age' in proxy);  // false
console.log('age' in target);  // true
console.log('name' in proxy);  // true
console.log('name' in target);  // true

deleteProperty Trap

delete操作可以从对象中移除属性,如果成功则返回true, 失败返回 false 。在严格模式下,删除一个不可配置属性,则会抛出错误,而在非严格模式下,只是返回false.

let target = {
    name: 'target',
    age: 24
}

let proxy = new Proxy(target, {
    deleteProperty(target, key) {
        if (key === 'age'){
            return false;
        } else {
            return Reflect.deleteProperty(target, key);
        }
    }
});

console.log( 'age' in proxy);

const r1 = delete proxy.age;

console.log(r1);

console.log( 'name' in proxy);

const r2 = delete proxy.name;

console.log(r2);

原型 Trap

这个要了解一点:Object.get/setPrototypeOf 与 Reflect.get/setPrototypeOf 的区别。
从功能上来讲,两个操作是一致的。但从实现上来讲,Object.get/setPrototypeOf 是高级方法,创建伊始就给开发者使用,而Reflect.get/setPrototypeOf 是更底层的操作,使开发者可以访问之前只在内部操作的[[Get/SetPrototypeOf]]。
面对两套方法选择时,可能会让人无所适从,不知道选哪个更好。有一点可以确认的是,Object提供的方法更高级,是对Reflect方法的包裹器,最终还是会调用Reflect方法,但在此之前,会执行一些额外的步骤,并通过检查返回值,来确定下一步怎么操作。

可扩展 Trap

对象可扩展,Object.isExtensible和Object.preventExtesions 方法,在ES5下已经实现,而ES6中通过 Reflect.isExtensible和Reflect.preventExtensions来实现。

var p = new Proxy({}, {
  isExtensible: function(target) {
    console.log('called');
    return true;//也可以return 1;等表示为true的值
  }
});

console.log(Object.isExtensible(p)); // "called"
                                     // true
var p = new Proxy({}, {
  preventExtensions: function(target) {
    console.log('called');
    Object.preventExtensions(target);
    return true;
  }
});

console.log(Object.preventExtensions(p)); // "called"
                                          // false                             

属性描述符

在代理中可以分别使用defineProperty陷阱和getOwnPropertyDescriptor陷阱拦截Object.defineProperty方法和Object.getOwnPropertyDescriptor方法的调用。

defineProperty
Object.defineProperty与Reflect.defineProperty方法返回值不同,前一个返回第一个参数,后一方法,则返回操作的结果是否成功。
let target = {};
let r1 = Object.defineProperty(target, 'name', { value: 'target' });
console.log(r1 === target);  // true

let r2 = Reflect.defineProperty(target, 'name', { value: 'target'});

console.log(r2);  // true
getOwnPropertyDescriptor
Object.getOwnPropertyDescriptor 如果原始值被传入第一个参数,内部将会对这个值进行强制转换,转成Object对象,而Reflect.getOwnPropertyDescriptor方法,则会报出一个错误。

ownKeys

这个trap可以拦截内部[[OwnPropertyKeys]]方法,通过返回数组的值可以覆写其行为。返回的数组被用于Object.keys(),Object.getOwnPropertyNames(),Object.getOwnPropertySymbols()和Object.assign()。

let proxy = new Proxy({}, {
    ownKeys(target) {
        return Reflect.ownKeys(target).filter(key => {
            // console.log(key);
            // return true; // 可以和下面的返回做个对比,看看输出有什么不同
            return typeof key !== 'string' || key[0] !== '_';
        })
    }
});

let nameSymbol = Symbol('name');

proxy.name = 'Tom';
proxy._name = 'Jerry';
proxy[nameSymbol] = 'Tom & Jerry';

const names = Object.getOwnPropertyNames(proxy),
      keys = Object.keys(proxy),
      symbols = Object.getOwnPropertySymbols(proxy);

console.log(names.length);
console.log(names);
console.log(keys.length);
console.log(keys);
console.log(symbols.length);
console.log(symbols);

apply & construct

MDN apply
MDN construct
这两个代理的目标,都是函数。

const target = function() {
    return 42;
}
const proxy = new Proxy(target, {
    apply: function(tt, ta, args) {
        return Reflect.apply(tt, ta, args);
    },
    constructor(tt, ta, args) {
      return Reflect.constructor(tt, ta, args);
    }
});

console.log(typeof proxy);
console.log(proxy());
const instance = new proxy();
console.log(instance instanceof proxy);
console.log(instance instanceof target);

所以,这两个trap,通常可以用来

  • 验证函数参数
  • 不用 new 调用构造函数
  • 覆写抽象基类构造函数
  • 可调用类构造函数

例子就不一一写了,知道用法,很方便就能实现。

可撤销代理

方法 Proxy.revocable() 创建一个可撤销的 Proxy 对象.
MDN revocable

var revocable = Proxy.revocable({}, {
  get: function(target, name) {
    return "[[" + name + "]]";
  }
});
var proxy = revocable.proxy;
console.log(proxy.foo); // "[[foo]]"

revocable.revoke();

console.log(proxy.foo); // TypeError is thrown
proxy.foo = 1           // TypeError again
delete proxy.foo;       // still TypeError
typeof proxy            // "object", typeof doesn't trigger any trap

在原型上进行Proxy应用

如果把代理当原型,将发生什么事呢?在JS中,一旦涉及到原型,事情往往就没那么简单了。如果原型是代理,而代理是透明的,仅当默认操作执行到原型上时,都会调用代理trap,这大大限制代理的能力。

const target = {};
 const newTarget = Object.create(new Proxy(target, {
     defineProperty(trapTarget, name, desc) {
         return false;
     }
 }));

 // 按原型执行顺序,这个方法可以被调用到吗?

 Object.defineProperty(newTarget, 'name', { value: 'newTarget' });
 console.log(newTarget.name);
 console.log(newTarget.hasOwnProperty('name'));
 // 可以看出,defineProperty方法,没有得到调用

在Object.create中,可以用以下几个trap

  • get
  • set
  • has

将代理用途类的原型

先看一个例子

function NoSuchProperty() {}

NoSuchProperty.prototype = new Proxy({}, {
    get(tt, key, receiver) {
        throw new ReferenceError(`${key} doesn't exist `);
    }
});

class Square extends NoSuchProperty {
    constructor(length, width) {
      super();
      this.length = length;
      this.width = width;
    }
}

let shape = new Square(2, 6);
const a1 = shape.length * shape.width;
console.log(a1); // 12

let a2 = shape.length * shape.wdth; // 抛出异常

说明:wdth是个拼写错误,但按JS原型机制,shape中没有时,会去原型中查找,所以就会抛出异常。虽然代理不是shape的直接原型,存在于shape对象的原型链中。


anleo
30 声望1 粉丝

准备写es6系列、设计模式(node,javascript)、前端工程化、TDD&BDD(Reactjs&angularjs)