ES6對象擴展

前面的話

  隨着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.valuesObject.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默認是falseObject.values()不會返回這個屬性。只要把enumerable改爲trueObject.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
相關文章
相關標籤/搜索