下面是我們要學(xué)習(xí)一些的知識:
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
第一個例子稍微有點復(fù)雜。我將在后面給出詳細(xì)解釋?,F(xiàn)在,來檢查一下剛才創(chuàng)建的對象:
> obj.count = 1;
setting count!
> ++obj.count;
getting count!
setting count!
2
執(zhí)行上面的代碼將會發(fā)生什么? 我門攔截了對象的屬性訪問過程,重載了 “.” 操作符。
最好的方式是虛擬化。這是一個用來實現(xiàn)驚人的事情的非常通用的技術(shù)。下面給出它是如何工作的。
取一張圖片。
http://wiki.jikexueyuan.com/project/es-six-deeply/images/power-plant.jpg" alt="Alt text" />
在圖片內(nèi)部畫一個輪廓。
http://wiki.jikexueyuan.com/project/es-six-deeply/images/power-plant-with-outline.png" alt="Alt text" />
現(xiàn)在可以替換輪廓內(nèi)部的圖像,也可以替換輪廓外部的圖像。用你完全意向不到的圖片來替換。這里只有一個規(guī)則:向后兼容規(guī)則。在你替換后,要保證其余的人看到替換后的圖不會覺得這幅圖有什么改變。
http://wiki.jikexueyuan.com/project/es-six-deeply/images/wind-farm.png" alt="Alt text" />
你可能從像 《The Truman Show》 或 《The Matrix》 這類關(guān)于計算機科學(xué)的經(jīng)典電影作品中看到過類似的 hack 方法,即把把一個人放到一個盒子里,而他周圍的世界都被一個精心設(shè)計的假像所替代。
為了滿足后向兼容規(guī)則,你需要巧妙地設(shè)計你的替換方法。但是關(guān)鍵的部分是如何正確畫出輪廓。
這里所謂的輪廓是指一個 API 的邊界。一個接口定義了兩部分的代碼,一是如何交互,另一個是一部分的代碼對另一部分的期望。所以一個系統(tǒng)接口的設(shè)計,就像是在圖中畫一個合適的輪廓一樣。你知道你可以替換任意輪廓內(nèi)外的圖像,但是其余未被替換的圖像并不在意這種替換。
當(dāng)系統(tǒng)中沒有接口的時候,你可以創(chuàng)建一個接口。有史以來最酷炫的黑客軟件涉及繪制 API 邊界,并把接口植入已有的大工程中。
虛擬存儲,硬件虛擬化, Docker, Valgrind, rr —— 這些項目不同程度地都會將新的接口引入到原有系統(tǒng)中。在某些情況下,要想這些新的接口能很好的工作甚至需要花費數(shù)年的時間,還需要操作系統(tǒng)新特性的支持,甚至需要新的硬件的支持。
ES6 引入了虛擬化來支持 javascript 最核心的概念:對象。
讓我們來回想一下,我們是什么時候接觸對象的。

這個問題對于我來說太難了!我到現(xiàn)在都還沒有聽到一個滿意的定義。
很吃驚嗎?定義基礎(chǔ)概念總是很困難的—— 就像定義歐幾里得中的最初定義。 ECMAScript 是有良好規(guī)范的語言,它將對象定義為“類型對象的成員”。
稍后,規(guī)范又添加了“一個對象是屬性的集合”來定義對象。這并不是壞事。如果你想有一個定義,那么這就是一個定義。稍后我們還會來講解。
我之前說關(guān)于寫 API 的一些事,你必須理解它。因此,在某種程度上,我已經(jīng)向大家承諾如果我們學(xué)習(xí)這些知識將有助于對于對象這個概念更加了解。
所以讓我們來跟著 ECMAScript 標(biāo)準(zhǔn)協(xié)會的腳步來學(xué)習(xí)如何定義一個 API ,一個接口和 JavaScript 對象。我們需要什么樣的方法?對象能做什么?
這取決于對象。 DOM 元素對象可以實現(xiàn)某些功能;AudioNode 對象可以實現(xiàn)其他的功能。但是所有的對象都可以共享一些基礎(chǔ)功能:
JS 程序中大部分處理對象都用到了屬性,原型和函數(shù)。甚至是一個單元或者一個 AudioNode 對象都是通過調(diào)用繼承屬性的方法來訪問。
所以當(dāng) ECMAScript 標(biāo)準(zhǔn)協(xié)會定義 14 個內(nèi)部方法,即所有對象的通用接口,毫無疑問,他們所有工作的重點最終都集中在這三種基礎(chǔ)構(gòu)建上:屬性,原型和方法。
關(guān)于定義的 14 個內(nèi)部方法詳細(xì)信息都在 ES6 標(biāo)準(zhǔn)版本的表格5和表格6當(dāng)中,參見:[ http://www.ecma-international.org/ecma-262/6.0/index.html#table-5]( http://www.ecma-international.org/ecma-262/6.0/index.html#table-5) 。這里我只對部分方法做解釋。 這個奇怪的雙層中括號 [[ ]] 表示這個方法是內(nèi)部方法,你不能像其他普通方法一樣調(diào)用,刪除,或者重寫。
obj.[[Get]](key, receiver) —— 獲得屬性值。
當(dāng)給出 obj.prop 或者 obj[key] 就能直接調(diào)用該方法。
obj 是當(dāng)前正被搜尋的對象;receiver 是我們正在搜尋屬性的對象。有時候我們需要搜巡一些對象。 obj 可能是 receiver’s 原型鏈上的一個對象。
obj.[[Set]](key, value, receiver) —— 分配對象屬性。
當(dāng)給出 obj.prop = value 或者 obj[key] = value 就能直接調(diào)用該方法。
在賦值式子中,像 obj.prop += 2,最先調(diào)用的是 [[Get]] 方法,接著調(diào)用 [[Set]] 方法。賦值操作同樣適用于 ++ 和 - 。
obj.[[HasProperty]](key) —— 測試是否有屬性存在。
當(dāng)給出 key in obj 就能直接調(diào)用該方法。
obj.[[Enumerate]]( ) —— 列出 obj 的可列舉屬性。
當(dāng)給出 for (key in obj) 就能直接調(diào)用該方法。
該方法返回一個可迭代的對象,通過 for-in 循環(huán)獲得對象的屬性名稱。
obj.[[GetPrototypeOf]]( ) —— 返回對象的原型。
當(dāng)給出 obj.proto 或者 Object.getPrototypeOf(obj) 就能直接調(diào)用該方法。
functionObj.[[Call]](thisValue, arguments) —— 調(diào)用方法。
當(dāng)給出 functionObj( ) 或者 x.method( ) 就能直接調(diào)用該方法。
該方法是可選的,并不是所有的對象都有方法。
constructorObj.[[Construct]](arguments, newTarget) —— 調(diào)用構(gòu)造器。
當(dāng)給出 new Date(2890, 6, 2) 類似的 JS 代碼就能直接調(diào)用該方法。
該方法是可選的,并不是所有的對象都有此方法。
newTarget 參數(shù)在子類中有發(fā)揮作用。我們將在未來的章節(jié)進(jìn)行討論。
或許你可以猜想其余七種方法是什么樣。
整個 ES6 標(biāo)準(zhǔn),任何一點語法或者內(nèi)置函數(shù)關(guān)于對象的操作都依據(jù)這 14 個內(nèi)部方法。 ES6 給對象的大腦描繪出一個清晰的輪廓。而代理所做的事就是用任意的 JS 代碼代替這個標(biāo)準(zhǔn)的大腦。
當(dāng)我們開始討論重寫這些內(nèi)部方法的時候,請記住,我們是在討論重寫核心語法的行為,像 obj.prop ,內(nèi)置函數(shù)像 Object.keys( ) , 等等。
ES6 定義了新的全局構(gòu)造器 Proxy 。Proxy 需要兩個參數(shù):一個目標(biāo)對象和一個處理對象。下面給出關(guān)于代理 Proxy 的一個簡單例子:
var target = {}, handler = {};
var proxy = new Proxy(target, handler);
讓我們先把 handler 對象放在一邊,這會主要關(guān)注如何 Proxy 和 target 的相關(guān)知識。
我可以告訴你在一個句子中如何實現(xiàn)代理。所有代理的內(nèi)部方法都將會轉(zhuǎn)發(fā)給 target 。那就是,所謂的代理。也就是當(dāng)調(diào)用 proxy. [[Enumerate]]( ) , 它將會返回一個 target.[[Enumerate]]( )對象 。
讓我們來試試。我們將來試試代理操作。 [[Set]]( ) 方法將會被調(diào)用。
proxy.color = "pink";
OK , 接下來什么會發(fā)生? proxy.[[Set]]( ) 會調(diào)用 target.[[Set]]( ), 所以,在 target 對象上也應(yīng)該有新的屬性,是這樣么?
> target.color
"pink"
果真是這樣。對于其他的內(nèi)部方法效果也是一樣的。代理所作的就是在 target 對象中實現(xiàn)同樣的方法。
上面的例子給我們一個幻覺是代理對象 proxy 和目標(biāo)對象 target 是一樣的。但實際上并非如此: proxy !== target 。并且有時候 proxy 會做一些類型檢查處理,而 target 不會。舉個例子,即使 proxy 的目標(biāo)是一個 DOM 元素, proxy 不會是一個真的 DOM 元素,所以像 document.body.appendChild(proxy) 代碼會因為類型檢查失敗而無法執(zhí)行。
現(xiàn)在讓我們返回到處理程序?qū)ο笊?。這就是使 proxy 變得有用的部分。
處理對象的方法可以重載代理 proxy 的任意內(nèi)部方法。
舉個例子,如果你想攔截所有試圖給對象屬性賦值的嘗試,你可以通過定義 handler.set( ) 來解決。
var target = {};
var handler = {
set: function (target, key, value, receiver) {
throw new Error("Please don't set properties on this object.");
}
};
var proxy = new Proxy(target, handler);
> proxy.name = "angelina";
Error: Please don't set properties on this object.
代理的所有處理函數(shù)列表可參見:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#Methods_of_the_handler_object 。這里有 14 種方法,這 14 種內(nèi)部方法都在 ES6 中有詳細(xì)解釋。
所有的處理程序方法都是可選的。如果一個內(nèi)部的方法沒有被處理程序截獲,那么它就會把信息轉(zhuǎn)發(fā)給 target , 就像我們之前看到的例子。
我們已經(jīng)知道代理 proxy 試圖來做一些神奇的事,但是有些事是代理不可能完成的。
這里給出第一個例子。函數(shù) Tree( ) 代碼見下:
> var tree = Tree();
> tree
{ }
> tree.branch1.branch2.twig = "green";
> tree
{ branch1: { branch2: { twig: "green" } } }
> tree.branch1.branch3.twig = "yellow";
{ branch1: { branch2: { twig: "green" },
branch3: { twig: "yellow" }}}
注意到內(nèi)部對象 branch1 ,branch2,和 branch3 是如何在需要的時候神奇般地被創(chuàng)建的。 很方便,對嗎?它怎么可能工作呢?
直到現(xiàn)在,上面代碼都無法工作。但是代理只用幾行代碼就能搬到。我們只需要利用 tree.[[Get]]( ) 。如果你是喜歡挑戰(zhàn)類型的人,那么你可能在閱讀 tree.[[Get]]( ) 源碼之前,想試著實現(xiàn)它。
http://wiki.jikexueyuan.com/project/es-six-deeply/images/maple-tap.jpg" alt="Alt text" />
下面給出我的解決方案:
function Tree() {
return new Proxy({}, handler);
}
var handler = {
get: function (target, key, receiver) {
if (!(key in target)) {
target[key] = Tree(); // auto-create a sub-Tree
}
return Reflect.get(target, key, receiver);
}
};
注意到最后調(diào)用的 Reflect.get( ) 函數(shù)。在代理 proxy 處理程序中,使用被代理對象的默認(rèn)方法被證明是一種非常普遍的需求。 因此 ES6 定義了一個新的 Reflect object 對象,該對象包含了默認(rèn)的 14 種內(nèi)部方法,這樣你可以就可以實現(xiàn)上述目標(biāo)了。
我想我可能給讀者造成一個錯誤的印象,那就是代理 proxy 易于使用。讓我們來看看更多例子來確認(rèn)是不是這樣。
這次我們的賦值任務(wù)將會變得復(fù)雜:我們要實現(xiàn)一個函數(shù):readOnlyView(object) ,該函數(shù)以任何對象為輸入并且返回一個代理,代理的行為和被代理的對象一樣,只是讓被代理的對象變得不可修改。所以,舉個例子,它應(yīng)該像這樣:
> var newMath = readOnlyView(Math);
> newMath.min(54, 40);
40
> newMath.max = Math.min;
Error: can't modify read-only view
> delete newMath.sin;
Error: can't modify read-only view
我們怎樣能實現(xiàn)它呢?
第一步就是攔截所有的內(nèi)部方法,阻止內(nèi)部方法修改目標(biāo)對象。這個有五個。
function NOPE() {
throw new Error("can't modify read-only view");
}
var handler = {
// Override all five mutating methods.
set: NOPE,
defineProperty: NOPE,
deleteProperty: NOPE,
preventExtensions: NOPE,
setPrototypeOf: NOPE
};
function readOnlyView(target) {
return new Proxy(target, handler);
}
上面的代碼通過只讀視圖阻止賦值運算,屬性定義以及其他賦值行為。
這個方案有什么漏洞嗎?
最大的問題就是 [[Get]] 方法和其他方法可能導(dǎo)致可變的對象。因此雖然某個對象 x 是只讀的,但是 x.prop 可能是可修改的!這就是一個巨大的漏洞。
為了彌補這個漏洞,我們必須加入 handler.get( ) 方法:
var handler = {
...
// Wrap other results in read-only views.
get: function (target, key, receiver) {
// Start by just doing the default behavior.
var result = Reflect.get(target, key, receiver);
// Make sure not to return a mutable object!
if (Object(result) === result) {
// result is an object.
return readOnlyView(result);
}
// result is a primitive, so already immutable.
return result;
},
...
};
這樣還不夠。其他方法也需要上述代碼,包括 getPrototypeOf 和 getOwnPropertyDescriptor 。
接下來還有一些問題。當(dāng)通過這種形式的代理調(diào)用 getter 或者方法時,傳遞給 getter 或者方法的值實際上是代理本身。但是像早前我們看到那樣,很多訪問函數(shù)會執(zhí)行類型檢查,而代理對象卻不能通過這樣的類型檢查。在這種情況下,用目標(biāo)對象本身替換掉代理。你能弄清楚如何實現(xiàn)嗎?
這一部分的知識告訴我們創(chuàng)建一個代理對象是很容易的,但是創(chuàng)建一個直覺正確的代理確是非常困難。
代理真的有那么好嗎?
他們非常有用當(dāng)你想要觀察或者記錄對象的訪問。他們對于調(diào)試很方便。測試框架能用它們來模仿真實的對象。
代理是有用的,如果你需要的只是普通對象能做到行為:就像是填充屬性。
我討厭提到這個,但是看代碼運行哪一步最好的方式是用代理。。。具體來說,就是將代理處理程序?qū)ο蠓庋b到另一個代理中,該代理在每次處理程序方法的訪問的時候記錄輸出到控制臺。
代理可以用來限制對象的訪問,就像 readOnlyView 的行為一樣。這一類用法在應(yīng)用代碼中很少見,但是 Firefox 用代理內(nèi)部實現(xiàn)不同區(qū)域邊界的安全。并且代理是我們安全模型的關(guān)鍵部分。
代理 ? WeakMap
在我們 readOnlyView 的例子中,我們在每次對象訪問的時候都創(chuàng)建了一個新的代理。把每次創(chuàng)建的代理緩存在 WeakMap 中可以節(jié)省大量的空間,因為無論一個對象被傳遞給 readOnlyView 多少次都只會為其創(chuàng)建一個代理。
這是一用 WeakMap 的例子。
可撤銷的代理
ES6 還定義了其他函數(shù), Proxy.revocable(target, handler),該函數(shù)創(chuàng)建了一個代理,像 new Proxy(target, handler) ,不同之處在于該函數(shù)創(chuàng)建的代理可以撤銷。(Proxy.revocable 返回一個 .proxy 屬性和 .revoke 方法。)一旦對象被取消,它將不再工作;所有內(nèi)部方法都將消失。
對象不變性
在某些情況下, ES6 需要代理處理程序方法報告與目標(biāo)對象狀態(tài)一致的結(jié)果。這樣做的目的是保證所有對象的不變性,包括代理對象本身。舉個例子,一個代理不能聲明不可擴展,除非它的 target 對象也是不可擴展的。
不變性的規(guī)則太復(fù)雜了,這里我們就不具體闡述。但是你如果看到這樣的一條錯誤信息,像是 “proxy can't report a non-existent property as non-configurable” ,這就是原因。最可能的補救方法就是改變代理的聲明。另一種可能方法就是運行時修改目標(biāo)對象來匹配代理的聲明。
我認(rèn)為現(xiàn)在剩下的就是:“一個對象是屬性的集合。” “An Object is a collection of properties.”
在今天來看,我對于這個定義并不十分滿意,即便我們默認(rèn)把原型考慮在內(nèi),這也并不能讓我感到滿意。我認(rèn)為這個詞 “collection ” 太大了,基本沒有定義清楚代理到底是什么。然而代理的處理程序方法能夠做任何事。它們甚至能夠返回隨機結(jié)果。
ECMAScript 標(biāo)準(zhǔn)協(xié)會通過清明地說明對象可以做什么,標(biāo)準(zhǔn)化這些方法,以及將虛擬化作為頭等特征,已經(jīng)極大擴大了這個可能的范圍。
對象幾乎已經(jīng)是一切了。
或許可以用將 12 個必需的內(nèi)部方法作為對 “什么是一個對象?” 這個問題的回答。一個 JS 程序中的對象能實現(xiàn) [[Get]] 操作和 [[Set]] 操作,等等。
我們已經(jīng)對對象有更深的理解了嗎?我不大確定!我們做了令人驚奇的事嗎?是的。我們做了在 JS 以前從不可能的事。
不!只有 Firefox 支持代理,并沒有 polyfill 。所以,隨意嘗試代理吧!創(chuàng)建一個大廳的鏡子,似乎每個對象有成千上萬的副本,它們非常相似,調(diào)試它們幾乎是不可能的!但是現(xiàn)在是時候了。因為即使不成熟的代碼放到了線上環(huán)境也不會有任何的風(fēng)險。
代理最初在 2010 年由 Andreas Gal 實現(xiàn),并由 Blake Kaplan 進(jìn)行代碼評審。然后標(biāo)準(zhǔn)協(xié)會重新設(shè)計了特征。 Eddy Bruel 在 2012 實現(xiàn)餓了新的規(guī)格。
我實現(xiàn)了 Reflect ,由 Jeff Walden 進(jìn)行代碼評審。除了 Reflect.enumerate( ) 沒完成以外,Reflect 的所有內(nèi)容本周末都會出現(xiàn)在 Firefox Nightly 中。