1. 程式人生 > >理解es6系列-----【proxy和refection】----未完待續

理解es6系列-----【proxy和refection】----未完待續

什麼是proxy和refection

  • 通過 new Proxy()可 生成一個proxy來代替目標物件(target object)來使用。它等於目標物件的虛擬化,對於使用了該proxy的方法而言,二者看起來是一樣的。通過proxy可以一窺原來只能由js引擎完成的底層操作。
  • reflection API 是以Reflect物件為代表的的一組方法,為同級的底層操作提供proxy可以重寫的預設行為。每個proxy trap都有Reflect方法。
Proxy Trap Overrides the Behavior Of Default Behavior
get Reading a property value Reflect.get()
set Writing to a property Reflect.set()
has The in operator Reflect.has()
deleteProperty The delete operator Reflect.deleteProperty()
getPrototypeOf Object.getPrototypeOf() Reflect.getPrototypeOf()
setPrototypeOf Object.setPrototypeOf() Reflect.setPrototypeOf()
isExtensible Object.isExtensible() Reflect.isExtensible()
preventExtensions Object.preventExtensions() Reflect.preventExtensions()
getOwnPropertyDescriptor Object.getOwnPropertyDescriptor() Reflect.getOwnPropertyDescriptor()
defineProperty Object.defineProperty() Reflect.defineProperty
ownKeys Object.keys, Object.getOwnPropertyNames(), Object.getOwnPropertySymbols() Reflect.ownKey()
apply Calling a function Reflect.apply()
construct Calling a function with new Reflect.construct()

生成一個新的簡單proxy

  • 傳入兩個引數,目標物件和控制代碼 (target and handler)。
  • 控制代碼(handler),就是一個定義了一個或多個“陷阱”(trap)的物件。
  • proxy在做沒有定義陷阱的其他操作時,使用預設的行為。此時二者的表現是相同的,操作proxy就等於操作target。
let target = {};

let proxy = new Proxy(target, {});

proxy.name = "proxy";
console.log(proxy.name);        // "proxy"
console.log(target.name);       // "proxy"

target.name = "target";
console.log(proxy.name);        // "target"
console.log(target.name);       // "target"

用set陷阱來驗證屬性

set 陷阱接受4個引數:

  1. trapTarget
  2. key 物件的key,字元或者symbol,重寫的就是這個屬性啦
  3. value 賦予這個屬性的值
  4. receiver 操作在哪個物件上發生,receiver就是哪個物件,通常就是proxy。
let target = {
    name: "target"
};

let proxy = new Proxy(target, {
    set(trapTarget, key, value, receiver) {
        // ignore existing properties so as not to affect them
        if (!trapTarget.hasOwnProperty(key)) {
            if (isNaN(value)) {
                throw new TypeError("Property must be a number.");
            }
        }
        console.table(Reflect)
        // add the property
        return Reflect.set(trapTarget, key, value, receiver);
    }
});

// adding a new property
proxy.count = 1;   // 這時trapTarget就是target,key等於count,value 等於1, receiver 是 proxy本身
console.log(proxy.count);       // 1
console.log(target.count);      // 1

// you can assign to name because it exists on target already
proxy.name = "proxy";
console.log(proxy.name);        // "proxy"
console.log(target.name);       // "proxy"

// throws an error
proxy.anotherName = "proxy";
  • new Proxy 裡,傳入的第二個引數即為handler,這裡定義了set方法,對應的內部操作是Reflect.set(),同時也是預設操作。
  • set proxy trap 和 Reflect.set() 接收同樣的四個引數
  • Reflect.set() 返回一個boolean值標識set操作是否成功,因此,如果set了屬性,trap會返回true,否則返回false。

用get陷阱來驗證物件結構(object shape)

object shape: 一個物件上可用的屬性和方法的集合。

與很多其他語言不通,js奇葩的一點在於,獲取某個不存在的屬性時,不會報錯,而是會返回undefined。在大型專案中,經常由於拼寫錯誤等原因造成這種情況。那麼,如何用Proxy的get方法來避免這一點呢?

使用Object.preventExtensions(), Object.seal(), Object.freeze() 等方法,可以強迫一個物件保持它原有的屬性和方法。現在要使每次試圖獲取物件上不存在的屬性時丟擲錯誤。在讀取屬性時,會走proxy。.get()接收3個引數。

  1. trapTarget
  2. key: 屬性的鍵。一個字串或者symbol。
  3. receiver

比起上面的set,少了一個value。Reflect.get()方法同樣接收這3個引數,並返回屬性的預設值。

let proxy = new Proxy({}, {
    get(trapTartet, key, receiver) {
        if(!(key in receiver)) {
            throw new TypeError(`property ${key} doesn't exist`)
        }
        return Reflect.get(trapTarget, key, recevier)
    }
})

// adding a property still works
proxy.name = "proxy";
console.log(proxy.name);            // "proxy"

// nonexistent properties throw an error
console.log(proxy.nme);             // 識別出了拼寫錯誤,並throws error

用has陷阱來隱藏屬性

  • 使用in操作符會使has陷阱被呼叫。它接收兩個引數trapTarget和key
  • 內部的Refelct.has()接收同樣的2個引數。可以修改其返回的預設值。
let target = {
    name: "target",
    value: 42
};

let proxy = new Proxy(target, {
    has(trapTarget, key) {

        if (key === "value") {
            return false;
        } else {
            return Reflect.has(trapTarget, key);
        }
    }
});


console.log("value" in proxy);      // false
console.log("name" in proxy);       // true
console.log("toString" in proxy);   // true

deleteProperty 來阻止屬性被刪除

  • delete操作符移除一個物件上的屬性,並返回一個boolean標識操作是否成功。
  • 嚴格模式下,試圖刪除一個nonconfigurable屬性(不可改)會丟擲錯誤;非嚴格模式下則返回false。
let target = {
    name: 'target',
    value: 42
}

Object.defineProperty(target, 'name', { configurable: false})

const res = delete target.name // 如果嚴格模式會丟擲錯誤
console.log(res)   // false
console.log('name' in target)  //true
  • delete 操作對應的是deleteProperty 陷阱。它接收2個引數, trapTarget 和 key。
let proxy = new Proxy(target, {
    deleteProperty(trapTarget, key) {

        if (key === "value") {
            return false;
        } else {
            return Reflect.deleteProperty(trapTarget, key);
        }
    }
});

getPrototypeOf 和 setPrototypeof

  • setPrototypeOf陷阱接收兩個引數, trapTarget 和 proto。如果操作不成功,必須返回false。
  • getPrototypeOf陷阱接收一個引數,就是trapTarget。必須返回一個物件或者null。否則會跑錯誤。
  • 對改寫這兩個函式的限制保證了js語言中Object方法的一致性。
//通過一直返回null隱藏了target的原型,同時不允許修改其原型
let target = {}
let proxy = new Proxy(target, {
    getPrototypeOf(trapTarget) {
        return null
    }
    setPrototyoeof(trapTarget, proto) {
        return false
    }
})

let targetProto = Object.getPrototypeOf(target)
let proxyProto = Object.getPrototypeOf(proxy)

console.log(targetProto === Object.prototype)   //true
console.log(proxyProto === Object.prototype)    //false
console.log(proxyProto)                         //null

//succeeds
Object.setPrototypeOf(target, {})

// throw error
Object.setPrototypeOf(proxy, {})   
  • 如果要用預設行為,直接呼叫Reflect.getPrototypeOf/setPrototypeOf, 而不是呼叫Object.getPrototypeOf/setPrototypeOf。這樣做是有原因的,兩者的區別在於:
  1. Object上的方法是高層的,而Refect上的方法是語言底層的。
  2. Refeclt.get/setPrototypeOf()方法其實是把內建的[[GetPrototypeOf]]操作包裝了一層,做了輸入校驗。
  3. Object上的這兩個方法其實也是呼叫內建的[[GetPrototypeOf]]操作,但在呼叫之前還幹了些別的,並且檢查了返回值,來決定後續行為。
  4. 比如說,當傳入的target不是物件時,Refeclt.getPrototypeOf()會丟擲錯誤,而Object.getPrototypeOf會強制轉換引數到物件,再繼續操作。有興趣的童鞋不妨傳個數字進去試試~
  5. 如果操作不成功,Reflect.setPrototypeOf()會返回一個布林值來標識操作是否成功,而如果Object.setPrototypeOf()失敗,則會直接拋錯。前者的返回false 其實就會導致後者的拋錯。

結論

Reflect是跟Object同級的一個js資料型別。一個類。 介紹了基本的api。本質是重寫方法。meta程式設計。

參考文獻: