[譯] JavaScript Proxy -- 一些真實的用例

騰訊IVWEB團隊發表於2020-03-26

原文: 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. 我們實現了一個擁有setget這兩個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 Sorhuson-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具備一些很神奇的功能, 但在使用時仍然具有一些不得不小心應對的限制:

  1. 效能會受到顯著的影響. 在注重效能的程式碼中應該避免對Proxy的使用
  2. 沒有辦法區分判斷一個物件是一個Proxy的物件或者是target的物件
  3. Proxy可能會導致程式碼在可讀性上面出現問題

總結

Proxy很強, 在很大範圍內都能夠得到應用, 或者被濫用. 這篇文章中我們討論了什麼是Proxy, 如何實現一個Proxy, 幾個真實案例中的用例, 以及它的缺陷限制.

相關文章