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

ECMAScript標準的第九版,官方稱爲ECMAScript2018(或簡稱ES2018),於2018年6月發佈。從ES2016開始,ECMAScript規範的新版本每年發佈而不是每幾年發佈,並且添加的功能少於主要版本以前。該標準的最新版本通過添加四個新RegExp功能,休息/傳播屬性,異步迭代和,繼續每年發佈週期Promise.prototype.finally。此外,ES2018從標記模板中刪除了轉義序列的語法限制。

其餘/傳播屬性

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}

但請注意,傳播屬性並不總是產生相同的結果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屬性只複製可枚舉的屬性。在以下示例中,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

該x物業copy1是指在內存中的同一對象x中copy2是指,所以全等運算的回報true。

ES2015增加的另一個有用功能是休息參數,它使JavaScript程序員可以使用它...來表示值作爲數組。例如:

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

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

這裏,第一個項目arr被分配給x,而剩餘的元素被分配給rest變量。這種稱爲陣列解構的模式變得如此受歡迎

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

clipboard.png

Node.js的:

8.0.0(需要--harmony運行時標誌)
8.3.0(全力支持)

異步迭代

迭代數據集是編程的重要部分。此前ES2015,提供的JavaScript語句如for,for...in和while,和如方法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()方法。此方法返回具有兩個屬性的對象:value和done。該value屬性包含集合中下一個元素的值。該done屬性包含true或false表示集合的結尾是否已到達。

默認情況下,普通對象不可迭代,但如果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常量。它還定義了一個計數器變量並給它一個初始值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}。異步iterable定義了一個返回異步迭代器的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的迭代器來實現相同的結果。雖然普通的同步迭代器可以異步確定值,但它仍然需要同步確定“完成”的狀態。

同樣,你可以使用生成器函數簡化過程,如下所示:

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 {value, done}。

一個簡單的方法來遍歷一個迭代的對象是使用for...of的語句,但for...of不與異步iterables的工作value和done不同步確定。因此,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語句僅在異步生成器和異步函數中有效。違反此規則會導致a SyntaxError。

該next()方法可以返回拒絕的承諾。要優雅地處理被拒絕的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.

clipboard.png

Node.js的:

8.10.0(需要--harmony_async_iteration標誌)
10.0.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.then(func, func)而不是實現相同的結果promise.finally(func),但是必須在履行處理程序和拒絕處理程序中重複相同的代碼,或者爲它聲明一個變量:

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()。

clipboard.png

新的RegExp功能

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

s(dotAll)標誌
命名捕獲組
Lookbehind斷言
Unicode屬性轉義

S(DOTALL)標誌

dot(.)是正則表達式模式中的特殊字符,它匹配除換行符之外的任何字符,例如換行符(n)或回車符(r)。匹配所有字符(包括換行符)的解決方法是使用具有兩個相反短字的字符類,例如[dD]。此字符類告訴正則表達式引擎找到一個數字(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引入了使用(?<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

後向斷言

ES2018爲JavaScript帶來了後瞻性斷言,這些斷言已在其他正則表達式實現中提供多年。以前,JavaScript只支持超前斷言。lookbehind斷言用表示(?<=...),並能夠根據模式之前的子字符串匹配模式。例如,如果要在不捕獲貨幣符號的情況下以美元,英鎊或歐元匹配產品的價格,您可以使用/(?<=&dollar;|£|€)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,用表示(?<!...)。負外觀允許您僅在模式不在後面的模式之前匹配模式。例如,如果模式/(?<!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的支持。假設要匹配字符串中的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屬性轉義中使用。

模板文字修訂

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

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:\xxx\uuu`;

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

const result = `\ubuntu`;
// → SyntaxError: Invalid Unicode escape sequence

結束

ES2018中引入的幾個關鍵特性,包括異步迭代,休息/擴展屬性Promise.prototype.finally()以及RegExp對象的添加。雖然有些瀏覽器供應商尚未完全實現其中一些功能,但由於像Babel這樣的JavaScript轉換器,它們今天仍然可以使用。

ECMAScript正在迅速發展,並且每隔一段時間就會引入新功能,因此請查看完整提案列表,瞭解新功能的全部範圍。你有什麼特別興奮的新功能嗎?在評論中分享!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章