跳至主要内容

20 篇文章 含有標籤「javascript」

檢視所有標籤

Django 入門教學筆記 | 學習筆記

· 閱讀時間約 3 分鐘
kdchang

前言

Django 是一個由 Python 編寫的高階 Web 框架,強調快速開發與簡潔設計。它提供完整的功能模組如 ORM、Admin 介面、Form 處理、URL 路由與認證系統,讓開發者能專注於商業邏輯而非重複造輪子。本文將帶你一步一步建立一個基本的 Django 應用程式,並說明其核心概念。

一、環境安裝

首先,請確保你已安裝好 Python(建議 3.8+)與 pip。以下是建立虛擬環境並安裝 Django 的方式:

python -m venv myenv
source myenv/bin/activate # Windows 請使用 myenv\Scripts\activate
pip install django

確認安裝成功:

django-admin --version

二、建立 Django 專案

Django 使用專案(project)與應用程式(app)來組織程式碼。先建立一個新的專案:

django-admin startproject mysite
cd mysite

你會看到以下結構:

mysite/
manage.py
mysite/
__init__.py
settings.py
urls.py
asgi.py
wsgi.py

三、建立應用程式

應用程式是 Django 專案的功能模組。例如一個部落格、用戶系統或留言板就是一個 app。以下我們建立一個簡單的留言板:

python manage.py startapp guestbook

guestbook/ 資料夾中會看到以下結構:

guestbook/
models.py
views.py
admin.py
apps.py
tests.py
...

接著,在 mysite/settings.py 中註冊這個 app:

INSTALLED_APPS = [
...
'guestbook',
]

四、定義資料模型(Model)

編輯 guestbook/models.py,新增一個留言的資料模型:

from django.db import models

class Message(models.Model):
name = models.CharField(max_length=100)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)

def __str__(self):
return f'{self.name} 說:{self.content[:20]}...'

建立資料表:

python manage.py makemigrations
python manage.py migrate

五、啟用後台管理介面

Django 提供強大的 admin 功能。先建立一個 superuser:

python manage.py createsuperuser

然後在 guestbook/admin.py 中註冊 model:

from django.contrib import admin
from .models import Message

admin.site.register(Message)

啟動伺服器並進入後台:

python manage.py runserver

前往瀏覽器開啟 http://127.0.0.1:8000/admin/,使用剛建立的帳號登入,即可操作資料。

六、建立 Views 與 Templates

接下來,我們撰寫一個簡單的留言列表與新增留言的畫面。

views.py

from django.shortcuts import render, redirect
from .models import Message

def message_list(request):
if request.method == "POST":
name = request.POST.get('name')
content = request.POST.get('content')
if name and content:
Message.objects.create(name=name, content=content)
return redirect('message_list')

messages = Message.objects.order_by('-created_at')
return render(request, 'guestbook/message_list.html', {'messages': messages})

templates

建立目錄 guestbook/templates/guestbook/message_list.html

<!DOCTYPE html>
<html>
<head>
<title>留言板</title>
</head>
<body>
<h1>留言板</h1>
<form method="post">
{% csrf_token %}
姓名:<input type="text" name="name"><br>
留言:<br>
<textarea name="content" rows="5" cols="40"></textarea><br>
<input type="submit" value="送出">
</form>
<hr>
{% for msg in messages %}
<p><strong>{{ msg.name }}</strong> 說:</p>
<p>{{ msg.content }}</p>
<p><small>{{ msg.created_at }}</small></p>
<hr>
{% endfor %}
</body>
</html>

七、設定 URL 路由

guestbook/ 建立 urls.py

from django.urls import path
from . import views

urlpatterns = [
path('', views.message_list, name='message_list'),
]

然後到 mysite/urls.py 中加入:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
path('admin/', admin.site.urls),
path('', include('guestbook.urls')),
]

八、啟動開發伺服器

python manage.py runserver

在瀏覽器開啟 http://127.0.0.1:8000/,你將看到留言板畫面,並能提交留言。


總結

Django 是功能非常齊全且具可擴展性的 Web 框架,初學者可從資料模型、後台管理、表單處理與模板引擎著手。本文僅為入門教學,未來更多進階功能如使用 class-based views、form 類別、自訂 middleware、部署到雲端等,規劃在之後的教學筆記進行介紹。

setTimeout + for loop + closure 核心介紹入門教學筆記 | 學習筆記

· 閱讀時間約 2 分鐘
kdchang

一、varlet 的作用域差異

宣告方式作用域是否有暫時性死區(TDZ)
var函式作用域
let區塊作用域
const區塊作用域
  • var 宣告的變數在整個函式內都可存取
  • let 則只在所在區塊 {} 中有效

二、閉包(Closure)

閉包是指「函式能記住它被定義時的作用域,即使在外部執行也能存取當時的變數」。

function outer() {
let count = 0;
return function inner() {
console.log(count++);
};
}

const fn = outer();
fn(); // 0
fn(); // 1

這個例子中,inner 函式記住了 outer 中的 count 變數。


三、事件迴圈與 setTimeout

console.log("start");

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

console.log("end");

輸出順序:

start
end
timeout

原因是 setTimeout 是非同步的,它會被放入「事件佇列」(event queue)中,等主程式執行完後才會被處理。


四、面試陷阱:varsetTimeout 搭配

for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}

這段程式的輸出是:

3
3
3

因為:

  • var 沒有區塊作用域,所有的 setTimeout 都引用同一個 i
  • setTimeout 執行時,迴圈已跑完,i 已是 3

五、正確解法:使用 let

for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}

輸出為:

0
1
2

因為 let 有區塊作用域,每次迴圈都建立新的 i,閉包會記住各自的值。


六、另一個解法:IIFE(立即執行函式)

若必須用 var,可以用 IIFE 綁定每次的 i

for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => {
console.log(j);
}, 1000);
})(i);
}

這樣也會正確輸出:

0
1
2

七、總結

  1. var 是函式作用域,容易被非同步邏輯影響
  2. let 是區塊作用域,與閉包搭配可解決常見陷阱
  3. setTimeout 是非同步,會延遲執行
  4. 通常面試會結合這些概念出題,考你對 JavaScript 執行流程、作用域與閉包的理解

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),提升程式的模組化與可讀性。

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