使用 ES6 Proxy 代理的 this 問題記錄

帶眼鏡的鄭_小二發表於2018-12-04

最近在專案裡使用了 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的普及使用,我們可以多多擁抱新語法、特性,簡潔程式碼,愉悅自己。

相關文章