1. 程式人生 > 實用技巧 >ES6中Reflect 與 Proxy

ES6中Reflect 與 Proxy

概述

Proxy 與 Reflect 是 ES6 為了操作物件引入的 API 。

Proxy 可以對目標物件的讀取、函式呼叫等操作進行攔截,然後進行操作處理。它不直接操作物件,而是像代理模式,通過物件的代理物件進行操作,在進行這些操作時,可以新增一些需要的額外操作。

Reflect 可以用於獲取目標物件的行為,它與 Object 類似,但是更易讀,為操作物件提供了一種更優雅的方式。它的方法與 Proxy 是對應的。


基本用法

Proxy

一個 Proxy 物件由兩個部分組成: target 、 handler 。在通過 Proxy 建構函式生成例項物件時,需要提供這兩個引數。 target 即目標物件, handler 是一個物件,聲明瞭代理 target 的指定行為。

let target = { name: 'Tom', age: 24 } let handler = { get: function(target, key) { console.log('getting '+key); return target[key]; // 不是target.key }, set: function(target, key, value) { console.log('setting '+key); target[key] = value; } } let proxy = new Proxy(target, handler) proxy.name // 實際執行 handler.get proxy.age = 25 // 實際執行 handler.set // getting name // setting age // 25 // target 可以為空物件 let targetEpt = {} let proxyEpt = new Proxy(targetEpt, handler) // 呼叫 get 方法,此時目標物件為空,沒有 name 屬性 proxyEpt.name // getting name // 呼叫 set 方法,向目標物件中添加了 name 屬性 proxyEpt.name = 'Tom' // setting name // "Tom" // 再次呼叫 get ,此時已經存在 name 屬性 proxyEpt.name // getting name // "Tom" // 通過建構函式新建例項時其實是對目標物件進行了淺拷貝,因此目標物件與代理物件會互相 // 影響 targetEpt) // {name: "Tom"} // handler 物件也可以為空,相當於不設定攔截操作,直接訪問目標物件 let targetEmpty = {} let proxyEmpty = new Proxy(targetEmpty,{}) proxyEmpty.name = "Tom" targetEmpty) // {name: "Tom"}

例項方法

get(target, propKey, receiver)

用於 target 物件上 propKey 的讀取操作。

let exam ={ name: "Tom", age: 24 } let proxy = new Proxy(exam, { get(target, propKey, receiver) { console.log('Getting ' + propKey); return target[propKey]; } }) proxy.name // Getting name // "Tom"

get() 方法可以繼承。

let proxy = new Proxy({}, { get(target, propKey, receiver) { // 實現私有屬性讀取保護 if(propKey[0] === '_'){ throw new Erro(`Invalid attempt to get private "${propKey}"`); } console.log('Getting ' + propKey); return target[propKey]; } }); let obj = Object.create(proxy); obj.name // Getting name
set(target, propKey, value, receiver)

用於攔截 target 物件上的 propKey 的賦值操作。如果目標物件自身的某個屬性,不可寫且不可配置,那麼set方法將不起作用。

let validator = { set: function(obj, prop, value) { if (prop === 'age') { if (!Number.isInteger(value)) { throw new TypeError('The age is not an integer'); } if (value > 200) { throw new RangeError('The age seems invalid'); } } // 對於滿足條件的 age 屬性以及其他屬性,直接儲存 obj[prop] = value; } }; let proxy= new Proxy({}, validator) proxy.age = 100; proxy.age // 100 proxy.age = 'oppps' // 報錯 proxy.age = 300 // 報錯

第四個引數 receiver 表示原始操作行為所在物件,一般是 Proxy 例項本身。

const handler = { set: function(obj, prop, value, receiver) { obj[prop] = receiver; } }; const proxy = new Proxy({}, handler); proxy.name= 'Tom'; proxy.name=== proxy // true const exam = {} Object.setPrototypeOf(exam, proxy) exam.name = "Tom" exam.name === exam // true

注意,嚴格模式下,set代理如果沒有返回true,就會報錯。

apply(target, ctx, args)

用於攔截函式的呼叫、call 和 reply 操作。target 表示目標物件,ctx 表示目標物件上下文,args 表示目標物件的引數陣列。

function sub(a, b){ return a - b; } let handler = { apply: function(target, ctx, args){ console.log('handle apply'); return Reflect.apply(...arguments); } } let proxy = new Proxy(sub, handler) proxy(2, 1) // handle apply // 1
has(target, propKey)

用於攔截 HasProperty 操作,即在判斷 target 物件是否存在 propKey 屬性時,會被這個方法攔截。此方法不判斷一個屬性是物件自身的屬性,還是繼承的屬性。

let handler = { has: function(target, propKey){ console.log("handle has"); return propKey in target; } } let exam = {name: "Tom"} let proxy = new Proxy(exam, handler) 'name' in proxy // handle has // true

注意:此方法不攔截 for ... in 迴圈。

construct(target, args)

用於攔截 new 命令。返回值必須為物件。

let handler = { construct: function (target, args, newTarget) { console.log('handle construct') return Reflect.construct(target, args, newTarget) } } class Exam { constructor (name) { this.name = name } } let ExamProxy = new Proxy(Exam, handler) let proxyObj = new ExamProxy('Tom') console.log(proxyObj) // handle construct // exam {name: "Tom"}
deleteProperty(target, propKey)

用於攔截 delete 操作,如果這個方法丟擲錯誤或者返回 false ,propKey 屬性就無法被 delete 命令刪除。

defineProperty(target, propKey, propDesc)

用於攔截 Object.definePro若目標物件不可擴充套件,增加目標物件上不存在的屬性會報錯;若屬性不可寫或不可配置,則不能改變這些屬性。

let handler = { defineProperty: function(target, propKey, propDesc){ console.log("handle defineProperty"); return true; } }plet target = {} let proxy = new Proxy(target, handler) proxy.name = "Tom" // handle defineProperty target // {name: "Tom"} // defineProperty 返回值為false,新增屬性操作無效 let handler1 = { defineProperty: function(target, propKey, propDesc){ console.log("handle defineProperty"); return false; } } let target1 = {} let proxy1 = new Proxy(target1, handler1) proxy1.name = "Jerry" target1 // {}

erty 操作

getOwnPropertyDescriptor(target, propKey)

用於攔截 Object.getOwnPropertyD() 返回值為屬性描述物件或者 undefined 。

let handler = { getOwnPropertyDescriptor: function(target, propKey){ return Object.getOwnPropertyDescriptor(target, propKey); } } let target = {name: "Tom"} let proxy = new Proxy(target, handler) Object.getOwnPropertyDescriptor(proxy, 'name') // {value: "Tom", writable: true, enumerable: true, configurable: // true}

ptor 屬性

getPrototypeOf(target)

主要用於攔截獲取物件原型的操作。包括以下操作:

- Object.prototype._proto_
- Object.prototype.isPrototypeOf()
- Object.getPrototypeOf()
- Reflect.getPrototypeOf()
- instanceof
let exam = {} let proxy = new Proxy({},{ getPrototypeOf: function(target){ return exam; } }) Object.getPrototypeOf(proxy) // {}

注意,返回值必須是物件或者 null ,否則報錯。另外,如果目標物件不可擴充套件(non-extensible),getPrototypeOf 方法必須返回目標物件的原型物件。

let proxy = new Proxy({},{ getPrototypeOf: function(target){ return true; } }) Object.getPrototypeOf(proxy) // TypeError: 'getPrototypeOf' on proxy: trap returned neither object // nor null
isExtensible(target)

用於攔截 Object.isExtensible 操作。

該方法只能返回布林值,否則返回值會被自動轉為布林值。

let proxy = new Proxy({},{ isExtensible:function(target){ return true; } }) Object.isExtensible(proxy) // true

注意:它的返回值必須與目標物件的isExtensible屬性保持一致,否則會丟擲錯誤。

let proxy = new Proxy({},{ isExtensible:function(target){ return false; } }) Object.isExtensible(proxy) // TypeError: 'isExtensible' on proxy: trap result does not reflect // extensibility of proxy target (which is 'true')
ownKeys(target)

用於攔截物件自身屬性的讀取操作。主要包括以下操作:

- Object.getOwnPropertyNames()
- Object.getOwnPropertySymbols()
- Object.keys()
- or...in

方法返回的陣列成員,只能是字串或 Symbol 值,否則會報錯。

若目標物件中含有不可配置的屬性,則必須將這些屬性在結果中返回,否則就會報錯。

若目標物件不可擴充套件,則必須全部返回且只能返回目標物件包含的所有屬性,不能包含不存在的屬性,否則也會報錯。

let proxy = new Proxy( { name: "Tom", age: 24 }, { ownKeys(target) { return ['name']; } }); Object.keys(proxy) // [ 'name' ]f返回結果中,三類屬性會被過濾: // - 目標物件上沒有的屬性 // - 屬性名為 Symbol 值的屬性 // - 不可遍歷的屬性 let target = { name: "Tom", [Symbol.for('age')]: 24, }; // 新增不可遍歷屬性 'gender' Object.defineProperty(target, 'gender', { enumerable: false, configurable: true, writable: true, value: 'male' }); let handler = { ownKeys(target) { return ['name', 'parent', Symbol.for('age'), 'gender']; } }; let proxy = new Proxy(target, handler); Object.keys(proxy) // ['name']
preventExtensions(target)

攔截 Object.preventExtensions 操作。

該方法必須返回一個布林值,否則會自動轉為布林值。

// 只有目標物件不可擴充套件時(即 Object.isExtensible(proxy) 為 false ), // proxy.preventExtensions 才能返回 true ,否則會報錯 var proxy = new Proxy({}, { preventExtensions: function(target) { return true; } }); // 由於 proxy.preventExtensions 返回 true,此處也會返回 true,因此會報錯 Object.preventExtensions(proxy)// TypeError: 'preventExtensions' on proxy: trap returned truish but // the proxy target is extensible // 解決方案 var proxy = new Proxy({}, { preventExtensions: function(target) { // 返回前先呼叫 Object.preventExtensions Object.preventExtensions(target); return true; } }); Object.preventExtensions(proxy) // Proxy {}
setPrototypeOf

主要用來攔截 Object.setPrototypeOf 方法。

返回值必須為布林值,否則會被自動轉為布林值。

若目標物件不可擴充套件,setPrototypeOf 方法不得改變目標物件的原型。

let proto = {} let proxy = new Proxy(function () {}, { setPrototypeOf: function(target, proto) { console.log("setPrototypeOf"); return true; } } ); Object.setPrototypeOf(proxy, proto); // setPrototypeOf
Proxy.revocable()

用於返回一個可取消的 Proxy 例項。

let {proxy, revoke} = Proxy.revocable({}, {}); proxy.name = "Tom"; revoke(); proxy.name // TypeError: Cannot perform 'get' on a proxy that has been revoked

Reflect

ES6 中將 Object 的一些明顯屬於語言內部的方法移植到了 Reflect 物件上(當前某些方法會同時存在於 Object 和 Reflect 物件上),未來的新方法會只部署在 Reflect 物件上。

Reflect 物件對某些方法的返回結果進行了修改,使其更合理。

Reflect 物件使用函式的方式實現了 Object 的命令式操作。

靜態方法

Reflect.get(target, name, receiver)

查詢並返回 target 物件的 name 屬性。

let exam = { name: "Tom", age: 24, get info(){ return this.name + this.age; } } Reflect.get(exam, 'name'); // "Tom" // 當 target 物件中存在 name 屬性的 getter 方法, getter 方法的 this 會繫結 // receiver let receiver = { name: "Jerry", age: 20 } Reflect.get(exam, 'info', receiver); // Jerry20 // 當 name 為不存在於 target 物件的屬性時,返回 undefined Reflect.get(exam, 'birth'); // undefined // 當 target 不是物件時,會報錯 Reflect.get(1, 'name'); // TypeError
Reflect.set(target, name, value, receiver)

將 target 的 name 屬性設定為 value。返回值為 boolean ,true 表示修改成功,false 表示失敗。當 target 為不存在的物件時,會報錯。

let exam = { name: "Tom", age: 24, set info(value){ return this.age = value; } } exam.age; // 24 Reflect.set(exam, 'age', 25); // true exam.age; // 25 // value 為空時會將 name 屬性清除 Reflect.set(exam, 'age', ); // true exam.age; // undefined // 當 target 物件中存在 name 屬性 setter 方法時,setter 方法中的 this 會繫結 // receiver , 所以修改的實際上是 receiver 的屬性, let receiver = { age: 18 } Reflect.set(exam, 'info', 1, receiver); // true receiver.age; // 1 let receiver1 = { name: 'oppps' } Reflect.set(exam, 'info', 1, receiver1); receiver1.age; // 1
Reflect.has(obj, name)

是 name in obj 指令的函式化,用於查詢 name 屬性在 obj 物件中是否存在。返回值為 boolean。如果 obj 不是物件則會報錯 TypeError。

let exam = { name: "Tom", age: 24 } Reflect.has(exam, 'name'); // true
Reflect.deleteProperty(obj, property)

是 delete obj[property] 的函式化,用於刪除 obj 物件的 property 屬性,返回值為 boolean。如果 obj 不是物件則會報錯 TypeError。

let exam = { name: "Tom", age: 24 } Reflect.deleteProperty(exam , 'name'); // true exam // {age: 24} // property 不存在時,也會返回 true Reflect.deleteProperty(exam , 'name'); // true
Reflect.construct(obj, args)

等同於 new target(...args)。

function exam(name){ this.name = name; } Reflect.construct(exam, ['Tom']); // exam {name: "Tom"}
Reflect.getPrototypeOf(obj)

用於讀取 obj 的 _proto_ 屬性。在 obj 不是物件時不會像 Object 一樣把 obj 轉為物件,而是會報錯。

class Exam{} let obj = new Exam() Reflect.getPrototypeOf(obj) === Exam.prototype // true
Reflect.setPrototypeOf(obj, newProto)

用於設定目標物件的 prototype。

let obj ={} Reflect.setPrototypeOf(obj, Array.prototype); // true
Reflect.apply(func, thisArg, args)

等同於 Function.prototype.apply.call(func, thisArg, args) 。func 表示目標函式;thisArg 表示目標函式繫結的 this 物件;args 表示目標函式呼叫時傳入的引數列表,可以是陣列或類似陣列的物件。若目標函式無法呼叫,會丟擲 TypeError 。

Reflect.apply(Math.max, Math, [1, 3, 5, 3, 1]); // 5
Reflect.defineProperty(target, propertyKey, attributes)

用於為目標物件定義屬性。如果 target 不是物件,會丟擲錯誤。

let myDate= {} Reflect.defineProperty(MyDate, 'now', { value: () => Date.now() }); // true const student = {}; Reflect.defineProperty(student, "name", {value: "Mike"}); // true student.name; // "Mike"
Reflect.getOwnPropertyDescriptor(target, propertyKey)

用於得到 target 物件的 propertyKey 屬性的描述物件。在 target 不是物件時,會丟擲錯誤表示引數非法,不會將非物件轉換為物件。

var exam = {} Reflect.defineProperty(exam, 'name', { value: true, enumerable: false, }) Reflect.getOwnPropertyDescriptor(exam, 'name') // { configurable: false, enumerable: false, value: true, writable: // false} // propertyKey 屬性在 target 物件中不存在時,返回 undefined Reflect.getOwnPropertyDescriptor(exam, 'age') // undefined
Reflect.isExtensible(target)

用於判斷 target 物件是否可擴充套件。返回值為 boolean 。如果 target 引數不是物件,會丟擲錯誤。

let exam = {} Reflect.isExtensible(exam) // true
Reflect.preventExtensions(target)

用於讓 target 物件變為不可擴充套件。如果 target 引數不是物件,會丟擲錯誤。

let exam = {} Reflect.preventExtensions(exam) // true
Reflect.ownKeys(target)

用於返回 target 物件的所有屬性,等同於 Object.getOwnPropertyNames 與Object.getOwnPropertySymbols 之和。

var exam = { name: 1, [Symbol.for('age')]: 4 } Reflect.ownKeys(exam) // ["name", Symbol(age)]

組合使用

Reflect 物件的方法與 Proxy 物件的方法是一一對應的。所以 Proxy 物件的方法可以通過呼叫 Reflect 物件的方法獲取預設行為,然後進行額外操作。

let exam = { name: "Tom", age: 24 } let handler = { get: function(target, key){ console.log("getting "+key); return Reflect.get(target,key); }, set: function(target, key, value){ console.log("setting "+key+" to "+value) Reflect.set(target, key, value); } } let proxy = new Proxy(exam, handler) proxy.name = "Jerry" proxy.name // setting name to Jerry // getting name // "Jerry"

使用場景拓展

實現觀察者模式

// 定義 Set 集合 const queuedObservers = new Set(); // 把觀察者函式都放入 Set 集合中 const observe = fn => queuedObservers.add(fn); // observable 返回原始物件的代理,攔截賦值操作 const observable = obj => new Proxy(obj, {set}); function set(target, key, value, receiver) { // 獲取物件的賦值操作 const result = Reflect.set(target, key, value, receiver); // 執行所有觀察者 queuedObservers.forEach(observer => observer()); // 執行賦值操作 return result; }