萬字長文:Proxy 和 Reflect

Proxy 用於許多庫和某些瀏覽器框架。在本章中,我們將看到許多實際應用。

語法:

let proxy = new Proxy(target, handler)

對 proxy 進行操作,如果在 handler 中存在相應的鉤子,則它將運行,並且 Proxy 有機會對其進行處理,否則將直接對 target 進行處理。

首先,讓我們創建一個沒有任何鉤子的代理:

let target = {};
let proxy = new Proxy(target, {}); // 空的handler對象

proxy.test = 5; // 寫入 Proxy 對象 (1)
alert(target.test); // 返回 5,test屬性出現在了 target 上!

alert(proxy.test); // 還是 5,我們也可以從 proxy 對象讀取它 (2)

for(let key in proxy) alert(key); // 返回 test,迭代也正常工作! (3)

由於沒有鉤子,所有對 proxy 的操作都直接轉發給 target

  1. 寫入操作 proxy.test= 會將值寫入 target

  2. 讀取操作 proxy.test 會從 target 返回對應的值。

  3. 迭代 proxy 會從 target 返回對應的值。

我們可以看到,沒有任何鉤子,proxy 是一個 target 的透明包裝.

Proxy 是一種特殊的 “奇異對象”。它沒有自己的屬性。如果 handler 爲空,則透明地將操作轉發給 target

要激活更多功能,讓我們添加鉤子。

我們可以用它們攔截什麼?

對於對象的大多數操作,JavaScript 規範中都有一個所謂的 “內部方法”,它描述了最底層的工作方式。例如 [[Get]],用於讀取屬性的內部方法, [[Set]],用於寫入屬性的內部方法,等等。這些方法僅在規範中使用,我們不能直接通過方法名調用它們。

Proxy 鉤子會攔截這些方法的調用。它們在代理規範和下表中列出。

對於每個內部方法,此表中都有一個鉤子:可用於添加到 new Proxy 時的 handler 參數中以攔截操作的方法名稱:

Invariants

JavaScript 強制執行某些不變式————當必須由內部方法和鉤子來完成操作時。

其中大多數用於返回值:

還有其他一些不變量,例如:

鉤子可以攔截這些操作,但是必須遵循這些規則。

不變量確保語言功能的正確和一致的行爲。完整的不變量列表在規範,如果您不做奇怪的事情,就不會違反它們。

規範:

https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots)。

讓我們看看實際示例中的工作原理。

帶 “get” 鉤子的默認值

最常見的鉤子是用於讀取 / 寫入屬性。

要攔截讀取操作,handler 應該有 get(target, property, receiver) 方法。

讀取屬性時觸發該方法,參數如下:

讓我們用 get 實現對象的默認值。

我們將創建一個對不存在的數組項返回 0 的數組。

通常,當人們嘗試獲取不存在的數組項時,他們會得到 undefined, 但是我們會將常規數組包裝到代理中,以捕獲讀取操作並在沒有此類屬性的情況下返回 0

let numbers = [0, 1, 2];

numbers = new Proxy(numbers, {
  get(target, prop) {
    if (prop in target) {
      return target[prop];
    } else {
      return 0; // 默認值
    }
  }
});

alert( numbers[1] ); // 1
alert( numbers[123] ); // 0 (沒有這樣的元素)

如我們所見,使用 get 鉤子非常容易。

我們可以用 Proxy 來實現任何讀取默認值的邏輯。

想象一下,我們有一本詞典,上面有短語及其翻譯:

let dictionary = {
  'Hello': 'Hola',
  'Bye': 'Adiós'
};

alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome'] ); // undefined

現在,如果沒有短語,從 dictionary 讀取將返回 undefined。但實際上,返回一個未翻譯短語通常比 undefined 要好。因此,讓我們在這種情況下返回一個未翻譯的短語,而不是 undefined

爲此,我們將包裝 dictionary 進一個攔截讀取操作的代理:

let dictionary = {
  'Hello': 'Hola',
  'Bye': 'Adiós'
};

dictionary = new Proxy(dictionary, {
  get(target, phrase) { // 攔截讀取屬性操作
    if (phrase in target) { //如果字典包含該短語
      return target[phrase]; // 返回譯文
    } else {
      // 否則返回未翻譯的短語
      return phrase;
    }
  }
});

// 在字典中查找任意短語!
// 最壞的情況也只是它們沒有被翻譯。
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome to Proxy']); // Welcome to Proxy

請注意代理如何覆蓋變量:

dictionary = new Proxy(dictionary, ...);

代理應該在所有地方都完全替代了目標對象。目標對象被代理後,任何人都不應該再引用目標對象。否則很容易搞砸。

使用 “set” 鉤子進行驗證

假設我們想要一個專門用於數字的數組。如果添加了其他類型的值,則應該拋出一個錯誤。

當寫入屬性時 set 鉤子觸發。

set(target, property, value, receiver):

如果寫入操作成功,set 鉤子應該返回 true,否則返回 false(觸發 TypeError)。

讓我們用它來驗證新值:

let numbers = [];

numbers = new Proxy(numbers, { // (*)
  set(target, prop, val) { // 攔截寫入操作
    if (typeof val == 'number') {
      target[prop] = val;
      return true;
    } else {
      return false;
    }
  }
});

numbers.push(1); // 添加成功
numbers.push(2); // 添加成功
alert("Length is: " + numbers.length); // 2

numbers.push("test"); // TypeError (proxy 的 `set` 操作返回 false)

alert("This line is never reached (error in the line above)");

我們不必重寫諸如 push 和 unshift 等添加元素的數組方法,就可以在其中添加檢查,因爲在內部它們使用代理所攔截的 [[Set]] 操作。

因此,代碼簡潔明瞭。

別忘了返回 true

如上所述,要保持不變式。

對於 set操作, 它必須在成功寫入時返回 true

如果我們忘記這樣做或返回任何 falsy 值,則該操作將觸發 TypeError

使用 “ownKeys” 和 “getOwnPropertyDescriptor” 進行迭代

Object.keysfor..in 循環和大多數其他遍歷對象屬性的方法都使用 [[OwnPropertyKeys]]內部方法(由 ownKeys 鉤子攔截) 來獲取屬性列表。

這些方法在細節上有所不同:

…… 但是所有這些都從該列表開始。

在下面的示例中,我們使用 ownKeys 鉤子攔截 for..in 對 user 的遍歷,還使用 Object.keys 和 Object.values 來跳過以下劃線 _ 開頭的屬性:

let user = {
  name: "John",
  age: 30,
  _password: "***"
};

user = new Proxy(user, {
  ownKeys(target) {
    return Object.keys(target).filter(key => !key.startsWith('_'));
  }
});

// "ownKeys" 過濾掉 _password
for(let key in user) alert(key); // name,然後是 age

// 對這些方法同樣有效:
alert( Object.keys(user) ); // name,age
alert( Object.values(user) ); // John,30

到目前爲止,它仍然有效。

雖然,如果我們返回對象中不存在的鍵,Object.keys 並不會列出該鍵:

let user = { };

user = new Proxy(user, {
  ownKeys(target) {
    return ['a', 'b', 'c'];
  }
});

alert( Object.keys(user) ); // <empty>

爲什麼?原因很簡單:Object.keys 僅返回帶有 enumerable 標記的屬性。爲了檢查它, 該方法會對每個屬性調用 [[GetOwnProperty]] 來獲得屬性描述符。在這裏,由於沒有屬性,其描述符爲空,沒有 enumerable 標記,因此它將略過。

爲了讓 Object.keys 返回一個屬性,我們要麼需要將該屬性及 enumerable 標記存入對象,或者我們可以攔截對它的調用 [[GetOwnProperty]] (鉤子getOwnPropertyDescriptor 會執行此操作),並返回描述符 enumerable: true。

這是一個例子:

let user = { };

user = new Proxy(user, {
  ownKeys(target) { // 一旦被調用,就返回一個屬性列表
    return ['a', 'b', 'c'];
  },

  getOwnPropertyDescriptor(target, prop) { // 被每個屬性調用
    return {
      enumerable: true,
      configurable: true
      /* 其他屬性,類似於 "value:..." */
    };
  }

});

alert( Object.keys(user) ); // a, b, c

讓我們再次注意:如果該屬性在對象中不存在,則我們只需要攔截 [[GetOwnProperty]]

具有 “deleteProperty” 和其他鉤子的受保護屬性

有一個普遍的約定,即下劃線 _ 前綴的屬性和方法是內部的。不應從對象外部訪問它們。

從技術上講,這是可能的:

let user = {
  name: "John",
  _password: "secret"
};

alert(user._password); // secret

讓我們使用代理來防止對以 _ 開頭的屬性的任何訪問。

我們需要以下鉤子:

代碼如下:

let user = {
  name: "John",
  _password: "***"
};

user = new Proxy(user, {
  get(target, prop) {
    if (prop.startsWith('_')) {
      throw new Error("Access denied");
    }
    let value = target[prop];
    return (typeof value === 'function') ? value.bind(target) : value; // (*)
  },
  set(target, prop, val) { // 攔截寫入操作
    if (prop.startsWith('_')) {
      throw new Error("Access denied");
    } else {
      target[prop] = val;
      return true;
    }
  },
  deleteProperty(target, prop) { // 攔截屬性刪除
    if (prop.startsWith('_')) {
      throw new Error("Access denied");
    } else {
      delete target[prop];
      return true;
    }
  },
  ownKeys(target) { // 攔截讀取屬性列表
    return Object.keys(target).filter(key => !key.startsWith('_'));
  }
});

// “get” 不允許讀取 _password
try {
  alert(user._password); // Error: Access denied
} catch(e) { alert(e.message); }

//  “set” 不允許寫入 _password
try {
  user._password = "test"; // Error: Access denied
} catch(e) { alert(e.message); }

// “deleteProperty” 不允許刪除 _password 屬性
try {
  delete user._password; // Error: Access denied
} catch(e) { alert(e.message); }

// “ownKeys” 過濾排除 _password
for(let key in user) alert(key); // name

請注意在行 (*) 中 get 鉤子的重要細節:

get(target, prop) {
  // ...
  let value = target[prop];
  return (typeof value === 'function') ? value.bind(target) : value; // (*)
}

爲什麼我們需要一個函數調用 value.bind(target)

原因是對象方法(例如 user.checkPassword())必須能夠訪問 _password

user = {
  // ...
  checkPassword(value) {
    //對象方法必須能讀取 _password
    return value === this._password;
  }
}

對 user.checkPassword() 的一個調用會調用代理對象 user 作爲 this(點運算符之前的對象會成爲 this),因此,當它嘗試訪問 this._password 時 get 鉤子將激活(它在讀取任何屬性時觸發)並拋出錯誤。

因此,我們在行 (*) 中將對象方法的上下文綁定到原始對象,target。然後,它們將來的調用將使用 target 作爲 this,不觸發任何鉤子。

該解決方案通常可行,但並不理想,因爲一種方法可能會將未代理的對象傳遞到其他地方,然後我們會陷入困境:原始對象在哪裏,代理的對象在哪裏?

此外,一個對象可能會被代理多次(多個代理可能會對該對象添加不同的 “調整”),並且如果我們將未包裝的對象傳遞給方法,則可能會產生意想不到的後果。

因此,在任何地方都不應使用這種代理。

類的私有屬性

現代 Javascript 引擎原生支持私有屬性,其以 # 作爲前綴。這在章節 私有的和受保護的屬性和方法 中有詳細描述。Proxy 並不是必需的。

但是,此類屬性有其自身的問題。特別是,它們是不可繼承的。

“In range” 及 “has” 鉤子

讓我們來看更多示例。

我們有一個 range 對象:

let range = {
  start: 1,
  end: 10
};

我們想使用 in 運算符來檢查數字是否在 range 範圍內。

該 has 鉤子攔截 in 調用。

has(target, property)

示例如下

let range = {
  start: 1,
  end: 10
};

range = new Proxy(range, {
  has(target, prop) {
    return prop >= target.start && prop <= target.end
  }
});

alert(5 in range); // true
alert(50 in range); // false

漂亮的語法糖,不是嗎?而且實現起來非常簡單。

包裝函數:"apply"

我們也可以將代理包裝在函數週圍。

apply(target, thisArg, args) 鉤子能使代理以函數的方式被調用:

例如,讓我們回想一下 delay(f, ms) 裝飾器,它是我們在 裝飾者模式,call/apply 一章中完成的。

在該章中,我們沒有用 proxy 來實現它。調用 delay(f, ms) 返回一個函數,該函數會將在 ms毫秒後把所有調用轉發到 f

這是以前的基於函數的實現:

function delay(f, ms) {
  // 返回一個超時後調用 f 函數的包裝器
  return function() { // (*)
    setTimeout(() => f.apply(this, arguments), ms);
  };
}

function sayHi(user) {
  alert(`Hello, ${user}!`);
}

// 這次包裝後,sayHi 在3秒後被調用
sayHi = delay(sayHi, 3000);

sayHi("John"); // Hello, John! (3秒後)

正如我們已經看到的那樣,大多數情況下都是可行的。包裝函數 (*) 在超時後執行調用。

但是包裝函數不會轉發屬性讀 / 寫操作或其他任何操作。包裝後,無法訪問原有函數的屬性,比如 namelength和其他:

function delay(f, ms) {
  return function() {
    setTimeout(() => f.apply(this, arguments), ms);
  };
}

function sayHi(user) {
  alert(`Hello, ${user}!`);
}

alert(sayHi.length); // 1 (函數的 length 是其聲明中的參數個數)

sayHi = delay(sayHi, 3000);

alert(sayHi.length); // 0 (在包裝器聲明中,參數個數爲0)

Proxy 功能強大得多,因爲它將所有東西轉發到目標對象。

讓我們使用 Proxy 而不是包裝函數:

function delay(f, ms) {
  return new Proxy(f, {
    apply(target, thisArg, args) {
      setTimeout(() => target.apply(thisArg, args), ms);
    }
  });
}

function sayHi(user) {
  alert(`Hello, ${user}!`);
}

sayHi = delay(sayHi, 3000);

alert(sayHi.length); // 1 (*) proxy 轉發“獲取 length” 操作到目標對象

sayHi("John"); // Hello, John! (3秒後)

結果是相同的,但現在不僅調用,而且代理上的所有操作都轉發到原始函數。所以 sayHi.length 在 (*) 行包裝後正確返回結果 (*)。

我們有一個 “更豐富” 的包裝器。

還存在其他鉤子:完整列表在本章的開頭。它們的使用模式與上述類似。

Reflect

Reflect 是一個內置對象,可簡化的創建 Proxy

以前的內部方法,比如[[Get]][[Set]] 等等都只是規範,不能直接調用。

Reflect 對象使調用這些內部方法成爲可能。它的方法是內部方法的最小包裝。

這是 Reflect 執行相同操作和調用的示例:

例如:

let user = {};

Reflect.set(user, 'name', 'John');

alert(user.name); // John

尤其是,Reflect 允許我們使用函數(Reflect.constructReflect.deleteProperty,……)執行操作(newdelete,……)。這是一個有趣的功能,但是這裏還有一點很重要。

對於每個可被 Proxy 捕獲的內部方法,Reflect 都有一個對應的方法 Reflect,其名稱和參數與 Proxy 鉤子相同。

因此,我們可以用 Reflect 來將操作轉發到原始對象。

在此示例中,鉤子get 和 set 透明地(好像它們都不存在)將讀 / 寫操作轉發到對象,並顯示一條消息:

let user = {
  name: "John",
};

user = new Proxy(user, {
  get(target, prop, receiver) {
    alert(`GET ${prop}`);
    return Reflect.get(target, prop, receiver); // (1)
  },
  set(target, prop, val, receiver) {
    alert(`SET ${prop}=${val}`);
    return Reflect.set(target, prop, val, receiver); // (2)
  }
});

let name = user.name; // shows "GET name"
user.name = "Pete"; // shows "SET

這裏:

就是說,一切都很簡單:如果鉤子想要將調用轉發給對象,則只需使用相同的參數調用 Reflect.<method> 就足夠了。

在大多數情況下,我們可以不使用 Reflect 完成相同的事情,例如,使用Reflect.get(target, prop, receiver) 讀取屬性可以替換爲 target[prop]。儘管有一些細微的差別。

代理一個 getter

讓我們看一個示例,說明爲什麼 Reflect.get 更好。我們還將看到爲什麼 get/set 有第四個參數 receiver,而我們以前沒有使用過它。

我們有一個帶有一個 _name 屬性和一個 getter 的對象 user

這是一個 Proxy:

let user = {
  _name: "Guest",
  get name() {
    return this._name;
  }
};

let userProxy = new Proxy(user, {
  get(target, prop, receiver) {
    return target[prop];
  }
});

alert(userProxy.name); // Guest
let user = {
  _name: "Guest",
  get name() {
    return this._name;
  }
};

let userProxy = new Proxy(user, {
  get(target, prop, receiver) {
    return target[prop]; // (*) target = user
  }
});

let admin = {
  __proto__: userProxy,
  _name: "Admin"
};

// Expected: Admin
alert(admin.name); // 輸出:Guest (?!?)
  1. 當我們讀取 admin.name,由於 admin 對象自身沒有對應的的屬性,搜索將轉到其原型。

  2. 原型是 userProxy

  3. 從代理讀取 name 屬性時,get 鉤子會觸發並從原始對象返回 target[prop] 屬性,在 (*)

    當調用 target[prop] 時,若 prop 是一個 getter,它將在 this=target 上下文中運行其代碼。因此,結果是來自原始對象 target 的 this._name 即來自 user

爲了解決這種情況,我們需要 get 鉤子的第三個參數 receiver。它保證傳遞正確的 this 給 getter。在我們的情況下是 admin

如何爲 getter 傳遞上下文?對於常規函數,我們可以使用 call/apply,但這是一個 getter,它不是 “被調用” 的,只是被訪問的。

Reflect.get 可以做到的。如果我們使用它,一切都會正常運行。

這是更正後的變體:

let user = {
  _name: "Guest",
  get name() {
    return this._name;
  }
};

let userProxy = new Proxy(user, {
  get(target, prop, receiver) { // receiver = admin
    return Reflect.get(target, prop, receiver); // (*)
  }
});


let admin = {
  __proto__: userProxy,
  _name: "Admin"
};

alert(admin.name); // Admin

現在 receiver,保留了對正確 this 的引用(即admin)的引用,該引用將在 (*) 行中使用Reflect.get傳遞給 getter。

我們可以將鉤子重寫得更短:

get(target, prop, receiver) {
  return Reflect.get(...arguments);
}

Reflect 調用的命名方式與鉤子完全相同,並且接受相同的參數。它們是通過這種方式專門設計的。

因此, return Reflect... 會提供一個安全的提示程序來轉發操作,並確保我們不會忘記與此相關的任何內容。

Proxy 的侷限

代理提供了一種獨特的方法,可以在最底層更改或調整現有對象的行爲。但是,它並不完美。有侷限性。

內置對象:內部插槽(Internal slots)

許多內置對象,例如 MapSetDatePromise 等等都使用了所謂的 “內部插槽”。

它們類似於屬性,但僅限於內部使用,僅用於規範目的。例如, Map 將項目存儲在 [[MapData]]中。內置方法直接訪問它們,而不通過 [[Get]]/[[Set]] 內部方法。所以 Proxy 不能攔截。

爲什麼要在意呢?他們是內部的!

好吧,這就是問題。在像這樣的內置對象被代理後,代理對象沒有這些內部插槽,因此內置方法將失敗。

例如:

let map = new Map();

let proxy = new Proxy(map, {});

proxy.set('test', 1); // Error

在內部,一個 Map 將所有數據存儲在其 [[MapData]] 內部插槽中。代理對象沒有這樣的插槽。內建方法 Map.prototype.set 方法試圖訪問內部屬性 this.[[MapData]],但由於 this=proxy 在 proxy 中不能找到它,只能失敗。

幸運的是,有一種解決方法:

let map = new Map();

let proxy = new Proxy(map, {
  get(target, prop, receiver) {
    let value = Reflect.get(...arguments);
    return typeof value == 'function' ? value.bind(target) : value;
  }
});

proxy.set('test', 1);
alert(proxy.get('test')); // 1 (works!)

現在它可以正常工作,因爲 get 鉤子將函數屬性(例如 map.set)綁定到目標對象(map)本身。

與前面的示例不同,proxy.set(...) 內部 this 的值並不是 proxy,而是原始對象 map。因此,當set 鉤子的內部實現嘗試訪問 this.[[MapData]] 內部插槽時,它會成功。

Array 沒有內部插槽

一個明顯的例外:內置 Array 不使用內部插槽。那是出於歷史原因,因爲它出現於很久以前。

因此,代理數組時沒有這種問題。

私有字段

類的私有字段也會發生類似的情況。

例如,getName() 方法訪問私有的 #name 屬性並在代理後中斷:

class User {
  #name = "Guest";

  getName() {
    return this.#name;
  }
}

let user = new User();

user = new Proxy(user, {});

alert(user.getName()); // Error

原因是專用字段是使用內部插槽實現的。JavaScript 訪問它們時不使用 [[Get]]/[[Set]]

在調用 getName() 時 this 的值是代理後的 user,它沒有帶私有字段的插槽。

再次,bind 方法的解決方案使它恢復正常:

class User {
  #name = "Guest";

  getName() {
    return this.#name;
  }
}

let user = new User();

user = new Proxy(user, {
  get(target, prop, receiver) {
    let value = Reflect.get(...arguments);
    return typeof value == 'function' ? value.bind(target) : value;
  }
});

alert(user.getName()); // Guest

該解決方案有缺點,如前所述:將原始對象暴露給該方法,可能使其進一步傳遞並破壞其他代理功能。

Proxy != target

代理和原始對象是不同的對象。很自然吧?

因此,如果我們使用原始對象作爲鍵,然後對其進行代理,則找不到代理:

let allUsers = new Set();

class User {
  constructor(name) {
    this.name = name;
    allUsers.add(this);
  }
}

let user = new User("John");

alert(allUsers.has(user)); // true

user = new Proxy(user, {});

alert(allUsers.has(user)); // false

如我們所見,代理後,我們在 allUsers 中找不到 user,因爲代理是一個不同的對象。

Proxy 無法攔截嚴格相等性測試 ===

Proxy 可以攔截許多運算符,例如 new(使用 construct),in(使用 has),delete(使用 deleteProperty)等。

但是沒有辦法攔截對象的嚴格相等性測試。一個對象嚴格只等於自身,沒有其他值。

因此,比較對象是否相等的所有操作和內置類都會區分 target 和 proxy。這裏沒有透明的替代品。

可取消的 Proxy

一個可撤銷的代理是可以被禁用的代理。

假設我們有一個資源,並且想隨時關閉對該資源的訪問。

我們可以做的是將其包裝成可撤銷的代理,而沒有任何鉤子。這樣的代理會將操作轉發給對象,我們可以隨時將其禁用。

語法爲:

let {proxy, revoke} = Proxy.revocable(target, handler)

該調用返回一個帶有 proxy 和 revoke 函數的對象以將其禁用。

這是一個例子:

let object = {
  data: "Valuable data"
};

let {proxy, revoke} = Proxy.revocable(object, {});

// proxy 正常工作
alert(proxy.data); // Valuable data

// 之後某處調用
revoke();

// proxy 不再工作(已吊銷)
alert(proxy.data); // Error

調用 revoke() 會從代理中刪除對目標對象的所有內部引用,因此不再連接它們。之後可以對目標對象進行垃圾回收。

我們還可以將 revoke 存儲在 WeakMap 中,以便能夠通過代理對象輕鬆找到它:

let revokes = new WeakMap();

let object = {
  data: "Valuable data"
};

let {proxy, revoke} = Proxy.revocable(object, {});

revokes.set(proxy, revoke);

// ..later in our code..
revoke = revokes.get(proxy);
revoke();

alert(proxy.data); // Error(已吊銷)

這種方法的好處是我們不必隨身攜帶 revoke。我們可以在需要時從 map proxy 上獲取它。

此處我們使用WeakMap 而不是 Map ,因爲它不會阻止垃圾收集。如果代理對象變得 “無法訪問”(例如,沒有變量再引用它),則 WeakMap 允許將其與 它的 revoke 對象一起從內存中擦除,因爲我們不再需要它了。

總結

Proxy 是對象的包裝,將代理上的操作轉發到對象,並可以選擇捕獲其中的一些操作。

它可以包裝任何類型的對象,包括類和函數。

語法爲:

let proxy = new Proxy(target, {
  /* traps */
});

…… 然後,我們應該在所有地方使用 proxy 而不是 target。代理沒有自己的屬性或方法。如果提供了鉤子,它將捕獲操作,否則將其轉發給 target 對象。

我們可以捕獲:

這使我們能夠創建 “虛擬” 屬性和方法,實現默認值,可觀察對象,函數裝飾器等等。

我們還可以將對象多次包裝在不同的代理中,並用多個函數進行裝飾。

該 Reflect API 旨在補充 Proxy。對於任何 Proxy 鉤子,都有一個帶有相同參數的 Reflect 調用。我們應該使用它們將調用轉發給目標對象。

Proxy 有一些侷限:

幾個小實例任務

讀取不存在的屬性時出錯

通常,嘗試讀取不存在的屬性會返回 undefined

創建一個代理,在嘗試讀取不存在的屬性時該代理拋出錯誤。

這可以幫助及早發現編程錯誤。

編寫一個接受 target 對象,並返回添加此方面功能的 proxy 的 wrap(target) 函數。

應滿足如下結果:

let user = {
  name: "John"
};

function wrap(target) {
  return new Proxy(target, {
      /* 你的代碼 */
  });
}

user = wrap(user);

alert(user.name); // John
alert(user.age); // 錯誤:屬性不存在

解決方案

let user = {
  name: "John"
};

function wrap(target) {
  return new Proxy(target, {
    get(target, prop, receiver) {
      if (prop in target) {
        return Reflect.get(target, prop, receiver);
      } else {
        throw new ReferenceError(`Property doesn't exist: "${prop}"`)
      }
    }
  });
}

user = wrap(user);

alert(user.name); // John
alert(user.age); // ReferenceError: Property doesn't exist

用 - 1 索引訪問數組

在某些編程語言中,我們可以使用從結尾算起的負索引訪問數組元素。

像這樣:

let array = [1, 2, 3];

array[-1]; // 3,最後一個元素
array[-2]; // 2,從末尾開始向前移動一步
array[-3]; // 1,從末尾開始向前移動兩步

換句話說,array[-N] 與 array[array.length \- N] 相同。

創建一個 proxy 來實現該行爲。

那應該是這樣的:

let array = [1, 2, 3];

array = new Proxy(array, {
  /* your code */
});

alert( array[-1] ); // 3
alert( array[-2] ); // 2

// 其他數組也應該適用於這個功能

解決方案

let array = [1, 2, 3];

array = new Proxy(array, {
  get(target, prop, receiver) {
    if (prop < 0) {
      // even if we access it like arr[1]
      // prop is a string, so need to convert it to number
      prop = +prop + target.length;
    }
    return Reflect.get(target, prop, receiver);
  }
});


alert(array[-1]); // 3
alert(array[-2]); // 2

Observable

創建一個通過返回代理 “使對象可觀察” 的 makeObservable(target) 函數。

它的工作方式如下:

function makeObservable(target) {
  /* your code */
}

let user = {};
user = makeObservable(user);

user.observe((key, value) => {
  alert(`SET ${key}=${value}`);
});

user.name = "John"; // alerts:設置 name 屬性爲 John

換句話說,makeObservable 返回的對象就像原始對象一樣,但是也具有將 handler 函數設置爲在任何屬性更改時都被調用的方法 observe(handler) 。

每當屬性更改時,都會使用屬性的名稱和值調用 handler(key, value) 。

P.S. 在此任務中,請僅注意寫入屬性。可以以類似方式實現其他操作。

解決方案

該解決方案包括兩部分:

  1. 無論 .observe(handler) 何時被調用,我們都需要在某個地方記住 handler,以便以後可以調用它。我們可以使用 Symbol 作爲屬性鍵,將 handler 直接存儲在對象中。

  2. 我們需要一個帶 set 鉤子的 proxy 來在發生任何更改時調用處理程序。

let handlers = Symbol('handlers');

function makeObservable(target) {
  // 1. 初始化 handler 存儲數組
  target[handlers] = [];

  // 存儲 handler 函數到數組中以便於未來調用
  target.observe = function(handler) {
    this[handlers].push(handler);
  };

  // 2. 創建代理以處理更改
  return new Proxy(target, {
    set(target, property, value, receiver) {
      let success = Reflect.set(...arguments); // 轉發寫入操作到目標對象
      if (success) { // 如果設置屬性的時候沒有報錯
        // 調用所有 handler
        target[handlers].forEach(handler => handler(property, value));
      }
      return success;
    }
  });
}

let user = {};

user = makeObservable(user);

user.observe((key, value) => {
  alert(`SET ${key}=${value}`);
});

user.name = "John";

轉自: 王小醬

https://juejin.cn/post/6844904090116292616

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Y--hyizEZCIMtnZzdeO1tA