1. 程式人生 > >ES6對象擴展

ES6對象擴展

變量 不同的 receive lec pre ini 轉換 控制 復制代碼

前面的話

  隨著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.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
技術分享

ES6對象擴展