跳至主要内容

70 篇文章 含有標籤「frontend engineer」

檢視所有標籤

JavaScript 箭頭函式 (Arrow Function)入門教學筆記 | 學習筆記

· 閱讀時間約 3 分鐘
kdchang

在 JavaScript 的 箭頭函式 (Arrow Function, =>) 中,this 的行為與傳統的 函式表達式 (Function Expression) 不同,主要特點如下:


箭頭函式的 this 綁定

  1. 箭頭函式不會建立自己的 this,而是繼承定義它的上下文(也稱為 詞法作用域 Lexical Scope)。
  2. 在箭頭函式內部,this 指向的是箭頭函式所處的外部函式的 this

範例

1. 一般函式的 this

function normalFunction() {
console.log(this); // this 取決於調用方式
}

const obj = {
method: normalFunction
};

obj.method(); // this 指向 obj
normalFunction(); // this 指向全域物件 (在瀏覽器是 window,在 Node.js 是 global)

2. 箭頭函式的 this

const arrowFunction = () => {
console.log(this); // 繼承外部作用域的 this
};

const obj2 = {
method: arrowFunction
};

obj2.method(); // this 指向定義時的外部作用域,而不是 obj2

解析:

  • arrowFunction 並未創建自己的 this,所以 this 仍然指向外部作用域的 this,而不是 obj2

箭頭函式適用場景

1. 在物件方法中避免 this 綁定問題

const person = {
name: "John",
sayHello: function() {
setTimeout(() => {
console.log(`Hello, ${this.name}`); // this 繼承 sayHello 的 this,即 person
}, 1000);
}
};

person.sayHello(); // Hello, John

解析:

  • setTimeout 中的箭頭函式不會創建新的 this,它會繼承 sayHello 方法中的 this,所以 this.name 正確指向 person.name

若使用一般函式,this 會指向 window(瀏覽器環境)或 undefined(嚴格模式)。


2. 當作回呼函式 (Callback)

const numbers = [1, 2, 3];

// 使用箭頭函式讓 this 指向外部作用域
const doubled = numbers.map(num => num * 2);

console.log(doubled); // [2, 4, 6]

map() 內的箭頭函式不需要 this,但讓語法更簡潔。


箭頭函式的 this 限制

1. 不能作為建構函式 (Constructor)

const Person = (name) => {
this.name = name; // 錯誤,this 不會指向新建的物件
};

const john = new Person("John"); // TypeError: Person is not a constructor

解法: 必須使用 function 來定義建構函式:

function Person(name) {
this.name = name;
}

const john = new Person("John"); // 正常運作

2. 不能使用 arguments

const sum = () => {
console.log(arguments); // ReferenceError: arguments is not defined
};

sum(1, 2, 3);

解法: 可以使用 展開運算符 ...args

const sum = (...args) => {
console.log(args); // [1, 2, 3]
};

sum(1, 2, 3);

3. 無法使用 .bind() 改變 this

const obj = {
value: 42,
method: () => {
console.log(this.value);
}
};

const newMethod = obj.method.bind({ value: 100 });
newMethod(); // undefined (this 不會變)

箭頭函式的 this 綁定無法透過 bind()call()apply() 來改變


總結

特性一般函式 (Function)箭頭函式 (Arrow Function)
this依呼叫方式決定繼承外部作用域
arguments有 (function 內部)無 (...args 取代)
bind()/call()/apply()可改變 this無效
new 關鍵字可用於建構函式無法當建構函式

適用場景

適合使用箭頭函式:

  • 短小的回呼函式 (e.g. map, filter, forEach)
  • setTimeout()setInterval()
  • 物件內部方法但不希望 this 被改變

不適合使用箭頭函式:

  • 建構函式
  • 需要動態 this 的方法
  • 使用 arguments 物件的場合

async/await 入門教學筆記 | 學習筆記

· 閱讀時間約 2 分鐘
kdchang

asyncawait 是 JavaScript 中處理非同步操作的語法糖,它們使得非同步代碼更加易讀和易寫,避免了傳統回調函數(callback)或 Promise.then() 鏈式調用的冗長性。

1. async 關鍵字

async 是一個關鍵字,用來標記一個函數為「非同步函數」。非同步函數會隱式地返回一個 Promise,並且在函數內部,你可以使用 await 來等待非同步操作的結果。

語法

async function example() {
// 可以在這裡使用 await
}

當你呼叫這個函數時,它會立即返回一個 Promise。如果函數內的代碼執行成功,這個 Promise 會被解析;如果有錯誤,Promise 會被拒絕。

2. await 關鍵字

await 必須在 async 函數內部使用,它會讓 JavaScript 等待某個 Promise 完成並返回結果。await 會使得後續代碼暫停,直到 Promise 被解決或拒絕(解決是指成功完成操作,拒絕則是發生錯誤)。

語法

const result = await promise; // 等待 Promise 完成並取得結果

如果 Promise 解決(成功),await 會返回結果。如果 Promise 被拒絕(失敗),會拋出錯誤,這通常需要使用 try...catch 來處理。

範例

// 模擬一個非同步操作
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data fetched!");
}, 2000);
});
}

// 使用 async/await
async function getData() {
try {
console.log("Fetching data...");
const data = await fetchData(); // 等待 fetchData 完成
console.log(data); // 顯示結果
} catch (error) {
console.error("Error:", error); // 處理錯誤
}
}

getData(); // 呼叫 async 函數

3. async/await 的特點:

  • 簡化非同步代碼async/await 讓非同步代碼的寫法更像是同步代碼,避免了回調函數的「Callback hell 回調地獄」。
  • 錯誤處理:你可以使用 try...catch 塊來捕獲非同步操作中的錯誤,這使得錯誤處理比傳統的 .catch() 更加簡單直觀。
  • 非阻塞執行:儘管代碼看起來是同步執行的,但非同步操作並不會阻塞主執行線程,其他代碼可以繼續執行。

總結

  • async 將函數標記為非同步函數。
  • await 使代碼等待 Promise 的解決結果,並可以在 async 函數內使用。
  • 使用 async/await 可以使非同步代碼更加簡潔且易於理解。

JavaScript Call Stack 入門教學筆記 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

JavaScript 開發中,理解 Call Stack(呼叫堆疊) 是掌握執行流程與錯誤除錯的關鍵概念之一。它決定了程式碼執行的順序,也與同步與非同步行為密切相關。本文將深入介紹 Call Stack 的基本原理,並透過實際範例幫助你理解它的運作方式。


1. 什麼是 Call Stack?

Call Stack 是 JavaScript 引擎用來管理函式呼叫的結構,它遵循 LIFO(Last In, First Out,後進先出) 原則。當一個函式被呼叫時,它會被推入(push)堆疊頂部,當函式執行完畢後,會從堆疊中彈出(pop)。

JavaScript 是**單執行緒(single-threaded)**的語言,這代表它一次只能執行一件事。Call Stack 便是 JavaScript 用來管理同步程式碼執行順序的機制。


2. Call Stack 運作原理

讓我們用一個簡單的範例來說明 Call Stack 的運作方式:

function first() {
console.log("First function start");
second();
console.log("First function end");
}

function second() {
console.log("Second function start");
third();
console.log("Second function end");
}

function third() {
console.log("Third function");
}

first();

執行步驟

  1. first() 被呼叫,推入 Call Stack。
  2. first() 內部呼叫 second()second() 推入 Call Stack。
  3. second() 內部呼叫 third()third() 推入 Call Stack。
  4. third() 執行 console.log("Third function"),然後執行完畢並從 Call Stack 移除。
  5. second() 繼續執行 console.log("Second function end"),執行完畢並從 Call Stack 移除。
  6. first() 繼續執行 console.log("First function end"),執行完畢並從 Call Stack 移除。
  7. Call Stack 清空,程式執行結束。

執行輸出

First function start
Second function start
Third function
Second function end
First function end

3. Call Stack 與錯誤訊息

如果函式之間發生無窮遞迴,Call Stack 會不斷推入函式,最終導致 Maximum call stack size exceeded 錯誤。

範例:遞迴導致 Stack Overflow

function recursiveFunction() {
recursiveFunction();
}

recursiveFunction();

錯誤訊息

Uncaught RangeError: Maximum call stack size exceeded

這是因為 recursiveFunction() 不斷呼叫自己,導致 Call Stack 無法清空,最終超過瀏覽器設定的堆疊大小。


4. Call Stack 與非同步

JavaScript 本質上是單執行緒的語言,但它透過 Event Loop(事件迴圈) 來處理非同步程式碼,例如 setTimeoutfetch 等。

範例:setTimeout 與 Call Stack

console.log("Start");

setTimeout(() => {
console.log("Inside setTimeout");
}, 0);

console.log("End");

執行輸出

Start
End
Inside setTimeout

執行流程解析

  1. console.log("Start") 直接執行,輸出 Start
  2. setTimeout 被呼叫,但它是非同步函式,會被放入 Web API(瀏覽器環境),不會影響 Call Stack。
  3. console.log("End") 直接執行,輸出 End
  4. 當 Call Stack 清空後,事件迴圈(Event Loop)會將 setTimeout 內的回呼函式放入 Call Stack,執行 console.log("Inside setTimeout")

這說明了 Call Stack 只負責同步程式碼,非同步程式碼會透過 Web API 與 Event Loop 處理。


5. 使用開發者工具檢查 Call Stack

大多數現代瀏覽器(如 Chrome、Firefox)都內建開發者工具,可用來觀察 Call Stack。

如何使用 Chrome DevTools

  1. 開啟 Chrome 瀏覽器,按 F12Ctrl + Shift + I(Mac 使用 Cmd + Option + I)。
  2. 進入 Sources 面板。
  3. 設置 斷點(breakpoint) 在 JavaScript 代碼內的某行。
  4. 重新載入頁面,當程式執行到該行時會暫停。
  5. 在右側 Call Stack 面板 中查看目前堆疊狀態。

這個工具能幫助開發者更直觀地理解 Call Stack 的運作方式。


6. 總結

Call Stack 是 JavaScript 引擎管理函式執行順序的核心機制,掌握它的運作原理對於理解 JavaScript 的同步與非同步行為至關重要。總結要點如下:

  1. Call Stack 採用 LIFO(後進先出)原則,函式執行時會推入堆疊,結束後會移除。
  2. 過多遞迴可能導致 Stack Overflow,應確保遞迴函式有適當的終止條件。
  3. JavaScript 是單執行緒的語言,但透過 Web API 和 Event Loop 可處理非同步操作。
  4. 開發者工具能幫助分析 Call Stack,對於除錯非常有幫助。

透過理解 Call Stack 的運作,開發者可以更有效地編寫、優化與除錯 JavaScript 程式碼。

JavaScript let 閉包(Closure)入門教學筆記 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

什麼是閉包(Closure)?

閉包是 JavaScript 中的一個強大概念,它允許函式「記住」其外部作用域(lexical scope),即使該作用域已經執行完畢並離開了執行環境。閉包使得函式能夠存取其外部函式內部的變數,而這些變數通常在外部函式執行結束後仍然可以被存取。

在 JavaScript 中,每當一個函式被創建時,它都會自動獲得對其外部變數的存取權,這就是閉包的核心概念。


閉包的基本概念

閉包的最基本形式是函式內部返回另一個函式,而返回的函式仍然能夠存取外部函式的變數,即使外部函式已經執行完畢。例如:

function outerFunction() {
let outerVariable = "我是外部變數";

function innerFunction() {
console.log(outerVariable);
}

return innerFunction;
}

const closureExample = outerFunction();
closureExample(); // 輸出: "我是外部變數"

在這個例子中:

  1. outerFunction 內部定義了一個變數 outerVariable,並宣告了一個 innerFunction
  2. innerFunction 存取 outerVariable,然後被 outerFunction 返回。
  3. closureExample 執行時,即使 outerFunction 已經執行完畢,它仍然可以存取 outerVariable,因為它形成了一個閉包。

閉包的實際應用

閉包在 JavaScript 中有許多實際用途,包括資料封裝、模擬私有變數、事件處理,以及避免全域變數污染等。

1. 資料封裝與模擬私有變數

在 JavaScript 中,沒有內建的 private 修飾符,但可以透過閉包來模擬私有變數:

function createCounter() {
let count = 0; // 私有變數

return {
increment: function() {
count++;
console.log(`計數器值: ${count}`);
},
decrement: function() {
count--;
console.log(`計數器值: ${count}`);
},
getCount: function() {
return count;
}
};
}

const counter = createCounter();
counter.increment(); // 計數器值: 1
counter.increment(); // 計數器值: 2
console.log(counter.getCount()); // 2
counter.decrement(); // 計數器值: 1

在這個例子中:

  • count 變數是 createCounter 的內部變數,外部無法直接存取。
  • incrementdecrementgetCount 方法則形成閉包,允許我們操作 count

這種方法可以防止外部直接修改 count,達到變數封裝的效果。


2. 事件處理中的閉包

閉包在事件處理中特別有用,例如當我們需要讓事件處理函式記住某些狀態時:

function attachEventListener() {
let count = 0;

document.getElementById("clickButton").addEventListener("click", function() {
count++;
console.log(`按鈕點擊次數: ${count}`);
});
}

attachEventListener();

這裡:

  • click 事件處理函式記住了 count 變數,即使 attachEventListener 已執行完畢,每次點擊按鈕時,count 仍然會被持續累加。

3. 立即函式(IIFE, Immediately Invoked Function Expression)

立即函式是一種使用閉包的技術,可用於模擬私有作用域,避免變數污染全域空間:

const counter = (function() {
let count = 0;

return {
increment: function() {
count++;
console.log(`計數器值: ${count}`);
},
decrement: function() {
count--;
console.log(`計數器值: ${count}`);
},
getCount: function() {
return count;
}
};
})();

counter.increment(); // 計數器值: 1
counter.increment(); // 計數器值: 2
console.log(counter.getCount()); // 2
counter.decrement(); // 計數器值: 1

這裡:

  • (function() {...})() 是一個立即執行函式,它執行後返回了一個對象,該對象內部的函式形成閉包,能夠存取 count

這種技術在早期 JavaScript 程式設計中常被用來創建模組化的程式碼,避免全域變數污染。


4. 用於函式工廠(Function Factory)

閉包可以用來創建不同的函式行為,例如建立不同的乘法器:

function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

這裡:

  • createMultiplier 接受 multiplier 作為參數,返回一個新的函式。
  • 該函式形成閉包,記住 multiplier,即使 createMultiplier 已經執行完畢。

這種模式在高階函式設計中非常常見。


閉包的注意事項

雖然閉包提供了強大的功能,但如果不當使用,可能會導致記憶體洩漏。例如:

function createLargeClosure() {
let largeArray = new Array(1000000).fill("資料");
return function() {
console.log(largeArray.length);
};
}

const closure = createLargeClosure();
// 如果 closure 持續存在,largeArray 也不會被回收
  • largeArray 變數被閉包記住,無法被垃圾回收機制(GC)回收,可能導致記憶體洩漏。
  • 解決方案是確保不再使用閉包時,讓變數參考變為 null 或適時使用 WeakMap 來管理記憶體。

結論

閉包是 JavaScript 的核心概念之一,理解閉包有助於寫出更靈活、可維護的程式碼。它主要用於:

  • 變數封裝(模擬私有變數)
  • 事件處理(記住狀態)
  • 函式工廠(創建可重複使用的函式)
  • IIFE(避免變數污染)

掌握閉包後,可以更進一步學習 JavaScript 中的函式式程式設計(Functional Programming),提升程式的模組化與可讀性。

JavaScript 物件導向(Object-oriented programming)入門教學筆記 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

1. 物件導向與 new 關鍵字

JavaScript 是基於 原型 (Prototype) 的物件導向語言,而非典型的 類別 (Class) 為基礎的語言。但 ES6 之後,JavaScript 引入了 class 語法,使其更接近傳統的物件導向語言,如 Java 或 C++。

在 JavaScript 中,new 關鍵字用於建立物件,並且會執行以下步驟:

  1. 建立一個新的空物件。
  2. 設定該物件的 __proto__ 指向建構函式 (Constructor) 的 prototype
  3. 執行建構函式內的程式碼,並將 this 綁定到新建立的物件。
  4. 如果建構函式沒有明確返回物件,則回傳該新物件。

範例:

function Person(name, age) {
this.name = name;
this.age = age;
}

const john = new Person("John", 30);
console.log(john.name); // John
console.log(john.age); // 30

2. __proto__ vs prototype

在 JavaScript 中,__proto__prototype 是兩個不同的概念。

prototype

prototype建構函式的一個屬性,它是一個物件,當我們使用 new 建立物件時,該物件的 __proto__ 會指向 prototype

範例:

function Animal(name) {
this.name = name;
}

Animal.prototype.sayHello = function() {
console.log("Hello, I am " + this.name);
};

const dog = new Animal("Dog");
dog.sayHello(); // Hello, I am Dog
console.log(dog.__proto__ === Animal.prototype); // true

__proto__

__proto__ 是物件的內部屬性,指向該物件的原型,即 prototype

範例:

console.log(dog.__proto__); // Animal.prototype
console.log(dog.__proto__.__proto__ === Object.prototype); // true
console.log(dog.__proto__.__proto__.__proto__); // null (最終的原型鏈結束)

關鍵點整理:

  • prototype函式的屬性。
  • __proto__物件的屬性,指向它的 prototype
  • Object.prototype 是所有物件的最終原型。

3. class 關鍵字

ES6 之後,JavaScript 引入了 class 語法,使物件導向的寫法更直覺。

定義類別 (Class)

class Car {
constructor(brand) {
this.brand = brand;
}
drive() {
console.log(this.brand + " is driving");
}
}

const myCar = new Car("Toyota");
myCar.drive(); // Toyota is driving

等同於 ES5 的寫法:

function Car(brand) {
this.brand = brand;
}

Car.prototype.drive = function() {
console.log(this.brand + " is driving");
};

優勢:

  • class 提供更簡潔的語法。
  • 更貼近傳統物件導向語言的語法風格。
  • constructor 方法負責初始化物件。
  • 方法定義在 prototype 上,並不會重複創建。

4. extends 繼承

在 ES6 之前,我們使用 Object.create() 或手動設定 prototype 來實現繼承。

傳統的原型繼承

function Animal(name) {
this.name = name;
}
Animal.prototype.makeSound = function() {
console.log("Some generic sound");
};

function Dog(name, breed) {
Animal.call(this, name); // 繼承屬性
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype); // 繼承方法
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log("Woof!");
};

const myDog = new Dog("Rex", "Golden Retriever");
myDog.makeSound(); // Some generic sound
myDog.bark(); // Woof!

使用 class 的繼承

class Animal {
constructor(name) {
this.name = name;
}
makeSound() {
console.log("Some generic sound");
}
}

class Dog extends Animal {
constructor(name, breed) {
super(name); // 呼叫父類別的 constructor
this.breed = breed;
}
bark() {
console.log("Woof!");
}
}

const myDog = new Dog("Rex", "Golden Retriever");
myDog.makeSound(); // Some generic sound
myDog.bark(); // Woof!

關鍵點:

  • extends 用於建立類別的繼承。
  • super(name) 呼叫父類別的 constructor,確保 this 正確初始化。
  • 子類別可以新增自己的方法。

5. 物件導向開發的最佳實踐

  1. 使用 class 提供更清晰的結構。
  2. 使用 extends 來建立繼承關係,並呼叫 super() 確保正確初始化。
  3. 方法定義於 prototype 來減少記憶體浪費。
  4. 理解 __proto__prototype 之間的關係,以便更好地管理原型鏈。
  5. 避免過度使用繼承,適時使用組合 (Composition) 來降低耦合度。

6. 總結

特性傳統原型 (Prototype)ES6 class
建立物件new Function()new Class()
方法定義Function.prototype.method = function() {}直接定義於 class
繼承Object.create() + call()extends + super()
this 綁定需要 call()bind()super() 自動綁定

JavaScript 的物件導向概念提供了靈活的方式來組織程式碼,掌握 prototypeclassextendssuper(),可以幫助開發者寫出更具可讀性與可維護性的程式碼。

JavaScript this 入門教學筆記 | 學習筆記

· 閱讀時間約 3 分鐘
kdchang

在 JavaScript 中,this 是一個關鍵字,它的值會根據執行環境的不同而改變。以下是 this 在不同情境下的行為:


1. 全域環境 (Global Context)

在瀏覽器中,this 預設指向 window 物件:

console.log(this); // 在瀏覽器中,this 指向 window

在 Node.js 環境下,this 則指向 global

console.log(this); // 在 Node.js 中,this 指向 global

2. 函式內部 (Function Context)

在一般函式中,this 的值取決於是否使用 "use strict"

function showThis() {
console.log(this);
}
showThis(); // 在非嚴格模式下,this 指向 window (瀏覽器) 或 global (Node.js)

"use strict";
function showStrictThis() {
console.log(this);
}
showStrictThis(); // 在嚴格模式下,this 變成 undefined

3. 物件方法 (Object Method)

this 被用在物件的方法內,它指向該物件:

const obj = {
name: "Alice",
greet: function () {
console.log(this.name);
},
};
obj.greet(); // "Alice"

4. 建構函式 (Constructor Function)

在建構函式中,this 會指向新建立的物件:

function Person(name) {
this.name = name;
}
const p = new Person("Bob");
console.log(p.name); // "Bob"

5. 箭頭函式 (Arrow Function)

箭頭函式中的 this 不會 指向它自己的執行環境,而是繼承自外層函式的作用域:

const obj = {
name: "Charlie",
greet: function () {
const arrowFunc = () => {
console.log(this.name);
};
arrowFunc();
},
};
obj.greet(); // "Charlie" (this 繼承自 obj)

6. setTimeout 和 setInterval

setTimeoutsetInterval 內,一般函式的 this 預設指向 window (瀏覽器) 或 global (Node.js):

const obj = {
name: "David",
greet: function () {
setTimeout(function () {
console.log(this.name);
}, 1000);
},
};
obj.greet(); // undefined,因為 this 指向 window/global

解法:改用箭頭函式:

const obj = {
name: "David",
greet: function () {
setTimeout(() => {
console.log(this.name);
}, 1000);
},
};
obj.greet(); // "David"

7. 事件處理器 (Event Handler)

在事件處理函式中,this 指向觸發事件的元素:

document.getElementById("btn").addEventListener("click", function () {
console.log(this); // 指向 <button> 元素
});

如果改用箭頭函式,this 會指向外部作用域:

document.getElementById("btn").addEventListener("click", () => {
console.log(this); // 指向 window
});

8. call、apply 和 bind

可以使用 call()apply()bind() 來改變 this 指向:

call()

function greet() {
console.log(this.name);
}
const person = { name: "Eve" };
greet.call(person); // "Eve"

apply()

apply()call() 類似,但參數是以陣列方式傳入:

function introduce(age, city) {
console.log(`${this.name} is ${age} years old and lives in ${city}.`);
}
const person = { name: "Frank" };
introduce.apply(person, [25, "Taipei"]);

bind()

bind() 會回傳一個新的函式,永久綁定 this

const boundFunc = greet.bind(person);
boundFunc(); // "Eve"

總結

  • 全域環境this 在瀏覽器中指向 window,在 Node.js 指向 global
  • 普通函式:嚴格模式下 thisundefined,否則指向 window
  • 物件方法this 指向該物件
  • 建構函式this 指向新建立的物件
  • 箭頭函式this 繼承外部作用域
  • 事件處理器:普通函式 this 指向事件元素,箭頭函式 this 指向外部作用域
  • callapplybind 可顯式設定 this

JavaScript var 閉包(Closure)入門教學筆記 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

什麼是閉包(Closure)?

閉包是 JavaScript 中的一個重要概念,指的是函式在創建時,能夠記住並存取其外部作用域的變數,即使該作用域已經執行完畢。這種特性使得 JavaScript 的函式可以擁有「記憶」的能力,允許函式保持對外部變數的存取權。

閉包的概念建立在 JavaScript 的**詞法作用域(Lexical Scope)**之上,也就是函式可以存取其被定義時所在的作用域中的變數,而不是函式執行時的作用域。


1. 基本範例

function outerFunction() {
var outerVariable = "我是外部變數";

function innerFunction() {
console.log(outerVariable);
}

return innerFunction;
}

var closureExample = outerFunction();
closureExample(); // 輸出: "我是外部變數"

解析

  1. outerFunction 內部宣告了一個變數 outerVariable,並定義了一個 innerFunction
  2. innerFunction 存取 outerVariable,並被 outerFunction 返回。
  3. closureExample 執行時,即使 outerFunction 早已執行完畢,innerFunction 仍然能存取 outerVariable,這就是閉包的作用。

2. var 變數與閉包的問題

JavaScript 在 ES6 之前使用 var 來宣告變數,但 var 具有函式作用域(Function Scope),這可能會導致閉包相關的陷阱。

問題:for 迴圈與 var

function varClosureExample() {
var functions = [];

for (var i = 0; i < 3; i++) {
functions.push(function() {
console.log(i);
});
}

return functions;
}

var closures = varClosureExample();
closures[0](); // 輸出: 3
closures[1](); // 輸出: 3
closures[2](); // 輸出: 3

為什麼會這樣?

  1. var函式作用域,它在整個 varClosureExample 內部是共享的。
  2. closures[0]()closures[1]()closures[2]() 執行時,它們都參考同一個 i,但 i 已經變成 3(因為 for 迴圈已經執行完畢)。
  3. 所有函式都會輸出 3,而不是 0, 1, 2

3. 如何修正 var 的問題

解決方案 1:使用 let(區塊作用域)

function letClosureExample() {
var functions = [];

for (let i = 0; i < 3; i++) {
functions.push(function() {
console.log(i);
});
}

return functions;
}

var closures = letClosureExample();
closures[0](); // 輸出: 0
closures[1](); // 輸出: 1
closures[2](); // 輸出: 2

為什麼 let 有效?

  • let 具有區塊作用域(Block Scope),每次迴圈執行時,i 都是一個新的變數,因此閉包綁定的 i 值不會變化。

解決方案 2:使用 IIFE(立即執行函式)

如果只能使用 var,可以使用 IIFE(Immediately Invoked Function Expression)

function varClosureFixedExample() {
var functions = [];

for (var i = 0; i < 3; i++) {
(function(i) {
functions.push(function() {
console.log(i);
});
})(i);
}

return functions;
}

var closures = varClosureFixedExample();
closures[0](); // 輸出: 0
closures[1](); // 輸出: 1
closures[2](); // 輸出: 2

為什麼 IIFE 有效?

  • IIFE 會立即執行,並建立一個新的作用域,確保每次迭代時 i 都是獨立的。

4. 閉包的實際應用

(1) 模擬私有變數

function createCounter() {
var count = 0; // 外部變數

return {
increment: function() {
count++;
console.log(`計數器值: ${count}`);
},
decrement: function() {
count--;
console.log(`計數器值: ${count}`);
},
getCount: function() {
return count;
}
};
}

var counter = createCounter();
counter.increment(); // 計數器值: 1
counter.increment(); // 計數器值: 2
console.log(counter.getCount()); // 2
counter.decrement(); // 計數器值: 1

解析

  • countcreateCounter 內部變數,外部無法直接存取,只能透過 incrementdecrement 方法修改,形成了私有變數的概念

(2) 函式工廠(Function Factory)

function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}

var double = createMultiplier(2);
var triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

解析

  • double 變數是一個閉包,記住了 multiplier2
  • triple 變數是一個閉包,記住了 multiplier3
  • 這種模式可以用來建立彈性的函式生成器。

(3) 延遲執行與事件處理

function delayedMessage(message, delay) {
setTimeout(function() {
console.log(message);
}, delay);
}

delayedMessage("Hello, JavaScript!", 2000); // 2 秒後輸出 "Hello, JavaScript!"

解析

  • setTimeout 內部的函式形成閉包,記住 message 變數,即使 delayedMessage 已經執行完畢。

5. 總結

閉包的核心概念

  • 函式可以存取其外部函式的變數,即使外部函式已經執行完畢。
  • var 變數的作用域為函式作用域,可能導致閉包內變數的值不如預期。
  • 使用 let 或 IIFE 可以避免 var 造成的閉包問題。

閉包的應用

  1. 私有變數(避免全域變數污染)
  2. 函式工廠(建立可重複使用的函式)
  3. 事件處理與回調函式
  4. 延遲執行與計時器

閉包是 JavaScript 的核心概念之一,它允許函式記住外部變數,即使作用域已經離開。var 宣告的變數可能會導致作用域問題,例如 for 迴圈內的閉包錯誤,而 let 或 IIFE 可以解決這個問題。

掌握閉包後,可以更進一步學習 JavaScript 的函式式程式設計(Functional Programming),提升程式的模組化與可讀性。

Vue3 Options API 和 Composition API 風格差異教學筆記 | 學習筆記

· 閱讀時間約 3 分鐘
kdchang

根據官方文件的說明,一般來說 Vue 元件可以用兩種不同的 API 風格來撰寫:Options APIComposition API


Options API

使用 Options API 時,我們透過一個包含 datamethodsmounted 等選項的物件來定義元件邏輯。選項中定義的屬性會在函式內透過 this 曝露,this 指向的是元件實例。

<script>
export default {
// 從 data() 返回的屬性成為響應式狀態,
// 並會透過 `this` 曝露。
data() {
return {
count: 0
}
},

// Methods 是變更狀態並觸發更新的函式,
// 可作為模板中的事件處理程序綁定。
methods: {
increment() {
this.count++
}
},

// 生命週期鉤子會在組件的不同階段被調用。
// 此函式會在組件掛載時調用。
mounted() {
console.log(`初始計數值為 ${this.count}`)
}
}
</script>

<template>
<button @click="increment">Count is: {{ count }}</button>
</template>

Composition API

使用 Composition API 時,我們透過導入的 API 函式來定義組件邏輯。在單文件組件 (SFC) 中,Composition API 通常搭配 <script setup> 使用。setup 屬性是一個提示,它讓 Vue 執行編譯時轉換,從而減少樣板代碼。舉例來說,在 <script setup> 中宣告的導入、第一層變數和函式可直接用於模板中。

以下是相同的組件,模板保持不變,但改用 Composition API 和 <script setup>

<script setup>
import { ref, onMounted } from 'vue'

// 響應式狀態
const count = ref(0)

// 改變狀態並觸發更新的函式
function increment() {
count.value++
}

// 生命週期鉤子
onMounted(() => {
console.log(`初始計數值為 ${count.value}`)
})
</script>

<template>
<button @click="increment">Count is: {{ count }}</button>
</template>

該選擇哪種風格?

這兩種 API 風格都能滿足常見的使用場景。它們是基於相同底層系統的不同介面。實際上,Options API 是基於 Composition API 實現的!Vue 的核心概念和知識在兩種風格中是共通的。

  1. Options API

    • 圍繞「組件實例」(如範例中的 this)進行設計,對於來自 OOP 語言背景的使用者,這種風格通常更符合以類為基礎的思維模型。
    • 初學者友好,通過選項分組的方式隱藏了響應式細節,簡化程式碼組織。
  2. Composition API

    • 在函式作用域中直接宣告響應式狀態變數,並透過組合多個函式來管理複雜性。
    • 更加靈活,但需要對 Vue 的響應式機制有深入理解才能有效使用。
    • 適合組織和重用邏輯的更強大模式。

學習上的建議

  • 新手學習:選擇對您來說更容易理解的風格。大多數核心概念在兩種風格中是共通的,您之後可以再學習另一種風格。
  • 生產使用
    • 如果不使用建構工具,或計畫主要用於低複雜度場景(如漸進式增強),建議使用 Options API
    • 如果計畫使用 Vue 構建完整應用,建議使用 Composition API + 單文件組件

在學習階段,我們也不需要僅限於某一種風格。可以根據適合的情境使用適合的 API 風格。

關於 Web 網頁前端工程師 Frontend Engineer 的 10 個關鍵字

· 閱讀時間約 2 分鐘
kdchang

以下整理了前端工程師相關的 10 個關鍵字,涵蓋技能、工具與工作方式:

  1. HTML:前端結構的基礎語言,用於定義網頁的內容和結構。

  2. CSS:用於設計網頁的樣式,包括佈局、配色和字體。

  3. JavaScript:前端開發的核心語言,為網頁添加互動性。

  4. React / Vue / Angular:主流的前端框架和函式庫,用於構建動態、模組化的應用程式。

  5. Responsive Design(響應式設計):確保網站在不同裝置和螢幕大小下的最佳顯示效果。

  6. REST API / GraphQL:前端與後端通訊的重要工具,用於前後端資料請求與傳輸。

  7. Webpack / Vite / Parcel:模組打包工具,用於構建和優化前端資源。

  8. Git / GitHub / GitLab:版本控制工具,用於協作開發與代碼管理。

  9. Cross-Browser Compatibility(跨瀏覽器相容性):確保網站在不同瀏覽器上的一致性表現。

  10. UI/UX Design(使用者介面/使用者體驗設計):理解使用者需求,優化網頁的易用性與美觀度。

以上關鍵字為前端工程師平常工作或是職涯的核心工作內容和技能樹,透過不斷累積相關經驗和專案開發能力,可以讓自己成為更優秀的軟體工程師。

JavaScript ES6 入門語法教學筆記 | 學習筆記

· 閱讀時間約 5 分鐘
kdchang

ECMAScript 6 又稱 ECMAScript 2015,是 JavaScript 語言的新一代標準,讓 JavaScript 可以更容易撰寫大型複雜的應用程式並避免不必要的錯誤。

以下介紹常用 ES6 入門語法:

一、let & const 變數宣告

  1. let:用於宣告變數,可重新賦值。
let name = 'John';
name = 'Mike'; // 可以重新賦值
  1. const:用於宣告常數,賦值後不可更改。
const pi = 3.14;
pi = 3.1415; // 會報錯

建議預設使用 const,僅需變更時使用 let。兩者作用域為 block scope

ES6 中,let區塊作用域(Block Scope) 是它與舊有的 var 最大的不同之一。


什麼是 Block Scope(區塊作用域)

  • 使用 let 宣告的變數,只能在該程式區塊 {} 內部存取。
  • 區塊作用域指的是任何用 {} 包起來的範圍,例如:
    • ifforwhile 等程式區塊。
    • 一般 {} 花括號內的區域。

範例說明

1. let 在區塊內的作用範圍

{
let x = 10;
console.log(x); // 10
}
console.log(x); // ReferenceError: x is not defined
  • x{} 區塊內宣告,僅在該區塊內有效。
  • 區塊外存取會出錯。

2. var 沒有區塊作用域(舊語法對比)

{
var y = 20;
console.log(y); // 20
}
console.log(y); // 20
  • var 沒有區塊作用域,y 雖在 {} 內宣告,但可在區塊外存取。

3. for 迴圈中的 let

for (let i = 0; i < 3; i++) {
console.log(i); // 0, 1, 2
}
console.log(i); // ReferenceError: i is not defined
  • i 只在 for 迴圈內有效。

let 的區塊作用域優點

  1. 避免變數污染:let 限制變數在區塊內,避免影響區塊外的程式碼。
  2. 防止重複定義:同一區塊內不能重複宣告相同變數。
    let a = 1;
    let a = 2; // SyntaxError: Identifier 'a' has already been declared
  3. 更安全、可預期的變數管理。

總結

關鍵字區塊作用域重複宣告提升(Hoisting)行為
let不可提升但不初始化(TDZ)
var提升並初始化 undefined

建議盡量用 letconst,避免使用 var
這樣可以減少潛在的 bug,也符合現代 JavaScript 開發的最佳實踐。


二、模板字串(Template Literals)

以前字串串變數要使用 +,現在可以使用反引號 (``) 定義字串,可插入變數。

const name = 'John';
const age = 25;
console.log(`我叫 ${name},今年 ${age}`);

三、箭頭函式(Arrow Functions)

  1. 基本語法:
const add = (a, b) => {
return a + b;
}
  1. 簡寫形式:
const add = (a, b) => a + b;
  1. 單一參數可省略括號:
const square = n => n * n;

箭頭函式不會綁定自己的 this,繼承外層作用域的 this


沒錯!這句話是 箭頭函式(Arrow Function) 很重要的特性之一,這裡幫你拆解得更清楚一點:


什麼是 this

this 代表函式執行時所屬的物件,依照函式被呼叫的方式不同,this 的值也會不同。

例如:

function normalFunction() {
console.log(this);
}
normalFunction(); // 在瀏覽器環境中,this 會是 window 物件

如果這個函式被某個物件呼叫:

const obj = {
name: 'John',
sayHi: function() {
console.log(this.name);
}
};
obj.sayHi(); // John,this 指向 obj

箭頭函式的 this 特性

箭頭函式不會綁定自己的 this,它會「繼承外層作用域」的 this

也就是說:

  • 傳統函式:this 依賴呼叫方式來決定。
  • 箭頭函式:this 取決於箭頭函式宣告時所在的外層作用域的 this

範例說明:

傳統函式 vs 箭頭函式

const obj = {
name: 'John',
normalFunc: function() {
console.log(this.name); // this 指向 obj
},
arrowFunc: () => {
console.log(this.name); // this 指向外層(通常是 window 或 undefined)
}
};

obj.normalFunc(); // John
obj.arrowFunc(); // undefined(或瀏覽器中可能是 window.name)

常見應用場景:回呼函式(callback)中的 this

假設我們有一個計時器:

const obj = {
name: 'John',
timer: function() {
setTimeout(function() {
console.log(this.name); // undefined 或 window.name
}, 1000);
}
};

obj.timer();

因為 setTimeout 裡的傳統函式,它的 this 在執行時會指向 window

若改用箭頭函式:

const obj = {
name: 'John',
timer: function() {
setTimeout(() => {
console.log(this.name); // John
}, 1000);
}
};

obj.timer();

箭頭函式不會綁定自己的 this,會繼承 timer 函式的 this,因此會正確印出 John


常見疑問

為什麼箭頭函式不綁定自己的 this

主要是為了解決回呼函式中 this 易出錯的問題

以前會這樣解法:

const that = this; // 變數 that 保存正確的 this
setTimeout(function() {
console.log(that.name);
}, 1000);

現在有箭頭函式,就不用這麼麻煩。


小結

類型this 綁定方式一般用途
傳統函式 function執行時決定物件方法、建構函式
箭頭函式 =>定義時決定callback 回呼函式、內部函式需要使用外部 this 的情境

總結:

  • 一般物件方法用傳統函式。this 由呼叫的物件決定
  • callback 回呼函式、內部函式用箭頭函式。

這樣就可以避免大部分 this 的混亂狀況!

四、解構賦值(Destructuring)

  1. 陣列解構:
const arr = [1, 2, 3];
const [a, b, c] = arr;
  1. 物件解構:
const person = { name: 'John', age: 25 };
const { name, age } = person;

五、展開運算符(Spread Operator)

  1. 陣列展開:
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5];
  1. 物件展開:
const obj1 = { name: 'John', age: 25 };
const obj2 = { ...obj1, city: 'Taipei' };

六、預設參數(Default Parameters)

函式參數可設定預設值:

const greet = (name = '訪客') => {
console.log(`Hello, ${name}!`);
}
greet(); // Hello, 訪客!
greet('John'); // Hello, John!

這些 ES6 基礎語法,是現代 JavaScript 開發的常用技巧,掌握這些概念能大幅提升程式撰寫效率。