深入實踐 ES6 Proxy & Reflect

引言


Vue中的數據綁定

Vue作為前端框架的三駕馬車之一,在眾多前端項目中具有極其重要的作用。

Vue中具有一個重要的功能點——“數據綁定”。使用者無需關心數據是如何綁定到dom上面,只需要關注數據本身即可。

那實現其功能的原理是什麼?

閱讀官方文檔(v2.0),我們會發現:

把一個普通 Javascript 對象傳給 Vue 實例來作為它的 data 選項,Vue 將遍歷它的屬性,用 Object.defineProperty 將它們轉為 getter/setter。

關鍵字是Object.defineProperty,在MDN文檔找到說明如下:

Object.defineProperty()方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。

我們再仔細查詢在MDN文檔的說明會發現,Object.defineProperty()存在兩種屬性描述符:

數據描述符(簡略介紹)

  • configurable:數據可改變
  • enumerable:可枚舉
  • value:屬性值
  • writable:可讀寫

存取描述符

  • get:一個給屬性提供 getter 的方法,如果沒有 getter 則為 undefined。
  • set:一個給屬性提供 setter 的方法,如果沒有 setter 則為 undefined。

至此也就引出了getter/setter。

ES5 getter/setter

讓我們通過一個例子來測試一下。

首先,建立一個英雄(Hero)對象並賦予其一些屬性:

<code>let hero = {
name:'趙雲',
hp: 100,
sp: 100
}
/<code>

然後使用Object.defineProperty()來對其具有的一些屬性進行修改,並且在控制檯輸出修改的內容:

<code>Object.defineProperty(hero, 'hp', {
set (val) {
console.log(`Set hp to ${val}`);
return val;
}
})

hero.hp = '200';
// --> Set hp to 200
/<code>

假若把console.log('Set hp to ${val}') 改為 element.innerHTML = val,是不是就可以實現數據綁定了?

那讓我們再修改一下英雄的屬性,假設英雄擁有很多裝備:

<code>let hero = {
name:'趙雲',
hp: 100,
sp: 100,
equipment:['馬','長槍']
}
/<code>

我們把“佩劍”添加到英雄的裝備中,並且輸出在控制檯:

<code>Object.defineProperty(hero.equipment, 'push', {
value () {
this[this.length] = arguments[0];
}
})

hero.equipment.push('佩劍');
console.log(hero.equipment);

// --> [ '馬','長槍', '佩劍' ]
/<code>

由此,我們可以看到對象的屬性變化可以依靠get()和set()方法去追蹤和改變;但對於數組則需要使用value()方法實現。

顯然,這不是最好的方法,那有沒有更好的方法可以簡化對象或數組屬性變化呢?

答案是肯定的。


概念


Proxy

Proxy意思為“代理”,即在訪問對象之前建立一道“攔截”,任何訪問該對象的操作之前都會通過這道“攔截”,即執行Proxy裡面定義的方法。

基本用法:

<code>let pro = new Proxy(target,handler);
/<code>
  • new Proxy()表示生成一個Proxy實例
  • target參數表示所要攔截的目標對象
  • handler參數也是一個對象,用來定製攔截行為。

handler

Proxy支持13種攔截行為(handle),針對解決上一節的問題,簡單介紹下其中2種攔截行為,get與set。

get

get(target, propKey, receiver)

用於攔截某個屬性的讀取操作,可以接受三個參數:

  • target:目標對象
  • propKey:屬性名
  • receiver(可選):proxy 實例本身(嚴格地說,是操作行為所針對的對象)

set

set(target, propKey, value, receiver)

用於攔截某個屬性的賦值操作,可以接受四個參數:

  • target:目標對象
  • propKey:屬性名
  • value:屬性值
  • receiver(可選):Proxy 實例本身

實例

在解決上一節問題之前,先一同看幾個實例。

實例1

<code>let hero = {
name: "趙雲",
age: 25
}

let handler = {}

let heroProxy = new Proxy(hero, handler);

console.log(heroProxy.name);
// --> 趙雲
heroProxy.name = "黃忠";
console.log(heroProxy.name);
// --> 黃忠
/<code>

解析:

創建hero對象為所要攔截的對象;

攔截操作對象handler為空,未對攔截對象設定攔截方法;

該情況下heroProxy直接指向原對象target,訪問heroProxy等同於訪問target,所以結果為target中的結果。

實例2

<code>let hero = {
name: "趙雲",
age: 25
}

let handler = {
get: (hero, name, ) => {
const heroName =`英雄名是${hero.name}`;
return heroName;
},
set:(hero,name,value)=>{
console.log(`${hero.name} change to ${value}`);
hero[name] = value;
return true;
}
}

let heroProxy = new Proxy(hero, handler);

console.log(heroProxy.name);
heroProxy.name = '黃忠';
console.log(heroProxy.name);

// --> 英雄名是趙雲
// --> 趙雲 change to 黃忠

// --> 英雄名是黃忠
/<code>

解析:

創建hero對象為所要攔截的對象;

handler對象為攔截對象後執行的操作,這裡get方法為讀取操作,即用戶想要讀取heroProxy中的屬性時執行的攔截操作。

最後創建一個Proxy實例,當讀取heroProxy中的屬性時,結果打印出來的總是“黃忠”字符串。

實例3

Proxy也可以作為其他對象的原型對象使用。

<code>let hero = {
name: "趙雲",
age: 25
}

let handler = {
get: (hero, name, ) => {
const heroName =`英雄名是${hero.name}`;
return heroName;
},
set:(hero,name,value)=>{
console.log(`${hero.name} change to ${value}`);
hero[name] = value;
return true;
}
}

let heroProxy = new Proxy(hero, handler);
let obj = Object.create(heroProxy);

console.log(obj.name);
obj.name = '黃忠';
console.log(obj.name);


// --> 英雄名是趙雲
// --> 趙雲 change to 黃忠
// --> 英雄名是黃忠
/<code>

解析:

在實例2的基礎上,將heroProxy作為obj的原型對象使用。

雖然obj本身沒有name這個屬性,但是根據原型鏈,會在heroProxy上讀取到name屬性,之後會執行相對應的攔截操作。

解決數據綁定問題

在我們對Proxy有了一定了解後,可以嘗試解決上一節的問題。

首先,還是定義一個英雄:

<code>let hero = {
name:'趙雲',
hp: 100,
sp: 100,
equipment:['馬','長槍']
}
/<code>

接著,定義一個handler:

<code>let handler = {
set(target, property, value) {
console.log(`hero's ${property} change to ${value}`);
target[property] = value;
return true;
}
}
/<code>

然後,修改英雄的hp值

<code>let heroProxy = new Proxy(hero, handler);
heroProxy.hp = 200;
// --> hero's hp change to 200
console.log(hero.hp);
// --> 200
/<code>

最後,同樣把“佩劍”添加到英雄的裝備中

<code>let heroProxy = new Proxy(hero.equipment, handler);
heroProxy.push('佩劍');
// --> hero's 2 change to 佩劍
// --> hero's length change to 3
console.log(hero.equipment);
// --> ["馬", "長槍", "佩劍"]
/<code>

可以發現,heroProxy.push('佩劍');觸發了兩次set,原因是push即修改了hero.equipment的內容,又修改了hero.equipment的length。

Reflect

在瞭解了Proxy之後,細心的我們一定發現,若需要在Proxy內部調用對象的默認行為,該如何實現?

Reflect正是ES6 為了操作對象而提供的新 API。

基本特點

  • 只要Proxy對象具有的代理方法,Reflect對象全部具有,以靜態方法的形式存在。這些方法能夠執行默認行為,無論Proxy怎麼修改默認行為,總是可以通過Reflect對應的方法獲取默認行為。
  • 修改某些Object方法的返回結果,讓其變得更合理。比如,Object.defineProperty(obj, name, desc)在無法定義屬性時,會拋出一個錯誤,而Reflect.defineProperty(obj, name, desc)則會返回false。
  • 讓Object操作都變成函數行為。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)讓它們變成了函數行為。

靜態方法

Reflect對象一共有 13 個靜態方法(匹配Proxy的13種攔截行為)。

  • Reflect.apply(target, thisArg, args)
  • Reflect.construct(target, args)
  • Reflect.get(target, name, receiver)
  • Reflect.set(target, name, value, receiver)
  • Reflect.defineProperty(target, name, desc)
  • Reflect.deleteProperty(target, name)
  • Reflect.has(target, name)
  • Reflect.ownKeys(target)
  • Reflect.isExtensible(target)
  • Reflect.preventExtensions(target)
  • Reflect.getOwnPropertyDescriptor(target, name)
  • Reflect.getPrototypeOf(target)
  • Reflect.setPrototypeOf(target, prototype)

大部分與Object對象的同名方法的作用都是相同的,而且它與Proxy對象的方法是一一對應的。

實例

下面通過3個實例來對比Object對象方法與Reflect對象方法。

實例1

<code>//Object對象方法
try {
Object.defineProperty(target, name, property);
} catch (e) {
console.log("error");
}

//Reflect對象方法
if (Reflect(target, name, property)) {
console.log("success");
} else {
console.log("error")
}
/<code>

解析:

由於Reflect(target, name, property)返回的是boolean,代碼語義性更好。

實例2

<code>let hero = {
name: '趙雲',
hp: 100,
sp: 100,
equipment: ['馬', '長槍']
}

//Object對象方法
console.log('name' in hero);
// --> true

//Reflect對象方法
console.log(Reflect.has(hero,'name'));
// --> true
/<code>

解析:

Object操作是命令式,而Reflect讓它們變成了函數行為

實例3

<code>let hero = {
name: '趙雲',
hp: 100,
sp: 100,
equipment: ['馬', '長槍']
}

let handler = {
get(target, name, receiver) {
if (name === "name") {
console.log("success");
} else {
console.log("failure");
}
return Reflect.get(target, name, receiver);
}
}

let heroProxy = new Proxy(hero, handler);
console.log(heroProxy.name);
// --> success
// --> 趙雲
/<code>

解析:

Reflect對象的操作和Proxy對象的操作一一對應,在Proxy的攔截操作中,可以直接利用Reflect對象直接獲取Proxy的默認值。


結合實踐

掌握了Proxy與Reflect的知識點後,除了解決文章開頭的數據綁定問題之外,挑選日常編碼中容易遇見的兩種情況進行編碼實踐。

觀察者模式

觀察者模式(Observer mode)指的是函數自動觀察數據對象,一旦數據有變化,函數就會自動執行。

<code>let hero = {
name: '趙雲',
hp: 100,
sp: 100,
equipment: ['馬', '長槍']
}

const handler = {
set(target, key, value, receiver) {
//內部調用對應的 Reflect 方法
const result = Reflect.set(target, key, value, receiver);
//執行觀察者隊列
observableArray.forEach(item => item());
return result;
}
}

//初始化Proxy對象,設置攔截操作
const createProxy = (obj) => new Proxy(obj, handler);

//初始化觀察者隊列
const observableArray = new Set();

const heroProxy = createProxy(hero);

//將監聽函數加入隊列
observableArray.add(() => {
console.log(heroProxy.name);
});

heroProxy.name = "黃忠";
// --> 黃忠
/<code>

該實例在set攔截行為中加入了監聽函數的執行,使每一次值的改變均能被監聽。

對象多重繼承

實現對象間的單繼承,比如obj2繼承obj1,可以使用Object.setPrototypeOf方法,但是沒法實現多繼承。

<code>const people = {
name: 'people',
run() {
console.log('people.run:', this.name);
}
};

const powerMan = {
name: 'powerMan',
run() {
console.log('powerMan.run:', this.name);
},
fight() {
console.log('powerMan.fight:', this.name);
}
};

const handler = {
get(target, name, receiver) {
if (Reflect.has(target, name)) {
return Reflect.get(target, name, receiver);
}
else {
for (let P of target[Symbol.for('[[Prototype]]')]) {
if (Reflect.has(P, name)) {
return Reflect.get(P, name, receiver);
}
}
}
}
};

const hero = new Proxy({
name: 'hero',
strike() {
this.run();
this.fight();
}
}, handler);

hero[Symbol.for('[[Prototype]]')] = [people, powerMan];
hero.strike();

// --> people.run:hero
// --> powerMan.fight:hero
/<code>

用了一個自定義的屬性Symbol.for("[[Prototype]]")來表示要繼承的多個父對象。

然後用Proxy來攔截所有hero中的get請求,使用Reflect.has方法檢查hero中是否存在相應的屬性或者方法。

如果存在,則直接轉發;如果不存在,則遍歷父對象列表,在父對象中逐個檢查是否存在相應的屬性或者方法。

若存在則調用。若不存在,則相當於get返回undefined。



如果文章能幫助你理解一些前端知識點,請收藏、分享和關注本專欄,謝謝。


分享到:


相關文章: