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:只有true和false兩個值,常用於條件判斷。須留意「真值(truthy)/ 假值(falsy)」概念:false、0、-0、0n、''、null、undefined、NaN為 falsy,其餘皆為 truthy(包含[]、{}與字串'0'、'false')Undefined:是型別也是值,表示某變數已經宣告但還沒有賦值;若存取未宣告變數則會出現ReferenceErrorNull:須注意如果用typeof判斷Null的資料型別,會得到ObjectNumber:不論整數或浮點數都是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)¶
let 和 const 宣告會提升(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;比較 NaN 和 NaN 會是 false¶
同值比較(SameValue equality):Object.is,顧名思義是在比較兩個值是不是相等,可以解決上述嚴格比較中的兩種問題¶
遍歷陣列的方法與其特性¶
for:傳統 C-style 迴圈for (init; condition; update),是最有彈性的迭代方式,支援break、continue、與任意初始化條件;缺點是樣板碼較多、容易寫錯邊界條件for...of:遍歷的是元素值 (value),會先檢查對象[Symbol.iterator]這個屬性,接著使用[Symbol.iterator].next()一個個迭代出值,來確保順序正確;但由於 Object 並不具備[Symbol.iterator]這個屬性,所以for...of並不能使用在 Object 上for...in:遍歷的是的鍵值 (key)(陣列中的鍵值就是索引 (index)),並有以下特性:- 以
String型別作為鍵值 - 並不保證遍歷的順序是正確的
- 當陣列中有空項目時,會忽略該項目
- 會檢查對象的屬性是否 enumerable ,如果為真則會把這些屬性名稱全部迭代出來
- 以
Array.prototype.foo = 1;
let a = [1, 2, 3];
for (let x in a) {
console.log(x);
}
// 0 1 2 foo
forEach:針對陣列中的元素執行提供的函式,但是不能使用break和continue跳出循環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 overflow(RangeError: 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 Function。map、filter、reduce 都是典型例子:
// 接受函式作為參數
[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 Hooks:
useState、useEffect等內部大量仰賴閉包來保存每次 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 不是在「函式被定義」時決定,而是在「函式被呼叫」時根據呼叫方式動態綁定的,以下五個由低到高優先級:
- 預設綁定:直接呼叫一般函式
fn(),嚴格模式下this為undefined,非嚴格模式下為全域物件(瀏覽器是window) - 隱式綁定:以方法形式呼叫
obj.fn(),this是obj。注意const f = obj.fn; f();會遺失隱式綁定,退回預設 - 顯式綁定:用
call、apply、bind強制指定this new綁定:new Fn()會建立新物件並綁定為建構函式內的this- 箭頭函式:沒有自己的
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(用這個判斷是否為陣列)
map、filter、reduce 等方法都定義在 Array.prototype 上,透過 prototype chain 讓所有陣列都能使用。
淺拷貝 (shallow copy) 與深拷貝 (deep copy)¶
Javascript 的物件是參考型別(reference type),賦值或傳遞參數時複製的是「指向同一塊記憶體的引用」,不是值本身。
- 淺拷貝:只複製第一層,巢狀物件仍與來源共享參考
- 寫法:
{...obj}、Object.assign({}, obj)、Array.prototype.slice()、Array.from() - 深拷貝:每一層都複製一份,源物件與複本完全獨立
structuredClone(obj)— 現代瀏覽器原生 API,支援Map、Set、Date、RegExp、ArrayBuffer、循環參考,首選JSON.parse(JSON.stringify(obj))— 簡單但有缺陷:函式、undefined、Symbol、循環參考會被丟棄或報錯,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):
setTimeout、setInterval、I/O、UI rendering 等 - Microtask Queue:
Promise.then/catch/finally、queueMicrotask、MutationObserver
每當 call stack 清空後:
- 把 microtask queue 裡的 task 全部執行完畢(包含執行過程中新進來的 microtask 也一併消耗光)
- 才會取一個 macrotask 執行
- 必要時觸發 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.target 與 event.currentTarget 之差異¶
讓我們從 MDN 的定義比較兩者的區別,首先是 event.target 的定義:
The read-only
targetproperty of theEventinterface is a reference to the object onto which the event was dispatched.
接著讓我們看到 event.currentTarget 的定義:
The
currentTargetread-only property of theEventinterface identifies the element to which the event handler has been attached.
也就是說當事件觸發時,event.target 作為觸發事件的元素,而 event.currentTarget 則是事件監聽器所監聽的元素。另外補充一點,在使用函式宣告式 (declaration) 時,this 相當於這裡的 event.currentTarget。
Sources¶
- 原始連結: https://overreacted.io/what-is-javascript-made-of/
- Local copy:
raw/What Is JavaScript Made Of.md - Ingested: 2026-05-20