自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

溫故而知新:你可能不知道的 Proxy

開(kāi)發(fā) 前端
本文主要是介紹Proxy以及配合Reflect的簡(jiǎn)單使用,再?gòu)腅CMAScript規(guī)范講起內(nèi)部方法、內(nèi)部槽以及普通對(duì)象、異質(zhì)對(duì)象的定義,進(jìn)而了解了Proxy能實(shí)現(xiàn)代理的內(nèi)部實(shí)現(xiàn)邏輯。

寫在最前面

我們都知道Vue2的響應(yīng)式系統(tǒng)是利用Object.defineProperty進(jìn)行數(shù)據(jù)劫持實(shí)現(xiàn)的,但是其本身語(yǔ)法有如以下幾個(gè)缺陷:

  • 對(duì)普通對(duì)象的監(jiān)聽(tīng)需要遍歷每一個(gè)屬性
  • 無(wú)法監(jiān)聽(tīng)數(shù)組的變動(dòng)
  • 無(wú)法監(jiān)聽(tīng)Map/Set數(shù)據(jù)結(jié)構(gòu)的變動(dòng)
  • 無(wú)法對(duì)對(duì)象新增/刪除的屬性進(jìn)行監(jiān)聽(tīng)

針對(duì)此,Vue3使用了Proxy實(shí)現(xiàn)的數(shù)據(jù)響應(yīng)式,并將其獨(dú)立成@vue/reactivity 模塊。因此,要了解學(xué)習(xí) Vue3的響應(yīng)式系統(tǒng),對(duì)Proxy的掌握尤為重要。閱讀完本文,我們可以學(xué)習(xí)到:

  • Proxy對(duì)象的基本用法
  • Proxy 能實(shí)現(xiàn)對(duì)對(duì)象的代理的工作原理

Proxy簡(jiǎn)介

首先,我們來(lái)看下Proxy在MDN上的定義:

Proxy 對(duì)象用于創(chuàng)建一個(gè)對(duì)象的代理,從而實(shí)現(xiàn)基本操作的攔截和自定義(如屬性查找、賦值、枚舉、函數(shù)調(diào)用等)。

其基本語(yǔ)法如下:

const p = new Proxy(target, handler);

參數(shù)說(shuō)明:

target: 即我們要代理的對(duì)象。我們都知道在JS里“萬(wàn)物皆對(duì)象”,因此這個(gè)target 可以是任何類型的對(duì)象,包括原生數(shù)組,函數(shù),甚至另一個(gè)Proxy對(duì)象。同時(shí),請(qǐng)注意到定義里的關(guān)鍵詞“用于創(chuàng)建一個(gè)對(duì)象的代理”,因此Proxy只能代理對(duì)象,任何原始值類型都是無(wú)法代理的 。如對(duì)number, boolean類型的原始值代理都會(huì)得到 “Cannot create proxy with a non-object as target or handler”的錯(cuò)誤:

圖片

handler:其是一個(gè)屬性全部為函數(shù)類型的對(duì)象。這些函數(shù)類型的屬性 ,也 稱之為捕獲器(trap),其作用就是為了實(shí)現(xiàn)定義里說(shuō)的“基本操作的攔截和自定義(如屬性查找、賦值、枚舉、函數(shù)調(diào)用等)”,注意,這里的攔截其實(shí)是對(duì)代理對(duì)象p的基本操作攔截,而并不是對(duì)被代理的對(duì)象target的攔截(至于為什么,會(huì)在接下來(lái)的工作原理章節(jié) 進(jìn)行解釋)。handler對(duì)象總共有以下截圖共計(jì)13個(gè)屬性方法(trap):

圖片

基本用法如下 :

const obj = {
foo: 'bar',
fn () {
console.log('fn調(diào)用了');
}
};
const handler = {
get (target, key) {
console.log(`我被讀取了${key}屬性`);
return target[key];
},
set (target, key, val) {
console.log(`我被設(shè)置了${key}屬性, val: ${val}`);
target[key] = val;
},
apply (target, thisArg, argumentsList) {
console.log('fn調(diào)用被攔截');
return target.call(thisArg, ...argumentsList);
}
};
const p = new Proxy(obj, handler);
p.foo; // 輸出:我被讀取了foo屬性
p.foo = 'bar1'; // 輸出:我被設(shè)置了foo屬性, val: bar1
p.fn(); // 輸出:我被讀取了fn屬性 fn調(diào)用了

在上述 代碼中,我們只是實(shí)現(xiàn)了13個(gè)方法其中的get/set/apply,這3個(gè)trap的含義分別是:屬性讀取操作的捕捉器、屬性設(shè)置操作的捕捉器、函數(shù)調(diào)用操作的捕捉器。關(guān)于其他10個(gè)方法(捕捉器 )的含義 在這里就不一一贅述了,感興趣的同學(xué)可以去MDN了解。

值得注意的是,在上述代碼中,并沒(méi)有攔截到obj.fn()函數(shù)調(diào)用操作,而卻是只是輸出了“我被讀取了fn屬性”。究其原因,我們可以再次從Proxy的定義里的關(guān)鍵詞“基本操作”找到答案 。那么何為基本操作呢?在上述代碼中就表明了對(duì)象屬性的讀?。╬.foo) 、設(shè)置(p.foo='xxx')就是基本操作,與之對(duì)應(yīng)的就是非基本操作,我們可以稱之為復(fù)合操作。而obj.fn()就是一個(gè)典型的復(fù)合操作,它是由兩個(gè)基本操作組成的分別是讀取操作(obj.fn), 和函數(shù)調(diào)用操作(取到obj.fn的值再進(jìn)行調(diào)用),而我們代理的對(duì)象是obj,并不是obj.fn。因此,我們只能攔截到fn屬性的讀取操作。這也說(shuō)明了Proxy只能對(duì)對(duì)象的基本操作進(jìn)行代理,這點(diǎn)尤為重要。

下面的代碼表明函數(shù)的調(diào)用也是基本操作,是可以被apply攔截到的:

const handler = {
apply (target, thisArg, argumentsList) {
console.log('函數(shù)調(diào)用被攔截');
return target.call(thisArg, ...argumentsList);
}
};
new Proxy(() => {}, handler)(); // 輸出:函數(shù)調(diào)用被攔截

Reflex和 Proxy

首先還是要來(lái)看下Reflex在MDN里的定義:

Reflect 是一個(gè)內(nèi)置的對(duì)象,它提供攔截 JavaScript 操作的方法。這些方法與proxy handlers 的方法相同.

不難發(fā)現(xiàn),Reflex對(duì)象的方法和proxy的攔截器(第二個(gè)入?yún)andler)的方法完全一致,同樣有著13個(gè)方法:

圖片

那么,Reflect對(duì)象的作用是 什么呢,拿Reflect.get舉例簡(jiǎn)單來(lái)說(shuō)其作用之一就是提供了訪問(wèn)一個(gè)對(duì)象屬性的默認(rèn)行為,如以下代碼:

const obj = {foo: 'foo'};
obj.foo;
// 等同于
Reflect.get(obj, 'foo');

既然 作用一致 ,那么使用Reflect.get有何意義呢,在回答這個(gè)問(wèn)題之前,我們先看下以下代碼:

const obj = {
foo: 'foo',
get bar () {
return this.foo;
}
};
const handler = {
get (target, key, receiver) {
console.log(`我被讀取了${key}屬性`);
return target[key];
},
set (target, key, val, receiver) {
console.log(`我被設(shè)置了${key}屬性, val: ${val}`);
target[key] = val;
}
};
const p = new Proxy(obj, handler);
p.bar; // 輸出:我被讀取了bar屬性
// Q: 為什么讀取foo屬性沒(méi)有被攔截

在上述代碼中我們定義了一個(gè)foo屬性和bar屬性,其中bar屬性是一個(gè)訪問(wèn)器屬性,通過(guò)get函數(shù) return this.foo獲取得到 的,因此按理來(lái)說(shuō)我們?cè)谧x取bar屬性時(shí)候會(huì)觸發(fā)讀取foo屬性,也同樣會(huì)被get的trap所攔截到,但實(shí)際代碼運(yùn)行結(jié)果并沒(méi)有攔截到foo屬性。這是為什么呢,答案的關(guān)鍵在于bar訪問(wèn)器里的this指向。梳理下代碼運(yùn)行過(guò)程:p.bar 實(shí)際上會(huì)被handler的get捕獲 返回 target['bar'],而這里的target實(shí)際上就是obj,所以這時(shí)候bar訪問(wèn)器里的this指向obj,this.foo,實(shí)際就是obj.foo。而obj并不是proxy對(duì)象p,所以訪問(wèn)其foo屬性并不會(huì)被攔截到。

那么如何也能觸發(fā)到foo屬性的攔截呢,這時(shí)候Reflect就派上用場(chǎng)了,有以下代碼:

const obj = {
foo: 'foo',
get bar () {
return this.foo;
}
};
const handler = {
get (target, key, receiver) {
console.log(`我被讀取了${key}屬性`);
return Reflect.get(target, key, receiver);
},
set (target, key, val, receiver) {
console.log(`我被設(shè)置了${key}屬性, val: ${val}`);
return Reflect.set(target, key, val, receiver);
}
};
const p = new Proxy(obj, handler)
p.bar; // 輸出:我被讀取了bar屬性 我被讀取了foo屬性

如上面代碼所示,我們能正確地觸發(fā)了foo屬性的攔截,其實(shí)現(xiàn)的關(guān)鍵在于Reflect.get的第三個(gè)參數(shù)receiver ,其作用就是改變this指向,在MDN里有以下描述:

如果target對(duì)象中指定了getter,receiver則為getter調(diào)用時(shí)的this值。

而我們這里的receiver就是p對(duì)象,this.foo 等同于 p.foo,因此訪問(wèn)bar屬性的 時(shí)候同樣可以攔截得到。也正是因?yàn)閠his指向的問(wèn)題,所以建議在proxy對(duì)象攔截器里的屬性方法都通過(guò)Reflex.*去操作。

Proxy的工作原理

內(nèi)部方法和內(nèi)部槽

在Proxy簡(jiǎn)介章節(jié)里我們?cè)岬剑骸癙roxy只能代理對(duì)象”。那么不知道你有沒(méi)有想過(guò)這樣的一個(gè)問(wèn)題,在JS里對(duì)象的定義又是什么?關(guān)于這個(gè)問(wèn)題的答案,我們需要從ECMAScript規(guī)范里找到答案 :

在ecma262規(guī)范6.1.7.2章節(jié)開(kāi)頭給出這樣的定義:

The actual semantics of objects, in ECMAScript, are specified via algorithms called internal methods. Each object in an ECMAScript engine is associated with a set of internal methods that defines its runtime behaviour. These internal methods are not part of the ECMAScript language. They are defined by this specification purely for expository purposes. However, each object within an implementation of ECMAScript must behave as specified by the internal methods associated with it. The exact manner in which this is accomplished is determined by the implementation.

也就是說(shuō):對(duì)象的實(shí)際語(yǔ)義是通過(guò)稱為內(nèi)部方法(internal methods)的算法指定的。

那么 ,什么又是內(nèi)部方法呢。閱讀完本章節(jié),我們不難發(fā)現(xiàn),其實(shí)對(duì)象 不僅有內(nèi)部 方法(internal methods)還有內(nèi)部槽(Internal Slots),在ECMAScript規(guī)范里使用[[ xxx  ]]來(lái)表示內(nèi)部方法或者內(nèi)部槽:

Internal methods and internal slots are identified within this specification using names enclosed in double square brackets [[ ]].

內(nèi)部方法對(duì)JavaScript開(kāi)發(fā)者來(lái)說(shuō)是不可見(jiàn)的,但當(dāng)我們 對(duì)一個(gè)對(duì)象進(jìn)行操作時(shí),JS引擎則會(huì) 調(diào)用其內(nèi)部方法。舉個(gè)例子來(lái)說(shuō):當(dāng)我們?cè)L問(wèn)一個(gè)對(duì)象的屬性時(shí):

const obj = { foo: 'foo'};
obj.foo;

引擎內(nèi)部則會(huì)調(diào)用obj內(nèi)部方法[[ Get ]] 來(lái)獲取foo屬性值;

圖片

以下是 作為一個(gè)對(duì)象,其必要的11個(gè)基本內(nèi)部方法,也就是說(shuō)凡是對(duì)象,其必然部署了以下11個(gè)內(nèi)部方法:

圖片

當(dāng)然,不同的對(duì)象,可能部署了不同的內(nèi)部方法。比如說(shuō)函數(shù)也是對(duì)象,那如何區(qū)分函數(shù)和普通對(duì)象呢,或者說(shuō)對(duì)象怎么能像函數(shù)一樣被調(diào)用呢,答案是只要部署了[[ Call ]]這個(gè)內(nèi)部方法,那么這個(gè)對(duì)象就是函數(shù)對(duì)象,同時(shí)如果這個(gè)函數(shù)對(duì)象也部署了[[ Construct ]]內(nèi)部方法,那么這個(gè)函數(shù)對(duì)象也是構(gòu)造函數(shù)對(duì)象也就意味著其可以使用new操作符:

圖片

同時(shí)內(nèi)部方法又是具有多態(tài)性的,也就是說(shuō)不同的對(duì)象在對(duì)相同的內(nèi)部方法的實(shí)現(xiàn)可能有所差異:

Internal method names are polymorphic. This means that different object values may perform different algorithms when a common internal method name is invoked upon them. That actual object upon which an internal method is invoked is the “target” of the invocation. If, at runtime, the implementation of an algorithm attempts to use an internal method of an object that the object does not support, a TypeError exception is thrown.

舉個(gè)例子來(lái)說(shuō):Proxy對(duì)象和普通對(duì)象其都有內(nèi)部方法[[ Get ]] , 但是他們的 [[  Get ]]實(shí)現(xiàn) 邏輯卻是不同的,Proxy對(duì)象 的[[ Get ]]實(shí)現(xiàn)邏輯是由ecma262規(guī)范 10.5.8章節(jié)里定義的,而普通對(duì)象的[[ Get ]]實(shí)現(xiàn)邏輯是由ecma262規(guī)范 10.1.8章節(jié)里定義的.

普通對(duì)象和異質(zhì)對(duì)象

在上 一節(jié)我們了解到了對(duì)象都有內(nèi)部方法和內(nèi)部槽,不同的對(duì)象可能有不同的內(nèi)部方法或者內(nèi)部槽,而即便 有相同的內(nèi)部 方法,但是其內(nèi)部方法的內(nèi)部實(shí)現(xiàn)邏輯可能也有所不同。

實(shí)際上,通過(guò)閱讀ECMAScript規(guī)范,我們可以將JS的對(duì)象分為兩大類:普通對(duì)象(ordinary object)和異質(zhì)對(duì)象(exotic object),而區(qū)分一個(gè)對(duì)象是普通對(duì)象還是異質(zhì)對(duì)象的標(biāo)準(zhǔn)就是:內(nèi)部方法或者內(nèi)部槽的不同。那么什么是普通對(duì)象呢,根據(jù)定義滿足以下要求即是:

圖片

也就是說(shuō),一個(gè)普通對(duì)象需要滿足以下3點(diǎn):

其內(nèi)部方法的定義是符合ECMAScript規(guī)范10.1.x章節(jié)定義的,如下圖所示10個(gè)內(nèi)部方法:

圖片

如果這個(gè)對(duì)象有內(nèi)部方法[[ Call ]] 那么其應(yīng)該是由ECMAScript規(guī)范10.2.1章節(jié)定義的

如果這個(gè)對(duì)象有內(nèi)部方法[[ Construct ]] 那么其應(yīng)該是由ECMAScript規(guī)范10.2.2章節(jié)定義的

圖片

綜上,就是 一個(gè)普通對(duì)象的定義。而異質(zhì)對(duì)象的定義就較為簡(jiǎn)單了,只要一個(gè)對(duì)象不是普通對(duì)象,那它就是異質(zhì)對(duì)象。

An exotic object is an object that is not an ordinary object.

再聊Proxy

通過(guò)上兩個(gè)小節(jié)我們了解到了普通對(duì)象和異質(zhì)對(duì)象的定義,當(dāng)我們?cè)匍喿x規(guī)范時(shí)就不難發(fā)現(xiàn)其實(shí)Proxy對(duì)象就是一個(gè)異質(zhì)對(duì)象,因?yàn)镻roxy對(duì)象的內(nèi)部方法是在10.5.x章節(jié)進(jìn)行定義的,并不滿足普通對(duì)象的定義:

圖片

Proxy是如何實(shí)現(xiàn)代理對(duì)象的,其實(shí)是和它的內(nèi)部方法實(shí)現(xiàn)邏輯息息相關(guān)的。還是拿代碼舉例來(lái)說(shuō)明:

const obj = {
foo: 'foo',
};
const handler = {
};
const p = new Proxy(obj, {});
p.foo; // 輸出:foo

在上述代碼中,我們的handler是一個(gè)空對(duì)象,但是它具體是如何實(shí)現(xiàn)代理的,但是Proxy對(duì)象p仍能實(shí)現(xiàn)對(duì)對(duì)象obj的代理,具體點(diǎn)來(lái)講p.foo 的值為什么和obj.foo的值等同。

通過(guò)上兩節(jié)學(xué)習(xí),我們知道對(duì)象屬性的讀取操作會(huì)觸發(fā)引擎內(nèi)部對(duì)這個(gè)對(duì)象的內(nèi)部方法[[ Get ]]的調(diào)用,那就讓我們看下Proxy的[[ Get ]]內(nèi)部方法:

圖片

這里我們重點(diǎn)看第5-7步,結(jié)合我們的代碼,簡(jiǎn)而言之,當(dāng)我們讀取p.foo時(shí),首選會(huì)檢查p對(duì)象有無(wú)get的trap 如果沒(méi)有,則會(huì)調(diào)用被代理的對(duì)象obj(target)的[[ Get ]]內(nèi)部方法,如果有則會(huì)調(diào)用handler的get 方法并將其調(diào)用結(jié)果 返回。

因此,我們可以得出一個(gè)結(jié)論:創(chuàng)建代理對(duì)象p時(shí)指定的攔截器handler,實(shí)際上是用來(lái)自定義這個(gè)代理對(duì)象p本身的操作行為,并不是攔截自定義被代理對(duì)象obj的操作行為的。這正是體現(xiàn)了代理透明性質(zhì),也解釋了我們?cè)?Proxy簡(jiǎn)介里提到的問(wèn)題:攔截其實(shí)是對(duì)代理對(duì)象p的基本操作攔截,而并不是對(duì)被代理的對(duì)象target的攔截。

總結(jié)

本文主要是介紹Proxy以及配合Reflect的簡(jiǎn)單使用,再?gòu)腅CMAScript規(guī)范講起內(nèi)部方法、內(nèi)部槽以及普通對(duì)象、異質(zhì)對(duì)象的定義,進(jìn)而了解了Proxy能實(shí)現(xiàn)代理的內(nèi)部實(shí)現(xiàn)邏輯。

參考文獻(xiàn)

Proxy - JavaScript | MDN (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)

Reflect - JavaScript | MDN (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect)

ECMAScript? 2023 Language Specification (https://tc39.es/ecma262/)

Vue.js設(shè)計(jì)與實(shí)現(xiàn) (https://www.ituring.com.cn/book/2953)

作者:張宇航,微醫(yī)前端技術(shù)部,一個(gè)不文藝的處女座程序員。

責(zé)任編輯:武曉燕 來(lái)源: 微醫(yī)大前端技術(shù)
相關(guān)推薦

2021-04-28 08:06:20

MeasureSpecView測(cè)量

2012-03-26 10:12:25

C#

2012-11-23 10:57:44

Shell

2015-08-13 09:03:14

調(diào)試技巧

2019-11-20 10:25:06

sudoLinux

2020-01-29 19:40:36

Python美好,一直在身邊Line

2021-01-05 11:22:58

Python字符串代碼

2023-02-27 09:20:24

絕對(duì)定位CSS

2023-01-29 09:46:47

Dialog彈窗模態(tài)

2014-12-08 10:39:15

2019-11-25 14:05:47

Python裝飾器數(shù)據(jù)

2021-07-12 07:59:06

安全 HTML 屬性

2021-12-17 00:10:00

ChromeDevtools功能

2021-02-01 23:23:39

FiddlerCharlesWeb

2018-05-10 11:50:13

Docker容器冷知識(shí)

2023-08-01 14:36:00

JavaScript開(kāi)發(fā)

2020-03-05 11:10:18

Left join數(shù)據(jù)庫(kù)MySQL

2010-08-06 13:15:35

2010-07-26 13:24:11

2020-05-09 08:48:21

JavaScript原生方法代碼
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)