在 ECMAScript 2015 之前,JavaScript 裡的物件字面量(也叫物件初始化器)功能很弱。它只能定義兩種屬性:
令人心痛地,物件字面量的所有用法只用一個簡單的例子就能囊括:
1 2 3 4 5 6 7 8 9 10 11 12 |
var myObject = { myString: 'value 1', get myNumber() { return this.myNumber; }, set myNumber(value) { this.myNumber = Number(value); } }; myObject.myString; // => 'value 1' myObject.myNumber = '15'; myObject.myNumber; // => 15 |
JavaScript 是一個基於原型的語言,因此一切皆是物件。當涉及到物件建立、結構和訪問原型時,語言必須提供簡單的結構。
定義一個物件並設定它的原型是一個常見的任務。我總覺得設定原型應該被物件字面量直接支援,使用單一的語法。
不幸的是,字面量的侷限性使得它沒有直接的解決方案。你不得不使用Object.create()
來結合物件字面量和設定原型:
1 2 3 4 5 6 7 8 9 |
var myProto = { propertyExists: function(name) { return name in this; } }; var myNumbers = Object.create(myProto); myNumbers['array'] = [1, 6, 7]; myNumbers.propertyExists('array'); // => true myNumbers.propertyExists('collection'); // => false |
在我看來,這是一個不舒服的解決方案。JavaScript 是基於原型的,為什麼從原型建立一個物件那麼麻煩?
幸運地是,JavaScript 在改變。比較令人沮喪的許多問題在 JavaScript 中正在被一步一步地解決。
這篇文章解釋 ES2015 是如何解決上面所說的問題以及改進物件字面量以獲得額外的好處::
- 在物件構造的過程中設定原型
- 速記方法定義
- 呼叫父類方法
- 計算屬性名稱
同時,讓我們看看未來,瞭解最新的提案(stage 2):物件的 rest 屬性和屬性展開操作符。
1. 在物件構造時設定原型
如你已經知道得,其中一個訪問一個已存在物件的原型的方法是使用 getter 屬性__proto__
:
1 2 3 4 5 |
var myObject = { name: 'Hello World!' }; myObject.__proto__; // => {} myObject.__proto__.isPrototypeOf(myObject); // => true |
myObject.__proto__
返回 myObject
的原型物件。
好訊息是 ES2015 允許使用 字面量 __proto__
作為屬性名來設定物件字面量的原型 { __proto__: protoObject }
。
讓我們用 __proto__
重寫一下上面那個例子,讓它看起來好一點:
1 2 3 4 5 6 7 8 9 10 11 |
var myProto = { propertyExists: function(name) { return name in this; } }; var myNumbers = { __proto__: myProto, array: [1, 6, 7] }; myNumbers.propertyExists('array'); // => true myNumbers.propertyExists('collection'); // => false |
myNumbers
物件使用原型 myProto
建立,這可以通過特殊屬性 __proto__
實現。
這個物件通過簡單的語句建立,而不需要額外的函式例如 Object.create()
。
如你所見,使用 __proto__
是簡單的。我偏愛簡單直接的解決方案。
有點說跑題了,回到主題來。我認為獲得簡單和可靠的解決方案需要通過大量的設計和實踐。如果一個解決方案是簡單的,你可能認為它同樣也很容易被設計出來,然而事實並不是這樣:
- 讓它變得簡單明瞭的過程是複雜的
- 讓它變得複雜和難以理解卻很容易
如果某個東西看起來太複雜或者用起來不舒服,很可能它的設計者考慮不周。
元芳,你怎麼看?(歡迎在文章底部發表評論參與討論)
2.1 使用 __proto__
的特例
儘管 __proto__
看似簡單,卻有一些特殊的場景你需要格外注意。
在物件字面量中 __proto__
只允許使用一次。多次是用的話 JavaScript 會丟擲異常:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var object = { __proto__: { toString: function() { return '[object Numbers]' } }, numbers: [1, 5, 89], __proto__: { toString: function() { return '[object ArrayOfNumbers]' } } }; |
上面例子中的物件字面量使用了 __proto__
屬性兩次,這是不允許的。這種情況下,會丟擲一個錯誤 SyntaxError: Duplicate __proto__ fields are not allowed in object literals
。
JavaScript 限制了只允許使用 object 或者 null
作為 __proto__
屬性的值。使用其它原生型別(如字串、數值、布林型別)或者 undefined
會被忽略,並不能改變物件的原型。
看一個例子:
1 2 3 4 5 6 7 8 |
var objUndefined = { __proto__: undefined }; Object.getPrototypeOf(objUndefined); // => {} var objNumber = { __proto__: 15 }; Object.getPrototypeOf(objNumber); // => {} |
上面的例子裡,物件字面量使用 undefined
和數值 15
來設定 __proto__
值。因為只有物件或者 null
才允許被使用,objUndefined
和 objNumber
仍然是它們預設的原型:簡單 JavaScript 物件 {}
。__proto__
的賦值被忽略了。
當然,嘗試使用原生型別來設定物件的原型,這本身是很奇怪的。所以在這裡做限制是符合預期的。
2. 速記方法定義
現在物件字面量中可以使用一個更短的語法來宣告方法,省略 function
關鍵字和冒號。這被叫做速記方法定義(shorthand method definition)。
讓我們用新的方式來定義一些方法:
1 2 3 4 5 6 7 8 9 10 11 12 |
var collection = { items: [], add(item) { this.items.push(item); }, get(index) { return this.items[index]; } }; collection.add(15); collection.add(3); collection.get(0); // => 15 |
add()
和 get()
是 collection
中使用快捷的方式定義的方法。
一個很好的地方是這樣宣告的方法是具名的,這對於除錯有幫助。執行collection.add.name
將返回函式名稱 'add'
。
3. 呼叫父類方法
一個有趣的改進是能夠使用 super
關鍵字來訪問從原型鏈繼承下來的屬性。看下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var calc = { sumArray (items) { return items.reduce(function(a, b) { return a + b; }); } }; var numbers = { __proto__: calc, numbers: [4, 6, 7], sumElements() { return super.sumArray(this.numbers); } }; numbers.sumElements(); // => 17 |
calc
是 numbers
物件的屬性。在 numbers
的方法 sumElements
裡面,要呼叫原型 calc
上的方法,可以使用 super
關鍵字: super.sumArray()
。
所以 super
是從原型鏈訪問被繼承的屬性的一個快捷的方法。
在上一個例子裡,我們也可以直接呼叫 cale.sumArray()
,不過 super
是一個更好的選擇因為它訪問物件的原型鏈。它的存在清晰地暗示了繼承的屬性將被使用。
3.1 使用 super
的限制
在物件字面量中, super
只能用在速記方法定義中。
如果在普通的方法宣告 { name: function() {} }
中使用它,JavaScript 會拋異常:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var calc = { sumArray (items) { return items.reduce(function(a, b) { return a + b; }); } }; var numbers = { __proto__: calc, numbers: [4, 6, 7], sumElements: function() { return super.sumArray(this.numbers); } }; // Throws SyntaxError: 'super' keyword unexpected here numbers.sumElements(); |
上面的程式碼裡,方法 sumElements
被定義為:sumElements:function(){...}
。由於super
要求在速記方法中使用,在其中呼叫 super
將丟擲異常:SyntaxError: 'super' keyword unexpected here
。
這個限制不會對物件字面量宣告有多少影響,因為大部分情況下我們沒有理由不用速記方法定義,畢竟它語法更簡單。
4. 計算屬性名稱
在 ES2015 之前,物件初始化器的屬性名稱是字面量,大多數情況下是靜態字串。要建立一個動態計算的屬性名,你不得不使用屬性訪問器:
1 2 3 4 5 6 7 |
function prefix(prefStr, name) { return prefStr + '_' + name; } var object = {}; object[prefix('number', 'pi')] = 3.14; object[prefix('bool', 'false')] = false; object; // => { number_pi: 3.14, bool_false: false } |
當然,這種定義屬性的方法差強人意。
計算屬性名稱能更優雅地解決這個問題。
當我們從表示式計算屬性名稱,將程式碼放在方括號之間 {[expression]: value}
。這個表示式計算結果將成為屬性名。
我真的很喜歡這個語法:短而簡單。
讓我們改進上面的程式碼:
1 2 3 4 5 6 7 8 |
function prefix(prefStr, name) { return prefStr + '_' + name; } var object = { [prefix('number', 'pi')]: 3.14, [prefix('bool', 'false')]: false }; object; // => { number_pi: 3.14, bool_false: false } |
[prefix('number', 'pi')]
通過計算 prefix('number', 'pi')
表示式來設定屬性名稱, 得到的結果是 'number_pi'
。
相應地, [prefix('bool', 'false')]
將第二個屬性名稱設為 'bool_false'
。
4.1 Symbol
作為屬性名
Symbols 也可以被用來計算屬性名稱,只需要將它包含在方括號中:{ [Symbol('name')]: 'Prop value' }
。
例如,使用特殊屬性 Symbol.iterator
來迭代遍歷一個物件的自有屬性名,程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var object = { number1: 14, number2: 15, string1: 'hello', string2: 'world', [Symbol.iterator]: function *() { var own = Object.getOwnPropertyNames(this), prop; while(prop = own.pop()) { yield prop; } } } [...object]; // => ['number1', 'number2', 'string1', 'string2'] |
[Symbol.iterator]: function *() { }
定義一個屬性來用於迭代遍歷物件自有屬性。展開操作符 [...object]
使用迭代器並返回自有屬性列表。
5. 展望未來:rest 和屬性展開
物件字面量的 Rest 和屬性展開 是新的標準草案中的一個提案(stage 2),意味著這一特性是新版本 JavaScript 的規範的候選。
陣列的 展開和 rest 操作符 已經被實現了。
Rest 屬性 允許我們從解構賦值左側使用物件來收集屬性,看下面的例子:
1 2 3 4 5 6 7 8 |
var object = { propA: 1, propB: 2, propC: 3 }; let {propA, ...restObject} = object; propA; // => 1 restObject; // => { propB: 2, propC: 3 } |
屬性展開 允許將源物件的自有屬性拷進物件字面量內部。在上面的例子中,物件字面量從souce
物件中收集額外的屬性。
1 2 3 4 5 6 7 8 9 |
var source = { propB: 2, propC: 3 }; var object = { propA: 1, ...source } object; // => { propA: 1, propB: 2, propC: 3 } |
6. 結論
JavaScript 在快速進步。
即使是相對小的結構比如物件字面量在 ES2015 中都有相當大的改進,更別說還有一大堆新特性在草案中。
你可以從初始化器中使用 __proto__
屬性直接設定物件的原型,這比使用Object.create()
要更方便。
方法宣告可以寫成更簡短的形式,這樣你就不用寫 function
關鍵字了。然後在速記方法中可以使用 super
關鍵字,它能提供方便的對被繼承原型鏈上屬性的訪問。
如果一個屬性名在執行時計算,現在你可以使用計算屬性名稱 [表示式]
來初始化物件。
的確,物件字面量現在很酷!