es6 Reflect詳解

http://es6.ruanyifeng.com/#docs/reflect

概述

Reflect對象與Proxy對象一樣,也是 ES6 爲了操作對象而提供的新 API。Reflect對象的設計目的有這樣幾個。

(1) 將Object對象的一些明顯屬於語言內部的方法(比如Object.defineProperty),放到Reflect對象上。現階段,某些方法同時在ObjectReflect對象上部署,未來的新方法將只部署在Reflect對象上。也就是說,從Reflect對象上可以拿到語言內部的方法。

(2) 修改某些Object方法的返回結果,讓其變得更合理。比如,Object.defineProperty(obj, name, desc)在無法定義屬性時,會拋出一個錯誤,而Reflect.defineProperty(obj, name, desc)則會返回false

// 老寫法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}

// 新寫法
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}

(3) 讓Object操作都變成函數行爲。某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)讓它們變成了函數行爲。

// 老寫法
'assign' in Object // true

// 新寫法
Reflect.has(Object, 'assign') // true

(4)Reflect對象的方法與Proxy對象的方法一一對應,只要是Proxy對象的方法,就能在Reflect對象上找到對應的方法。這就讓Proxy對象可以方便地調用對應的Reflect方法,完成默認行爲,作爲修改行爲的基礎。也就是說,不管Proxy怎麼修改默認行爲,你總可以在Reflect上獲取默認行爲。

Proxy(target, {
  set: function(target, name, value, receiver) {
    var success = Reflect.set(target,name, value, receiver);
    if (success) {
      console.log('property ' + name + ' on ' + target + ' set to ' + value);
    }
    return success;
  }
});

上面代碼中,Proxy方法攔截target對象的屬性賦值行爲。它採用Reflect.set方法將值賦值給對象的屬性,確保完成原有的行爲,然後再部署額外的功能。

下面是另一個例子。

var loggedObj = new Proxy(obj, {
  get(target, name) {
    console.log('get', target, name);
    return Reflect.get(target, name);
  },
  deleteProperty(target, name) {
    console.log('delete' + name);
    return Reflect.deleteProperty(target, name);
  },
  has(target, name) {
    console.log('has' + name);
    return Reflect.has(target, name);
  }
});

上面代碼中,每一個Proxy對象的攔截操作(getdeletehas),內部都調用對應的Reflect方法,保證原生行爲能夠正常執行。添加的工作,就是將每一個操作輸出一行日誌。

有了Reflect對象以後,很多操作會更易讀。

// 老寫法
Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1

// 新寫法
Reflect.apply(Math.floor, undefined, [1.75]) // 1

靜態方法

Reflect對象一共有 13 個靜態方法。

  • Reflect.apply(target, thisArg, args)
  • Reflect.construct(target, args)
  • Reflect.get(target, name, receiver)
  • Reflect.set(target, name, value, receiver)
  • Reflect.defineProperty(target, name, desc)
  • Reflect.deleteProperty(target, name)
  • Reflect.has(target, name)
  • Reflect.ownKeys(target)
  • Reflect.isExtensible(target)
  • Reflect.preventExtensions(target)
  • Reflect.getOwnPropertyDescriptor(target, name)
  • Reflect.getPrototypeOf(target)
  • Reflect.setPrototypeOf(target, prototype)

上面這些方法的作用,大部分與Object對象的同名方法的作用都是相同的,而且它與Proxy對象的方法是一一對應的。下面是對它們的解釋。

Reflect.get(target, name, receiver)

Reflect.get方法查找並返回target對象的name屬性,如果沒有該屬性,則返回undefined

var myObject = {
  foo: 1,
  bar: 2,
  get baz() {
    return this.foo + this.bar;
  },
}

Reflect.get(myObject, 'foo') // 1
Reflect.get(myObject, 'bar') // 2
Reflect.get(myObject, 'baz') // 3

如果name屬性部署了讀取函數(getter),則讀取函數的this綁定receiver

var myObject = {
  foo: 1,
  bar: 2,
  get baz() {
    return this.foo + this.bar;
  },
};

var myReceiverObject = {
  foo: 4,
  bar: 4,
};

Reflect.get(myObject, 'baz', myReceiverObject) // 8

如果第一個參數不是對象,Reflect.get方法會報錯。

Reflect.get(1, 'foo') // 報錯
Reflect.get(false, 'foo') // 報錯

Reflect.set(target, name, value, receiver)

Reflect.set方法設置target對象的name屬性等於value

var myObject = {
  foo: 1,
  set bar(value) {
    return this.foo = value;
  },
}

myObject.foo // 1

Reflect.set(myObject, 'foo', 2);
myObject.foo // 2

Reflect.set(myObject, 'bar', 3)
myObject.foo // 3

如果name屬性設置了賦值函數,則賦值函數的this綁定receiver

var myObject = {
  foo: 4,
  set bar(value) {
    return this.foo = value;
  },
};

var myReceiverObject = {
  foo: 0,
};

Reflect.set(myObject, 'bar', 1, myReceiverObject);
myObject.foo // 4
myReceiverObject.foo // 1

注意,如果 Proxy對象和 Reflect對象聯合使用,前者攔截賦值操作,後者完成賦值的默認行爲,而且傳入了receiver,那麼Reflect.set會觸發Proxy.defineProperty攔截。

let p = {
  a: 'a'
};

let handler = {
  set(target, key, value, receiver) {
    console.log('set');
    Reflect.set(target, key, value, receiver)
  },
  defineProperty(target, key, attribute) {
    console.log('defineProperty');
    Reflect.defineProperty(target, key, attribute);
  }
};

let obj = new Proxy(p, handler);
obj.a = 'A';
// set
// defineProperty

上面代碼中,Proxy.set攔截裏面使用了Reflect.set,而且傳入了receiver,導致觸發Proxy.defineProperty攔截。這是因爲Proxy.setreceiver參數總是指向當前的 Proxy實例(即上例的obj),而Reflect.set一旦傳入receiver,就會將屬性賦值到receiver上面(即obj),導致觸發defineProperty攔截。如果Reflect.set沒有傳入receiver,那麼就不會觸發defineProperty攔截。

let p = {
  a: 'a'
};

let handler = {
  set(target, key, value, receiver) {
    console.log('set');
    Reflect.set(target, key, value)
  },
  defineProperty(target, key, attribute) {
    console.log('defineProperty');
    Reflect.defineProperty(target, key, attribute);
  }
};

let obj = new Proxy(p, handler);
obj.a = 'A';
// set

如果第一個參數不是對象,Reflect.set會報錯。

Reflect.set(1, 'foo', {}) // 報錯
Reflect.set(false, 'foo', {}) // 報錯

Reflect.has(obj, name)

Reflect.has方法對應name in obj裏面的in運算符。

var myObject = {
  foo: 1,
};

// 舊寫法
'foo' in myObject // true

// 新寫法
Reflect.has(myObject, 'foo') // true

如果Reflect.has()方法的第一個參數不是對象,會報錯。

Reflect.deleteProperty(obj, name)

Reflect.deleteProperty方法等同於delete obj[name],用於刪除對象的屬性。

const myObj = { foo: 'bar' };

// 舊寫法
delete myObj.foo;

// 新寫法
Reflect.deleteProperty(myObj, 'foo');

該方法返回一個布爾值。如果刪除成功,或者被刪除的屬性不存在,返回true;刪除失敗,被刪除的屬性依然存在,返回false

如果Reflect.deleteProperty()方法的第一個參數不是對象,會報錯。

Reflect.construct(target, args)

Reflect.construct方法等同於new target(...args),這提供了一種不使用new,來調用構造函數的方法。

function Greeting(name) {
  this.name = name;
}

// new 的寫法
const instance = new Greeting('張三');

// Reflect.construct 的寫法
const instance = Reflect.construct(Greeting, ['張三']);

如果Reflect.construct()方法的第一個參數不是函數,會報錯。

Reflect.getPrototypeOf(obj)

Reflect.getPrototypeOf方法用於讀取對象的__proto__屬性,對應Object.getPrototypeOf(obj)

const myObj = new FancyThing();

// 舊寫法
Object.getPrototypeOf(myObj) === FancyThing.prototype;

// 新寫法
Reflect.getPrototypeOf(myObj) === FancyThing.prototype;

Reflect.getPrototypeOfObject.getPrototypeOf的一個區別是,如果參數不是對象,Object.getPrototypeOf會將這個參數轉爲對象,然後再運行,而Reflect.getPrototypeOf會報錯。

Object.getPrototypeOf(1) // Number {[[PrimitiveValue]]: 0}
Reflect.getPrototypeOf(1) // 報錯

Reflect.setPrototypeOf(obj, newProto)

Reflect.setPrototypeOf方法用於設置目標對象的原型(prototype),對應Object.setPrototypeOf(obj, newProto)方法。它返回一個布爾值,表示是否設置成功。

const myObj = {};

// 舊寫法
Object.setPrototypeOf(myObj, Array.prototype);

// 新寫法
Reflect.setPrototypeOf(myObj, Array.prototype);

myObj.length // 0

如果無法設置目標對象的原型(比如,目標對象禁止擴展),Reflect.setPrototypeOf方法返回false

Reflect.setPrototypeOf({}, null)
// true
Reflect.setPrototypeOf(Object.freeze({}), null)
// false

如果第一個參數不是對象,Object.setPrototypeOf會返回第一個參數本身,而Reflect.setPrototypeOf會報錯。

Object.setPrototypeOf(1, {})
// 1

Reflect.setPrototypeOf(1, {})
// TypeError: Reflect.setPrototypeOf called on non-object

如果第一個參數是undefinednullObject.setPrototypeOfReflect.setPrototypeOf都會報錯。

Object.setPrototypeOf(null, {})
// TypeError: Object.setPrototypeOf called on null or undefined

Reflect.setPrototypeOf(null, {})
// TypeError: Reflect.setPrototypeOf called on non-object

Reflect.apply(func, thisArg, args)

Reflect.apply方法等同於Function.prototype.apply.call(func, thisArg, args),用於綁定this對象後執行給定函數。

一般來說,如果要綁定一個函數的this對象,可以這樣寫fn.apply(obj, args),但是如果函數定義了自己的apply方法,就只能寫成Function.prototype.apply.call(fn, obj, args),採用Reflect對象可以簡化這種操作。

const ages = [11, 33, 12, 54, 18, 96];

// 舊寫法
const youngest = Math.min.apply(Math, ages);
const oldest = Math.max.apply(Math, ages);
const type = Object.prototype.toString.call(youngest);

// 新寫法
const youngest = Reflect.apply(Math.min, Math, ages);
const oldest = Reflect.apply(Math.max, Math, ages);
const type = Reflect.apply(Object.prototype.toString, youngest, []);

Reflect.defineProperty(target, propertyKey, attributes)

Reflect.defineProperty方法基本等同於Object.defineProperty,用來爲對象定義屬性。未來,後者會被逐漸廢除,請從現在開始就使用Reflect.defineProperty代替它。

function MyDate() {
  /*…*/
}

// 舊寫法
Object.defineProperty(MyDate, 'now', {
  value: () => Date.now()
});

// 新寫法
Reflect.defineProperty(MyDate, 'now', {
  value: () => Date.now()
});

如果Reflect.defineProperty的第一個參數不是對象,就會拋出錯誤,比如Reflect.defineProperty(1, 'foo')

這個方法可以與Proxy.defineProperty配合使用。

const p = new Proxy({}, {
  defineProperty(target, prop, descriptor) {
    console.log(descriptor);
    return Reflect.defineProperty(target, prop, descriptor);
  }
});

p.foo = 'bar';
// {value: "bar", writable: true, enumerable: true, configurable: true}

p.foo // "bar"

上面代碼中,Proxy.defineProperty對屬性賦值設置了攔截,然後使用Reflect.defineProperty完成了賦值。

Reflect.getOwnPropertyDescriptor(target, propertyKey)

Reflect.getOwnPropertyDescriptor基本等同於Object.getOwnPropertyDescriptor,用於得到指定屬性的描述對象,將來會替代掉後者。

var myObject = {};
Object.defineProperty(myObject, 'hidden', {
  value: true,
  enumerable: false,
});

// 舊寫法
var theDescriptor = Object.getOwnPropertyDescriptor(myObject, 'hidden');

// 新寫法
var theDescriptor = Reflect.getOwnPropertyDescriptor(myObject, 'hidden');

Reflect.getOwnPropertyDescriptorObject.getOwnPropertyDescriptor的一個區別是,如果第一個參數不是對象,Object.getOwnPropertyDescriptor(1, 'foo')不報錯,返回undefined,而Reflect.getOwnPropertyDescriptor(1, 'foo')會拋出錯誤,表示參數非法。

Reflect.isExtensible (target)

Reflect.isExtensible方法對應Object.isExtensible,返回一個布爾值,表示當前對象是否可擴展。

const myObject = {};

// 舊寫法
Object.isExtensible(myObject) // true

// 新寫法
Reflect.isExtensible(myObject) // true

如果參數不是對象,Object.isExtensible會返回false,因爲非對象本來就是不可擴展的,而Reflect.isExtensible會報錯。

Object.isExtensible(1) // false
Reflect.isExtensible(1) // 報錯

Reflect.preventExtensions(target)

Reflect.preventExtensions對應Object.preventExtensions方法,用於讓一個對象變爲不可擴展。它返回一個布爾值,表示是否操作成功。

var myObject = {};

// 舊寫法
Object.preventExtensions(myObject) // Object {}

// 新寫法
Reflect.preventExtensions(myObject) // true

如果參數不是對象,Object.preventExtensions在 ES5 環境報錯,在 ES6 環境返回傳入的參數,而Reflect.preventExtensions會報錯。

// ES5 環境
Object.preventExtensions(1) // 報錯

// ES6 環境
Object.preventExtensions(1) // 1

// 新寫法
Reflect.preventExtensions(1) // 報錯

Reflect.ownKeys (target)

Reflect.ownKeys方法用於返回對象的所有屬性,基本等同於Object.getOwnPropertyNamesObject.getOwnPropertySymbols之和。

var myObject = {
  foo: 1,
  bar: 2,
  [Symbol.for('baz')]: 3,
  [Symbol.for('bing')]: 4,
};

// 舊寫法
Object.getOwnPropertyNames(myObject)
// ['foo', 'bar']

Object.getOwnPropertySymbols(myObject)
//[Symbol(baz), Symbol(bing)]

// 新寫法
Reflect.ownKeys(myObject)
// ['foo', 'bar', Symbol(baz), Symbol(bing)]

如果Reflect.ownKeys()方法的第一個參數不是對象,會報錯。

實例:使用 Proxy 實現觀察者模式

觀察者模式(Observer mode)指的是函數自動觀察數據對象,一旦對象有變化,函數就會自動執行。

const person = observable({
  name: '張三',
  age: 20
});

function print() {
  console.log(`${person.name}, ${person.age}`)
}

observe(print);
person.name = '李四';
// 輸出
// 李四, 20

上面代碼中,數據對象person是觀察目標,函數print是觀察者。一旦數據對象發生變化,print就會自動執行。

下面,使用 Proxy 寫一個觀察者模式的最簡單實現,即實現observableobserve這兩個函數。思路是observable函數返回一個原始對象的 Proxy 代理,攔截賦值操作,觸發充當觀察者的各個函數。

const queuedObservers = new Set();

const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});

function set(target, key, value, receiver) {
  const result = Reflect.set(target, key, value, receiver);
  queuedObservers.forEach(observer => observer());
  return result;
}

上面代碼中,先定義了一個Set集合,所有觀察者函數都放進這個集合。然後,observable函數返回原始對象的代理,攔截賦值操作。攔截函數set之中,會自動執行所有觀察者。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章