理解ES6 proxy&reflection
簡介
-
proxy
proxy
可以攔截目標(target)上的物件進行操作,使用trap
攔截這些操作,trap
是響應特定操作的方法。 -
reflection
reflection
是通過Reflect
物件表示,他提供了一些方法集,為代理proxy
提供預設行為。
下面是一些proxy trap
和Reflect
方法,每個proxy trap
都有提供對應的Reflect
方法,他們接收相同的引數。
Proxy Trap | Overrides the Behavior Of | Default Behavior |
---|---|---|
get | Reading a property value | Reflect.get() |
set | Writing to a property | Reflect.set() |
has | The in operator | Reflect.has() |
deleteProperty | The delete operator | 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 | Calling a function | Reflect.apply() |
construct | Calling a function with new | Reflect.construct() |
這裡的每個trap
都會覆蓋物件的內建行為,便於攔截和修改物件。如果你真的需要內建行為,可以使用相對應的Reflect
方法。
開始的時候,
ES6
的規範有個enumerate trap
,用於改變for..in
和Object.keys
的列舉屬性,但是在實行的時候發現有些困難,於是在ES7
中移除了。所以這裡不討論他。
建立一個簡單的代理
當你使用Proxy
的建構函式去建立代理的時候,他接受兩個引數,一個是目標物件(target),另外一個是處理物件(handler)。這個handler
定義了一個或者多個trap
去處理代理,如果沒有定義trap
,那麼就會使用預設的行為。
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"
複製程式碼
從上面這個例子可以發現,不管是proxy
或者是target
的屬性更改,都會影響到另外一個。其實這就是這兩個的關係: proxy
本身不儲存這個屬性,他只是把操作轉發到target
。 上面的這個例子似乎沒啥意思,並沒有體現出核心trap
的價值所在。
使用set trap
驗證屬性
set trap
是在設定屬性值時觸發。 set trap
接收這幾個引數:
trapTarget
- 接收的屬性的物件,就是代理的目標。key
- 要寫入的屬性的key
(string || symbol
)value
- 寫入屬性的值receiver
- 操作的物件,通常是代理
Reflect.set
是set trap
相對應的方法。如果屬性被設定,那麼trap
應該返回true
,如果沒有被設定,那就返回false
。Reflect.set()
會根據操作是否成功返回正確的值。
要驗證一個屬性的值,那就需要使用set
trap來檢查這個值,看下面程式碼:
let target = {
name: "target"
};
let proxy = new Proxy(target, {
set(trapTarget, key, value, receiver) {
console.log(`trapTarget is ${trapTarget}, key is ${key}, value is ${value}, receiver is ${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
// 賦值給存在target上的屬性
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
// 新的屬性值不是數字會丟擲異常
proxy.anotherName = "proxy";
複製程式碼
可以發現,每次設定屬性值的時候都會進行攔截判斷,所以,相對的,你在獲取的時候,可以使用get
進行攔截判斷。
使用get trap
驗證
js
一個有趣又令人困惑的地方就是獲取一個不存在的屬性的時候,不會丟擲異常,只會返回一個undefined
。不像其他的大多數語言,都會丟擲一個錯誤,可能你寫了大量的程式碼,你可能會意識到這是一個嚴重的問題,比如拼寫錯誤的這些問題,代理可以為你處理這些。
get
是在讀取物件屬性的時候用到的trap
。 他接收三個引數:
trapTarget
- 從哪個物件讀取的屬性,就是target.key
- 讀取的key
receiver
- 操作的物件,通常是代理(proxy)
可以發現這個和上面的set
差不多,就是少了一個設定的value
引數。相對的,Reflect.get
方法接受與get trap
相同的三個引數,並返回屬性的預設值。
var 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.age); // throws error
複製程式碼
不知道你有沒有發現,我們在這裡使用receiver
代替trapTarget
配合in
一起使用,我們將在下面介紹。
使用has trap
隱藏屬性的存在
in
這個操作想來大家比較熟悉的,是確定屬性是否存在物件及原型鏈上。
var target = {
value: 42;
}
console.log("value" in target); // true
console.log("toString" in target); // true
複製程式碼
代理允許你使用has
這個trap
去返回不同的值。 這個has trap
是在使用in
操作時觸發。has trap
接收兩個引數:
trapTarget
key
Reflect.has
方法接受這些相同的引數並返回in
運算子的預設響應。使用has trap
和Reflect.has
可以改變某些屬性的in
行為,同時又回退到其他屬性的預設行為。例如你只想隱藏value
屬性:
var target = {
name: "target",
value: 42
};
var 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
複製程式碼
可以發現上例直接判斷,如果不是value key
,就使用Reflect
去返回其預設行為。
使用deleteProperty trap
對刪除進行操作
通過屬性描述那部分我們知道,delete
是通過configurable
來控制的,非嚴格模式下刪除會返回false
,嚴格模式下會報錯。但是我們可以使用代理deleteProperty trap
去操作他這個行為。
下面我們再來看看deleteProperty
這個trap
。他也是接受兩個引數:
trapTarget
key
Reflect.deleteProperty
方法提供了deleteProperty trap
相對的行為去實現。所以我們可以使用這兩個去改變delete
的預設行為。
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);
}
}
});
// Attempt to delete proxy.value
console.log("value" in proxy); // true
let result1 = delete proxy.value;
console.log(result1); // false
console.log("value" in proxy); // true
// Attempt to delete proxy.name
console.log("name" in proxy); // true
let result2 = delete proxy.name;
console.log(result2); // true
console.log("name" in proxy); // false
複製程式碼
這樣可以攔截操作,好奇的你可能會想去操作nonconfigurable
的時候,也可以刪除,你可以嘗試一下。這個方法在受保護的屬性被刪除的時候,非嚴格模式下會拋錯。
原型的代理trap
在這個章節裡介紹了setPrototypeOf
和getPrototypeOf
。代理也為這兩種情況添加了相應的trap
。針對這兩個代理的trap
,都有不同的引數:
- setPrototypeOf
trapTarget
proto
這個用作原型的物件
他和Reflect.setPrototypeOf
接收的引數相同,去做相對應的操作。另一方面,getPrototypeOf
只接收一個引數trapTarget
,相應的也存在Reflect.getPrototypeOf
方法.
原型代理是如何工作的
他們有一些限制。首先,getPrototypeOf
只可以返回物件或者null
,返回其他的,在執行的時候會報錯。同樣的,setPrototypeOf trap
如果失敗,必須返回false
,並且Object.setPrototypeOf
會拋錯, 如果返回其他的值,那就是認為操作成功。
下面來看一個例子:
var target = {};
var proxy = new Proxy(target, {
getPrototypeOf(trapTarget) {
return null;
},
setPrototypeOf(trapTarget, proto) {
return false;
}
});
var targetProto = Object.getPrototypeOf(target);
var proxyProto = Object.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // false
console.log(proxyProto); // null
// succeeds
Object.setPrototypeOf(target, {});
// throws error
Object.setPrototypeOf(proxy, {});
複製程式碼
從上面可以發現,對於proxy
進行了攔截,以至於原型不同。然後對proxy
進行setPrototypeOf
會丟擲異常,就是上面提到的,setPrototypeOf
返回false
,那麼Object.setPrototypeOf
會丟擲錯誤。 當然,如果你想要使用它的預設行為,那就需要使用Reflect
物件的方法來操作。
為什麼會有兩套方法
讓人感到困惑的是, setPrototypeOf trap
和getPrototypeOf trap
看起來和Object.getPrototypeOf() or Object.setPrototypeOf()
看起來類似,為什麼還要這兩套方法。其實他們看起來是類似,但是還有很大的差異:
首先,Object.getPrototype||Object.setPrototypeOf
在一開始就是為開發人員建立的高級別的操作。然而Reflect.getPrototypeOf || Reflect.setPrototypeOf
是提供了操作以前僅僅用於內部的[[GetPrototypeOf]] || [[SetPrototypeOf]]
的底層屬性。Reflect.getPrototypeOf
方法是內部[[GetPrototypeOf]]
操作的包裝器(帶有一些輸入驗證)。Reflect.setPrototypeOf
方法和[[SetPrototypeOf]]
具有相同的關係。Object
上的相應方法也呼叫[[GetPrototypeOf]]
和[[SetPrototypeOf]]
,但在呼叫之前執行幾個步驟並檢查返回值以確定如何操作。
上面說的比較泛泛,下面來詳細說下:
如果Reflect.getPrototypeOf
方法的引數不是物件或者null
,則丟擲錯誤;而Object.getPrototypeOf
在執行操作之前首先將值強制轉換為物件。
var result1 = Object.getPrototypeOf(1);
console.log(result1 === Number.prototype); // true
// throws an error
Reflect.getPrototypeOf(1);
複製程式碼
很明顯,Object
上的可以工作,他把數字1
轉換成了物件,Reflect
上的不會進行轉換,所以丟擲異常。
setPrototypeOf
也有一些不同,Reflect.setPrototypeOf
會返回一個布林來確定是否成功,false
就是失敗。然而Object.setPrototypeOf
如果失敗,會丟擲錯誤。
物件 Extensibility trap
ECMAScript 5
通過Object.preventExtensions
和Object.isExtensible
方法添加了物件可擴充套件性的操作,因此ES6
在此基礎上對這兩個方法添加了代理。並且這兩個代理方法都只接收一個引數trapTarget
.isExtensible trap
必須返回布林值來確定是否是可擴充套件的,preventExtensions trap
返回布林值確定是否成功。
Reflect
物件裡的這兩個方法都會返回布林值,所以這兩個是可以作為相對應的方法去使用實現預設行為。
兩個簡單的例子
var target = {};
var 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
屬性操作之後,就是返回預設的行為。
如果我們不想他拓展,我們可以這樣去處理:
var target = {};
var 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
Object.preventExtensions(proxy);
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
複製程式碼
這裡他不會成功,因為返回了false
,沒有使用對應的Reflect
去做相對的預設行為處理,所以操作不會轉發到操作的目標。
Duplicate Extensibility Methods
如果傳遞物件值作為引數,方法Object.isExtensible
和Reflect.isExtensible
類似。如果不是物件作為引數傳遞,Object.isExtensible
始終返回false
,而Reflect.isExtensible
則丟擲錯誤。
let result1 = Object.isExtensible(2);
console.log(result1); // false
// throws error, Reflect.isExtensible called on non-object
let result2 = Reflect.isExtensible(2);
複製程式碼
這個限制類似於Object.getPrototypeOf
和Reflect.getPrototypeOf
方法之間的差異,因為具有較低級別功能的方法具有比其更高級別對應方更嚴格的錯誤檢查。
Object.preventExtensions
和Reflect.preventExtensions
方法也非常相似。 Object.preventExtensions
方法始終返回作為引數傳遞給它的值,即使該值不是物件也是如此。然而另一方面,如果引數不是物件,那麼Reflect.preventExtensions
方法會丟擲錯誤;如果引數是一個物件,那麼 Reflect.preventExtensions
在操作成功時返回true
,否則返回false
。
var result1 = Object.preventExtensions(2);
console.log(result1); // 2
var target = {};
var result2 = Reflect.preventExtensions(target);
console.log(result2); // true
// throws error
var result3 = Reflect.preventExtensions(2);
複製程式碼
這個例子就是對上面的總結。
Property Descriptor Traps
ECMAScript 5
最重要的功能之一是使用Object.defineProperty
方法定義屬性具體屬性的能力。在以前的JavaScript版本中,無法定義訪問者屬性,使屬性成為只讀,或使屬性不可數。具體參考這裡
代理允許分別 使用defineProperty trap
和getOwnPropertyDescriptor trap
攔截對Object.defineProperty
和Object.getOwnPropertyDescriptor
的呼叫。 defineProperty trap
接收以下引數:
trapTarget
- 被定義屬性的物件(代理的目標)key
descriptor
defineProperty trap
返回布林值。getOwnPropertyDescriptor trap
只接收trapTarget
和key
,並且返回描述資訊。相應的Reflect.defineProperty
和Reflect.getOwnPropertyDescriptor
方法接受與其代理trap
對應方相同的引數。
例如:
var 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"
var descriptor = Object.getOwnPropertyDescriptor(proxy, "name");
console.log(descriptor.value); // "proxy"
複製程式碼
很簡單的一個例子,基本沒有在攔截上做任何操作,只是返回他的預設行為。
Blocking Object.defineProperty()
trap
返回true
時,Object.defineProperty
表示成功; trap
返回false
時,Object.defineProperty
會丟擲錯誤。 可以使用這個功能來限制Object.defineProperty
方法可以定義的屬性型別.如下:
var 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"
var nameSymbol = Symbol("name");
// throws error
Object.defineProperty(proxy, nameSymbol, {
value: "proxy"
});
複製程式碼
這裡我們檢測key
的型別,如果是symbol
就返回false
.對於Object.defineProperty
,返回false
會丟擲異常。
當然可以通過返回
true
而不呼叫Reflect.defineProperty
方法使Object.defineProperty
預設是失敗的,這就避免錯誤的丟擲。
Descriptor Object Restrictions
為了確保在使用Object.defineProperty
和Object.getOwnPropertyDescriptor
方法時的一致行為,傳遞給defineProperty trap
的描述符物件被規範化。從getOwnPropertyDescriptor trap
返回的物件總是出於同樣的原因進行驗證。
不管哪個引數作為第三個引數傳遞給Object.defineProperty
方法,都只能是下面這幾種:enumerable
, configurable
, value
, writable
, get
, set
這些將被作為descriptor
傳遞。例如:
var proxy = new Proxy({}, {
defineProperty(trapTarget, key, descriptor) {
console.log(descriptor.value); // "proxy"
console.log(descriptor.name); // undefined
console.log(descriptor.writable) // undefined
return Reflect.defineProperty(trapTarget, key, descriptor);
}
});
Object.defineProperty(proxy, "name", {
value: "proxy",
name: "custom"
});
複製程式碼
可以發現,name
不存在那幾個descriptor
裡,所以傳遞不進去,不接收。並且這個和Object.defineProperty
不同,沒有進行一些包裝,不存在預設的writable
, configurable
這些..。但是按理來說,你傳遞一個物件進行,他就應該接收啊,為啥這裡會是undefined
呢?這是因為**descriptor
實際上不是對傳遞給Object.defineProperty
方法的第三個引數的引用,而是一個僅包含允許屬性的新物件。Reflect.defineProperty
方法還會忽略描述符上的任何非標準屬性**
getOwnPropertyDescriptor
稍微有些不同,他會返回null
, undefined
,object
.如果返回的是物件,那麼物件只會包含上面可能出現的descriptor
的這幾種情況。
如果返回具有不允許的屬性的物件,會導致錯誤,如下程式碼:
var proxy = new Proxy({}, {
getOwnPropertyDescriptor(trapTarget, key) {
return {
name: "proxy"
};
}
});
// throws error
var descriptor = Object.getOwnPropertyDescriptor(proxy, "name");
複製程式碼
因為name
不屬於descriptor
接受的範圍,所以引發了錯誤。這個限制可確保Object.getOwnPropertyDescriptor
返回的值始終具有可靠的結構,無論代理使用什麼。
Duplicate Descriptor Methods
和上面的一些trap
類似,這個也有一些讓人為之困惑的類似的方法。這裡的是Object.defineProperty&Object. getOwnPropertyDescriptor
和Reflect. defineProperty&Reflect.getOwnPropertyDescriptor
.
defineProperty() Methods
看看這個方法的異同.
Object.defineProperty
和Reflect.defineProperty
方法完全相同,只是它們的返回值有所不同。
var target = {};
var result1 = Object.defineProperty(target, "name", { value: "target "});
console.log(target === result1); // true
var result2 = Reflect.defineProperty(target, "name", { value: "reflect" });
console.log(result2); // true
複製程式碼
可以發現,Object.defineProperty
返回的是傳入的第一個引數,Reflect.defineProperty
返回的布林值確定是否成功。
getOwnPropertyDescriptor() Methods
Object.getOwnPropertyDescriptor
方法傳入的引數是原始值的時候,會轉換成物件進行處理。至於Reflect.getOwnPropertyDescriptor
傳入的不是物件,會丟擲錯誤:
descriptor1 = Object.getOwnPropertyDescriptor(2, "name");
console.log(descriptor1); // undefined
// throws an error
descriptor2 = Reflect.getOwnPropertyDescriptor(2, "name");
複製程式碼
The ownKeys Trap
ownKeys trap
允許你攔截內部的方法[[OwnPropertyKeys]]
並覆蓋預設的行為通過返回一組值。返回的這個陣列值用於四個方法:Object.getOwnPropertyNames
, Object.keys
,Object.getOwnPropertySymbols()
和Object.assign
(Object.assign
用於陣列來確定要複製的屬性)。
ownKeys trap
的預設行為是通過Reflect.ownKeys
來實現,返回的就是一個數組,裡面包含所有的屬性keys(strings, symbols)
. 我們知道Object.keys
和Object.getOwnPropertyNames
返回的是過濾掉symbol key
的集合,但是Object.getOwnPropertySymbols
卻是相反,所以ownKeys
集合了這幾個之後,就可以返回所有的keys
.並且Object.assign
作用於strings
和symbols
鍵的物件。
ownKeys trap
接收一個引數,就是trapTarget
。他總是返回陣列或者類似陣列的值,否則會引發錯誤。
看下面這個例子:
var proxy = new Proxy({}, {
ownKeys(trapTarget) {
return Reflect.ownKeys(trapTarget).filter(key => {
return typeof key !== "string" || key[0] !== "_";
});
}
});
var nameSymbol = Symbol("name");
proxy.name = "proxy";
proxy._name = "private";
proxy[nameSymbol] = "symbol";
var 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 trap
也會影響for-in
迴圈,該迴圈呼叫trap
來確定在迴圈內使用哪些鍵。
Function Proxies with the apply and construct Traps
這個可能是比較特殊的了。在代理的所有的trap
中,只有apply trap
和construct trap
這兩個要求代理的target
是必須一個function
,我們知道 function
有兩個內部的屬性[[Call]]
和[[Construct]]
分別用於直接呼叫和new
關鍵字呼叫的時候。因此apply trap
在攔截直接呼叫的時候用到的,construct trap
是攔截new
呼叫時候用到的。
我們先來看看直接呼叫的的時候,
apply trap
的引數:trapTarget
thisArg
- 呼叫期間的上下文物件this
argumentsList
- 傳遞到方法的陣列引數
再來看看new
關鍵字呼叫時候。
construct trap
的引數trapTarget
argumentsList
Reflect.construct
方法也接受這兩個引數,並有一個名為newTarget
的可選第三個引數。如果給定這個第三個引數,newTarget
這個引數就是new.target
的值。
使用apply
和construct
兩個trap
就可以攔截所有的方法呼叫.
var target = function() { return 42 },
var proxy = new Proxy(target, {
apply: function(trapTarget, thisArg, argumentList) {
return Reflect.apply(trapTarget, thisArg, argumentList);
},
construct: function(trapTarget, argumentList) {
return Reflect.construct(trapTarget, argumentList);
}
});
// a proxy with a function as its target looks like a function
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
複製程式碼
這個和上面幾個類似,都是攔截之後使用它的預設行為。
Validating Function Parameters
下面來一個驗證引數型別的例子:
// adds together all arguments
function sum(...values) {
return values.reduce((previous, current) => previous + current, 0);
}
var sumProxy = new Proxy(sum, {
apply(trapTarget, thisArg, argumentList) {
argumentList.forEach((arg) => {
if (typeof arg !== "number") {
throw new TypeError("All arguments must be numbers.");
}
});
return Reflect.apply(trapTarget, thisArg, argumentList);
},
construct(trapTarget, argumentList) {
throw new TypeError("This function can't be called with new.");
}
});
console.log(sumProxy(1, 2, 3, 4)); // 10
// throws error
console.log(sumProxy(1, "2", 3, 4));
// also throws error
var result = new sumProxy();
複製程式碼
在這裡,我們對引數進行了過濾處理,並且在new
呼叫的時候,直接丟擲錯誤,不讓他去new
。
Calling Constructors Without new
我們之前介紹了關於new
的相關介紹,判斷一個函式是不是new
呼叫,需要使用new.target
來判斷。
function Numbers(...values) {
if (typeof new.target === "undefined") {
throw new TypeError("This function must be called with new.");
}
this.values = values;
}
let instance = new Numbers(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
// throws error
Numbers(1, 2, 3, 4);
複製程式碼
可以發現,這個類似於在上面提到的使用proxy
驗證,但是這個明顯更加方便一點。如果只是為了判斷是否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]
複製程式碼
可以發現這個,在函式內部還是有檢查,但是在表面呼叫的時候是沒有使用這個new
的,只是我們在代理裡的apply trap
裡使用了Reflect.construct
。
Overriding Abstract Base Class Constructors
可以在Reflect.construct
內傳入第三個引數,用作new.target
的值。這可以在建構函式中檢查new.target
的值的時候用到。
class AbstractNumbers {
constructor(...values) {
if (new.target === AbstractNumbers) {
throw new TypeError("This function must be inherited from.");
}
this.values = values;
}
}
class Numbers extends AbstractNumbers {}
let instance = new Numbers(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
// throws error
new AbstractNumbers(1, 2, 3, 4);
複製程式碼
上面可以發現有個限制,下面我們來試試使用代理來跳過.
class AbstractNumbers {
constructor(...values) {
if (new.target === AbstractNumbers) {
throw new TypeError("This function must be inherited from.");
}
this.values = values;
}
}
var AbstractNumbersProxy = new Proxy(AbstractNumbers, {
construct: function(trapTarget, argumentList) {
return Reflect.construct(trapTarget, argumentList, function() {});
}
});
let instance = new AbstractNumbersProxy(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
複製程式碼
這樣,添加了第三個引數,這樣new.target
的值就是一個另外一個值了(匿名函式)。
Callable Class Constructors
我們知道class
只能被new
去呼叫,這是因為在建構函式的內部方法,[[Call]]
被指定丟擲錯誤。但是我們使用代理可以攔截這個內部屬性,所以可以改變我們的呼叫方法。
例如我們想不通過new
來呼叫一個class
,可以通過代理,如下:
class Person {
constructor(name) {
this.name = name;
}
}
var PersonProxy = new Proxy(Person, {
apply: function(trapTarget, thisArg, argumentList) {
return new trapTarget(...argumentList);
}
});
var me = PersonProxy("Nicholas");
console.log(me.name); // "Nicholas"
console.log(me instanceof Person); // true
console.log(me instanceof PersonProxy); // true
複製程式碼
可以發現,我們在apply
這個trap
對他進行了new
一個。
Revocable Proxies
通常情況下,綁定了代理之後都是沒有辦法撤掉的,但是這個可以取消,通過Proxy.revocable
去取消。這個方法和Proxy
的建構函式傳參類似,一個target
和一個handler
.
他返回的物件是有兩個屬性:
proxy
- 被撤銷的代理物件revoke
- 呼叫撤銷代理的函式
當revoke
被呼叫的時候,就不能繼續使用代理了。
var target = {
name: "target"
};
var { proxy, revoke } = Proxy.revocable(target, {});
console.log(proxy.name); // "target"
revoke();
// throws error
console.log(proxy.name);
複製程式碼
可以發現,在呼叫revoke
方法之後,代理就不能繼續使用了。如果呼叫,就會丟擲錯誤,不會返回undefined
。
Solving the Array Problem
看一個關於陣列的問題:
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"
複製程式碼
在這裡,length
控制著陣列的資料,一般情況下,我們沒法子修改這些高階操作。
Detecting Array Indices
ECMAScript 6
規範提供了有關如何確定屬性鍵是否為陣列索引的說明:
當且僅當
ToString(ToUint32(P))
等於P
且ToUint32(p)
不等於2的32次方減1
時,字串屬性名P
才是陣列索引。
這個規範,在js
中可以實現:
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);
}
複製程式碼
toUint32
函式使用規範中描述的演算法將給定值轉換為無符號的32
位整數,isArrayIndex
函式首先將金鑰轉換為uint32
,然後執行比較以確定金鑰是否為陣列索引。
Increasing length when Adding New Elements
可以發現數組的行為,其實使用set trap
就可以完成這兩個行為。
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);
}
function createMyArray(length=0) {
return new Proxy({ length }, {
set(trapTarget, key, value) {
let currentLength = Reflect.get(trapTarget, "length");
// the special case
if (isArrayIndex(key)) {
let numericKey = Number(key);
if (numericKey >= currentLength) {
Reflect.set(trapTarget, "length", numericKey + 1);
}
}
// always do this regardless of key type
return Reflect.set(trapTarget, key, value);
}
});
}
var colors = createMyArray(3);
console.log(colors.length); // 3
colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
console.log(colors.length); // 3
colors[3] = "black";
console.log(colors.length); // 4
console.log(colors[3]); // "black"
複製程式碼
可以發現,上面對寫入的key
進行了驗證。如果符合規範,則會給length
進行新增操作。其他的會一直操作key
. 現在,基於陣列的length的第一個功能成立了,接下來是進行第二步。
Deleting Elements on Reducing length
這裡就需要對減少的長度的部分進行刪除了。
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);
}
function createMyArray(length=0) {
return new Proxy({ length }, {
set(trapTarget, key, value) {
let currentLength = Reflect.get(trapTarget, "length");
// the special case
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);
}
}
}
// always do this regardless of key type
return Reflect.set(trapTarget, key, value);
}
});
}
let colors = createMyArray(3);
console.log(colors.length); // 3
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"
複製程式碼
可以發現,我們在每次length
操作的時候,都會進行一次監聽操作,用來減去他刪除的部分。
Implementing the MyArray Class
使用代理建立類的最簡單方法是像往常一樣定義類,然後從建構函式返回代理。這樣,例項化類時返回的物件將是代理而不是例項。(例項是建構函式內部的this
值)。例項成為代理的目標,並返回代理,就好像它是例項一樣。那麼這個例項將完全私有,無法直接訪問它,但可以通過代理間接訪問它。 看一個簡單的例子:
class Thing {
constructor() {
return new Proxy(this, {});
}
}
var myThing = new Thing();
console.log(myThing instanceof Thing); // true
複製程式碼
我們知道,constructor
內返回的基本資料型別不會影響他的返回,如果是非基本型別,那麼就是具體的返回物件了。所以這裡返回到是proxy
,因此這裡的這個myThing
就是這個proxy
. 由於代理會把他的行為傳遞給他的目標,因此myThing
仍然被當做是Thing
的例項。
考慮到上面這一點,使用代理建立自定義陣列類相對簡單點。程式碼與“刪除減少長度的元素”部分中的程式碼大致相同。使用相同的代理程式碼,但這一次,它在類建構函式中。
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");
// the special case
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; i\
ndex--) {
Reflect.deleteProperty(trapTarget, index);
}
}
}
// always do this regardless of key type
return Reflect.set(trapTarget, key, value);
}
});
}
}
let colors = new MyArray(3);
console.log(colors instanceof MyArray); // true
console.log(colors.length); // 3
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"
複製程式碼
這就是利用我們上面提到的那點,返回的最終是個代理來完成這個操作。
儘管這樣很容易,但是他為每一個新的例項都建立了一個代理。但是我們可以為每一個例項都共享一個代理,那就是通過原型。
Using a Proxy as a Prototype
代理可以用作原型,但是這樣會提高複雜度,比上面的實現還要複雜。當代理是原型時,僅當預設操作通常繼續到原型時才會呼叫代理trap
,這會將代理的功能限制為原型。如下:
var target = {};
var newTarget = Object.create(new Proxy(target, {
// never called
defineProperty(trapTarget, name, descriptor) {
// would cause an error if called
return false;
}
}));
Object.defineProperty(newTarget, "name", {
value: "newTarget"
});
console.log(newTarget.name); // "newTarget"
console.log(newTarget.hasOwnProperty("name")); // true
複製程式碼
newTarget
代理是作為一個原型物件被建立。現在,只有在newTarget
上的操作並將操作傳遞到目標(target
)上時,這樣才會呼叫代理trap
.
defineProperty
在newTarget
的基礎上建立了自己的屬性name
,在物件上定義屬性,不會作用到原型, 可以看下原型的影子方法,並且不會呼叫代理的defineProperty trap
,只會把這個name
屬性新增到自己的屬性裡。
雖然代理在用作原型時受到嚴重限制,但仍有一些陷阱仍然有用。
Using the get Trap on a Prototype
我們知道原型鏈的查詢是現在自己的屬性裡查詢,如果找不到會遍歷原型鏈。因此,只需要給代理設定一個get trap
,當查詢的屬性不存在的時候,就會觸發原型上的trap
。
var target = {};
var thing = Object.create(new Proxy(target, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
}));
thing.name = "thing";
console.log(thing.name); // "thing"
// throw an error
var unknown = thing.unknown;
複製程式碼
可以發現,使用代理作為原型建立thing
物件。當呼叫不存在的時候,會丟擲錯誤,如果存在,便不會遍歷到原型,所以不會出錯。
在這個例子中,要理解trapTarget
和receiver
是不同的物件。當代理當做原型使用時,trapTarget
是原型物件本身,receiver
是例項物件。在上例中,trapTarget
等同於target
, receiver
等同於thing
Using the set Trap on a Prototype
這個比較麻煩,如果賦值操作繼續到原型,觸發這個trap
,他會根據引數情況確定是在原型上或者是在當前例項上建立屬性,他的預設情況就和我們上面說的影子方法一樣。這裡可能有些繞,可以看看下面這個例子:
var target = {};
var thing = Object.create(new Proxy(target, {
set(trapTarget, key, value, receiver) {
return Reflect.set(trapTarget, key, value, receiver);
}
}));
console.log(thing.hasOwnProperty("name")); // false
// triggers the `set` proxy trap
thing.name = "thing";
console.log(thing.name); // "thing"
console.log(thing.hasOwnProperty("name")); // true
// does not trigger the `set` proxy trap
thing.name = "boo";
console.log(thing.name); // "boo"
複製程式碼
在這個例子中,target
沒有自己的屬性。 thing
物件有一個代理作為其原型,它定義了一個set trap
來捕獲任何新屬性的建立。當thing.name
被賦值為“thing”
作為其值時,將呼叫代理set trap
,因為thing
沒有自己的name
屬性。在這個set trap
裡,trapTarget
等於target
,receiver
等於thing
。該操作最終在thing
上建立一個新屬性,幸運的是,如果你將receiver
作為第四個引數傳入,Reflect.set
會為你實現這個預設行為。
如果不傳遞這個第四個receiver
引數呢,那麼就會在原型物件上(target)
建立屬性, 不會在例項上建立屬性,那麼就導致每次set
都去原型操作;如果傳遞之後,那麼在設定過一次就不會去再次觸發原型上的set trap
.
Proxies as Prototypes on Classes
類不可以直接修改原型做代理,因為prototype
屬性是不可寫的。
'use strict'
class X {}
X.prototype = new Proxy({}, {
get(trapTarget, key, receiver){
console.log('class prototype proxy')
}
})
// Cannot assign to read only property 'prototype' of function 'class X {}'
複製程式碼
但是,可以建立一個通過使用繼承將代理作為其原型的類。首先,需要使用建構函式建立ES5
樣式型別定義。然後,用原型覆蓋為代理。
function NoSuchProperty() {
// empty
}
NoSuchProperty.prototype = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
var thing = new NoSuchProperty();
// throws error due to `get` proxy trap
var result = thing.name;
複製程式碼
函式的prototype
屬性沒有限制,因此可以使用代理覆蓋它。
接下來就是建立一個類去繼承這個函式。
function NoSuchProperty() {
// empty
}
NoSuchProperty.prototype = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
class Square extends NoSuchProperty {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
}
var shape = new Square(2, 6);
var area1 = shape.length * shape.width;
console.log(area1); // 12
// throws an error because "anotherWidth" doesn't exist
var area2 = shape.length * shape.anotherWidth;
複製程式碼
這樣,就很好的在原型上使用了代理,一個折中的法子來實現。
我們來該寫下,這樣可能會更直觀:
function NoSuchProperty() {
// empty
}
// store a reference to the proxy that will be the prototype
var proxy = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
NoSuchProperty.prototype = proxy;
class Square extends NoSuchProperty {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
}
var shape = new Square(2, 6);
var shapeProto = Object.getPrototypeOf(shape);
console.log(shapeProto === proxy); // false
var secondLevelProto = Object.getPrototypeOf(shapeProto);
console.log(secondLevelProto === proxy); // true
複製程式碼
這裡,把代理存在變數中,更加直觀。在這裡shape
的原型是Square.prototype
,不是proxy
。但是Square.prototype
的原型是代理,因為他繼承自NoSuchProperty
。
作者:xiaohesong
連結:https://juejin.im/post/5baf3c865188255c64190886
來源:掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。