每一個JavaScript開發人員都應該知道的新ES2018功能(譯文)

前言

本文首發於個人我的網站: Timbok.topjavascript

正文

ECMAScript標準的第九版,官方稱爲ECMAScript 2018(或簡稱ES2018),於2018年6月發佈。從ES2016開始,ECMAScript規範的新版本每一年發佈而不是每幾年發佈一次,而且添加的功能少於主要版本之前。該標準的最新版本經過添加四個新RegExp功能,rest/spread屬性,asynchronous iteration,和Promise.prototype.finally。此外,ES2018從標記模板中刪除了轉義序列的語法限制。css

這些新變化將在後面的小節中解釋。java

rest/spread屬性

ES2015最有趣的功能之一是點差運算符。該運算符使複製和合並數組變得更加簡單。您可使用運算符...,而不是調用concat()or slice()方法:git

const arr1 = [10, 20, 30];

// make a copy of arr1
const copy = [...arr1];

console.log(copy);    // → [10, 20, 30]

const arr2 = [40, 50];

// merge arr2 with arr1
const merge = [...arr1, ...arr2];

console.log(merge);    // → [10, 20, 30, 40, 50]
複製代碼

在必須做爲函數的單獨參數傳入數組的狀況下,擴展運算符也派上用場。例如:程序員

const arr = [10, 20, 30]

// equivalent to
// console.log(Math.max(10, 20, 30));
console.log(Math.max(...arr));    // → 30
複製代碼

ES2018經過向對象文字添加擴展屬性來進一步擴展此語法。使用spread屬性,您能夠將對象的自身可枚舉屬性複製到新對象上。請考慮如下示例:github

const obj1 = {
  a: 10,
  b: 20
};

const obj2 = {
  ...obj1,
  c: 30
};

console.log(obj2);    // → {a: 10, b: 20, c: 30}
複製代碼

在此代碼中,...運算符用於檢索屬性obj1並將其分配給obj2。在ES2018以前,嘗試這樣作會引起錯誤。若是有多個具備相同名稱的屬性,則將使用最後一個屬性:正則表達式

const obj1 = {
  a: 10,
  b: 20
};

const obj2 = {
  ...obj1,
  a: 30
};

console.log(obj2);    // → {a: 30, b: 20}
複製代碼

Spread屬性還提供了一種合併兩個或多個對象的新方法,能夠將其用做方法的替代Object.assign()方法:編程

const obj1 = {a: 10};
const obj2 = {b: 20};
const obj3 = {c: 30};

// ES2018
console.log({...obj1, ...obj2, ...obj3});    // → {a: 10, b: 20, c: 30}

// ES2015
console.log(Object.assign({}, obj1, obj2, obj3));    // → {a: 10, b: 20, c: 30}
複製代碼

但請注意,spread屬性並不老是產生相同的結果Object.assign()。請考慮如下代碼:ubuntu

Object.defineProperty(Object.prototype, 'a', {
  set(value) {
    console.log('set called!');
  }
});

const obj = {a: 10};

console.log({...obj});    
// → {a: 10}

console.log(Object.assign({}, obj));    
// → set called!
// → {}
複製代碼

在此代碼中,該Object.assign()方法執行繼承的setter屬性。相反,傳播屬性徹底忽略了setter。數組

重要的是要記住,spread屬性只複製可枚舉的屬性。在如下示例中,type屬性不會顯示在複製的對象中,由於其enumerable屬性設置爲false

const car = {
  color: 'blue'
};

Object.defineProperty(car, 'type', {
  value: 'coupe',
  enumerable: false
});

console.log({...car});    // → {color: "blue"}
複製代碼

即便它們是可枚舉的,也會忽略繼承的屬性:

const car = {
  color: 'blue'
};

const car2 = Object.create(car, {
  type: {
    value: 'coupe',
    enumerable: true,
  }
});

console.log(car2.color);                      // → blue
console.log(car2.hasOwnProperty('color'));    // → false

console.log(car2.type);                       // → coupe
console.log(car2.hasOwnProperty('type'));     // → true

console.log({...car2});                       // → {type: "coupe"}
複製代碼

在此代碼中,car2繼承color屬性car。由於spread屬性只複製對象的本身的屬性,color因此不包含在返回值中。

請記住,spread屬性只能生成對象的淺表副本。若是屬性包含對象,則僅複製對象的引用:

const obj = {x: {y: 10}};
const copy1 = {...obj};    
const copy2 = {...obj}; 

console.log(copy1.x === copy2.x);    // → true
複製代碼

這裏copy1copy2的x是指在內存中的同一對象,因此全等運算返回true

ES2015中添加的另外一個有用功能是rest參數,它使JavaScript程序員可使用它...來表示值做爲數組。例如:

const arr = [10, 20, 30];
const [x, ...rest] = arr;

console.log(x);       // → 10
console.log(rest);    // → [20, 30]
複製代碼

這裏,arr的第一個值被分配給對應的x,而剩餘的元素被分配給rest變量。這種稱爲陣列解構的模式變得如此受歡迎,以致於Ecma技術委員會決定爲對象帶來相似的功能:

const obj = {
  a: 10,
  b: 20,
  c: 30
};

const {a, ...rest} = obj;

console.log(a);       // → 10
console.log(rest);    // → {b: 20, c: 30}
複製代碼

此代碼使用解構賦值中的其他屬性將剩餘的自身可枚舉屬性複製到新對象中。請注意,rest屬性必須始終出如今對象的末尾,不然會引起錯誤:

const obj = {
  a: 10,
  b: 20,
  c: 30
};

const {...rest, a} = obj;    // → SyntaxError: Rest element must be last element
複製代碼

還要記住,在對象中使用多個rest會致使錯誤,除非它們是嵌套的:

const obj = {
  a: 10,
  b: {
    x: 20,
    y: 30,
    z: 40
  }
};

const {b: {x, ...rest1}, ...rest2} = obj;    // no error

const {...rest, ...rest2} = obj;    // → SyntaxError: Rest element must be last element
複製代碼

Support for Rest/Spread

Chrome Firefox Safari Edge
60 55 11.1 No
Chrome Android Firefox Android iOS Safari Edge Mobile Samsung Internet Android Webview
60 55 11.3 No 8.2 60

Node.js

  • 8.0.0(運行時須要加-harmony
  • 8.3.0(徹底支持)

Asynchronous Iteration(異步迭代)

迭代數據集是編程的重要部分。此前ES2015,提供的JavaScript語句如forfor...inwhile,和方法map()filter()以及forEach()都用於此目的。爲了使程序員可以一次一個地處理集合中的元素,ES2015引入了迭代器接口。

若是對象具備Symbol.iterator屬性,則該對象是可迭代的。在ES2015中,字符串和集合對象(如Set,Map和Array)帶有Symbol.iterator屬性,所以能夠迭代。如下代碼給出瞭如何一次訪問可迭代元素的示例:

const arr = [10, 20, 30];
const iterator = arr[Symbol.iterator]();
  
console.log(iterator.next());    // → {value: 10, done: false}
console.log(iterator.next());    // → {value: 20, done: false}
console.log(iterator.next());    // → {value: 30, done: false}
console.log(iterator.next());    // → {value: undefined, done: true}
複製代碼

Symbol.iterator是一個衆所周知的符號,指定一個返回迭代器的函數。與迭代器交互的主要方法是next()方法。此方法返回具備兩個屬性的對象:valuedonevalue屬性爲集合中下一個元素的值。done屬性的值爲truefalse表示集合是否迭代完成。

默認狀況下,普通對象不可迭代,但若是在其上定義Symbol.iterator屬性,則它能夠變爲可迭代,以下例所示:

const collection = {
  a: 10,
  b: 20,
  c: 30,
  [Symbol.iterator]() {
    const values = Object.keys(this);
    let i = 0;
    return {
      next: () => {
        return {
          value: this[values[i++]],
          done: i > values.length
        }
      }
    };
  }
};

const iterator = collection[Symbol.iterator]();
  
console.log(iterator.next());    // → {value: 10, done: false}
console.log(iterator.next());    // → {value: 20, done: false}
console.log(iterator.next());    // → {value: 30, done: false}
console.log(iterator.next());    // → {value: undefined, done: true}
複製代碼

此對象是可迭代的,由於它定義了一個Symbol.iterator屬性。迭代器使用該Object.keys()方法獲取對象屬性名稱的數組,而後將其分配給values常量。它還定義了一個計數器變量i,並給它一個初始值0.當執行迭代器時,它返回一個包含next()方法的對象。每次調用next()方法時,它都返回一對{value, done}value保持集合中的下一個元素並done保持一個布爾值,指示迭代器是否已達到集合的須要。

雖然這段代碼天衣無縫,但卻沒必要要。使用生成器函數能夠大大簡化過程:

const collection = {
  a: 10,
  b: 20,
  c: 30,
  [Symbol.iterator]: function * () {
    for (let key in this) {
      yield this[key];
    }
  }
};

const iterator = collection[Symbol.iterator]();
  
console.log(iterator.next());    // → {value: 10, done: false}
console.log(iterator.next());    // → {value: 20, done: false}
console.log(iterator.next());    // → {value: 30, done: false}
console.log(iterator.next());    // → {value: undefined, done: true}
複製代碼

在這個生成器中,for...in循環用於枚舉集合併產生每一個屬性的值。結果與前一個示例徹底相同,但它大大縮短了。

迭代器的缺點是它們不適合表示異步數據源。ES2018的補救解決方案是異步迭代器和異步迭代。異步迭代器與傳統迭代器的不一樣之處在於,它不是以形式返回普通對象{value, done},而是返回履行的承諾{value, done}。異步迭代定義了一個返回異步迭代器的Symbol.asyncIterator方法(而不是Symbol.iterator)。

一個例子讓這個更清楚:

const collection = {
  a: 10,
  b: 20,
  c: 30,
  [Symbol.asyncIterator]() {
    const values = Object.keys(this);
    let i = 0;
    return {
      next: () => {
        return Promise.resolve({
          value: this[values[i++]], 
          done: i > values.length
        });
      }
    };
  }
};

const iterator = collection[Symbol.asyncIterator]();
  
console.log(iterator.next().then(result => {
  console.log(result);    // → {value: 10, done: false}
}));

console.log(iterator.next().then(result => {
  console.log(result);    // → {value: 20, done: false} 
}));

console.log(iterator.next().then(result => {
  console.log(result);    // → {value: 30, done: false} 
}));

console.log(iterator.next().then(result => {
  console.log(result);    // → {value: undefined, done: true} 
}));
複製代碼

請注意,不可以使用promises的迭代器來實現相同的結果。雖然普通的同步迭代器能夠異步肯定值,但它仍然須要同步肯定done的狀態。

一樣,您可使用生成器函數簡化過程,以下所示:

const collection = {
  a: 10,
  b: 20,
  c: 30,
  [Symbol.asyncIterator]: async function * () {
    for (let key in this) {
      yield this[key];
    }
  }
};

const iterator = collection[Symbol.asyncIterator]();
  
console.log(iterator.next().then(result => {
  console.log(result);    // → {value: 10, done: false}
}));

console.log(iterator.next().then(result => {
  console.log(result);    // → {value: 20, done: false} 
}));

console.log(iterator.next().then(result => {
  console.log(result);    // → {value: 30, done: false} 
}));

console.log(iterator.next().then(result => {
  console.log(result);    // → {value: undefined, done: true} 
}));
複製代碼

一般,生成器函數返回帶有next()方法的生成器對象。當調用next()時,它返回一個{value,done},其value屬性保存了yield值。異步生成器執行相同的操做,除了它返回一個履行{value,done}的promise。

迭代可迭代對象的一種簡單方法是使用for...of語句,可是for...of不能與async iterables一塊兒使用,由於valuedone不是同步肯定的。所以,ES2018提供了for...await...of。咱們來看一個例子:

const collection = {
  a: 10,
  b: 20,
  c: 30,
  [Symbol.asyncIterator]: async function * () {
    for (let key in this) {
      yield this[key];
    }
  }
};

(async function () {
  for await (const x of collection) {
    console.log(x);
  }
})();

// logs:
// → 10
// → 20
// → 30
複製代碼

在此代碼中,for...await...of語句隱式調用Symbol.asyncIterator集合對象上的方法以獲取異步迭代器。每次循環時,都會調用迭代器的next()方法,它返回一個promise。一旦解析了promise,就會將結果對象的value屬性讀取到x變量中。循環繼續,直到返回的對象的done屬性值爲true

請記住,該for...await...of語句僅在異步生成器和異步函數中有效。違反此規則會致使一個SyntaxError報錯。

next()方法可能會返回拒絕的promise。要優雅地處理被拒絕的promise,您能夠將for...await...of語句包裝在語句中try...catch,以下所示:

const collection = {
  [Symbol.asyncIterator]() {
    return {
      next: () => {
        return Promise.reject(new Error('Something went wrong.'))
      }
    };
  }
};

(async function() {
  try {
    for await (const value of collection) {}
  } catch (error) {
    console.log('Caught: ' + error.message);
  }
})();

// logs:
// → Caught: Something went wrong.
複製代碼

Support for Asynchronous Iterators

Chrome Firefox Safari Edge
63 57 12 No
Chrome Android Firefox Android iOS Safari Edge Mobile Samsung Internet Android Webview
63 57 12 No 8.2 63

Node.js

  • 8.0.0(運行時須要加-harmony
  • 8.3.0(徹底支持)

Promise.prototype.finally

ES2018的另外一個使人興奮的補充是finally()方法。一些JavaScript庫以前已經實現了相似的方法,這在許多狀況下證實是有用的。這鼓勵了Ecma技術委員會正式添加finally()到規範中。使用這個方法,程序員將能無論promise的結果如何,都能執行一個代碼塊。咱們來看一個簡單的例子:

fetch('https://www.google.com')
  .then((response) => {
    console.log(response.status);
  })
  .catch((error) => { 
    console.log(error);
  })
  .finally(() => { 
    document.querySelector('#spinner').style.display = 'none';
  });
複製代碼

finally()不管操做是否成功,當您須要在操做完成後進行一些清理時,該方法會派上用場。在此代碼中,該finally()方法只是在獲取和處理數據後隱藏加載微調器。代碼不是在then()catch()方法中複製最終邏輯,而是在promise被fulfilled或rejected後註冊要執行的函數。

你可使用promise.then(func,func)而不是promise.finally(func)來實現相同的結果,但你必須在fulfillment處理程序和rejection處理程序中重複相同的代碼,或者爲它聲明一個變量:

fetch('https://www.google.com')
  .then((response) => {
    console.log(response.status);
  })
  .catch((error) => { 
    console.log(error);
  })
  .then(final, final);

function final() {
  document.querySelector('#spinner').style.display = 'none';
}
複製代碼

then()catch()同樣,finally()方法老是返回一個promise,所以能夠連接更多的方法。一般,您但願使用finally()做爲最後一個鏈,但在某些狀況下,例如在發出HTTP請求時,最好連接另外一個catch()以處理finally()中可能發生的錯誤。

Support for Promise.prototype.finally

Chrome Firefox Safari Edge
63 58 11.1 18
Chrome Android Firefox Android iOS Safari Edge Mobile Samsung Internet Android Webview
63 58 11.1 No 8.2 63

Node.js

  • 10.0.0(徹底支持)

新的RegExp功能

ES2018爲該RegExp對象增長了四個新功能,進一步提升了JavaScript的字符串處理能力。這些功能以下:

  • S(DOTALL)標誌
  • Named Capture Groups(命名捕獲組)
  • Lookbehind Assertions(後向斷言)
  • Unicode Property Escapes(Unicode屬性轉義)

S(DOTALL)標誌

點(.)是正則表達式模式中的特殊字符,它匹配除換行符以外的任何字符,例如換行符(\n)或回車符(\r)。匹配全部字符(包括換行符)的解決方法是使用具備兩個相反短字的字符類,例如[\d\D]。此字符類告訴正則表達式引擎找到一個數字(\d)或非數字(\D)的字符。所以,它匹配任何字符:

console.log(/one[\d\D]two/.test('one\ntwo'));    // → true
複製代碼

ES2018引入了一種模式,其中點可用於實現相同的結果。可使用s標誌在每一個正則表達式的基礎上激活此模式:

console.log(/one.two/.test('one\ntwo'));     // → false
console.log(/one.two/s.test('one\ntwo'));    // → true
複製代碼

使用標誌來選擇新行爲的好處是向後兼容性。所以,使用點字符的現有正則表達式模式不受影響。

Named Capture Groups(命名捕獲組)

在一些正則表達式模式中,使用數字來引用捕獲組可能會使人困惑。例如,使用/(\d{4})-(\d{2})-(\d{2})/與日期匹配的正則表達式。因爲美式英語中的日期符號與英式英語不一樣,所以很難知道哪一個組指的是哪一天,哪一個組指的是月份:

const re = /(\d{4})-(\d{2})-(\d{2})/;
const match= re.exec('2019-01-10');

console.log(match[0]);    // → 2019-01-10
console.log(match[1]);    // → 2019
console.log(match[2]);    // → 01
console.log(match[3]);    // → 10
複製代碼

ES2018引入了使用(?<name>...)語法的命名捕獲組。所以,匹配日期的模式能夠用不那麼模糊的方式編寫:

const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = re.exec('2019-01-10');

console.log(match.groups);          // → {year: "2019", month: "01", day: "10"}
console.log(match.groups.year);     // → 2019
console.log(match.groups.month);    // → 01
console.log(match.groups.day);      // → 10
複製代碼

您可使用\k<name>語法在模式中稍後調用命名的捕獲組。例如,要在句子中查找連續的重複單詞,您可使用/\b(?<dup>\w+)\s+\k<dup>\b/

const re = /\b(?<dup>\w+)\s+\k<dup>\b/;
const match = re.exec('Get that that cat off the table!');        

console.log(match.index);    // → 4
console.log(match[0]);       // → that that
複製代碼

要將命名的捕獲組插入到方法的替換字符串中replace(),您須要使用$<name>構造。例如:

const str = 'red & blue';

console.log(str.replace(/(red) & (blue)/, '$2 & $1'));    
// → blue & red

console.log(str.replace(/(?<red>red) & (?<blue>blue)/, '$<blue> & $<red>'));    
// → blue & red
複製代碼

Lookbehind Assertions(後向斷言)

ES2018爲JavaScript帶來了後向性斷言,這些斷言已經在其餘正則表達式實現中可用多年。之前,JavaScript只支持超前斷言。後向斷言用表示(?<=...),並使您可以匹配基於模式以前的子字符串的模式。例如,若是要在不捕獲貨幣符號的狀況下以美圓,英鎊或歐元匹配產品的價格,則可使用/(?<=\$|£|€)\d+(\.\d*)?/

const re = /(?<=\$|£|€)\d+(\.\d*)?/;

console.log(re.exec('199'));     
// → null

console.log(re.exec('$199'));    
// → ["199", undefined, index: 1, input: "$199", groups: undefined]

console.log(re.exec('€50'));     
// → ["50", undefined, index: 1, input: "€50", groups: undefined]
複製代碼

還有一個lookbehind的否認版本,用(?<!...),只有當模式前面沒有lookbehind中的模式時,負lookbehind才容許您匹配模式。例如,模式/(?<!un)available/匹配沒有「un」前綴的可用詞

這段翻譯的很差,放上原文

There is also a negative version of lookbehind, which is denoted by (?<!...). A negative lookbehind allows you to match a pattern only if it is not preceded by the pattern within the lookbehind. For example, the pattern /(?<!un)available/ matches the word available if it does not have a "un" prefix:

Unicode Property Escapes(Unicode屬性轉義)

ES2018提供了一種稱爲Unicode屬性轉義的新類型轉義序列,它在正則表達式中提供對完整Unicode的支持。假設您要在字符串中匹配Unicode字符㉛。雖然㉛被認爲是一個數字,可是你不能將它與\d速記字符類匹配,由於它只支持ASCII [0-9]字符。另外一方面,Unicode屬性轉義可用於匹配Unicode中的任何十進制數:

const str = '㉛';

console.log(/\d/u.test(str));    // → false
console.log(/\p{Number}/u.test(str));     // → true
複製代碼

一樣,若是要匹配任何Unicode字母字符,你可使用\p{Alphabetic}

const str = 'ض';

console.log(/\p{Alphabetic}/u.test(str));     // → true

// the \w shorthand cannot match ض
  console.log(/\w/u.test(str));    // → false
複製代碼

還有一個否認版本\p{...},表示爲\P{...}

console.log(/\P{Number}/u.test('㉛'));    // → false
console.log(/\P{Number}/u.test('ض'));    // → true

console.log(/\P{Alphabetic}/u.test('㉛'));    // → true
console.log(/\P{Alphabetic}/u.test('ض'));    // → false
複製代碼

除了字母和數字以外,還有幾個屬性能夠在Unicode屬性轉義中使用。您能夠在當前規範提案中找到支持的Unicode屬性列表。

Support for New RegExp

* Chrome Firefox Safari Edge
S(DOTALL)標誌 62 No 11.1 No
命名捕獲組 64 No 11.1 No
後向斷言 62 No No No
Unicode屬性轉義 64 No 11.1 No
* Chrome Android Firefox Android iOS Safari Edge Mobile Samsung Internet Android Webview
S(DOTALL)標誌 62 No 11.3 No 8.2 62
命名捕獲組 64 No 11.3 No No 64
後向斷言 62 No No No 8.2 62
Unicode屬性轉義 64 No 11.3 No No 64

Node.js

  • 8.3.0 (運行時須要加-harmony)
  • 8.10.0 (support for s (dotAll) flag and lookbehind assertions)
  • 10.0.0 (徹底支持)

模板字符串

當模板字符串緊跟在表達式以後時,它被稱爲標記模板字符串。當您想要使用函數解析模板文字時,標記的模板會派上用場。請考慮如下示例:

function fn(string, substitute) {
  if(substitute === 'ES6') {
    substitute = 'ES2015'
  }
  return substitute + string[1];
}

const version = 'ES6';
const result = fn`${version} was a major update`;

console.log(result);    // → ES2015 was a major update
複製代碼

在此代碼中,調用標記表達式(它是常規函數)並傳遞模板文字。該函數只是修改字符串的動態部分並返回它。

在ES2018以前,標記的模板字符串具備與轉義序列相關的語法限制。反斜槓後跟某些字符序列被視爲特殊字符:\x解釋爲十六進制轉義符,\u解釋爲unicode轉義符,\後跟一個數字解釋爲八進制轉義符。其結果是,字符串,例如"C:\xxx\uuu"或者"\ubuntu"被認爲是由解釋無效轉義序列,並會拋出SyntaxError。

ES2018從標記模板中刪除了這些限制,而不是拋出錯誤,表示無效的轉義序列以下undefined

function fn(string, substitute) {
  console.log(substitute);    // → escape sequences:
  console.log(string[1]);     // → undefined
}

const str = 'escape sequences:';
const result = fn`${str} \ubuntu C:\xxx\uuu`;
複製代碼

請記住,在常規模板文字中使用非法轉義序列仍會致使錯誤:

const result = `\ubuntu`;
// → SyntaxError: Invalid Unicode escape sequence
複製代碼

Support for Template Literal Revision

Chrome Firefox Safari Edge
62 56 11 No
Chrome Android Firefox Android iOS Safari Edge Mobile Samsung Internet Android Webview
62 56 11 No 8.2 62

Node.js

  • 8.3.0 (運行時須要加-harmony
  • 8.10.0(徹底支持)

總結

咱們已經仔細研究了ES2018中引入的幾個關鍵特性,包括異步迭代,rest/spread屬性Promise.prototype.finally()以及RegExp對象的添加。雖然其中一些瀏覽器供應商還沒有徹底實現其中一些功能,但因爲像Babel這樣的JavaScript轉換器,它們今天仍然可使用。

ECMAScript正在迅速發展,而且每隔一段時間就會引入新功能,所以請查看已完成提案的列表,瞭解新功能的所有內容。

第一次翻譯文章,能力有限,水平通常,翻譯不妥之處,還望指正。感謝。

相關文章
相關標籤/搜索