get
、set
)get
、set
)get
)set
)enumerate
陷阱在哪裡?代理讓您可以攔截和自訂在物件上執行的操作(例如取得屬性)。它們是一種元程式編寫功能。
在以下範例中,proxy
是我們要攔截其操作的物件,而 handler
是處理攔截的物件。在這個案例中,我們只攔截一個操作,get
(取得屬性)。
const
target
=
{};
const
handler
=
{
get
(
target
,
propKey
,
receiver
)
{
console
.
log
(
'get '
+
propKey
);
return
123
;
}
};
const
proxy
=
new
Proxy
(
target
,
handler
);
當我們取得屬性 proxy.foo
時,處理常式會攔截該操作
> proxy.foo
get foo
123
參閱 完整 API 的參考,以取得可攔截操作的清單。
在我們深入了解代理是什麼以及它們為何有用的之前,我們首先需要了解什麼是元編程。
在編程中,有許多層級
基本層級和元層級可以使用不同的語言。在下列元程式中,元編程語言是 JavaScript,而基本編程語言是 Java。
const
str
=
'Hello'
+
'!'
.
repeat
(
3
);
console
.
log
(
'System.out.println("'
+
str
+
'")'
);
元編程可以採取不同的形式。在先前的範例中,我們已將 Java 程式碼列印到主控台。讓我們將 JavaScript 用作元編程語言和基本編程語言。這方面的經典範例是 eval()
函式,它讓您可以即時評估/編譯 JavaScript 程式碼。eval()
的實際使用案例並不多,請參閱 此處。在下列互動中,我們使用它來評估表達式 5 + 2
。
> eval('5 + 2')
7
其他 JavaScript 操作可能看起來不像元編程,但如果您仔細觀察,它們實際上是
// Base level
const
obj
=
{
hello
()
{
console
.
log
(
'Hello!'
);
}
};
// Meta level
for
(
const
key
of
Object
.
keys
(
obj
))
{
console
.
log
(
key
);
}
程式在執行時會檢查自己的結構。這看起來不像元編程,因為在 JavaScript 中,編程結構和資料結構之間的區別很模糊。所有 Object.*
方法 都可以視為元編程功能。
反射式元編程表示程式會處理它自己。 Kiczales 等人 [2] 區分出三種類型的反射式元編程
讓我們來看一些範例。
範例:內省。 Object.keys()
會執行內省(請參閱前一個範例)。
範例:自我修改。下列函式 moveProperty
將一個屬性從來源移動到目標。它透過屬性存取的方括號運算子、賦值運算子,以及 delete
運算子來執行自我修改。(在實際程式碼中,你可能會使用 屬性描述符 來執行此任務。)
function
moveProperty
(
source
,
propertyName
,
target
)
{
target
[
propertyName
]
=
source
[
propertyName
];
delete
source
[
propertyName
];
}
使用 moveProperty()
>
const
obj1
=
{
prop
:
'abc'
};
>
const
obj2
=
{};
>
moveProperty
(
obj1
,
'prop'
,
obj2
);
>
obj1
{}
>
obj2
{
prop
:
'abc'
}
ECMAScript 5 不支援中介;代理物件被建立來填補這個空缺。
ECMAScript 6 代理物件為 JavaScript 帶來中介。它們的工作方式如下。你可以對物件 obj
執行許多作業。例如
obj
的屬性 prop
(obj.prop
)obj
是否有屬性 prop
('prop' in obj
)代理物件是特殊的物件,允許你自訂其中一些作業。代理物件使用兩個參數建立
handler
:對於每個作業,都有對應的處理函式方法,如果存在,則執行該作業。這種方法會攔截作業(在傳遞到目標的過程中),並稱為陷阱(一個從作業系統領域借用的術語)。target
:如果處理函式沒有攔截作業,則會在目標上執行該作業。也就是說,它作為處理函式的後備。在某種程度上,代理物件包裝了目標。在以下範例中,處理函式攔截作業 get
和 has
。
const
target
=
{};
const
handler
=
{
/** Intercepts: getting properties */
get
(
target
,
propKey
,
receiver
)
{
console
.
log
(
`GET
${
propKey
}
`
);
return
123
;
},
/** Intercepts: checking whether properties exist */
has
(
target
,
propKey
)
{
console
.
log
(
`HAS
${
propKey
}
`
);
return
true
;
}
};
const
proxy
=
new
Proxy
(
target
,
handler
);
當我們取得屬性 foo
時,處理函式攔截該作業
> proxy.foo
GET foo
123
類似地,in
運算子會觸發 has
> 'hello' in proxy
HAS hello
true
處理函式沒有實作陷阱 set
(設定屬性)。因此,設定 proxy.bar
會轉發到 target
,並導致設定 target.bar
。
> proxy.bar = 'abc';
> target.bar
'abc'
如果目標是函式,則可以攔截兩個額外的作業
apply
:進行函式呼叫,透過以下方式觸發
proxy(···)
proxy.call(···)
proxy.apply(···)
construct
:進行建構函式呼叫,透過以下方式觸發
new proxy(···)
僅對函式目標啟用這些陷阱的原因很簡單:否則,您將無法轉送運算 apply
和 construct
。
如果您想透過代理攔截方法呼叫,有一個挑戰:您可以攔截運算 get
(取得屬性值),也可以攔截運算 apply
(呼叫函式),但沒有單一運算可以攔截方法呼叫。這是因為方法呼叫被視為兩個獨立的運算:首先是 get
來擷取函式,然後是 apply
來呼叫該函式。
因此,您必須攔截 get
並傳回攔截函式呼叫的函式。下列程式碼示範如何執行此操作。
function
traceMethodCalls
(
obj
)
{
const
handler
=
{
get
(
target
,
propKey
,
receiver
)
{
const
origMethod
=
target
[
propKey
];
return
function
(...
args
)
{
const
result
=
origMethod
.
apply
(
this
,
args
);
console
.
log
(
propKey
+
JSON
.
stringify
(
args
)
+
' -> '
+
JSON
.
stringify
(
result
));
return
result
;
};
}
};
return
new
Proxy
(
obj
,
handler
);
}
我沒有對後續任務使用 Proxy,我只是使用函式包裝原始方法。
讓我們使用下列物件來試用 traceMethodCalls()
const
obj
=
{
multiply
(
x
,
y
)
{
return
x
*
y
;
},
squared
(
x
)
{
return
this
.
multiply
(
x
,
x
);
},
};
tracedObj
是 obj
的追蹤版本。每個方法呼叫後的第一行是 console.log()
的輸出,第二行是方法呼叫的結果。
> const tracedObj = traceMethodCalls(obj);
> tracedObj.multiply(2,7)
multiply[2,7] -> 14
14
> tracedObj.squared(9)
multiply[9,9] -> 81
squared[9] -> 81
81
很棒的是,即使在 obj.squared()
內進行的呼叫 this.multiply()
也會被追蹤。這是因為 this
持續參照代理。
這不是最有效率的解決方案。例如,可以快取方法。此外,Proxy 本身會影響效能。
ECMAScript 6 讓您可以建立可以撤銷(關閉)的代理
const
{
proxy
,
revoke
}
=
Proxy
.
revocable
(
target
,
handler
);
在指定運算子 (=
) 的左側,我們使用解構存取 Proxy.revocable()
傳回的物件的屬性 proxy
和 revoke
。
您第一次呼叫函式 revoke
後,您對 proxy
執行的任何運算都會導致 TypeError
。後續呼叫 revoke
沒有進一步的效果。
const
target
=
{};
// Start with an empty object
const
handler
=
{};
// Don’t intercept anything
const
{
proxy
,
revoke
}
=
Proxy
.
revocable
(
target
,
handler
);
proxy
.
foo
=
123
;
console
.
log
(
proxy
.
foo
);
// 123
revoke
();
console
.
log
(
proxy
.
foo
);
// TypeError: Revoked
代理程式 proto
可以成為物件 obj
的原型。在 obj
中開始的一些操作可能會在 proto
中繼續進行。其中一項操作是 get
。
const
proto
=
new
Proxy
({},
{
get
(
target
,
propertyKey
,
receiver
)
{
console
.
log
(
'GET '
+
propertyKey
);
return
target
[
propertyKey
];
}
});
const
obj
=
Object
.
create
(
proto
);
obj
.
bla
;
// Output:
// GET bla
屬性 bla
在 obj
中找不到,這就是為什麼搜尋會繼續在 proto
中進行,而且陷阱 get
會在那裡觸發。還有更多會影響原型的操作;它們列在本章節的最後面。
處理常式未執行的陷阱的操作會自動轉發到目標。有時您會想要執行一些任務,而且還要轉發操作。例如,攔截所有操作並記錄它們的處理常式,但不會阻止它們到達目標
const
handler
=
{
deleteProperty
(
target
,
propKey
)
{
console
.
log
(
'DELETE '
+
propKey
);
return
delete
target
[
propKey
];
},
has
(
target
,
propKey
)
{
console
.
log
(
'HAS '
+
propKey
);
return
propKey
in
target
;
},
// Other traps: similar
}
對於每個陷阱,我們會先記錄操作的名稱,然後手動執行它來轉發它。ECMAScript 6 有類似模組的物件 Reflect
,它有助於轉發:對於每個陷阱
handler
.
trap
(
target
,
arg_1
,
···
,
arg_n
)
Reflect
有方法
Reflect
.
trap
(
target
,
arg_1
,
···
,
arg_n
)
如果我們使用 Reflect
,前一個範例如下所示。
const
handler
=
{
deleteProperty
(
target
,
propKey
)
{
console
.
log
(
'DELETE '
+
propKey
);
return
Reflect
.
deleteProperty
(
target
,
propKey
);
},
has
(
target
,
propKey
)
{
console
.
log
(
'HAS '
+
propKey
);
return
Reflect
.
has
(
target
,
propKey
);
},
// Other traps: similar
}
現在每個陷阱所做的都非常相似,我們可以透過代理程式來實作處理常式
const
handler
=
new
Proxy
({},
{
get
(
target
,
trapName
,
receiver
)
{
// Return the handler method named trapName
return
function
(...
args
)
{
// Don’t log args[0]
console
.
log
(
trapName
.
toUpperCase
()
+
' '
+
args
.
slice
(
1
));
// Forward the operation
return
Reflect
[
trapName
](...
args
);
}
}
});
對於每個陷阱,代理程式會透過 get
操作要求處理常式方法,而我們會提供一個給它。也就是說,所有處理常式方法都可以透過單一元方法 get
來實作。讓這種虛擬化變得簡單是代理程式 API 的目標之一。
讓我們使用這個基於代理程式的處理常式
>
const
target
=
{};
>
const
proxy
=
new
Proxy
(
target
,
handler
);
>
proxy
.
foo
=
123
;
SET
foo
,
123
,[
object
Object
]
>
proxy
.
foo
GET
foo
,[
object
Object
]
123
下列互動確認 set
操作已正確轉發到目標
> target.foo
123
代理程式物件可以視為攔截對其目標物件執行的操作,代理程式包裝目標。代理程式的處理常式物件就像代理程式的觀察者或監聽者。它指定應該攔截哪些操作,方法是實作對應的方法(get
用於讀取屬性等)。如果操作的處理常式方法遺失,則不會攔截該操作。它會簡單地轉發到目標。
因此,如果處理常式是空物件,代理程式應該透明地包裝目標。唉,這並不總是有效。
this
在我們深入探討之前,讓我們快速回顧一下包裝目標如何影響 this
const
target
=
{
foo
()
{
return
{
thisIsTarget
:
this
===
target
,
thisIsProxy
:
this
===
proxy
,
};
}
};
const
handler
=
{};
const
proxy
=
new
Proxy
(
target
,
handler
);
如果你直接呼叫 target.foo()
,this
指向 target
> target.foo()
{ thisIsTarget: true, thisIsProxy: false }
如果你透過代理呼叫該方法,this
指向 proxy
> proxy.foo()
{ thisIsTarget: false, thisIsProxy: true }
這樣做是為了讓代理繼續在迴圈中,例如,如果目標呼叫 this
上的方法。
通常,具有空處理常式的代理會透明包裝目標:你不會注意到它們在那裡,而且它們不會改變目標的行為。
但是,如果目標透過代理無法控制的機制將資訊與 this
關聯起來,你就會遇到問題:事情會失敗,因為根據目標是否包裝而關聯不同的資訊。
例如,以下類別 Person
將私人資訊儲存在 WeakMap _name
中(有關此技術的更多資訊,請參閱 類別章節)
const
_name
=
new
WeakMap
();
class
Person
{
constructor
(
name
)
{
_name
.
set
(
this
,
name
);
}
get
name
()
{
return
_name
.
get
(
this
);
}
}
Person
的執行個體無法透明包裝
> const jane = new Person('Jane');
> jane.name
'Jane'
> const proxy = new Proxy(jane, {});
> proxy.name
undefined
jane.name
與包裝的 proxy.name
不同。以下實作沒有這個問題
class
Person2
{
constructor
(
name
)
{
this
.
_name
=
name
;
}
get
name
()
{
return
this
.
_name
;
}
}
const
jane
=
new
Person2
(
'Jane'
);
console
.
log
(
jane
.
name
);
// Jane
const
proxy
=
new
Proxy
(
jane
,
{});
console
.
log
(
proxy
.
name
);
// Jane
大多數內建建構函式的執行個體也有代理無法攔截的機制。因此,它們也無法透明包裝。我將展示 Date
執行個體的問題
const
target
=
new
Date
();
const
handler
=
{};
const
proxy
=
new
Proxy
(
target
,
handler
);
proxy
.
getDate
();
// TypeError: this is not a Date object.
不受代理影響的機制稱為內部插槽。這些插槽是與執行個體關聯的類屬性儲存。規格將這些插槽視為名稱以方括號括起來的屬性。例如,以下方法是內部的,可以在所有物件 O
上呼叫
O
.[[
GetPrototypeOf
]]()
但是,對內部插槽的存取並非透過正常的「取得」和「設定」操作。如果透過代理呼叫 getDate()
,它無法在 this
上找到需要的內部插槽,並透過 TypeError
抱怨。
對於 Date
方法,語言規格指出
除非另有明確說明,否則以下定義的 Number 原型物件的方法並非一般性的,傳遞給它們的
this
值必須是 Number 值或具有已初始化為 Number 值的[[NumberData]]
內部插槽的物件。
與其他內建函數不同,陣列可以被透明地包裝
> const p = new Proxy(new Array(), {});
> p.push('a');
> p.length
1
> p.length = 0;
> p.length
0
陣列可以被包裝的原因是,儘管屬性存取已自訂為讓 length
運作,但陣列方法並未依賴內部插槽,它們是通用的。
作為解決方法,你可以變更處理常式轉發方法呼叫的方式,並選擇性地將 this
設定為目標,而不是代理
const
handler
=
{
get
(
target
,
propKey
,
receiver
)
{
if
(
propKey
===
'getDate'
)
{
return
target
.
getDate
.
bind
(
target
);
}
return
Reflect
.
get
(
target
,
propKey
,
receiver
);
},
};
const
proxy
=
new
Proxy
(
new
Date
(
'2020-12-24'
),
handler
);
proxy
.
getDate
();
// 24
這種方法的缺點是,方法對 this
執行的操作都不會經過代理。
致謝:感謝 Allen Wirfs-Brock 指出本節說明的陷阱。
本節說明代理的用途。這將讓你看到 API 的實際應用。
get
、set
) 假設我們有一個函數 tracePropAccess(obj, propKeys)
,當 obj
的屬性(其鍵位於陣列 propKeys
中)被設定或取得時,它會記錄下來。在下列程式碼中,我們將該函數套用至 Point
類別的執行個體
class
Point
{
constructor
(
x
,
y
)
{
this
.
x
=
x
;
this
.
y
=
y
;
}
toString
()
{
return
`Point(
${
this
.
x
}
,
${
this
.
y
}
)`
;
}
}
// Trace accesses to properties `x` and `y`
const
p
=
new
Point
(
5
,
7
);
p
=
tracePropAccess
(
p
,
[
'x'
,
'y'
]);
取得和設定追蹤物件 p
的屬性會產生下列效應
> p.x
GET x
5
> p.x = 21
SET x=21
21
有趣的是,當 Point
存取屬性時,追蹤也會運作,因為 this
現在是指向追蹤物件,而不是 Point
的執行個體。
> p.toString()
GET x
GET y
'Point(21, 7)'
在 ECMAScript 5 中,你可以如下實作 tracePropAccess()
。我們以追蹤存取的 getter 和 setter 取代每個屬性。setter 和 getter 使用額外的物件 propData
來儲存屬性的資料。請注意,我們正在破壞性地變更原始實作,這表示我們正在進行元程式設計。
function
tracePropAccess
(
obj
,
propKeys
)
{
// Store the property data here
const
propData
=
Object
.
create
(
null
);
// Replace each property with a getter and a setter
propKeys
.
forEach
(
function
(
propKey
)
{
propData
[
propKey
]
=
obj
[
propKey
];
Object
.
defineProperty
(
obj
,
propKey
,
{
get
:
function
()
{
console
.
log
(
'GET '
+
propKey
);
return
propData
[
propKey
];
},
set
:
function
(
value
)
{
console
.
log
(
'SET '
+
propKey
+
'='
+
value
);
propData
[
propKey
]
=
value
;
},
});
});
return
obj
;
}
在 ECMAScript 6 中,我們可以使用更簡單的基於代理的解決方案。我們攔截屬性取得和設定,而且不必變更實作。
function
tracePropAccess
(
obj
,
propKeys
)
{
const
propKeySet
=
new
Set
(
propKeys
);
return
new
Proxy
(
obj
,
{
get
(
target
,
propKey
,
receiver
)
{
if
(
propKeySet
.
has
(
propKey
))
{
console
.
log
(
'GET '
+
propKey
);
}
return
Reflect
.
get
(
target
,
propKey
,
receiver
);
},
set
(
target
,
propKey
,
value
,
receiver
)
{
if
(
propKeySet
.
has
(
propKey
))
{
console
.
log
(
'SET '
+
propKey
+
'='
+
value
);
}
return
Reflect
.
set
(
target
,
propKey
,
value
,
receiver
);
},
});
}
get
、set
) 在存取屬性方面,JavaScript 非常寬容。例如,如果你嘗試讀取屬性並拼錯其名稱,你不會收到例外,你會收到結果 undefined
。你可以使用代理在這種情況下取得例外。其運作方式如下。我們讓代理成為物件的原型。
如果在物件中找不到屬性,則會觸發代理的 get
陷阱。如果在代理之後的原型鏈中甚至不存在該屬性,則它確實不存在,我們會擲回例外。否則,我們會傳回繼承屬性的值。我們透過將 get
操作轉發至目標來執行此操作(目標的原型也是代理的原型)。
const
PropertyChecker
=
new
Proxy
({},
{
get
(
target
,
propKey
,
receiver
)
{
if
(
!
(
propKey
in
target
))
{
throw
new
ReferenceError
(
'Unknown property: '
+
propKey
);
}
return
Reflect
.
get
(
target
,
propKey
,
receiver
);
}
});
讓我們對我們建立的物件使用 PropertyChecker
>
const
obj
=
{
__proto__
:
PropertyChecker
,
foo
:
123
};
>
obj
.
foo
// own
123
>
obj
.
fo
ReferenceError
:
Unknown
property
:
fo
>
obj
.
toString
()
// inherited
'
[
object
Object
]
'
如果我們將 PropertyChecker
變成建構函數,我們可以使用它透過 extends
來建立 ECMAScript 6 類別
function
PropertyChecker
()
{
}
PropertyChecker
.
prototype
=
new
Proxy
(
···
);
class
Point
extends
PropertyChecker
{
constructor
(
x
,
y
)
{
super
();
this
.
x
=
x
;
this
.
y
=
y
;
}
}
const
p
=
new
Point
(
5
,
7
);
console
.
log
(
p
.
x
);
// 5
console
.
log
(
p
.
z
);
// ReferenceError
如果您擔心意外建立屬性,您有兩個選項:您可以將代理程式封裝在攔截set
的物件周圍。或者,您可以透過 Object.preventExtensions(obj)
使物件obj
不可延伸,這表示 JavaScript 不允許您將新的(自有)屬性新增到obj
。
get
) 有些陣列方法允許您透過-1
參照最後一個元素,透過-2
參照倒數第二個元素,等等。例如
> ['a', 'b', 'c'].slice(-1)
[ 'c' ]
唉,透過方括弧運算子 ([]
) 存取元素時,這項功能無法運作。不過,我們可以使用代理程式新增該功能。下列函式createArray()
建立支援負索引的陣列。它透過將代理程式封裝在陣列實例周圍來執行此動作。代理程式攔截由方括弧運算子觸發的get
運算。
function
createArray
(...
elements
)
{
const
handler
=
{
get
(
target
,
propKey
,
receiver
)
{
// Sloppy way of checking for negative indices
const
index
=
Number
(
propKey
);
if
(
index
<
0
)
{
propKey
=
String
(
target
.
length
+
index
);
}
return
Reflect
.
get
(
target
,
propKey
,
receiver
);
}
};
// Wrap a proxy around an Array
const
target
=
[];
target
.
push
(...
elements
);
return
new
Proxy
(
target
,
handler
);
}
const
arr
=
createArray
(
'a'
,
'b'
,
'c'
);
console
.
log
(
arr
[
-
1
]);
// c
致謝:此範例的構想來自 hemanth.hm 的 部落格文章。
set
) 資料繫結是關於在物件之間同步資料。一個常見的用例是基於 MVC(模型檢視控制器)模式的小工具:透過資料繫結,如果您變更模型(小工具視覺化的資料),檢視(小工具)會保持最新狀態。
若要實作資料繫結,您必須觀察並對對物件所做的變更做出反應。在下列程式碼片段中,我概述了觀察變更如何適用於陣列。
function
createObservedArray
(
callback
)
{
const
array
=
[];
return
new
Proxy
(
array
,
{
set
(
target
,
propertyKey
,
value
,
receiver
)
{
callback
(
propertyKey
,
value
);
return
Reflect
.
set
(
target
,
propertyKey
,
value
,
receiver
);
}
});
}
const
observedArray
=
createObservedArray
(
(
key
,
value
)
=>
console
.
log
(
`
${
key
}
=
${
value
}
`
));
observedArray
.
push
(
'a'
);
輸出
0=a
length=1
代理程式可用於建立可在其上呼叫任意方法的物件。在下列範例中,函式createWebService
建立一個這樣的物件service
。在service
上呼叫方法會擷取具有相同名稱的網路服務資源的內容。擷取透過 ECMAScript 6 Promise 處理。
const
service
=
createWebService
(
'http://example.com/data'
);
// Read JSON data in http://example.com/data/employees
service
.
employees
().
then
(
json
=>
{
const
employees
=
JSON
.
parse
(
json
);
···
});
下列程式碼是 ECMAScript 5 中createWebService
的快速且簡陋的實作。由於我們沒有代理程式,因此我們需要事先知道將在service
上呼叫哪些方法。參數propKeys
提供我們該資訊,它包含一個具有方法名稱的陣列。
function
createWebService
(
baseUrl
,
propKeys
)
{
const
service
=
{};
propKeys
.
forEach
(
function
(
propKey
)
{
service
[
propKey
]
=
function
()
{
return
httpGet
(
baseUrl
+
'/'
+
propKey
);
};
});
return
service
;
}
createWebService
的 ECMAScript 6 實作可以使用代理程式,而且更為簡單
function
createWebService
(
baseUrl
)
{
return
new
Proxy
({},
{
get
(
target
,
propKey
,
receiver
)
{
// Return the method to be called
return
()
=>
httpGet
(
baseUrl
+
'/'
+
propKey
);
}
});
}
這兩個實作都使用以下函式來進行 HTTP GET 要求(其運作方式說明於 Promise 章節 中。
function
httpGet
(
url
)
{
return
new
Promise
(
(
resolve
,
reject
)
=>
{
const
request
=
new
XMLHttpRequest
();
Object
.
assign
(
request
,
{
onload
()
{
if
(
this
.
status
===
200
)
{
// Success
resolve
(
this
.
response
);
}
else
{
// Something went wrong (404 etc.)
reject
(
new
Error
(
this
.
statusText
));
}
},
onerror
()
{
reject
(
new
Error
(
'XMLHttpRequest Error: '
+
this
.
statusText
));
}
});
request
.
open
(
'GET'
,
url
);
request
.
send
();
});
}
可撤銷的參考運作方式如下:不允許客戶端直接存取重要資源(物件),只能透過參考(中介物件,資源的包裝器)存取。通常,套用至參考的每個操作都會轉送至資源。客戶端完成後,資源會透過關閉參考(撤銷)來受到保護。從此以後,對參考套用操作會擲回例外,而且不再轉送任何內容。
在以下範例中,我們為資源建立可撤銷的參考。然後,我們透過參考讀取資源的其中一個屬性。這會運作,因為參考授予我們存取權。接下來,我們撤銷參考。現在,參考不再讓我們讀取屬性。
const
resource
=
{
x
:
11
,
y
:
8
};
const
{
reference
,
revoke
}
=
createRevocableReference
(
resource
);
// Access granted
console
.
log
(
reference
.
x
);
// 11
revoke
();
// Access denied
console
.
log
(
reference
.
x
);
// TypeError: Revoked
Proxy 非常適合用來實作可撤銷的參考,因為它們可以攔截並轉送操作。以下是 createRevocableReference
的簡單 Proxy 實作
function
createRevocableReference
(
target
)
{
let
enabled
=
true
;
return
{
reference
:
new
Proxy
(
target
,
{
get
(
target
,
propKey
,
receiver
)
{
if
(
!
enabled
)
{
throw
new
TypeError
(
'Revoked'
);
}
return
Reflect
.
get
(
target
,
propKey
,
receiver
);
},
has
(
target
,
propKey
)
{
if
(
!
enabled
)
{
throw
new
TypeError
(
'Revoked'
);
}
return
Reflect
.
has
(
target
,
propKey
);
},
···
}),
revoke
()
{
enabled
=
false
;
},
};
}
可以透過前一節的 Proxy 作為處理常式的技巧來簡化程式碼。這次,處理常式基本上是 Reflect
物件。因此,get
陷阱通常會傳回適當的 Reflect
方法。如果參考已被撤銷,則會擲回 TypeError
。
function
createRevocableReference
(
target
)
{
let
enabled
=
true
;
const
handler
=
new
Proxy
({},
{
get
(
dummyTarget
,
trapName
,
receiver
)
{
if
(
!
enabled
)
{
throw
new
TypeError
(
'Revoked'
);
}
return
Reflect
[
trapName
];
}
});
return
{
reference
:
new
Proxy
(
target
,
handler
),
revoke
()
{
enabled
=
false
;
},
};
}
不過,您不必自己實作可撤銷的參考,因為 ECMAScript 6 讓您可以建立可撤銷的 Proxy。這次,撤銷發生在 Proxy 中,而非處理常式中。處理常式必須做的就是將每個操作轉送至目標。正如我們所見,如果處理常式未實作任何陷阱,就會自動發生。
function
createRevocableReference
(
target
)
{
const
handler
=
{};
// forward everything
const
{
proxy
,
revoke
}
=
Proxy
.
revocable
(
target
,
handler
);
return
{
reference
:
proxy
,
revoke
};
}
膜建立在可撤銷的參考概念上:設計用來執行不受信任程式碼的環境會將膜包覆在該程式碼周圍,以隔離程式碼並保持系統的其他部分安全。物件會以兩個方向通過膜
在這兩種情況下,可撤銷的參考都會包覆在物件周圍。由包覆函式或方法傳回的物件也會被包覆。此外,如果將包覆的濕物件傳遞回膜中,則會將其解開。
不受信任的程式碼完成後,所有可撤銷的參考都會被撤銷。因此,它在外部的程式碼都不會再被執行,而且它所擁有的外部物件也會停止運作。Caja 編譯器 是「用於讓第三方 HTML、CSS 和 JavaScript 安全嵌入至您的網站」的工具。它使用膜來達成此任務。
瀏覽器的文件物件模型 (DOM) 通常實作為 JavaScript 和 C++ 的混合。在純 JavaScript 中實作它對於下列情況很有用
唉,標準 DOM 可以執行一些在 JavaScript 中不易複製的事情。例如,大多數 DOM 集合都是對 DOM 目前狀態的即時檢視,當 DOM 發生變更時會動態變更。因此,DOM 的純 JavaScript 實作並非十分有效率。將代理新增到 JavaScript 的原因之一,就是為了協助撰寫更有效率的 DOM 實作。
代理還有更多使用案例。例如
在本節中,我們將深入探討代理如何運作以及為何它們會這樣運作。
Firefox 允許您執行一些攔截式元程式設計一段時間:如果您定義一個名稱為 __noSuchMethod__
的方法,則在呼叫不存在的方法時會通知它。以下是使用 __noSuchMethod__
的範例。
const
obj
=
{
__noSuchMethod__
:
function
(
name
,
args
)
{
console
.
log
(
name
+
': '
+
args
);
}
};
// Neither of the following two methods exist,
// but we can make it look like they do
obj
.
foo
(
1
);
// Output: foo: 1
obj
.
bar
(
1
,
2
);
// Output: bar: 1,2
因此,__noSuchMethod__
的運作方式類似於代理陷阱。與代理不同的是,陷阱是我們要攔截其運作的物件的自有或繼承方法。這種方法的問題在於基本層級(一般方法)和元層級(__noSuchMethod__
)會混在一起。基本層級程式碼可能會意外呼叫或看到元層級方法,而且有可能會意外定義元層級方法。
即使在標準 ECMAScript 5 中,基本層級和元層級有時也會混在一起。例如,下列元程式設計機制可能會失敗,因為它們存在於基本層級
obj.hasOwnProperty(propKey)
:如果原型鏈中的某個屬性覆寫內建實作,則此呼叫可能會失敗。例如,如果 obj
為
{
hasOwnProperty
:
null
}
呼叫此方法的安全方式為
Object
.
prototype
.
hasOwnProperty
.
call
(
obj
,
propKey
)
// Abbreviated version:
{}.
hasOwnProperty
.
call
(
obj
,
propKey
)
func.call(···)
、func.apply(···)
:這兩個方法的問題和解決方案都與 hasOwnProperty
相同。obj.__proto__
:在大多數 JavaScript 引擎中,__proto__
是讓您取得和設定 obj
原型的特殊屬性。因此,當您將物件用作字典時,您必須小心 避免將 __proto__
用作屬性金鑰。現在,顯然可以看出將(基本層級)屬性金鑰設為特殊屬性會造成問題。因此,代理會分層 – 基本層級(代理物件)和元層級(處理常式物件)是分開的。
代理用於兩種角色
代理 API 的早期設計將代理視為純粹的虛擬物件。然而,結果證明,即使在該角色中,目標也很有用,用於強制不變式(稍後說明)和作為處理常式未實作的陷阱的後備。
代理以兩種方式受到保護
這兩個原則賦予代理相當大的能力,可以冒充其他物件。強制不變式(稍後說明)的一個原因是為了控制這種能力。
如果您確實需要一種方法來區分代理和非代理,則必須自行實作。下列程式碼是一個模組 lib.js
,它會匯出兩個函式:其中一個會建立代理,另一個會判斷物件是否為其中一個代理。
// lib.js
const
proxies
=
new
WeakSet
();
export
function
createProxy
(
obj
)
{
const
handler
=
{};
const
proxy
=
new
Proxy
(
obj
,
handler
);
proxies
.
add
(
proxy
);
return
proxy
;
}
export
function
isProxy
(
obj
)
{
return
proxies
.
has
(
obj
);
}
這個模組使用 ECMAScript 6 資料結構 WeakSet
來追蹤代理。WeakSet
非常適合這個目的,因為它不會阻止其元素被垃圾回收。
下一個範例顯示如何使用 lib.js
。
// main.js
import
{
createProxy
,
isProxy
}
from
'./lib.js'
;
const
p
=
createProxy
({});
console
.
log
(
isProxy
(
p
));
// true
console
.
log
(
isProxy
({}));
// false
本節探討 JavaScript 的內部結構以及代理陷阱的選擇方式。
在程式語言和 API 設計的脈絡中,協定是一組介面加上使用它們的規則。ECMAScript 規格說明如何執行 JavaScript 程式碼。它包含一個處理物件的協定。此協定在元層級運作,有時稱為元物件協定 (MOP)。JavaScript MOP 包含所有物件具有的內部方法。“內部”表示它們只存在於規格中(JavaScript 引擎可能會有或沒有它們),而且無法從 JavaScript 存取。內部方法的名稱以雙中括號撰寫。
取得屬性的內部方法稱為[[Get]]
。如果我們假裝方括號中的屬性名稱是合法的,此方法將大致在 JavaScript 中實作如下。
// Method definition
[[
Get
]](
propKey
,
receiver
)
{
const
desc
=
this
.[[
GetOwnProperty
]](
propKey
);
if
(
desc
===
undefined
)
{
const
parent
=
this
.[[
GetPrototypeOf
]]();
if
(
parent
===
null
)
return
undefined
;
return
parent
.[[
Get
]](
propKey
,
receiver
);
// (A)
}
if
(
'value'
in
desc
)
{
return
desc
.
value
;
}
const
getter
=
desc
.
get
;
if
(
getter
===
undefined
)
return
undefined
;
return
getter
.[[
Call
]](
receiver
,
[]);
}
此程式碼中呼叫的 MOP 方法為
[[GetOwnProperty]]
(陷阱 getOwnPropertyDescriptor
)[[GetPrototypeOf]]
(陷阱 getPrototypeOf
)[[Get]]
(陷阱 get
)[[Call]]
(陷阱 apply
)在 A 行中,您可以看到為什麼原型鏈中的代理會在「較早」的物件中找不到屬性時找出 get
:如果沒有金鑰為 propKey
的自有屬性,搜尋將在 this
的原型 parent
中繼續。
基本運算與衍生運算。您可以看到 [[Get]]
會呼叫其他 MOP 運算。執行此動作的運算稱為衍生。不依賴其他運算的運算稱為基本。
代理的元物件協定不同於一般物件。對於一般物件,衍生運算會呼叫其他運算。對於代理,每個運算(不論是基本或衍生)都會由處理常式攔截或轉送至目標。
哪些操作應透過代理攔截?一種可能性是僅提供基本操作的陷阱。另一種選擇是包含一些衍生操作。這樣做的優點是它可以提升效能且更方便。例如,如果沒有針對 get
的陷阱,您必須透過 getOwnPropertyDescriptor
來實作其功能。衍生陷阱的一個問題是它們可能導致代理行為不一致。例如,get
可能會傳回一個與 getOwnPropertyDescriptor
傳回的描述符中值不同的值。
透過代理的攔截是選擇性的:您無法攔截每個語言操作。為什麼有些操作會被排除?讓我們來看兩個原因。
首先,穩定的操作不適合攔截。如果一個操作總是針對相同的引數產生相同的結果,則該操作就是穩定的。如果一個代理可以攔截一個穩定的操作,它可能會變得不穩定,因此不可靠。嚴格相等 (===
) 就是這樣一個穩定的操作。它無法被攔截,而且其結果是透過將代理本身視為另一個物件來計算的。維持穩定的另一種方式是對目標套用一個操作,而不是對代理套用。如後續說明,當我們探討如何對代理強制不變式時,這會在將 Object.getPrototypeOf()
套用到目標不可擴充的代理時發生。
不讓更多操作可攔截的第二個原因是,攔截表示在通常不可能的情況下執行自訂程式碼。這種程式碼交錯發生的次數越多,就越難理解和除錯程式。它也會對效能造成負面影響。
get
與 invoke
如果您想透過 ECMAScript 6 代理建立虛擬方法,您必須從 get
陷阱傳回函式。這引發了一個問題:為什麼不針對方法呼叫(例如 invoke
)引入一個額外的陷阱?這將使我們能夠區分
obj.prop
取得屬性(陷阱 get
)obj.prop()
呼叫方法(陷阱 invoke
)有兩個原因不這麼做。
首先,並非所有實作都能區分 get
和 invoke
。例如,Apple 的 JavaScriptCore 無法區分。
其次,透過 call()
或 apply()
擷取方法並稍後呼叫它,應與透過 dispatch 呼叫方法具有相同效果。換句話說,下列兩個變體應以相同方式運作。如果有一個額外的陷阱 invoke
,則較難維持這種等效性。
// Variant 1: call via dynamic dispatch
const
result
=
obj
.
m
();
// Variant 2: extract and call directly
const
m
=
obj
.
m
;
const
result
=
m
.
call
(
obj
);
invoke
的使用案例 有些事情只有在您能夠區分 get
和 invoke
時才能執行。因此,這些事情在目前的代理程式 API 中是不可能的。兩個範例是:自動繫結和攔截遺失的方法。讓我們探討如果代理程式支援 invoke
,將如何實作這些範例。
自動繫結。透過將代理程式設為物件 obj
的原型,您可以自動繫結方法
obj.m
擷取方法 m
的值,會傳回一個其 this
繫結至 obj
的函式。obj.m()
執行方法呼叫。自動繫結有助於將方法用作回呼。例如,前一個範例中的變體 2 會變得更簡單
const
boundMethod
=
obj
.
m
;
const
result
=
boundMethod
();
攔截遺失的方法。 invoke
讓代理程式能夠模擬 Firefox 支援的先前提到的 __noSuchMethod__
機制。代理程式將再次成為物件 obj
的原型。它會根據如何存取未知的屬性 foo
而產生不同的反應
obj.foo
讀取該屬性,則不會發生任何攔截,並傳回 undefined
。obj.foo()
,則代理程式會攔截,並例如通知回呼。在我們探討什麼是限制以及如何對代理程式強制執行這些限制之前,讓我們回顧一下如何透過不可擴充性和不可設定性來保護物件。
有兩種保護物件的方法
不可擴充性。如果一個物件不可擴充,則您無法新增屬性,也無法變更其原型
'use strict'
;
// switch on strict mode to get TypeErrors
const
obj
=
Object
.
preventExtensions
({});
console
.
log
(
Object
.
isExtensible
(
obj
));
// false
obj
.
foo
=
123
;
// TypeError: object is not extensible
Object
.
setPrototypeOf
(
obj
,
null
);
// TypeError: object is not extensible
不可設定性。屬性的所有資料都儲存在屬性中。屬性就像記錄,而屬性就像該記錄的欄位。屬性的範例
value
保留屬性的值。writable
控制是否可以變更屬性的值。configurable
控制是否可以變更屬性的屬性。因此,如果一個屬性同時不可寫入且不可設定,則它為唯讀且會保持這種狀態
'use strict'
;
// switch on strict mode to get TypeErrors
const
obj
=
{};
Object
.
defineProperty
(
obj
,
'foo'
,
{
value
:
123
,
writable
:
false
,
configurable
:
false
});
console
.
log
(
obj
.
foo
);
// 123
obj
.
foo
=
'a'
;
// TypeError: Cannot assign to read only property
Object
.
defineProperty
(
obj
,
'foo'
,
{
configurable
:
true
});
// TypeError: Cannot redefine property
有關這些主題的更多詳細資訊(包括 Object.defineProperty()
的運作方式),請參閱「Speaking JavaScript」中的下列章節
傳統上,不可擴充性和不可組態性是
這些和在語言操作中保持不變的其他特性稱為不變式。使用代理時,很容易違反不變式,因為它們本質上不受不可擴充性等約束。
代理 API 透過檢查處理常式方法的參數和結果,防止代理違反不變式。以下是四個不變式的範例(對於任意物件 obj
),以及它們如何對代理強制執行的說明(本章的結尾提供了詳盡的清單)。
前兩個不變式涉及不可擴充性和不可組態性。這些透過使用目標物件進行簿記來強制執行:處理常式方法傳回的結果必須大部分與目標物件同步。
Object.preventExtensions(obj)
傳回 true
,則所有後續呼叫都必須傳回 false
,而且 obj
現在必須不可擴充。
true
,但目標物件不可擴充,則透過擲出 TypeError
來強制執行代理。Object.isExtensible(obj)
必須總是傳回 false
。
Object.isExtensible(target)
不同,則透過擲出 TypeError
來強制執行代理。其餘兩個不變式透過檢查回傳值來強制執行
Object.isExtensible(obj)
必須傳回布林值。
Object.getOwnPropertyDescriptor(obj, ···)
必須傳回物件或 undefined
。
TypeError
來強制執行代理。強制執行不變式具有以下好處
接下來的兩個區段提供了強制執行不變式的範例。
回應 getPrototypeOf
陷阱時,如果目標不可擴充,代理程式必須傳回目標的原型。
為了示範此不變式,我們來建立一個處理常式,傳回與目標原型不同的原型
const
fakeProto
=
{};
const
handler
=
{
getPrototypeOf
(
t
)
{
return
fakeProto
;
}
};
如果目標可擴充,偽造原型會成功
const
extensibleTarget
=
{};
const
ext
=
new
Proxy
(
extensibleTarget
,
handler
);
console
.
log
(
Object
.
getPrototypeOf
(
ext
)
===
fakeProto
);
// true
不過,如果我們偽造不可擴充物件的原型,就會發生錯誤。
const
nonExtensibleTarget
=
{};
Object
.
preventExtensions
(
nonExtensibleTarget
);
const
nonExt
=
new
Proxy
(
nonExtensibleTarget
,
handler
);
Object
.
getPrototypeOf
(
nonExt
);
// TypeError
如果目標有不可寫入的不可設定屬性,則處理常式必須在回應 get
陷阱時傳回該屬性的值。為了示範此不變式,我們來建立一個處理常式,總是傳回屬性的相同值。
const
handler
=
{
get
(
target
,
propKey
)
{
return
'abc'
;
}
};
const
target
=
Object
.
defineProperties
(
{},
{
foo
:
{
value
:
123
,
writable
:
true
,
configurable
:
true
},
bar
:
{
value
:
456
,
writable
:
false
,
configurable
:
false
},
});
const
proxy
=
new
Proxy
(
target
,
handler
);
屬性 target.foo
不是不可寫入且不可設定,這表示處理常式可以假裝它有不同的值
> proxy.foo
'abc'
不過,屬性 target.bar
是不可寫入且不可設定。因此,我們無法偽造它的值
> proxy.bar
TypeError: Invariant check failed
enumerate
陷阱在哪裡? ES6 原本有一個陷阱 enumerate
,會由 for-in
迴圈觸發。但它最近被移除,以簡化代理程式。Reflect.enumerate()
也被移除。(來源:TC39 備忘錄)
本節提供代理程式 API 的快速參考:全域物件 Proxy
和 Reflect
。
有兩種方式可以建立代理程式
const proxy = new Proxy(target, handler)
const {proxy, revoke} = Proxy.revocable(target, handler)
revoke
撤銷的代理。revoke
可呼叫多次,但只有第一次呼叫會生效並關閉 proxy
。之後,對 proxy
執行的任何操作都會導致擲出 TypeError
。本小節說明處理器可以實作哪些陷阱以及哪些操作會觸發這些陷阱。幾個陷阱會傳回布林值。對於陷阱 has
和 isExtensible
,布林值是操作的結果。對於所有其他陷阱,布林值表示操作是否成功。
所有物件的陷阱
defineProperty(target, propKey, propDesc) : 布林值
Object.defineProperty(proxy, propKey, propDesc)
deleteProperty(target, propKey) : 布林值
delete proxy[propKey]
delete proxy.foo // propKey = 'foo'
get(target, propKey, receiver) : 任意
receiver[propKey]
receiver.foo // propKey = 'foo'
getOwnPropertyDescriptor(target, propKey) : PropDesc|Undefined
Object.getOwnPropertyDescriptor(proxy, propKey)
getPrototypeOf(target) : 物件|Null
Object.getPrototypeOf(proxy)
has(target, propKey) : 布林值
propKey in proxy
isExtensible(target) : 布林值
Object.isExtensible(proxy)
ownKeys(target) : Array<PropertyKey>
Object.getOwnPropertyPropertyNames(proxy)
(僅使用字串金鑰)Object.getOwnPropertyPropertySymbols(proxy)
(僅使用符號金鑰)Object.keys(proxy)
(僅使用可列舉的字串金鑰;可列舉性透過 Object.getOwnPropertyDescriptor
檢查)preventExtensions(target) : 布林值
Object.preventExtensions(proxy)
set(target, propKey, value, receiver) : 布林值
receiver[propKey] = value
receiver.foo = value // propKey = 'foo'
setPrototypeOf(target, proto) : 布林值
Object.setPrototypeOf(proxy, proto)
函數的陷阱(如果 target 是函數,則可用)
apply(target, thisArgument, argumentsList) : 任意
proxy.apply(thisArgument, argumentsList)
proxy.call(thisArgument, ...argumentsList)
proxy(...argumentsList)
construct(target, argumentsList, newTarget) : 物件
new proxy(..argumentsList)
下列操作是基本操作,它們不會使用其他操作來執行工作:apply
、defineProperty
、deleteProperty
、getOwnPropertyDescriptor
、getPrototypeOf
、isExtensible
、ownKeys
、preventExtensions
、setPrototypeOf
所有其他操作都是衍生的,它們可透過基本操作來實作。例如,對於資料屬性,get
可透過 getPrototypeOf
迭代原型鏈,並對每個鏈成員呼叫 getOwnPropertyDescriptor
,直到找到自己的屬性或鏈結束。
常數是處理常式的安全約束。此小節說明代理 API 如何強制執行常數以及如何執行。每當您在下方讀到「處理常式必須執行 X」時,表示如果它沒有執行,就會擲回 TypeError
。有些常數會限制回傳值,有些則會限制參數。陷阱回傳值的正確性有兩種確保方式:通常,非法值表示會擲回 TypeError
。但每當預期為布林值時,就會使用強制轉換將非布林值轉換為合法值。
以下是強制執行的常數完整清單
apply(target, thisArgument, argumentsList)
construct(target, argumentsList, newTarget)
null
或基本值)。defineProperty(target, propKey, propDesc)
propKey
必須是目標的自有金鑰之一。propDesc
將屬性 configurable
設定為 false
,則目標必須有金鑰為 propKey
的不可設定自有屬性。propDesc
用於(重新)定義目標的自有屬性,則不能導致例外狀況。如果變更被屬性 writable
和 configurable
禁止,就會擲回例外狀況(不可擴充性由第一個規則處理)。deleteProperty(target, propKey)
get(target, propKey, receiver)
propKey
的自有、不可寫入、不可設定資料屬性,則處理常式必須回傳該屬性的值。propKey
的自有、不可設定、無 getter 的存取器屬性,則處理常式必須回傳 undefined
。getOwnPropertyDescriptor(target, propKey)
undefined
。writable
和 configurable
屬性允許,則會擲回例外狀況(不可擴充性由第三條規則處理)。因此,處理常式無法將不可組態屬性報告為可組態,也無法為不可組態、不可寫入屬性報告不同的值。getPrototypeOf(target)
null
。has(target, propKey)
isExtensible(target)
target.isExtensible()
相同。ownKeys(target)
preventExtensions(target)
target.isExtensible()
之後必須為 false
。set(target, propKey, value, receiver)
propKey
,則 value
必須與該屬性的值相同(亦即,屬性無法變更)。TypeError
(亦即,無法設定此類屬性)。setPrototypeOf(target, proto)
proto
必須與目標的原型相同。否則,會擲回 TypeError
。一般物件的下列運算會對原型鏈中的物件執行運算。因此,如果該鏈中的某個物件是代理,則會觸發其陷阱。規範將運算實作為內部自有方法(JavaScript 程式碼看不到)。但在本區段中,我們假裝它們是與陷阱同名的一般方法。參數 target
會變成方法呼叫的接收器。
target.get(propertyKey, receiver)
target
沒有具有給定金鑰的自有屬性,則會在 target
的原型上呼叫 get
。target.has(propertyKey)
get
類似,如果 target
沒有具有給定金鑰的自有屬性,則會在 target
的原型上呼叫 has
。target.set(propertyKey, value, receiver)
get
類似,如果 target
沒有具有給定金鑰的自有屬性,則會在 target
的原型上呼叫 set
。所有其他運算只會影響自有屬性,它們不會對原型鏈產生影響。
全域物件 Reflect
將 JavaScript 元物件通訊協定的所有可攔截運算實作為方法。這些方法的名稱與處理常式方法相同,如我們所見,這有助於將運算從處理常式轉送至目標。
Reflect.apply(target, thisArgument, argumentsList) : any
Function.prototype.apply()
相同。Reflect.construct(target, argumentsList, newTarget=target) : Object
new
運算子。target
是要呼叫的建構函式,選擇性參數 newTarget
指向啟動目前建構函式呼叫鏈的建構函式。有關 ES6 中建構函式呼叫如何串連的詳細資訊,請參閱類別章節。Reflect.defineProperty(target, propertyKey, propDesc) : boolean
Object.defineProperty()
。Reflect.deleteProperty(target, propertyKey) : boolean
delete
算子作為一個函式。它運作方式略有不同:如果它成功刪除屬性或屬性從未存在,它會傳回 true
。如果屬性無法刪除且仍然存在,它會傳回 false
。保護屬性免於被刪除的唯一方法是讓它們不可設定。在不嚴謹模式中,delete
算子會傳回相同的結果。但在嚴謹模式中,它會擲回 TypeError
而不是傳回 false
。Reflect.get(target, propertyKey, receiver=target) : any
get
在原型鏈中稍後到達一個 getter 時,需要選用的參數 receiver
。然後它會提供 this
的值。Reflect.getOwnPropertyDescriptor(target, propertyKey) : PropDesc|Undefined
Object.getOwnPropertyDescriptor()
相同。Reflect.getPrototypeOf(target) : Object|Null
Object.getPrototypeOf()
相同。Reflect.has(target, propertyKey) : boolean
in
算子作為一個函式。Reflect.isExtensible(target) : boolean
Object.isExtensible()
相同。Reflect.ownKeys(target) : Array<PropertyKey>
Reflect.preventExtensions(target) : boolean
Object.preventExtensions()
。Reflect.set(target, propertyKey, value, receiver=target) : boolean
Reflect.setPrototypeOf(target, proto) : boolean
__proto__
。數個方法有布林結果。對於 has
和 isExtensible
,它們是運算的結果。對於其餘的方法,它們表示運算是否成功。
Reflect
的使用案例 除了轉發運算之外,為什麼 Reflect
會有用 [4]?
Reflect
重複 Object
的下列方法,但它的方法會傳回布林值表示運算是否成功(其中 Object
方法會傳回已修改的物件)。
Object.defineProperty(obj, propKey, propDesc) : Object
Object.preventExtensions(obj) : Object
Object.setPrototypeOf(obj, proto) : Object
Reflect
方法實作了其他只能透過算子取得的功能
Reflect.construct(target, argumentsList, newTarget=target) : Object
Reflect.deleteProperty(target, propertyKey) : boolean
Reflect.get(target, propertyKey, receiver=target) : any
Reflect.has(target, propertyKey) : boolean
Reflect.set(target, propertyKey, value, receiver=target) : boolean
apply()
的較短版本:如果你想要完全安全地呼叫函式的 apply()
方法,你不能透過動態調用來這麼做,因為函式可能有一個具有 'apply'
鍵的自訂屬性
func
.
apply
(
thisArg
,
argArray
)
// not safe
Function
.
prototype
.
apply
.
call
(
func
,
thisArg
,
argArray
)
// safe
使用 Reflect.apply()
較短
Reflect
.
apply
(
func
,
thisArg
,
argArray
)
delete
算子會在嚴格模式中引發例外。Reflect.deleteProperty()
在這種情況下會傳回 false
。Object.*
與 Reflect.*
未來,Object
將會承載一般應用程式感興趣的操作,而 Reflect
將會承載較低層級的操作。
這結束了我們對代理 API 的深入探討。對於每個應用程式,你必須考量效能,並在必要時進行衡量。代理可能並不總是夠快。另一方面,效能通常不是關鍵,而代理所提供的元程式設計能力是很棒的。正如我們所見,它們可以協助處理許多使用案例。
[1] “ECMAScript Reflection API 的設計” by Tom Van Cutsem 和 Mark Miller。技術報告,2012。[本章的重要來源。]
[2] “元物件協定的藝術” by Gregor Kiczales、Jim des Rivieres 和 Daniel G. Bobrow。書籍,1991。
[3] “發揮元類別的功用:物件導向程式設計的新面向” by Ira R. Forman 和 Scott H. Danforth。書籍,1999。
[4] “Harmony-reflect:我為什麼應該使用這個函式庫?” by Tom Van Cutsem。[說明為什麼 Reflect
很實用。]