ES6 ...操做符

在ES6語法中,...操做符有兩種意義:rest(剩餘語法,rest參數) 和 spread(展開語法,展開數組/對象),做爲函數、數組、對象的擴展運算符。javascript

從某種意義上說,剩餘語法與展開語法是相反的:
剩餘語法將多個元素收集起來並「凝聚」爲單個元素,而展開語法則是將數組/對象展開爲其中的各個元素。

剩餘語法

rest參數

形式爲 (...變量名),將一個不定數量的參數表示爲一個數組。用於獲取 函數實參中的多餘參數,組成一個數組,這樣就不須要使用arguments對象了。

語法html

function(a, b, ...theArgs) {
  // ...
}
注意:
  • rest參數以後不能再有其餘參數(只能是最後一個參數)不然會報錯。
  • 函數的length屬性不包括rest參數。java

    (function(a,b,...c){}).length //2

一個實例node

function sumOnlyNumbers() {  
  var args = arguments;
  var numbers = filterNumbers();
  return numbers.reduce((sum, element) => sum + element);


  function filterNumbers() {
    return Array.prototype.filter.call(args, 
        element => typeof element === 'number' 
    );
  }
}
sumOnlyNumbers(1, 'Hello', 5, false); // => 6

缺點: 首先咱們要將arguments分配給給一個臨時新變量args,這樣才能在內部函數filterNumbers中能夠訪問args新變量,由於 filterNumbers()定義了它本身的arguments 會覆蓋外部的arguments 。這種作法太冗餘了。
優化: 使用rest操做符能夠靈活解決這個問題,容許在函數中定義一個rest參數 ...args:es6

function sumOnlyNumbers(...args) {  
  var numbers = filterNumbers();
  return numbers.reduce((sum, element) => sum + element);
  function filterNumbers() {
    return args.filter(element => typeof element === 'number');
  }
}
sumOnlyNumbers(1, 'Hello', 5, false); // => 6
剩餘參數和 arguments 對象的區別摘自
  • 剩餘參數只包含那些沒有對應形參的實參,而 arguments 對象包含了傳給函數的全部實參。
  • arguments 對象不是一個真實的數組,而剩餘參數是真實的 Array實例。
    因此,可以在剩餘參數上面直接使用全部的數組方法,好比 sort,map,forEach,pop。
    而若是想在arguments對象上使用數組方法,你首先得將它轉換爲真實的數組。數據庫

    [].slice.call(arguments)
  • arguments 對象對象還有一些附加的屬性 (好比callee屬性)。

解構賦值

數組
let [first,...rest]=[1,2,3,4,5];
first //1
rest  //[2,3,4,5]
// ES5
a = list[0], rest = list.slice(1)
// ES6
[a, ...rest] = list
const [first, ...rest] = [];
first // undefined
rest  // []
對象
let { x, ...y } = { x: 1, a: 2, b: 3 };
x // 1
y // { a: 2, b: 3 }
// 淺拷貝

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

obj.a.b = 2;
x.a.b // 2
// ...運算符的解構賦值不能繼承自原型對象的屬性

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

o2.__proto__ = o1;
let { ...o3 } = o2;

o3 // { b: 2 }
o3.a // undefined
const o = Object.create({ x: 1, y: 2 });
o.z = 3;

let { x, ...newObj } = o;
let { y, z } = newObj;
x // 1
y // undefined
z // 3

上面代碼中,變量x是單純的解構賦值,因此能夠讀取對象o繼承的屬性;變量y和z是擴展運算符的解構賦值,只能讀取對象o自身的屬性,因此變量z能夠賦值成功,變量y取不到值。
ES6 規定,變量聲明語句之中,若是使用解構賦值,擴展運算符後面必須是一個變量名,而不能是一個解構賦值表達式,因此上面代碼引入了中間變量newObj,若是寫成下面這樣會報錯。數組

let { x, ...{ y, z } } = o;
// SyntaxError: ... must be followed by an identifier in declaration contexts

注意:瀏覽器

  • 解構賦值只能放在參數最後一位,不然會報錯。
  • 解構賦值要求等號右邊是一個對象,若是等號右邊是undefined或null,就會報錯,由於它們沒法轉爲對象。
  • 解構賦值的拷貝是淺拷貝,即若是一個鍵的值是複合類型的值(數組、對象、函數)、那麼解構賦值拷貝的是這個值的引用,而不是這個值的副本
  • ...運算符的解構賦值,不能複製繼承自原型對象的屬性
  • 變量聲明語句之中,若是使用解構賦值,擴展運算符後面必須是一個變量名,而不能是一個解構賦值表達式

rest參數&解構賦值

擴展某個函數的參數,引入其餘操做。數據結構

function baseFunction({ a, b }) {
  // ...
}
function wrapperFunction({ x, y, ...restConfig }) {
  // 使用 x 和 y 參數進行操做
  // 其他參數傳給原始函數
  return baseFunction(restConfig);
}

展開語法

spread運算則能夠看做是rest參數的逆運算。app

數組

展開數組做爲參數序列、複製數組、合併數組、代替apply、...+表達式

1.展開數組做爲函數參數(...arr), 能夠將數組轉化爲逗號分隔的參數序列

console.log(1,...arr)

arr1.push(...arr);

注意:push方法的參數不能是數組

函數應用實例
JavaScript 的函數只能返回一個值,若是須要返回多個值,只能返回數組或對象。擴展運算符提供瞭解決這個問題的一種變通方法。

var dateFields = readDateFields(database);
var d = new Date(...dateFields);

上面代碼從數據庫取出一行數據,經過擴展運算符,直接將其傳入構造函數Date。

2.複製數組

const a2 = a1.concat();    // ES5

const itemsCopy = [...items];    // ES6

注意:這兩種方法都是淺拷貝,使用的時候須要注意。

3.合併數組

// 合併生成一個新的數組,不影響原來的兩個數組
arr = [4].concat(list)    // ES5

arr = [4, ...list]    // ES6
// 擴展arr變量,追加arr2
Array.prototype.push.apply(arr, list);    // ES5

arr.push(...list);    // ES6

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

[...[], 1]
// [1]

4.能夠替代apply方法,apply要求將參數合併爲數組,做爲參數傳入

Function.apply(obj,args)方法能接收兩個參數

  • obj:這個對象將代替Function類裏this對象
  • args:這個是數組,它將做爲參數傳給Function(args-->arguments)

數組沒有max方法。Math.max.apply(null,[]);

Math.max.apply(null,[14,3,7])    // ES5寫法
Math.max(...[14,3,7])    // ES6寫法

// 等同於
Math.max(14, 3, 77);

5.擴展運算符後面還能夠放置表達式。

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

對象

複製對象、完整克隆、合併對象、...+表達式、取值函數get

1.拷貝

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

這等同於使用Object.assign方法。

let aClone = { ...a };    // 等同於,
let aClone = Object.assign({}, a);

上面的例子只是拷貝了對象實例的屬性。

2.完整克隆一個對象,拷貝實例屬性+對象原型的屬性

// 寫法一
const clone1 = {
  __proto__: Object.getPrototypeOf(obj),
  ...obj
};

// 寫法二
const clone2 = Object.assign(
  Object.create(Object.getPrototypeOf(obj)),
  obj
);

// 寫法三
const clone3 = Object.create(
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
)

上面代碼中,寫法一的__proto__屬性在非瀏覽器的環境不必定部署,所以推薦使用寫法二和寫法三。

3.合併對象

// 合併
let ab = { ...a, ...b };    // 等同於,
let ab = Object.assign({}, a, b);
若是用戶自定義的屬性,放在擴展運算符後面,則擴展運算符內部的同名屬性會被覆蓋掉
let aWithOverrides = { ...a, x: 1, y: 2 };    // 等同於

let aWithOverrides = { ...a, ...{ x: 1, y: 2 } };    // 等同於

let x = 1, y = 2, aWithOverrides = { ...a, x, y };    // 等同於

let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 });

上面代碼中,a對象的x屬性和y屬性,拷貝到新對象後會被覆蓋掉。這用來修改現有對象部分的屬性就很方便了

let newVersion = {
  ...previousVersion,
  name: 'New Name' // Override the name property
};

上面代碼中,newVersion對象自定義了name屬性,其餘屬性所有複製自previousVersion對象。

若是把自定義屬性放在擴展運算符前面,就變成了設置新對象的默認屬性值。
let aWithDefaults = { x: 1, y: 2, ...a };
// 等同於
let aWithDefaults = Object.assign({}, { x: 1, y: 2 }, a);
// 等同於
let aWithDefaults = Object.assign({ x: 1, y: 2 }, a);

若是擴展運算符後面是一個空對象,則沒有任何效果。
若是擴展運算符的參數是null或undefined,這兩個值會被忽略,不會報錯

{...{}, a: 1}    // { a: 1 }

let emptyObject = { ...null, ...undefined }; // 不報錯

4.與數組的擴展運算符同樣,對象的擴展運算符後面能夠跟表達式。

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

5.對象中取值函數get問題

擴展運算符的參數對象之中,若是有取值函數get,這個函數是會執行的
// 並不會拋出錯誤,由於 x 屬性只是被定義,但沒執行
let aWithXGetter = {
  ...a,
  get x() {
    throw new Error('not throw yet');
  }
};

// 會拋出錯誤,由於 x 屬性被執行了
let runtimeError = {
  ...a,
  ...{
    get x() {
      throw new Error('throw now');
    }
  }
};

字符串

[...'hello']    // [ "h", "e", "l", "l", "o" ]

Unicode 是有兩個字節、四字節之區分。上面的寫法,有一個重要的好處,那就是可以正確識別四個字節的 Unicode 字符。

'x\uD83D\uDE80y'    //'x🚀y'

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

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

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

length('x\uD83D\uDE80y') // 3

凡是涉及到操做四個字節的 Unicode 字符的函數,都有這個問題。所以,最好都用擴展運算符改寫。

let str = 'x\uD83D\uDE80y';

str.split('').reverse().join('')    // 'y\uDE80\uD83Dx'

[...str].reverse().join('')    // 'y\uD83D\uDE80x'

上面代碼中,若是不用擴展運算符,字符串的reverse操做就不正確。reverse,顛倒數組中元素的順序,會改變原來的數組,而不會建立新的數組。

實現了 Iterator 接口的對象

任何 Iterator 接口的對象,均可以用擴展運算符轉爲真正的數組。

let nodeList = document.querySelectorAll('div'); //nodeList 類數組對象
let array = [...nodeList];

這時,...運算符能夠將其轉爲真正的數組,緣由就在於NodeList對象實現了 Iterator 。
對於那些沒有部署 Iterator 接口的相似數組的對象,...運算符就沒法將其轉爲真正的數組。

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

// TypeError: Cannot spread non-iterable object.
let arr = [...arrayLike];

上面代碼中,arrayLike是一個相似數組的對象,可是沒有部署 Iterator 接口,擴展運算符就會報錯。這時,能夠改成使用Array.from方法將arrayLike轉爲真正的數組。

Map 和 Set 結構,Generator 函數

擴展運算符內部調用的是數據結構的 Iterator 接口,所以只要具備 Iterator 接口的對象,均可以使用擴展運算符,好比 Map 結構。

let map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

let arr = [...map.keys()]; // [1, 2, 3]

Generator 函數運行後,返回一個遍歷器對象,所以也可使用擴展運算符。

const go = function*(){
  yield 1;
  yield 2;
  yield 3;
};

[...go()] // [1, 2, 3]

上面代碼中,變量go是一個 Generator 函數,執行後返回的是一個遍歷器對象,對這個遍歷器對象執行擴展運算符,就會將內部遍歷獲得的值,轉爲一個數組。

若是對沒有 Iterator 接口的對象,使用擴展運算符,將會報錯。

const obj = {a: 1, b: 2};
let arr = [...obj]; // TypeError: Cannot spread non-iterable object

阮老師的文章:數組中的擴展運算
阮老師的文章:對象中的擴展運算
可算有個跟我想的同樣的了

相關文章
相關標籤/搜索