Proxy

431 阅读5分钟

概述

Proxy 可以理解成在目标对象前架设一个“拦截”层,外界对该对象的访问都必须先通过这层拦截层,即提供了一种机制可以对外界的访问进行过滤和改写。

ES6 提供 Proxy 构造函数,用于生成 Proxy 实例。

var proxy = new Proxy(target, handler);

其中,target 参数表示所要拦截的目标对象,handler 参数也是一个对象,定义拦截行为,如果没有设置任何拦截,那就等同于直接通向原对象。

var proxy = new Proxy({}, {
    // get方法的两个参数分别是目标对象和所要访问的属性
    get: function(target, property) {
        return 35;
    }
});

Proxy 实例的方法

get()

get 方法用于拦截某个属性的读取操作。

get 方法可以继承,即拦截操作定义在原型对象上时,读取目标对象继承的属性时,拦截会生效。

let proto = new Proxy({}, {
    get(target, propertyKey, receiver) {
        console.log('GET' + propertyKey);
        return target[propertyKey];
    }
});

let obj = Object.create(proto);
obj.xxx // "GET xxx"

set()

set 方法用于拦截某个属性的赋值操作。

如果目标对象的某个属性不可写也不可配置,那么 set 不得改变这个属性的值,只能返回同样的值,否则会报错。

apply()

apply 方法拦截函数的调用、call 和 apply 操作。接收3个参数,目标对象、目标对象的上下文对象(this)、目标对象的参数数组。

直接调用 Reflect.apply 方法也会被拦截。

let twice = {
    apply(target, ctx, args) {
        return Reflect.apply(...arguments) * 2;
    }
};
function sum (left, right) {
    return left + right;
};
let proxy = new Proxy(sum, twice);

proxy(1, 2); // 6
proxy(null, 5, 6); // 22
proxy.apply(null, [7, 8]); // 30

Reflect.apply(proxy, null, [9, 10]); // 38

has()

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

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

 'prop' in proxy; // true
'_prop' in proxy; // false
for (let a in proxy) {
    console.log(proxy[a]); // true, true
}

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

let obj = { a: 10 };
Object.preventExtensions(obj);

let p = new Proxy(obj, {
    has: function(target, prop) {
        return false;
    }
});

'a' in p; // TypeError

construct()

construct 方法用于拦截 new 命令,接收两个参数:目标对象、构建函数的参数对象。 construct 方法返回的必须是一个对象,否则会报错。

let p = new Proxy(function () {}, {
   construct: function(target, args) {
       console.log('called:' + args.join(', '));
       return { value: args[0] * 10 };
   } 
});

(new p(1)).value; 
// 'called: 1'
// 10

deleteProperty()

deleteProperty 方法用于拦截 delete 操作,如果这个方法跑出错误或者返回 false,当前属性就无法被 delete 命令删除。

目标对象自身的不可配置属性不能被 deleteProperty 方法删除,否则会报错。

defineProperty()

defineProperty 方法拦截了 Object.defineProperty 操作,如果这个方法返回 false,当前属性就无法定义。

let handler = {
    defineProperty (target, key, descriptor) {
        return false;
    }
};
let target = {};
let proxy = new Proxy(target, handler);
proxy.foo = 'bar';

getOwnPropertyDescriptor()

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

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

var handler = {
    getOwnPropertyDescriptor (target, key) {
        if (key[0] === '_') {
            return;
        }
        return Object.getOwnPropertyDescriptor(target, key);
    }
};
var target = { _prop: 'bar', baz: 'tar' };
var proxy = new Proxy(target, handler);

Object.getOwnPropertyDescriptor(proxy, 'wat'); // undefined
Object.getOwnPropertyDescriptor(proxy, '_prop'); // undefined
Object.getOwnPropertyDescriptor(proxy, 'baz'); // { value: 'tar', writable: true, enumerable: true, configurable: true }

isExtensible()

isExtensible 方法拦截 Object.isExtensible 操作。该方法只能返回布尔值,否则返回值会被自动转为布尔值,并且返回值必须与目标对象的 isExtensible 属性保持一致,否则会报错。

var p = new Proxy({}, {
    isExtensible: function(target) {
        console.log('callef');
        return true;
    }
});

Object.isExtensible(p); 
// 'called'
// true

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

ownKeys()

ownKeys 方法用来拦截对象自身属性的读取操作。拦截以下操作:

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

返回值:

  • ownKeys 方法返回的数组成员只能是字符串或 Symbol 值
  • 如果有其他类型的值,或者返回的根本不是数组,就会报错
  • 如果目标对象包含不可配置的属性,则该属性必须被 ownKeys 方法返回,否则会报错
  • 如果目标对象是不可扩展的,这时 ownKeys 方法返回的数组之中必须包含原对象的所有属性,且不能包含多余的属性,否则会报错

preventExtensions()

preventExtensions 方法拦截 Object.preventExtensions()。该方法必须返回一个布尔值,否则会被自动转换为布尔值。

只有目标对象为不可扩展时,即 Object.isExtensible (proxy)为false,proxy.preventExtensions 才能返回 true,否则会报错。

setPrototypeOf()

setPrototypeOf 方法主要用于拦截 Object.setPrototypeOf 方法。

该方法只能返回布尔值,否则会被自动转换为布尔值。如果目标对象不可扩展,setPrototypeOf 方法不得改变目标对象的原型。

Proxy.revocable()

Proxy.revocable() 方法返回一个可取消的 Proxy 实例。该实例的 proxy 属性是 Proxy 实例,revoke 属性是一个函数,可以取消Proxy 实例。

let target = {};
let handler = {};

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

proxy.foo = 123;
proxy.foo // 123

revoke();
proxy.foo // TypeError: Revoked

this 问题

在 Proxy 代理的情况下,目标对象内部的 this 关键字会指向 Proxy 代理。有些原生对象的内部属性只有通过正确的 this 才能获取,所以 Proxy 也无法代理这些原生对象的属性,解决方法是 this 绑定原始对象。

const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);

proxy.getDate(); // TypeError: this is not a Date object

const target1 = new Date('2021-4-11');
const handler1 = {
    get(target, prop) {
        if (prop === "getDate") {
            return target.getDate.bind(target);
        }
        return Reflect.get(target, prop);
    }
}
const proxy1 = new Proxy(target1, handler1);

proxy.getDate(); // 11