隨着JS應用複雜度的不斷增長,開發者在程序中使用對象的數量也在持續增加,所以對象使用效率的提高就變得相當重要。ES6經過多種方式來增強對象的使用,經過簡單的語法擴展,提供更多操做對象及與對象交互的方法。本章將詳細介紹ES6對象擴展node
在瀏覽器這樣的執行環境中,對象沒有統一的標準,在標準中又使用不一樣的術語描述對象,ES6規範清晰定義了每個類別的對象,對象的類別以下es6
一、普通(Ordinary)對象數據庫
具備JS對象全部的默認內部行爲數組
二、特異(Exotic)對象瀏覽器
具備某些與默認行爲不符的內部行爲mongoose
三、標準(Standard)對象函數
ES6規範中定義的對象,例如,Array、Date等。標準對象既能夠是普通對象,也能夠是特異對象this
四、內建對象es5
腳本開始執行時存在於JS執行環境中的對象,全部標準對象都是內建對象spa
【屬性初始值簡寫】
在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
【展開運算符】
在ES7中,支持對象展開運算符的寫法,來替代Object.assign()
Object.assign({}, state, {visibilityFilter: action.filter})
//等同於
{ ...state, visibilityFilter: action.filter }
[注意]在某些狀況下,展開運行符的寫法不生效
在nodejs中使用mongoose數據庫篩選數據時要使用Object.assign()。若是使用...t,會輸出一些無用的值
"$__": { "strictMode": true, "selected": { "_id": 0, "content": 0 }, "getters": {}, "wasPopulated": false, "activePaths": { "paths": { "title": "init", "categories": "default", "comments": "default", "likes": "default", "collections": "default", "createdAt": "init", "updatedAt": "init", "__v": "init" }, ...
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()方法處理屬性的順序也將隨之改變
自有屬性枚舉順序的基本規則是
一、全部數字鍵按升序排序
二、全部字符串鍵按照它們被加入對象的順序排序
三、全部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、一、二、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