ES2018新特性(譯文)


原文鏈接:css-tricks.com

第9版ECMAScript標准於2018年6月發布,正式名稱為ECMAScript 2018(簡稱ES2018)。從ES2016開始,ECMAScript規范的新版本每年發布一次,而不是每隔幾年發布一次,相應的,每版增加的功能也更少一些。最新版本的標准通過添加4個新的RegExp特性、rest/spread屬性、異步迭代和Promise.prototype.finally來延續每年的發布周期。此外,ES2018取消了標記模板轉義序列的語法限制。

以下將逐一解釋這些變動。

Rest/Spread 特性


 

ES2015中添加的最有趣的特性之一是spread操作符。你可以用它替換cancat()和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]

在數組必須以拆解的方式作為函數參數的情況下,spread操作符也很有用。例如:

const arr = [10, 20, 30]

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

ES2018通過向對象文本添加擴展屬性進一步擴展了這種語法。他可以將一個對象的屬性拷貝到另一個對象上,參考以下情形:

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

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

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

在上述代碼中,spread操作符遍歷obj1屬性,並將其添加到obj2的屬性中;而在之前的版本中,如此處理會拋出一個異常。需要注意的是,如果存在相同的屬性名,只有最后一個會生效。

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屬性;而spread操作忽略了setter。

划重點:spread只復制枚舉屬性。在下面的例子中,type屬性不會出現在復制對象中,因為它的枚舉屬性被設置為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繼承了car中的color屬性。因為spread操作只會復制對象自身的屬性,color並沒有出現在新的對象中。

spread只會進行淺拷貝,如果屬性的值是一個對象的話,只有對象的引用會被拷貝:

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

console.log(copy1.x === copy2.x);    // → true

copy1.x 和 copy2.x 指向同一個對象的引用,所以他們嚴格相等。

ES2015增加的另一個有用特性是rest參數,它允許JS使用……將值表示為數組:

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屬性將剩余的可枚舉屬性復制到一個新對象中。注意,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

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 (完全支持)

 

異步迭代


 

遍歷是編程的一個重要部分。JS提供了for、for…in和while以及map()、filter()和forEach()等遍歷數據的方法。在ES2015則引入了迭代器接口。

包含Symbol.iterator屬性的對象是可迭代對象,如字符串和集合對象(如Set、Map和Array)。如下為迭代遍歷的示例:

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()方法,返回包含value和done屬性的對象。其中value為下一個元素,done為布爾值,表示遍歷是否結束。

普通對象進行迭代需要定義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}

對象的迭代器通過Object.keys()方法獲取屬性名數組,將其賦值給values常量,同時定義一個默認值為0的計數器。當迭代器開始執行時,會返回一個包含next()方法的對象。該方法會返回包含value和done的對象,value為下一迭代值,done為布爾值,表示迭代器是否到達終點。

上述實現方式還是過於復雜,可以通過generator函數簡化:

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}

在該generator函數中,利用for in循環枚舉生成屬性值。結果與前面的示例完全相同,但是要短得多。

迭代器的缺點是不適合表示異步數據源。ES2018的解決方案是異步迭代器和異步迭代。異步迭代器與傳統迭代器的不同之處在於,它沒有返回{value, done}形式的普通對象,而是返回一個Promise,其resolve返回{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} 
}));

注意,promise+迭代器並不能代替異步迭代器。雖然一個普通的同步迭代器可以異步地確定值,但是它仍然需要同步地確定“完成”的狀態。

當然,您同樣可以使用generator函數簡化該過程,如下所示:

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值則變為一個promise對象

在可迭代對象上迭代的一個簡單方法是使用for of,但由於異步迭代對象的value和done並不是同步指定的,因此for of並不適用。基於此,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()方法可能返回一個包含rejects的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.

異步迭代器支持

 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\ async\ iteration標志)

  • 8.3.0 (完全支持)

 

Promise.prototype.finally


ES2018的另一個令人興奮的新特性是finally()方法。幾個JavaScript庫以前實現過類似的方法,這在許多情況下都很有用。這鼓勵Ecma技術委員會正式將finally()添加到規范中。無論promise的結果如何,finally()方法中的代碼都會執行。讓我們看一個簡單的例子:

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()方法在請求數據之后隱藏loading,無論請求是否成功。

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

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()中可能出現的錯誤。

Promise.prototype.finall支持

 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 (完全支持)

 

新的正則表達式特性


ES2018為正則表達式添加了四個新特性,進一步提高了JavaScript的字符串處理能力。這些特點如下:

  • s (dotAll) 標志

  • 命名捕獲組

  • Lookbehind 后行斷言

  • 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

使用標志位來定義新行為的好處是向后兼容性。因此,使用點字符的現有正則表達式模式不受影響。

命名捕獲組

在一些正則表達式模式中,使用數字進行匹配可能會令人混淆。例如,使用正則表達式/(\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引入了使用(?…)語法的命名捕獲組。因此,匹配日期的模式可以用一種不那么模棱兩可的方式來寫:

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語法重復調用名稱捕獲組。例如,要在一個句子中找到連續重復的單詞,可以使用/\b(?\w+)\s+\k\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()方法的替換字符串中,需要使用$構造。例如:

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后行斷言

ES2018將lookbehind后行斷言引入JavaScript,以前JavaScript只支持前行斷言。后行斷言由(?<=…)表示,代表字符串中的一個位置,緊接該位置之前的字符序列能夠匹配pattern。例如,如果您想匹配以美元、英鎊或歐元表示的產品的價格,而不需要捕獲貨幣符號,您可以使用/(?<=\$|£|€)\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]

還有一種負向后行斷言,表示為(?<!…),代表字符串中的一個位置,緊接該位置之前的字符序列不能匹配pattern。例如,如果模式/(?<!un)available/沒有“un”前綴,那么它將匹配可用的單詞:

const re = /(?<!un)available/;

console.log(re.exec('We regret this service is currently unavailable'));    
// → null

console.log(re.exec('The service is available'));             
// → ["available", index: 15, input: "The service is available", groups: undefined]

Unicode 屬性轉義

ES2018提供了一種新的轉義序列類型,稱為Unicode屬性轉義,可以匹配所有的Unicode。你可以使用\p{Number}來匹配所有的Unicode數字,例如,假設你想匹配的Unicode字符㉛字符串:

const str = '㉛';

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

同樣的,你可以使用\p{Alphabetic}來匹配所有的Unicode單詞字符:

const str = 'ض';

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

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

同樣有一個負向的Unicode屬性轉義模板 \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屬性列表。

新正則表達式支持

  Chrome Firefox Safari Edge
s (dotAll) Flag 62 No 11.1 No
Named Capture Groups 64 No 11.1 No
Lookbehind Assertions 62 No No No
Unicode Property Escapes 64 No 11.1 No

 

 

 

 

 

 

Chrome

(Android)

Firefox

(Android)

iOS

Safari

Edge

Mobile

Samsung

Internet

Android

Webview

s (dotAll) Flag 62 No 11.3 No 8.2 62

Named

Capture

Groups

64 No 11.3 No No 64

Lookbehind

Assertions

62 No No No 8.2 62

Unicode

Property

Escapes

64 No 11.3 No No 64

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Node.js:

  • 8.3.0 (需要 --harmony 標志)

  • 8.10.0 (支持 s (dotAll) 標志和后行斷言)

  • 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、unicode轉義的\u和八進制轉義的\u。因此,像“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

模板文字修訂支持

 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:

  • 8.3.0 (需要 --harmony 標志)

  • 8.10.0 (全部支持)

 

總結

我們已經很好地了解了ES2018中引入的幾個關鍵特性,包括異步迭代、rest/spread屬性、Promise.prototype.finally()以及正則表達式新特性的添加。盡管一些瀏覽器廠商還沒有完全實現其中的一些特性,但是仍然可以用諸如Babel之類轉義器進行使用。

ECMAScript正在快速發展,經常會有新特性被引入,有興趣可以查詢已完成提案列表,了解全部最新內容。有沒有什么新功能讓你特別興奮?在評論中分享吧!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM