跳至主要内容

83 篇文章 含有標籤「前端」

檢視所有標籤

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 風格。

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 開發的常用技巧,掌握這些概念能大幅提升程式撰寫效率。