最近在專案裡使用了 Proxy,遇到一些問題記錄一下
Proxy 簡介
簡單來說 Proxy 是對物件
設定一個攔截,直接上程式碼
let obj = {
attr: 1
};
// 對 obj 進行攔截
obj = new Proxy(obj, {
get: function (target, key, receiver) {
//如果 obj 有這個屬性,則直接返回
if(key in target) {
return target[key];
}
//如果 obj 沒有這個屬性,則統一返回 '沒有這個值'
return '沒有這個值';
},
set: function (target, key, value, receiver) {
// 想設定物件屬性,直接返回(不讓設定)
return true;
}
});
// 使用
console.log(obj.attr); // 輸出 1
console.log(obj.abc); // 輸出 '沒有這個值',
obj.attr = 2;//
console.log(obj.attr); // 輸出 1, 不能設定物件屬性
複製程式碼
可以進行的攔截型別不止 get, set
,還有很多,例如:(來自阮一峰老師的書 ES6 Proxy):
- set(target, propKey, value, receiver):攔截物件屬性的設定,比如proxy.foo = v或proxy['foo'] = v,返回一個布林值。
- has(target, propKey):攔截propKey in proxy的操作,返回一個布林值。
- deleteProperty(target, propKey):攔截delete proxy[propKey]的操作,返回一個布林值。
- ownKeys(target):攔截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in迴圈,返回一個陣列。該方法返回目標物件所有自身的屬性的屬性名,而Object.keys()的返回結果僅包括目標物件自身的可遍歷屬性。
- getOwnPropertyDescriptor(target, propKey):攔截Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述物件。
- defineProperty(target, propKey, propDesc):攔截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一個布林值。
- preventExtensions(target):攔截Object.preventExtensions(proxy),返回一個布林值。
- getPrototypeOf(target):攔截Object.getPrototypeOf(proxy),返回一個物件。
- isExtensible(target):攔截Object.isExtensible(proxy),返回一個布林值。
- setPrototypeOf(target, proto):攔截Object.setPrototypeOf(proxy, proto),返回一個布林值。如果目標物件是函式,那麼還有兩種額外操作可以攔截。
- apply(target, object, args):攔截 Proxy 例項作為函式呼叫的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。 construct(target, args):攔截 Proxy 例項作為建構函式呼叫的操作,比如new proxy(...args)。
如上這些都是介紹都是直接拷貝的 阮一峰老師的書 ES6 Proxy
遇到的問題
對原生的瀏覽器 HTMLElement 物件進行攔截
1、攔截時的 this
指向問題 阮老師書裡也有提及
在攔截方法裡,this
指向 Proxy 物件,所以在呼叫原物件的方法時,需要注意,直接看程式碼
let div = document.querySelector('div'); // 隨便拿一個頁面的 div, 對他進行代理
// 第一種代理方法
let divProxy = new Proxy(div, {
get: function (target, key, receiver) {
// 訪問這個 div 的任何屬性,都直接返回
return target[key];
}
});
// 呼叫 div 的 querySelector 方法,拿他下邊的 a 標籤
console.log(divProxy.querySelector('a')); // chrome 上會報錯:Uncaught TypeError: Illegal invocation
複製程式碼
如上的程式碼,猜測因為 querySelector 方法內部有訪問 this
指向,導致報錯。修改為如下程式碼,則可以修復這個問題:
let div = document.querySelector('div'); // 隨便拿一個頁面的 div, 對他進行代理
// 第二種代理方法
let divProxy = new Proxy(div, {
get: function (target, key, receiver) {
if( !!target[key] && !!target[key].bind) {
// 使用 bind 繫結 this 指向
return target[key].bind(target);
} else {
return target[key];
}
}
});
console.log(divProxy.querySelector('a')); // <a href='https://t.tt'>來買呀</a>
複製程式碼
另外還有一個問題,通過代理設定物件屬性時,也會有問題相同的,看程式碼吧
let div = document.querySelector('div');
let divProxy = new Proxy(div, {});
// 設定 div 的 innerHTML 屬性,設定物件屬性預設其實是呼叫 `div.innerHTML.__defineSetter__` 方法
divProxy.innerHTML = '<a href="https://t.tt">來買呀</a>'; //chrome 報錯: Uncaught TypeError: Illegal invocation
複製程式碼
直接上解決辦法
let div = document.querySelector('div');
let divProxy = new Proxy(div, {
// 攔截所有的 set 行為
set: function (target, key, value, receiver) {
if(key in target) {
target[key] = value; // 直接呼叫原物件屬性進行設定(target[key])
}
return true;
}
});
divProxy.innerHTML = '<a href="https://t.tt">來買呀</a>';
複製程式碼
結尾
一般 Proxy
會和 Reflect
一起使用,本文不做介紹了,直接上阮老師的書;
JavaScript 的語法越來越規範,之前的一些坑,新的標準規範也開始慢慢填,加上babel
的普及使用,我們可以多多擁抱新語法、特性,簡潔程式碼,愉悅自己。