[譯]ES6 中的元編程:第二部分 —— 反射(Reflect)

在個人上一篇博文,咱們探索了 Symbols,以及它們是如何爲 JavaScript 添加了有用的元編程特性。這一次,咱們(終於!)要開始討論反射了。若是你還沒有讀過 第一部分:Symbols,那我建議你先去讀讀。在上一篇文章中,我不厭其煩地強調一點:html

  • Symbols 是 實現了的反射(Reflection within implementation)—— 你將 Symbols 應用到你已有的類和對象上去改變它們的行爲。
  • Reflect 是 經過自省(introspection)實現反射(Reflection through introspection) —— 一般用來探索很是底層的代碼信息。
  • Proxy 是 經過調解(intercession)實現反射(Reflection through intercession) —— 包裹對象並經過自陷(trap)來攔截對象行爲。

Reflect 是一個新的全局對象(相似 JSON 或者 Math),該對象提供了大量有用的內省(introspection)方法(內省是 「看看那個東西」 的一個很是華麗的表述)。內省工具已經存在於 JavaScript 了,例如 Object.keysObject.getOwnPropertyNames 等等。因此,爲何咱們仍然新的 API ,而不是直接在 Object 上作擴展呢? 前端

「內置方法」

全部的 JavaScript 規範,以及所以誕生的引擎,都來源於一系列的 「內置方法」。這些內置方法可以有效地讓 JavaScript 引擎在對象上執行一些遍及你代碼的基礎操做。若是你通讀了規範,你會發現這些方法散落各處,例如 [[Get]][[Set]][[HasOwnProperty]] 等等(若是你沒有耐心通讀全部規範,那麼這些內置方法列表在 ES5 8.12 部分 以及 ES6 9.1 部分 能夠查閱到)。react

其中一些 「內置方法」 對 JavaScript 代碼是隱藏的,另外一些則應用在了其餘方法中,即便這些方法可用,它們仍被隱藏於難於窺見的縫隙之中。例如,Object.prototype.hasOwnProperty[[HasOwnProperty]] 的一個實現,但不是全部的對象都繼承自 Object,爲此,有時你不得不寫出一些古怪的代碼才能用上 hasOwnProperty,以下例所示:android

var myObject = Object.create(null); // 這段代碼比你想象得更加常見(尤爲是在使用了新的 ES6 的類的時候)
assert(myObject.hasOwnProperty === undefined);
// 若是你想在 `myObject` 上使用 hasOwnProperty:
Object.prototype.hasOwnProperty.call(myObject, 'foo');複製代碼

再看到另外一個例子,[[OwnPropertyKeys]] 這一內置方法能得到對象上全部的字符串 key 和 Symbol key,並做爲一個數組返回。在不使用 Reflect 的狀況下,能一次性得到這些 key 的方式只有鏈接 Object.getOwnPropertyNamesObject.getOwnPropertySymbols 的結果:ios

var s = Symbol('foo');
var k = 'bar';
var o = { [s]: 1, [k]: 1 };
// 模擬 [[OwnPropertyKeys]]
var keys = Object.getOwnPropertyNames(o).concat(Object.getOwnPropertySymbols(o));
assert.deepEqual(keys, [k, s]);複製代碼

反射方法

反射是一個很是有用的集合,它囊括了全部 JavaScript 引擎內部專有的 「內部方法」,如今被暴露爲了一個單1、方便的對象 —— Reflect。你可能會問:「這聽起來不錯,可是爲何不直接將內置方法綁定到 Object 上呢?就像 Object.keysObject.getOwnPropertyNames 這樣」。如今,我告訴你這麼作的理由:git

  1. 反射擁有的方法不只針對於 Object,還可能針對於函數,例如 Reflect.apply,畢竟調用 Object.apply(myFunction) 看起來太怪了。
  2. 用一個單一對象貯存內置方法能保持 JavaScript 其他部分的純淨性,這要優於將反射方法經過點操做符掛載到構造函數或者原型上,更要優於直接使用全局變量。
  3. typeofinstanceof 以及 delete 已經做爲反射運算符存在了 —— 爲此添加一樣功能的新關鍵字將會加劇開發者的負擔,同時,對於向後兼容性也是一個夢魘,而且會讓 JavaScript 中的保留字數量急速膨脹。

Reflect.apply ( target, thisArgument [, argumentList] )

Reflect.applyFunction#apply 相似 —— 它接受一個函數,一個調用該函數的上下文以及一個參數數組。從如今開始,你 能夠 認爲 Function#call/Function#apply 的已是過期版本了。這不是翻天覆地的變化,但卻有很大意義。下面展現了 Reflect.apply 的用法:es6

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

// Function.prototype 風格:
var youngest = Math.min.apply(Math, ages);
var oldest = Math.max.apply(Math, ages);
var type = Object.prototype.toString.call(youngest);

// Reflect 風格:
var youngest = Reflect.apply(Math.min, Math, ages);
var oldest = Reflect.apply(Math.max, Math, ages);
var type = Reflect.apply(Object.prototype.toString, youngest);複製代碼

從 Function.prototype.apply 到 Reflect.apply 的變遷的真正益處是防護性:任何代碼都可以嘗試改變函數的 call 或者 apply 方法,這會讓你受困於崩潰的代碼或者某些糟糕的情境。在現實世界中,這不會成爲一件大事,可是下面這樣的代碼可能真正存在:github

function totalNumbers() {
  return Array.prototype.reduce.call(arguments, function (total, next) {
    return total + next;
  }, 0);
}
totalNumbers.apply = function () {
  throw new Error('Aha got you!');
}

totalNumbers.apply(null, [1, 2, 3, 4]); // 拋出 Error('Aha got you!');

// ES5 中保證防護性的代碼看起來很糟糕:
Function.prototype.apply.call(totalNumbers, null, [1, 2, 3, 4]) === 10;

// 你也能夠這樣作,但看起來仍是不夠整潔:
Function.apply.call(totalNumbers, null, [1, 2, 3, 4]) === 10;

// Reflect.apply 會是救世主!
Reflect.apply(totalNumbers, null, [1, 2, 3, 4]) === 10;複製代碼

Reflect.construct ( target, argumentsList [, constructorToCreateThis] )

相似於 Reflect.apply —— Reflect.construct 讓你傳入一系列參數來調用構造函數。它可以服務於類,而且設置正確的對象來使 Constructor 有正確的 this 引用以匹配對應的原型。在 ES5 時期,你會使用 Object.create(Constructor.prototype) 模式,而後傳遞對象到 Constructor.call 或者 Constructor.applyReflect.construct 的不一樣之處在於,你只須要傳遞構造函數,而不須要傳遞對象 —— Reflect.construct 處理好一切(若是省略第三個參數,那麼構造的對象原型將默認綁定到 target 參數)。在以前的風格中,完成對象構造是一件繁重的事兒,而在新的風格之下,這事兒簡單到一行代碼便可完成:編程

class Greeting {

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

    greet() {
      return Hello ${this.name};
    }

}

// ES5 風格的工廠函數:
function greetingFactory(name) {
    var instance = Object.create(Greeting.prototype);
    Greeting.call(instance, name);
    return instance;
}

// ES6 風格的工廠函數:
function greetingFactory(name) {
    return Reflect.construct(Greeting, [name], Greeting);
}

// 若是省略第三個參數,那麼默認綁定對象原型到第一個參數
function greetingFactory(name) {
  return Reflect.construct(Greeting, [name]);
}

// ES6 下順滑無比的線性工廠函數:
const greetingFactory = (name) => Reflect.construct(Greeting, [name]);複製代碼

Reflect.defineProperty ( target, propertyKey, attributes )

Reflect.definedProperty 很大程度上源於 Object.defineProperty —— 它容許你定義一個屬性的元信息。 相較於 Object.definePropertyReflect.defineProperty 要更加適合,由於 Obejct.* 暗示了它是做用在對象字面量上(畢竟 Object 是對象字面量的構造函數),然而 Reflect.defineProperty 僅只暗示了你正在作反射,這要更加的語義化。後端

要留心的是 Reflect.defineProperty —— 正如 Object.defineProperty 同樣 —— 對於無效的 target,例如 Number 或者 String 原始值(Reflect.defineProperty(1, 'foo')),將拋出一個 TypeError。相較於靜默失敗,當參數類型錯誤時,拋出錯誤以引發你的注意是一件更好的事兒。

再重複一次,你能夠認爲 Object.defineProperty 從如今起過期了,並使用 Reflect.defineProperty 代替:

function MyDate() {
  /*…*/
}

// 老的風格下,咱們使用 Object.defineProperty 來定義一個函數的屬性,顯得很奇怪
// (爲何咱們不用 Function.defineProperty ?)
Object.defineProperty(MyDate, 'now', {
  value: () => currentms
});

// 新的風格下,語義就通暢得多,由於 Reflect 只是在作反射。
Reflect.defineProperty(MyDate, 'now', {
  value: () => currentms
});複製代碼

Reflect.getOwnPropertyDescriptor ( target, propertyKey )

同上面同樣,咱們優先使用 Reflect.getOwnPropertyDescriptor 代替 Object.getOwnPropertyDescriptor 來得到一個屬性的描述子元信息。與 Object.getOwnPropertyDescriptor(1, 'foo') 會靜默失敗,返回 undefined 不一樣,Reflect.getOwnPropertyDescriptor(1, 'foo') 將拋出一個 TypeError 錯誤 —— 與 Reflect.defineProperty 同樣,該錯誤是針對於 target 無效拋出的。你如今也知道了,咱們可使用 Reflect.getOwnPropertyDescriptor 替換掉 Object.getOwnPropertyDescriptor 了:

var myObject = {};
Object.defineProperty(myObject, 'hidden', {
  value: true,
  enumerable: false,
});
var theDescriptor = Reflect.getOwnPropertyDescriptor(myObject, 'hidden');
assert.deepEqual(theDescriptor, { value: true, enumerable: true });

// 老的風格
var theDescriptor = Object.getOwnPropertyDescriptor(myObject, 'hidden');
assert.deepEqual(theDescriptor, { value: true, enumerable: true });

assert(Object.getOwnPropertyDescriptor(1, 'foo') === undefined)
Reflect.getOwnPropertyDescriptor(1, 'foo'); // throws TypeError複製代碼

Reflect.deleteProperty ( target, propertyKey )

很是很是使人興奮,Reflect.deleteProperty 可以刪除目標對象上的一個屬性。在 ES6 以前,你通常是經過 delete obj.foo,如今,你可使用 Reflect.deleteProperty(obj, 'foo') 來刪除對象屬性了。Reflect.deleteProperty 稍顯冗長,在語義上與 delete 關鍵字有些不一樣,但對於刪除對象卻有相同的做用。兩者都是調用內置的 target[[Delete]](propertyKey) 方法 —— 可是 delete 運算也能 「工做」 在非對象引用上(例如變量),所以它會對傳遞給它的運算數作更多的檢查,潛在地,也就存在拋出錯誤的可能性:

var myObj = { foo: 'bar' };
delete myObj.foo;
assert(myObj.hasOwnProperty('foo') === false);

myObj = { foo: 'bar' };
Reflect.deleteProperty(myObj, 'foo');
assert(myObj.hasOwnProperty('foo') === false);複製代碼

再重複一遍,若是你想的話,你能夠考慮使用這個 「新的方式」 來刪除屬性。這個方式顯然意圖更加明確,就是刪除屬性。

Reflect.getPrototypeOf ( target )

關於替代/淘汰 Object 方法的議題還在繼續 —— 這一次該是 Object.getPrototypeOf 了。正如其兄妹方法同樣,若是你傳入了一個諸如 Number 和 String 字面量、null 或者是 undefined 這樣無效的 targetReflect.getPropertyOf 將拋出一個 TypeError 錯誤,而 Object.getPropertyOf 強制轉化 target 爲一個對象 —— 因此 'a' 變爲了 Object('a')。除了語法之外,兩者幾乎相同:

var myObj = new FancyThing();
assert(Reflect.getPrototypeOf(myObj) === FancyThing.prototype);

// 老的風格
assert(Object.getPrototypeOf(myObj) === FancyThing.prototype);

Object.getPrototypeOf(1); // undefined
Reflect.getPrototypeOf(1); // TypeError複製代碼

Reflect.setPrototypeOf ( target, proto )

固然,getProtopertyOf 不能沒了 setPropertyOf。如今,Object.setPrototypeOf 對於傳入非對象參數,將拋出錯誤,但它會嘗試將傳入參數強制轉換爲 Object,而且若是內置的 [[SetPrototype]] 操做失敗,將拋出 TypeError,而若是成功的話,將返回 target 參數。Reflect.setPrototypeOf 則更加簡單基礎 —— 若是其收到了一個非對象參數,它就將拋出一個 TypeError 錯誤,但除此以外,它還會返回 [[SetPrototypeOf]] 的結果 —— 這是一個 Boolean 值,指出了操做是否錯誤。這是頗有用的,由於你能夠直接知曉操做錯誤與否,而不須要使用 try/catch,這將會俘獲其餘因爲參數傳遞錯誤形成的 TypeErrors

var myObj = new FancyThing();
assert(Reflect.setPrototypeOf(myObj, OtherThing.prototype) === true);
assert(Reflect.getPrototypeOf(myObj) === OtherThing.prototype);

// 老的風格
assert(Object.setPrototypeOf(myObj, OtherThing.prototype) === myObj);
assert(Object.getPrototypeOf(myObj) === FancyThing.prototype);

Object.setPrototypeOf(1); // TypeError
Reflect.setPrototypeOf(1); // TypeError

var myFrozenObj = new FancyThing();
Object.freeze(myFrozenObj);

Object.setPrototypeOf(myFrozenObj); // TypeError
assert(Reflect.setPrototypeOf(myFrozenObj) === false);複製代碼

Reflect.isExtensible (target)

再一次強調這是用來替代 Object.isExtensible 的 —— 可是它比後者要更加複雜。在 ES6 以前(例如說 ES5),若是你傳入了非對象參數(typeof target !== object),Object.isExtensible 會拋出一個 TypeError。ES6 則在語義上發生了改變(天哪!竟然改變了現有的 API!)使得傳入非對象參數時,Object.isExtensible 返回 false —— 由於非對象確實就是不可擴展。因此在 ES6 下,這個早先會拋出錯誤的語句:Object.isExtensible(1) === false 如今表現得如你所想,語義更加準確。

上面簡短的歷史回顧引出關鍵點就是 Reflect.isExtensible 使用的是老舊行爲,即當傳入非對象參數時,拋出錯誤。我不真正肯定爲何它要這麼作,但它確實這麼作了。因此技術上 Reflect.isExtensible 改變了 Object.isExtensible 的語義,可是 Object.isExtensible 本身也發生了語義改變。下面的代碼說明了這些:

var myObject = {};
var myNonExtensibleObject = Object.preventExtensions({});

assert(Reflect.isExtensible(myObject) === true);
assert(Reflect.isExtensible(myNonExtensibleObject) === false);
Reflect.isExtensible(1); // 拋出 TypeError
Reflect.isExtensible(false);  // 拋出 TypeError

// 使用 Object.isExtensible
assert(Object.isExtensible(myObject) === true);
assert(Object.isExtensible(myNonExtensibleObject) === false);

// ES5 Object.isExtensible 語義
Object.isExtensible(1); // 在老版本的瀏覽器下,會拋出 TypeError
Object.isExtensible(false);  // 在老版本的瀏覽器下,會拋出 TypeError

// ES6 Object.isExtensible 語義
assert(Object.isExtensible(1) === false); // 只工做在新的瀏覽器
assert(Object.isExtensible(false) === false); // 只工做在新的瀏覽器複製代碼

Reflect.preventExtensions ( target )

這是最後一個反射對象從 Object 上借鑑的方法。它和 Reflect.isExtensible 有相似的故事;ES5 的 Object.preventExtensions 過去會對非對象參數拋出錯誤,可是如今,在 ES6 中,它會返回傳入值,而 Reflect.preventExtensions 聽從的則是老的 ES5 行爲 —— 即對非對象參數拋出錯誤。另外,在操做成功的狀況下,Object.preventExtensions 可能拋出錯誤,但 Reflect.preventExtension 僅簡單地返回 true 或者 false,容許你優雅地操控失敗場景:

var myObject = {};
var myObjectWhichCantPreventExtensions = magicalVoodooProxyCode({});

assert(Reflect.preventExtensions(myObject) === true);
assert(Reflect.preventExtensions(myObjectWhichCantPreventExtensions) === false);
Reflect.preventExtensions(1); // 拋出 TypeError
Reflect.preventExtensions(false);  // 拋出 TypeError

// 使用 Object.preventExtensions
assert(Object.preventExtensions(myObject) === true);
Object.preventExtensions(myObjectWhichCantPreventExtensions); // throws TypeError

// ES5 Object.preventExtensions 語義
Object.preventExtensions(1); // 拋出 TypeError
Object.preventExtensions(false);  // 拋出 TypeError

// ES6 Object.preventExtensions 語義
assert(Object.preventExtensions(1) === 1);
assert(Object.preventExtensions(false) === false);複製代碼

Reflect.enumerate ( target )

更新:在 ES2016(也稱 ES7)中,這被刪除了。myObject[Symbol.iterator]() 是在對象 key 或者 value 上迭代的惟一方式。

最後,將引出一個全新的 Reflect 方法!Reflect.enumerate 使用了和新的 Symbol.iterator 函數(在前一章節,已對此有過討論) 同樣的語法,兩者都使用了隱藏的,只有 JavaScript 引擎知道的 [[Enumerate]] 方法。換句話說,Reflect.enumerate 的惟一替代只是 myObject[Symbol.iterator()],只是後者能夠被重寫,而前者不行。使用範例以下:

var myArray = [1, 2, 3];
myArray[Symbol.enumerate] = function () {
  throw new Error('Nope!');
}
for (let item of myArray) { // error thrown: Nope!
}
for (let item of Reflect.enumerate(myArray)) {
  // 1 then 2 then 3
}複製代碼

Reflect.get ( target, propertyKey [ , receiver ])

Reflect.get 也是一個全新的方法。它是一個很是簡單的方法,其有效地調用了 target[propertyKey]。若是 target 是一個非對象,函數調用將拋出錯誤 —— 這是頗有用的,由於目前若是你寫了 1['foo'] 這樣的代碼,它只會靜默返回 undefined,而 Reflect.get(1, 'foo') 將拋出一個 TypeError 錯誤!Reflect.get 一個有趣的部分是它的 receiver 參數,若是 target[propertyKey] 是一個 getter 函數,它則做爲該函數的 this,例子以下所示:

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

assert(Reflect.get(myObject, 'foo') === 1);
assert(Reflect.get(myObject, 'bar') === 2);
assert(Reflect.get(myObject, 'baz') === 3);
assert(Reflect.get(myObject, 'baz', myObject) === 3);

var myReceiverObject = {
  foo: 4,
  bar: 4,
};
assert(Reflect.get(myObject, 'baz', myReceiverObject) === 8);

// 非對象將拋出錯誤
Reflect.get(1, 'foo'); // 拋出 TypeError
Reflect.get(false, 'foo'); // 拋出 TypeError

// 老的風格下,靜默返回 `undefined`:
assert(1['foo'] === undefined);
assert(false['foo'] === undefined);複製代碼

Reflect.set ( target, propertyKey, V [ , receiver ] )

你大體可以猜出該方法是作什麼的。它是 Reflect.get 的兄弟方法,它接收另一個參數 —— 須要被設置的值。如 Reflect.get 同樣,Reflect.set 將在傳入非對象參數時,拋出錯誤,而且也有一個 receiver 參數指明 target[propertyKey] 爲 setter 函數時使用的 this。必須上個代碼示例:

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

assert(myObject.foo === 1);
assert(Reflect.set(myObject, 'foo', 2));
assert(myObject.foo === 2);
assert(Reflect.set(myObject, 'bar', 3));
assert(myObject.foo === 3);
assert(Reflect.set(myObject, 'bar', myObject) === 4);
assert(myObject.foo === 4);

var myReceiverObject = {
  foo: 0,
};
assert(Reflect.set(myObject, 'bar', 1, myReceiverObject));
assert(myObject.foo === 4);
assert(myReceiverObject.foo === 1);

// 非對象將拋出錯誤
Reflect.set(1, 'foo', {}); // 拋出 TypeError
Reflect.set(false, 'foo', {}); // 拋出 TypeError

// 老的風格下,靜默返回 `undefined`:
1['foo'] = {};
false['foo'] = {};
assert(1['foo'] === undefined);
assert(false['foo'] === undefined);複製代碼

Reflect.has ( target, propertyKey )

Reflect.has 是一個很是有趣的方法,由於它本質上與 in 運算符有同樣的功能(在循環以外)。兩者都使用了內置的 [[HasProperty]],而且都會在 target 不爲對象時拋出錯誤。除非你更偏向於函數調用的風格,相較於 in,沒有多少使用 Reflect.has 的理由,可是它在語言的其餘方面有重要的使用,這將在下一章有清楚的講述。不管如何,先看看怎麼用它:

myObject = {
  foo: 1,
};
Object.setPrototypeOf(myObject, {
  get bar() {
    return 2;
  },
  baz: 3,
});

// 不使用 Reflect.has:
assert(('foo' in myObject) === true);
assert(('bar' in myObject) === true);
assert(('baz' in myObject) === true);
assert(('bing' in myObject) === false);

// 使用 Reflect.has:
assert(Reflect.has(myObject, 'foo') === true);
assert(Reflect.has(myObject, 'bar') === true);
assert(Reflect.has(myObject, 'baz') === true);
assert(Reflect.has(myObject, 'bing') === false);複製代碼

Reflect.ownKeys ( target )

該方法已經在本文有所說起了,你能夠看到 Reflect.ownKeys 實現了 [[OwnPropertyKeys]],你回想一下上文的內容,你知道它鏈接了 Object.getOwnPropertyNamesObject.getOwnPropertySymbols 的結果。這讓 Reflect.ownKeys 有着不可替代的做用。下面看到用法:

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

assert.deepEqual(Object.getOwnPropertyNames(myObject), ['foo', 'bar']);
assert.deepEqual(Object.getOwnPropertySymbols(myObject), [Symbol.for('baz'), Symbol.for('bing')]);

// 不使用 Reflect.ownKeys:
var keys = Object.getOwnPropertyNames(myObject).concat(Object.getOwnPropertySymbols(myObject));
assert.deepEqual(keys, ['foo', 'bar', Symbol.for('baz'), Symbol.for('bing')]);

// 使用 Reflect.ownKeys:
assert.deepEqual(Reflect.ownKeys(myObject), ['foo', 'bar', Symbol.for('baz'), Symbol.for('bing')]);複製代碼

結論

咱們對各個 Reflect 方法進行了完全的討論。咱們看到了一些現有方法的新版本,一些作了微調,一些則是完徹底全新的方法 —— 這將 JavaScript 的反射提高到了一個新的層面。若是你想的話,大能夠徹底的拋棄 Object.*/Function.* 方法,用 Reflect 替代之,若是你不想的話,別擔憂,不用就不用,什麼都不會改變。

如今,我不想你看完兩手空空,毫無所獲。若是你想要使用 Reflect,咱們已經給予了你支持 —— 做爲這個文章背後工做的一部分,我提交了一個 pull request 到 eslint,在 v1.0.0 版本,ESlint 有了一個 prefer-reflect 規則,這可讓你在使用老舊版本的 Reflect 方法時,獲得 ESLint 的提示。你也能夠看下個人 eslint-config-strict 配置,該開啓 prefer-reflect 規則(也添加了許多額外的規則)。固然,若是你決定你想要使用 Reflect,你可能須要 polyfill 它;幸運的是,如今已經有了一些好的 polyfill,如 core-jsharmony-reflect

對於新的 Reflect API ,你是怎麼看待的 ?計劃在你的項目中使用它了 ?能夠在個人 Twitter 給我留言,我是 @keithamus

也別忘了,這個系列的第三部分 —— 代理(Proxy)也快發佈了,我不會再拖延兩個月了。(已經發布:juejin.im/post/5a0f05…

最後,要謝謝 @mttshw@WebReflection 對我工做的審視,才讓文章比預計的更加高質。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索