【译】《Understanding ECMAScript6》- 第三章-Object
目錄
- Object分類
- Object字面量擴展
- Object.assign()
- 重復屬性
- 改變原型
- super引用
- 方法
- 總結
ES6針對Object的改進,旨在使JavaScript語言更加接近“萬物皆對象”的理念。隨著越來越多地使用Object類型進行開發,開發者們越來越不滿足于Object相對低下的開發效率。
ES6通過多種途徑對Object進行了改進,包括語法的調整、以及新的操作和交互方式等。
Object分類
JavaScript中的Object有很多不同的類別,比如自定義的對象和語言內置的對象,很容易產生混淆。為了更精確地區分不同類別的對象,ES6引入了幾個新的術語,這些術語將Object的類別具體為以下幾種;
- 普通對象(Ordinary objects)是指具備JavaScript對象所有默認行為的對象;
- 怪異對象(Exotic objects)是指某些行為與默認行為不同的對象;
- 標準對象(Standard objects)是指由ES6語言規范定義的對象,比如Array、Date等。標準對象可以是普通對象,也可以是怪異對象;
- 內置對象(Built-in objects)是指JavaScript運行環境定義的對象。所有的標準對象都是內置對象。
本書將在后續的內容中詳細講述每種對象的具體細節。
Object字面量擴展
Object字面量表達式被廣泛使用于JavaScript程序中,幾乎所有的JavaScript應用程序中都可以找到這種模式。Object字面量有著類似JSON的簡潔語法,這是它之所以如此流行的主要原因。ES6對Object字面量語法進行了擴展,保持語法簡潔的前提下,也增強了功能性。
屬性初始化的縮寫模式
在ES5及其之前的版本中,Object字面量必須寫成鍵值對的格式,這就意味著,在某些場景下會產生一些重復的語句,如下:
function createPerson(name, age) {return {name: name,age: age}; }上述代碼中的createPerson()函數創建了一個對象,這個對象的屬性值與createPerson()的實參值相同。這會令許多開發者誤認為對象的值是createPerson()實參的副本。
ES6中新增了Object字面量的簡潔聲明語法,可以一定程度上消除以上誤解。如果對象的某個屬性與一個本地變量同名,就可以在聲明對象時只寫這個屬性的key,省略冒號和value。如下:
function createPerson(name, age) {return {name,age}; }如果Object字面量的某個屬性只有key沒有value,JavaScript引擎便會在當前作用域內搜索是否存在與key同名的變量。如果存在,則將同名變量的值賦值為key對應的value。上述代碼中的name屬性對應的value就是本地變量name的值。
ES6新增這種機制的目的是令Object字面量語法更加簡潔化。使用本地變量的值作為對象屬性的value是一種很常見的模式,初始化屬性的縮寫模式可以令代碼更加簡潔。
函數初始化的縮寫模式
ES6同樣精簡了對象內函數的聲明語法。在ES6之前,開發者必須按照鍵值對的格式聲明對象內的函數,如下:
var person = {name: "Nicholas",sayName: function() {console.log(this.name);} };ES6中,可以省略冒號和function關鍵字,如下:
var person = {name: "Nicholas",sayName() {console.log(this.name);} };使用上述代碼中的縮寫模式聲明的函數與上例中的作用完全相同。
計算屬性名
JavaScript允許使用方括號計算對象的屬性名,一方面令對象屬性的操作更加動態化,另一方面避免了不能使用.直接訪問的屬性名引起的語法錯誤。如下:
var person = {},lastName = "last name"; person["first name"] = "Nicholas"; person[lastName] = "Zakas"; console.log(person["first name"]); // "Nicholas" console.log(person[lastName]); // "Zakas"上述代碼中的person對象的兩個屬性名first name和last name都包含空格,無法直接使用.訪問,只能通過方括號訪問。方括號內可以包括字符串和變量。
ES5中可以使用字符串作為對象的屬性名:
var person = {"first name": "Nicholas" }; console.log(person["first name"]); // "Nicholas"使用字符串作為對象屬性名的前提是在聲明之前必須明確知道此字符串的值。如果此字符串并非固定值,比如需要根據變量值動態計算,這種場景下便不能使用上述的聲明方式了。
為滿足以上需求,ES6將方括號計算屬性名的機制引入了Object字面量,如下:
var lastName = "last name"; var person = {"first name": "Nicholas",[lastName]: "Zakas" }; console.log(person["first name"]); // "Nicholas" console.log(person[lastName]); // "Zakas"上述代碼中,Object字面量內的方括號的作用是計算對象的屬性名,其內部為字符串運算。你同樣可以使用以下模式:
var suffix = " name"; var person = {["first" + suffix]: "Nicholas",["last" + suffix]: "Zakas" }; console.log(person["first name"]); // "Nicholas" console.log(person["last name"]); // "Zakas"ES6中,方括號不僅可以在訪問對象屬性時計算屬性名,同樣可以在對象聲明時計算屬性名。
Object.assign()
mixin是組合對象常用的模式之一,本質是將一個對象的屬性鍵值對克隆給另一個對象。很多JavaScript類庫有類似如下的mixin函數:
function mixin(receiver, supplier) {Object.keys(supplier).forEach(function(key) {receiver[key] = supplier[key];});return receiver; }上述代碼中的mixin()函數將supplier對象的自有屬性(不包括原型鏈屬性)克隆賦值給receiver對象。這種方式可以不通過繼承實現receiver屬性的擴展。請看如下示例:
function EventTarget() { /*...*/ } EventTarget.prototype = {constructor: EventTarget,emit: function() { /*...*/ },on: function() { /*...*/ } }; var myObject = {}; mixin(myObject, EventTarget.prototype); myObject.emit("somethingChanged");上述代碼中,myObject對象通過克隆EventTarget.prototype的屬性獲取到了打印事件以及使用emit()和on()函數的功能。
ES6新增的Object.assign()進一步加強了這種模式,并且更加語義化。上文提到的mixin()函數使用賦值運算符=進行屬性克隆,這樣的缺點是無法處理對象的存儲器屬性(后續章節詳細講述)。Object.assign()解決了這一問題。
不同的JavaScript類庫實現mixin模式的函數取名迥異,其中extend()和mix()是使用面很廣泛的函數名。ES6初期除了Object.assign()以外,還曾引入了Object.mixin()方法。Object.mixin()可以克隆對象的存儲器屬性,但是由于super的引入(后續章節詳細講述),最終取消了Object.mixin()的使用。
Object.assign()可以取代上文提到的mixin()函數:
function EventTarget() { /*...*/ } EventTarget.prototype = {constructor: EventTarget,emit: function() { /*...*/ },on: function() { /*...*/ } } var myObject = {} Object.assign(myObject, EventTarget.prototype); myObject.emit("somethingChanged");Object.assign()可以接受任意數目的克隆源對象,克隆目標對象按照克隆源對象的順序依次克隆。也就是說,隊列后面的源對象屬性會覆蓋它前面的源對象同名屬性。如下:
var receiver = {}; Object.assign(receiver, {type: "js",name: "file.js"}, {type: "css"} ); console.log(receiver.type); // "css" console.log(receiver.name); // "file.js"上述代碼最終的receiver.type值為css,因為第二個源對象覆蓋了第一個源對象的同名屬性。
Object.assign()對于ES6來說,并不是一個革命性的功能,但是它規范了mixin模式,而不必依賴于第三方類庫。
存儲器屬性的處理
mixin模式下存儲器屬性是不能被完全克隆的,Object.assign()本質上是通過賦值運算符克隆屬性,在處理存儲器屬性時,將源對象的存儲器屬性的運算結果克隆至目標對象。如下:
var receiver = {},supplier = {get name() {return "file.js"}}; Object.assign(receiver, supplier); var descriptor = Object.getOwnPropertyDescriptor(receiver, "name"); console.log(descriptor.value); // "file.js" console.log(descriptor.get); // undefined上述代碼中的源對象supplier有一個存儲器屬性name。使用Object.assign()之后,receiver被賦予一個值為"filter.js"的常規屬性receiver.name。這是由于 supplier.name的運算結果為"filter.js",Object.assign()將運算結果克隆為receiver的一個常規屬性。
重復屬性
ES5嚴格模式下不允許Object字面量存在key值重復的屬性,比如:
var person = {name: "Nicholas",name: "Greg" // syntax error in ES5 strict mode };上述代碼在ES5嚴格模式下會拋出語法錯誤。
ES6移除了重復屬性的語法錯誤。不論是在非嚴格模式還是嚴格模式下,上例中的代碼都不會拋錯,而且后面的name屬性值將覆蓋前面的值。
var person = {name: "Nicholas",name: "Greg" // not an error in ES6 }; console.log(person.name); // "Greg"上述代碼person.name的取值為"Greg",因為name屬性取的是最后一次賦值。
改變原型
原型是JavaScript實現繼承的基礎,ES6進一步加強了原型的作用。ES5中提供Object.getPrototypeOf()函數用來獲取指定對象的原型。ES6引入了其逆向操作函數Object.setPrototypeOf()用來改變指定對象的原型。
不論是使用構造函數還是通過Object.create()創建的對象,它們的原型在創建的時候就被指定了。在ES6之前,并沒有規范的方法改變對象的原型。Object.setPrototypeOf()打破了對象被創建后不能更改原型的規范,在此意義上,可以說Object.setPrototypeOf()是革命性的。
Object.setPrototypeOf()接收兩個參數,第一個參數是被更改原型的對象,第二個參數是第一個參數被更改后的原型。如下:
let person = {getGreeting() {return "Hello";} }; let dog = {getGreeting() {return "Woof";} }; // prototype is person let friend = Object.create(person); console.log(friend.getGreeting()); // "Hello" console.log(Object.getPrototypeOf(friend) === person); // true // set prototype to dog Object.setPrototypeOf(friend, dog); console.log(friend.getGreeting()); // "Woof" console.log(Object.getPrototypeOf(friend) === dog); // true上述代碼中定義了兩個基礎對象:person和dog。兩者都有一個getGreeting()函數。對象friend創建時繼承自person,此時friend.getGreeting()的運算結果為”Hello“。然后通過Object.setPrototypeOf()函數將friend對象的原型更改為dog,此時friend.getGreeting()的運算結果為”woof“。
一個對象的原型儲存在內部隱藏屬性[[Prototype]]中。Object.getPrototypeOf()函數返回[[Prototype]]的值,Object.setPrototypeOf()則是將[[Prototype]]的值改變為指定的value。除了上述兩個函數以外,還有一些其他途徑對[[Prototype]]進行操作。
在ES5之前就已經有少數JavaScript引擎實現了通過操作__proto__屬性來獲取和更改對象原型的方法。可以說__proto__是Object.getPrototypeOf()和Object.setPrototypeOf()的先行者。但是并非所有的JavaScript引擎都支持__proto__,所以ES6對此進行了規范。
在ES6中,Object.prototype.__proto__作為一個存儲器屬性,它的get方法為Object.getPrototypeOf(),set方法為Object.setPrototypeOf()。所以,使用Object.getPrototypeOf()和Object.setPrototypeOf()函數與直接操作__proto__本質上是相同的。如下:
let person = {getGreeting() {return "Hello";} }; let dog = {getGreeting() {return "Woof";} }; // prototype is person let friend = {__proto__: person }; console.log(friend.getGreeting()); // "Hello" console.log(Object.getPrototypeOf(friend) === person); // true console.log(friend.__proto__ === person); // true // set prototype to dog friend.__proto__ = dog; console.log(friend.getGreeting()); // "Woof" console.log(friend.__proto__ === dog); // true console.log(Object.getPrototypeOf(friend) === dog); // true上述代碼與前例的功能相同。唯一的區別是用Object字面量配合__proto__屬性取代了Object.create()。
__proto__屬性有以下特性:
使用Object字面量聲明時,__proto__屬性只能被賦值一次。重復賦值會引起錯誤。__proto__是ES6中Object字面量中唯一有次限制的屬性。
使用["__proto__"]訪問對象屬性時,方括號內的字符串只能作為一個常規的屬性key,并不能操作__proto__屬性。它是不適用于方括號計算對象屬性key規則的少數屬性之一。
操作__proto__屬性時要時刻謹記上述規則。
super引用
如前文所述,原型是JavaScript中非常重要的環節,ES6針對原型新增了很多強化功能,super引用便是其中之一。舉個例子,在ES5環境下,如果想復寫一個對象原型的同名函數,你可能會選擇類似下述代碼的方式:
let person = {getGreeting() {return "Hello";} }; let dog = {getGreeting() {return "Woof";} }; // prototype is person let friend = {__proto__: person,getGreeting() {// same as this.__proto__.getGreeting.call(this)return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";} }; console.log(friend.getGreeting()); // "Hello, hi!" console.log(Object.getPrototypeOf(friend) === person); // true console.log(friend.__proto__ === person); // true // set prototype to dog friend.__proto__ = dog; console.log(friend.getGreeting()); // "Woof, hi!" console.log(friend.__proto__ === dog); // true console.log(Object.getPrototypeOf(friend) === dog); // true上述代碼中,friend有一個與原型鏈中重名的getGreeting()。通過Object.getPrototypeOf()調用其原型的同名函數后追加字符串", hi!"。.call(this)確保原型函數中的作用域為friend。
需要注意的是,Object.getPrototypeOf()與.call(this)必須配合使用。遺漏任何一方都可能引起問題。因此,ES6引入了super以簡化這種操作。
簡單來講,super可以理解為一個指向當前對象原型的指針,等價于Object.getPrototypeOf(this)。所以,前例中的代碼可以改寫為以下形式:
let friend = {__proto__: person,getGreeting() {// in the previous example, this is the same as:// 1. Object.getPrototypeOf(this).getGreeting.call(this)// 2. this.__proto__.getGreeting.call(this)return super.getGreeting() + ", hi!";} };上述代碼中的super.getGreeting()等價于Object.getPrototypeOf(this).getGreeting.call(this) 或者this.__proto__.getGreeting.call(this)。
super的功能并不僅限于此。比如在多重繼承的場景下,Object.getPrototypeOf()并不能滿足需求,如下:
let person = {getGreeting() {return "Hello";} };// prototype is person let friend = {__proto__: person,getGreeting() {return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";} };// prototype is friend let relative = {__proto__: friend };console.log(person.getGreeting()); // "Hello" console.log(friend.getGreeting()); // "Hello, hi!" console.log(relative.getGreeting()); // error!上述代碼中,執行relative.getGreeting()時Object.getPrototypeOf()會報錯。因為此時的this指向的是relative,relative的原型為friend。所以當friend.getGreeting().call()的this被指定為relative時相當于執行relative.getGreeting(),形成了無限遞歸的死循環,直到堆棧溢出。使用super可以很輕易的化解這種問題。如下:
let person = {getGreeting() {return "Hello";} }; // prototype is person let friend = {__proto__: person,getGreeting() {return super.getGreeting() + ", hi!";} }; // prototype is friend let relative = {__proto__: friend }; console.log(person.getGreeting()); // "Hello" console.log(friend.getGreeting()); // "Hello, hi!" console.log(relative.getGreeting()); // "Hello, hi!"super的指向是固定的。不論有多少層繼承關系,super.getGreeting()永遠指向person.getGreeting()。
super只能在對象方法中使用,不能在常規函數和全局作用域內使用,否則會拋出語法錯誤。
方法
在ES6之前的版本中,方法并沒有準確的定義。通常認為方法是一種函數類型的對象屬性。ES6正式規范了方法的定義,作為方法的函數有一個內部屬性[[HomeObject]]表明方法的歸屬對象:
let person = {// 方法getGreeting() {return "Hello";} }; // 不是方法 function shareGreeting() {return "Hi!"; }上述代碼中,person對象有一個方法getGreeting()。getGreeting()的內部屬性[[HomeObject]]值為person,表明它的歸屬對象是person。shareGreeting()是一個函數,它沒有[[HomeObject]]屬性,因為它不是任何對象的方法。大多數場景下,方法與函數的區別并不是很重要,但是使用super時需要謹慎處理兩者的異同。
super引用是根據[[HomeObject]]屬性來決定其指向的。其內部機制如下:
如果一個函數沒有[[HomeObject]]屬性或者屬性值是錯誤的,以上的機制就無法運行。如下:
let person = {getGreeting() {return "Hello";} }; // prototype is person let friend = {__proto__: person,getGreeting() {return super() + ", hi!";} }; function getGlobalGreeting() {return super.getGreeting() + ", yo!"; } console.log(friend.getGreeting()); // "Hello, hi!" getGlobalGreeting(); // throws error上述代碼中的friend.getGreeting()返回了正確結果,而getGlobalGreeting()運行產生了錯誤,因為super并不能在常規函數內使用。由于getGlobalGreeting()函數不存在[[HomeObject]]屬性,所以不能通過super向上檢索。即便getGlobalGreeting()函數被動態的賦值給對象的方法,它仍然不能使用super。如下:
// prototype is person let friend = {__proto__: person,getGreeting() {return super() + ", hi!";} }; function getGlobalGreeting() {return super.getGreeting() + ", yo!"; } console.log(friend.getGreeting()); // "Hello, hi!" // assign getGreeting to the global function friend.getGreeting = getGlobalGreeting; friend.getGreeting(); // throws error上述代碼中,全局函數getGlobalGreeting()被動態地賦值給friend的getGreeting()方法。隨后調用friend.getGreeting()產生和前例相同的錯誤。這是因為[[HomeObject]]的屬性值在函數/方法被創建的時候就固定了,隨后不能被改變。
總結
Object是JavaScript語言中至關重要的模塊,ES6在簡化操作和強化功能方面進行了許多改進。
在Object字面量方面,屬性初始化的縮寫模式可以更加簡潔地通過當前作用域的同名變量進行賦值;計算屬性名為對象擴展屬性提供更多的動態化支持;函數初始化的縮寫模式簡化了對象方法的聲明語法;屬性重復聲明在ES6嚴格和非嚴格模式下都不會報錯。
Object.assign()函數可以進行對象多重屬性的克隆,統一mixin模式的操作流程。
Object.setPrototypeOf()函數可以更改對象的原型。ES6規范了__proto__ 屬性,作為一個存儲器屬性,它的get方法為Object.getPrototypeOf(),set方法為Object.setPrototypeOf()。
super引用永遠指向當前對象的原型。super可以作為函數使用,比如super(),也可以作為指針使用,比如super.getGreeting()。不論哪種形式,super調用的內部this永遠指向當前的作用域。
轉載于:https://www.cnblogs.com/ihardcoder/articles/5293390.html
總結
以上是生活随笔為你收集整理的【译】《Understanding ECMAScript6》- 第三章-Object的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java学习(eclipse环境的使用)
- 下一篇: 【leetcode】Search for