原文: Javascript Proxies: Real World Use Case -- Arbaz Siddiqui
譯者注, 為了防止出現"魯棒性"這種因翻譯習慣差異導致的混淆, 文中部分術語將不會進行翻譯.
Proxy介紹
在程式設計術語範疇中, Proxy指的是幫助/替代另一個實體(Entity)完成一系列操作的實體. 一個架設在客戶端與服務端之間的Proxy伺服器分別充當了客戶端的服務端和服務端的客戶端. 對於Proxy來說, 它們的任務就是介入收到的請求/呼叫, 並在處理後傳遞給其上游. 這些介入允許Proxy新增一些額外的業務邏輯或者改變整個操作的行為.
JavaScript的Proxy從某種意義上來說是相似的. 它處在程式碼所操作的物件與實際被操作的物件之間進行處理.
根據MDN Web文件
The Proxy is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).
Proxy被用來自定義一些基礎層面的操作(例如屬性查詢, 賦值, 列舉, 函式呼叫等)
術語
在完成一個Proxy的使用之前, 有三個術語需要我們提前進行了解:
Target(目標)
Target就是實際被Proxy操作修改的物件. 它可以是任何一個JavaScript物件.
Traps(阱)
譯者注: 這個地方的翻譯說實話有點不太好翻譯, 但實際上只要能夠理解所謂Traps就是用來過載(代理)Target對應的名字的屬性/方法的屬性/方法就行
Traps是指那些在Target的屬性或者方法被呼叫時會介入干涉的方法. 有許多定義了的Traps可以被實現(implement)
Handler(處理器)
Handler是一個所有的Traps生存的佔位物件. 簡單來說, 可以把它當做一個存放且實現各個traps的物件.
我們來看看下面這個例子:
//movie is a target
const movie = {
name: "Pulp Fiction",
director: "Quentin Tarantino"
};
//this is a handler
const handler = {
//get is a trap
get: (target, prop) => {
if (prop === 'director') {
return 'God'
}
return target[prop]
},
set: function (target, prop, value) {
if (prop === 'actor') {
target[prop] = 'John Travolta'
} else {
target[prop] = value
}
}
};
const movieProxy = new Proxy(movie, handler);
console.log(movieProxy.director); //God
movieProxy.actor = "Tim Roth";
movieProxy.actress = "Uma Thurman";
console.log(movieProxy.actor); //John Travolta
console.log(movieProxy.actress); //Uma Thurman
複製程式碼
輸出如下
God
John Travolta
Uma Thurman
複製程式碼
上面這個例子中, movie
就是我們所說的Target. 我們實現了一個擁有set
和get
這兩個trap的handler
. 在其中我們新增了兩個邏輯: 在訪問director
時, get
這個trap會直接返回God
而不是它實際的值; 在對actor
賦值時, set
這個trap會干涉所有的賦值操作, 並在鍵為actor
時將值改變成John Travlota
.
真實的案例
雖然並不如其他的ES2015的特性那樣廣為人知, Proxy還是有諸如所有屬性的預設值這樣的現在看來挺亮眼的用例. 讓我們來看看其他的在真實生產環場景中能夠利用Proxy的地方.
驗證 Validation
既然我們已經可以干涉物件的屬性賦值過程, 那麼我們可以藉此來校驗我們將要賦予給物件屬性的值. 看下面這個例子
const handler = {
set: function (target, prop, value) {
const houses = ['Stark', 'Lannister'];
if (prop === 'house' && !(houses.includes(value))) {
throw new Error(`House ${value} does not belong to allowed ${houses}`)
}
target[prop] = value
}
};
const gotCharacter = new Proxy({}, handler);
gotCharacter.name = "Jamie";
gotCharacter.house = "Lannister";
console.log(gotCharacter);
gotCharacter.name = "Oberyn";
gotCharacter.house = "Martell";
複製程式碼
執行結果如下:
{ name: 'Jamie', house: 'Lannister' }
Error: House Martell does not belong to allowed Stark,Lannister
複製程式碼
上面這個例子中, 我們嚴格限制了house
這個屬性所能被賦予的值的範圍. 只需要建立一個set
的trap, 我們甚至能用這個實現方式來實現一個只讀的物件.
副作用 Side Effects
我們可以通過Proxy來建立一個在讀寫屬性時的副作用. 出發點在於某些特定的屬性被訪問或者寫入時觸發一些函式. 看下面這個例子:
const sendEmail = () => {
console.log("sending email after task completion")
};
const handler = {
set: function (target, prop, value) {
if (prop === 'status' && value === 'complete') {
sendEmail()
}
target[prop] = value
}
};
const tasks = new Proxy({}, handler);
tasks.status = "complete";
複製程式碼
執行結果如下:
sending email after task completion
複製程式碼
這裡我們干涉了status
這個屬性的寫入. 當寫入的值是complete
時, 會觸發一個副作用函式. 在Sindre Sorhus的on-change這個包中就一個很Cooooooool的實現.
快取 Caching
利用介入干涉物件屬性讀寫的能力, 我們能夠建立一個基於記憶體的快取. 它只會在值過期前返回值. 看下面這個例子:
const cacheTarget = (target, ttl = 60) => {
const CREATED_AT = Date.now();
const isExpired = () => (Date.now() - CREATED_AT) > (ttl * 1000);
const handler = {
get: (target, prop) => isExpired() ? undefined : target[prop]
};
return new Proxy(target, handler)
};
const cache = cacheTarget({age: 25}, 5);
console.log(cache.age);
setTimeout(() => {
console.log(cache.age)
}, 6 * 1000);
複製程式碼
執行結果如下:
25
undefined
複製程式碼
這裡我們建立了一個函式, 並返回一個Proxy. 在獲取target的屬性前, 這個Proxy的handler首先會檢查target物件是否過期. 基於此, 我們可以針對每個鍵值都設定一個基於TTLs或者其他機制的過期檢查.
缺點
雖然Proxy具備一些很神奇的功能, 但在使用時仍然具有一些不得不小心應對的限制:
- 效能會受到顯著的影響. 在注重效能的程式碼中應該避免對Proxy的使用
- 沒有辦法區分判斷一個物件是一個Proxy的物件或者是target的物件
- Proxy可能會導致程式碼在可讀性上面出現問題
總結
Proxy很強, 在很大範圍內都能夠得到應用, 或者被濫用. 這篇文章中我們討論了什麼是Proxy, 如何實現一個Proxy, 幾個真實案例中的用例, 以及它的缺陷限制.