1. 程式人生 > >理解ES6 proxy&reflection

理解ES6 proxy&reflection

簡介

  • proxy

    proxy可以攔截目標(target)上的物件進行操作,使用trap攔截這些操作,trap是響應特定操作的方法。

  • reflection

    reflection是通過Reflect物件表示,他提供了一些方法集,為代理proxy提供預設行為。

下面是一些proxy trapReflect方法,每個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.ownKeys()
apply Calling a function Reflect.apply()
construct Calling a function with new Reflect.construct()

這裡的每個trap都會覆蓋物件的內建行為,便於攔截和修改物件。如果你真的需要內建行為,可以使用相對應的Reflect方法。

開始的時候,ES6的規範有個enumerate trap,用於改變for..inObject.keys的列舉屬性,但是在實行的時候發現有些困難,於是在ES7中移除了。所以這裡不討論他。

建立一個簡單的代理

當你使用Proxy的建構函式去建立代理的時候,他接受兩個引數,一個是目標物件(target),另外一個是處理物件(handler)。這個handler定義了一個或者多個trap去處理代理,如果沒有定義trap,那麼就會使用預設的行為。

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"
複製程式碼

從上面這個例子可以發現,不管是proxy或者是target的屬性更改,都會影響到另外一個。其實這就是這兩個的關係: proxy本身不儲存這個屬性,他只是把操作轉發到target 上面的這個例子似乎沒啥意思,並沒有體現出核心trap的價值所在。

使用set trap驗證屬性

set trap是在設定屬性值時觸發。 set trap接收這幾個引數:

  1. trapTarget - 接收的屬性的物件,就是代理的目標。
  2. key - 要寫入的屬性的key(string || symbol)
  3. value - 寫入屬性的值
  4. receiver - 操作的物件,通常是代理

Reflect.setset trap相對應的方法。如果屬性被設定,那麼trap應該返回true,如果沒有被設定,那就返回falseReflect.set()會根據操作是否成功返回正確的值。

要驗證一個屬性的值,那就需要使用set trap來檢查這個值,看下面程式碼:

let target = {
    name: "target"
};

let proxy = new Proxy(target, {
    set(trapTarget, key, value, receiver) {
        console.log(`trapTarget is ${trapTarget}, key is ${key}, value is ${value}, receiver is ${receiver}`)
        // 忽視存在的屬性,以免產生影響
        if (!trapTarget.hasOwnProperty(key)) {
            if (isNaN(value)) {
                throw new TypeError("Property must be a number.");
            }
        }

        // 新增到屬性
        return Reflect.set(trapTarget, key, value, receiver);
    }
});

// 新增一個新的屬性
proxy.count = 1;
console.log(proxy.count);       // 1
console.log(target.count);      // 1

// 賦值給存在target上的屬性
proxy.name = "proxy";
console.log(proxy.name);        // "proxy"
console.log(target.name);       // "proxy"

// 新的屬性值不是數字會丟擲異常
proxy.anotherName = "proxy";
複製程式碼

可以發現,每次設定屬性值的時候都會進行攔截判斷,所以,相對的,你在獲取的時候,可以使用get進行攔截判斷。

使用get trap驗證

js一個有趣又令人困惑的地方就是獲取一個不存在的屬性的時候,不會丟擲異常,只會返回一個undefined。不像其他的大多數語言,都會丟擲一個錯誤,可能你寫了大量的程式碼,你可能會意識到這是一個嚴重的問題,比如拼寫錯誤的這些問題,代理可以為你處理這些。

get是在讀取物件屬性的時候用到的trap 他接收三個引數:

  1. trapTarget - 從哪個物件讀取的屬性,就是target.
  2. key - 讀取的key
  3. receiver - 操作的物件,通常是代理(proxy)

可以發現這個和上面的set差不多,就是少了一個設定的value引數。相對的,Reflect.get方法接受與get trap相同的三個引數,並返回屬性的預設值。

var proxy = new Proxy({}, {
        get(trapTarget, key, receiver) {
            if (!(key in receiver)) {
                throw new TypeError("Property " + key + " doesn't exist.");
            }

            return Reflect.get(trapTarget, key, receiver);
        }
    });

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

// 不存在這個屬性,丟擲錯誤
console.log(proxy.age);             // throws error
複製程式碼

不知道你有沒有發現,我們在這裡使用receiver代替trapTarget配合in一起使用,我們將在下面介紹。

使用has trap隱藏屬性的存在

in這個操作想來大家比較熟悉的,是確定屬性是否存在物件及原型鏈上。

var target = {
    value: 42;
}

console.log("value" in target);     // true
console.log("toString" in target);  // true
複製程式碼

代理允許你使用has這個trap去返回不同的值。 這個has trap是在使用in操作時觸發。has trap接收兩個引數:

  1. trapTarget
  2. key

Reflect.has方法接受這些相同的引數並返回in運算子的預設響應。使用has trapReflect.has可以改變某些屬性的in行為,同時又回退到其他屬性的預設行為。例如你只想隱藏value屬性:

var target = {
    name: "target",
    value: 42
};

var 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
複製程式碼

可以發現上例直接判斷,如果不是value key,就使用Reflect去返回其預設行為。

使用deleteProperty trap對刪除進行操作

通過屬性描述那部分我們知道,delete是通過configurable來控制的,非嚴格模式下刪除會返回false,嚴格模式下會報錯。但是我們可以使用代理deleteProperty trap去操作他這個行為。

下面我們再來看看deleteProperty這個trap。他也是接受兩個引數:

  1. trapTarget
  2. key

Reflect.deleteProperty方法提供了deleteProperty trap相對的行為去實現。所以我們可以使用這兩個去改變delete的預設行為。

let target = {
    name: "target",
    value: 42
};

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

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

// Attempt to delete proxy.value

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

let result1 = delete proxy.value;
console.log(result1);               // false

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

// Attempt to delete proxy.name

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

let result2 = delete proxy.name;
console.log(result2);               // true

console.log("name" in proxy);       // false
複製程式碼

這樣可以攔截操作,好奇的你可能會想去操作nonconfigurable的時候,也可以刪除,你可以嘗試一下。這個方法在受保護的屬性被刪除的時候,非嚴格模式下會拋錯。

原型的代理trap

這個章節裡介紹了setPrototypeOfgetPrototypeOf。代理也為這兩種情況添加了相應的trap。針對這兩個代理的trap,都有不同的引數:

  • setPrototypeOf
    1. trapTarget
    2. proto 這個用作原型的物件

他和Reflect.setPrototypeOf接收的引數相同,去做相對應的操作。另一方面,getPrototypeOf只接收一個引數trapTarget,相應的也存在Reflect.getPrototypeOf方法.

原型代理是如何工作的

他們有一些限制。首先,getPrototypeOf只可以返回物件或者null,返回其他的,在執行的時候會報錯。同樣的,setPrototypeOf trap如果失敗,必須返回false,並且Object.setPrototypeOf會拋錯, 如果返回其他的值,那就是認為操作成功。

下面來看一個例子:

var target = {};
var proxy = new Proxy(target, {
    getPrototypeOf(trapTarget) {
        return null;
    },
    setPrototypeOf(trapTarget, proto) {
        return false;
    }
});

var targetProto = Object.getPrototypeOf(target);
var proxyProto = Object.getPrototypeOf(proxy);

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

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

// throws error
Object.setPrototypeOf(proxy, {});
複製程式碼

從上面可以發現,對於proxy進行了攔截,以至於原型不同。然後對proxy進行setPrototypeOf會丟擲異常,就是上面提到的,setPrototypeOf返回false,那麼Object.setPrototypeOf會丟擲錯誤。 當然,如果你想要使用它的預設行為,那就需要使用Reflect物件的方法來操作。

為什麼會有兩套方法

讓人感到困惑的是, setPrototypeOf trapgetPrototypeOf trap看起來和Object.getPrototypeOf() or Object.setPrototypeOf()看起來類似,為什麼還要這兩套方法。其實他們看起來是類似,但是還有很大的差異:

首先,Object.getPrototype||Object.setPrototypeOf在一開始就是為開發人員建立的高級別的操作。然而Reflect.getPrototypeOf || Reflect.setPrototypeOf是提供了操作以前僅僅用於內部的[[GetPrototypeOf]] || [[SetPrototypeOf]]的底層屬性。Reflect.getPrototypeOf方法是內部[[GetPrototypeOf]]操作的包裝器(帶有一些輸入驗證)。Reflect.setPrototypeOf方法和[[SetPrototypeOf]]具有相同的關係。Object上的相應方法也呼叫[[GetPrototypeOf]][[SetPrototypeOf]],但在呼叫之前執行幾個步驟並檢查返回值以確定如何操作。

上面說的比較泛泛,下面來詳細說下:

如果Reflect.getPrototypeOf方法的引數不是物件或者null,則丟擲錯誤;而Object.getPrototypeOf在執行操作之前首先將值強制轉換為物件。

var result1 = Object.getPrototypeOf(1);
console.log(result1 === Number.prototype);  // true

// throws an error
Reflect.getPrototypeOf(1);
複製程式碼

很明顯,Object上的可以工作,他把數字1轉換成了物件,Reflect上的不會進行轉換,所以丟擲異常。

setPrototypeOf也有一些不同,Reflect.setPrototypeOf會返回一個布林來確定是否成功,false就是失敗。然而Object.setPrototypeOf如果失敗,會丟擲錯誤。

物件 Extensibility trap

ECMAScript 5通過Object.preventExtensionsObject.isExtensible方法添加了物件可擴充套件性的操作,因此ES6在此基礎上對這兩個方法添加了代理。並且這兩個代理方法都只接收一個引數trapTarget.isExtensible trap必須返回布林值來確定是否是可擴充套件的,preventExtensions trap返回布林值確定是否成功。

Reflect物件裡的這兩個方法都會返回布林值,所以這兩個是可以作為相對應的方法去使用實現預設行為。

兩個簡單的例子

var target = {};
var proxy = new Proxy(target, {
    isExtensible(trapTarget) {
        return Reflect.isExtensible(trapTarget);
    },
    preventExtensions(trapTarget) {
        return Reflect.preventExtensions(trapTarget);
    }
});


console.log(Object.isExtensible(target));       // true
console.log(Object.isExtensible(proxy));        // true

Object.preventExtensions(proxy);

console.log(Object.isExtensible(target));       // false
console.log(Object.isExtensible(proxy));        // false
複製程式碼

這個例子就是使用代理攔截並返回他的預設行為,等於預設的情況。所以經過Object屬性操作之後,就是返回預設的行為。

如果我們不想他拓展,我們可以這樣去處理:

var target = {};
var proxy = new Proxy(target, {
    isExtensible(trapTarget) {
        return Reflect.isExtensible(trapTarget);
    },
    preventExtensions(trapTarget) {
        return false
    }
});


console.log(Object.isExtensible(target));       // true
console.log(Object.isExtensible(proxy));        // true

Object.preventExtensions(proxy);

console.log(Object.isExtensible(target));       // true
console.log(Object.isExtensible(proxy));        // true
複製程式碼

這裡他不會成功,因為返回了false,沒有使用對應的Reflect去做相對的預設行為處理,所以操作不會轉發到操作的目標。

Duplicate Extensibility Methods

如果傳遞物件值作為引數,方法Object.isExtensibleReflect.isExtensible類似。如果不是物件作為引數傳遞,Object.isExtensible始終返回false,而Reflect.isExtensible則丟擲錯誤。

let result1 = Object.isExtensible(2);
console.log(result1);                       // false

// throws error, Reflect.isExtensible called on non-object
let result2 = Reflect.isExtensible(2);
複製程式碼

這個限制類似於Object.getPrototypeOfReflect.getPrototypeOf方法之間的差異,因為具有較低級別功能的方法具有比其更高級別對應方更嚴格的錯誤檢查。

Object.preventExtensionsReflect.preventExtensions方法也非常相似。 Object.preventExtensions方法始終返回作為引數傳遞給它的值,即使該值不是物件也是如此。然而另一方面,如果引數不是物件,那麼Reflect.preventExtensions方法會丟擲錯誤;如果引數是一個物件,那麼 Reflect.preventExtensions在操作成功時返回true,否則返回false

var result1 = Object.preventExtensions(2);
console.log(result1);                               // 2

var target = {};
var result2 = Reflect.preventExtensions(target);
console.log(result2);                               // true

// throws error
var result3 = Reflect.preventExtensions(2);
複製程式碼

這個例子就是對上面的總結。

Property Descriptor Traps

ECMAScript 5最重要的功能之一是使用Object.defineProperty方法定義屬性具體屬性的能力。在以前的JavaScript版本中,無法定義訪問者屬性,使屬性成為只讀,或使屬性不可數。具體參考這裡

代理允許分別 使用defineProperty trapgetOwnPropertyDescriptor trap攔截對Object.definePropertyObject.getOwnPropertyDescriptor的呼叫。 defineProperty trap接收以下引數:

  1. trapTarget - 被定義屬性的物件(代理的目標)
  2. key
  3. descriptor

defineProperty trap返回布林值。getOwnPropertyDescriptor trap只接收trapTargetkey,並且返回描述資訊。相應的Reflect.definePropertyReflect.getOwnPropertyDescriptor方法接受與其代理trap對應方相同的引數。

例如:

var proxy = new Proxy({}, {
    defineProperty(trapTarget, key, descriptor) {
        return Reflect.defineProperty(trapTarget, key, descriptor);
    },
    getOwnPropertyDescriptor(trapTarget, key) {
        return Reflect.getOwnPropertyDescriptor(trapTarget, key);
    }
});


Object.defineProperty(proxy, "name", {
    value: "proxy"
});

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

var descriptor = Object.getOwnPropertyDescriptor(proxy, "name");

console.log(descriptor.value);      // "proxy"
複製程式碼

很簡單的一個例子,基本沒有在攔截上做任何操作,只是返回他的預設行為。

Blocking Object.defineProperty()

trap返回true時,Object.defineProperty表示成功; trap返回false時,Object.defineProperty會丟擲錯誤。 可以使用這個功能來限制Object.defineProperty方法可以定義的屬性型別.如下:

var proxy = new Proxy({}, {
    defineProperty(trapTarget, key, descriptor) {
        if (typeof key === "symbol") {
            return false;
        }

        return Reflect.defineProperty(trapTarget, key, descriptor);
    }
});


Object.defineProperty(proxy, "name", {
    value: "proxy"
});

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

var nameSymbol = Symbol("name");

// throws error
Object.defineProperty(proxy, nameSymbol, {
    value: "proxy"
});
複製程式碼

這裡我們檢測key的型別,如果是symbol就返回false.對於Object.defineProperty,返回false會丟擲異常。

當然可以通過返回true而不呼叫Reflect.defineProperty方法使Object.defineProperty預設是失敗的,這就避免錯誤的丟擲。

Descriptor Object Restrictions

為了確保在使用Object.definePropertyObject.getOwnPropertyDescriptor方法時的一致行為,傳遞給defineProperty trap的描述符物件被規範化。從getOwnPropertyDescriptor trap返回的物件總是出於同樣的原因進行驗證。

不管哪個引數作為第三個引數傳遞給Object.defineProperty方法,都只能是下面這幾種:enumerable, configurable, value, writable, get, set 這些將被作為descriptor傳遞。例如:

var proxy = new Proxy({}, {
    defineProperty(trapTarget, key, descriptor) {
        console.log(descriptor.value);              // "proxy"
        console.log(descriptor.name);               // undefined
        console.log(descriptor.writable)          // undefined
        return Reflect.defineProperty(trapTarget, key, descriptor);
    }
});


Object.defineProperty(proxy, "name", {
    value: "proxy",
    name: "custom"
});
複製程式碼

可以發現,name不存在那幾個descriptor裡,所以傳遞不進去,不接收。並且這個和Object.defineProperty不同,沒有進行一些包裝,不存在預設的writable, configurable這些..。但是按理來說,你傳遞一個物件進行,他就應該接收啊,為啥這裡會是undefined呢?這是因為**descriptor實際上不是對傳遞給Object.defineProperty方法的第三個引數的引用,而是一個僅包含允許屬性的新物件。Reflect.defineProperty方法還會忽略描述符上的任何非標準屬性**

getOwnPropertyDescriptor稍微有些不同,他會返回null, undefined,object.如果返回的是物件,那麼物件只會包含上面可能出現的descriptor的這幾種情況。

如果返回具有不允許的屬性的物件,會導致錯誤,如下程式碼:

var proxy = new Proxy({}, {
    getOwnPropertyDescriptor(trapTarget, key) {
        return {
            name: "proxy"
        };
    }
});

// throws error
var descriptor = Object.getOwnPropertyDescriptor(proxy, "name");
複製程式碼

因為name不屬於descriptor接受的範圍,所以引發了錯誤。這個限制可確保Object.getOwnPropertyDescriptor返回的值始終具有可靠的結構,無論代理使用什麼。

Duplicate Descriptor Methods

和上面的一些trap類似,這個也有一些讓人為之困惑的類似的方法。這裡的是Object.defineProperty&Object. getOwnPropertyDescriptorReflect. defineProperty&Reflect.getOwnPropertyDescriptor.

defineProperty() Methods

看看這個方法的異同.

Object.definePropertyReflect.defineProperty方法完全相同,只是它們的返回值有所不同。

var target = {};

var result1 = Object.defineProperty(target, "name", { value: "target "});

console.log(target === result1);        // true

var result2 = Reflect.defineProperty(target, "name", { value: "reflect" });

console.log(result2);                   // true
複製程式碼

可以發現,Object.defineProperty返回的是傳入的第一個引數,Reflect.defineProperty返回的布林值確定是否成功。

getOwnPropertyDescriptor() Methods

Object.getOwnPropertyDescriptor方法傳入的引數是原始值的時候,會轉換成物件進行處理。至於Reflect.getOwnPropertyDescriptor傳入的不是物件,會丟擲錯誤:

descriptor1 = Object.getOwnPropertyDescriptor(2, "name");
console.log(descriptor1);       // undefined

// throws an error
descriptor2 = Reflect.getOwnPropertyDescriptor(2, "name");
複製程式碼

The ownKeys Trap

ownKeys trap允許你攔截內部的方法[[OwnPropertyKeys]]並覆蓋預設的行為通過返回一組值。返回的這個陣列值用於四個方法:Object.getOwnPropertyNames, Object.keys,Object.getOwnPropertySymbols()Object.assign(Object.assign用於陣列來確定要複製的屬性)。

ownKeys trap的預設行為是通過Reflect.ownKeys來實現,返回的就是一個數組,裡面包含所有的屬性keys(strings, symbols). 我們知道Object.keysObject.getOwnPropertyNames返回的是過濾掉symbol key的集合,但是Object.getOwnPropertySymbols卻是相反,所以ownKeys集合了這幾個之後,就可以返回所有的keys.並且Object.assign作用於stringssymbols鍵的物件。

ownKeys trap接收一個引數,就是trapTarget。他總是返回陣列或者類似陣列的值,否則會引發錯誤。

看下面這個例子:

var proxy = new Proxy({}, {
    ownKeys(trapTarget) {
        return Reflect.ownKeys(trapTarget).filter(key => {
            return typeof key !== "string" || key[0] !== "_";
        });
    }
});

var nameSymbol = Symbol("name");

proxy.name = "proxy";
proxy._name = "private";
proxy[nameSymbol] = "symbol";

var names = Object.getOwnPropertyNames(proxy),
    keys = Object.keys(proxy);
    symbols = Object.getOwnPropertySymbols(proxy);

console.log(names.length);      // 1
console.log(names[0]);          // "name"

console.log(keys.length);      // 1
console.log(keys[0]);          // "name"

console.log(symbols.length);    // 1
console.log(symbols[0]);        // "Symbol(name)"
複製程式碼

最終返回的這個陣列就是得到的結果。

ownKeys trap也會影響for-in迴圈,該迴圈呼叫trap來確定在迴圈內使用哪些鍵。

Function Proxies with the apply and construct Traps

這個可能是比較特殊的了。在代理的所有的trap中,只有apply trapconstruct trap這兩個要求代理的target是必須一個function,我們知道 function有兩個內部的屬性[[Call]][[Construct]]分別用於直接呼叫和new關鍵字呼叫的時候。因此apply trap在攔截直接呼叫的時候用到的,construct trap是攔截new呼叫時候用到的。

我們先來看看直接呼叫的的時候,

  • apply trap的引數:
    1. trapTarget
    2. thisArg - 呼叫期間的上下文物件this
    3. argumentsList - 傳遞到方法的陣列引數

 

再來看看new關鍵字呼叫時候。

  • construct trap的引數
    1. trapTarget
    2. argumentsList

Reflect.construct方法也接受這兩個引數,並有一個名為newTarget的可選第三個引數。如果給定這個第三個引數,newTarget這個引數就是new.target的值。

使用applyconstruct兩個trap就可以攔截所有的方法呼叫.

var target = function() { return 42 },
var proxy = new Proxy(target, {
        apply: function(trapTarget, thisArg, argumentList) {
            return Reflect.apply(trapTarget, thisArg, argumentList);
        },
        construct: function(trapTarget, argumentList) {
            return Reflect.construct(trapTarget, argumentList);
        }
    });

// a proxy with a function as its target looks like a function
console.log(typeof proxy);                  // "function"

console.log(proxy());                       // 42

var instance = new proxy();
console.log(instance instanceof proxy);     // true
console.log(instance instanceof target);    // true
複製程式碼

這個和上面幾個類似,都是攔截之後使用它的預設行為。

Validating Function Parameters

下面來一個驗證引數型別的例子:

// adds together all arguments
function sum(...values) {
    return values.reduce((previous, current) => previous + current, 0);
}

var sumProxy = new Proxy(sum, {
        apply(trapTarget, thisArg, argumentList) {

            argumentList.forEach((arg) => {
                if (typeof arg !== "number") {
                    throw new TypeError("All arguments must be numbers.");
                }
            });

            return Reflect.apply(trapTarget, thisArg, argumentList);
        },
        construct(trapTarget, argumentList) {
            throw new TypeError("This function can't be called with new.");
        }
    });

console.log(sumProxy(1, 2, 3, 4));          // 10

// throws error
console.log(sumProxy(1, "2", 3, 4));

// also throws error
var result = new sumProxy();
複製程式碼

在這裡,我們對引數進行了過濾處理,並且在new呼叫的時候,直接丟擲錯誤,不讓他去new

Calling Constructors Without new

我們之前介紹了關於new的相關介紹,判斷一個函式是不是new呼叫,需要使用new.target來判斷。

function Numbers(...values) {

    if (typeof new.target === "undefined") {
        throw new TypeError("This function must be called with new.");
    }

    this.values = values;
}

let instance = new Numbers(1, 2, 3, 4);
console.log(instance.values);               // [1,2,3,4]

// throws error
Numbers(1, 2, 3, 4);
複製程式碼

可以發現,這個類似於在上面提到的使用proxy驗證,但是這個明顯更加方便一點。如果只是為了判斷是否new呼叫,這個是可取的,但是有時候你需要知道做更多的控制,這個就辦不到了。

function Numbers(...values) {

    if (typeof new.target === "undefined") {
        throw new TypeError("This function must be called with new.");
    }

    this.values = values;
}


let NumbersProxy = new Proxy(Numbers, {
        apply: function(trapTarget, thisArg, argumentsList) {
            return Reflect.construct(trapTarget, argumentsList);
        }
    });


let instance = NumbersProxy(1, 2, 3, 4);
console.log(instance.values);               // [1,2,3,4]
複製程式碼

可以發現這個,在函式內部還是有檢查,但是在表面呼叫的時候是沒有使用這個new的,只是我們在代理裡的apply trap裡使用了Reflect.construct

Overriding Abstract Base Class Constructors

可以在Reflect.construct內傳入第三個引數,用作new.target的值。這可以在建構函式中檢查new.target的值的時候用到。

class AbstractNumbers {

    constructor(...values) {
        if (new.target === AbstractNumbers) {
            throw new TypeError("This function must be inherited from.");
        }

        this.values = values;
    }
}

class Numbers extends AbstractNumbers {}

let instance = new Numbers(1, 2, 3, 4);
console.log(instance.values);           // [1,2,3,4]

// throws error
new AbstractNumbers(1, 2, 3, 4);
複製程式碼

上面可以發現有個限制,下面我們來試試使用代理來跳過.

class AbstractNumbers {

    constructor(...values) {
        if (new.target === AbstractNumbers) {
            throw new TypeError("This function must be inherited from.");
        }

        this.values = values;
    }
}

var AbstractNumbersProxy = new Proxy(AbstractNumbers, {
        construct: function(trapTarget, argumentList) {
            return Reflect.construct(trapTarget, argumentList, function() {});
        }
    });


let instance = new AbstractNumbersProxy(1, 2, 3, 4);
console.log(instance.values);               // [1,2,3,4]
複製程式碼

這樣,添加了第三個引數,這樣new.target的值就是一個另外一個值了(匿名函式)。

Callable Class Constructors

我們知道class只能被new去呼叫,這是因為在建構函式的內部方法,[[Call]]被指定丟擲錯誤。但是我們使用代理可以攔截這個內部屬性,所以可以改變我們的呼叫方法。

例如我們想不通過new來呼叫一個class,可以通過代理,如下:

class Person {
    constructor(name) {
        this.name = name;
    }
}

var PersonProxy = new Proxy(Person, {
        apply: function(trapTarget, thisArg, argumentList) {
            return new trapTarget(...argumentList);
        }
    });


var me = PersonProxy("Nicholas");
console.log(me.name);                   // "Nicholas"
console.log(me instanceof Person);      // true
console.log(me instanceof PersonProxy); // true
複製程式碼

可以發現,我們在apply這個trap對他進行了new一個。

Revocable Proxies

通常情況下,綁定了代理之後都是沒有辦法撤掉的,但是這個可以取消,通過Proxy.revocable去取消。這個方法和Proxy的建構函式傳參類似,一個target和一個handler.

他返回的物件是有兩個屬性:

  • proxy - 被撤銷的代理物件
  • revoke - 呼叫撤銷代理的函式

revoke被呼叫的時候,就不能繼續使用代理了。

var target = {
    name: "target"
};

var { proxy, revoke } = Proxy.revocable(target, {});

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

revoke();

// throws error
console.log(proxy.name);
複製程式碼

可以發現,在呼叫revoke方法之後,代理就不能繼續使用了。如果呼叫,就會丟擲錯誤,不會返回undefined

Solving the Array Problem

看一個關於陣列的問題:

let colors = ["red", "green", "blue"];

console.log(colors.length);         // 3

colors[3] = "black";

console.log(colors.length);         // 4
console.log(colors[3]);             // "black"

colors.length = 2;

console.log(colors.length);         // 2
console.log(colors[3]);             // undefined
console.log(colors[2]);             // undefined
console.log(colors[1]);             // "green"
複製程式碼

在這裡,length控制著陣列的資料,一般情況下,我們沒法子修改這些高階操作。

Detecting Array Indices

ECMAScript 6規範提供了有關如何確定屬性鍵是否為陣列索引的說明:

當且僅當ToString(ToUint32(P))等於PToUint32(p)不等於2的32次方減1時,字串屬性名P才是陣列索引。

這個規範,在js中可以實現:

function toUint32(value) {
    return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}

function isArrayIndex(key) {
    let numericKey = toUint32(key);
    return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}
複製程式碼

toUint32函式使用規範中描述的演算法將給定值轉換為無符號的32位整數,isArrayIndex函式首先將金鑰轉換為uint32,然後執行比較以確定金鑰是否為陣列索引。

Increasing length when Adding New Elements

可以發現數組的行為,其實使用set trap就可以完成這兩個行為。

function toUint32(value) {
    return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}

function isArrayIndex(key) {
    let numericKey = toUint32(key);
    return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}

function createMyArray(length=0) {
    return new Proxy({ length }, {
        set(trapTarget, key, value) {

            let currentLength = Reflect.get(trapTarget, "length");

            // the special case
            if (isArrayIndex(key)) {
                let numericKey = Number(key);

                if (numericKey >= currentLength) {
                    Reflect.set(trapTarget, "length", numericKey + 1);
                }
            }

            // always do this regardless of key type
            return Reflect.set(trapTarget, key, value);
        }
    });
}

var colors = createMyArray(3);
console.log(colors.length);         // 3

colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";

console.log(colors.length);         // 3

colors[3] = "black";

console.log(colors.length);         // 4
console.log(colors[3]);             // "black"
複製程式碼

可以發現,上面對寫入的key進行了驗證。如果符合規範,則會給length進行新增操作。其他的會一直操作key. 現在,基於陣列的length的第一個功能成立了,接下來是進行第二步。

Deleting Elements on Reducing length

這裡就需要對減少的長度的部分進行刪除了。

function toUint32(value) {
    return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}

function isArrayIndex(key) {
    let numericKey = toUint32(key);
    return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}

function createMyArray(length=0) {
    return new Proxy({ length }, {
        set(trapTarget, key, value) {

            let currentLength = Reflect.get(trapTarget, "length");

            // the special case
            if (isArrayIndex(key)) {
                let numericKey = Number(key);

                if (numericKey >= currentLength) {
                    Reflect.set(trapTarget, "length", numericKey + 1);
                }
            } else if (key === "length") {

                if (value < currentLength) {
                    for (let index = currentLength - 1; index >= value; index\
--) {
                        Reflect.deleteProperty(trapTarget, index);
                    }
                }

            }

            // always do this regardless of key type
            return Reflect.set(trapTarget, key, value);
        }
    });
}

let colors = createMyArray(3);
console.log(colors.length);         // 3

colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
colors[3] = "black";

console.log(colors.length);         // 4

colors.length = 2;

console.log(colors.length);         // 2
console.log(colors[3]);             // undefined
console.log(colors[2]);             // undefined
console.log(colors[1]);             // "green"
console.log(colors[0]);             // "red"
複製程式碼

可以發現,我們在每次length操作的時候,都會進行一次監聽操作,用來減去他刪除的部分。

Implementing the MyArray Class

使用代理建立類的最簡單方法是像往常一樣定義類,然後從建構函式返回代理。這樣,例項化類時返回的物件將是代理而不是例項。(例項是建構函式內部的this值)。例項成為代理的目標,並返回代理,就好像它是例項一樣。那麼這個例項將完全私有,無法直接訪問它,但可以通過代理間接訪問它。 看一個簡單的例子:

class Thing {
    constructor() {
        return new Proxy(this, {});
    }
}

var myThing = new Thing();
console.log(myThing instanceof Thing);      // true
複製程式碼

我們知道,constructor內返回的基本資料型別不會影響他的返回,如果是非基本型別,那麼就是具體的返回物件了。所以這裡返回到是proxy,因此這裡的這個myThing就是這個proxy. 由於代理會把他的行為傳遞給他的目標,因此myThing仍然被當做是Thing的例項。

考慮到上面這一點,使用代理建立自定義陣列類相對簡單點。程式碼與“刪除減少長度的元素”部分中的程式碼大致相同。使用相同的代理程式碼,但這一次,它在類建構函式中。

function toUint32(value) {
    return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}

function isArrayIndex(key) {
    let numericKey = toUint32(key);
    return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}

class MyArray {
    constructor(length=0) {
        this.length = length;

        return new Proxy(this, {
            set(trapTarget, key, value) {

                let currentLength = Reflect.get(trapTarget, "length");

                // the special case
                if (isArrayIndex(key)) {
                    let numericKey = Number(key);

                    if (numericKey >= currentLength) {
                        Reflect.set(trapTarget, "length", numericKey + 1);
                    }
                } else if (key === "length") {

                    if (value < currentLength) {
                        for (let index = currentLength - 1; index >= value; i\
ndex--) {
                            Reflect.deleteProperty(trapTarget, index);
                        }
                    }

                }

                // always do this regardless of key type
                return Reflect.set(trapTarget, key, value);
            }
        });

    }
}


let colors = new MyArray(3);
console.log(colors instanceof MyArray);     // true

console.log(colors.length);         // 3

colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
colors[3] = "black";

console.log(colors.length);         // 4

colors.length = 2;

console.log(colors.length);         // 2
console.log(colors[3]);             // undefined
console.log(colors[2]);             // undefined
console.log(colors[1]);             // "green"
console.log(colors[0]);             // "red"
複製程式碼

這就是利用我們上面提到的那點,返回的最終是個代理來完成這個操作。

儘管這樣很容易,但是他為每一個新的例項都建立了一個代理。但是我們可以為每一個例項都共享一個代理,那就是通過原型。

Using a Proxy as a Prototype

代理可以用作原型,但是這樣會提高複雜度,比上面的實現還要複雜。當代理是原型時,僅當預設操作通常繼續到原型時才會呼叫代理trap,這會將代理的功能限制為原型。如下:

var target = {};
var newTarget = Object.create(new Proxy(target, {

    // never called
    defineProperty(trapTarget, name, descriptor) {

        // would cause an error if called
        return false;
    }
}));

Object.defineProperty(newTarget, "name", {
    value: "newTarget"
});

console.log(newTarget.name);                    // "newTarget"
console.log(newTarget.hasOwnProperty("name"));  // true
複製程式碼

newTarget代理是作為一個原型物件被建立。現在,只有在newTarget上的操作並將操作傳遞到目標(target)上時,這樣才會呼叫代理trap.

definePropertynewTarget的基礎上建立了自己的屬性name,在物件上定義屬性,不會作用到原型, 可以看下原型的影子方法,並且不會呼叫代理的defineProperty trap,只會把這個name屬性新增到自己的屬性裡。

雖然代理在用作原型時受到嚴重限制,但仍有一些陷阱仍然有用。

Using the get Trap on a Prototype

我們知道原型鏈的查詢是現在自己的屬性裡查詢,如果找不到會遍歷原型鏈。因此,只需要給代理設定一個get trap,當查詢的屬性不存在的時候,就會觸發原型上的trap

var target = {};
var thing = Object.create(new Proxy(target, {
    get(trapTarget, key, receiver) {
        throw new ReferenceError(`${key} doesn't exist`);
    }
}));

thing.name = "thing";

console.log(thing.name);        // "thing"

// throw an error
var unknown = thing.unknown;
複製程式碼

可以發現,使用代理作為原型建立thing物件。當呼叫不存在的時候,會丟擲錯誤,如果存在,便不會遍歷到原型,所以不會出錯。

在這個例子中,要理解trapTargetreceiver是不同的物件。當代理當做原型使用時,trapTarget是原型物件本身,receiver是例項物件。在上例中,trapTarget等同於target, receiver等同於thing

Using the set Trap on a Prototype

這個比較麻煩,如果賦值操作繼續到原型,觸發這個trap,他會根據引數情況確定是在原型上或者是在當前例項上建立屬性,他的預設情況就和我們上面說的影子方法一樣。這裡可能有些繞,可以看看下面這個例子:

var target = {};
var thing = Object.create(new Proxy(target, {
    set(trapTarget, key, value, receiver) {
        return Reflect.set(trapTarget, key, value, receiver);
    }
}));

console.log(thing.hasOwnProperty("name"));      // false

// triggers the `set` proxy trap
thing.name = "thing";

console.log(thing.name);                        // "thing"
console.log(thing.hasOwnProperty("name"));      // true

// does not trigger the `set` proxy trap
thing.name = "boo";

console.log(thing.name);                        // "boo"
複製程式碼

在這個例子中,target沒有自己的屬性。 thing物件有一個代理作為其原型,它定義了一個set trap來捕獲任何新屬性的建立。當thing.name被賦值為“thing”作為其值時,將呼叫代理set trap,因為thing沒有自己的name屬性。在這個set trap裡,trapTarget等於targetreceiver等於thing。該操作最終在thing上建立一個新屬性,幸運的是,如果你將receiver作為第四個引數傳入,Reflect.set會為你實現這個預設行為。

如果不傳遞這個第四個receiver引數呢,那麼就會在原型物件上(target)建立屬性, 不會在例項上建立屬性,那麼就導致每次set都去原型操作;如果傳遞之後,那麼在設定過一次就不會去再次觸發原型上的set trap.

Proxies as Prototypes on Classes

類不可以直接修改原型做代理,因為prototype屬性是不可寫的。

'use strict'
class X {}
X.prototype = new Proxy({}, {
	get(trapTarget, key, receiver){
		console.log('class prototype proxy')
	}
})
// Cannot assign to read only property 'prototype' of function 'class X {}'
複製程式碼

但是,可以建立一個通過使用繼承將代理作為其原型的類。首先,需要使用建構函式建立ES5樣式型別定義。然後,用原型覆蓋為代理。

function NoSuchProperty() {
    // empty
}

NoSuchProperty.prototype = new Proxy({}, {
    get(trapTarget, key, receiver) {
        throw new ReferenceError(`${key} doesn't exist`);
    }
});

var thing = new NoSuchProperty();

// throws error due to `get` proxy trap
var result = thing.name;
複製程式碼

函式的prototype屬性沒有限制,因此可以使用代理覆蓋它。

接下來就是建立一個類去繼承這個函式。

function NoSuchProperty() {
    // empty
}

NoSuchProperty.prototype = new Proxy({}, {
    get(trapTarget, key, receiver) {
        throw new ReferenceError(`${key} doesn't exist`);
    }
});

class Square extends NoSuchProperty {
    constructor(length, width) {
        super();
        this.length = length;
        this.width = width;
    }
}

var shape = new Square(2, 6);

var area1 = shape.length * shape.width;
console.log(area1);                         // 12

// throws an error because "anotherWidth" doesn't exist
var area2 = shape.length * shape.anotherWidth;
複製程式碼

這樣,就很好的在原型上使用了代理,一個折中的法子來實現。

我們來該寫下,這樣可能會更直觀:

function NoSuchProperty() {
    // empty
}

// store a reference to the proxy that will be the prototype
var proxy = new Proxy({}, {
    get(trapTarget, key, receiver) {
        throw new ReferenceError(`${key} doesn't exist`);
    }
});

NoSuchProperty.prototype = proxy;

class Square extends NoSuchProperty {
    constructor(length, width) {
        super();
        this.length = length;
        this.width = width;
    }
}

var shape = new Square(2, 6);

var shapeProto = Object.getPrototypeOf(shape);

console.log(shapeProto === proxy);                  // false

var secondLevelProto = Object.getPrototypeOf(shapeProto);

console.log(secondLevelProto === proxy);            // true
複製程式碼

這裡,把代理存在變數中,更加直觀。在這裡shape的原型是Square.prototype,不是proxy。但是Square.prototype的原型是代理,因為他繼承自NoSuchProperty

原文出處請戳這裡


作者:xiaohesong
連結:https://juejin.im/post/5baf3c865188255c64190886
來源:掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。