- 原文地址:Metaprogramming in ES6: Part 2 - Reflect
- 原文做者:Keith Cirkel
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:yoyoyohamapi
- 校對者:IridescentMia ParadeTo
在個人上一篇博文,咱們探索了 Symbols,以及它們是如何爲 JavaScript 添加了有用的元編程特性。這一次,咱們(終於!)要開始討論反射了。若是你還沒有讀過 第一部分:Symbols,那我建議你先去讀讀。在上一篇文章中,我不厭其煩地強調一點:html
Reflect
是一個新的全局對象(相似 JSON
或者 Math
),該對象提供了大量有用的內省(introspection)方法(內省是 「看看那個東西」 的一個很是華麗的表述)。內省工具已經存在於 JavaScript 了,例如 Object.keys
,Object.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.getOwnPropertyNames
和 Object.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.keys
、Object.getOwnPropertyNames
這樣」。如今,我告訴你這麼作的理由:git
Reflect.apply
,畢竟調用 Object.apply(myFunction)
看起來太怪了。 typeof
、instanceof
以及 delete
已經做爲反射運算符存在了 —— 爲此添加一樣功能的新關鍵字將會加劇開發者的負擔,同時,對於向後兼容性也是一個夢魘,而且會讓 JavaScript 中的保留字數量急速膨脹。Reflect.apply
與 Function#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.apply
—— Reflect.construct
讓你傳入一系列參數來調用構造函數。它可以服務於類,而且設置正確的對象來使 Constructor 有正確的 this
引用以匹配對應的原型。在 ES5 時期,你會使用 Object.create(Constructor.prototype)
模式,而後傳遞對象到 Constructor.call
或者 Constructor.apply
。 Reflect.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.definedProperty
很大程度上源於 Object.defineProperty
—— 它容許你定義一個屬性的元信息。 相較於 Object.defineProperty
,Reflect.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
代替 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
可以刪除目標對象上的一個屬性。在 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);複製代碼
再重複一遍,若是你想的話,你能夠考慮使用這個 「新的方式」 來刪除屬性。這個方式顯然意圖更加明確,就是刪除屬性。
關於替代/淘汰 Object 方法的議題還在繼續 —— 這一次該是 Object.getPrototypeOf
了。正如其兄妹方法同樣,若是你傳入了一個諸如 Number 和 String 字面量、null
或者是 undefined
這樣無效的 target
,Reflect.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複製代碼
固然,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);複製代碼
再一次強調這是用來替代 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); // 只工做在新的瀏覽器複製代碼
這是最後一個反射對象從 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);複製代碼
更新:在 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]
。若是 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.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
是一個很是有趣的方法,由於它本質上與 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
實現了 [[OwnPropertyKeys]]
,你回想一下上文的內容,你知道它鏈接了 Object.getOwnPropertyNames
和 Object.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-js 和 harmony-reflect。
對於新的 Reflect API ,你是怎麼看待的 ?計劃在你的項目中使用它了 ?能夠在個人 Twitter 給我留言,我是 @keithamus。
也別忘了,這個系列的第三部分 —— 代理(Proxy)也快發佈了,我不會再拖延兩個月了。(已經發布:juejin.im/post/5a0f05…
最後,要謝謝 @mttshw 和 @WebReflection 對我工做的審視,才讓文章比預計的更加高質。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。