沐鳴總代平台_es6-快速掌握Proxy、Reflect

前言

ES6新增的代理和反射為開發者提供了攔截並向基本操作嵌入額外行為的能力。具體地說,可以給目標對象定義一個關聯的代理對象,而這個代理對象可以作為抽象的目標對象來使用。在對目標對象的各種操作影響目標對象之前,可以在代理對象中對這些操作加以控制。

Proxy (代理)

代理是使用 Proxy 構造函數創建的。這個構造函數接收兩個參數:目標對象和處理程序對象。缺少其中任何一個參數都會拋出 TypeError。

創建空代理

如下面的代碼所示,在代理對象上執行的任何操作實際上都會應用到目標對象。唯一可感知的不同
就是代碼中操作的是代理對象。

const target = { 
 id: 'target' 
}; 
const handler = {}; 
const proxy = new Proxy(target, handler); 
// id 屬性會訪問同一個值
console.log(target.id); // target 
console.log(proxy.id); // target

// 給目標屬性賦值會反映在兩個對象上
// 因為兩個對象訪問的是同一個值
target.id = 'foo'; 
console.log(target.id); // foo 
console.log(proxy.id); // foo

// 給代理屬性賦值會反映在兩個對象上
// 因為這個賦值會轉移到目標對象
proxy.id = 'bar'; 
console.log(target.id); // bar 
console.log(proxy.id); // bar

定義捕獲器

捕獲器可以理解為處理程序對象中定義的用來直接或間接在代理對象上使用的一種“攔截器”,每次在代理對象上調用這些基本操作時,代理可以在這些操作傳播到目標對象之前先調用捕獲器函數,從而攔截並修改相應的行為。

const target = { 
 foo: 'bar' 
};
const handler = { 
 // 捕獲器在處理程序對象中以方法名為鍵
 get() { 
 return 'handler override'; 
 } 
};
const proxy = new Proxy(target, handler); 
console.log(target.foo); // bar 
console.log(proxy.foo); // handler override

get() 捕獲器會接收到目標對象,要查詢的屬性和代理對象三個參數。我們可以對上述代碼進行如下改造

const target = { 
 foo: 'bar' 
};
const handler = { 
 // 捕獲器在處理程序對象中以方法名為鍵
 get(trapTarget, property, receiver) { 
 console.log(trapTarget === target); 
 console.log(property); 
 console.log(receiver === proxy); 
 return trapTarget[property]
 } 
};
const proxy = new Proxy(target, handler); 
proxy.foo; 
// true 
// foo 
// true
console.log(proxy.foo); // bar 
console.log(target.foo); // bar

處理程序對象中所有可以捕獲的方法都有對應的反射(Reflect)API 方法。這些方法與捕獲器攔截的方法具有相同的名稱和函數簽名,而且也具有與被攔截方法相同的行為。因此,使用反射 API 也可以像下面這樣定義出空代理對象:

const target = { 
 foo: 'bar' 
}; 
const handler = { 
 get() { 
     // 第一種寫法
     return Reflect.get(...arguments); 
     // 第二種寫法
     return Reflect.get
 } 
}; 
const proxy = new Proxy(target, handler); 
console.log(proxy.foo); // bar 
console.log(target.foo); // bar

我們也可以以此,來對將要訪問的屬性的返回值進行修飾。

const target = { 
 foo: 'bar', 
 baz: 'qux' 
}; 
const handler = { 
 get(trapTarget, property, receiver) { 
 let decoration = ''; 
 if (property === 'foo') { 
 decoration = ' I love you'; 
 } 
 return Reflect.get(...arguments) + decoration; 
 } 
}; 
const proxy = new Proxy(target, handler); 
console.log(proxy.foo); // bar I love you 
console.log(target.foo); // bar 
console.log(proxy.baz); // qux 
console.log(target.baz); // qux

可撤銷代理

有時候可能需要中斷代理對象與目標對象之間的聯繫。對於使用 new Proxy()創建的普通代理來說,這種聯繫會在代理對象的生命周期內一直持續存在。Proxy 也暴露了 revocable()方法,這個方法支持撤銷代理對象與目標對象的關聯。撤銷代理的操作是不可逆的。而且,撤銷函數(revoke())是冪等的,調用多少次的結果都一樣。撤銷代理之後再調用代理會拋出 TypeError。

const target = { 
 foo: 'bar' 
}; 
const handler = { 
 get() { 
 return 'intercepted'; 
 } 
}; 
const { proxy, revoke } = Proxy.revocable(target, handler); 
console.log(proxy.foo); // intercepted 
console.log(target.foo); // bar 
revoke(); 
console.log(proxy.foo); // TypeError

代理另一個代理

代理可以攔截反射 API 的操作,而這意味着完全可以創建一個代理,通過它去代理另一個代理。這樣就可以在一個目標對象之上構建多層攔截網:

const target = { 
 foo: 'bar' 
}; 
const firstProxy = new Proxy(target, { 
 get() { 
 console.log('first proxy'); 
 return Reflect.get(...arguments); 
 } 
}); 
const secondProxy = new Proxy(firstProxy, { 
 get() { 
 console.log('second proxy'); 
 return Reflect.get(...arguments); 
 } 
}); 
console.log(secondProxy.foo); 
// second proxy 
// first proxy 
// bar

代理的問題與不足

1. 代理中的this

const target = { 
 thisValEqualsProxy() { 
 return this === proxy; 
 } 
} 
const proxy = new Proxy(target, {}); 
console.log(target.thisValEqualsProxy()); // false 
console.log(proxy.thisValEqualsProxy()); // true

這樣看起來並沒有什麼問題,this指向調用者。但是如果目標對象依賴於對象標識,那就可能碰到意料之外的問題。

const wm = new WeakMap(); 
class User { 
 constructor(userId) { 
     wm.set(this, userId); 
 } 
 set id(userId) { 
     wm.set(this, userId); 
 } 
 get id() { 
     return wm.get(this); 
 } 
}
const user = new User(123); 
console.log(user.id); // 123 
const userInstanceProxy = new Proxy(user, {}); 
console.log(userInstanceProxy.id); // undefined

這是因為 User 實例一開始使用目標對象作為 WeakMap 的鍵,代理對象卻嘗試從自身取得這個實
例。要解決這個問題,就需要重新配置代理,把代理 User 實例改為代理 User 類本身。之後再創建代
理的實例就會以代理實例作為 WeakMap 的鍵了:

const UserClassProxy = new Proxy(User, {}); 
const proxyUser = new UserClassProxy(456); 
console.log(proxyUser.id);

2. 代理與內部槽位

在代理Date類型時:根據 ECMAScript 規範,Date 類型方法的執行依賴 this 值上的內部槽位[[NumberDate]]。代理對象上不存在這個內部槽位,而且這個內部槽位的值也不能通過普通的 get()和 set()操作訪問到,於是代理攔截后本應轉發給目標對象的方法會拋出 TypeError:

const target = new Date(); 
const proxy = new Proxy(target, {}); 
console.log(proxy instanceof Date); // true 
proxy.getDate(); // TypeError: 'this' is not a Date object

Reflect(反射)

Reflect對象與Proxy對象一樣,也是 ES6 為了操作對象而提供的新 API。Reflect的設計目的:

  1. 將Object對象的一些明顯屬於語言內部的方法(比如Object.defineProperty),放到Reflect對象上。
  2. 修改某些Object方法的返回結果,讓其變得更合理。比如,Object.defineProperty(obj, name, desc)在無法定義屬性時,會拋出一個錯誤,而Reflect.defineProperty(obj, name, desc)則會返回false。
  3. 讓Object操作都變成函數行為。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)讓它們變成了函數行為。
  4. Reflect對象的方法與Proxy對象的方法一一對應,只要是Proxy對象的方法,就能在Reflect對象上找到對應的方法。這就讓Proxy對象可以方便地調用對應的Reflect方法,完成默認行為,作為修改行為的基礎。也就是說,不管Proxy怎麼修改默認行為,你總可以在Reflect上獲取默認行為。

代理與反射API

get()

接收參數:

  • target:目標對象。
  • property:引用的目標對象上的字符串鍵屬性。
  • receiver:代理對象或繼承代理對象的對象。
    返回:
  • 返回值無限制
    get()捕獲器會在獲取屬性值的操作中被調用。對應的反射 API 方法為 Reflect.get()。
const myTarget = {}; 
const proxy = new Proxy(myTarget, { 
 get(target, property, receiver) { 
 console.log('get()'); 
 return Reflect.get(...arguments) 
 } 
}); 
proxy.foo; 
// get()

set()

接收參數:

  • target:目標對象。
  • property:引用的目標對象上的字符串鍵屬性。
  • value:要賦給屬性的值。
  • receiver:接收最初賦值的對象。
    返回:
  • 返回 true 表示成功;返回 false 表示失敗,嚴格模式下會拋出 TypeError。

set()捕獲器會在設置屬性值的操作中被調用。對應的反射 API 方法為 Reflect.set()。

const myTarget = {}; 
const proxy = new Proxy(myTarget, { 
 set(target, property, value, receiver) { 
 console.log('set()'); 
 return Reflect.set(...arguments) 
 } 
}); 
proxy.foo = 'bar'; 
// set()

has()

接收參數:

  • target:目標對象。
  • property:引用的目標對象上的字符串鍵屬性。

返回:

  • has()必須返回布爾值,表示屬性是否存在。返回非布爾值會被轉型為布爾值。

has()捕獲器會在 in 操作符中被調用。對應的反射 API 方法為 Reflect.has()。

const myTarget = {}; 
const proxy = new Proxy(myTarget, { 
 has(target, property) { 
 console.log('has()'); 
 return Reflect.has(...arguments) 
 } 
}); 
'foo' in proxy; 
// has()

defineProperty()

Reflect.defineProperty方法基本等同於Object.defineProperty,用來為對象定義屬性。

接收參數:

  • target:目標對象。
  • property:引用的目標對象上的字符串鍵屬性。
  • descriptor:包含可選的 enumerable、configurable、writable、value、get 和 set定義的對象。

返回:

  • defineProperty()必須返回布爾值,表示屬性是否成功定義。返回非布爾值會被轉型為布爾值。
const myTarget = {}; 
const proxy = new Proxy(myTarget, { 
 defineProperty(target, property, descriptor) { 
 console.log('defineProperty()'); 
 return Reflect.defineProperty(...arguments) 
 } 
}); 
Object.defineProperty(proxy, 'foo', { value: 'bar' }); 
// defineProperty()

getOwnPropertyDescriptor()

Reflect.getOwnPropertyDescriptor基本等同於Object.getOwnPropertyDescriptor,用於得到指定屬性的描述對象。

接收參數:

  • target:目標對象。
  • property:引用的目標對象上的字符串鍵屬性。

返回:

  • getOwnPropertyDescriptor()必須返回對象,或者在屬性不存在時返回 undefined。
const myTarget = {}; 
const proxy = new Proxy(myTarget, { 
 getOwnPropertyDescriptor(target, property) { 
 console.log('getOwnPropertyDescriptor()'); 
 return Reflect.getOwnPropertyDescriptor(...arguments) 
 } 
}); 
Object.getOwnPropertyDescriptor(proxy, 'foo'); 
// getOwnPropertyDescriptor()

deleteProperty()

Reflect.deleteProperty方法等同於delete obj[name],用於刪除對象的屬性。

接收參數:

  • target:目標對象。
  • property:引用的目標對象上的字符串鍵屬性。

返回:

  • deleteProperty()必須返回布爾值,表示刪除屬性是否成功。返回非布爾值會被轉型為布爾值。

ownKeys()

Reflect.ownKeys方法用於返回對象的所有屬性,基本等同於Object.getOwnPropertyNames與Object.getOwnPropertySymbols之和。

接收參數:

  • target:目標對象。

返回:

  • ownKeys()必須返回包含字符串或符號的可枚舉對象。

getPrototypeOf()

Reflect.getPrototypeOf方法用於讀取對象的__proto__屬性

接收參數:

  • target:目標對象。

返回:

  • getPrototypeOf()必須返回對象或 null。

等等。。

代理模式

跟蹤屬性訪問

通過捕獲 get、set 和 has 等操作,可以知道對象屬性什麼時候被訪問、被查詢。把實現相應捕獲器的某個對象代理放到應用中,可以監控這個對象何時在何處被訪問過:

const user = { 
 name: 'Jake' 
}; 
const proxy = new Proxy(user, { 
 get(target, property, receiver) { 
 console.log(`Getting ${property}`); 
 return Reflect.get(...arguments); 
 }, 
 set(target, property, value, receiver) { 
 console.log(`Setting ${property}=${value}`); 
 return Reflect.set(...arguments); 
 } 
}); 
proxy.name; // Getting name 
proxy.age = 27; // Setting age=27

隱藏屬性

代理的內部實現對外部代碼是不可見的,因此要隱藏目標對象上的屬性也輕而易舉。

const hiddenProperties = ['foo', 'bar']; 
const targetObject = { 
 foo: 1, 
 bar: 2, 
 baz: 3 
}; 
const proxy = new Proxy(targetObject, { 
 get(target, property) { 
 if (hiddenProperties.includes(property)) { 
 return undefined; 
 } else { 
 return Reflect.get(...arguments); 
 } 
 }, 
 has(target, property) {
  if (hiddenProperties.includes(property)) { 
 return false; 
 } else { 
 return Reflect.has(...arguments); 
 } 
 } 
}); 
// get() 
console.log(proxy.foo); // undefined 
console.log(proxy.bar); // undefined 
console.log(proxy.baz); // 3 
// has() 
console.log('foo' in proxy); // false 
console.log('bar' in proxy); // false 
console.log('baz' in proxy); // true

屬性驗證

因為所有賦值操作都會觸發 set()捕獲器,所以可以根據所賦的值決定是允許還是拒絕賦值:

const target = { 
 onlyNumbersGoHere: 0 
}; 
const proxy = new Proxy(target, { 
 set(target, property, value) { 
 if (typeof value !== 'number') { 
 return false; 
 } else { 
 return Reflect.set(...arguments); 
 } 
 } 
}); 
proxy.onlyNumbersGoHere = 1; 
console.log(proxy.onlyNumbersGoHere); // 1 
proxy.onlyNumbersGoHere = '2'; 
console.log(proxy.onlyNumbersGoHere); // 1

函數與構造函數參數驗證

跟保護和驗證對象屬性類似,也可對函數和構造函數參數進行審查。比如,可以讓函數只接收某種類型的值:

function median(...nums) { 
     return nums.sort()[Math.floor(nums.length / 2)]; 
} 
const proxy = new Proxy(median, { 
     apply(target, thisArg, argumentsList) { 
         for (const arg of argumentsList) { 
             if (typeof arg !== 'number') { 
                 throw 'Non-number argument provided'; 
             } 
         }
  return Reflect.apply(...arguments); 
 } 
}); 
console.log(proxy(4, 7, 1)); // 4 
console.log(proxy(4, '7', 1)); 
// Error: Non-number argument provided 
類似地,可以要求實例化時必須給構造函數傳參:
class User { 
 constructor(id) { 
     this.id_ = id; 
 } 
} 
const proxy = new Proxy(User, { 
 construct(target, argumentsList, newTarget) { 
     if (argumentsList[0] === undefined) { 
         throw 'User cannot be instantiated without id'; 
     } else { 
         return Reflect.construct(...arguments); 
     } 
 } 
}); 
new proxy(1); 
new proxy(); 
// Error: User cannot be instantiated without id

數據綁定與可觀察對象

通過代理可以把運行時中原本不相關的部分聯繫到一起。這樣就可以實現各種模式,從而讓不同的代碼互操作。比如,可以將被代理的類綁定到一個全局實例集合,讓所有創建的實例都被添加到這個集合中:

const userList = []; 
class User { 
 constructor(name) { 
 this.name_ = name; 
 } 
} 
const proxy = new Proxy(User, { 
 construct() { 
 const newUser = Reflect.construct(...arguments); 
 userList.push(newUser); 
 return newUser; 
 } 
}); 
new proxy('John'); 
new proxy('Jacob'); 
new proxy('Jingleheimerschmidt'); 
console.log(userList); // [User {}, User {}, User{}]

另外,還可以把集合綁定到一個事件分派程序,每次插入新實例時都會發送消息:

const userList = []; 
function emit(newValue) { 
 console.log(newValue); 
} 
const proxy = new Proxy(userList, { 
 set(target, property, value, receiver) { 
 const result = Reflect.set(...arguments); 
 if (result) { 
 emit(Reflect.get(target, property, receiver)); 
 } 
 return result; 
 } 
}); 
proxy.push('John'); 
// John 
proxy.push('Jacob'); 
// Jacob

使用 Proxy 實現觀察者模式

const queuedObservers = new Set();

const observe = fn => queuedObservers.add(fn);
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;
}

const person = observable({
  name: '張三',
  age: 20
});

function print() {
  console.log(`${person.name}, ${person.age}`)
}

observe(print);
person.name = '李四';
// 輸出
// 李四, 20

結尾

本文主要參考阮一峰es6教程、js紅寶書第四版

來自:https://segmentfault.com/a/1190000039956559

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

鏈接: http://www.fly63.com/article/detial/10266