ES6 如何改變JS內(nèi)置行為的代理與反射
代理(Proxy)可以攔截并改變 JS 引擎的底層操作,如數(shù)據(jù)讀取、屬性定義、函數(shù)構(gòu)造等一系列操作。ES6 通過(guò)對(duì)這些底層內(nèi)置對(duì)象的代理陷阱和反射函數(shù),讓開(kāi)發(fā)者能進(jìn)一步接近 JS 引擎的能力。
一、代理與反射的基本概念
什么是代理和反射呢?
代理是用來(lái)替代另一個(gè)對(duì)象(target),JS 通過(guò)new Proxy()創(chuàng)建一個(gè)目標(biāo)對(duì)象的代理,該代理與該目標(biāo)對(duì)象表面上可以被當(dāng)作同一個(gè)對(duì)象來(lái)對(duì)待。
當(dāng)目標(biāo)對(duì)象上的進(jìn)行一些特定的底層操作時(shí),代理允許你攔截這些操作并且覆寫(xiě)它,而這原本只是 JS 引擎的內(nèi)部能力。
如果你對(duì)些代理&反射的概念比較困惑的話(huà),可以直接看后面的應(yīng)用示例,最后再重新看這些定義就會(huì)更清晰!
攔截行為使用了一個(gè)能夠響應(yīng)特定操作的函數(shù)( 被稱(chēng)為陷阱),每個(gè)代理陷阱對(duì)應(yīng)一個(gè)反射(Reflect)方法。
ES6 的反射 API 以 Reflect 對(duì)象的形式出現(xiàn),對(duì)象每個(gè)方法都與對(duì)應(yīng)的陷阱函數(shù)同名,并且接收的參數(shù)也與之一致。以下是 Reflect 對(duì)象的一些方法:
| 代理陷阱 | 覆寫(xiě)的特性 | 方法 | |
|---|---|---|---|
| get | 讀取一個(gè)屬性的值 | Reflect.get() | |
| set | 寫(xiě)入一個(gè)屬性 | Reflect.set() | |
| has | in 運(yùn)算符 | Reflect.has() | |
| deleteProperty | delete 運(yùn)算符 | Reflect.deleteProperty() | |
| getPrototypeOf | Object.getPrototypeOf() | Reflect.getPrototypeOf() | |
| isExtensible | Object.isExtensible() | Reflect.isExtensible() | |
| defineProperty | Object.defineProperty() | Reflect.defineProperty | |
| apply | 調(diào)用一個(gè)函數(shù) | Reflect.apply() | |
| construct | 使用 new 調(diào)用一個(gè)函數(shù) | Reflect.construct() |
每個(gè)陷阱函數(shù)都可以重寫(xiě) JS 對(duì)象的一個(gè)特定內(nèi)置行為,允許你攔截并修改它。
綜合來(lái)說(shuō),想要控制或改變JS的一些底層操作,可以先創(chuàng)建一個(gè)代理對(duì)象,在這個(gè)代理對(duì)象上掛載一些陷阱函數(shù),陷阱函數(shù)里面有反射方法。通過(guò)接下來(lái)的應(yīng)用示例可以更清晰的明白代理的過(guò)程。
二、開(kāi)始一個(gè)簡(jiǎn)單的代理
當(dāng)你使用 Proxy 構(gòu)造器來(lái)創(chuàng)建一個(gè)代理時(shí),需要傳遞兩個(gè)參數(shù):目標(biāo)對(duì)象(target)以及一個(gè)處理器( handler),
先創(chuàng)建一個(gè)僅進(jìn)行傳遞的代理如下:
// 目標(biāo)對(duì)象
let target = {};
// 代理對(duì)象
let proxy = new Proxy(target, {});
proxy.name = "hello";
console.log(proxy.name); // "hello"
console.log(target.name); // "hello"
target.name = "world";
console.log(proxy.name); // "world"
console.log(target.name); // "world
上例中的 proxy 代理對(duì)象將所有操作直接傳遞給 target 目標(biāo)對(duì)象,代理對(duì)象 proxy 自身并沒(méi)有存儲(chǔ)該屬性,它只是簡(jiǎn)單將值傳遞給 target 對(duì)象,proxy.name 與 target.name 的屬性值總是相等,因?yàn)樗鼈兌贾赶?target.name。
此時(shí)代理陷阱的處理器為空對(duì)象,當(dāng)然處理器可以定義了一個(gè)或多個(gè)陷阱函數(shù)。
2.1 set 驗(yàn)證對(duì)象屬性的存儲(chǔ)
假設(shè)你想要?jiǎng)?chuàng)建一個(gè)對(duì)象,并要求其屬性值只能是數(shù)值,這就意味著該對(duì)象的每個(gè)新增屬性
都要被驗(yàn)證,并且在屬性值不為數(shù)值類(lèi)型時(shí)應(yīng)當(dāng)拋出錯(cuò)誤。
這時(shí)需要使用 set 陷阱函數(shù)來(lái)攔截傳入的 value,該陷阱函數(shù)能接受四個(gè)參數(shù):
- trapTarget :將接收屬性的對(duì)象( 即代理的目標(biāo)對(duì)象)
- key :需要寫(xiě)入的屬性的鍵( 字符串類(lèi)型或符號(hào)類(lèi)型)
- value :將被寫(xiě)入屬性的值;
- receiver :操作發(fā)生的對(duì)象( 通常是代理對(duì)象)
set 陷阱對(duì)應(yīng)的反射方法和默認(rèn)特性是Reflect.set(),和陷阱函數(shù)一樣接受這四個(gè)參數(shù),并會(huì)基于操作是否成功而返回相應(yīng)的結(jié)果:
let targetObj = {};
let proxyObj = new Proxy(targetObj, {
set: set
});
/* 定義 set 陷阱函數(shù) */
function set (trapTarget, key, value, receiver) {
if (isNaN(value)) {
throw new TypeError("Property " + key + " must be a number.");
}
return Reflect.set(trapTarget, key, value, receiver);
}
/* 測(cè)試 */
proxyObj.count = 123;
console.log(proxyObj.count); // 123
console.log(targetObj.count); // 123
proxyObj.anotherName = "proxy" // TypeError: Property anotherName must be a number.
示例中set 陷阱函數(shù)成功攔截傳入的 value 值,你可以嘗試一下,如果注釋或不return Reflect.set()會(huì)發(fā)生什么?,答案是攔截陷阱就不會(huì)有反射響應(yīng)。
需要注意的是,直接給 targetObj 目標(biāo)對(duì)象賦值時(shí)是不會(huì)觸發(fā) set 代理陷阱的,需要通過(guò)給代理對(duì)象賦值才會(huì)觸發(fā) set 代理陷阱與反射。
2.2 get 驗(yàn)證對(duì)象屬性的讀取
JS 非常有趣的特性之一,是讀取不存在的屬性時(shí)并不會(huì)拋出錯(cuò)誤,而會(huì)把undefined當(dāng)作該屬性的值。
對(duì)于大型的代碼庫(kù),當(dāng)屬性名稱(chēng)存在書(shū)寫(xiě)錯(cuò)誤時(shí)(不會(huì)拋錯(cuò))會(huì)導(dǎo)致嚴(yán)重的問(wèn)題。這時(shí)使用 get 代理陷阱驗(yàn)證對(duì)象結(jié)構(gòu)(Object Shape),訪問(wèn)不存在的屬性時(shí)就拋出錯(cuò)誤,使對(duì)象結(jié)構(gòu)驗(yàn)證變得簡(jiǎn)單。
get 陷阱函數(shù)會(huì)在讀取屬性時(shí)被調(diào)用,即使該屬性在對(duì)象中并不存在,它能接受三個(gè)參數(shù):
- trapTarget :將會(huì)被讀取屬性的對(duì)象( 即代理的目標(biāo)對(duì)象)
- key :需要讀取的屬性的鍵( 字符串類(lèi)型或符號(hào)類(lèi)型)
- receiver :操作發(fā)生的對(duì)象( 通常是代理對(duì)象)
Reflect.get()方法接受與之相同的參數(shù),并返回默認(rèn)屬性的默認(rèn)值。
let proxyObj = new Proxy(targetObj, {
set: set,
get: get
});
/* 定義 get 陷阱函數(shù) */
function get(trapTarget, key, receiver) {
if (!(key in receiver)) {
throw new TypeError("Property " + key + " doesn't exist.");
}
return Reflect.get(trapTarget, key, receiver);
}
console.log(proxyObj.count); // 123
console.log(proxyObj.newcount) // TypeError: Property newcount doesn't exist.
這段代碼允許添加新的屬性,并且此后可以正常讀取該屬性的值,但當(dāng)讀取的屬性并
不存在時(shí),程序拋出了一個(gè)錯(cuò)誤,而不是將其默認(rèn)為undefined。
還可以使用 has 陷阱驗(yàn)證in運(yùn)算符,使用 deleteProperty 陷阱函數(shù)避免屬性被delete刪除。
注:in運(yùn)算符用于判斷對(duì)象中是否存在某個(gè)屬性,如果自有屬性或原型屬性匹配這個(gè)名稱(chēng)字符串或Symbol,那么in運(yùn)算符返回 true。
targetObj = {
name: 'targetObject'
};
console.log("name" in targetObj); // true
console.log("toString" in targetObj); // true
其中 name 是對(duì)象自身的屬性,而 toString 則是原型屬性( 從 Object 對(duì)象上繼承而來(lái)),所以檢測(cè)結(jié)果都為 true。
has 陷阱函數(shù)會(huì)在使用in運(yùn)算符時(shí)被調(diào)用,并且會(huì)傳入兩個(gè)參數(shù)(同名反射Reflect.has()方法也一樣):
- trapTarget :需要讀取屬性的對(duì)象( 代理的目標(biāo)對(duì)象)
- key :需要檢查的屬性的鍵( 字符串類(lèi)型或 Symbol符號(hào)類(lèi)型)
deleteProperty 陷阱函數(shù)會(huì)在使用delete運(yùn)算符去刪除對(duì)象屬性時(shí)下被調(diào)用,并且也會(huì)被傳入兩個(gè)參數(shù)(Reflect.deleteProperty() 方法也接受這兩個(gè)參數(shù)):
- trapTarget :需要?jiǎng)h除屬性的對(duì)象( 即代理的目標(biāo)對(duì)象) ;
- key :需要?jiǎng)h除的屬性的鍵( 字符串類(lèi)型或符號(hào)類(lèi)型) 。
一些思考:分析過(guò) Vue 源碼的都了解過(guò),給一個(gè) Vue 實(shí)例中掛載的 data,是通過(guò)Object.defineProperty代理 vm._data 中的對(duì)象屬性,實(shí)現(xiàn)雙向綁定...... 同理可以考慮使用 ES6 的 Proxy 的 get 和 set 陷阱實(shí)現(xiàn)這個(gè)代理。
三、對(duì)象屬性陷阱
3.1 數(shù)據(jù)屬性與訪問(wèn)器屬性
ES5 最重要的特征之一就是引入了 Object.defineProperty() 方法定義屬性的特性。屬性的特性是為了實(shí)現(xiàn)javascript引擎用的,屬于內(nèi)部值,因此不能直接訪問(wèn)他們。
屬性分為數(shù)據(jù)屬性和訪問(wèn)器屬性。使用Object.defineProperty()方法修改數(shù)據(jù)屬性的特性值的示例如下:
let obj1 = {
name: 'myobj',
}
/* 數(shù)據(jù)屬性*/
Object.defineProperty(obj1,'name',{
configurable: false, // default true
writable: false, // default true
enumerable: true, // default true
value: 'jenny' // default undefined
})
console.log(obj1.name) // 'jenny'
其中[[Configurable]] 表示能否通過(guò) delete 刪除屬性從而重新定義為訪問(wèn)器屬性;[[Enumerable]] 表示能否通過(guò)for-in循環(huán)返回屬性;[[Writable]] 表示能否修改屬性的值; [[Value]] 包含這個(gè)屬性的數(shù)據(jù)值。
對(duì)于訪問(wèn)器屬性,該屬性不包含數(shù)據(jù)值,包含一對(duì)getter和setter函數(shù),定義訪問(wèn)器屬性必須使用Object.defineProperty()方法:
let obj2 = {
age: 18
}
/* 訪問(wèn)器屬性 */
Object.defineProperty(obj2,'_age',{
configurable: false, // default true
enumerable: false, // default true
get () { // default undefined
return this.age
},
set (num) { // default undefined
this.age = num
}
})
/* 修改訪問(wèn)器屬性調(diào)用 getter */
obj2._age = 20
console.log(obj2.age) // 20
/* 輸出訪問(wèn)器屬性 */
console.log(Object.getOwnPropertyDescriptor(obj2,'_age'))
// { get: [Function: get],
// set: [Function: set],
// enumerable: false,
// configurable: false }
[[Get]] 在讀取屬性時(shí)調(diào)用的函數(shù), [[Set]] 再寫(xiě)入屬性時(shí)調(diào)用的函數(shù)。使用訪問(wèn)器屬性的常用方式,是設(shè)置一個(gè)屬性的值導(dǎo)致其他屬性發(fā)生變化。
3.2 檢查屬性的修改
代理允許你使用 defineProperty 同名函數(shù)陷阱函數(shù)攔截Object.defineProperty()的調(diào)用,defineProperty 陷阱函數(shù)接受下列三個(gè)參數(shù):
- trapTarget :需要被定義屬性的對(duì)象( 即代理的目標(biāo)對(duì)象);
- key :屬性的鍵( 字符串類(lèi)型或符號(hào)類(lèi)型);
- descriptor :為該屬性準(zhǔn)備的描述符對(duì)象。
defineProperty 陷阱函數(shù)要求在操作后返回一個(gè)布爾值用于判斷操作是否成功,如果返回了 false 則拋出錯(cuò)誤,故可以使用該功能來(lái)限制哪些屬性可以被Object.defineProperty() 方法定義。
例如,如果想阻止定義Symbol符號(hào)類(lèi)型的屬性,你可以檢查傳入的屬性值,若是則返回 false:
/* 定義代理 */
let proxy = new Proxy({}, {
defineProperty(trapTarget, key, descriptor) {
if (typeof key === "symbol") {
return false;
}
return Reflect.defineProperty(trapTarget, key, descriptor);
}
});
Object.defineProperty(proxy, "name", {
value: "proxy"
});
console.log(proxy.name); // "proxy"
let nameSymbol = Symbol("name");
// 拋出錯(cuò)誤
Object.defineProperty(proxy, nameSymbol, {
value: "proxy"
})
四、函數(shù)代理
4.1 構(gòu)造函數(shù) & 立即執(zhí)行
函數(shù)的兩個(gè)內(nèi)部方法:[[Call]] 與[[Construct]]會(huì)在函數(shù)被調(diào)用時(shí)調(diào)用,通過(guò)代理函數(shù)來(lái)為這兩個(gè)內(nèi)部方法設(shè)置陷阱,從而控制函數(shù)的行為。
[[Construct]]會(huì)在函數(shù)被使用new運(yùn)算符調(diào)用時(shí)執(zhí)行,代理觸發(fā)construct()陷阱函數(shù),并和Reflect.construct()一樣接收到下列兩個(gè)參數(shù):
- trapTarget :被執(zhí)行的函數(shù)( 即代理的目標(biāo)對(duì)象) ;
- argumentsList :被傳遞給函數(shù)的參數(shù)數(shù)組。
[[Call]]會(huì)在函數(shù)被直接調(diào)用時(shí)執(zhí)行,代理觸發(fā)apply()陷阱函數(shù),它和Reflect.apply()都接收三個(gè)參數(shù):
- trapTarget :被執(zhí)行的函數(shù)( 代理的目標(biāo)函數(shù)) ;
- thisArg :調(diào)用過(guò)程中函數(shù)內(nèi)部的 this 值;
- argumentsList :被傳遞給函數(shù)的參數(shù)數(shù)組。
每個(gè)函數(shù)都包含call()和apply()方法,用于重置函數(shù)運(yùn)行的作用域即 this 指向,區(qū)別只是接收參數(shù)的方式不同:call()的參數(shù)需要逐個(gè)列舉、apply()是參數(shù)數(shù)組。
顯然,apply 與 construct 要求代理目標(biāo)對(duì)象必須是一個(gè)函數(shù),這兩個(gè)代理陷阱在函數(shù)的執(zhí)行方式上開(kāi)啟了很多的可能性,結(jié)合使用就可以完全控制任意的代理目標(biāo)函數(shù)的行為。
4.2 驗(yàn)證函數(shù)的參數(shù)
看到apply()和construct()陷阱的參數(shù)都有被傳遞給函數(shù)的參數(shù)數(shù)組argumentsList,所以可以用來(lái)驗(yàn)證函數(shù)的參數(shù)。
例如需要保證所有參數(shù)都是某個(gè)特定類(lèi)型的,并且不能通過(guò) new 構(gòu)造使用,示例如下:
/* 定義 sum 目標(biāo)函數(shù) */
function sum(...values) {
return values.reduce((previous, current) => previous + current, 0);
}
/* 定義 apply 陷阱函數(shù) */
function applyRef (trapTarget, thisArg, argumentList) {
argumentList.forEach((arg) => {
if (typeof arg !== "number") {
throw new TypeError("All arguments must be numbers.");
}
});
return Reflect.apply(trapTarget, thisArg, argumentList);
}
/* 定義 construct 陷阱函數(shù) */
function constructRef () {
throw new TypeError("This function can't be called with new.");
}
/* 定義 sumProxy 代理函數(shù) */
let sumProxy = new Proxy(sum, {
apply: applyRef,
construct: constructRef
});
console.log(sumProxy(1, 2, 3, 4)); // 10
// console.log(sumProxy(1, "2", 3, 4)); // TypeError: All arguments must be numbers.
// let result = new sumProxy() // TypeError: This function can't be called with new.
sum() 函數(shù)會(huì)將所有傳遞進(jìn)來(lái)的參數(shù)值相加,此代碼通過(guò)將 sum() 函數(shù)封裝在 sumProxy() 代理中,如果傳入?yún)?shù)的值不是數(shù)值類(lèi)型,該函數(shù)仍然會(huì)嘗試加法操作,但在函數(shù)運(yùn)行之前攔截了函數(shù)調(diào)用,觸發(fā)apply陷阱函數(shù)以保證每個(gè)參數(shù)都是數(shù)值。
出于安全的考慮,這段代碼使用 construct 陷阱拋出錯(cuò)誤,以確保該函數(shù)不會(huì)被使用 new 運(yùn)算符調(diào)用
實(shí)例對(duì)象 instance 對(duì)象會(huì)被同時(shí)判定為 proxy 與 target 對(duì)象的實(shí)例,是因?yàn)?instanceof 運(yùn)算符使用了原型鏈來(lái)進(jìn)行推斷,而原型鏈查找并沒(méi)有受到這個(gè)代理的影響,因此 proxy 對(duì)象與 target 對(duì)象對(duì)于 JS 引擎來(lái)說(shuō)就有同一個(gè)原型。
4.3 調(diào)用類(lèi)的構(gòu)造函數(shù)
ES6 中新引入了class類(lèi)的概念,類(lèi)使用constructor構(gòu)造函數(shù)封裝數(shù)據(jù),并規(guī)定必須始終使用 new 來(lái)調(diào)用,原因是類(lèi)構(gòu)造器的內(nèi)部方法 [[Call]] 被明確要求拋出錯(cuò)誤。
代理可以攔截對(duì)于 [[Call]] 方法的調(diào)用,你可以借助代理調(diào)用的類(lèi)構(gòu)造器。例如在缺少 new 的情況下創(chuàng)建一個(gè)新實(shí)例,就使用 apply 陷阱函數(shù)實(shí)現(xiàn):
class Person {
constructor(name) {
this.name = name;
}
}
let PersonProxy = new Proxy(Person, {
apply: function(trapTarget, thisArg, argumentList) {
return new trapTarget(...argumentList);
}
});
let me = PersonProxy("Jenny");
console.log(me.name); // "Jenny"
console.log(me instanceof Person); // true
console.log(me instanceof PersonProxy); // true
類(lèi)構(gòu)造器即類(lèi)的構(gòu)造函數(shù),使用代理時(shí)它的行為就像函數(shù)一樣,apply陷阱函數(shù)重寫(xiě)了默認(rèn)的構(gòu)造行為。
關(guān)于類(lèi)的更多有趣的用法,可參考 【ES6】更易于繼承的類(lèi)語(yǔ)法
總結(jié)來(lái)說(shuō),代理的用途非常廣泛,因?yàn)樗峁┝诵薷?JS 內(nèi)置對(duì)象的所有行為的入口。上述例子只是簡(jiǎn)單的一些應(yīng)用入門(mén),還有更多復(fù)雜的示例,推薦閱讀《深入理解ES6》。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
extjs中form與grid交互數(shù)據(jù)(record)的方法
這篇文章介紹了extjs中form與grid交互數(shù)據(jù)(record)的方法,有需要的朋友可以參考一下2013-08-08
關(guān)于編寫(xiě)性能高效的javascript事件的技術(shù)
這篇文章主要介紹了關(guān)于編寫(xiě)性能高效的javascript事件的技術(shù) ,需要的朋友可以參考下2014-11-11
JS call()及apply()方法使用實(shí)例匯總
這篇文章主要介紹了JS call()及apply()方法使用實(shí)例匯總,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07
JavaScript高級(jí)程序設(shè)計(jì) 閱讀筆記(十四) js繼承機(jī)制的實(shí)現(xiàn)
繼承是面向?qū)ο笳Z(yǔ)言的必備特征,即一個(gè)類(lèi)能夠重用另一個(gè)類(lèi)的方法和屬性。在JavaScript中繼承方式的實(shí)現(xiàn)方式主要有以下五種:對(duì)象冒充、call()、apply()、原型鏈、混合方式2012-08-08
JavaScript中Promise的使用方法實(shí)例
現(xiàn)在不會(huì)用Promise都不好意思說(shuō)自己是前端,下面這篇文章主要給大家介紹了關(guān)于JavaScript中Promise使用的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-05-05
JS判斷當(dāng)前是否平板安卓并是否支持cordova方法的示例代碼
這篇文章主要介紹了JS判斷當(dāng)前是否平板安卓并是否支持cordova方法,pc和安卓平板共用一套代碼,平板的代碼用了cordova做了一個(gè)殼子嵌套如果用了cordova就不支持elementUI中的上傳功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-08-08
javascript顯示選擇目錄對(duì)話(huà)框的代碼
js 目錄對(duì)話(huà)框顯示代碼2008-11-11
JS如何將秒數(shù)轉(zhuǎn)化為時(shí)分秒的形式
在實(shí)際工作中經(jīng)常會(huì)遇見(jiàn)把秒數(shù)轉(zhuǎn)化為時(shí)分秒的問(wèn)題,如何處理呢?下面這篇文章主要給大家介紹了關(guān)于JS如何將秒數(shù)轉(zhuǎn)化為時(shí)分秒形式的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-12-12
關(guān)于原生js中bind函數(shù)的簡(jiǎn)單實(shí)現(xiàn)
下面小編就為大家?guī)?lái)一篇關(guān)于原生js中bind函數(shù)的簡(jiǎn)單實(shí)現(xiàn)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-08-08

