掃二維碼與項目經(jīng)理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術咨詢/運營咨詢/技術建議/互聯(lián)網(wǎng)交流
本文轉載自微信公眾號「全棧修仙之路」,作者阿寶哥。轉載本文請聯(lián)系全棧修仙之路公眾號。

阿寶哥將從 6 個方面入手,帶你一步一步揭開 Proxy 對象的神秘面紗。閱讀完本文,你將了解以下內(nèi)容:
一、聊一聊代理
在日常工作中,相信挺多小伙伴都用過 Web 調試代理工具,比如 Fiddler 或 Charles。通過使用 Web 調試代理工具,我們可以抓取 HTTP/HTTPS 協(xié)議請求,還可以手動修改請求參數(shù)和響應結果。不僅如此,在調試線上問題時,利用 Web 調試代理工具,你還可以把線上壓縮混淆過 的 JS 文件映射成本地 未壓縮混淆過 的 JS 文件。
在簡單介紹了 Web 調試代理工具的基本功能之后,我們來看一下使用 Web 調試代理工具的 HTTP 請求流程:
通過上圖可知,在引入 Web 調試代理工具之后,我們發(fā)起的 HTTP 請求都會通過 Web Proxy 進行轉發(fā)和處理。增加了 Web Proxy 代理層,讓我們能夠更好地控制 HTTP 請求的流程。對于單頁應用程序來說,當從服務器獲取數(shù)據(jù)之后,我們就會讀取相應的數(shù)據(jù)在頁面上顯示出來:
以上流程與瀏覽器直接從服務器獲取數(shù)據(jù)類似:
為了能夠靈活控制 HTTP 請求的流程,我們增加了的 Web Proxy 層。那么我們能否控制數(shù)據(jù)對象的讀取流程呢?答案是可以的,我們可以利用 Web API,比如 Object.defineProperty 或 Proxy API。在引入 Web API 之后,數(shù)據(jù)的訪問流程如下圖所示:
接下來,阿寶哥將重點介紹 Proxy API,它可是 Vue3 實現(xiàn)數(shù)據(jù)響應式幕后的 “功臣” 喲。對它感興趣的小伙伴,跟阿寶哥一起學起來吧。
二、Proxy 對象簡介
Proxy 對象用于創(chuàng)建一個對象的代理,從而實現(xiàn)基本操作的攔截和自定義(如屬性查找、賦值、枚舉、函數(shù)調用等)。Proxy 的構造函數(shù)語法為:
- const p = new Proxy(target, handler)
相關的參數(shù)說明如下:
在介紹 Proxy 對象的使用示例前,我們先來了解一下它的兼容性:
(圖片來源:https://caniuse.com/?search=Proxy)
由上圖可知,Proxy API 的兼容性并不是很好,所以大家在使用的時候要注意其兼容性問題。
2.1 Proxy 對象使用示例
了解完 Proxy 構造函數(shù),我們來看一個簡單的例子:
- const man = {
- name: "阿寶哥",
- };
- const proxy = new Proxy(man, {
- get(target, property, receiver) {
- console.log(`正在訪問${property}屬性`);
- return target[property];
- },
- });
- console.log(proxy.name);
- console.log(proxy.age);
在以上示例中,我們使用了 Proxy 構造函數(shù)為 man 對象,創(chuàng)建了一個代理對象。在創(chuàng)建代理對象時,我們定義了一個 get 捕獲器,用于捕獲屬性讀取的操作。 捕獲器的作用就是用于攔截用戶對目標對象的相關操作,在這些操作傳播到目標對象之前,會先調用對應的捕獲器函數(shù),從而攔截并修改相應的行為。
在設置了 get 捕獲器之后,當成功運行以上的示例代碼,控制臺會輸出以下結果:
- 正在訪問name屬性
- 阿寶哥
- 正在訪問age屬性
- undefined
通過觀察以上輸出結果,我們可以發(fā)現(xiàn) get 捕獲器 不僅可以攔截已知屬性的讀取操作,也可以攔截未知屬性的讀取操作。在創(chuàng)建 Proxy 對象時,除了定義 get 捕獲器 之外,我們還可以定義其他的捕獲器,比如 has、set、delete、apply 或 ownKeys 等。
2.2 handler 對象支持的捕獲器
handler 對象支持 13 種捕獲器,這里阿寶哥只列舉以下 5 種常用的捕獲器:
需要注意的是,所有的捕獲器是可選的。如果沒有定義某個捕獲器,那么就會保留源對象的默認行為。 看完上面的捕獲器介紹,是不是覺得 Proxy 對象很強大。
三、Reflect 對象簡介
Reflect 是一個內(nèi)置的對象,它提供攔截 JavaScript 操作的方法。這些方法與 proxy handlers 的方法相同。Reflect 不是一個函數(shù)對象,因此它是不可構造的。
在介紹 Reflect 對象的使用示例前,我們先來了解一下它的兼容性:
(圖片來源:https://caniuse.com/?search=Reflect)
3.1 Reflect 對象使用示例
- const man = {
- name: "阿寶哥",
- city: "Xiamen",
- };
- console.log(Reflect.set(man, "sex", 1)); // true
- console.log(Reflect.has(man, "name")); // true
- console.log(Reflect.has(man, "age")); // false
- console.log(Reflect.ownKeys(man)); // [ 'name', 'city', 'sex' ]
除了示例中介紹的 set、has 和 ownKeys 方法之外,Reflect 對象還支持 get、defineProperty 和 deleteProperty 等方法。下面阿寶哥將簡單介紹 Reflect 對象所支持的一些靜態(tài)方法。
3.2 Reflect 對象支持的靜態(tài)方法
Reflect 的所有屬性和方法都是靜態(tài)的,該對象提供了與 Proxy handler 對象相關的 13 個方法。同樣,這里阿寶哥只列舉以下 5 個常用的方法:
在實際的 Proxy 使用場景中,我們往往會結合 Reflect 對象提供的靜態(tài)方法來實現(xiàn)某些特定的功能。為了讓大家能夠更好地理解并掌握 Proxy 對象,接下來的環(huán)節(jié),阿寶哥將列舉 Proxy 對象的 6 個使用場景。
四、Proxy 使用場景
這里我們先來介紹 Proxy 對象的第一個使用場景 —— 增強型數(shù)組。
4.1 增強型數(shù)組
定義 enhancedArray 函數(shù)
- function enhancedArray(arr) {
- return new Proxy(arr, {
- get(target, property, receiver) {
- const range = getRange(property);
- const indices = range ? range : getIndices(property);
- const values = indices.map(function (index) {
- const key = index < 0 ? String(target.length + index) : index;
- return Reflect.get(target, key, receiver);
- });
- return values.length === 1 ? values[0] : values;
- },
- });
- function getRange(str) {
- var [start, end] = str.split(":").map(Number);
- if (typeof end === "undefined") return false;
- let range = [];
- for (let i = start; i < end; i++) {
- range = range.concat(i);
- }
- return range;
- }
- function getIndices(str) {
- return str.split(",").map(Number);
- }
- }
使用 enhancedArray 函數(shù)
- const arr = enhancedArray([1, 2, 3, 4, 5]);
- console.log(arr[-1]); //=> 5
- console.log(arr[[2, 4]]); //=> [ 3, 5 ]
- console.log(arr[[2, -2, 1]]); //=> [ 3, 4, 2 ]
- console.log(arr["2:4"]); //=> [ 3, 4]
- console.log(arr["-2:3"]); //=> [ 4, 5, 1, 2, 3 ]
由以上的輸出結果可知,增強后的數(shù)組對象,就可以支持負數(shù)索引、分片索引等功能。除了可以增強數(shù)組之外,我們也可以使用 Proxy API 來增強普通對象。
4.2 增強型對象
創(chuàng)建 enhancedObject 函數(shù)
- const enhancedObject = (target) =>
- new Proxy(target, {
- get(target, property) {
- if (property in target) {
- return target[property];
- } else {
- return searchFor(property, target);
- }
- },
- });
- let value = null;
- function searchFor(property, target) {
- for (const key of Object.keys(target)) {
- if (typeof target[key] === "object") {
- searchFor(property, target[key]);
- } else if (typeof target[property] !== "undefined") {
- value = target[property];
- break;
- }
- }
- return value;
- }
使用 enhancedObject 函數(shù)
- const data = enhancedObject({
- user: {
- name: "阿寶哥",
- settings: {
- theme: "dark",
- },
- },
- });
- console.log(data.user.settings.theme); // dark
- console.log(data.theme); // dark
以上代碼運行后,控制臺會輸出以下代碼:
- dark
- dark
通過觀察以上的輸出結果可知,使用 enhancedObject 函數(shù)處理過的對象,我們就可以方便地訪問普通對象內(nèi)部的深層屬性。
4.3 創(chuàng)建只讀的對象
創(chuàng)建 Proxy 對象
- const man = {
- name: "semlinker",
- };
- const handler = {
- set: "Read-Only",
- defineProperty: "Read-Only",
- deleteProperty: "Read-Only",
- preventExtensions: "Read-Only",
- setPrototypeOf: "Read-Only",
- };
- const proxy = new Proxy(man, handler);
使用 proxy 對象
- console.log(proxy.name);
- proxy.name = "kakuqo";
以上代碼運行后,控制臺會輸出以下代碼:
- semlinker
- proxy.name = "kakuqo";
- ^
- TypeError: 'Read-Only' returned for property 'set' of object '#
觀察以上的異常信息可知,導致異常的原因是因為 handler 對象的 set 屬性值不是一個函數(shù)。如果不希望拋出運行時異常,我們可以定義一個 freeze 函數(shù):
- function freeze (obj) {
- return new Proxy(obj, {
- set () { return true; },
- deleteProperty () { return false; },
- defineProperty () { return true; },
- setPrototypeOf () { return true; }
- });
- }
定義好 freeze 函數(shù),我們使用數(shù)組對象來測試一下它的功能:
- let frozen = freeze([1, 2, 3]);
- frozen[0] = 6;
- delete frozen[0];
- frozen = Object.defineProperty(frozen, 0, { value: 66 });
- console.log(frozen); // [ 1, 2, 3 ]
上述代碼成功執(zhí)行后,控制臺會輸出 [ 1, 2, 3 ],很明顯經(jīng)過 freeze 函數(shù)處理過的數(shù)組對象,已經(jīng)被 “凍結” 了。
4.4 攔截方法調用
定義 traceMethodCalls 函數(shù)
- function traceMethodCalls(obj) {
- const handler = {
- get(target, propKey, receiver) {
- const origMethod = target[propKey]; // 獲取原始方法
- return function (...args) {
- const result = origMethod.apply(this, args);
- console.log(
- propKey + JSON.stringify(args) + " -> " + JSON.stringify(result)
- );
- return result;
- };
- },
- };
- return new Proxy(obj, handler);
- }
使用 traceMethodCalls 函數(shù)
- const obj = {
- multiply(x, y) {
- return x * y;
- },
- };
- const tracedObj = traceMethodCalls(obj);
- tracedObj.multiply(2, 5); // multiply[2,5] -> 10
上述代碼成功執(zhí)行后,控制臺會輸出 multiply[2,5] -> 10,即我們能夠成功跟蹤 obj對象中方法的調用過程。其實,除了能夠跟蹤方法的調用,我們也可以跟蹤對象中屬性的訪問,具體示例如下:
- function tracePropAccess(obj, propKeys) {
- const propKeySet = new Set(propKeys);
- return new Proxy(obj, {
- get(target, propKey, receiver) {
- if (propKeySet.has(propKey)) {
- console.log("GET " + propKey);
- }
- return Reflect.get(target, propKey, receiver);
- },
- set(target, propKey, value, receiver) {
- if (propKeySet.has(propKey)) {
- console.log("SET " + propKey + "=" + value);
- }
- return Reflect.set(target, propKey, value, receiver);
- },
- });
- }
- const man = {
- name: "semlinker",
- };
- const tracedMan = tracePropAccess(man, ["name"]);
- console.log(tracedMan.name); // GET name; semlinker
- console.log(tracedMan.age); // undefined
- tracedMan.name = "kakuqo"; // SET name=kakuqo
在以上示例中,我們定義了一個 tracePropAccess 函數(shù),該函數(shù)接收兩個參數(shù):obj 和 propKeys,它們分別表示需跟蹤的目標和需跟蹤的屬性列表。調用 tracePropAccess 函數(shù)后,會返回一個代理對象,當我們訪問被跟蹤的屬性時,控制臺就會輸出相應的訪問日志。
4.5 隱藏屬性
創(chuàng)建 hideProperty 函數(shù)
- const hideProperty = (target, prefix = "_") =>
- new Proxy(target, {
- has: (obj, prop) => !prop.startsWith(prefix) && prop in obj,
- ownKeys: (obj) =>
- Reflect.ownKeys(obj).filter(
- (prop) => typeof prop !== "string" || !prop.startsWith(prefix)
- ),
- get: (obj, prop, rec) => (prop in rec ? obj[prop] : undefined),
- });
使用 hideProperty 函數(shù)
- const man = hideProperty({
- name: "阿寶哥",
- _pwd: "www.semlinker.com",
- });
- console.log(man._pwd); // undefined
- console.log('_pwd' in man); // false
- console.log(Object.keys(man)); // [ 'name' ]
通過觀察以上的輸出結果,我們可以知道,利用 Proxy API,我們實現(xiàn)了指定前綴屬性的隱藏。除了能實現(xiàn)隱藏屬性之外,利用 Proxy API,我們還可以實現(xiàn)驗證屬性值的功能。
4.6 驗證屬性值
創(chuàng)建 validatedUser 函數(shù)
- const validatedUser = (target) =>
- new Proxy(target, {
- set(target, property, value) {
- switch (property) {
- case "email":
- const regex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
- if (!regex.test(value)) {
- console.error("The user must have a valid email");
- return false;
- }
- break;
- case "age":
- if (value < 20 || value > 80) {
- console.error("A user's age must be between 20 and 80");
- return false;
- }
- break;
- }
- return Reflect.set(...arguments);
- },
- });
使用 validatedUser 函數(shù)
- let user = {
- email: "",
- age: 0,
- };
- user = validatedUser(user);
- user.email = "semlinker.com"; // The user must have a valid email
- user.age = 100; // A user's age must be between 20 and 80
上述代碼成功執(zhí)行后,控制臺會輸出以下結果:
- The user must have a valid email
- A user's age must be between 20 and 80
介紹完 Proxy 對象的使用場景之后,我們來繼續(xù)介紹與 Proxy 對象相關的一些問題。
五、Proxy 相關問題
5.1 this 的指向問題
- const target = {
- foo() {
- return {
- thisIsTarget: this === target,
- thisIsProxy: this === proxy,
- };
- },
- };
- const handler = {};
- const proxy = new Proxy(target, handler);
- console.log(target.foo()); // { thisIsTarget: true, thisIsProxy: false }
- console.log(proxy.foo()); // { thisIsTarget: false, thisIsProxy: true }
上述代碼成功執(zhí)行后,控制臺會輸出以下結果:
- { thisIsTarget: true, thisIsProxy: false }
- { thisIsTarget: false, thisIsProxy: true }
通過以上輸出的結果,foo 方法中的 this 指向與當前的調用者有關??雌饋硗唵蔚?,但在一些場景下如果稍不注意的話,就會出現(xiàn)問題,比如以下這個示例:
- const _name = new WeakMap();
- class Person {
- constructor(name) {
- _name.set(this, name);
- }
- get name() {
- return _name.get(this);
- }
- }
在以上示例中,我們使用 WeakMap 對象來存儲 Person 對象的私有信息。定義完 Person類,我們就可以通過以下方式來使用它:
- const man = new Person("阿寶哥");
- console.log(man.name); // 阿寶哥
- const proxy = new Proxy(man, {});
- console.log(proxy.name); // undefined
對于以上的代碼,當我們通過 proxy 對象來訪問 name 屬性時,你會發(fā)現(xiàn)輸出的結果是undefined。這是因為當使用 proxy.name 的方式訪問 name 屬性時,this 指向的是 proxy 對象,而 _name WeakMap 對象中存儲的是 man 對象,所以輸出的結果是 undefined。
然而,對于以上的問題,如果我們按照以下方式定義 Person 類,就不會出現(xiàn)以上問題:
- class Person {
- constructor(name) {
- this._name = name;
- }
- get name() {
- return this._name;
- }
- }
- const man = new Person("阿寶哥");
- console.log(man.name); // 阿寶哥
- const proxy = new Proxy(man, {});
- console.log(proxy.name); // 阿寶哥
另外,如果你對 WeakMap 感興趣的話,可以閱讀 你不知道的 WeakMap 這篇文章。
5.2 get 捕獲器 receiver 參數(shù)是什么
- const p = new Proxy(target, {
- get: function(target, property, receiver) {
- // receiver
- }
- });
get 捕獲器用于攔截對象的讀取屬性操作,該捕獲器含有三個參數(shù):
為了更好地了解 receiver 參數(shù)的描述信息,我們來舉個具體的示例:
- const proxy = new Proxy({},
- {
- get: function (target, property, receiver) {
- return receiver;
- },
- }
- );
- console.dir(proxy.getReceiver === proxy); // true
- var inherits = Object.create(proxy);
- console.dir(inherits.getReceiver === inherits); // true
那么我們能否改變 receiver 指向的對象呢?答案是可以的,通過 Reflect 對象提供的get 方法,我們可以動態(tài)設置 receiver 對象的值,具體使用方式如下所示:
- console.dir(Reflect.get(proxy, "getReceiver", "阿寶哥"));
其實 receiver 的名稱是來源于 ECMAScript 規(guī)范:
以上的 [[Get]] 和 [[Set]] 被稱為內(nèi)部方法,ECMAScript 引擎中的每個對象都與一組內(nèi)部方法相關聯(lián),這些內(nèi)部方法定義了其運行時行為。
需要注意的是,這些內(nèi)部方法不是 ECMAScript 語言的一部分。對于對象的訪問器屬性來說,在執(zhí)行內(nèi)部代碼時,Receiver 將被作為 this 的值,同樣使用 Reflect 對象提供的 API,我們也可以通過設置 receiver 參數(shù)的值來改變 this 的值:
- const obj = {
- get foo() {
- return this.bar;
- },
- };
- console.log(Reflect.get(obj, "foo")); // undefined
- console.log(Reflect.get(obj, "foo", { bar: 2021 })); // 2021
5.3 包裝內(nèi)置構造函數(shù)的實例
當使用 Proxy 包裝內(nèi)置構造函數(shù)實例的時候,可能會出現(xiàn)一些問題。比如使用 Proxy 代理 Date 構造函數(shù)的實例:
- const target = new Date();
- const handler = {};
- const proxy = new Proxy(target, handler);
- proxy.getDate(); // Error
當以上代碼運行后,控制臺會輸出以下異常信息:
- proxy.getDate();
- ^
- TypeError: this is not a Date object.
出現(xiàn)以上問題的原因是因為有些原生對象的內(nèi)部屬性,只有通過正確的 this 才能拿到,所以 Proxy 無法代理這些原生對象的屬性。那么如何解決這個問題呢?要解決這個問題,我們可以為 getDate 方法綁定正確的 this:
- const target = new Date();
- const handler = {
- get(target, property, receiver) {
- if (property === "getDate") {
- return target.getDate.bind(target);
- }
- return Reflect.get(target, property, receiver);
- },
- };
- const proxy = new Proxy(target, handler);
- console.log(proxy.getDate());
5.4 創(chuàng)建可撤銷的代理對象
通過 Proxy.revocable() 方法可以用來創(chuàng)建一個可撤銷的代理對象,該方法的簽名為:
- Proxy.revocable(target, handler);
相關的參數(shù)說明如下:
調用 Proxy.revocable 方法之后,其返回值是一個對象,其結構為:{"proxy": proxy, "revoke": revoke},其中:
了解完 revocable 方法之后,我們來舉一個具體的示例:
- const target = {};
- const handler = {};
- const { proxy, revoke } = Proxy.revocable(target, handler);
- proxy.name = "阿寶哥";
- console.log(proxy.name); // 阿寶哥
- revoke();
- console.log(proxy.name); // TypeError: Revoked
當以上代碼成功運行之后,控制臺會輸出以下內(nèi)容:
- 阿寶哥
- Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
- at
通過觀察以上的結果,我們可知當 proxy 對象被撤銷之后,我們就沒有辦法對已撤銷的proxy 對象執(zhí)行任何操作。
六、Proxy 在開源項目中的應用
因為 Proxy 對象能夠提供強大的攔截能力,所以它被應用在一些成熟的開源項目中,用于實現(xiàn)響應式的功能,比如 vue-next 和 observer-util 項目。對于 observer-util 這個項目,阿寶哥已經(jīng)寫了一篇 從觀察者模式到響應式的設計原理 的文章來介紹該項目,感興趣的小伙伴可以自行閱讀。
而對于 vue-next 項目來說,響應式的功能被封裝在 @vue/reactivity 模塊中,該模塊為我們提供了一個 reactive 函數(shù)來創(chuàng)建響應式對象。下面我們來簡單了解一下 reactive函數(shù)的實現(xiàn):
- // packages/reactivity/src/reactive.ts
- export function reactive(target: object) {
- if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
- return target
- }
- return createReactiveObject(
- target,
- false,
- mutableHandlers,
- mutableCollectionHandlers
- )
- }
在 reactive 函數(shù)內(nèi)部,會繼續(xù)調用 createReactiveObject 函數(shù)來創(chuàng)建響應式對象,該函數(shù)也是被定義在 reactive.ts 文件中,該函數(shù)的的具體實現(xiàn)如下:
- // packages/reactivity/src/reactive.ts
- function createReactiveObject(
- target: Target,
- isReadonly: boolean,
- baseHandlers: ProxyHandler
, - collectionHandlers: ProxyHandler
- ) {
- // 省略部分代碼
- const proxyMap = isReadonly ? readonlyMap : reactiveMap
- const existingProxy = proxyMap.get(target)
- if (existingProxy) {
- return existingProxy
- }
- const proxy = new Proxy(
- target,
- targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
- )
- proxyMap.set(target, proxy)
- return proxy
- }
在 createReactiveObject 函數(shù)內(nèi)部,我們終于見到了期待已久的 Proxy 對象。當 target 對象不是集合類型的對象,比如 Map、Set、WeakMap 和 WeakSet 時,在創(chuàng)建 Proxy 對象時,使用的是 baseHandlers,該 handler 對象定義了以下 5 種捕獲器:
- export const mutableHandlers: ProxyHandler
- get,
- set,
- deleteProperty,
- has,
- ownKeys
- }
其中 get 和 set 捕獲器是分別用于收集 effect 函數(shù)和觸發(fā) effect 函數(shù)的執(zhí)行。好了,這里阿寶哥只是介紹一下 @vue/reactivity 中的 reactive 函數(shù),關于該模塊是如何實現(xiàn)響應式的細節(jié),這里就不展開介紹了,阿寶哥后續(xù)會單獨寫一篇文章來詳細分析該模塊的功能。
七、參考資源MDN - Proxy
MDN - Reflect
exploringjs - proxies
stackoverflow - what-is-a-receiver-in-javascript

我們在微信上24小時期待你的聲音
解答本文疑問/技術咨詢/運營咨詢/技術建議/互聯(lián)網(wǎng)交流