前端博文

ES6之Proxy代理

2019-12-20  本文已影响0人  27亿光年中的小小尘埃

什么是Proxy代理

ES6 让开发者能进一步接近 JS 引擎的能力,这些能力原先只存在于内置对象上。语言通过代
理( proxy )暴露了在对象上的内部工作,代理是一种封装,能够拦截并改变 JS 引擎的底
层操作。

人话是:把代理看做是设计模式代理模式中的一种,有一个代理对象来代理本体,而ES6的Proxy牛逼的一点是可以把本体没法改变的内部属性改了

代理与反射是什么?

通过调用 new Proxy() ,你可以创建一个代理用来替代另一个对象(被称为目标),这个代理对目标对象进行了虚拟,因此该代理与该目标对象表面上可以被当作同一个对象来对待。

代理允许你拦截在目标对象上的底层操作,而这原本是JS引擎的内部能力。拦截行为使用了一个能够响应特定操作的函数(被称为陷阱)。

被 Reflect 对象所代表的反射接口,是给底层操作提供默认行为的方法的集合,这些操作是能够被代理重写的。每个代理陷阱都有一个对应的反射方法,每个方法都与对应的陷阱函数同名,并且接收的参数也与之一致。下表总结了这些行为:

代理陷阱 被重写的行为 默认行为
get 读取一个属性的值 Reflect.get()
set 写入一个属性 Reflect.set()
has in 运算符 Reflect.has()
deleteProperty delete 运算符 Reflect.deleteProperty()
getPrototypeOf Object.getPrototypeOf() Reflect.getPrototypeOf()
setPrototypeOf Object.setPrototypeOf() Reflect.setPrototypeOf()
isExtensible Object.isExtensiable() Reflect.setPrototypeOf()
preventExtensions Object.preventExtensions() Reflect.preventExtensions()
getOwnPropertyDescriptor Object.getOwnPropertyDescriptor() Reflect.getOwnPropertyDescriptor()
defineProperty Object.defineProperty() Reflect.defineProperty()
ownKeys Object.keys 、Object.getOwnPropertyNames() 与Object.getOwnPropertySymbols() Reflect.ownKeys()
apply 调用一个函数 Reflect.apply()
construct 使用new调用一个函数 Reflect.construct()

每个陷阱函数都可以重写 JS 对象的一个特定内置行为,允许你拦截并修改它。如果你仍然需
要使用原先的内置行为,则可使用对应的反射接口方法。一旦创建了代理,你就能清晰了解
代理与反射接口之间的关系,因此我们最好通过一些例子来进行深入研究。

创建一个简单的代理

当你使用 Proxy 构造器来创建一个代理时,需要传递两个参数:目标对象以及一个处理器(handler),后者是定义了一个或多个陷阱函数的对象。如果未提供陷阱函数,代理会对所有操作采取默认行为。为了创建一个仅进行传递的代理,你需要使用不包含任何陷阱函数的处理器:

let target={}
let proxy=new Proxy(target,{})

proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
target.name = "target";
console.log(proxy.name); // "target"
console.log(target.name); // "target"

使用 set 陷阱函数验证属性值

假设你想要创建一个对象,并要求其属性值只能是数值,这就意味着该对象的每个新增属性都要被验证,并且在属性值不为数值类型时应当抛出错误。为此你需要定义 set 陷阱函数来重写设置属性值时的默认行为,该陷阱函数能接受四个参数:

Reflect.set() 是 set 陷阱函数对应的反射方法,同时也是set操作的默认行为。Reflect.set()方法与set陷阱函数一样,能接受这四个参数,让该方法能在陷阱函数内部被方便使用。该陷阱函数需要在属性被设置完成的情况下返回 true ,否则就要返回 false,而 Reflect.set() 也会基于操作是否成功而返回相应的结果。

你需要使用 set 陷阱函数来拦截传入的 value 值,以便对属性值进行验证。这里有个例子:

let target = {
    name: "target"
};
let proxy = new Proxy(target, {
    set(trapTarget, key, value, receiver) {
    // 忽略已有属性,避免影响它们
    if (!trapTarget.hasOwnProperty(key)) {
        if (isNaN(value)) {
            throw new TypeError("Property must be a number.");
        }
    }
    // 添加属性
    return Reflect.set(trapTarget, key, value, receiver);
    }
});
// 添加一个新属性
proxy.count = 1;
console.log(proxy.count); // 1
console.log(target.count); // 1
// 你可以为 name 赋一个非数值类型的值,因为该属性已经存在
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
// 抛出错误
proxy.anotherName = "proxy";

使用 get 陷阱函数进行对象外形验证

该陷阱函数会在读取属性时被调用,即使该属性在对象中并不存在,它能接受三个参数:

你可以使用 get 陷阱函数与 Reflect.get() 方法在目标属性不存在时抛出错误,就像这样:

let proxy = new Proxy({}, {
    get(trapTarget, key, receiver) {
        if (!(key in receiver)) {
            throw new TypeError("Property " + key + " doesn't exist.");
        }
        return Reflect.get(trapTarget, key, receiver);
    }
});
// 添加属性的功能正常
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
// 读取不存在属性会抛出错误
console.log(proxy.nme); // 抛出错误

使用 has 陷阱函数隐藏属性

has 陷阱函数会在使用 in 运算符的情况下被调用,并且会被传入两个参数:

Reflect.has() 方法接受与之相同的参数,并向 in 运算符返回默认响应结果。使用 has陷阱函数以及 Reflect.has() 方法,允许你修改部分属性在接受 in 检测时的行为,但保留其他属性的默认行为。例如,假设你只想要隐藏 value 属性,你可以这么做:

let target = {
    name: "target",
    value: 42
};
let proxy = new Proxy(target, {
    has(trapTarget, key) {
        if (key === "value") {
            return false;
        } else {
            return Reflect.has(trapTarget, key);
        }
    }
});
console.log("value" in proxy); // false
console.log("name" in proxy); // true
console.log("toString" in proxy); // true

这里的 proxy 对象使用了 has 陷阱函数,用于检查 key 值是否为 "value" 。如果是,则返回 false ;否则通过调用 Reflect.has() 方法来返回默认的结果。这样,虽然 value 属性确实存在于目标对象中,但 in 运算符却会对该属性返回 false ;而其他的属性(name 与 toString )则会正确地返回 true 。

使用 deleteProperty 陷阱函数避免属性被删除

deleteProperty 陷阱函数会在使用 delete 运算符去删除对象属性时下被调用,并且会被传入两个参数:

Reflect.deleteProperty() 方法也接受这两个参数,并提供了 deleteProperty 陷阱函数的默认实现。你可以结合 Reflect.deleteProperty() 方法以及 deleteProperty 陷阱函数,来修改 delete 运算符的行为。例如,能确保 value 属性不被删除:

let target = {
    name: "target",
    value: 42
};
let proxy = new Proxy(target, {
    deleteProperty(trapTarget, key) {
        if (key === "value") {
            return false;
        } else {
            return Reflect.deleteProperty(trapTarget, key);
        }
    }
});
// 尝试删除 proxy.value
console.log("value" in proxy); // true
let result1 = delete proxy.value;
console.log(result1); // false
console.log("value" in proxy); // true
// 尝试删除 proxy.name
console.log("name" in proxy); // true
let result2 = delete proxy.name;
console.log(result2); // true
console.log("name" in proxy); // false

原型代理的陷阱函数

ES6 引入该方法用于对 ES5 的Object.getPrototypeOf() 方法进行补充。代理允许你通过 setPrototypeOf 与
getPrototypeOf 陷阱函数来对这两个方法的操作进行拦截。Object对象上的这两个方法都会调用代理中对应名称的陷阱函数,从而允许你改变这两个方法的行为。

setPrototypeOf 陷阱函数接受三个参数:

getPrototypeOf 陷阱函数的返回值必须是一个对象或者null,其他任何类型的返回值都会引发“运行时”错误。对于返回值的检测确保了Object.getPrototypeOf() 会返回预期的结果。类似的, setPrototypeOf 必须在操作没有成功的情况下返回 false ,这样会让 Object.setPrototypeOf()抛出错误;而若setPrototypeOf的返回值不是false,则Object.setPrototypeOf() 就会认为操作已成功。

let target = {};
let proxy = new Proxy(target, {
    getPrototypeOf(trapTarget) {
        return null;
    },
    setPrototypeOf(trapTarget, proto) {
        return false;
    }
});
let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // false
console.log(proxyProto); // null
// 成功
Object.setPrototypeOf(target, {});
// 抛出错误
Object.setPrototypeOf(proxy, {});

如果你想在这两个陷阱函数中使用默认的行为,那么只需调用Reflect对象上的相应方法。例如,下面的代码为getPrototypeOf 方法与 setPrototypeOf 方法实现了默认的行为:

let target = {};
let proxy = new Proxy(target, {
    getPrototypeOf(trapTarget) {
        return Reflect.getPrototypeOf(trapTarget);
    },
    setPrototypeOf(trapTarget, proto) {
        return Reflect.setPrototypeOf(trapTarget, proto);
    }
});
let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // true
// 成功
Object.setPrototypeOf(target, {});
// 同样成功
Object.setPrototypeOf(proxy, {});

对象可扩展性的陷阱函数

ES5 通过 Object.preventExtensions() 与 Object.isExtensible() 方法给对象增加了可扩展性。而 ES6 则通过 preventExtensions 与 isExtensible 陷阱函数允许代理拦截对于底层对象的方法调用。

isExtensible 陷阱函数必须返回一个布尔值用于表明目标对象是否可被扩展,而preventExtensions陷阱函数也需要返回一个布尔值,用于表明操作是否已成功。同时也存在 Reflect.preventExtensions() 与 Reflect.isExtensible() 方法,用于实现默认的行为。这两个方法都返回布尔值,因此它们可以在对应的陷阱函数内直接使用。

为了弄懂对象可扩展性的陷阱函数如何运作,可研究如下代码,该代码实现了 isExtensible与 preventExtensions 陷阱函数的默认行为。

let target = {};
let proxy = new Proxy(target, {
    isExtensible(trapTarget) {
        return Reflect.isExtensible(trapTarget);
    },
    preventExtensions(trapTarget) {
        return Reflect.preventExtensions(trapTarget);
    }
});
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
Object.preventExtensions(proxy);
console.log(Object.isExtensible(target)); // false
console.log(Object.isExtensible(proxy)); // false

属性描述符的陷阱函数

ES5 最重要的特征之一就是引入了Object.defineProperty()方法用于定义属性的特性。在JS之前的版本中,没有方法可以定义一个访问器属性,也不能让属性变成只读或是不可枚举。而这些特性都能够利用Object.defineProperty()方法来实现,并且你还可以利用Object.getOwnPropertyDescriptor() 方法来检索这些特性。

代理允许你使用 defineProperty 与 getOwnPropertyDescriptor 陷阱函数,来分别拦截对Object.defineProperty() 与 Object.getOwnPropertyDescriptor() 的调用。 defineProperty陷阱函数接受下列三个参数:

默认的行为:

let proxy = new Proxy({}, {
    defineProperty(trapTarget, key, descriptor) {
        return Reflect.defineProperty(trapTarget, key, descriptor);
    },
    getOwnPropertyDescriptor(trapTarget, key) {
        return Reflect.getOwnPropertyDescriptor(trapTarget, key);
    }
});
Object.defineProperty(proxy, "name", {
value: "proxy"
});
console.log(proxy.name); // "proxy"
let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");
console.log(descriptor.value); // "proxy"

阻止 Object.defineProperty(

defineProperty 陷阱函数要求你返回一个布尔值用于表示操作是否已成功。当它返回 true时, Object.defineProperty() 会正常执行;而如果它返回了 false ,则Object.defineProperty() 会抛出错误。

let proxy = new Proxy({}, {
    defineProperty(trapTarget, key, descriptor) {
        if (typeof key === "symbol") {
            return false;
        }
        return Reflect.defineProperty(trapTarget, key, descriptor);
    }
});
Object.defineProperty(proxy, "name", {
value: "proxy"
});
console.log(proxy.name); // "proxy"
let nameSymbol = Symbol("name");
// 抛出错误
Object.defineProperty(proxy, nameSymbol, {
value: "proxy"
});

描述符对象的限制

为了确保 Object.defineProperty() 与 Object.getOwnPropertyDescriptor() 方法的行为一致,传递给 defineProperty 陷阱函数的描述符对象必须是正规的。出于同一原因,getOwnPropertyDescriptor陷阱函数返回的对象也始终需要被验证。

任意对象都能作为 Object.defineProperty() 方法的第三个参数;然而传递给defineProperty 陷阱函数的描述符对象参数,则只有 enumerable 、 configurable 、value 、 writable 、 get 与 set 这些属性是被许可的。

ownKeys 陷阱函数

ownKeys 代理陷阱拦截了内部方法[[OwnPropertyKeys]],并允许你返回一个数组用于重写该行为。返回的这个数组会被用于四个方法: Object.keys() 方法、Object.getOwnPropertyNames() 方法、 Object.getOwnPropertySymbols() 方法与
Object.assign() 方法,其中 Object.assign() 方法会使用该数组来决定哪些属性会被复制。

ownKeys 陷阱函数的默认行为由Reflect.ownKeys()方法实现,会返回一个由全部自有属性的键构成的数组,无论键的类型是字符串还是符号。 Object.getOwnProperyNames() 方法与Object.keys() 方法会将符号值从该数组中过滤出去;相反,
Object.getOwnPropertySymbols() 会将字符串值过滤掉;而Object.assign()方法会使用数组中所有的字符串值与符号值。

ownKeys 陷阱函数接受单个参数,即目标对象,同时必须返回一个数组或者一个类数组对象,不合要求的返回值会导致错误。你可以使用 ownKeys 陷阱函数去过滤特定的属性,以避免这些属性被Object.keys()方法、Object.getOwnPropertyNames() 方法、Object.getOwnPropertySymbols()方法或Object.assign()方法使用。假设你不想在结果中包含任何以下划线打头的属性(在 JS 的编码惯例中,这代表该字段是私有的),那么可以使用ownKeys陷阱函数来将它们过滤掉,就像下面这样:

let proxy = new Proxy({}, {
    ownKeys(trapTarget) {
        return Reflect.ownKeys(trapTarget).filter(key => {
            return typeof key !== "string" || key[0] !== "_";
        });
    }
});
let nameSymbol = Symbol("name");
proxy.name = "proxy";
proxy._name = "private";
proxy[nameSymbol] = "symbol";
let names = Object.getOwnPropertyNames(proxy),
keys = Object.keys(proxy);
symbols = Object.getOwnPropertySymbols(proxy);
console.log(names.length); // 1
console.log(names[0]); // "name"
console.log(keys.length); // 1
console.log(keys[0]); // "name"
console.log(symbols.length); // 1
console.log(symbols[0]); // "Symbol(name)"

ownKeys 陷阱函数也能影响 for-in 循环,因为这种循环调用了陷阱函数来决定哪些值能够被用在循环内。

使用 apply 与 construct 陷阱函数的函数代理

在所有的代理陷阱中,只有 apply 与 construct 要求代理目标对象必须是一个函数。函数拥有两个内部方法: [[Call]] 与 [[Construct]] ,前者会在函数被直接调用时执行,而后者会在函数被使用 new 运算符调用时执行。 apply 与 construct陷阱函数对应着这两个内部方法,并允许你对其进行重写。当不使用 new 去调用一个函数时, apply 陷阱函数会接收到下列三个参数( Reflect.apply() 也会接收这些参数):

当使用 new 去执行函数时, construct 陷阱函数会被调用并接收到下列两个参数:

因此,可以用来做很多骚操作,比如

调用构造器而无须使用 new

function Numbers(...values) {
    if (typeof new.target === "undefined") {
        throw new TypeError("This function must be called with new.");
    }
    this.values = values;
}
let NumbersProxy = new Proxy(Numbers, {
    apply: function(trapTarget, thisArg, argumentsList) {
        return Reflect.construct(trapTarget, argumentsList);
    }
});
let instance = NumbersProxy(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]

可被撤销的代理

在被创建之后,代理通常就不能再从目标对象上被解绑。本章之前的例子都使用了不可被撤销的代理,但有的情况下你可能想撤销一个代理以便让它不能再被使用。当你想通过公共接口向外提供一个安全的对象,并且要求要随时都能切断对某些功能的访问,这种情况下可被撤销的代理就会非常有用。

你可以使用 Proxy.revocable()方法来创建一个可被撤销的代理,该方法接受的参数与Proxy构造器的相同:一个目标对象- 、一个代理处理器,而返回值是包含下列属性的一个对象:

当 revoke() 函数被调用后,就不能再对该proxy对象进行更多操作,任何与该代理对象交互的意图都会触发代理的陷阱函数,从而抛出一个错误。

总结

在 ES6 之前,特定对象(例如数组)会显示出一些非常规的、无法被开发者复制的行为,而代理的出现改变了这种情况。代理允许你为一些 JS 底层操作自行定义非常规行为,因此你就可以通过代理陷阱来复制JS内置对象的所有行为。在各种不同操作发生时(例如对于 in运算符的使用),这些代理陷阱会在后台被调用。

反射接口也是在 ES6 中引入的,允许开发者为每个代理陷阱实现默认的行为。每个代理陷阱在 Reflect 对象( ES6 的另一个新特性)上都有一个同名的对应方法。将代理陷阱与反射接口方法结合使用,就可以在特定条件下让一些操作有不同的表现,有别于默认的内置行为。

可被撤销的代理是一种特殊的代理,可以使用revoke()函数去有效禁用。revoke()函数终结了代理的所有功能,因此在它被调用之后,所有与代理属性交互的意图都会导致抛出错误。第三方开发者可能需要在一定时间内获取特定对象的使用权,在这种场合,可被撤销的代理对应用的安全性来说就非常重要。

尽管直接使用代理是最有力的使用方式,但你也可以把代理用作另一个对象的原型。但只有很少的代理陷阱能在作为原型的代理上被有效使用,包括 get 、 set 与 has 这几个,这让这方面的用例变得十分有限。

上一篇下一篇

猜你喜欢

热点阅读