跳轉到

Basic concepts of Javascript

值(Value)與字面量(Literal)

在 JavaScript 的世界裡,值(value) 是最基本的概念。數字 2、字串 "Banana"、物件 {}、陣列 [] 都是值;但 if 陳述式、for 迴圈這類控制流程結構不是值,無法被賦值給變數或傳入函式。

字面量(Literal)

字面量是直接在程式碼中寫出值的語法:

2           // number literal
"Banana"    // string literal
true        // boolean literal
null        // null literal

每次寫下 2,你得到的都是同一個 2;每次寫下 "Banana",你得到的都是同一個字串值。

Primitive values 的不可變性

Primitive values(原生值)是 不可變(immutable) 的:你無法「建立另一個 2」,也無法修改字串 "hello" 本身。對 primitive value 進行操作,得到的永遠是一個新的值,原本的值不受影響。

let str = "hello";
str.toUpperCase(); // 回傳新字串 "HELLO",str 本身仍是 "hello"

資料型別

原生值(primitive values)

截至目前為止的原生值如下:

  • String:表示字串值,可用單引號 '...'、雙引號 "..." 或反引號 `...` 三種界定符。反引號額外支援字串插值 ${expr} 與多行字串
  • Boolean:只有 truefalse 兩個值,常用於條件判斷。須留意「真值(truthy)/ 假值(falsy)」概念:false0-00n''nullundefinedNaN 為 falsy,其餘皆為 truthy(包含 []{} 與字串 '0''false'
  • Undefined:是型別也是值,表示某變數已經宣告但還沒有賦值;若存取未宣告變數則會出現 ReferenceError
  • Null:須注意如果用 typeof 判斷 Null 的資料型別,會得到 Object
  • Number:不論整數或浮點數都是 Number 型別,其精確度介於 -(2^53 − 1)2^53 − 1 之間;須注意 +Infinity, -Infinity, 與 NaN 亦為此型別
  • BigInt:允許讓我們任意選擇其精準度,無法與 Number 型別的值交互使用
  • Symbol:為一個獨特(unique)值,可以用 Symbol() 工廠方法建立

物件(objects)

除了上述的原生值以外的存在皆是物件


變數(Variable)與作用域(Scope)

變數是「wire」

Dan Abramov 用「wire(電線)」來比喻變數:變數本身不是值,而是指向某個值的連結。你可以改變 wire 的指向(重新賦值),但這不會改變原本的值本身。

let message = "Hello";
message = "World"; // wire 改指向 "World","Hello" 本身沒有改變

Scope(作用域)

變數的作用域由最近的 {} 區塊決定。在區塊外宣告的變數可以在區塊內存取,但在區塊內宣告的變數無法在區塊外存取:

let outer = "I'm outside";

{
    let inner = "I'm inside";
    console.log(outer); // ✓ 可以存取
}

console.log(inner); // ✗ ReferenceError

Assignment(賦值)

賦值(=)改變的是 wire 的指向,不改變值本身。這對理解物件的 mutation 與 primitive 的不可變性都很重要。

暫時死區(TDZ, Temporal dead zone)

letconst 宣告會提升(hoist)至區塊作用域,但不會初始化。在宣告語句執行之前,該變數存在於暫時死區(TDZ),此時存取會拋出 ReferenceError

var 則不同:提升至函式作用域並初始化為 undefined,因此不存在 TDZ。

換個角度來看,const 作為一個常數本就不應變動,若賦值前後拿到不同值,就不符合常數原本設計的理念!

let vs const vs var

關鍵字 作用域 可重新賦值 Hoisting 行為
let 區塊(block) 提升但不初始化(TDZ)
const 區塊(block) ✗(禁止重新賦值) 提升但不初始化(TDZ)
var 函式(function) 提升並初始化為 undefined

建議優先使用 let;若值不需要重新賦值則用 const;避免使用 var,其函式作用域規則容易造成難以追蹤的 bug。


比較的區別

鬆散比較(loose equality):==,在比較之前會先強制轉換型別與值

嚴格比較(strict equality):===,在比較之前不會強制轉換型別與值。須注意當我們比較 +0-0 時,嚴格比較會回傳 true;比較 NaNNaN 會是 false

同值比較(SameValue equality):Object.is,顧名思義是在比較兩個值是不是相等,可以解決上述嚴格比較中的兩種問題


遍歷陣列的方法與其特性

  • for:傳統 C-style 迴圈 for (init; condition; update),是最有彈性的迭代方式,支援 breakcontinue、與任意初始化條件;缺點是樣板碼較多、容易寫錯邊界條件
  • for...of:遍歷的是 元素值 (value),會先檢查對象 [Symbol.iterator] 這個屬性,接著使用 [Symbol.iterator].next() 一個個迭代出值,來確保順序正確;但由於 Object 並不具備 [Symbol.iterator] 這個屬性,所以 for...of 並不能使用在 Object 上
  • for...in:遍歷的是的 鍵值 (key)(陣列中的鍵值就是 索引 (index)),並有以下特性:
    1. String 型別作為鍵值
    2. 並不保證遍歷的順序是正確的
    3. 當陣列中有空項目時,會忽略該項目
    4. 會檢查對象的屬性是否 enumerable ,如果為真則會把這些屬性名稱全部迭代出來
    Array.prototype.foo = 1;

    let a = [1, 2, 3];
    for (let x in a) {
      console.log(x);
    }
    // 0 1 2 foo
  • forEach:針對陣列中的元素執行提供的函式,但是不能使用 breakcontinue 跳出循環
  • map:針對陣列中的元素執行提供的函式,並回傳新陣列
  • filter過濾 掉沒有通過所提供函式的元素,並回傳新陣列
  • every:測試 每一個 元素是否通過提供的函式,最終返回一個 布林值(Boolean),若其中有一個元素測試值為 false,就會提前結束
  • some:同上,但若其中有一個元素測試值為 true,就會提前結束

函式(Function)進階

Function Expression vs Function Declaration

JavaScript 有兩種定義函式的語法:

// Function Declaration(函式宣告)
function sayHi() {
    console.log("Hi!");
}

// Function Expression(函式表達式)
let sayHi = function() {
    console.log("Hi!");
};

兩者在語意上相似,但有一個關鍵差異:hoisting

Function Hoisting

只有 Function Declaration 會被完整 hoisting(包含函式本體),因此可以在宣告之前呼叫:

sayHi(); // ✓ 正常執行,因為 declaration 被 hoisted

function sayHi() {
    console.log("Hi!");
}

greet(); // ✗ TypeError: greet is not a function

let greet = function() {
    console.log("Hello!");
};

Function Expression 賦值給的變數(let greet)雖然會被 hoisted,但在賦值前處於 TDZ,呼叫時會報錯。

Arguments / Parameters

Parameters(參數) 是函式定義時的佔位符;Arguments(引數) 是實際呼叫時傳入的值。兩詞在日常溝通中常互換使用:

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

add(1, 2);             // 1, 2 是 arguments

Call Stack(呼叫堆疊)

每次呼叫函式,JavaScript 引擎會建立一個新的執行環境(execution context),並推入 call stack。可以把每個執行環境想像成一個「房間(room)」:進入函式就開一間新房間,函式結束就關掉這間房間,回到上一間。

function greet(name) {
    return sayHello(name);  // 進入 sayHello 的房間
}

function sayHello(name) {
    return `Hello, ${name}!`;  // 執行完畢,關閉此房間
}

greet("John");  // 進入 greet 的房間

Recursion(遞迴)與 Stack Overflow

遞迴是函式呼叫自身的技巧。每次遞迴呼叫都會在 call stack 上新增一個執行環境;若遞迴沒有終止條件(base case),call stack 會不斷增長直到超出限制,引發 stack overflowRangeError: Maximum call stack size exceeded):

// 正確的遞迴:有 base case
function factorial(n) {
    if (n <= 1) return 1;       // base case
    return n * factorial(n - 1);
}

// 危險的遞迴:無限遞迴 → stack overflow
function infinite() {
    return infinite();
}

Higher-Order Function(高階函式)

接受函式作為參數、或回傳函式的函式,稱為 Higher-Order Functionmapfilterreduce 都是典型例子:

// 接受函式作為參數
[1, 2, 3].map(x => x * 2); // [2, 4, 6]

// 回傳函式
function multiplier(factor) {
    return (number) => number * factor;
}
const double = multiplier(2);
double(5); // 10

Callback(回呼)

Callback 是一種 pattern(設計模式),而非 JavaScript 的特定語法。把函式 A 傳入函式 B,期待 B 在適當時機「call back」A:

function doSomething(callback) {
    // ... 做一些事
    callback(); // 呼叫傳入的函式
}

doSomething(() => console.log("Done!"));

非同步場景(如 setTimeout、事件監聽)大量使用 callback pattern。

Function Binding(函式綁定)

.bind(thisArg, ...args) 建立一個新函式,預先綁定 this 與部分參數,原函式不受影響:

function greet(greeting, name) {
    console.log(`${greeting}, ${name}! I am ${this.role}.`);
}

const admin = { role: "Admin" };
const adminGreet = greet.bind(admin, "Hello"); // 預綁 this 與第一個參數
adminGreet("John"); // "Hello, John! I am Admin."

立即調用函式 IIFE (Immediately Invoked Function Expression)

宣告完後立刻執行的函式表達式,主要目的是建立一個獨立的作用域,避免污染全域。以下三種寫法皆可:

(function () {
    var secret = 'cannot be accessed outside';
    console.log(secret);
})();

(function () { /* ... */ }());   // Crockford 風格
(() => { /* ... */ })();         // 箭頭函式版

ES6 之前,IIFE 是模擬「區塊作用域」與「模組私有變數」的主要手法。ES6 之後有了 let/const 的區塊作用域,以及具備自身作用域的 ES Module,IIFE 的需求大幅降低。但以下情境仍適用:

  • 立即計算並取值給某個常數(特別是初始化邏輯需要多行)
  • 一次性的副作用,且不希望留下任何識別符在外層
  • 需要在表達式中插入一段命令式邏輯

閉包 (Closure)

當函式內回傳另一個函式(或將內部函式以其他方式傳出去),即使外層函式已經執行完畢,內層函式仍能存取外層當時的變數 — 這個保留住詞法環境的函式即為閉包。

function counter() {
    let count = 0;
    return function () {
        return ++count;
    };
}

const c = counter();
c(); // 1
c(); // 2
c(); // 3

count 並沒有隨著 counter() 結束就被回收,因為回傳的函式仍引用著它。閉包的典型應用:

  • 私有變數:模擬封裝,外部無法直接讀寫
  • 工廠函式:每次呼叫都產生獨立的狀態(如上例的 counter
  • 柯里化(currying)與部分應用:把參數分階段帶入
  • React HooksuseStateuseEffect 等內部大量仰賴閉包來保存每次 render 之間的狀態

經典陷阱 — 在迴圈中以 var 配合 callback,所有 callback 共用同一個變數:

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

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

let 每次迭代都會產生新的 binding,閉包各自捕捉到不同的 i。這也是 let 相較 var 安全的重要理由之一。


Javascript 中的 this

this 不是在「函式被定義」時決定,而是在「函式被呼叫」時根據呼叫方式動態綁定的,以下五個由低到高優先級:

  1. 預設綁定:直接呼叫一般函式 fn(),嚴格模式下 thisundefined,非嚴格模式下為全域物件(瀏覽器是 window
  2. 隱式綁定:以方法形式呼叫 obj.fn()thisobj。注意 const f = obj.fn; f(); 會遺失隱式綁定,退回預設
  3. 顯式綁定:用 callapplybind 強制指定 this
  4. new 綁定new Fn() 會建立新物件並綁定為建構函式內的 this
  5. 箭頭函式沒有自己的 this,沿用宣告時所在的詞法作用域,且不受 call/apply/bind 影響
const obj = {
    name: 'A',
    say() { console.log(this.name); }
};

obj.say();              // 'A',隱式綁定
const detached = obj.say;
detached();             // undefined(嚴格)或全域,遺失綁定
detached.call(obj);     // 'A',顯式綁定救回來

// 箭頭函式的 this 在宣告處就決定
const arrow = () => console.log(this);
setTimeout(arrow, 0);   // 沿用外層 this,不會被 setTimeout 改變

常見場景:

  • DOM event handler 用 function 宣告,this 是觸發事件的元素(也就是前面提到的 event.currentTarget);改用箭頭函式則沿用外層 this
  • React class component 的方法需 .bind(this),或直接以 class field 的箭頭函式定義,才能在 callback 中保留 this

關於原型 (prototype)

Javascript 是 prototype-based 語言,物件之間透過 prototype 鏈共享行為,而非透過 class 繼承。每個物件都有一個內部插槽 [[Prototype]],指向另一個物件(或 null)。屬性查找會沿著 [[Prototype]] 一路往上找直到 null,找不到才回 undefined

const obj = {};
Object.getPrototypeOf(obj) === Object.prototype; // true
Object.getPrototypeOf(Object.prototype);         // null(鏈的終點)

函式(function)有 prototype 屬性,作為以 new 建構出的實例的原型:

function Person(name) {
    this.name = name;
}
Person.prototype.sayHi = function () {
    return `Hi, ${this.name}`;
};

const p = new Person('John');
p.sayHi();                                       // 'Hi, John'
Object.getPrototypeOf(p) === Person.prototype;   // true

ES6 的 class 語法本質上是 prototype 機制的語法糖,並未引入新的繼承模型。實務上建議:

  • Object.getPrototypeOf / Object.setPrototypeOf 操作原型,避免使用已不建議的 __proto__
  • Object.create(proto) 直接以指定原型建立物件
  • 不要動 built-in 物件的原型(如 Array.prototype.foo = ...),會污染所有陣列並破壞 for...in 等行為

物件(Object)進階

Object Identity(物件識別)

每次寫下 {},JavaScript 都會建立一個全新的物件。即使兩個物件的內容完全相同,它們也不是同一個物件:

{} === {}   // false,兩個不同的物件
[] === []   // false,兩個不同的陣列

const a = {};
const b = a;
a === b;    // true,a 和 b 指向同一個物件

=== 對物件比較的是參考(reference),即兩個變數是否指向記憶體中的同一個值。這與 primitive values 的比較(比較值本身)不同。

Dot Notation vs Bracket Notation

存取物件屬性有兩種語法:

const iceCream = { flavor: "vanilla", price: 50 };

// Dot Notation(點記法):屬性名稱為固定字串
iceCream.flavor;          // "vanilla"

// Bracket Notation(括號記法):屬性名稱可以是變數或動態字串
const ourProperty = "flavor";
iceCream[ourProperty];    // "vanilla"
iceCream["price"];        // 50

Bracket Notation 在屬性名稱為動態值、或包含特殊字元時不可或缺。

Mutation(變異)

Mutation 是指直接修改物件的屬性,而非建立新物件:

const iceCream = { flavor: "vanilla" };
iceCream.flavor = "chocolate"; // mutation:修改了 iceCream 的 flavor 屬性

注意:const 只禁止重新賦值(改變 wire 的指向),無法阻止 mutation:

const iceCream = { flavor: "vanilla" };
iceCream = { flavor: "chocolate" }; // ✗ TypeError:不能重新賦值
iceCream.flavor = "chocolate";      // ✓ 合法:mutation 不受 const 限制

這是 React / Redux 強調「不可變更新(immutable update)」的原因:直接 mutate state 不會觸發 re-render,必須建立新物件。

Array 是特殊的 Object

Array 本質上是以數字索引為 key 的物件:

// ["banana", "chocolate"] 在概念上等同於:
// { 0: "banana", 1: "chocolate", length: 2 }

const fruits = ["banana", "chocolate"];
fruits[0];          // "banana"(bracket notation)
typeof fruits;      // "object"
Array.isArray(fruits); // true(用這個判斷是否為陣列)

mapfilterreduce 等方法都定義在 Array.prototype 上,透過 prototype chain 讓所有陣列都能使用。


淺拷貝 (shallow copy) 與深拷貝 (deep copy)

Javascript 的物件是參考型別(reference type),賦值或傳遞參數時複製的是「指向同一塊記憶體的引用」,不是值本身。

  • 淺拷貝:只複製第一層,巢狀物件仍與來源共享參考
  • 寫法:{...obj}Object.assign({}, obj)Array.prototype.slice()Array.from()
  • 深拷貝:每一層都複製一份,源物件與複本完全獨立
  • structuredClone(obj) — 現代瀏覽器原生 API,支援 MapSetDateRegExpArrayBuffer、循環參考,首選
  • JSON.parse(JSON.stringify(obj)) — 簡單但有缺陷:函式、undefinedSymbol、循環參考會被丟棄或報錯,Date 會變字串
  • 第三方:lodash 的 cloneDeep
const original = { a: 1, nested: { b: 2 } };

const shallow = { ...original };
shallow.nested.b = 99;
console.log(original.nested.b); // 99,被牽連到了

const deep = structuredClone(original);
deep.nested.b = 42;
console.log(original.nested.b); // 仍然是 99,互不影響

實務上:React / Redux 的 state 更新必須產生新的 reference 才能觸發 re-render;理解淺/深拷貝的邊界對於避免「state 改了畫面沒更新」這類 bug 很關鍵。


Event Loop 與 microtask / macrotask

Javascript 是單執行緒,所有非同步機制都透過 event loop 排程。執行模型可拆成:

  • Call Stack:目前正在執行的同步程式碼
  • Task Queue(macrotask)setTimeoutsetInterval、I/O、UI rendering 等
  • Microtask QueuePromise.then/catch/finallyqueueMicrotaskMutationObserver

每當 call stack 清空後:

  1. 把 microtask queue 裡的 task 全部執行完畢(包含執行過程中新進來的 microtask 也一併消耗光)
  2. 才會取一個 macrotask 執行
  3. 必要時觸發 rendering,回到步驟 1
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 輸出順序:1 → 4 → 3 → 2

理解 microtask 優先於 macrotask,能解釋很多「setTimeout(0) 為什麼仍比預期晚」的現象。另外,在 microtask 中不斷產生新的 microtask 會「餓死」macrotask 與 rendering,導致畫面凍結。


event.targetevent.currentTarget 之差異

讓我們從 MDN 的定義比較兩者的區別,首先是 event.target 的定義:

The read-only target property of the Event interface is a reference to the object onto which the event was dispatched.

接著讓我們看到 event.currentTarget 的定義:

The currentTarget read-only property of the Event interface identifies the element to which the event handler has been attached.

也就是說當事件觸發時,event.target 作為觸發事件的元素,而 event.currentTarget 則是事件監聽器所監聽的元素。另外補充一點,在使用函式宣告式 (declaration) 時,this 相當於這裡的 event.currentTarget


Sources