筆記:JavaScript ES6

AurLemon發表於2024-09-06

前言

和前面寫的那篇文章一個背景,最近看 Nest.js 後端的東西發現挺多 JS 的東西,ES6 裡面除了幾個常用的importexport以及箭頭函式其它約等於不知道,這裡再整理一下。

ECMAScript 6

ES6 算是 JS 史上更新最重磅的一次,這裡簡單列一下 ES6 新增的東西,後面挑幾個自己不熟的出來說。

變數

在ES6之前,變數是用 var 宣告的,但 var 有一些問題,比如變數提升(變數在宣告之前可以訪問)。ES6引入了 letconst,用來解決這些問題。

let 宣告的變數只能在它所在的程式碼塊中使用,const 用來宣告常量,一旦賦值不能再修改。

模板字串

以前我們拼接字串需要用加號,但 ES6 裡引入了模板字串,讓我們可以透過 ${} 的形式直接在字串中插入變數,如果還是以前那種方法還要擔心各種符號。

let name = "114514";
console.log(`你好,${name}`); // 輸出 你好,114514

解構賦值

這個解構賦值簡單來說就是允許我們直接從陣列或物件中提取值,用來簡化程式碼的。

let [a, b] = [1, 2]; // 從陣列中提取 a=1, b=2
let {name, age} = {name: "Lemon", age: 23}; // 從物件中提取 name 和 age

物件導向程式設計,看這裡

箭頭函式、模組化和 Promise

這三個都後面說,箭頭函式是 ES6 新增的函式簡潔的寫法,模組化是可以透過 importexport 來管理程式碼,Promise 是讓處理非同步更方便的一種辦法。

箭頭函式

箭頭函式是 ES6 的一種簡潔的函式寫法。傳統的函式是這樣寫的:

function add(a, b) {
  return a + b;
}

箭頭函式可以寫成:

const add = (a, b) => a + b;

如果函式只有一個引數,括號也可以省略:

const square = x => x * x;

箭頭函式不僅語法簡潔,還有一個特別重要的特性:**它不會建立自己的 this。而 this 這個東西又有的說了。

this 關鍵字

在 JavaScript 中,this 是一個非常重要的概念,雖然他不是 ES6 新增的,老早就有了。this 指向的是當前執行上下文的物件。

在普通函式中,this 通常是指向呼叫該函式的物件;在箭頭函式中,this 不會像普通函式一樣根據呼叫方式改變,而是繼承自定義該箭頭函式時的上下文。簡單來說,箭頭函式里的 this 是固定的,跟它在哪定義有關,而不是在哪呼叫。

function Person() {
  this.age = 0;

  setInterval(() => {
    this.age++;
    console.log(this.age);  // 這裡的 `this` 指向 Person 物件
  }, 1000);
}

let p = new Person();

在 Vue 選項式 API 或者 Nest 的控制器中,this 都是非常常見的,但這裡的 this 不是簡單的普通函式或箭頭函式中的。這裡的 this 是根據類或物件例項的上下文來的。

export default {
  data() {
    return {
      message: 'Hello'
    };
  },
  methods: {
    greet() {
      console.log(this.message);  // 這裡的 this 指向 Vue 元件例項
    }
  }
};

在這段 Vue 的選項式 API 中,this.message 就是訪問當前元件例項的 data 中的 message。這個 this 的行為類似於類的例項,不會根據函式呼叫方式改變,而是始終指向當前的元件例項(那 Vue 中的 this 是如何訪問到 data、methods 等的?這個就是 Vue 的內部機制了,Vue 內部叫代理)。

methods: {
  greet: () => {
    console.log(this.message);  // 箭頭函式會繼承外層的 this,而不是 Vue 例項
  }
}

另外,Vue 中一般不用箭頭函式也是因為 this 上下文不同,箭頭函式的 this 會從定義時的上下文中繼承,通常是外層作用域的 this,而不是 Vue 元件的例項。通常不推薦在 Vue 的 methods 中使用箭頭函式,因為它會導致 this 繫結到錯誤的上下文。

Nest 中的 this 也和 Vue 類似,都是依據類或物件例項的上下文。它的 this 是指向類例項的,它的行為類似於物件導向程式設計中的 this。每個控制器或服務通常是透過類的例項化來建立的,而類中的 this 總是指向該類的當前例項。

@Injectable()
export class AppService {
  private readonly data: string = 'Hello';

  getData(): string {
    return this.data;  // 這裡的 this 指向 AppService 類的例項
  }
}

在這個例子中,this.data訪問的是當前類例項的 data 屬性,這與物件導向程式設計中的類例項一致。this 在 Nest 中的行為類似於普通類的 this,與 Vue 類似,它指向當前例項,並不會因為呼叫方式不同而變化。

模組化

ES6 引入了 exportimport 語法,使得 JavaScript 支援模組化程式設計。模組化程式設計的優勢在於,可以將程式碼分成多個檔案和模組,方便管理、維護、複用程式碼,避免全域性變數汙染。透過 export 可以在一個檔案中匯出變數、函式或類,而其他檔案可以使用 import 語法來引入這些匯出的內容。

命名匯出

首先來看如何使用 export 匯出模組中的內容:

// math.js
export const PI = 3.14159;

export function add(a, b) {
  return a + b;
}

export class Calculator {
  multiply(a, b) {
    return a * b;
  }
}

在這個例子中,檔案 math.js 中使用了三種 export 語法:

  1. export const PI:匯出一個常量 PI,可以在其他模組中使用。
  2. export function add:匯出一個函式 add,可以在其他地方呼叫。
  3. export class Calculator:匯出一個類 Calculator,這個類可以被例項化並使用它的方法。

接下來,在另一個檔案中,我們可以透過 import 引入這些匯出的內容:

// app.js
import { PI, add } from './math.js';  // 匯入 PI 常量和 add 函式
import { Calculator } from './math.js';  // 匯入 Calculator 類

console.log(PI);  // 列印 3.14159
console.log(add(2, 3));  // 列印 5

const calc = new Calculator();
console.log(calc.multiply(2, 3));  // 列印 6

這裡使用了 import { ... } from '...' 語法來從 math.js 檔案中匯入 PIaddCalculator。匯入的名稱必須與匯出時的名稱一致。

預設匯出

除了上面的命名匯出,ES6 還支援預設匯出,即一個模組中可以標記某個變數、函式或類為預設匯出。在匯入時,預設匯出可以用任意名字來引用,不必與匯出時的名字一致。

// greeting.js
export default function greeting(name) {
  return `Hello, ${name}!`;
}

在這個例子中,greeting 函式被預設匯出。我們在其他檔案中匯入時,可以用任意名字:

// app.js
import greet from './greeting.js';  // 任意命名為 greet

console.log(greet('John'));  // 列印 "Hello, John!"

預設匯出的內容不需要使用 {} 括號來匯入,匯入時可以隨意命名。

混合匯出

一個模組可以同時包含命名匯出和預設匯出。例子如下:

// user.js
export const userName = 'Alice';
export const userAge = 25;

export default function greetUser() {
  return `Hello, ${userName}!`;
}

匯入時既可以使用預設匯出,也可以使用命名匯出:

// app.js
import greetUser, { userName, userAge } from './user.js';

console.log(greetUser());  // 列印 "Hello, Alice!"
console.log(userName);  // 列印 "Alice"
console.log(userAge);  // 列印 25

重新匯出

ES6 還支援從一個模組中重新匯出內容,這在模組拆分和程式碼組織時非常有用。透過 export ... from '...' 語法可以直接從另一個模組匯出內容,而不需要顯式地匯入後再匯出。

// utils.js
export { PI, add } from './math.js';  // 從 math.js 重新匯出 PI 和 add

現在在其他檔案中可以像直接從 math.js 匯出一樣使用這些重新匯出的內容:

// app.js
import { PI, add } from './utils.js';

console.log(PI);  // 列印 3.14159
console.log(add(4, 5));  // 列印 9

非同步

JavaScript 是單執行緒的,意味著它一次只能執行一件事。但是很多操作,比如網路請求、讀寫檔案,需要時間,所以 JavaScript 提供了一個叫非同步的東西來避免程式“卡住”,與之相對的詞語叫“同步”。

常見的非同步有回撥函式、Promise 和 async/await。

回撥函式

這個簡單懶得講,因為我天天寫,網路請求很多 callback,有時候封裝函式也要加 callback。就是執行完某個操作後再執行另一個函式。

setTimeout(function() {
  console.log('Hello');
}, 1000);  // 一秒後執行回撥函式

Promise

Promise 是 ES6 新增的一種機制,用來更優雅地處理非同步操作。這個機制的出現解決了回撥地獄的問題,也讓程式碼更加簡潔和易於維護。在 Promise 出現之前,處理非同步操作往往需要使用巢狀回撥(如 setTimeout、ajax 等),這會導致程式碼層層巢狀,難以維護,俗稱“回撥地獄”,看著都頭暈。

Promise 的核心思想是:它代表一個未來的值,這個值在操作完成後要麼成功(resolved),要麼失敗(rejected)。Promise 提供了 thencatch 方法,分別用於處理成功和失敗的結果。

工作流程

  • Pending(待定):Promise 物件剛建立時,處於“待定”狀態,還沒有最終結果。
  • Resolved(已成功):非同步操作成功完成,Promise 被標記為“已成功”,並返回一個結果。
  • Rejected(已失敗):非同步操作失敗,Promise 被標記為“已失敗”,並返回一個錯誤原因。

用法示例

let promise = new Promise((resolve, reject) => {
  let success = true;
  // 模擬非同步操作
  setTimeout(() => {
    if (success) {
      resolve("成功");  // 呼叫 resolve 表示操作成功
    } else {
      reject("失敗");   // 呼叫 reject 表示操作失敗
    }
  }, 1000);
});

// 處理 Promise 的結果
promise
  .then(result => {
    console.log(result);  // 如果操作成功,列印 "成功"
  })
  .catch(error => {
    console.log(error);   // 如果操作失敗,列印 "失敗"
  });

這段程式碼中,new Promise() 建立了一個新的 Promise 物件。建構函式接受一個函式作為引數,這個函式有兩個引數:resolve 和 reject,分別表示操作成功和失敗時的回撥。隨後,setTimeout() 模擬了一個非同步操作,非同步操作結束後根據條件呼叫 resolve() 或 reject()。

鏈式呼叫

Promise 還支援鏈式呼叫。也就是你可以在 then() 中返回另一個 Promise,來進行更復雜的非同步操作,不用寫成巢狀套娃那樣噁心。每個 then() 返回的值會被傳遞到下一個 then()。

let promise = new Promise((resolve, reject) => {
  let success = true;
  setTimeout(() => {
    if (success) {
      resolve("Step 1 成功");
    } else {
      reject("Step 1 失敗");
    }
  }, 1000);
});

promise
  .then(result => {
    console.log(result);  // 列印 "Step 1 成功"
    // 返回新的 Promise
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve("Step 2 成功");
      }, 1000);
    });
  })
  .then(result => {
    console.log(result);  // 列印 "Step 2 成功"
    // 再返回一個新的 Promise
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve("Step 3 成功");
      }, 1000);
    });
  })
  .then(result => {
    console.log(result);  // 列印 "Step 3 成功"
  })
  .catch(error => {
    console.log(error);   // 如果任何一個步驟失敗,捕獲錯誤
  });

這段程式碼是這樣子的:

  • 第一個 Promise:
    第一個 Promise 模擬一個 1 秒延遲的非同步操作,成功後 resolve("Step 1 成功")。
    then() 方法接收第一個 Promise 的結果並列印 "Step 1 成功"。

  • 在 then() 中返回新的 Promise:
    第一個 then() 處理完後,返回了一個新的 Promise,它再次模擬了一個 1 秒延遲的非同步操作。
    第二個 then() 接收並處理新的 Promise,列印 "Step 2 成功"。

  • 再返回一個 Promise:
    第二個 then() 處理完後,再次返回一個新的 Promise,繼續模擬非同步操作。
    第三個 then() 接收並處理該 Promise,列印 "Step 3 成功"。

如果在任何一個 Promise 鏈的步驟中發生錯誤(呼叫 reject()),catch() 會捕獲錯誤並進行處理。

為什麼 Promise 更優雅?

避免回撥地獄。在沒有 Promise 之前,處理多個非同步操作時往往要巢狀多個回撥函式,導致程式碼結構複雜、難以理解。而 Promise 透過 then 鏈式呼叫,減少了巢狀。

傳統回撥地獄的寫法:

setTimeout(() => {
 console.log("Step 1");
 setTimeout(() => {
   console.log("Step 2");
   setTimeout(() => {
     console.log("Step 3");
   }, 1000);
 }, 1000);
}, 1000);

使用 Promise 後的寫法:

new Promise((resolve) => {
 setTimeout(() => {
   console.log("Step 1");
   resolve();
 }, 1000);
}).then(() => {
 return new Promise((resolve) => {
   setTimeout(() => {
     console.log("Step 2");
     resolve();
   }, 1000);
 });
}).then(() => {
 return new Promise((resolve) => {
   setTimeout(() => {
     console.log("Step 3");
     resolve();
   }, 1000);
 });
});

錯誤處理機制。使用回撥時,如果發生錯誤,往往需要在每個回撥中手動處理錯誤。而 Promise 透過 catch 統一處理所有的錯誤,簡化了程式碼邏輯。

new Promise((resolve, reject) => {
 let success = false;
 if (success) {
   resolve("操作成功");
 } else {
   reject("操作失敗");
 }
})
.then(result => {
 console.log(result);
})
.catch(error => {
 console.log("發生錯誤: " + error);
});

鏈式呼叫。Promise 支援鏈式呼叫,使得多個非同步操作可以順序執行,每一步的輸出會傳遞給下一步,這樣寫法更加直觀。

new Promise((resolve) => {
  resolve(1);
}).then(result => {
  console.log(result);  // 輸出 1
  return result + 1;
}).then(result => {
  console.log(result);  // 輸出 2
  return result + 1;
}).then(result => {
  console.log(result);  // 輸出 3
});

Promise 的程式碼可讀性和維護性都比以前更好,非同步程式碼看起來像同步程式碼一樣直觀

async/await

async/await 是基於 Promise 的一種簡潔的非同步處理方式,讓程式碼更易於編寫和理解的。

使用 async 關鍵字宣告一個非同步函式時,函式的返回值是一個 Promise,即使函式內部返回的不是 Promise,JavaScript 也會自動將其封裝為 Promise。而 await 關鍵字只能在 async 函式內部使用,它會暫停函式的執行,直到 Promise 解決(resolved)或拒絕(rejected),從而讓非同步程式碼看起來像同步程式碼一樣順序執行。

async function fetchData() {
  try {
    let result = await someAsyncFunction();  // 等待非同步操作完成
    console.log(result);  // 列印操作結果
  } catch (error) {
    console.error("發生錯誤:", error);  // 處理異常
  }
}

在這個示例中,fetchData 函式是一個非同步函式,內部的 await 用來等待 someAsyncFunction() 的結果。在非同步操作完成之前,程式碼會暫停,直到 Promise 完成,接著繼續執行。如果 Promise 成功,結果會被賦值給 result。如果 Promise 失敗,錯誤會被捕獲並處理。

更復雜的情況是多個非同步操作需要順序執行。比如:

async function processData() {
  try {
    let data1 = await fetchData1();  // 等待第一個非同步操作完成
    console.log('Data 1:', data1);
    
    let data2 = await fetchData2();  // 等待第二個非同步操作完成
    console.log('Data 2:', data2);
    
    let data3 = await fetchData3();  // 等待第三個非同步操作完成
    console.log('Data 3:', data3);
  } catch (error) {
    console.error("發生錯誤:", error);
  }
}

在這個例子中,每個 await 都等待上一個非同步操作完成後才繼續執行,確保操作按順序進行。

如果希望多個非同步操作並行執行,可以結合 Promise.all(),這會同時啟動所有非同步操作,並在所有操作都完成後返回結果。例如:

async function processParallel() {
  try {
    let [data1, data2, data3] = await Promise.all([fetchData1(), fetchData2(), fetchData3()]);
    console.log('Data 1:', data1);
    console.log('Data 2:', data2);
    console.log('Data 3:', data3);
  } catch (error) {
    console.error("發生錯誤:", error);
  }
}

這裡,Promise.all() 接收一個 Promise 陣列,並行執行這三個非同步操作。只有當所有 Promise 都完成後,結果才會被返回,並透過解構賦值一次性獲取。

相關文章