es6入門(二)

一、數組擴展

1.1 擴展運算符

1.1.1 擴展運算符的基本用法

擴展運算符(spread)是三個點(…)。它好比 rest 參數的逆運算,將一個數組轉爲用逗號分隔的參數序列。

擴展運算符主要在函數調用時候使用。

function add(x, y) {
  return x + y;
}

const numbers = [4, 38];
add(...numbers) // 42

擴展運算符與正常的函數參數可以結合使用,非常靈活

function f(v, w, x, y, z) { }
const args = [0, 1];
f(-1, ...args, 2, ...[3]);

擴展運算符後面還可以放置表達式。

const arr = [
  ...(x > 0 ? ['a'] : []),
  'b',
];

如果擴展運算符後面是一個空數組,則不產生任何效果。

注意,只有函數調用時,擴展運算符纔可以放在圓括號中,否則會報錯。

(...[1, 2]) // 報錯

console.log((...[1, 2])) // 報錯

console.log(...[1, 2]) // 1 2
  • 由於擴展運算符可以展開數組,所以不再需要apply方法,將數組轉爲函數的參數了。
// ES5 的寫法
function f(x, y, z) {
  // ...
}
var args = [0, 1, 2];
f.apply(null, args);

// ES6的寫法
function f(x, y, z) {
  // ...
}
let args = [0, 1, 2];
f(...args);
  • 在Math.max函數中使用擴展運算符。
// ES5 的寫法
Math.max.apply(null, [14, 3, 77])

// ES6 的寫法
Math.max(...[14, 3, 77])

// 等同於
Math.max(14, 3, 77);
  • 在數組push函數中使用擴展運算符。
// ES5的 寫法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);

// ES6 的寫法
let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];
arr1.push(...arr2);

1.1.2 擴展運算符的實際應用

  • 使用擴展運算符克隆數組。
const a1 = [1, 2];
const a2 = a1;

上面代碼中,a2並不是a1的克隆,而是指向同一份數據的另一個指針。修改a2,會直接導致a1的變化。

const a1 = [1, 2];
// 寫法一
const a2 = [...a1];
// 寫法二
const [...a2] = a1;

上面的兩種寫法,a2都是a1的克隆。因此,修改a2就不會對a1產生影響。

  • 合併數組
const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];

// ES5 的合併數組
arr1.concat(arr2, arr3);

// ES6 的合併數組
[...arr1, ...arr2, ...arr3]

不過,這兩種方法都是淺拷貝。因此,如果修改了原數組的成員,會同步反映到新數組。

  • 與解構賦值結合

擴展運算符可以與解構賦值結合起來,用於生成數組。

const [first, ...rest] = [1, 2, 3, 4, 5];

first // 1
rest  // [2, 3, 4, 5]

如果將擴展運算符用於數組賦值,只能放在參數的最後一位,否則會報錯。

const [...first, last] = [1, 2, 3, 4, 5]; // 報錯

const [first, ...middle, last] = [1, 2, 3, 4, 5]; // 報錯
  • 把字符串轉換成數組
[...'hello'] // [ "h", "e", "l", "l", "o" ]

上面的寫法,有一個重要的好處,那就是能夠正確識別四個字節的 Unicode 字符。

'x\uD83D\uDE80y'.length // 4
[...'x\uD83D\uDE80y'].length // 3

上面代碼的第一種寫法,JavaScript 會將四個字節的 Unicode 字符,識別爲 2 個字符,採用擴展運算符就沒有這個問題。因此,正確返回字符串長度的函數,可以像下面這樣寫。

function length(str) {
  return [...str].length;
}

length('x\uD83D\uDE80y') // 3
  • 將實現了Iterator接口的對象轉換成數組

任何定義了遍歷器(Iterator)接口的對象,都可以用擴展運算符轉爲真正的數組。

let nodeList = document.querySelectorAll('div');
let arr = [...nodeList];

上面代碼中,querySelectorAll方法返回的是一個nodeList對象。它不是數組,而是一個類似數組的對象。這時,擴展運算符可以將其轉爲真正的數組,原因就在於nodeList對象實現了 Iterator 。

1.2 Array.from()

Array.from方法用於將兩類對象轉爲真正的數組:類似數組的對象(array-like object)和可遍歷(iterable)的對象。

類似數組的對象包括:上面通過querySelectorAll方法返回的nodeList對象,方法中的arguments對象等等。

// 將nodeList轉換成數組
let arr = Array.from(nodeList);

// arguments對象
function foo() {
  var args = Array.from(arguments);
}

下面是一個類似數組的對象,Array.from將它轉爲真正的數組。

let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
};

// ES5的寫法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']

// ES6的寫法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

另外,只要部署了Iterator接口的對象,也可以使用Array.from轉換成成數組。

// 將字符串轉換成數組
Array.from('hello') // ['h', 'e', 'l', 'l', 'o']

// 將實現了Iterator接口的對象轉換成數組
let namesSet = new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']

如果參數是一個真正的數組,Array.from會返回一個一模一樣的新數組。

// 返回一個新的數組[1, 2, 3]
Array.from([1, 2, 3])

擴展運算符背後調用的是遍歷器接口(Symbol.iterator),如果一個對象沒有部署這個接口,就無法轉換。Array.from方法還支持類似數組的對象。所謂類似數組的對象,本質特徵只有一點,即必須有length屬性。因此,任何有length屬性的對象,都可以通過Array.from方法轉爲數組,而此時擴展運算符就無法轉換。

// 返回一個長度爲3的空數組
Array.from({ length: 3 });  // [ undefined, undefined, undefined ]

上面代碼中,Array.from返回了一個具有三個成員的數組,每個位置的值都是undefined。擴展運算符轉換不了這個對象。

Array.from還可以接受第二個參數,作用類似於數組的map方法,用來對每個元素進行處理,將處理後的值放入返回的數組。

Array.from(arrayLike, x => x * x);

// 等同於
Array.from(arrayLike).map(x => x * x);

1.3 Array.of()

Array.of方法用於將一組值,轉換爲數組。

Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]

上面代碼中,Array方法沒有參數、一個參數、三個參數時,返回結果都不一樣。只有當參數個數不少於 2 個時,Array()纔會返回由參數組成的新數組。參數個數只有一個時,實際上是指定數組的長度。

Array.of基本上可以用來替代Array()或new Array(),並且不存在由於參數不同而導致的重載。它的行爲非常統一。

Array.of() // []
Array.of(undefined) // [undefined]
Array.of(1) // [1]
Array.of(1, 2) // [1, 2]

Array.of總是返回參數值組成的數組。如果沒有參數,就返回一個空數組。

1.4 copyWithin()

在當前數組內部,將指定位置的成員複製到其他位置(會覆蓋原有成員),然後返回當前數組。也就是說,使用這個方法,會修改當前數組。

語法格式:

Array.prototype.copyWithin(target, start = 0, end = this.length)

target(必需):從該位置開始替換數據。如果爲負值,表示倒數。
start(可選):從該位置開始讀取數據,默認爲 0。如果爲負值,表示從末尾開始計算。
end(可選):到該位置前停止讀取數據,默認等於數組長度。如果爲負值,表示從末尾開始計算。

例如:

// 將3號位複製到0號位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4)  // [4, 2, 3, 4, 5]

// -2相當於3號位,-1相當於4號位
[1, 2, 3, 4, 5].copyWithin(0, -2, -1)  // [4, 2, 3, 4, 5]

// 將3號位複製到0號位
[].copyWithin.call({length: 5, 3: 1}, 0, 3)  // {0: 1, 3: 1, length: 5}

// 將2號位到數組結束,複製到0號位
let i32a = new Int32Array([1, 2, 3, 4, 5]);
i32a.copyWithin(0, 2);  // Int32Array [3, 4, 5, 4, 5]

1.5 find()和findIndex()

數組的find方法,用於找出第一個符合條件的數組成員。它的參數是一個回調函數,所有數組成員依次執行該回調函數,直到找出第一個返回值爲true的成員,然後返回該成員。如果沒有符合條件的成員,則返回undefined。

[1, 4, -5, 10].find((n) => n < 0) // -5

上面代碼找出數組中第一個小於 0 的成員。

[1, 5, 10, 15].find(function(value, index, arr) {
  return value > 9;
}) // 10

上面代碼中,find方法的回調函數可以接受三個參數,依次爲當前的值、當前的位置和原數組。

數組實例的findIndex方法的用法與find方法非常類似,返回第一個符合條件的數組成員的位置,如果所有成員都不符合條件,則返回-1。

同時,這兩個方法都可以接受第二個參數,用來綁定回調函數的this對象。

// 定義回調函數
function f(v){
  return v > this.age;
}

// 綁定this的對象
let person = {name: 'John', age: 20};

// 查找數組,返回大於person.age的數組元素
[10, 12, 26, 15].find(f, person);    // 26

1.6 fill()

fill方法使用給定值,填充一個數組。

['a', 'b', 'c'].fill(7) // [7, 7, 7]

new Array(3).fill(7) // [7, 7, 7]

上面代碼表明,fill方法用於空數組的初始化非常方便。數組中已有的元素,會被全部抹去。

fill方法還可以接受第二個和第三個參數,用於指定填充的起始位置和結束位置。

['a', 'b', 'c'].fill(7, 1, 2) // ['a', 7, 'c']

上面代碼表示,fill方法從 1 號位開始,向原數組填充 7,到 2 號位之前結束。

如果填充的類型爲對象,那麼被賦值的是同一個內存地址的對象,而不是深拷貝對象。

let arr = new Array(3).fill({name: "Mike"});
arr[0].name = "Ben";
arr // [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]

上面代碼修改了位置爲0的數組元素的name屬性,但是其他元素的name屬性也被修改了。

1.7 entries()、keys()和values()

ES6提供了這三個方法用於遍歷數組,它們返回一個遍歷器對象,可以通過for…of循環進行遍歷。

for (let index of ['a', 'b'].keys()) {
  console.log(index);
}  
// 0 1

for (let elem of ['a', 'b'].values()) {
  console.log(elem);
} 
// a b

for (let [index, elem] of ['a', 'b'].entries()) {
  console.log(index, elem);
} 
// 0 "a"
// 1 "b"

如果不使用for…of循環,可以手動調用遍歷器對象的next方法,進行遍歷。

let letter = ['a', 'b', 'c'];
let entries = letter.entries();
console.log(entries.next().value); // [0, 'a']
console.log(entries.next().value); // [1, 'b']
console.log(entries.next().value); // [2, 'c']

1.8 includes()

判斷某個數組是否包含給定的值,如果包含返回true,否則返回false。

沒有該方法之前,我們通常使用數組的indexOf方法,檢查是否包含某個值。

if (arr.indexOf(el) !== -1) {
  // ...
}

indexOf方法有兩個缺點,一是不夠語義化,它的含義是找到參數值的第一個出現位置,所以要去比較是否不等於-1,表達起來不夠直觀。二是,它內部使用嚴格相等運算符(===)進行判斷,這會導致對NaN的誤判。例如:

[NaN].indexOf(NaN)  // -1

includes使用的是不一樣的判斷算法,就沒有這個問題。

[1, 2, 3].includes(2)     // true
[1, 2, 3].includes(4)     // false
[1, 2, NaN].includes(NaN) // true

該方法的第二個參數表示搜索的起始位置,默認爲0。如果第二個參數爲負數,則表示倒數的位置。

[1, 2, 3].includes(3, 3);  // false
[1, 2, 3].includes(3, -1); // true

二、對象擴展

對象(object)是 JavaScript 最重要的數據結構。ES6 對它進行了重大升級。

2.1 屬性簡潔表示法

ES6 允許在大括號裏面,直接寫入變量和函數,作爲對象的屬性和方法。

const foo = 'bar';
const baz = {foo}; // 相當於{foo: "bar"}

// 等同於
const baz = {foo: foo};

上面代碼中,變量foo直接寫在大括號裏面。這時,屬性名就是變量名, 屬性值就是變量值。

除了屬性簡寫,方法也可以簡寫。

const o = {
  foo() {
    return "Hello!";
  }
};

// 等同於
const o = {
  foo: function() {
    return "Hello!";
  }
};

示例:

let birth = '2000/01/01';

const Person = {

  name: '張三',

  //等同於birth: birth
  birth,

  // 等同於hello: function ()...
  hello() { console.log('我的名字是', this.name); }

};

2.2 屬性名錶達式

JavaScript 定義對象的屬性,有兩種方法。

// 方法一
obj.foo = true;

// 方法二
obj['a' + 'bc'] = 123;

方法二是用表達式作爲屬性名,這時要將表達式放在方括號之內。

ES6 還允許字面量定義對象時,用方法二(表達式)作爲對象的屬性名,即把表達式放在方括號內。

let propKey = 'foo';

let obj = {
  [propKey]: true,
  ['a' + 'bc']: 123
};

表達式還可以用於定義方法名。

let obj = {
  ['h' + 'ello']() {
    return 'hi';
  }
};

console.log(obj.hello()) // hi

注意,屬性名錶達式與簡潔表示法,不能同時使用,會報錯。

const foo = 'bar';
const baz = {[foo]}; // 報錯
const baz = {[foo]: 'abc'}; // 正確

注意,屬性名錶達式如果是一個對象,默認情況下會自動將對象轉爲字符串[object Object],這一點要特別小心。

const o1 = {a: 1};
const o2 = {b: 2};

const myObject = {
  [o1]: 'valueA',
  [o2]: 'valueB'
};

myObject // Object {[object Object]: "valueB"}

上面代碼中,[o1]和[o2]得到的都是[object Object],所以[o2]會把[o1]覆蓋掉。因此,myObject最後只有一個[object Object]屬性。

2.3 super關鍵字

我們知道,this關鍵字總是指向函數所在的當前對象,ES6 又新增了另一個類似的關鍵字super,指向當前對象的原型對象。

const proto = {
  foo: 'hello'
};

const obj = {
  foo: 'world',
  find() {
    return super.foo;
  }
};

Object.setPrototypeOf(obj, proto);
obj.find() // "hello"

上面代碼中,對象obj.find()方法之中,通過super.foo引用了原型對象proto的foo屬性。

注意,super關鍵字表示原型對象時,只能用在對象的方法之中,用在其他地方都會報錯。

// 報錯
const obj = {
  foo: super.foo
}

// 報錯
const obj = {
  foo: () => super.foo
}

// 報錯
const obj = {
  foo: function () {
    return super.foo
  }
}

上面代碼對於 JavaScript 引擎來說,這裏的super都沒有用在對象的方法之中。第一種寫法是super用在屬性裏面,第二種和第三種寫法是super用在一個函數裏面,然後賦值給foo屬性。目前,只有對象方法的簡寫法可以讓 JavaScript 引擎確認,定義的是對象的方法。

JavaScript 引擎內部,super.foo等同於Object.getPrototypeOf(this).foo或Object.getPrototypeOf(this).foo.call(this)。因此,下面代碼的super.foo指向原型對象proto的foo方法,但是綁定的this卻還是當前對象obj,因此輸出的就是world。

const proto = {
  x: 'hello',
  foo() {
    console.log(this.x);
  },
};

const obj = {
  x: 'world',
  foo() {
    super.foo();
  }
}

Object.setPrototypeOf(obj, proto);

obj.foo() // "world"

2.4 對象的擴展運算符

2.4.1 解構賦值

對象的解構賦值用於從一個對象取值,相當於將目標對象自身的所有可遍歷的、但尚未被讀取的屬性,分配到指定的對象上。所有的鍵值都會拷貝到新的對象上。

let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }

上面代碼中,變量z是解構賦值所在的對象。它獲取等號右邊的所有尚未讀取的鍵(a和b),將它們連同值一起拷貝過來。

由於解構賦值要求等號右邊是一個對象,所以如果等號右邊是undefined或null,就會報錯,因爲它們無法轉爲對象。

let { ...z } = null; // 運行時錯誤
let { ...z } = undefined; // 運行時錯誤

解構賦值必須是最後一個參數,否則會報錯。

let { ...x, y, z } = someObject; // 句法錯誤
let { x, ...y, ...z } = someObject; // 句法錯誤

注意,解構賦值的拷貝是淺拷貝,即如果一個鍵的值是複合類型的值(數組、對象、函數)、那麼解構賦值拷貝的是這個值的引用,而不是這個值的副本。

let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2;
x.a.b // 2

上面代碼中,x是解構賦值所在的對象,拷貝了對象obj的a屬性。a屬性引用了一個對象,修改這個對象的值,會影響到解構賦值對它的引用。

另外,擴展運算符的解構賦值,不能複製繼承自原型對象的屬性。

let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
console.log('o2.b = ', o2.b) // 2
console.log('o2.a = ', o2.a) // 1

let { ...o3 } = o2;
console.log('o3.b = ', o3.b) // 2
console.log('o3.a = ', o3.a) // undefined

上面代碼中,對象o3複製了o2,但是隻複製了o2自身的屬性,沒有複製它的原型對象o1的屬性。

// 指定原型對象創建對象o
const o = Object.create({ x: 1, y: 2 });
o.z = 3;
let { x, ...newObj } = o;

console.log(x) // 1
console.log(newObj.y) // undefined
console.log(newObj.z) // 3

上面代碼中,變量x是單純的解構賦值,所以可以讀取對象o繼承的屬性;變量newObj.y和newObj.z是擴展運算符的解構賦值,只能讀取對象o自身的屬性,所以變量z可以賦值成功,變量y取不到值。

ES6 還規定,變量聲明語句之中,如果使用解構賦值,擴展運算符後面必須是一個變量名,而不能是一個解構賦值表達式,所以上面代碼引入了中間變量newObj,如果寫成下面這樣會報錯。

let { x, ...newObj } = o;  // 正確
let { x, ...{ y, z } } = o; // 報錯

下面是錯誤的信息:
在這裏插入圖片描述

2.4.2 擴展運算符

對象的擴展運算符(…)用於取出參數對象的所有可遍歷屬性,拷貝到當前對象之中。

let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }

由於數組是特殊的對象,所以對象的擴展運算符也可以用於數組。

let foo = { ...['a', 'b', 'c'] };
foo
// {0: "a", 1: "b", 2: "c"}

如果擴展運算符後面是一個空對象,則沒有任何效果。

let foo = {...{}, a: 1}
foo
// { a: 1 }

如果擴展運算符後面不是對象,則會自動將其轉爲對象。

// 等同於 {...Object(1)}
{...1} // {}

// 等同於 {...Object(true)}
{...true} // {}

// 等同於 {...Object(undefined)}
{...undefined} // {}

// 等同於 {...Object(null)}
{...null} // {}

上面代碼中,擴展運算符後面不是對象,因此會自動轉爲Object{xx}。由於該對象沒有自身屬性,所以返回一個空對象。

但是,如果擴展運算符後面是字符串,它會自動轉成一個類似數組的對象,因此返回的不是空對象。

{...'hello'}
// {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}

擴展運算符可以用於合併兩個對象。

let ab = {a: 10, b: 20}
let xy = {x: 30, y: 40}
let abxy = {...ab, ...xy};
abxy 
// {a: 10, b: 20, x: 30, y: 40}

如果用戶自定義的屬性,放在擴展運算符後面,則擴展運算符內部的同名屬性會被覆蓋掉。

let x = {a: 10, b: 20}
let y = {...x, a: 30, c: 40};
y
// {a: 30, b: 20, c: 40}

如果把自定義屬性放在擴展運算符前面,就變成了設置新對象的默認屬性值。

let y = {a: 30, c: 40, ...x};
y
// {a: 10, b: 20, c: 40}

與數組的擴展運算符一樣,對象的擴展運算符後面可以跟表達式。

const obj = {
  ...(x > 1 ? {a: 1} : {}),
  b: 2,
};

三、對象新增方法

3.1 Object.is()

作用:用來比較兩個值是否嚴格相等,與嚴格比較運算符(===)的行爲基本一致。

Object.is('foo', 'foo') // true
Object.is({}, {}) // false

+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

3.2 Object.assign()

Object.assign方法用於對象的合併,將源對象(source)的所有可枚舉屬性,複製到目標對象(target)。

const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
// 將source1和source2的所有屬性複製到對象target中
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

注意,如果目標對象與源對象有同名屬性,或多個源對象有同名屬性,則後面的屬性會覆蓋前面的屬性。

如果只有一個參數,Object.assign會直接返回該參數。

const o1 = {a: 1};
const o2 = Object.assign(obj)
console.log(o1 == o2) // true

如果該參數不是對象,則會先轉成對象,然後返回。

typeof Object.assign(2) // "object"

由於undefined和null無法轉成對象,所以如果它們作爲參數,就會報錯。

Object.assign(undefined) // 報錯
Object.assign(null) // 報錯

如果非對象參數出現在源對象的位置(即非首參數),那麼處理規則有所不同。首先,這些參數都會轉成對象,如果無法轉成對象,就會跳過。這意味着,下面代碼不會報錯。

let obj = {a: 1};
Object.assign(obj, undefined) // 運行正常
Object.assign(obj, null) // 運行正常

其他類型的值(即數值、字符串和布爾值)不在首參數,也不會報錯。但是,除了字符串會以數組形式,拷貝入目標對象,其他值都不會產生任何效果。

const obj = Object.assign({}, 'abc', 10, true);
console.log(obj); // { "0": "a", "1": "b", "2": "c" }

上面的代碼的10和true無法轉換成對象,因爲不是首參數,所以不會報錯,但是也不會有任何效果。

另外一個值得我們注意的地方,Object.assign拷貝的屬性是有限制的,只拷貝源對象的自身屬性(不拷貝繼承屬性),也不拷貝不可枚舉的屬性。

注意事項

(1)淺拷貝
Object.assign方法實行的是淺拷貝,而不是深拷貝。也就是說,如果源對象某個屬性的值是對象,那麼目標對象拷貝得到的是這個對象的引用。

const obj1 = {a: {b: 1}};
const obj2 = Object.assign({}, obj1);

obj1.a.b = 2;
obj2.a.b // 2

(2)同名屬性的替換
對於這種嵌套的對象,一旦遇到同名屬性,Object.assign的處理方法是替換,而不是添加。

const target = { a: { b: 'c', d: 'e' } }
const source = { a: { b: 'hello' } }
Object.assign(target, source)
// { a: { b: 'hello' } }

(3)數組處理
Object.assign可以用來處理數組,但是會把數組視爲對象。

Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3]

上面代碼中,Object.assign把數組視爲屬性名爲 0、1、2 的對象,因此源數組的 0 號屬性4覆蓋了目標數組的 0 號屬性1。

(4)取值函數的處理
Object.assign只能進行值的複製,如果要複製的值是一個取值函數,那麼將求值後再複製。

const source = {
  get foo() { return 1 }
};
const target = {};

Object.assign(target, source)
// { foo: 1 }

上面代碼中,source對象的foo屬性是一個取值函數,Object.assign不會複製這個取值函數,只會拿到值以後,將這個值複製過去。

Object.assign()的實際用途

(1)爲對象添加屬性

class Point {
  constructor(x, y) {
    Object.assign(this, {x, y});
  }
}

(2)爲對象添加方法

Object.assign(SomeClass.prototype, {
  someMethod(arg1, arg2) {
    ···
  },
  anotherMethod() {
    ···
  }
});

// 等同於下面的寫法
SomeClass.prototype.someMethod = function (arg1, arg2) {
  ···
};
SomeClass.prototype.anotherMethod = function () {
  ···
};

(3)克隆對象

function clone(origin) {
  return Object.assign({}, origin);
}

上面代碼將原始對象拷貝到一個空對象,就得到了原始對象的克隆。

不過,採用這種方法克隆,只能克隆原始對象自身的值,不能克隆它繼承的值。如果想要保持繼承鏈,可以採用下面的代碼。

function clone(origin) {
  let originProto = Object.getPrototypeOf(origin);
  return Object.assign(Object.create(originProto), origin);
}

(4)合併多個對象

Object.assign(target, ...sources);

如果希望合併後返回一個新對象,可以改寫上面函數,對一個空對象合併。

Object.assign({}, ...sources)

(5)爲屬性指定默認值

const DEFAULTS = {
  logLevel: 0,
  outputFormat: 'html'
};

function processContent(options) {
  options = Object.assign({}, DEFAULTS, options);
  console.log(options);
  // ...
}

上面代碼中,DEFAULTS對象是默認值,options對象是用戶提供的參數。Object.assign方法將DEFAULTS和options合併成一個新對象,如果兩者有同名屬性,則options的屬性值會覆蓋DEFAULTS的屬性值。

注意,由於存在淺拷貝的問題,DEFAULTS對象和options對象的所有屬性的值,最好都是簡單類型,不要指向另一個對象。

const DEFAULTS = {
  url: {
    host: 'example.com',
    port: 7070
  },
};

const newObj = Object.assign(DEFAULTS, { url: {port: 8000}})
url.port // 8000
url.host // 不存在

上面代碼的原意是將url.port改成 8000,url.host保持不變。實際結果卻是options.url覆蓋掉DEFAULTS.url,所以url.host就不存在了。

3.3 Object.getOwnPropertyDescriptors()

ES5 的Object.getOwnPropertyDescriptor()方法會返回某個對象屬性的描述對象(descriptor)。ES2017 引入了Object.getOwnPropertyDescriptors()方法,返回指定對象所有自身屬性(非繼承屬性)的描述對象。

const obj = {
  foo: 123,
  get bar() { return 'abc' }
};

console.log(Object.getOwnPropertyDescriptors(obj))
// { foo:
//    { value: 123,
//      writable: true,
//      enumerable: true,
//      configurable: true },
//   bar:
//    { get: [Function: get bar],
//      set: undefined,
//      enumerable: true,
//      configurable: true } }

上面代碼中,Object.getOwnPropertyDescriptors()方法返回一個對象,所有原對象的屬性名都是該對象的屬性名,對應的屬性值就是該屬性的描述對象。

該方法的引入目的,主要是爲了解決Object.assign()無法正確拷貝get屬性和set屬性的問題。

const obj = {
  foo: 123,
  get bar() { return 'abc' }
};

const target = {}

// 將obj所有屬性合併到target對象中
Object.assign(target, obj)

console.log(target)

運行結果:
在這裏插入圖片描述
上面代碼中,source對象的bar屬性的值是一個get函數,Object.assign方法將這個屬性拷貝給target對象,結果該屬性的值變成了abc。這是因爲Object.assign方法總是拷貝一個屬性的值,而不會拷貝它背後的賦值方法或取值方法。

這時,Object.getOwnPropertyDescriptors()方法配合Object.defineProperties()方法,就可以實現正確拷貝。

const obj = {
  foo: 123,
  get bar() { return 'abc' }
};

const target = {}

// 給target對象定義屬性
Object.defineProperties(target, 
	Object.getOwnPropertyDescriptors(obj));

console.log(target)

Object.getOwnPropertyDescriptors()方法的另一個用處,是配合Object.create()方法,將對象屬性克隆到一個新對象。這屬於淺拷貝。

const obj = {
  foo: {
  	aa : 123
  },
  get bar() { return 'abc' }
};

// 克隆對象
const clone = Object.create(
	Object.getPrototypeOf(obj), 
	Object.getOwnPropertyDescriptors(obj));
	
obj.foo.aa = 999
console.log(clone.foo.aa) // 999

從輸出結果看出,obj.foo和clone.foo其實是同一對象的引用。

另外,Object.getOwnPropertyDescriptors()方法可以實現一個對象繼承另一個對象。以前,繼承另一個對象,常常寫成下面這樣。

const prot = {
  get bar() { return 'abc' }
};

// 繼承obj
const obj = {
  __proto__: prot,
  foo: 123,
};

console.log(childObj.bar)

如果去除__proto__,上面代碼就要改成下面這樣。

const obj = Object.create(prot);
obj.foo = 123;

// 或者
const obj = Object.assign(
  Object.create(prot),
  {
    foo: 123,
  }
);

有了Object.getOwnPropertyDescriptors(),我們就有了另一種寫法。

const obj = Object.create(
  prot, // 新建對象的原型對象
  Object.getOwnPropertyDescriptors({foo: 123}) // 添加到新對象的可枚舉屬性
);

3.4 proto,Object.setPrototypeOf(),Object.getPrototypeOf()

(1)__proto__屬性

__proto__屬性(前後各兩個下劃線),用來讀取或設置當前對象的prototype對象。目前,所有瀏覽器(包括 IE11)都部署了這個屬性。

// es5 的寫法
const obj = {
  method: function() { ... }
};
obj.__proto__ = someOtherObj;

// es6 的寫法
var obj = Object.create(someOtherObj);
obj.method = function() { ... };

雖然可以使用__proto__屬性設置原型對象,但是es6標準明確規定,除了瀏覽器必須部署該屬性以外,在代碼中最好認爲這個屬性是不存在的。因此,無論從語義的角度,還是從兼容性的角度,都不要使用這個屬性,而是使用Object.setPrototypeOf()(寫操作)、Object.getPrototypeOf()(讀操作)、Object.create()(生成操作)代替。

(2)Object.setPrototypeOf()

Object.setPrototypeOf方法的作用與__proto__相同,用來設置一個對象的prototype對象,返回參數對象本身。它是 ES6 正式推薦的設置原型對象的方法。

// 格式
Object.setPrototypeOf(object, prototype)

該方法等同於:

function setPrototypeOf(obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

例如:

let proto = {};
let obj = { x: 10 };
Object.setPrototypeOf(obj, proto);

proto.y = 20;
proto.z = 40;

console.log(obj.x) // 10
console.log(obj.y) // 20
console.log(obj.z) // 40

上面代碼將proto對象設爲obj對象的原型,所以從obj對象可以讀取proto對象的屬性。

如果第一個參數不是對象,會自動轉爲對象。但是如果第一個參數是undefined或null,就會報錯。

Object.setPrototypeOf(undefined, {})
// TypeError: Object.setPrototypeOf called on null or undefined

Object.setPrototypeOf(null, {})
// TypeError: Object.setPrototypeOf called on null or undefined

(3)Object.getPrototypeOf()

該方法與Object.setPrototypeOf方法配套,用於讀取一個對象的原型對象。

// 格式
Object.getPrototypeOf(obj);

例如:

function Rectangle() {
  // ...
}
const rec = new Rectangle();

Object.getPrototypeOf(rec) === Rectangle.prototype
// true

如果參數不是對象,會被自動轉爲對象。

// 等同於 Object.getPrototypeOf(Number(1))
Object.getPrototypeOf(1)
// Number {[[PrimitiveValue]]: 0}

// 等同於 Object.getPrototypeOf(String('foo'))
Object.getPrototypeOf('foo')
// String {length: 0, [[PrimitiveValue]]: ""}

// 等同於 Object.getPrototypeOf(Boolean(true))
Object.getPrototypeOf(true)
// Boolean {[[PrimitiveValue]]: false}

Object.getPrototypeOf(1) === Number.prototype // true
Object.getPrototypeOf('foo') === String.prototype // true
Object.getPrototypeOf(true) === Boolean.prototype // true

如果參數是undefined或null,它們無法轉爲對象,所以會報錯。

Object.getPrototypeOf(null)
// TypeError: Cannot convert undefined or null to object

Object.getPrototypeOf(undefined)
// TypeError: Cannot convert undefined or null to object

3.5 Object.keys(),Object.values(),Object.entries()

(1)Object.keys()

ES5 引入了Object.keys方法,返回一個數組,成員是參數對象自身的(不含繼承的)所有可遍歷(enumerable)屬性的鍵名。

var obj = { foo: 'bar', baz: 42 };
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]
}

(2)Object.values()

Object.values方法返回一個數組,成員是參數對象自身的(不含繼承的)所有可遍歷(enumerable)屬性的鍵值。

const obj = { 100: 'a', 2: 'b', 7: 'c' };
Object.values(obj)
// ["b", "c", "a"]

上面代碼中,屬性名爲數值的屬性,是按照數值大小,從小到大遍歷的,因此返回的順序是b、c、a。

Object.values只返回對象自身的可遍歷屬性。

const obj = Object.create({}, {p: {value: 42}});
Object.values(obj) // []

上面代碼中,Object.create方法的第二個參數添加的對象屬性(屬性p),如果不顯式聲明,默認是不可遍歷的,因爲p的屬性描述對象的enumerable默認是false,Object.values不會返回這個屬性。只要把enumerable改成true,Object.values就會返回屬性p的值。

var prot = {
	p: {
		value: 42,
		enumerable: true,
	},
}
const obj = Object.create({}, prot);
Object.values(obj) // [42]

如果Object.values方法的參數是一個字符串,會返回各個字符組成的一個數組。

Object.values('foo')
// ['f', 'o', 'o']

上面代碼中,字符串會先轉成一個類似數組的對象。字符串的每個字符,就是該對象的一個屬性。因此,Object.values返回每個屬性的鍵值,就是各個字符組成的一個數組。

如果參數不是對象,Object.values會先將其轉爲對象。由於數值和布爾值的包裝對象,都不會爲實例添加非繼承的屬性。所以,Object.values會返回空數組。

Object.values(42) // []
Object.values(true) // []

(3)Object.entries()

Object.entries()方法返回一個數組,成員是參數對象自身的(不含繼承的)所有可遍歷(enumerable)屬性的鍵值對數組。

const obj = { foo: 'bar', baz: 42 };
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)}`
  );
}

Object.entries方法的另一個用處是,將對象轉爲真正的Map結構。

const obj = { foo: 'bar', baz: 42 };
const map = new Map(Object.entries(obj));
map // Map { foo: "bar", baz: 42 }

3.6 Object.fromEntries()

Object.fromEntries()方法是Object.entries()的逆操作,用於將一個鍵值對數組轉爲對象。

Object.fromEntries([
  ['foo', 'bar'],
  ['baz', 42]
])
// { foo: "bar", baz: 42 }

該方法的主要目的,是將鍵值對的數據結構還原爲對象,因此特別適合將 Map 結構轉爲對象。

const map = new Map().set('foo', true).set('bar', false);
Object.fromEntries(map)

該方法的一個用處是配合URLSearchParams對象,將查詢字符串轉爲對象。

Object.fromEntries(new URLSearchParams('foo=bar&baz=qux'))
// { foo: "bar", baz: "qux" }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章