ES6精華:Proxy & Reflect

導語

本文主要介紹了ES6中ProxyReflect的精華知識,並附有恰當實例。Proxy意爲代理器,經過操做爲對象生成的代理器,實現對對象各種操做的攔截式編程。Reflect是一個包攬更爲嚴格、健全的操做對象方法的模塊。由於Proxy所能代理的方法和Reflect所包括的方法基本對應,並且在攔截方法裏應該使用對應的Reflect方法返回結果,因此將二者合併在一塊兒分享。編程

1 Proxy

1.1 登堂

先想個問題,如何管控對象某一屬性的讀取和修改(不涉及閉包建立私有屬性)?
先建立不該被直接改動的容器屬性:_屬性名,再設置相應的settergetter函數或建立相應的操做方法。數組

--- 設置 setter 和 getter 函數
let obj = {
  _id: undefined,
  get id() {
    return this._id;
  },
  set id(v) {
    this._id = v;
  }
};
obj.id = 3; // 至關:obj._id = 3
console.log(obj.id); // console.log(obj._id);

--- 建立獲取及修改方法
let obj = {
  _id: undefined,
  id() {
    if (!arguments.length) return this._id;
    this._id = arguments[0];
  }
};
obj.id(3); // 至關:obj._id = 3
console.log(obj.id()); // console.log(obj._id);

這樣有明顯的缺陷:要爲每一個須要管控的屬性進行重複設置,並且實際上容器屬性能夠被任意修改。
若是要求升級,咱們須要監聽查看、刪除、遍歷對象屬性的操做,怎麼辦?ES6以前只能涼拌,ES6以後Proxy代你辦。閉包

let obj = { id: 0, name: 'Wmaker' };
let objProxy = new Proxy(obj, {
  get(target, attr) {
    console.log(`Get ${attr}`);
    return target[attr];
  },
  set(target, attr, val) {
    console.log(`Set ${attr}`);
    target[attr] = val;
    return true;
  }
});
objProxy.id; // 打印出:Get id,至關:obj.id;
objProxy.name; // 打印出:Get name,至關:obj.name;

1.2 入室

如前節示例可知,Proxy是生成代理器的構造函數。傳入的第一個參數爲須要被代理的對象,第二個參數是須要攔截操做的配置對象(以後會列出全部可攔截的操做及其意義和注意事項)。配置對象中的每一個攔截操做,都有默認格式的傳入參數,有些也要求有特定的返回值(下面會羅列出某些規律)。 app

生成的代理器是一個與被代理對象關聯的代理實例,能夠像操做普通對象同樣對待它。便是說,能夠被delete掉某個屬性,能夠被遍歷或獲取原型。全部做用於代理器的操做,都至關直接做用於被代理對象,還可爲其配置攔截操做。說的激憤點,蒼老師能作好的,咱們的小澤老師怎麼會差呢?另外可代理的不僅僅是對象,屬於對象的函數、數組都是無條件接受的。 函數

爲對象生成代理器以後,依然能夠操做原對象,但這天然是不建議的。this

參數
不一樣攔截函數的傳入參數不盡相同,但通常爲被代理對象,該操做須要的參數等和代理器對象。
沒必要記憶每一個攔截函數的參數,爲腦瓜減減負擔,使用時先打印出arguments查看便會一目瞭然。prototype

返回值
在攔截方法裏,應儘可能使用Reflect對應的方法進行操做,並返回該方法的返回值。一方面是簡單,更重要的是在不一樣方法或某些環境下,對返回值有硬性的要求,不然直接報錯。好比construct()必須返回一個對象,不然報錯。再好比set()在嚴格模式下,必須返回true或可轉化成true的值,不管操做是否成功。代理

"use strict";

--- 錯誤的作法
let obj = { id: 0 };
let objProxy = new Proxy(obj, {
  set(target, attr, val) {
    console.log(`Set ${attr}`);
    return target[attr] = val;
  }
});

objProxy.id = 1; // 操做成功。
objProxy.id = 0; // 操做已經成功,但報錯,再也不往下執行。

--- 推薦的作法
let obj = { id: 0 };
let objProxy = new Proxy(obj, {
  set(target, attr, val) {
    console.log(`Set ${attr}`);
    return Reflect.set(target, attr, val);
  }
});

objProxy.id = 1; // 操做成功。
objProxy.id = 0; // 操做成功。

攔截方法的返回值並不會直接反映到外部,JS會進行某些驗證和排除。
好比即使在ownKeys()中返回所有的值,但實際到外部的只有相應的系列。code

兩次打印的結果不相等。

let obj = { id: 0, [Symbol.iterator]() {} };
let objProxy = new Proxy(obj, {
  ownKeys(target) {
    let res = Reflect.ownKeys(target);
    console.log('1', res);
    return res;
  }
});

console.log('2', Object.keys(objProxy));

限制性的延續
當被代理對象自身已有某些限制,好比不可擴展或屬性不可寫不可配置等。對象自己的操做已經受到了限制,這時若是執行相應的代理操做,天然會報錯。好比當屬性不可寫時,若是代理並執行了set()操做,則會直接報錯。對象

let obj = { id: 0 };
Object.defineProperty(obj, 'name', {
  value: 'Wmaker'
});

let objProxy = new Proxy(obj, {
  set(target, attr, val) {
    return Reflect.set(target, attr, val);
  }
});

objProxy.id = 1; // 操做成功。
objProxy.name = 'Limo'; // 報錯。

this
有些原生對象的內部屬性或方法,只有經過正確的this才能訪問,因此沒法進行代理。
好比日期對象,new Proxy(new Date(), {}).getDate(),報錯提示:這不是個Date對象。

也有變通的辦法,好比對於須要正確 this 的方法能夠這樣作。

let p = new Proxy(new Date(), {
  get(target, attr) {
    const v = Reflect.get(target, attr);
    return typeof v === 'function'
      ? v.bind(target)
      : v;
  }
});
p.getTime(); // 沒有錯誤。

處於配置對象中的this直接指向配置對象,不是被代理對象或代理器對象。
處於被代理對象中的this,分爲存在於方法和存在於getter/setter中兩種。二者獲取this的方式不一樣,咱們以實例說明。

--- 例一,沒有 set 攔截操做。
let obj = {
  get id() {
    console.log('o', this);
  },
  fn() {
    console.log('o', this);
  }
};

let objProxy = new Proxy(obj, {});

objProxy.id; // 打印出 objProxy 。
objProxy.fn(); // 打印出 objProxy 。


--- 例二,有 set 攔截操做。實際使用了 target[attr] 獲取屬性值。
let obj = {
  get id() {
    console.log('o', this);
  },
  fn() {
    console.log('o', this);
  }
};

let objProxy = new Proxy(obj, {
  get(target, attr) {
    console.log('p', this);
    return target[attr];
  }
});

objProxy.id;
// 打印出配置對象和 obj。
// 由於實際是經過被代理對象即 target 訪問到 id 值的。

objProxy.fn();
// 打印出配置對象和 objProxy。
// 能夠等價的將方法轉化成 (objProxy.fn).call(objProxy)。
// 雖然方法也是經過 target 訪問到的,但對於方法來講,環境變量一開始就肯定了。

原型爲代理器
若是對象的原型爲代理器,當操做進行到原型時,實際是操做原型的代理器對象。這時,其行爲和普通代理器一致。

let obj = Object.create(new Proxy({}, {
  get(target, attr) {
    console.log('In proxy.');
    return Reflect.get(target, attr);
  }
}));

obj.name; // 打印出 In proxy. 。
// 當在實例上找不到對應屬性時,轉到了原型上,這時便被攔截了。

1.3 代理類別

這裏僅僅是羅列出某些重點,詳細的請看手冊(標準和行爲處於變更中)。

get
攔截屬性的讀取操做,包括數組取值。

set
攔截屬性的賦值操做,包括數組賦值。
嚴格模式下,必須返回可轉化爲true的值。
嚴格模式下,若是代理對象有某些限制(屬性不可寫等),執行相應的攔截操做天然會報錯。

apply
攔截函數的調用、callapply操做。

has
攔截判斷對象是否具備某屬性。
只對inReflect.has()方法有效,for in屬於遍歷系列。

construct
攔截new命令,必須返回一個對象。

deleteProperty
攔截delete屬性操做。
嚴格模式下,必須返回可轉化爲true的值。
嚴格模式下,若是代理對象有某些限制(屬性不可寫等),執行相應的攔截操做天然會報錯。

defineProperty
攔截Object.defineProperty(),不會攔截defineProperties
嚴格模式下,若是代理對象有某些限制(屬性不可配置等),執行相應的攔截操做天然會報錯。

getOwnPropertyDescriptor
攔截Object.getOwnPropertyDescriptor(),不會攔截getOwnPropertyDescriptors
必須返回一個對象或undefined,即返回與原方法相同的返回值,不然報錯。

getPrototypeOf
攔截獲取對象原型,必須返回一個對象或者null
若是對象不可擴展,必須返回真實的原型對象。
直接訪問__proto__,經過各種方法等等都會觸發攔截。

setPrototypeOf
攔截Object.setPrototypeOf()方法。

isExtensible
攔截Object.isExtensible()操做。
返回值必須與目標對象的isExtensible屬性保持一致,不然會拋出錯誤。

preventExtensions
攔截Object.preventExtensions(),返回值會自動轉成布爾值。

ownKeys
攔截對象自身屬性的遍歷操做。
好比keys(),getOwnPropertyNames(),getOwnPropertySymbols()等等。
返回值必須是數組,項只能是字符串或Symbol,不然報錯。
返回值會根據調用方法的需求進行過濾,好比Object.keys()裏不會有symbole
若是目標對象自身包含不可配置的屬性,則該屬性必須被返回,不然報錯。
若是目標對象不可擴展,返回值必須包含原對象的全部屬性,且不能包含多餘的屬性,不然報錯。

1.4 Proxy.revocable

此靜態方法也用於生成代理器對象的,但它還會返回一個回收代理器的方法。
使用場景:不容許直接訪問目標對象,必須經過代理訪問。並且一旦訪問結束,就收回代理權,不容許再次訪問。

let {proxy, revoke} = Proxy.revocable(obj, {});
revoke();
此時其內部屬性值 [[IsRevoked]] 爲 true,不能再操做代理器,不然報錯。

2 Reflect

2.1 做用

最終目的是成爲語言內部方法的宿主對象。
好比說defineProperty, getPrototypeOf, preventExtensions都很明顯屬於內部方法,不該掛在Object下。

提供替代命令式操做的相應函數。
使用Reflect.has(obj, attr)替代in操做
使用Reflect.deleteProperty(obj, attr)替代delete操做

使函數的行爲更加完善和嚴格。
在沒法定義屬性時,Reflect.defineProperty返回false而不是拋出錯誤。
在要求類型是對象的參數爲非對象時,會直接報錯而不是調用Object()進行轉化。

Porxy可攔截的方法對應,方便在實現自定義行爲時,直接調用以完成默認行爲。

2.2 靜態方法

這裏僅僅是羅列出某些重點,詳細的請看手冊

Reflect.get
Reflect.get(obj, attr, receiver)
返回屬性值,沒有則返回undefined
receiver是設置gettersetter裏的this指向,默認指向obj

Reflect.set
Reflect.set(obj, attr, value, receiver)
設置屬性值,返回布爾值。
注意:當該屬性不是getter時,傳入receiver等同於設置receiver對象上的屬性值。

Reflect.has
Reflect.has(obj, attr)
等同attr in obj

Reflect.deleteProperty
Reflect.deleteProperty(obj, attr)
等同delete obj[attr]

Reflect.construct
Reflect.construct(func, args)
args等同於使用apply方法傳入的參數數組。
提供了不使用new來調用構造函數的方法,等同new func(...args)

Reflect.getPrototypeOf
做用及參數等同Object.getPrototypeOf(obj)

Reflect.setPrototypeOf
做用及參數等同Object.setPrototypeOf(obj, newProto)

Reflect.apply
做用及參數等同Function.prototype.apply.call(func, thisArg, args)

Reflect.defineProperty
做用及參數等同Object.defineProperty(obj, attr, descriptor)

Reflect.getOwnPropertyDescriptor
做用及參數等同Object.getOwnPropertyDescriptor(obj, attr)

Reflect.isExtensible
Reflect.isExtensible(obj)
返回一個布爾值,表示當前對象是否可擴展。

Reflect.preventExtensions
Reflect.preventExtensions(obj)
設置對象爲不可擴展,返回布爾值。

Reflect.ownKeys
Reflect.ownKeys(obj)
返回對象自己的全部屬性。
等同於Object.getOwnPropertyNamesObject.getOwnPropertySymbols之和。

相關文章
相關標籤/搜索