摘要:通過對這些底層內(nèi)置對象的代理陷阱和反射函數(shù),讓開發(fā)者能進一步接近引擎的能力。顯然,與要求代理目標對象必須是一個函數(shù),這兩個代理陷阱在函數(shù)的執(zhí)行方式上開啟了很多的可能性,結(jié)合使用就可以完全控制任意的代理目標函數(shù)的行為。
代理(Proxy)可以攔截并改變 JS 引擎的底層操作,如數(shù)據(jù)讀取、屬性定義、函數(shù)構(gòu)造等一系列操作。ES6 通過對這些底層內(nèi)置對象的代理陷阱和反射函數(shù),讓開發(fā)者能進一步接近 JS 引擎的能力。一、代理與反射的基本概念
什么是代理和反射呢?
代理是用來替代另一個對象(target),JS 通過new Proxy()創(chuàng)建一個目標對象的代理,該代理與該目標對象表面上可以被當(dāng)作同一個對象來對待。
當(dāng)目標對象上的進行一些特定的底層操作時,代理允許你攔截這些操作并且覆寫它,而這原本只是 JS 引擎的內(nèi)部能力。
如果你對些代理&反射的概念比較困惑的話,可以直接看后面的應(yīng)用示例,最后再重新看這些定義就會更清晰!
攔截行為使用了一個能夠響應(yīng)特定操作的函數(shù)( 被稱為陷阱),每個代理陷阱對應(yīng)一個反射(Reflect)方法。
ES6 的反射 API 以 Reflect 對象的形式出現(xiàn),對象每個方法都與對應(yīng)的陷阱函數(shù)同名,并且接收的參數(shù)也與之一致。以下是 Reflect 對象的一些方法:
代理陷阱 | 覆寫的特性 | 方法 | |
---|---|---|---|
get | 讀取一個屬性的值 | Reflect.get() | |
set | 寫入一個屬性 | Reflect.set() | |
has | in 運算符 | Reflect.has() | |
deleteProperty | delete 運算符 | Reflect.deleteProperty() | |
getPrototypeOf | Object.getPrototypeOf() | Reflect.getPrototypeOf() | |
isExtensible | Object.isExtensible() | Reflect.isExtensible() | |
defineProperty | Object.defineProperty() | Reflect.defineProperty | |
apply | 調(diào)用一個函數(shù) | Reflect.apply() | |
construct | 使用 new 調(diào)用一個函數(shù) | Reflect.construct() |
每個陷阱函數(shù)都可以重寫 JS 對象的一個特定內(nèi)置行為,允許你攔截并修改它。
綜合來說,想要控制或改變JS的一些底層操作,可以先創(chuàng)建一個代理對象,在這個代理對象上掛載一些陷阱函數(shù),陷阱函數(shù)里面有反射方法。通過接下來的應(yīng)用示例可以更清晰的明白代理的過程。
二、開始一個簡單的代理當(dāng)你使用 Proxy 構(gòu)造器來創(chuàng)建一個代理時,需要傳遞兩個參數(shù):目標對象(target)以及一個處理器( handler),
先創(chuàng)建一個僅進行傳遞的代理如下:
// 目標對象 let target = {}; // 代理對象 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 代理對象將所有操作直接傳遞給 target 目標對象,代理對象 proxy 自身并沒有存儲該屬性,它只是簡單將值傳遞給 target 對象,proxy.name 與 target.name 的屬性值總是相等,因為它們都指向 target.name。
此時代理陷阱的處理器為空對象,當(dāng)然處理器可以定義了一個或多個陷阱函數(shù)。
2.1 set 驗證對象屬性的存儲假設(shè)你想要創(chuàng)建一個對象,并要求其屬性值只能是數(shù)值,這就意味著該對象的每個新增屬性
都要被驗證,并且在屬性值不為數(shù)值類型時應(yīng)當(dāng)拋出錯誤。
這時需要使用 set 陷阱函數(shù)來攔截傳入的 value,該陷阱函數(shù)能接受四個參數(shù):
trapTarget :將接收屬性的對象( 即代理的目標對象)
key :需要寫入的屬性的鍵( 字符串類型或符號類型)
value :將被寫入屬性的值;
receiver :操作發(fā)生的對象( 通常是代理對象)
set 陷阱對應(yīng)的反射方法和默認特性是Reflect.set(),和陷阱函數(shù)一樣接受這四個參數(shù),并會基于操作是否成功而返回相應(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); } /* 測試 */ 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()會發(fā)生什么?,答案是攔截陷阱就不會有反射響應(yīng)。
需要注意的是,直接給 targetObj 目標對象賦值時是不會觸發(fā) set 代理陷阱的,需要通過給代理對象賦值才會觸發(fā) set 代理陷阱與反射。
2.2 get 驗證對象屬性的讀取JS 非常有趣的特性之一,是讀取不存在的屬性時并不會拋出錯誤,而會把undefined當(dāng)作該屬性的值。
對于大型的代碼庫,當(dāng)屬性名稱存在書寫錯誤時(不會拋錯)會導(dǎo)致嚴重的問題。這時使用 get 代理陷阱驗證對象結(jié)構(gòu)(Object Shape),訪問不存在的屬性時就拋出錯誤,使對象結(jié)構(gòu)驗證變得簡單。
get 陷阱函數(shù)會在讀取屬性時被調(diào)用,即使該屬性在對象中并不存在,它能接受三個參數(shù):
trapTarget :將會被讀取屬性的對象( 即代理的目標對象)
key :需要讀取的屬性的鍵( 字符串類型或符號類型)
receiver :操作發(fā)生的對象( 通常是代理對象)
Reflect.get()方法接受與之相同的參數(shù),并返回默認屬性的默認值。
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)讀取的屬性并
不存在時,程序拋出了一個錯誤,而不是將其默認為undefined。
注:in運算符用于判斷對象中是否存在某個屬性,如果自有屬性或原型屬性匹配這個名稱字符串或Symbol,那么in運算符返回 true。
targetObj = { name: "targetObject" }; console.log("name" in targetObj); // true console.log("toString" in targetObj); // true
其中 name 是對象自身的屬性,而 toString 則是原型屬性( 從 Object 對象上繼承而來),所以檢測結(jié)果都為 true。
has 陷阱函數(shù)會在使用in運算符時被調(diào)用,并且會傳入兩個參數(shù)(同名反射Reflect.has()方法也一樣):
trapTarget :需要讀取屬性的對象( 代理的目標對象)
key :需要檢查的屬性的鍵( 字符串類型或 Symbol符號類型)
deleteProperty 陷阱函數(shù)會在使用delete運算符去刪除對象屬性時下被調(diào)用,并且也會被傳入兩個參數(shù)(Reflect.deleteProperty() 方法也接受這兩個參數(shù)):
trapTarget :需要刪除屬性的對象( 即代理的目標對象) ;
key :需要刪除的屬性的鍵( 字符串類型或符號類型) 。
一些思考:分析過 Vue 源碼的都了解過,給一個 Vue 實例中掛載的 data,是通過Object.defineProperty代理 vm._data 中的對象屬性,實現(xiàn)雙向綁定...... 同理可以考慮使用 ES6 的 Proxy 的 get 和 set 陷阱實現(xiàn)這個代理。三、對象屬性陷阱 3.1 數(shù)據(jù)屬性與訪問器屬性
ES5 最重要的特征之一就是引入了 Object.defineProperty() 方法定義屬性的特性。屬性的特性是為了實現(xiàn)javascript引擎用的,屬于內(nèi)部值,因此不能直接訪問他們。
屬性分為數(shù)據(jù)屬性和訪問器屬性。使用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]] 表示能否通過 delete 刪除屬性從而重新定義為訪問器屬性;[[Enumerable]] 表示能否通過for-in循環(huán)返回屬性;[[Writable]] 表示能否修改屬性的值; [[Value]] 包含這個屬性的數(shù)據(jù)值。
對于訪問器屬性,該屬性不包含數(shù)據(jù)值,包含一對getter和setter函數(shù),定義訪問器屬性必須使用Object.defineProperty()方法:
let obj2 = { age: 18 } /* 訪問器屬性 */ 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 } }) /* 修改訪問器屬性調(diào)用 getter */ obj2._age = 20 console.log(obj2.age) // 20 /* 輸出訪問器屬性 */ console.log(Object.getOwnPropertyDescriptor(obj2,"_age")) // { get: [Function: get], // set: [Function: set], // enumerable: false, // configurable: false }
[[Get]] 在讀取屬性時調(diào)用的函數(shù), [[Set]] 再寫入屬性時調(diào)用的函數(shù)。使用訪問器屬性的常用方式,是設(shè)置一個屬性的值導(dǎo)致其他屬性發(fā)生變化。
3.2 檢查屬性的修改代理允許你使用 defineProperty 同名函數(shù)陷阱函數(shù)攔截Object.defineProperty()的調(diào)用,defineProperty 陷阱函數(shù)接受下列三個參數(shù):
trapTarget :需要被定義屬性的對象( 即代理的目標對象);
key :屬性的鍵( 字符串類型或符號類型);
descriptor :為該屬性準備的描述符對象。
defineProperty 陷阱函數(shù)要求在操作后返回一個布爾值用于判斷操作是否成功,如果返回了 false 則拋出錯誤,故可以使用該功能來限制哪些屬性可以被Object.defineProperty() 方法定義。
例如,如果想阻止定義Symbol符號類型的屬性,你可以檢查傳入的屬性值,若是則返回 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"); // 拋出錯誤 Object.defineProperty(proxy, nameSymbol, { value: "proxy" })四、函數(shù)代理 4.1 構(gòu)造函數(shù) & 立即執(zhí)行
函數(shù)的兩個內(nèi)部方法:[[Call]] 與[[Construct]]會在函數(shù)被調(diào)用時調(diào)用,通過代理函數(shù)來為這兩個內(nèi)部方法設(shè)置陷阱,從而控制函數(shù)的行為。
[[Construct]]會在函數(shù)被使用new運算符調(diào)用時執(zhí)行,代理觸發(fā)construct()陷阱函數(shù),并和Reflect.construct()一樣接收到下列兩個參數(shù):
trapTarget :被執(zhí)行的函數(shù)( 即代理的目標對象) ;
argumentsList :被傳遞給函數(shù)的參數(shù)數(shù)組。
[[Call]]會在函數(shù)被直接調(diào)用時執(zhí)行,代理觸發(fā)apply()陷阱函數(shù),它和Reflect.apply()都接收三個參數(shù):
trapTarget :被執(zhí)行的函數(shù)( 代理的目標函數(shù)) ;
thisArg :調(diào)用過程中函數(shù)內(nèi)部的 this 值;
argumentsList :被傳遞給函數(shù)的參數(shù)數(shù)組。
每個函數(shù)都包含call()和apply()方法,用于重置函數(shù)運行的作用域即 this 指向,區(qū)別只是接收參數(shù)的方式不同:call()的參數(shù)需要逐個列舉、apply()是參數(shù)數(shù)組。
顯然,apply 與 construct 要求代理目標對象必須是一個函數(shù),這兩個代理陷阱在函數(shù)的執(zhí)行方式上開啟了很多的可能性,結(jié)合使用就可以完全控制任意的代理目標函數(shù)的行為。
4.2 驗證函數(shù)的參數(shù)看到apply()和construct()陷阱的參數(shù)都有被傳遞給函數(shù)的參數(shù)數(shù)組argumentsList,所以可以用來驗證函數(shù)的參數(shù)。
例如需要保證所有參數(shù)都是某個特定類型的,并且不能通過 new 構(gòu)造使用,示例如下:
/* 定義 sum 目標函數(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ù)會將所有傳遞進來的參數(shù)值相加,此代碼通過將 sum() 函數(shù)封裝在 sumProxy() 代理中,如果傳入?yún)?shù)的值不是數(shù)值類型,該函數(shù)仍然會嘗試加法操作,但在函數(shù)運行之前攔截了函數(shù)調(diào)用,觸發(fā)apply陷阱函數(shù)以保證每個參數(shù)都是數(shù)值。
出于安全的考慮,這段代碼使用 construct 陷阱拋出錯誤,以確保該函數(shù)不會被使用 new 運算符調(diào)用
實例對象 instance 對象會被同時判定為 proxy 與 target 對象的實例,是因為 instanceof 運算符使用了原型鏈來進行推斷,而原型鏈查找并沒有受到這個代理的影響,因此 proxy 對象與 target 對象對于 JS 引擎來說就有同一個原型。4.3 調(diào)用類的構(gòu)造函數(shù)
ES6 中新引入了class類的概念,類使用constructor構(gòu)造函數(shù)封裝數(shù)據(jù),并規(guī)定必須始終使用 new 來調(diào)用,原因是類構(gòu)造器的內(nèi)部方法 [[Call]] 被明
確要求拋出錯誤。
代理可以攔截對于 [[Call]] 方法的調(diào)用,你可以借助代理調(diào)用的類構(gòu)造器。例如在缺少 new 的情況下創(chuàng)建一個新實例,就使用 apply 陷阱函數(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
類構(gòu)造器即類的構(gòu)造函數(shù),使用代理時它的行為就像函數(shù)一樣,apply陷阱函數(shù)重寫了默認的構(gòu)造行為。
關(guān)于類的更多有趣的用法,可參考 【ES6】更易于繼承的類語法
總結(jié)來說,代理的用途非常廣泛,因為它提供了修改 JS 內(nèi)置對象的所有行為的入口。上述例子只是簡單的一些應(yīng)用入門,還有更多復(fù)雜的示例,推薦閱讀《深入理解ES6》。
繼續(xù)加油鴨少年!!!文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/101528.html
摘要:是陷阱函數(shù)對應(yīng)的反射方法,同時也是操作的默認行為。對象外形指的是對象已有的屬性與方法的集合,由于該屬性驗證只須在讀取屬性時被觸發(fā),因此只要使用陷阱函數(shù)。無論該屬性是對象自身的屬性還是其原型的屬性。 主要知識點:代理和反射的定義、常用的陷阱函數(shù)、可被撤銷的代理、將代理對象作為原型使用、將代理作為類的原型showImg(https://segmentfault.com/img/bVbfWr...
摘要:方法與代理處理程序的方法相同。使用給目標函數(shù)傳入指定的參數(shù)。當(dāng)然,不用反射也可以讀取的值。的例子我們可以理解成是攔截了方法,然后傳入?yún)?shù),將返回值賦值給,這樣我們就能在需要讀取這個返回值的時候調(diào)用。這種代理模式和的代理有異曲同工之妙。 反射 Reflect 當(dāng)你見到一個新的API,不明白的時候,就在瀏覽器打印出來看看它的樣子。 showImg(https://segmentfault....
摘要:方法與代理處理程序的方法相同。使用給目標函數(shù)傳入指定的參數(shù)。當(dāng)然,不用反射也可以讀取的值。的例子我們可以理解成是攔截了方法,然后傳入?yún)?shù),將返回值賦值給,這樣我們就能在需要讀取這個返回值的時候調(diào)用。這種代理模式和的代理有異曲同工之妙。 反射 Reflect 當(dāng)你見到一個新的API,不明白的時候,就在瀏覽器打印出來看看它的樣子。 showImg(https://segmentfault....
摘要:元編程另一個方面是反射其用于發(fā)現(xiàn)和調(diào)整你的應(yīng)用程序結(jié)構(gòu)和語義。下的元編程帶來了三個全新的以及。它是語言的第七種數(shù)據(jù)類型,前六種是布爾值字符串?dāng)?shù)值對象。等同于對象的屬性等于一個布爾值,表示該對象用于時,是否可以展開。 元編程 一系列優(yōu)秀的 ES6 的新特性都來自于新的元編程工具,這些工具將底層鉤子(hooks)注入到了代碼機制中。元編程(籠統(tǒng)地說)是所有關(guān)于一門語言的底層機制,而不是數(shù)據(jù)...
閱讀 3769·2021-09-02 09:53
閱讀 2749·2021-07-30 14:57
閱讀 3492·2019-08-30 13:09
閱讀 1179·2019-08-29 13:25
閱讀 810·2019-08-29 12:28
閱讀 1453·2019-08-29 12:26
閱讀 1129·2019-08-28 17:58
閱讀 3305·2019-08-26 13:28