(JS基礎)Proxy 對象 與 Reflect 對象

Proxy 初步認識

Proxy 意爲"代理器",是 ES6 新增,用於修改目標對象的成員以前進行"攔截",外界對該對象的成員的訪問,都必須先經過這層攔截。其中,成員包括未定義的,即訪問或設置不存在的屬性,也會觸發相應的代理函數。javascript

建立

經過構造函數能夠建立 Proxy 對象(new Proxy()),須要依次傳入兩個參數java

  • 被代理的目標對象。能夠是任意類型的對象,包括數組、函數等,甚至是另外一個 Proxy 對象。
  • 控制器對象。控制器是一個對象,內部定義零個或多個代理函數。若控制器爲空對象,等同於直接訪問目標對象。
下面給出簡單的例子:
let obj = { a: 1 }
let proxy = new Proxy(obj, {
  get(obj, prop) {
    return 'proxyReturn'
  }
});
console.log(proxy.a);  // 'proxyReturn'
console.log(proxy.b);  // 'proxyReturn'複製代碼

例子中,proxyobj對象攔截,訪問其屬性時,永遠返回的是'proxyReturn',即便屬性是不存在。而get函數是衆多代理函數中的一種,下面會分別介紹其餘的代理函數。es6

做爲 prototype 的狀況

有一種狀況,Proxy 對象做爲某個對象的原型。看下面例子:數組

let proxy = new Proxy({}, {
  get(obj, prop) {
    return 'proxyReturn'
  }
});
let obj = { a: 1 }
Object.setPrototypeOf(obj, proxy);
console.log(obj.a);   // 1
console.log(obj.b);   // 'proxyReturn'複製代碼

當訪問obj.b時,因爲bobj對象內不存在,所以會到訪問原型是否存在該屬性,即訪問了proxy對象,從而觸發了代理函數。瀏覽器

注意

代理對象必須與被代理的對象具備相同特性(這裏說的特性指的是,對象是否凍結,是否密封,是否可擴展),即被代理的對象不能修改,使用代理對象的set方法也會報錯。再例如,被代理的對象被凍結,get方法不能返回與原值不一樣的值;getOwnPropertyDescriptor必須返回描述符對象。總之一句,被代理對象不能改的,代理對象一樣不能改;代理對象的方法的返回值類型必須與被代理對象一致bash

控制器對象內方法的 this

控制器對象內方法的this指向控制器對象自己,看以下例子:
app

let handler = {
  get() {
    return this
  }
}
let proxy = new Proxy({}, handler);
console.log(proxy.a === handler);   // true複製代碼


Proxy 的控制器對象

上面提到,控制器對象能夠包含零個或多個的代理函數。像get這樣的代理函數,一共有 13 個。簡單介紹下面出現的參數名意義:dom

  • target:表示被代理的對象
  • propKey:表示屬性名(或方法名)。
  • receiver:表示該方法所在的代理對象

get(target, propKey, receiver)

get方法用於攔截某個成員的讀取操做(包括方法),上面已經簡單給出了例子。下面給出代理數組的例子,使其能夠用負數索引。函數

let arr = [1, 2, 3, 4, 5];
let proxy = new Proxy(arr, {
  get(target, prop) {
    let length = target.length,
      index = Number.parseInt(prop);
    if (index < 0) {
      return target[length + index]
    }
    if (index >= length) {
      return target[length - 1]
    }
    return target[index]
  }
});
console.log(proxy[-1]);   // 5
console.log(proxy[10]);   // 5
console.log(proxy[2]);    // 3複製代碼

set(target, propKey, value, receiver)

set方法用於攔截對象屬性的設置。其中,value表示屬性的目標值。比較實用的用法是,與 DOM 的數據綁定。ui

須要注意的是!當存在二級屬性時,如[{}, {}]{ a: {}, b: {} }對二級或以上的屬性修改並不會觸發set方法

以下例子:

let listData = ['one', 'two', 'three'];
let proxy = new Proxy(listData, {
  set(target, prop, value) {
    let dom = document.getElementsByClassName('list-item')[prop]
    dom ? dom.innerText = value : null;
    target[prop] = value;
  }
})
proxy[0] = 'changed-one';複製代碼

has(target, propKey)

has方法用來攔截HasProperty操做,即判斷對象是否具備某個屬性時,這個方法會生效。典型的操做就是in運算符

注意,has​OwnProperty()for...in都不能觸發has方法。

注意,has方法只能返回布爾值return非布爾值會自動轉成布爾值。

let arr = [1, 2, 3, 4];
let proxyArr = new Proxy(arr, {
  has(target, prop) {
    return 'a'
  }
})
console.log('x' in proxyArr);    // true複製代碼

deleteProperty(target, propKey)

deleteProperty方法用於攔截delete操做,只能返回布爾值,非布爾值會被轉成布爾值,表明屬性是否被刪除成功,省略默認返回false

let obj = { a: 1, b: 2 }
let proxyDel = new Proxy(obj, {
  deleteProperty(target, prop) {
    delete target[prop]
  }
});
console.log(delete proxyDel.a);   // false
console.log(obj);   //  { b: 2 }複製代碼

getOwnPropertyDescriptor(target, propKey)

getOwnPropertyDescriptor方法攔截Object.getOwnPropertyDescriptor()Object.getOwnPropertyDescriptors(),返回一個屬性描述對象或者undefined

注意,若返回值不是undefined,則返回值必須是包含[[configurable]]true的對象,該值爲false或省略會致使報錯;若其餘描述符未定義,以默認值填充;若返回對象包含除描述符之外的屬性,則該屬性被忽略。

//  undefined
let proxy1 = new Proxy({}, {
  getOwnPropertyDescriptor(target, prop) {
    return undefined
  }
});
console.log(Object.getOwnPropertyDescriptor(proxy1, 'a'));
// { value: undefined, writable: false, enumerable: false, configurable: true }
let proxy2 = new Proxy({}, {
  getOwnPropertyDescriptor(target, prop) {
    return { configurable: true }
  }
});
console.log(Object.getOwnPropertyDescriptor(proxy2, 'a'));
// 報錯
let proxy3 = new Proxy({}, {
  getOwnPropertyDescriptor(target, prop) {
    return { configurable: false }
  }
});
console.log(Object.getOwnPropertyDescriptor(proxy3, 'a'));複製代碼

defineProperty(target, propKey, propDesc)

defineProperty方法攔截Object.defineProperty()Object.defineProperties()obj.prop = 'value'修改/添加對象的屬性的操做。該方法只能返回布爾值,非布爾值會轉成布爾值,表示是否成功定義屬性,返回fasle會致使報錯。其中,propDesc表示屬性的描述符。

注意,若是對象被凍結(Object.freeze())或對象被密封(Object.seal())或對象不可擴展( Object.preventExtensions()),甚至是某個屬性被密封([[configurable]]特性爲false),對代理對象使用Object.defineProperty()均可能會報錯。總之,和原對象的特性一致,不能被修改的被代理後一樣不能被修改。

let obj = { d: 4 }
let proxyObj = new Proxy(obj, {
  defineProperty() {
    console.log('proxy-defineProperty');
    return true
  }
})
// 如下4種方法都會觸發 defineProperty 方法
Object.defineProperty(proxyObj, 'a', {});
Object.defineProperties(proxyObj, { b: {} });
proxyObj.c = 3;
proxyObj.d = 5;複製代碼

apply(target, object, args)

apply方法攔截函數的調用callapplybind操做。其中,object表示函數上下文對象args表示函數的參數

let obj = { a: 10 }
function fn() { }
let proxyFn = new Proxy(fn, {
  apply(target, object, args) {
    console.log('proxyFn-apply');
  }
})
// 直接執行函數 會觸發
proxyFn();
// apply 和 call 會觸發
proxyFn.apply(obj);
proxyFn.call(obj);
// bind 定義時不會觸發 執行時觸發
let p = proxyFn.bind(obj);
p();複製代碼

construct(target, args, receiver)

construct方法用於攔截new命令

注意,被代理的函數不能是箭頭函數,不然也會報錯。

注意,construct方法必需要return對象(包括函數、數組等),不然會報錯

// 錯誤例子1
let P1 = new Proxy(()=>{},{
  construct(){
    return {}
  }
});
let p1 = new P1();    // 報錯,被代理函數不能是箭頭函數
// 錯誤例子2
let P2 = new Proxy(function(){},{
  construct(){}
});
let p2 = new P2();    // 報錯,construct必須返回對象複製代碼

getPrototypeOf(target)

getPrototypeOf方法主要用來攔截獲取對象原型,返回值必須是null或對象。具體來講,攔截下面這些操做。

  • Object.prototype.__proto__        // 只能在瀏覽器環境下執行
  • Object.prototype.isPrototypeOf()        // 代理對象做爲參數
  • Object.getPrototypeOf()        // 代理對象做爲參數
  • Reflect.getPrototypeOf()        // 代理對象做爲參數
  • instanceof        // 代理對象在運算符的左邊

下面給出簡單例子:

let proxy = new Proxy({},{
  getPrototypeOf(){
    console.log('proxy-getPrototypeOf');
    return null
  }
});
// 都會打印 'proxy-getPrototypeOf'
let x1 = proxy.__proto__;   // 只能在瀏覽器下執行
let x2 = {}.isPrototypeOf(proxy);
let x3 = Object.getPrototypeOf(proxy);
let x4 = Reflect.getPrototypeOf(proxy);
let x5 = (proxy instanceof function(){});複製代碼

setPrototypeOf(target, proto)

setPrototypeOf方法主要用來攔截Object.setPrototypeOf()方法。和Object.setPrototypeOf()方法同樣,必須返回被設置的對象,不然報錯。其中,proto表示被設爲原型的對象。

注意,瀏覽器環境下能夠經過__proto__設置原型對象,一樣能觸發setPrototypeOf方法。

let proxy = new Proxy({}, {
  setPrototypeOf(target, proto) {
    console.log('proxy-setPrototypeOf')
    return target;
  }
});
// 都打印 'proxy-setPrototypeOf'
Object.setPrototypeOf(proxy, {});
proxy.__proto__ = {}  // 非瀏覽器環境下使用會報錯複製代碼

isExtensible(target)

isExtensible方法攔截Object.isExtensible()方法,必須返回布爾值,非布爾值會被轉成布爾值,且返回值必須與源對象所對應的值相同,不然報錯。

// 錯誤示範
let proxy = new Proxy({}, {
  isExtensible() {
    return false
  }
});
console.log(Object.isExtensible(proxy));複製代碼

preventExtensions(target)

preventExtensions方法攔截Object.preventExtensions()方法,只能返回被設爲不可擴展的被代理對象,不然報錯。以下:

let proxy = new Proxy({}, {
  preventExtensions(target) {
    console.log('proxy-preventExtensions')
    return Object.preventExtensions(target);
  }
});
Object.preventExtensions(proxy);  // proxy-preventExtensions
// 錯誤示範
let proxyErr = new Proxy({}, {
  preventExtensions(target) {
    return {}
  }
});
Object.preventExtensions(proxyErr);  // 報錯複製代碼

ownKeys(target)

ownKeys方法用來攔截對象自身全部屬性的讀取操做,必須返回數組或對象,不然報錯。具體來講,攔截如下操做。

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • getOwnPropertyDescriptors()
  • Object.entries()Object.keys()Object.values()
  • for...in循環。

注意,雖然能夠返回非數組的對象,對於如Object.keys這種默認返回數組的方法,獲得的結果會是一個空數組。

注意,諸如Object.entries方法返回的數組不會含有Symbol屬性名、不存在的屬性名、不可遍歷(enumerable)的屬性名,與被代理對象的行爲一致。Object.getOwnPropertySymbols及其餘方法同理,與被代理對象一致。

看下面簡單例子:

let proxy = new Proxy({ a: 1, b: 2 }, {
  ownKeys() {
    console.log('proxy-ownKeys');
    return ['a', 'c']
  }
});
// 都會打印 'proxy-ownKeys'
let x1 = Object.entries(proxy);
let x2 = Object.keys(proxy);
let x3 = Object.values(proxy);
for (let x4 in proxy) { }
let x5 = Object.getOwnPropertyNames(proxy);
let x6 = Object.getOwnPropertyDescriptors(proxy);
let x7 = Object.getOwnPropertySymbols(proxy);
// entries、keys、values 都會過濾不可能顯示的屬性,跟被代理對象一致
console.log(x1);    // [ [ 'a', 1 ] ]
複製代碼


Proxy.revocable()

Proxy.revocable()方法與new Proxy()用法相似,都是依次傳入被代理對象和控制器對象,返回一個包含proxy對象和revoke方法的對象。其中,proxy對象和上面描述的一致,revoke方法用於解除proxy對象的代理,一旦取消將沒法繼續使用。

let proxyR = Proxy.revocable({ a: 1 }, {});
console.dir(proxyR);        // { proxy: { a: 1 }, revoke: [Function] }
console.log(proxyR.proxy);  // { a: 1 }
proxyR.revoke();
// console.log(proxyR); // 報錯複製代碼

Reflect 對象

Reflect對象也是 ES6 新增的,意爲"映射",表示把其餘內置對象中經常使用的方法映射到Reflect對象上。因此只有靜態方法Reflect對象的設計目的有這樣幾個:

  • Object對象的一些明顯屬於語言內部的方法放到Reflect對象上。現階段,某些方法同時在ObjectReflect對象上部署,將來的新方法部署在Reflect對象上。也就是說,Reflect對象上能夠拿到語言內部的方法
  • 修改某些Object方法的返回結果,讓其變得更合理。好比,Object.defineProperty(obj, name, desc)在沒法定義屬性時,會拋出一個錯誤,而Reflect.defineProperty(obj, name, desc)則會返回false
  • Object操做都變成函數行爲。某些Object操做是命令式,好比name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)讓它們變成了函數行爲。
  • Reflect對象的方法與Proxy對象的方法一一對應,只要是Proxy對象的方法,就能在Reflect對象上找到對應的方法。這就讓Proxy對象能夠方便地調用對應的Reflect方法,完成默認行爲,做爲修改行爲的基礎。也就是說,無論Proxy怎麼修改默認行爲,你總能夠在Reflect上獲取默認行爲。

以上摘自阮一峯的《ES6入門》

靜態方法

Reflect的靜態方法與Proxy的控制器方法一一對應,甚至連傳參都是一致的,目前一樣是 13 個,下面簡單列出方法及其對應的"舊"方法。

  • Reflect.get(target, name, receiver):對應獲取屬性值,如obj.a,不存在則返回undefined。這裏須要注意第三個參數receiver,若獲取的屬性是getter,那麼getterthis會指向receiver對象。
  • Reflect.set(target, name, value, receiver):對應設置屬性值,如obj.a = 'value'receiver對象一樣影響setterthis指向。
  • Reflect.has(obj, name):對應in運算符,如'a' in obj
  • Reflect.deleteProperty(obj, name):對應delete運算符,如delete obj.a。方法返回布爾值,若刪除成功或被刪除的屬性不存在,返回true;刪除失敗,即被刪除的屬性依然存在([[configurable]] = false),返回false
  • Reflect.construct(target, args):對應new運算符,如new Fn(...args)
  • Reflect.getPrototypeOf(obj):對應Object.getPrototypeOf()。二者差異在於當參數爲非對象時,Object.getPrototypeOf()會被"包裝"成對象後傳入;而Reflect.getPrototypeOf()會報錯。
  • Reflect.setPrototypeOf(obj, newProto):對應Object.setPrototypeOf()。方法返回布爾值,表示是否設置成功。
  • Reflect.apply(func, thisArg, args):對應Function.prototype.apply.call()
  • Reflect.defineProperty(target, propKey, attributes):對應Object.defineProperty()。方法返回布爾值,表示是否認義成功。
  • Reflect.getOwnPropertyDescriptor(target, propKey):對應Object.getOwnPropertyDescriptor()
  • Reflect.isExtensible(target):對應Object.isExtensible()
  • Reflect.preventExtensions(target):對應Object.preventExtensions()
  • Reflect.ownKeys(target):對應Object.getOwnPropertyNames()Object.getOwnPropertySymbols()
相關文章
相關標籤/搜索