詳解JavaScript ES6~11新語法

前言

JavaScript是前端三劍客中的程式語言,獨佔前端市場
作者Brendan Eich只花了10天做出來的語言
關於他的歷史,可以參考阮一峰:Javascript诞生记
知道他的出生,就可以理解為何時至今日,仍有許多修修補補(es6~es11),讓js更能應付當今複雜的業務需求

js的缺陷有多少,我想看了下圖就可以理解了

多少開發者犧牲無數個夜晚,就是在這些坑裡打滾…

雖然現在已經有了TypeScript讓開發者可以避開js的許多缺陷
但最終仍是會回歸到JavaScript,亦可以在ts裡直接使用js原生各種語法

直至po文時的最新版本es11,可以當作是更多更好用的語法糖來輔助開發者撰寫程式

ECMAScript

而這些語法又是怎麼決定的呢?就是ECMAScript

ECMA規範是由包括瀏覽器廠商在內的各方組成,他們開會推動JavaScript提案,最終入選會有以下幾個階段:

  • Stage 0 strawman:最初想法的提交
  • Stage 1 proposal(提案):至少一名成員提供的正式提案文件,該文件包括API實例
  • Stage 2 draft(草案):功能規範的初始版本,該版本包含功能規範的兩個實驗實現
  • Stage 3 candidate(候選):提案規範通過審查並從廠商那裡收集回饋
  • Stage 4 finished(完成):提案準備加入ECMAScript。要到瀏覽器或Node.Js,可能要更長的時間

ES6

1. CLASS

JavaScript是採用原型鏈的語言
早期都是透過原型鏈做出類似OO的概念
寫法相當複雜。在ES6終於推出了CLASS

 class Animal {
   constructor(name, color) {
     this.name = name;
     this.color = color;
   }
   // 此為原型鏈上的屬性
   toString() {
     console.log('name:' + this.name + ', color:' + this.color);

   }
 }

var animal = new Animal('小黃', '黃色');//實例化
animal.toString(); // name: 小黃, color: 黃色

console.log(animal.hasOwnProperty('name')); //true
console.log(animal.hasOwnProperty('toString')); // false
console.log(animal.__proto__.hasOwnProperty('toString')); // true

class Cat extends Animal {
 constructor(action) {
   // 子類必須在constructor中呼叫super函數,否則在new出來時會報錯
   // 如果原本就沒寫constructor,預設自帶super的constructor會自動產生
   super('cat','white');
   this.action = action;
 }
 toString() {
   console.log(super.toString());
 }
}

var cat = new Cat('catch')
cat.toString();

console.log(cat instanceof Cat); // true
console.log(cat instanceof Animal); // true

2. 模組(Module)

每個模組有自己的命名空間避免衝突,使用importexport來匯入匯出
基本上以一個.js檔案視為一個模組

具體細節可以參考以前整理的筆記:[淺談JavaScript ES6的import與import{}及export及export default使用方式]
(https://blog.typeart.cc/%E6%B7%BA%E8%AB%87JavaScript%20ES6%E7%9A%84import%E8%88%87import%7B%7D%E5%8F%8Aexport%E5%8F%8Aexport%20default%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F/)

3. 箭頭函數 (arrow funciton)

() => {...},是function的縮寫。最重要的是,他可以確保this永遠指向自己
再也不需要寫var self = thisvar that = this之類的東西了!

const add = (a, b) => { return a + b};

const res = add(1, 2); // 3

// 若語法簡單的話,可以省略`{}`與`return`。看起來會更加俐落
const minus = (a, b) => a - b;
const res1 = minus(3, 1); // 2

4. 函式參數預設值

在函式若不傳入參數時,則使用預設值。更加精簡寫法

function example(height = 50, width = 40) { 
     const newH = height * 10;
     const newW = width * 10;
     return newH + newW;
}

example(); // 900 (50*10 + 40*10)

5. 模板字串

長字串的組成,在以往都是透過+來串接
其可讀性相當糟。有了模板字串,就會好閱讀許多

const firstName = 'Andy';
const lastName = 'Chou';
// 不使用模板字串
const name = 'Hello, My name is' + firstName + ', ' + lastName;
// 使用模板字串
const nameWithLiteralString = `Hello, My name is ${firstName}, ${lastName}`;

甚至還可以做些簡單運算或函式呼叫

const example = `1 + 2 = ${1 + 2}` // 輸出:1 + 2 = 3

const arr = [1, 2, 3];
const exampleArr = `陣列資料: ${arr.join('-')}` // 陣列資料: 1-2-3

6. 解構賦值

讓JavaScript可以方便的從陣列、物件裡取得內容

const arr = [1, 2, 3, 4, 5];
const [one, two, three] = arr;
console.log(one); // 1
console.log(two); // 2
console.log(three); // 3

// 若要跳過某些值
const [first,,,,last] = arr;
console.log(first); // 1
console.log(last); // 5

// 物件也可以解構賦值
const student = { 
    name: '小明', 
    age: 18,
    city: 'Taipei'
};
const {name, age, city} = student;
console.log(name); // "小明"
console.log(age); // "18"
console.log(city); // "Taipei"

透過解構賦值,可以很方便的做資料交換

let a = 1;
let b = 3;
[a, b] = [b, a];
console.log(a); // 3
console.log(b); // 1

若無法從陣列取到值,可以設定預設值

let a, b;

[a=3, b=4] = [1];
console.log(a); // 1
console.log(b); // 4

7. Spread operator

就是...,可以將Array展開,若是Object時,則依key-value展開

const stuendts = ['Andy', 'Ben']; 
const people = ['Cara', ...stuendts, 'Daniel', 'Eason'];
conslog.log(people); // ["Cara", "Andy", "Ben", "Daniel", "Eason"]

陣列的拷貝

const arr = [1, 2, 3];
const arr2 = [...arr]; // [1, 2, 3]。寫法等價於arr.slice()
arr2.push(4); 
console.log(arr2); //[1, 2, 3, 4]
console.log(arr); // [1, 2, 3]
console.log(arr === arr2); // false

串接陣列

const arr = [1, 2, 3];
const arr2 = [4, 5];
const arr3 = [...arr, ...arr2]; // [1, 2, 3, 4, 5];  寫法等價於 arr3 = arr.concat(arr2);

在es9時,可以做為物件的淺拷貝

const sourceObj = {a: 1, b: 2};
const copiedObj = {...sourceObj}; // {a: 1, b: 2}
sourceObj === copiedObj; // false

es9也提供了串接物件

const obj1 = {a:1, b: 2};
const obj2 = {c:3, d: 4};
const obj3 = {...obj1, ...obj2}; // {a:1, b: 2, c:3, d: 4}

8. 物件(Object)屬性簡寫

若要組成物件的欄位名稱與前文中的變數一樣時,可以省略值。看起來更加精簡

const name = '小明', age = 18, city = '台北';

// ES6之前必須這樣寫
const customer = {
    name: name,
    age: age,
    city: city
} // // {name: '小明', age: 18, city: '台北'}

// es6後,可以直接這樣寫
const newCustomer = {
    name,
    age,
    city
} // {name: '小明', age: 18, city: '台北'}

9. Promise

Promise是異步(非同步)寫法的一種解決方式,相比原本的callback寫法更加優雅
早期是開源社群的套件,後來納入語言標準

早期的 callback hell…

使用promise後,把callbackk hell壓平了

const waitSecond = new Promise((resolve, reject) => {
    setTimeout(resolve, 1000);
});
waitSecond.then( () => {
    console.log('hello World after 1 second.'); // 1秒後輸出此行
    return waitSecond;
}).then( () => {
    console.log('Hell World after 2 sceond.');  // 2秒後輸出此行
})

而es8則再釋出更加完美的async, await,直接把異步寫法變得像同步一樣
缺點就是,思緒落在複雜的業務邏輯時,有時會遺漏了await,到runtime才發現錯誤XD

10. let 與 const 取代var

let: 一般變數,可以被蓋掉
const: 宣告後,就不能修改其內容。陣列、物件因為是指標,所以可以對其內容增減。但不得變更其指標

在早期,js的var作用域是全域的
也就是變數你在使用到之後才宣告,在執行時,會自動提到最上層,而到後面才賦值
也更容易會有污染問題

console.log(a); // 輸出undefined
var a = 10;

使用let或const後,提早發現問題

console.log(a); // 拋出 ReferenceError: Cannot access 'a' before initialization
let a = 10;

或著將變數限制在區塊內

{ var a = 10; }
console.log(a); // 輸出10

使用let

{ let a = 10; }
console.log(a); // 拋出 ReferenceError: a is not defined

const、let 就是要完全取代var。
沒有特殊操作的話,就別在使用var了

ES7 (2016)

1. Array.prototype.includes()

用來判斷陣列裡是否包含指定的值,若是的話回傳true;否則回傳false
跟以前indexOf的用法一樣,可以想成變成回傳boolean,在語意上更加明確

const arr = [1, 2, 3, 4, 5];
// 檢查陣列裡是否有3這個數字
arr.include(3); // true

if (arr.include(3)) { ... }
// ... 等同於以前indexOf的寫法
arr.indexOf(3); // 2 (回傳其陣列位置)
// 要寫在if裡的話,則須加 `> -1`,語意上就不如ES7裡的include來得明確
if (arr.indexOf(3) > -1) { ... }

2. 指數操作

使用**代表指數,等同於Math.pow()語法

console.log(2**10); // 1024
// 等同於
console.log(Math.pow(2, 10)); // 1024

ES8 (2017)

1. 非同步寫法((異步寫法): async, await

台灣習慣叫非同步寫法,但我個人更喜歡大陸翻譯的異步寫法。覺得念起來較俐落
js最可怕的就是callback hell,層層嵌套的callback寫法,著實讓人吃不消
寫完放置1個月,自己再回來讀,沒有註解幫忙的話,都不一定能理解自己再寫些什麼
asyncawait則是直接彌平了callback hell,看起來就像同步寫法
而用起來也相當簡單,也可以直接使用try-catch捕捉錯誤

async test() {
    try {
        const result = await otherAsyncFunction();
        console.log(result); // 輸出結果
    } catch(e) {
        console.log(e); // 若otherAsyncFunction()拋錯,可以捕捉錯誤
    }
}

2. Object.values()

Object.values()回傳的是Object自身屬性的所有值,不包括繼承來的值

const exampleObj = {a: 1, b: 2, c: 3, d:4};
console.log(Object.value(exampleObj)); // [1, 2, 3, 4];

// 若以前要做到同樣的事情,須用下列寫法。冗長許多
const values = Object.keys(exampleObj).map(key => exampleObj[key]);

3. Object.entries()

Object.entries()回傳傳入物件自身可列舉的key, value

const exampleObj = {a: 1, b: 2, c: 3, d:4};
console.log(Object.entries(exampleObj)); // [["a", 1], ["b", 2], ["c", 3], ["d", 4]];

// 通常會跟for搭配使用
for (const [key, value] of Object.entries(exampleObj)) {
	console.log(`key: ${key}, value: ${value}`);
}
// key: a, value: 1
// key: b, value: 2
// key: c, value: 3
// key: d, value: 4

4. String padStart() 和 padEnd()

可以將字串開頭或結尾增加其他內容,並填到指定長度
在之前這些功能通常是引入萬用輔助套件(如lodash)會一併擁有
現在原生語法直接提供

String.padStart(填充長度, 要填充的內容);
// 如果要填充的內容太多,超過「填充長度」,則從最左邊開始填到長度上限,多餘的部份將被截斷

最常使用的情況應該就是金額了吧,填到指定長度,不足補0

// padStart
'100'.padStart(5, 0); // 00100
// 如果要填充的內容超過「填充長度」。則從最左邊開始填到長度上限
'100'.padStart(5, '987'); // 98100

// padEnd
'100'.padEnd(5, 9); // 10099
// 如果要填充的內容超過「填充長度」。則從最左邊開始填到長度上限
'100'.padStart(5, '987'); // 10098

5. 函數參數列表结尾允许逗號

主要是方便git版控降低不必要的行數變更
以Angular為例

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'], // <--這裡的最後一個「,」
})
export class AppComponent { }

6. Object.getOwnPropertyDescriptors()

取得自身的Descriptor。一般開發業務需求通常不太會用到

const exampleObj = {a: 1, b: 2, c: 3, d:4};

Object.getOwnPropertyDescriptors(exampleObj);
// {a: {…}, b: {…}, c: {…}, d: {…}}
// a: {value: 1, writable: true, enumerable: true, configurable: true}
// b: {value: 2, writable: true, enumerable: true, configurable: true}
// c: {value: 3, writable: true, enumerable: true, configurable: true}
// d: {value: 4, writable: true, enumerable: true, configurable: true}
// __proto__: Object

7. SharedArrayBuffer 物件

SharedArrayBuffer 是一固定長度的原始二進位資料緩沖區,類似於 ArrayBuffer
都可以用來在shared memory 上建立資料。與 ArrayBuffer 不同的是,SharedArrayBuffer 不能被分離

/**
 * @param length 大小,單位是byte
 * @returns {SharedArrayBuffer} 其内容初始化為0。
 */
const sab = new SharedArrayBuffer(length);

8. Atomics 物件

Atomics 物件,提供了一组靜態方法對 SharedArrayBuffer 進行原子操作
Atomics 所有屬性與函式都是靜態的,就像Math一樣。不能new出來
若是multi-thread同時讀寫同一位置的資料,原子操作保證正在操作的資料符合預期:即一定會在前一個前子操作結束後才會進行下一個。操作過程不會被中斷

可以說是因應Node.Js 開發 multi-thread Server 而補強的功能,在前端開發上用到機會頗低
chrome已經提供支援

ES9 (2018)

1. 在迴圈中使用await

在async function裡,有時會需要在同步寫法的for-loop使用異步(非同步)函式

async function process(array) {
  for (const i of array) {
    await doSomething(i);
  }
}

async function process(array) {
  array.forEach(async i => {
    await doSomething(i);
  });
}

以上程式碼不會如期望的輸出想要的結果
for-loop本身仍是同步,會在迴圈內的異步函式完成前,執行完整個for迴圈,接著才把裡面的異步函式逐一執行

ES9增加了asynchronous iterators,讓await可以和for-loop一起使用,逐步執行異步操作

async function process(array) {
  for await (const i of array) {
    doSomething(i);
  }
}

2. Promise.finally()

Promise不論成功(.then())或失敗(.catch()),都會再往後執行的部份

function process() {
  process1()
  .then(process2)
  .then(process3)
  .catch(err => {
    console.log(err);
  })
  .finally(() => {
    console.log(`不論前面成功或失敗,此處一定會執行!`);
  });
}

3. Rest、Spread 不定長度參數

在ES2015引入了Rest不定長度參數 ...,可以將不定長度的參數轉成陣列傳入

function restParams(p1, p2, ...p3) {
    console.log(p1); // 1
    console.log(p2); // 2
    console.log(p3); // [3, 4, 5]
}
restParams(1, 2, 3, 4, 5);

而spread則是與rest相反,將陣列轉換成單獨參數
如Math.max()回傳傳入數字中的最大值

const values = [19, 90, -2, 6, 25];
console.log( Math.max(...values) ); // 90

也為Object提供解構賦值的功能

const myObject = {
  a: 1,
  b: 2,
  c: 3
};
const { a, ...r } = myObject;
// a = 1
// r = { b: 2, c: 3 }

// 亦可以用在function入參裡
function restObjectInParam({ a, ...r }) {
    console.log(a); // 1
    console.log(r); // {b: 2, c: 3}
}

restObjectInParam({
  a: 1,
  b: 2,
  c: 3
});

與陣列一樣,spread參數只能在宣告的结尾處使用

spread參數可以在其他object使用

const obj1 = { a: 1, b: 2, c: 3 };
const obj2 = { ...obj1, d: 4 };
// obj2 = { a: 1, b: 2, c: 3, d: 4 }

spread可以做為object的淺拷貝。像是obj2 = { ...obj1 }

4. 正則表達式(RegExp)分組命名

RegExp可以回傳匹配的分組

const regExpDate = /([0-9]{4})-([0-9]{2})-([0-9]{2})/;
const match      = regExpDate.exec('2020-06-25');
const year       = match[1]; // 2020
const month      = match[2]; // 06
const day        = match[3]; // 25

但這樣較難閱讀
在ES9裡則提供在(前使用?<name>來命名分組

const regExpDate = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;
const match      = regExpDate.exec('2020-06-25');
const year       = match.groups.year; // 2020
const month      = match.groups.month; // 06
const day        = match.groups.day; // 25
// 以上任意若匹配失敗,則回傳undefined

也可以使用在replace()。如將日期轉換成美國的mm-dd-yyyy格式

const regExpDate = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;
const d          = '2020-06-25';
const usDate     = d.replace(regExpDate, '$<month>-$<day>-$<year>'); // 06-25-2020

5. 正則表達式(RegExp)反向斷言

JavaScript原本是先行斷言(lookahead),代表會成功匹配,但不會將他納入結果

const reLookahead = /\D(?=\d+)/;
const match       = reLookahead.exec('$123.45');
console.log(match[0]); // $
console.log(match[1]); // undefiled。不會納入後面的數字結果

es9增加反向斷言,就可以忽略前方符號,只單純取得後面數字

const reLookbehind = /(?<=\D)\d+/;
const match       = reLookbehind.exec('$123.45');
console.log(match[0]); // 123

以上皆是肯定斷言。前面寫的非數字\D必須存在,否則會匹配失敗
有肯定就會有否定,也存在著否定反向斷言,表示一個值必須不存在

const reLookbehindNegative = /(?<!\D)\d+/;
const match                = reLookbehindNegative.exec('$123.45');
console.log(match[0]); // 23

6. 正則表達式(RegExp) dotAll模式

.代表匹配除了enter的任何符號,加上s flag後,允許匹配enter

/hello.world/.test('hello\nworld');  // false
/hello.world/s.test('hello\nworld'); // true

7. 正則表達式(RegExp) Unicode 轉義

格式為\p{...}\P{...},並在RegExp中增加u flag。
p的區塊內可以以key-value的方式設定要匹配的屬性,不一定是具體內容

const reGreekSymbol = /\p{Script=Greek}/u;
reGreekSymbol.test('π'); // true

可以避免使用特定Unicode區間來判斷,提升可讀性

ES10 (2019)

1. 行分隔符(U + 2028)和段分隔符(U + 2029)現在允許存在字串文字裡,與json匹配

以前會視為終止符號,導致SyntaxError

2. 更加友好的 JSON.stringify

如果輸入 Unicode 格式但超出範圍,原先JSON.stringify會回傳格式錯誤的的Unicode字串。
現在第3階段提案,使其成為有效的Unicode,並以UTF-8呈現

3. Array增加的flat()flatMap()

flat() 壓平陣列

const arr1 = [1, 2, [3, 4]];
arr1.flat(); // [1, 2, 3, 4]

const arr2 = [1, 2, [3, 4, [5, 6]]];
arr2.flat(); // [1, 2, 3, 4, [5, 6]]
// 在flat 傳入數字,代表壓平深度
arr2.flat(2); // [1, 2, 3, 4, 5, 6]

flatMap(),相當於reduce + concat,可以壓平一個維度

let arr = ["早安", "", "今天天氣不錯"]

arr.map(s => s.split(""))
// [["早", "安"], [""], ["今", "天", "天", "氣", "不", "錯"]]

arr.flatMap(s => s.split(''));
// ["早", "安", "", "今", "天", "天", "氣", "不", "錯"]

4. String增加trimStart()trimEnd()

字串去頭或去尾空白

5. Object.fromEntries()

Object.fromEntries()Object.entries() 的反轉
透過Object.fromEntries(),可以將Map或Array轉成Object

// 將 Map 轉成 object
const map = new Map([ ['a', 'b'], ['c', 25] ]); // Map(2) {"a" => "b", "c" => 25}
const obj = Object.fromEntries(map);
console.log(obj); // { a: "b", c: 25 }
// 將 Array 轉成 object
const arr = [ ['0', 'a'], ['1', 'b'], ['2', 'c'] ];
const obj = Object.fromEntries(arr);
console.log(obj); // { 0: "a", 1: "b", 2: "c" }

6. Symbol.prototype.description

若不熟悉Symbol,這篇寫的還不錯:ES6 Symbol 到底有什么用?

以前要得到Symbol的字串型式,需再轉型成字串類型

const sym = Symbol('The description');
String(sym) === 'Symbol(The description)'; // true
sym.toString() === 'Symbol(The description)'; // true

現在則是提供使用description 可以直接取得

sym.description === 'The description'; // true

7. String.prototype.matchAll

matchAll會得到匹配正則表達式分組結果的迭代器。傳入的正則表達式需有g flag
可以直接透過使用for-of loop 或 Array.from()逐一取得資料

const regexp = /foo*/g; 
const str = 'baseball, football, foot';
const matches = str.matchAll(regexp);

for (const match of matches) {
  console.log(match);
}
// 或
Array.from(matches, m => m);

// [Array(1), Array(1)]
// 0: ["foo", index: 10, input: "baseball, football, foot", groups: undefined]
// 1: ["foo", index: 20, input: "baseball, football, foot", groups: undefined]

8.Function.prototype.toString()回傳精確說明,包含空格、註解

function /* 註解 */ foo /* 第二個 註解 */() {}

// 之前不會輸出註解
console.log(foo.toString()); // function foo(){}

// es2019 後,則會輸出註解
console.log(foo.toString()); // function /* 註解 */ foo /* 第二個 註解 */() {}

// arrow fuction亦同
const arrow  = /* 註解 */ () /* 第二個 註解 */ => {};

console.log(arrow.toString()); // () /* 第二個 註解 */ => {}
// 注意,只有輸出`() =>`` 之間的註解,`()`前面的註解並不會輸出唷!

9. 修改 catch 綁定

在之前使用catch,不論是否有用到,都一定要傳入一個e參數代表接到的錯誤
現在則若用不到,可以省略不傳

try {...} catch(e) {...}

// 若用不到e,可以省略不傳
try {...} catch {...}

10. 新的基本數字類型:BigInt

ES5 之前的基本類型有5種:String、Number、Boolean、Null、Undefined
ES6 增加:Symbol,6種
ES10 再增加:BigInt,達到7種

BigInt可以用任意精度表示整數
使用BigInt可以安全的儲存、操作大整數。以避免這個數字超過Number能夠表示的安全整數範圍

具體細節可以參考BigInt:JavaScript 中的任意精度整数

ES11 (2020)

1. Promise.allSettled

Promise.all可以讓多個promise都處理完後再進行到下一步
但最大的問題:其中一個失敗時,就會全部進入catch區塊
無法針對部份成功繼續處理
allSettled就是為了解決這個問題而新出來的語法

Promise.all([
    Promise.reject({code: 500, msg: '內部錯誤'}),
    Promise.resolve({ code: 200, list: [1, 2, 3]}),
    Promise.resolve({code: 200, list: ['a', 'b', 'c']})
]).then((res) => {
    // 若其中一個reject,則不會進入
    renderData(res);
})
.catch((error) => {
    // 會直接到catch區塊
    showErrorMessage({code: 500, msg: '內部錯誤'});
})
Promise.allSettled([
    Promise.reject({code: 500, msg: '內部錯誤'}),
    Promise.resolve({ code: 200, list: [1, 2, 3]}),
    Promise.resolve({code: 200, list: ['a', 'b', 'c']})
])
.then((res) => {
    /*
        0: {status: "rejected", reason: {…}}
        1: {status: "fulfilled", value: {…}}
        2: {status: "fulfilled", value: {…}}
    */
    // 部份失敗時,仍會進來。再手動排除取得失敗的資料
    renderData(res.filter(r => r.status !== 'rejected'));
});

2. 可選鏈(Optional chaining) ?

在開發中,很容易遇到檢查資料是否存在而在前面先寫if判斷

const isUserExist = user && user.info;
if (isUserExist) { 
    username = user.info.name; 
}

若回來的資料是null或著user物件底下沒有.info,就會拋Uncaught TypeError: Cannot read property... 這錯誤
導致程式無法繼續執行

有了?後,語法就精簡多了

const username = user?.info?.name;

若存在,則得到name的值,不存在,賦予undefined

搭配||使用,只要一行搞定!

const username = user?.name || 'guest';

3. 空值合併運算(Nullish coalescing Operator) ??

在js中,碰到0nullundefined,會自動轉型成false
但有時0其實是正常值,只能容錯到undefinednull

/**
 * user = {
 *    level: 0
 * }
 */
const level = user.level || '查無等級'; // 會變成「查無等級」,而不是期望的0
// 為了解決這問題,必須展開使用 if 簡單式處理
const level = user.level !== undefined && user.level !== null ? user.level : '查無等級';

但有了??後,就可以保持精簡的寫法

const username = user.level ?? '查無等級'; // 0 。若level值不存在,則變成「查無等級」

4. dynamic-import 動態載入

就是字面上的意思,應該很容易理解。就是有需要時再載入相關邏輯

el.onclick = () => {
    import(`/js/current-logic.js`)
    .then((module) => {
        module.doSomthing();
    })
    .catch((err) => {
        handleError(err);
    })
}

5. globalThis

JavaScript在不同環境對全局有不同的方式
有用Node.Js 開發 Server 程式的話應該都知道
在Node.Js中透過global,web則是Window, this 等。
而js最大毛病就是this複雜的指向,會依賴上下文而有所不同

在以前的作法是

// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/globalThis
const getGlobal = function () { 
  if (typeof self !== 'undefined') { return self; } 
  if (typeof window !== 'undefined') { return window; } 
  if (typeof global !== 'undefined') { return global; } 
  throw new Error('unable to locate global object'); 
}; 

var globals = getGlobal(); 

而ES2020提供了globalThis,提供了統一的方式來獲得全局對象

參考資料

掘金 / 种草 ES2020 新特性
掘金 / ES6、ES7、ES8、ES9、ES10新特性一览
ES6 Symbol 到底有什么用?
知乎 / BigInt:JavaScript 中的任意精度整数