[1]對象類別 [2]對象簡寫 [3]可計算屬性名 [4]判斷相等 [5]對象合併 [6]屬性名重覆 [7]枚舉順序 [8]對象原型 [9]方法定義 [10]對象遍歷 ...
前面的話
隨著JS應用複雜度的不斷增加,開發者在程式中使用對象的數量也在持續增長,因此對象使用效率的提升就變得至關重要。ES6通過多種方式來加強對象的使用,通過簡單的語法擴展,提供更多操作對象及與對象交互的方法。本章將詳細介紹ES6對象擴展
對象類別
在瀏覽器這樣的執行環境中,對象沒有統一的標準,在標準中又使用不同的術語描述對象,ES6規範清晰定義了每一個類別的對象,對象的類別如下
1、普通(Ordinary)對象
具有JS對象所有的預設內部行為
2、特異(Exotic)對象
具有某些與預設行為不符的內部行為
3、標準(Standard)對象
ES6規範中定義的對象,例如,Array、Date等。標準對象既可以是普通對象,也可以是特異對象
4、內建對象
腳本開始執行時存在於JS執行環境中的對象,所有標準對象都是內建對象
對象簡寫
【屬性初始值簡寫】
在ES5中,對象字面量只是簡單的鍵值對集合,這意味著初始化屬性值時會有一些重覆
function createPerson(name, age) { return { name: name, age: age }; }
這段代碼中的createPerson()函數創建了一個對象,其屬性名稱與函數的參數相同,在返回的結果中,name和age分別重覆了兩遍,只是其中一個是對象屬性的名稱,另外一個是為屬性賦值的變數
在ES6中,通過使用屬性初始化的簡寫語法,可以消除這種屬性名稱與局部變數之間的重覆書寫。當一個對象的屬性與本地變數同名時,不必再寫冒號和值,簡單地只寫屬性名即可
function createPerson(name, age) { return { name, age }; }
當對象字面量里只有一個屬性的名稱時,JS引擎會在可訪問作用域中查找其同名變數;如果找到,則該變數的值被賦給對象字面量里的同名屬性。在本示例中,對象字面量屬性name被賦予了局部變數name的值
在JS中,為對象字面量的屬性賦同名局部變數的值是一種常見的做法,這種簡寫方法有助於消除命名錯誤
【對象方法簡寫】
在ES5中,如果為對象添加方法,必須通過指定名稱並完整定義函數來實現
var person = { name: "Nicholas", sayName: function() { console.log(this.name); } };
而在ES6中,語法更簡潔,消除了冒號和function關鍵字
var person = { name: "Nicholas", sayName() { console.log(this.name); } };
在這個示例中,通過對象方法簡寫語法,在person對象中創建一個sayName()方法,該屬性被賦值為一個匿名函數表達式,它擁有在ES5中定義的對象方法所具有的全部特性
二者唯一的區別是,簡寫方法可以使用super關鍵字,而普通方法不可以
[註意]通過對象方法簡寫語法創建的方法有一個name屬性,其值為小括弧前的名稱
可計算屬性名
在ES5版本中,如果想要通過計算得到屬性名,就需要用方括弧代替點記法
var person = {}, lastName = "last name"; person["first name"] = "huochai"; person[lastName] = "match"; console.log(person["first name"]); // "huochai" console.log(person[lastName]); // "match"
變數lastName被賦值為字元串"last name",引用的兩個屬性名稱中都含有空格,因而不可使用點記法引用這些屬性,卻可以使用方括弧,因為它支持通過任何字元串值作為名稱訪問屬性的值。此外,在對象字面量中,可以直接使用字元串字面量作為屬性名稱
var person = { "first name": "huochai" }; console.log(person["first name"]); // "huochai"
這種模式適用於屬性名提前已知或可被字元串字面量表示的情況。然而,如果屬性名稱"first name"被包含在一個變數中,或者需要通過計算才能得到該變數的值,那麼在ES5中是無法為一個對象字面量定義該屬性的
在ES6中,可在對象字面量中使用可計算屬性名稱,其語法與引用對象實例的可計算屬性名稱相同,也是使用方括弧
var lastName = "last name"; var person = { "first name": "huochai", [lastName]: "match" }; console.log(person["first name"]); // "huochai" console.log(person[lastName]); // "match"
在對象字面量中使用方括弧表示的該屬性名稱是可計算的,它的內容將被名稱求值並被最終轉化為一個字元串,因而同樣可以使用表達式作為屬性的可計算名稱
var suffix = " name"; var person = { ["first" + suffix]: "huochai", ["last" + suffix]: "match" }; console.log(person["first name"]); // "huochai" console.log(person["last name"]); // "match"
這些屬性被求值後為字元串"first name"和"last name",然後它們可用於屬性引用。任何可用於對象實例括弧記法的屬性名,也可以作為字面量中的計算屬性名
判斷相等
【Object.is()】
在JS中比較兩個值時,可能習慣於使用相等運算符(==)或全等運算符(===),使用後者可以避免觸發強制類型轉換的行為。但是,即使使用全等運算符也不完全準確
console.log(+0 === -0);//true console.log(NaN === NaN);//false
ES6引入了Object.is()方法來彌補全等運算符的不准確運算。這個方法接受兩個參數,如果這兩個參數類型相等且具有相同的值,則返回true,否則返回false
console.log(+0 == -0); // true console.log(+0 === -0); // true console.log(Object.is(+0, -0)); // false console.log(NaN == NaN); // false console.log(NaN === NaN); // false console.log(Object.is(NaN, NaN)); // true console.log(5 == 5); // true console.log(5 == "5"); // true console.log(5 === 5); // true console.log(5 === "5"); // false console.log(Object.is(5, 5)); // true console.log(Object.is(5, "5")); // false
對於Object.is()方法來說,其運行結果在大部分情況中與"==="運算符相同,唯一的區別在於+0和-0被識別為不相等並且NaN與NaN等價。但是大可不必拋棄等號運算符,是否選擇用Object.is()方法而不是==或===取決於那些特殊情況如何影響代碼
對象合併
【Object.assign()】
混合(Mixin)是JS實現對象組合最流行的一種模式。在一個mixin方法中,一個對象接收來自另一個對象的屬性和方法,許多JS庫中都有類似的minix方法
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對象的所有行為,從而使myObject可以分別通過emit()方法發佈事件或通過on()方法訂閱事件
這種混合模式非常流行,因而ES6添加了object.assign()方法來實現相同的功能,這個方法接受一個接收對象和任意數量的源對象,最終返回接收對象
function EventTarget() { /*...*/ } EventTarget.prototype = { constructor: EventTarget, emit: function() { /*...*/ }, on: function() { /*...*/ } } var myObject = {} Object.assign(myObject, EventTarget.prototype); myObject.emit("somethingChanged");
【對象合併】
Object.assign()方法不叫對象複製,或對象拷貝,而叫對象合併,是因為源對象本身的屬性和方法仍然存在
var target = { a: 1 };
var source1 = { b: 2 };
var source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
Object.assign()方法可以接受任意數量的源對象,並按指定的順序將屬性複製到接收對象中。如果目標對象與源對象有同名屬性,或多個源對象有同名屬性,則後面的屬性會覆蓋前面的屬性
var target = { a: 1, b: 1 }; var source1 = { b: 2, c: 2 }; var source2 = { c: 3 }; Object.assign(target, source1, source2); target // {a:1, b:2, c:3}
【淺拷貝】
在對象合併的過程中,Object.assign()
拷貝的屬性是有限制的,只拷貝源對象的自身屬性(不拷貝繼承屬性),也不拷貝不可枚舉的屬性(enumerable: false
)
Object.assign({b: 'c'}, Object.defineProperty({}, 'invisible', { enumerable: false, value: 'hello' }) ) // { b: 'c' }
Object.assign()
方法實行的是淺拷貝,而不是深拷貝。也就是說,如果源對象某個屬性的值是對象,那麼目標對象拷貝得到的是這個對象的引用
var obj1 = {a: {b: 1}}; var obj2 = Object.assign({}, obj1); obj1.a.b = 2; obj2.a.b // 2
屬性名重覆
ES5嚴格模式中加入了對象字面量重覆屬性的校驗,當同時存在多個同名屬性時會拋出錯誤
"use strict"; var person = { name: "huochai", name: "match" // 在 ES5 嚴格模式中是語法錯誤 };
當運行在ES5嚴格模式下時,第二個name屬性會觸發二個語法錯誤
但在ES6中,重覆屬性檢查被移除了,無論是在嚴格模式還是非嚴格模式下,代碼不再檢查重覆屬性,對於每一組重覆屬性,都會選取最後一個取值
"use strict"; var person = { name: "huochai", name: "match" }; console.log(person.name); // "match"
在這個示例中,屬性person.name取最後一次賦值"match"
枚舉順序
ES5中未定義對象屬性的枚舉順序,由JS引擎廠商自行決定。然而,ES6嚴格規定了對象的自有屬性被枚舉時的返回順序,這會影響到Object.getOwnPropertyNames()方法及Reflect.ownKeys返回屬性的方式,Object.assign()方法處理屬性的順序也將隨之改變
自有屬性枚舉順序的基本規則是
1、所有數字鍵按升序排序
2、所有字元串鍵按照它們被加入對象的順序排序
3、所有symbol鍵按照它們被加入對象的順序排序
var obj = { a: 1, 0: 1, c: 1, 2: 1, b: 1, 1: 1 }; obj.d = 1; console.log(Object.getOwnPropertyNames(obj).join("")); // "012acbd"
Object.getOwnPropertyNames()方法按照0、1、2、a、c、b、d的順序依次返回對象obj中定義的屬性。對於數值鍵,儘管在對象字面量中的順序是隨意的,但在枚舉時會被重新組合和排序。字元串鍵緊隨數值鍵,並按照在對象obj中定義的順序依次返回,所以隨後動態加入的字元串鍵最後輸出
[註意]對於for-in迴圈,由於並非所有廠商都遵循相同的實現方式,因此仍未指定一個明確的枚舉順序而Object.keys()方法和JSON.stringify()方法都指明與for-in使用相同的枚舉順序,因此它們的枚舉順序目前也不明晰
對於JS,枚舉順序的改變其實微不足道,但是有很多程式都需要明確指定枚舉順序才能正確運行。ES6中通過明確定義枚舉順序,確保用到枚舉的代碼無論處於何處都可以正確地執行
對象原型
原型是JS繼承的基礎,在早期版本中,JS嚴重限制了原型的使用。隨著語言逐漸成熟,開發者們也更加熟悉原型的運行方式,他們希望獲得更多對於原型的控制力,並以更簡單的方式來操作原型。於是,ES6針對原型進行了改進
【__proto__】
__proto__
屬性(前後各兩個下劃線),用來讀取或設置當前對象的prototype
對象。目前,所有瀏覽器(包括IE11)都部署了這個屬性
// es6的寫法 var obj = { method: function() { ... } }; obj.__proto__ = someOtherObj; // es5的寫法 var obj = Object.create(someOtherObj); obj.method = function() { ... };
標準明確規定,只有瀏覽器必須部署這個屬性,其他運行環境不一定需要部署,而且新的代碼最好認為這個屬性是不存在的。因此,無論從語義的角度,還是從相容性的角度,都不要使用這個屬性,而是使用下麵的Object.setPrototypeOf()
(寫操作)、Object.getPrototypeOf()
(讀操作)、Object.create()
(生成操作)代替
【Object.getPrototypeOf()】
該方法與Object.setPrototypeOf()
方法配套,用於讀取一個對象的原型對象
Object.getPrototypeOf(obj);
【Object.setPrototypeOf()】
ES6添加了Object.setPrototypeOf()方法,與__proto__作用相同,通過這個方法可以改變任意指定對象的原型,它接受兩個參數:被改變原型的對象及替代第一個參數原型的對象,它是ES6正式推薦的設置原型對象的方法
// 格式 Object.setPrototypeOf(object, prototype) // 用法 var o = Object.setPrototypeOf({}, null);
例子如下
let person = { getGreeting() { return "Hello"; } }; let dog = { getGreeting() { return "Woof"; } }; // 原型為 person let friend = Object.create(person); console.log(friend.getGreeting()); // "Hello" console.log(Object.getPrototypeOf(friend) === person); // true // 將原型設置為 dog Object.setPrototypeOf(friend, dog); console.log(friend.getGreeting()); // "Woof" console.log(Object.getPrototypeOf(friend) === dog); // true
這段代碼中定義了兩個基對象:person和dog。二者都有getGreeting()方法,且都返回一個字元串。friend對象先繼承person對象,調用getGreeting()方法輸出"Hello";當原型被變更為dog對象時,原先與person對象的關聯被解除,調用person.getGreeting()方法時輸出的內容就變為了"Woof"
對象原型的真實值被儲存在內部專用屬性[[protơtype]]中,調用Object.getPrototypeOf()方法返回儲存在其中的值,調用Object.setPrototypeOf()方法改變其中的值。然而,這不是操作[[prototype]]值的唯一方法
【簡化原型訪問的Super引用】
ES6引入了Super引用,使用它可以更便捷地訪問對象原型
如果想重寫對象實例的方法,又需要調用與它同名的原型方法,則在ES5中可以這樣實現
let person = { getGreeting() { return "Hello"; } }; let dog = { getGreeting() { return "Woof"; } }; let friend = { getGreeting() { return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!"; } }; // 將原型設置為 person Object.setPrototypeOf(friend, person); console.log(friend.getGreeting()); // "Hello, hi!" console.log(Object.getPrototypeOf(friend) === person); // true // 將原型設置為 dog Object.setPrototypeOf(friend, dog); console.log(friend.getGreeting()); // "Woof, hi!" console.log(Object.getPrototypeOf(friend) === dog); // true
在這個示例中,friend對象的getGreeting()方法調用了同名的原型方法。object.getPrototypeOf()方法可以確保調用正確的原型,並向輸出字元串疊加另一個字元串;後面的.call(this)可以確保正確設置原型方法中的this值
要準確記得如何使用Object.getPrototypeOf()方法和call(this)方法來調用原型上的方法實在有些複雜,所以ES6引入了Super關鍵字。簡單來說,Super引用相當於指向對象原型的指針,實際上也就是Object.getPrototypeOf(this)的值。於是,可以這樣簡化上面的getGreeting()方法
let friend = { getGreeting() { // 這相當於上個例子中的: // Object.getPrototypeOf(this).getGreeting.call(this) return super.getGreeting() + ", hi!"; } };
調用super.getGreeting()方法相當於在當前上下文中調用Object.getPrototypeOf(this).getGreeting.call(this)。同樣,可以通過Super引用調用對象原型上所有其他的方法。當然,必須要在使用簡寫方法的對象中使用Super引用,如果在其他方法聲明中使用會導致語法錯誤
let friend = { getGreeting: function() { // 語法錯誤 return super.getGreeting() + ", hi!"; } };
在這個示例中用匿名function定義一個屬性,由於在當前上下文中Super引用是非法的,因此當調用super.getGreeting()方法時會拋出語法錯誤
Super引用在多重繼承情況下非常有用,因為在這種情況下,使用Object.getPrototypeOf()方法將會出現問題
let person = { getGreeting() { return "Hello"; } }; // 原型為 person let friend = { getGreeting() { return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!"; } }; Object.setPrototypeOf(friend, person); // 原型為 friend let relative = Object.create(friend); console.log(person.getGreeting()); // "Hello" console.log(friend.getGreeting()); // "Hello, hi!" console.log(relative.getGreeting()); // error!
this是relative,relative的原型是friend對象,當執行relative的getGreeting()方法時,會調用friend的getGreeting()方法,而此時的this值為relative。object.getPrototypeOf(this)又會返回friend對象。所以就會進入遞歸調用直到觸發棧溢出報錯
在ES5中很難解決這個問題,但在ES6中,使用Super引用便可以迎刃而解
let person = { getGreeting() { return "Hello"; } }; // 原型為 person let friend = { getGreeting() { return super.getGreeting() + ", hi!"; } }; Object.setPrototypeOf(friend, person); // 原型為 friend let relative = Object.create(friend); console.log(person.getGreeting()); // "Hello" console.log(friend.getGreeting()); // "Hello, hi!" console.log(relative.getGreeting()); // "Hello, hi!"
Super引用不是動態變化的,它總是指向正確的對象,在這個示例中,無論有多少其他方法繼承了getGreeting()方法,super.getGreeting()始終指向person.getGreeting()方法
方法定義
在ES6以前從未正式定義過"方法"的概念,方法僅僅是一個具有功能而非數據的對象屬性。而在ES6中正式將方法定義為一個函數,它會有一個內部的[[HomeObject]]屬性來容納這個方法從屬的對象
let person = { // 方法 getGreeting() { return "Hello"; } }; // 並非方法 function shareGreeting() { return "Hi!"; }
這個示例中定義了person對象,它有一個getGreeting()方法,由於直接把函數賦值給了person對象,因而getGreetingo方法的[[HomeObject]]屬性值為person。而創建shareGreeting()函數時,由於未將其賦值給一個對象,因而該方法沒有明確定義[[HomeObject]]屬性。在大多數情況下這點小差別無關緊要,但是當使用Super引用時就變得非常重要了
Super的所有引用都通過[[HomeObject]]屬性來確定後續運行過程。第一步是在[[HomeObject]]屬性上調用Object.getprototypeof()方法來檢索原型的引用,然後搜尋原型找到同名函數,最後設置this綁定並且調用相應方法
let person = { getGreeting() { return "Hello"; } }; // 原型為 person let friend = { getGreeting() { return super.getGreeting() + ", hi!"; } }; Object.setPrototypeOf(friend, person); console.log(friend.getGreeting()); // "Hello, hi!"
調用friend.getGreeting()方法會將person.getGreeting()的返回值與",hi!"拼接成新的字元串並返回。friend.getGreeting()方法的[[HomeObject]]屬性值是friend,friend的原型是person,所以super.getGreeting()等價於Person.getGreeting.call(this)
對象遍歷
【Object.keys()】
ES5 引入了Object.keys()
方法,返回一個數組,成員是參數對象自身的(不含繼承的)所有可遍歷(enumerable)屬性的鍵名
var obj = { foo: 'bar', baz: 42 }; console.log(Object.keys(obj));// ["foo", "baz"]
ES2017 引入了跟Object.keys
配套的Object.values
和Object.entries
,作為遍歷一個對象的補充手段,供for...of
迴圈使用
let {keys, values, entries} = Object; let obj = { a: 1, b: 2, c: 3 }; for (let key of keys(obj)) { console.log(key); // 'a', 'b', 'c' } for (let value of values(obj)) { console.log(value); // 1, 2, 3 } for (let [key, value] of entries(obj)) { console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3] }
【Object.values()】
Object.values()
方法返回一個數組,成員是參數對象自身的(不含繼承的)所有可遍歷(enumerable)屬性的鍵值
var obj = { foo: 'bar', baz: 42 }; console.log(Object.values(obj));// ["bar", 42]
Object.values()
只返回對象自身的可遍歷屬性
var obj = Object.create({}, {p: {value: 42}}); console.log(Object.values(obj)); // []
上面代碼中,Object.create()
方法的第二個參數添加的對象屬性(屬性p
),如果不顯式聲明,預設是不可遍歷的,因為p
的屬性描述對象的enumerable
預設是false
,Object.values()
不會返回這個屬性。只要把enumerable
改成true
,Object.values
就會返回屬性p
的值
var obj = Object.create({}, {p: { value: 42, enumerable: true } }); console.log(Object.values(obj)); // [42]
【Object.entries()】
Object.entries()
方法返回一個數組,成員是參數對象自身的(不含繼承的)所有可遍歷(enumerable)屬性的鍵值對數組
var obj = { foo: 'bar', baz: 42 }; console.log(Object.entries(obj));// [ ["foo", "bar"], ["baz", 42] ]
除了返回值不一樣,該方法的行為與Object.values
基本一致
Object.entries()
的基本用途是遍歷對象的屬性
let obj = { one: 1, two: 2 }; for (let [k, v] of Object.entries(obj)) { console.log( `${JSON.stringify(k)}: ${JSON.stringify(v)}` ); } // "one": 1 // "two": 2