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

Timbok發表於2019-01-15

前言

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

正文

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

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

rest/spread屬性

ES2015最有趣的功能之一是點差運算子。該運算子使複製和合並陣列變得更加簡單。您可以使用運算子...,而不是呼叫concat()or slice()方法:

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屬性,您可以將物件的自身可列舉屬性複製到新物件上。請考慮以下示例:

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()。請考慮以下程式碼:

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)標誌

點(.)是正規表示式模式中的特殊字元,它匹配除換行符之外的任何字元,例如換行符(
)或回車符(
)。匹配所有字元(包括換行符)的解決方法是使用具有兩個相反短字的字元類,例如[dD]。此字元類告訴正規表示式引擎找到一個數字(d)或非數字(D)的字元。因此,它匹配任何字元:

console.log(/one[dD]two/.test(`one
two`));    // → true

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

console.log(/one.two/.test(`one
two`));     // → false
console.log(/one.two/s.test(`one
two`));    // → 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>語法在模式中稍後呼叫命名的捕獲組。例如,要在句子中查詢連續的重複單詞,您可以使用/(?<dup>w+)s+k<dup>/

const re = /(?<dup>w+)s+k<dup>/;
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:xxxuuu“或者”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:xxxuuu`;

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

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正在迅速發展,並且每隔一段時間就會引入新功能,因此請檢視已完成提案的列表,瞭解新功能的全部內容。

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

相關文章