前言
和前面寫的那篇文章一個背景,最近看 Nest.js 後端的東西發現挺多 JS 的東西,ES6 裡面除了幾個常用的import
和export
以及箭頭函式其它約等於不知道,這裡再整理一下。
ECMAScript 6
ES6 算是 JS 史上更新最重磅的一次,這裡簡單列一下 ES6 新增的東西,後面挑幾個自己不熟的出來說。
變數
在ES6之前,變數是用 var
宣告的,但 var
有一些問題,比如變數提升(變數在宣告之前可以訪問)。ES6引入了 let
和 const
,用來解決這些問題。
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 新增的函式簡潔的寫法,模組化是可以透過 import
和 export
來管理程式碼,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 引入了 export
和 import
語法,使得 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
語法:
export const PI
:匯出一個常量PI
,可以在其他模組中使用。export function add
:匯出一個函式add
,可以在其他地方呼叫。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
檔案中匯入 PI
、add
和 Calculator
。匯入的名稱必須與匯出時的名稱一致。
預設匯出
除了上面的命名匯出,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 提供了 then
和 catch
方法,分別用於處理成功和失敗的結果。
工作流程
- 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 都完成後,結果才會被返回,並透過解構賦值一次性獲取。