Reflect
Reflect 是ES6中新增的特性。它是一個普通對象,下面有13個靜態方法(enumerate
在最終的發佈版中被移除),可以再全局下訪問。它不能當做函數調用,也不可以用new
操作符生成一個實例。
首先來說,Reflect的提出是爲了整合之前JS中存在的一些不太合理的地方。
1)更加有用的返回值
Object.getOwnPropertyNames(Reflect)
// ["defineProperty", "deleteProperty", "apply", "construct", "get", "getOwnPropertyDescriptor", "getPrototypeOf", "has", "isExtensible", "ownKeys", "preventExtensions", "set", "setPrototypeOf"]
Reflect.apply
功能跟Function.prototype.apply
類似。
var a = [1,2,3];
Math.max.apply(null, a);// 3
Reflect.apply(Math.max, null, a); // 3
Reflect.isExtensible / Reflect.preventExtensions
Reflect.preventExtensions用於讓一個對象變爲不可擴展。它返回一個布爾值,表示是否操作成功。
Reflect.isExtensible表示當前對象是否可擴展, 返回一個布爾值。
var o = {};
Reflect.isExtensible(o);// true
Object.isExtensible(o);// true
Object.freeze(o);
Reflect.isExtensible(o);// false
Object.isExtensible(o);// false
// 這兩方法的區別
Object.isExtensible(1) // false
Reflect.isExtensible(1) // 報錯
var newO = {};
Reflect.isExtensible(newO);// true
Reflect.preventExtensions(newO); // true 返回true,表示該操作成功
Reflect.isExtensible(newO);// false
Reflect.set / Reflect.get
設置和獲取對象屬性, 這兩個方法還允許接受一個reciever,用於重定義setter和getter方法的上下文。
var o = {};
Reflect.set(o, 'key', 'value');
console.log(o); // {key: "value"}
Reflect.get(o, 'key'); // "value"
Reflect.get(o, 'nokey'); // undefined
下面演示一下receiver的使用方法
var o = {
name: 'O'
}
Object.defineProperty(o, 'sayHi', {
get(){
return 'hi, I am ' + this.name
}
})
o.sayHi; // "hi, I am O"
var receiver = {
name: 'receiver'
}
// 下面是關鍵, 看好咯~
Reflect.get(o, 'sayHi', receiver); // "hi, I am receiver"
// 下面試驗了下Proxy的用法,可以忽略
var p = new Proxy(o, {
get(o, k, p){
return o[k] ? Reflect.get(o, k, receiver) : undefined;
},
set(o, k, v, p){
v += +new Date();
Reflect.set(o, k, v, p)
}
});
p.sayHi; // "hi, I am receiver"
p.t = 'time:';
p.t; // "time:1530865528713"
Reflect.ownKeys
Reflect.ownKeys方法用於返回對象的所有屬性數組。
這個數組的排序是根據: 先顯示數字, 數字根據大小排序,然後是 字符串根據插入的順序排序, 最後是symbol類型的key也根據插入插入順序排序。
出現這種排序是因爲,你給一個對象屬性賦值時候, 對象的key的排序規則就是先數字, 在字符串, 最後是symbol類型的數據。
Reflect.ownKeys(JSON); // ["parse", "stringify", Symbol(Symbol.toStringTag)]
Object.getOwnPropertyNames(JSON); // ["parse", "stringify"]
Object.getOwnPropertySymbols(JSON); // [Symbol(Symbol.toStringTag)]
Reflect.has
用於判斷對象是否具有某個屬性
Reflect.has(JSON, 'parse'); // true
Reflect.has(JSON, 'nokeyxx'); // false
Reflect.has(1, 'parse');// Uncaught TypeError: Reflect.has called on non-object
Reflect.construct
功能類似於new
操作符
var a = new Array(3);
console.log(a); // [empty × 3]
var b = Reflect.construct(Array, [3])
console.log(b); // [empty × 3]
Reflect.defineProperty / Reflect.deleteProperty
Reflect.defineProperty 對應於Object.DefineProperty;
Reflect.deleteProperfy 對應於 delete
語句;
var o = {};
Object.defineProperty(o, 'sayHi', {
configurable: true,
get(k){
return this[k] ? "Hi, I am " + this[k] : 'I have no name';
}
});
o.sayHi;// "I have no name"
Reflect.defineProperty(o, 'sayHello', {
configurable: true,
get(k){
return this[k] ? "Hi, I am " + this[k] : 'I have no name';
}
})
o.sayHello; // "I have no name"
// 演示屬性刪除方法
delete o.sayHi; // true
Reflect.deleteProperty(o, 'sayHello'); // true
Reflect.setPrototypeOf / Reflect.getPrototypeOf
這兩個方法用於設置和訪問對象的原型__proto__
function A(){};
A.prototype.name= "A";
Reflect.getPrototypeOf(A)
// ƒ () { [native code] }
var a = new A();
Reflect.getPrototypeOf(a)
// {name: "A", constructor: ƒ}
a.name; // "A"
var c = {};
Reflect.setPrototypeOf(c, A.prototype);
c.name; // "A"
A.prototype.newName = "C";
c.newName; // "C"
Reflect.getOwnPropertyDescriptor
基本等同於Object.getOwnPropertyDescriptor,用於得到指定屬性的描述對象。
Reflect.getOwnPropertyDescriptor(JSON, 'parse'); // {value: ƒ, writable: true, enumerable: false, configurable: true}
var c = {};
Reflect.defineProperty(c, 'name', {
configurable: true,
enumerable: true,
value: 'CCC',
writable: false
});
Reflect.getOwnPropertyDescriptor(c, 'name'); // {value: "CCC", writable: false, enumerable: true, configurable: true}
c.name; // "CCC"
c.name = 111;
c.name; // "CCC", 因爲writable=== false
Proxy
Proxy 對象用於定義JS對象的基本操作行爲(如屬性查找,賦值,枚舉,函數調用等)。
下面看一個修改對象get行爲的demo:
var obj = { n : 1};
var p = new Proxy(obj, {
get(t, k){
if( Reflect.has(t, k) ){
return t[k] + 100;
} else {
throw Error('no the property')
}
}
});
p.n; // 101
p.p; // Uncaught Error: no the property
關於Proxy具體可以重新定義哪些基本操作:
defineProperty
deleteProperty
apply
construct
get
getOwnPropertyDescriptor
getPrototypeOf
has
isExtensible
ownKeys
preventExtensions
set
setPrototypeOf
這個列表跟 Reflect 具有的靜態方法是一致的。更多信息可以參考Proxy可以處理的方法列表 的描述
不說上面這些值基本操作,我們列舉下一下Proxy
的一些使用場景。
設置默認值
如果訪問一個對象中還沒有初始化的值,Proxy可以通過代理get方法,返回一個默認值。
function setDefault(defaults) {
const handler = {
get(t, k) {
return Reflect.get(t, k) || defaults[k];
}
}
return new Proxy({},handler);
}
const d = setDefault({
name: 'name'
});
const log = console.log;
log(d.name); // "name"
d.name = 'new name';
log(d.name); // "new name"
log(d);
隱藏私有屬性
Proxy可以代理對象的基礎操作,我們通過代理 get 方法,控制外部對對象內部屬性的方法。這樣便可以達到隱藏某些特性(私有屬性)的目的。
const log = console.log;
function hideProp(obj,filter) {
const handler = {
get(t, k) {
if(!filter(k)){
let v = Reflect.get(t, k);
if(typeof v === 'function'){
v = v.bind(t);
}
return v;
}
}
}
return new Proxy(obj,handler);
}
function filter(key){
return key[0] === '_';
}
const o = {
_p : 'no access',
p: 'access',
f: function(){
log(this._p);
}
};
const p = hideProp(o, filter);
log(p.p); // access
log(p._p); // undefined
log(p.f()); // no access
更完美的枚舉
很多JavaScript代碼使用字符串、普通或凍結的對象作爲枚舉。這些解決方案有其類型的安全問題,而且通常容易出錯。
Proxy可以提供一個可行的替代方案。我們採用一個簡單的鍵值對象,並通過保護它免受(甚至無意的)修改而使其更加健壯。甚至比Object.freeze更加完善。(Object.freeze不允許修改,但不會引發錯誤,而是靜默報錯)
const log = console.log;
function enumF(obj) {
const handler = {
get(t, k) {
if(Reflect.has(t, k)){
let v = Reflect.get(t, k);
if(typeof v === 'function'){
v = v.bind(t);
}
return v;
}
},
set(t){
throw new TypeError('Enum is read only');
}
}
return new Proxy(obj,handler);
}
const o = {
p: 'access',
f: function(){
log(this._p);
}
};
const p = enumF(o);
log(p.p); // access
p.p = 'modified'; // TypeError
// 對比Object.freeze
const o1 = {
p: '1'
}
Object.freeze(o1);
o1.p = 'modified';
log(o1.p);
跟蹤對象變化
因爲Proxy可以截獲對象的大部分基礎操作,因此我們實際上是可以跟蹤的對 對象的訪問和修改。通過記錄這些訪問和修改信息,能記錄下對這個對象的所有操作記錄。
const log = console.log;
function trackChange(obj, onchange) {
const handler = {
deleteProperty(t, k){
const oldVal = t[k];
Reflect.deleteProperty(t, k);
onchange(t, k , undefined, oldVal)
},
set(t,k, v){
const oldVal = t[k];
Reflect.set(t, k , v);
onchange(t, k, v, oldVal);
}
}
return new Proxy(obj,handler);
}
const o = {
p: 'access',
f: function(){
log(this._p);
}
};
const p = trackChange(o, (t,k,newV, oldV)=>{
log(`the property: ${k} change from old value [${oldV}] to new value [${newV}]`)
});
p.p = 'modified';
delete p.f;
Proxy實現單例模式
來看看通過Proxy如何實現 單例模式。
const log = console.log;
function singleInstance(obj){
let instance,
handler = {
construct:function(t){
if(!instance){
instance = Reflect.construct(...arguments);
}
return instance;
}
}
return new Proxy(obj, handler);
}
function A(){
this.v = 1;
}
const p = singleInstance(A);
const p1 = new p();
const p2 = new p();
log(p1.v);
log(p2.v);
p1.v = 'new value';
log(p1.v);
log(p2.v);
總結
總結一下,上面的幾個例子中,我們基本上代理的都是對象的get
,set
,deleteProperty
方法。這些都是對象的常見操作。用好了Proxy可以解決很多問題,例如vue.js數據雙向綁定的特性實際上也可以使用Proxy實現。
說了這麼多,但是並不極力推薦使用Proxy,並不是出於兼容性問題。主要是性能方面,可以肯定的是,使用了Proxy肯定不如原有對象訪問速度更快。Proxy相當於在對象前添加了一道牆,任何操作都要先經過Proxy處理。這肯定會增加成本。寫這篇文章的主要目的是 希望能夠了解更多的東西,爲自己解決問題增加另外一種方案。
【參考資料】