反射和代理的具體應用

原文發佈於 github.com/ta7sudan/no…, 如需轉載請保留原做者 @ta7sudan.javascript

ES6 爲咱們提供了許多新的 API, 其中我的以爲最有用的(之一)即是代理了. 代理和反射都被歸爲反射 API, 那什麼是反射? 根據 wiki 上的解釋.java

反射是指計算機程序在運行時(Run time)能夠訪問、檢測和修改它自己狀態或行爲的一種能力。git

因此廣義上來講, 並不是只有使用了 Proxy Reflect 相關的 API 才叫反射, 而是隻要在運行時訪問, 檢測和修改自身狀態和行爲的均可以認爲是用到了反射. 拿比較常見的 new 無關的構造函數來講, 咱們經常會這樣實現.es6

function Person() {
	var self = this instanceof Person ? this : Object.create(Person.prototype);
	return self;
}
複製代碼

像上面這樣, 咱們在運行時經過檢測 this 進而檢測是不是經過 new 調用的函數, 從而決定返回值, 這也算是反射.github

不少語言都提供了反射機制, 即便是彙編, 也可以在運行時修改自身的代碼(誰讓指令和數據是在一塊兒呢...不過即使不在一塊兒也是能夠的). 那反射和代理到底有什麼用?數組

有人認爲反射破壞了封裝, 可是它也帶來了更多的靈活性, 使得本來沒法實現或難以實現的事情變得很容易實現, 儘管有缺點, 但缺點是咱們能夠避免的(若是有人使用了反射來破壞封裝, 那說明他在使用的時候已經清楚這樣作的結果, 產生的後果也應當本身承擔, 而若是不是必要, 則大部分時候也不會用到反射, 不存在破壞封裝), 而帶來的好處相較缺點則是明顯划算的.函數

就 API 而言, 反射和代理用起來是很簡單的, 因此這裏就不提了. 下面以比較經常使用的 get trap 來講明代理的實際應用場景. 後文中的反射泛指反射 API, 即包含了 ProxyReflect, 並再也不區分.性能

考慮咱們有一個對象 obj, 對象具備一個 sayHello 方法, 咱們可能會這麼寫.優化

var obj = {};
obj.sayHello = function () {
	console.log('hello');
};
複製代碼

在初始化的時候便定義了 sayHello 方法, 但可能有時候咱們以爲這不必, 畢竟一個函數表達式也是有開銷的. 固然你能夠說咱們直接在字面量裏寫好 sayHello 不就好了, 爲何必定要用函數表達式? 這裏只是演示, 不用在乎細節, 總之咱們但願在運行時某一時刻再實例化這個 sayHello 方法, 而不是一開始就實例化它, 緣由多是應用啓動速度比較重要. 那咱們可能會這麼寫.ui

var obj = {};

setTimeout(() => {
	obj.sayHello = function () {
		console.log('hello');
	};
}, 3000);
複製代碼

如今咱們過了 3 秒才實例化了 sayHello 方法, 的確知足了咱們前面說的, 在運行時某一時刻實例化的需求, 至少咱們的啓動速度提高了那麼一點. 那假如如今咱們但願這個某一時刻不是 3 秒, 而是咱們調用 sayHello 的時候呢? 換句話說, 若是調用 sayHello 時它尚未實例化, 則咱們先實例化它, 再調用它.

Proxy 咱們能夠這樣寫.

var obj = {};

var pobj = new Proxy(obj, {
	get(target, key) {
		if (key === 'sayHello') {
			if (!target[key]) {
				target[key] = function () {
					console.log('hello');
				};
			}
			return target[key];
		}
	}
});

pobj.sayHello();
複製代碼

很好, 這樣咱們就實現了在調用時實例化, 而且只實例化一次 sayHello 以後不會重複實例化. 可是也有人會說, 這不就是一個 getter 嗎? 這種事情用 Object.defineProperty() 也能作到. 好比.

var obj = {};

Object.defineProperty(obj, 'sayHello', {
	get() {
		if (!this._sayHello) {
			this._sayHello = function () {
				console.log('hello');
			};
		}
		return this._sayHello;
	}
});

obj.sayHello();
複製代碼

的確, 從這個角度來看, 使用 Proxy 和使用 Object.defineProperty() 幾乎沒什麼區別. 而另外一方面是, 儘管這兩種方法是沒有在初始化的時候實例化 sayHello 而是把這一過程推遲到調用 sayHello 的時刻了, 可是使用 Proxy 要建立一個代理對象, 使用 Object.defineProperty() 也要執行一次函數調用, 它們的開銷可能比初始化時候使用一個函數表達式來得更大, 這有什麼意義?

get trap 不只僅是 getter

前面的例子中咱們遇到了兩個問題, 一個是 Object.defineProperty() 某種意義上也能完成 Proxy 同樣的功能, 那 Proxy 有什麼意義? get trap 有什麼意義? 另外一個是建立一個 Proxy 對象的開銷並不必定比使用一個函數表達式來得小, 這又有什麼意義?

爲了回答這兩個問題, 如今咱們考慮 obj 不只僅有一個 sayHello 方法, 它有成百上千個方法, 每一個方法打印了方法名. 使用 Proxy 的話, 咱們能夠這樣寫.

var obj = {};

var pobj = new Proxy(obj, {
	get(target, key) {
		if (!target[key]) {
			target[key] = function () {
				console.log(key);
			};
		}
		return target[key];
	}
});

pobj.sayHello();
pobj.sayGoodBye();
// sayHello
// sayGoodBye
複製代碼

依舊簡短. 那用 Object.defineProperty() 呢? 不可能實現, 而即使是肯定只有 100 個方法, 而且它們名字肯定, 也須要調用 100 次 Object.defineProperty(), 對於函數表達式來講, 也是同樣的. 而從開銷的角度來看呢? 這時候 Proxy 依然只建立了一個代理對象, 而即使是可使用 Object.defineProperty() 或函數表達式, 它們也要調用成百上千次.

當咱們使用 obj.xxx() 去調用一個 xxx() 方法時, obj 對象自己並不知道本身是否具備 xxx() 方法, 而反射就像是一面鏡子, 讓 obj 可以知道本身是否具備 xxx() 方法, 而且根據狀況作出對應的處理.

儘管咱們能夠在運行時經過 Object.defineProperty() 或函數表達式動態地爲 obj 對象添加方法, 但這是由於咱們知道 obj 在那個時候是否存在對應方法, 而不是 obj 自己知道本身當時是否存在對應方法. 換句話說, 咱們在使用對象的方法時, 老是要先知道方法名, 哪怕可以在運行時知道, 可是[知道]這個動做也必須發生在[方法調用]這個動做以前. 這就致使了一些現實問題難以被優雅地解決.

好比前面的 obj 對象是咱們暴露的 API, 給用戶使用, 它的方法都是按需實例化的. 若是沒有 Proxy, 則用戶何時調用 obj 的方法咱們是不知道的, 因此[知道]這一動做是不可能在[方法調用]以前, 咱們也就沒辦法按需實例化. 固然用戶是可以在[方法調用]以前[知道]何時會有方法調用的, 但咱們不可能讓用戶本身來實例化方法.

從編譯器角度來看, Proxy 是攔截了對象全部屬性的右值查詢, 而 Object.defineProperty() 則只是攔截了特定屬性的右值查詢, 這意味着 Object.defineProperty() 必須知道屬性名這一信息, 而 Proxy 則不須要知道.

前置代理和後置代理

大部分時候咱們使用的都是前置代理, 即咱們把直接和代理對象進行交互(全部操做都發生在代理對象身上)的方式叫作前置代理. 那什麼是後置代理? 看代碼.

var pobj = new Proxy({}, {
	get(target, key) {
		if (!target[key]) {
			target[key] = function () {
				console.log(key);
			};
		}
		return target[key];
	}
});

var obj = Object.create(pobj);
obj.sayHello();
obj.sayGoodBye();
複製代碼

藉助原型鏈機制, 咱們直接和 obj 進行交互而不是和代理對象進行交互, 只有當 obj 不存在對應方法時纔會經過原型鏈去查找代理對象.

能夠看出來的是, 對於本來存在於目標對象(target)上的屬性, 使用代理前置開銷更大, 由於明明已經具備對應屬性了卻還要通過一次代理對象, 而使用代理後置開銷更小. 對於那些不存在的屬性, 使用後置代理開銷更大, 由於不只要通過原型鏈查找還要通過一次代理對象, 而使用前置代理只須要通過一次代理對象. 固然也可能引擎有特殊的優化技巧使得這種性能差別並不明顯, 因此也看我的喜歡採用哪一種方式吧.

Reflect

講了這麼多都是在講 Proxy, 那 Reflect 呢? 它和之前的一些方法只有一些細微差異, 因此它的意義是什麼? 有什麼用?

Reflect 的方法和 Proxy 的方法是成對出現的, 和之前的一些方法相比, Reflect 的方法對參數的處理不一樣或返回值不一樣, 儘管很細微的差異, 可是當和 Proxy 配合使用的時候, 使用之前的方法可能致使 Proxy 對象和普通對象的一些行爲不一致, 而使用 Reflect 則不會有這樣的問題, 因此建議在 Proxy 中都使用 Reflect 的對應方法.

另外一方面是 Reflect 暴露的 API 相對更加底層, 性能會好一些.

最後是有些事情只能經過 Reflect 實現, 具體參考這個例子. 可是我的感受這個例子並非很好, 畢竟這個場景太少見了.

讓咱們先來回顧一下前面後置代理的例子.

var pobj = new Proxy({}, {
	get(target, key) {
		if (!target[key]) {
			target[key] = function () {
				console.log(key);
			};
		}
		return target[key];
	}
});

var obj = Object.create(pobj);
obj.sayHello();
obj.sayGoodBye();
複製代碼

在這個例子中, 調用 obj 上一開始不存在的方法最終都會經過原型鏈找到代理對象, 進而找到 target 也即空對象, 而後對空對象實例化對應的方法. 這裏的原型鏈查找老是讓人感受不太爽, 明明進入到 get trap 就確定說明 obj 一開始不存在對應方法, 那咱們理應能夠在這時候給 obj 設置對應方法, 這樣下次調用的時候就不會進行原型鏈的查找了, 爲何非要給那個毫無卵用的空對象設置方法, 致使每次對 obj 進行方法調用仍是要進行原型鏈查找?

因而咱們想起 get trap 還有個 receiver 參數, 大多數地方都寫着 receiver 就是代理對象, 也即咱們這裏的 pobj, 其實不是, 準確說它是實際發生屬性查找的對象, 也即咱們這裏的 obj, 有點像 DOM 事件中 event.target 的意思.

因而咱們立刻將原有的寫法改爲這樣.

var pobj = new Proxy({}, {
	get(target, key, receiver) {
		if (!receiver[key]) {
			receiver[key] = function () {
				console.log(key);
			};
		}
		return receiver[key];
	}
});

var obj = Object.create(pobj);
obj.sayHello();
// RangeError: Maximum call stack size exceeded
複製代碼

看上去沒什麼毛病, 而後咱們立馬獲得一個堆棧溢出的錯誤. 仔細看看咱們發現關鍵問題就出在這個 receiver[key], 它對 obj.sayHello 進行了查找, 但此時 obj.sayHello 還未實例化, 因而無限對 obj.sayHello 進行查找, 最終致使堆棧溢出.

這裏出現問題的根本緣由是 a[b] 這樣的取值操做妥妥地會觸發 Proxy 的 get trap 的, 由於 Proxy 是更爲底層的存在, 可是仔細想一想咱們的需求其實不是爲了取值, 而是爲了知道 obj 自身是否存在 sayHello 屬性, 從這一點來講, 咱們不必使用 a[b] 這樣的方式來判斷, 咱們能夠用 hasOwnProperty(). 因而繼續改造.

var pobj = new Proxy({}, {
	get(target, key, receiver) {
		if (!receiver.hasOwnProperty(key)) {
			receiver[key] = function () {
				console.log(key);
			};
		}
		return receiver[key];
	}
});

var obj = Object.create(pobj);
obj.sayHello();
// RangeError: Maximum call stack size exceeded
複製代碼

仍是堆棧溢出, 由於 hasOwnProperty() 實際上是 Object.prototype.hasOwnProperty(), 意味着在原型鏈的盡頭, 而 pobj 在原型鏈上更近的位置, 因而至關於 receiver/obj 並不存在 hasOwnProperty(), 因而變成了對 obj.hasOwnProperty() 無限查找致使堆棧溢出.

那繼續吧, 咱們直接用 Object.prototype.hasOwnProperty() 總行了吧.

var pobj = new Proxy({}, {
	get(target, key, receiver) {
		if (!Object.prototype.hasOwnProperty.call(receiver, key)) {
			receiver[key] = function () {
				console.log(key);
			};
		}
		return receiver[key];
	}
});

var obj = Object.create(pobj);
obj.sayHello();
obj.sayHello();
// sayHello
// sayHello
複製代碼

到這裏其實問題已經解決了, 咱們的後置代理只會在第一次未實例化方法時進行原型鏈查找, 以後調用 obj.sayHello() 都是直接和 obj 進行交互, 既沒有原型鏈查找也沒有代理. 那這和 Reflect 有什麼關係?

其實這裏用 Reflect 會更好一點, 一方面相對於長長的 Object.prototype.hasOwnProperty.call 來講會更短更直觀, 一方面性能也好一點(反正 Node 源碼中是把 call 換成了 Reflect).

var pobj = new Proxy({}, {
	get(target, key, receiver) {
		if (!Reflect.has(receiver, key)) {
			Reflect.set(receiver, key, function () {
				console.log(key);
			});
			return Reflect.get(receiver, key);
		} else {
			return Reflect.get(target, key);
		}
	}
});

var obj = Object.create(pobj);
obj.sayHello();
obj.sayHello();
console.log(obj.hasOwnProperty('sayHello'));
複製代碼

最終咱們改爲了這樣子, 和前面又稍稍有一些不同, 有個 else 把非 obj 自身的屬性查找轉發給了 target, 由於後面有個 hasOwnProperty() 調用, 若是不轉發給 target 的話, 則致使繼承自 Object 的屬性和方法全都會產生堆棧溢出.

後續補充: 這裏我犯了兩個錯誤, 爲了說明這個錯誤因此前面的內容再也不修改, 看成標本.

先讓咱們來看看最終版本的 if (!Reflect.has(receiver, key)) 這段邏輯和以前的 if (!receiver[key]), 咱們說, 最終咱們但願的是檢測對應屬性是否存在, 這話嚴格來講也不算錯. 但每一個人對存在的定義可能都不一樣, 有人認爲 receiver[key] === undefined 就算不存在, 而若是有人以爲 Reflect.has(receiver, key)false 算不存在, 但其實它們是很不同的. 這裏咱們準肯定義應該是, receiver[key] === undefined 是作的可用性檢測, 而 Reflect.has(receiver, key) 是作的存在性檢測. 因此這裏用 Reflect.has(receiver, key) 嚴格來講也不能算錯, 可是很容易被人忽視的一點就是, 在後置代理中, receiver 對象的任何存在但不可用的屬性, 都會致使沒法委託到原型鏈上的代理對象. 這也算是使用後置代理的一點限制吧.

而第二個錯誤, 則是實實在在的錯誤了. 前面說過, 一旦進入到 get trap 就確定說明 obj 一開始不存在對應方法, 既然咱們已經知道不存在對應方法了, 那爲何還要用 if (!Reflect.has(receiver, key)) 作存在性檢測? 因此這步邏輯是多餘的. 可是另外一方面是, 不少 Object.prototype 上的方法, 其實 receiver 也是不存在的, 因此當調用這些方法的時候也是會進入到 get trap 的, 咱們依舊須要把它們轉發到 target 上去. 因而咱們應當寫成這樣.

var pobj = new Proxy({}, {
	get(target, key, receiver) {
		if (Reflect.has(target, key)) {
			return Reflect.get(target, key);
		}
		Reflect.set(receiver, key, function () {
			console.log(key);
		});
		return Reflect.get(receiver, key);
	}
});

var obj = Object.create(pobj);
obj.sayHello();
obj.sayHello();
console.log(obj.hasOwnProperty('sayHello'));
複製代碼

其實也沒有省太多事就是了, 雖然咱們去掉了一個判斷, 可是爲了保證繼承自 Object 的方法正常使用, 又引入了一個新的判斷, 看上去只是把 if-else 中的邏輯調換了位置而已, 不過邏輯上講, 這樣更合理一些吧.

其餘細節

對於數組使用代理的話, get trap 和 set trap 也能夠攔截到數組方法, 好比 forEach push 等, 由於實際上這些方法也會對數組使用如 arr[index] 這樣的形式去獲取和設置值.

另外 Proxy 的各個 trap 中的 this 均是指向 handler 對象, 而不是代理對象, 也不是目標對象, 而 trap 中返回函數(若是能夠返回一個函數的話)的 this 指向的是代理對象而不是目標對象. 即

var obj = {}, handler = {
	get(target, key, receiver) {
		console.log(this === target);
		console.log(this === receiver);
		console.log(this === handler);
	}
};

var pobj = new Proxy(obj, handler);
pobj.name;
// false
// false
// true


var obj = {}, handler = {
	get(target, key, receiver) {
		return function () {
			console.log(this === target);
			console.log(this === receiver);
			console.log(this === handler);
		};
	}
};

var pobj = new Proxy(obj, handler);
pobj.test();
// false
// true
// false
複製代碼

這裏也順便提下 Object.defineProperty()this 的處理. Object.defineProperty() 的 getter/setter 中的 this 指向的是目標對象而非屬性描述符對象, 若是 getter 中返回函數, 則函數的 this 也是指向目標對象.

var obj = {
	name: 'aaa'
};

Object.defineProperty(obj, 'test', {
	get() {
		console.log(this.name);
	}
});

obj.test;
// aaa


var obj = {
	name: 'aaa'
};

Object.defineProperty(obj, 'test', {
	get() {
		return function () {
			console.log(this.name);
		};
	}
});

obj.test();
// aaa
複製代碼

參考資料

相關文章
相關標籤/搜索