介紹正式加入 JavaScript 的反射 API 和新的代理對象,開發者可以通過代理對象攔截每一個對象中執行的操作,代理也賦予了開發者空前的對象控制權,同樣也為定義新的交互模式帶來無限可能。 ...
代理(Proxy)是一種可以攔截並改變底層JavaScript引擎操作的包裝器,在新語言中通過它暴露內部運作的對象,從而讓開發者可以創建內建的對象。
數組問題
在ECMAScript6出現之前,開發者不能通過自己定義的對象模仿JavaScript數組對象的行為方式。當給數組的特定元素賦值時,影響到該數組的length屬性,也可以通過length屬性修改數組元素。
let colors = ["red", "green", "blue"];
console.log(colors.length); // 3
colors[3] = "black";
console.log(colors.length); // 4
console.log(colors[3]); // "black"
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
Note
數值屬性和length屬性具有這種非標準行為,因而在ECMAScript中數組被認為是奇異對象(exotic object,與普通對象相對)。
代理和反射
調用 new Proxy() 可創建代替其他目標(taget)對象的代理,它虛擬化了目標,所以二者看起來功能一致。
代理可以攔截 JavaScript 引擎內部目標的底層對象操作,這些底層操作被攔截後會觸發響應特定操作的陷阱函數。
反射API可以Reflect對象的形式出現,對象中方法的預設特性與相同的底層操作一致,而代理可以覆寫這些操作,每個代理陷阱對應一個命名和參數都相同的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.isExtensible() | Reflect.isExtensible() |
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() |
創建一個簡單的代理
Proxy構造函數有兩個參數
- 目標(target)
- 處理程式(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陷阱接受4個參數:
- trapTarget
用於接收屬性(代理的目標)的對象 - key
要寫入的屬性鍵(字元串或Symbol類型) - value
被寫入屬性的值 - receiver
操作發生的對象(通常是代理)
Reflect.set()是set陷阱對應的反射方法和預設特性,它和set陷阱一樣也接受同樣的4個參數。
let target = {
name: "target"
};
let proxy = new Proxy(target, {
set(trapTarget, key, value, receiver) {
if (!trapTarget.hasOwnProperty(key)) {
if (isNaN(value)) {
throw new TypeError("屬性必須是數字");
}
}
return Reflect.set(trapTarget, key, value, receiver);
}
});
proxy.count = 1;
console.log(proxy.count); // 1
console.log(target.count); // 1
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
// 拋出錯誤:
// Uncaught TypeError: 屬性必須是數字
proxy.anotherName = "proxy";
用get陷阱驗證對象結構(Object Shape)
get陷阱接受3個參數:
- trapTarget
被讀取屬性的源的對象(代理的目標) - key
被讀取的屬性鍵 - value
操作發生的對象(通常是代理)
Reflect.get()也接受同樣3個參數並返回屬性的預設值。
let proxy = new Proxy({}, {
get(trapTarget, key, receiver) {
if (!(key in receiver)) {
throw new TypeError("屬性" + key + "不存在");
}
return Reflect.get(trapTarget, key, receiver);
}
});
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
// 拋出錯誤:
// Uncaught TypeError: 屬性nme不存在
console.log(proxy.nme);
使用has陷阱隱藏已有屬性
可以用in操作符來檢測給定對象中是否含有某個屬性,如果自由屬性或原型屬性匹配的名稱或Symbol就返回true。
let target = {
value: 42
};
console.log("value" in target); // true
console.log("toString" in target); // true
在代理中使用has陷阱可以攔截這些in操作並返回一個不同的值。
in陷阱接受2個參數:
- trapTarget
讀取屬性的對象(代理的目標) - key
要檢查的屬性值(字元串或Symbol)
let target = {
name: "target",
vlaue: 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
用deleteProperty陷阱防止刪除屬性
delete操作符可以從對象中刪除屬性,如果成功則返回true,不成功則返回false。
在嚴格模式下,如果你嘗試刪除一個不可配置(nonconfigurable)屬性則會導致程式拋出錯誤,而在非嚴格模式下只是返回false。
每當通過delete操作符刪除對象屬性時,deleteProperty陷阱都會被調用,它接受2個參數:
- trapTarget
要刪除屬性的對象(代理的目標) - key
要刪除的屬性鍵(字元串或Symbol)
Reflect.deleteProperty()方法為deleteProperty陷阱提供預設實現,並且接受同樣的兩個參數。
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);
}
}
});
console.log("value" in proxy); // true
let result1 = delete proxy.value;
console.log(result1); // false
console.log("value" in proxy); // true
console.log("name" in proxy); // true
let result2 = delete proxy.name;
console.log(result2); // true
console.log("name" in proxy); // false
原型代理陷阱
ES6中新增的Object.setPrototypeOf()方法,它被用於作為ES5中的Object.getPrototype()方法的補充。通過代理中的setPrototypeOf陷阱和getPrototypeOf陷阱可以攔截這兩個方法的執行過程。
setPrototypeOf陷阱接受2個參數:
- trapTarget
接受原型設置的對象(代理的目標) - proto
作為原型使用的對象
傳入Object.setPrototypeOf()方法和Reflect.setPrototypeOf()方法的均是以上兩個參數。
getPrototypeOf陷阱、Object.getPrototypeOf()方法和Reflect.getPrototypeOf()方法只接受參數trapTarget。
原型代理陷阱的運行機制
原型代理陷阱有一些限制:
getPrototypeOf陷阱必須返回對象或null
在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, {});
// 給不存在的屬性賦值會拋出錯誤:
// Uncaught TypeError: 'setPrototypeOf' on proxy: trap returned falsish
Object.setPrototypeOf(proxy, {});
可以使用Reflect上的對應方法實現這兩個陷阱的預設行為。
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, {});
對象可擴展性陷阱
ECMAScript 5 已經通過 Object.preventExtensions() 方法和 Object.isExtensible() 方法修正了對象的可擴展性;
ECMAScript 6 可以通過代理中的 preventExtensions 和 isExtensible 陷阱攔截這兩個方法並調用底層對象。
兩個陷阱都接受唯一參數 trapTarget 對象,並調用它上面的方法。
isExtensible 陷阱返回的一定是一個 boolean 值,表示對象是否可擴展;
preventExtensions 陷阱返回的也一定是布爾值,表示操作是否成功。
Reflect.preventExtensions() 方法和 Reflect.isExtensible() 方法實現了相應陷阱中的預設行為,二兩都返回布爾值。
兩個基礎示例
預設實現:
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
使用陷阱使 Object.preventExtensions() 對 proxy 失效。
let target = {};
let proxy = new Proxy(target, {
isExtensible(trapTarget) {
return Reflect.isExtensible(trapTarget);
},
preventExtensions(trapTarget) {
return false;
}
});
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
// 拋出錯誤:
// Uncaught TypeError: 'preventExtensions' on proxy: trap returned falsish
Object.preventExtensions(proxy);
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
屬性描述符陷阱
ECMAScript 5 最重要的特性之一是可以使用 Object.defineProperty() 方法定義屬性特性(property attribute)。可以通過 Object.getOwnPropertyDescriptor() 方法來獲取這些屬性。
在代理中可以分別用 defineProperty 陷阱和 getOwnPropertyDescriptor 陷阱攔截 Object.defineProperty() 方法和 Object.getOwnPropertyDescriptor() 方法的調用。
defineProperty 陷阱接受以下參數:
- trapTarget
要定義屬性的對象(代理的目標) - key
屬性的鍵(字元串或Symbol) - descriptor
屬性的描述符對象
操作成功後返回 true,否則返回 false。
getOwnPropertyDescriptor 陷阱接受以下參數:
- trapTarget
要定義屬性的對象(代理的目標) - key
屬性的鍵(字元串或Symbol)
最終返回描述符。
陷阱預設行為示例:
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() 方法拋出錯誤。
例:阻止 Symbol 類型的屬性
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");
// 拋出錯誤:
// Uncaught TypeError: 'defineProperty' on proxy: trap returned falsish for property 'Symbol(name)'
Object.defineProperty(proxy, nameSymbol, {
value: "proxy"
});
描述符對象限制
defineProperty 陷阱
defineProperty 陷阱的描述對象已規範化,只有下列屬性會被傳遞給 defineProperty 陷阱的描述符對象。
- enumerable
- configurable
- value
- writable
- get
- set
let proxy = new Proxy({}, {
defineProperty(trapTarget, key, descriptor) {
console.log(descriptor.value); // "proxy"
console.log(descriptor.name); // undefined
return Reflect.defineProperty(trapTarget, key, descriptor);
}
});
Object.defineProperty(proxy, "name", {
value: "proxy",
name: "custom"
});
getOwnPropertyDescriptor 陷阱
getOwnPropertyDescriptor 陷阱的返回值必須是 null、undefined或一個對象;
如果返回對象,則對象自己的屬性只能是 enumerable、configurable、vlaue、writable、get和set;
在返回的對象中使用不被允許的屬性會拋出一個錯誤。
let proxy = new Proxy({}, {
getOwnPropertyDescriptor(trapTarget, key) {
return {
name: "proxy"
};
}
});
// 給不存在的屬性賦值會拋出錯誤
// Uncaught TypeError: 'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for property 'name' which is either non-existant or configurable in the proxy target
let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");
這條限制可以確保無論代理中使用了什麼方法,Object.getOwnPropertyDescriptor() 返回值的結構總是可靠的。
ownKeys 陷阱
ownKeys 陷阱可以攔截內部方法 [[OwnPropertyKeys]],通過返回一個數組的值可以覆寫其行為。
這個數組被用於 Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols() 和 Object.assign() 4個方法,Object.assign() 方法用數組來確定需要複製的屬性。
ownKeys 陷阱通過 Reflect.ownKeys() 方法實現預設的行為,返回的數組中包含所有自有屬性的鍵名,字元串類型和 Symbol 類型的都包含在內。
Object.getOwnPropertyNames() 方法和 Object.keys() 方法返回的結果將 Symbol 類型的屬性名排除在外;
Object.getOwnPropertySymbols() 方法返回的結果將字元串類型的屬性名排除在外;
Object.assign() 方法支持字元串和 Symbol 兩種類型。
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)"
函數代理的 apply 和 construct 陷阱
所有代理陷阱中,只有 apply 和 construct 的代理目標是一個函數。
函數有兩個內部方法 [[Call]] 和 [[Construct]],apply 陷阱和 construct 陷阱可以覆寫這些內部方法。
若使用 new 操作符調用函數,則執行 [[Construct]] 方法;若不用,則執行 [[Call]] 方法。
apply 陷阱和 Reflect.apply() 都接受以下參數:
- trapTarget
被執行的函數(代理的目標) - thisArg
函數被調用時內部this的值 - argumentList
傳遞給函數的參數數組
當使用 new 調用函數時調用的 construct 陷阱接受以下參數:
- trapTarget
被執行的函數(代理的目標) - argumentList
傳遞給函數的參數數組
Reflect.construct() 方法也接受這兩個參數,其還有一個可選的第三個參數 newTarget。
let target = function() { return 42; },
proxy = new Proxy(target, {
apply: function(trapTarget, thisArg, argumentList) {
return Reflect.apply(trapTarget, thisArg, argumentList);
},
construct: function(trapTarget, argumentList) {
return Reflect.construct(trapTarget, argumentList);
}
});
// 一個目標是函數的代理開起來也像一個函數
console.log(typeof proxy); // function
console.log(proxy()); // 42
var instance = new proxy();
console.log(instance instanceof proxy); // true
console.log(instance instanceof target); // true
驗證函數參數
例:驗證所有參數必須是數字:
// 將所有參數相加
function sum(...values) {
return values.reduce((previous, current) => previous + current, 0);
}
let sumProxy = new Proxy(sum, {
apply: function(trapTarget, thisArg, argumentList) {
argumentList.forEach((arg) => {
if (typeof arg !== "number") {
throw new TypeError("所有參數必須是數字");
}
});
return Reflect.apply(trapTarget, thisArg, argumentList);
},
construct: function(trapTarget, argumentList) {
throw new TypeError("該函數不可通過new來調用");
}
});
console.log(sumProxy(1, 2, 3, 4)); // 10
// 拋出錯誤
// Uncaught TypeError: 所有參數必須是數字
console.log(sumProxy(1, "2", 3, 4));
// 拋出錯誤
// Uncaught TypeError: 該函數不可通過new來調用
let result = new sumProxy();
可調用的類構造函數
使用 apply 陷阱創建實例
class Person {
constructor(name) {
this.name = name;
}
}
let PersonProxy = new Proxy(Person, {
apply: function(trapTarget, thisArg, argumentList) {
return new trapTarget(...argumentList);
}
});
let me = PersonProxy("JiaJia");
console.log(me.name); // JiaJia
console.log(me instanceof Person); // true
console.log(me instanceof PersonProxy); // true
可撤銷代理
可以使用 Proxy.revocable() 方法創建可撤銷的代理,該方法採用與 Proxy 構造函數相同的參數:目標對象和代理處理程式。
返回值是具有以下屬性的對象:
- proxy
可被撤銷的代理對象 - revoke
撤銷代理要調用的函數
let target = {
name: "target"
};
let { proxy, revoke } = Proxy.revocable(target, {});
console.log(proxy.name); // target
revoke();
// 拋出錯誤
// Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
console.log(proxy.name);
解決數組問題
數組問題
let colors = ["red", "green", "blue"];
console.log(colors.length); // 3
colors[3] = "black";
console.log(colors.length); // 4
console.log(colors[3]); // black
colors.length = 2;
自定義數組類型
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}
function isArrayIndex(key) {
let numericKey = toUint32(key);
return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}
class MyArray {
constructor(length = 0) {
this.length = length;
return new Proxy(this, {
set(trapTarget, key, value) {
let currentLength = Reflect.get(trapTarget, "length");
if (isArrayIndex(key)) {
let numericKey = Number(key);
if (numericKey >= currentLength) {
Reflect.set(trapTarget, "length", numericKey + 1);
}
} else if (key === "length") {
if (value < currentLength) {
for (let index = currentLength - 1; index >= value; index--) {
Reflect.deleteProperty(trapTarget, index);
}
}
}
return Reflect.set(trapTarget, key, value);
}
});
}
}
let colors = new MyArray(3);
console.log(colors instanceof MyArray); // true
console.log(colors.length); // 3
let colors2 = new MyArray(5);
console.log(colors.length); // 3
console.log(colors2.length); // 5
colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
colors[3] = "black";
console.log(colors.length); // 4
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
console.log(colors[0]); // "red"
將代理作為原型
let target = {};
let proxy = new Proxy(target, {
defineProperty(trapTarget, name, descriptor) {
return false;
}
});
let newTarget = Object.create(proxy);
Object.defineProperty(newTarget, "name", {
value: "newTarget"
});
console.log(newTarget.name); // "newTarget"
console.log(newTarget.hasOwnProperty("name")); // true
console.log(Object.getPrototypeOf(newTarget) === proxy); // true
關於 Object.create() 方法可以參照 這裡
上例中 newTarget 的原型是代理,但是在對象上定義屬性的操作不需要操作對象原型,所以沒有觸發代理中的陷阱。
儘管代理作為原型使用時及其受限,但有幾個陷阱仍然有用。
在原型上使用 get 陷阱
let target = {};
let thing = Object.create(new Proxy(target, {
get(trapTarget, key, value) {
throw new ReferenceError(`${key} deesn't exist`);
}
}));
thing.name = "thing";
console.log(thing.name); // "thing"
// 拋出異常:
// Uncaught ReferenceError: unknown deesn't exist
let unknown = thing.unknown;
訪問對象上不存在的屬性時,會觸發原型中的 get 陷阱。
在原型上使用 set 陷阱
let target = {};
let thing = Object.create(new Proxy(target, {
set (trapTarget, key, value, receiver) {
return Reflect.set(trapTarget, key, value, receiver);
}
}));