JavaScript 基礎
Execution Context
當 JavaScript Engine 執行時,
會先產出全域執行上下文(Global Execution Context)
其中內建 Global Object 與 this。
此後每次調用函式就會產生一個新的執行上下文,將之送入堆疊。
Global Object 在瀏覽器稱為 window,在 Node.js 稱為 global。
所有 JS 代碼都是在全域執行上下文當中執行。
全域變數屬於 Global Object。
Lexical Environment
每個執行上下文有其所屬的 Lexical Environment(詞法環境、語彙環境)。
function add(a, b) {
return a + b;
}
function sub(a, b) {
return a - b;
}
以上述代碼為例,函式 add 與 sub 都隸屬於同一個 Lexical Environment
In JavaScript our lexical scope determines our available variables. Not where the function is called(dynamic scope).
Hoisting
Any time we call a function, there is a creation phase and an execution phase where there's hoisting involved.
在每個執行上下文的建立階段中,如果代碼結構符合 Hoisting(提升)的要件就會觸發提升;
建立階段結束後,才會進入執行階段。
以保留詞 var 或 function 開頭的代碼才會被提升。
Function Declaration 如果是編寫於 () 內,就不符合提升的要件。
test1(); // Test 1 done!
function test1() {
console.log("Test 1 done!");
}
test2(); // ReferenceError: test2 is not defined
(function test2() {
console.log("Test 2 done!");
});
變數宣告與提升
console.log(a); // undefined
var a = 1;
var a = undefined;
console.log(a);
a = 1;
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 1;
函式與提升
定義函式的方式(8 天重新認識 JavaScript)
Function Declaration 適用函式提升
square(2); // 因為 hoisting 的效果,這行不會導致報錯,可得回傳值 4
function square(number) {
return number * number;
}Function Expressions 不適用函式提升
square(2); // 因為不適用 hoisting 的效果,這行會導致報錯。
var square = function (number) {
return number * number;
};透過 new Function 關鍵詞建立函式
// 雖然可行,但效能差,實務上少用。
const square = new Function("number", "return number * number");
Function Invocation
Function Expression 定義於 runtime; Function Declaration 定義於 Parse time。
arguments
調用函式時,會創造一個新的執行上下文,在此之中會自動建立 this 與 arguments 兩個物件。
sample("Ace", "Big");
function sample(a, b) {
console.log({ arguments }); // { arguments: [Arguments] { '0': 'Ace', '1': 'Big' } }
console.log({ a, b }); // { a: 'Ace', b: 'Big' }
}
arguments Keyword
若要對傳入函式的引數進行迭代處理,可使用 arguments 物件或其餘參數(rest parameter)
sample("Ace", "Big");
function sample(a, b) {
for (value of Array.from(arguments)) {
console.log(value);
}
}
sample2(1, 2, 3);
function sample2(...nums) {
for (value of Array.from(nums)) {
console.log(value);
}
}
Variable Environment
Each execution context has its own variable environment.
function a() {
var isValid;
console.log(isValid);
}
function b() {
var isValid = true;
console.log(isValid);
a();
}
var isValid = false;
/*
以下輸出結果依序為:
true
undefined
false
*/
b();
console.log(isValid);
Scope Chain
所有函式都有一條銜接上層函式的鏈子,這條鏈子最終會連接 Global Lexical Environment,這些鏈子稱為 Scope Chain(作用鏈) 因為有作用鏈,函式可逐層尋找在外層宣告的變數。
Exercise: JS is Weird
如下列代碼所示,變數 a 未曾使用 var、let、const 之任一宣告,最後執行函式卻可輸出 10。
由於此現象容易弄髒代碼,因此為求嚴謹可在代碼首行使用嚴謹模式。
function jsIsWeird() {
a = 10;
console.log(a);
}
jsIsWeird(); // 10
"use strict";
function jsIsWeird() {
a = 10; // ReferenceError: a is not defined
console.log(a);
}
jsIsWeird();
Function Scope VS Block Scope
JavaScript 使用 var 宣告變數時,其作用範圍是 Function Scope,
亦即只有在函式內使用 var 宣告變數,該變數才會是區域變數。
反之,若不在函式內使用 var 宣告變數,該變數就會是全域變數;
因為 JS 代碼本身就彷彿是個函式。
if (5 > 4) {
// secret 之宣告發生於函式以外的地方,因此是全域變數。
var secret = 12345;
}
console.log(secret); // 12345
// secret2 宣告於函式內,屬於區域變數。
function eg() {
var secret2 = "54321";
}
console.log(secret2); // ReferenceError: secret2 is not defined
從 ES6 起,JavaScript 可使用 let 或 const 宣告變數。
以這兩個關鍵詞宣告變數時,變數作用範圍是 Block Scope。
Exercise: Block Scope
// 非 Block Scope
function loop() {
for (var i = 0; i < 5; i++) {
console.log(i);
}
// 變數 i 宣告於圓括號內,屬於 Function Scope,因此只要在 loop() 函式內都可存取。
console.log("Final", i);
}
loop(); // Final 5
console.log(i); // ReferenceError: i is not defined
// Block Scope
function loop2() {
for (let i = 0; i < 5; i++) {
console.log(i);
}
// 先前用 let 宣告 i,屬於 Block Scope,因此只要離開宣告變數的 Block 就無法存取。
console.log("Final", i); // ReferenceError: i is not defined
}
loop2();
IIFE
IIFE 是早期的 JS 用以避免全域變數過多而發生衝突的手段
// 由於編碼的開頭並非關鍵詞 function,而是括號,因此 IIFE 屬於 function expression。
(function () {})(); // undefined
// function declaration 無法在宣告後即刻調用
function test (){}(); // SyntaxError: Unexpected token ')'
// a 的作用範圍是 function scope,所以無法在全域存取。
(function () {
var a = 1;
})();
console.log(a); // ReferenceError: a is not defined
利用 IIFE 管理變數等。在 ESM 問世前,
善用此法可避免在全域環境儲存大量變數,導致管理不易。
jQuery 也是善用此技術而開發。
const myLibrary = (function a() {
function a() {
return {
a: 1,
b: 2,
c: 3,
};
}
return { a };
})();
console.log(myLibrary.a()); // { a: 1, b: 2, c: 3 }
this Keyword
this is the object that the function is a property of
function a() {
console.log(this);
}
a(); // 在瀏覽器執行時印出 window 物件,在 Node.js 執行時印出 global 物件。
function a() {
"use strict"; // 這會使 this 不指向全域物件
console.log(this);
}
a(); // undefined
this 的存在意義
- 使物件內的方法可以存取其所屬的物件
- 讓多個物件可以執行相同的代碼
// 藉由 this 存取函式所屬的物件,減少代碼重複。
const obj = {
name: "Billy",
sing: function () {
return "la~ la~ la~ " + this.name;
},
singAgain() {
return this.sing() + "!";
},
};
console.log(obj.singAgain());
// 善用 this 使多個物件共用相同的代碼
function importantPerson() {
console.log(this.name);
}
const name = "Sunny";
const obj1 = {
name: "Andy",
importantPerson,
};
const obj2 = {
name: "Beggy",
importantPerson,
};
importantPerson();
obj1.importantPerson();
obj2.importantPerson();
Exercise: Dynamic Scope vs Lexical Scope
this 的指向取決於誰調用 this 所屬的函式,而非在何處定義 this。
this 指向位於「.」的左邊的物件
// 下列程式碼中的三個 this 分別指向什麼?
const a = function () {
console.log("a", this);
const b = function () {
console.log("b", this);
const c = {
hi: function () {
console.log("c", this);
},
};
c.hi();
};
b();
};
a();
上述問題答案:
a -> window(相當於 window.a( ) ) b -> window(相當於 window.a(b( ) ),此時 b 向左延伸的第一個 . 指向 window) c -> 物件 c(上例巢狀結構中,c.hi( ) 顯示 hi 是由 c 調用,可見 this 指向 c)
ChatGPT
Q:JavaScript 中的 Dynamic Scope 與 Lexical Scope 分別是什麼意思?
在 JavaScript 中,"Dynamic Scope" 和 "Lexical Scope" 是指不同的作用域規則。
"Dynamic Scope" 是指變數的作用域是在程式執行時才決定的。這種作用域規則可以在一些非主流的程式語言中找到,但是 JavaScript 不支持動態作用域。(但根據 ZTM 影片解說,this 是例外,this 的規則相當於 Dynamic Scope)
"Lexical Scope" 是指變數的作用域是在程式編寫時已經定義的。這是 JavaScript 使用的作用域規則,它取決於變數在代碼中的位置和定義的區域。這意味著,如果一個變數被定義在一個函數中,則只有在該函數內可以訪問該變數。
const obj = {
name: "Billy",
sing() {
console.log("a", this);
var anotherFunc = function () {
console.log("b", this);
};
anotherFunc();
},
};
// 下列程式碼中執行後預期顯示什麼?
obj.sing();
// 承上題,如何使 anotherFunc 內的 this 在調用時指向 obj?
// 答:改用箭頭函式。箭頭函式屬於 Lexical Scope,this 的指向取決於其在何處定義,而非由誰調用。
// 承上題,在 ES6 之前如何使 anotherFunc 內的 this 在調用時指向 obj?答案在以下三個區塊。
const obj = {
name: "Billy",
sing() {
console.log("a", this);
var self = this;
var anotherFunc = () => {
console.log("b", self);
};
return anotherFunc();
},
};
obj.sing();
const obj = {
name: "Billy",
sing() {
console.log("a", this);
var anotherFunc = function () {
console.log("b", this);
}.bind(this);
return anotherFunc();
},
};
obj.sing();
const obj = {
name: "Billy",
sing() {
console.log("a", this);
var self = this;
var anotherFunc = function () {
console.log("b", self);
};
return anotherFunc()
},
};
obj.sing();
call(), apply(), bind()
apply 與 call 的主要用途:從其他物件借用函式並調用之
bind() 的主要用途:將某物件的函式儲存於變數,以利未來調用之。
這麼做可確保 this 在函式調用時指向其定義時所指的物件。
function test() {
console.log("test");
}
// 其實「函式名稱()」是「函式名稱.call()」的縮寫
test.call(); // test
test(); // test
const wizard = {
name: "Merlin",
health: 50,
heal() {
return (this.health = 100);
},
};
const archer = {
name: "Robin Hood",
health: 30,
};
console.log("1: ", archer); // 1: { name: 'Robin Hood', health: 30 }
// call( 調用函式的主體 )
console.log(wizard.heal.call(archer)); // 100
console.log("2: ", archer); // 2: { name: 'Robin Hood', health: 100 }
const wizard = {
name: "Merlin",
health: 50,
heal(num1, num2) {
return (this.health = num1 + num2);
},
};
const archer = {
name: "Robin Hood",
health: 30,
};
console.log("1: ", archer);
// call( 調用函式的主體, 參數1, 參數2 )
console.log(wizard.heal.call(archer, 111, 222)); // 333
console.log("2: ", archer);
// apply( 調用函式的主體, [參數1, 參數2] )
console.log(wizard.heal.apply(archer, [1, 1])); //2
console.log("3: ", archer);
apply 與 call 的差別僅止於代入參數的方法,apply 用陣列代入,call 依序代入值。
實務上會選擇較容易代入參數的方法使用。
const wizard = {
name: "Merlin",
health: 50,
heal(num1, num2) {
return (this.health = num1 + num2);
},
};
const archer = {
name: "Robin Hood",
health: 30,
};
console.log("1: ", archer);
console.log(wizard.heal.call(archer, 111, 222));
console.log("2: ", archer);
console.log(wizard.heal.apply(archer, [1, 1]));
console.log("3: ", archer);
const healArcher = wizard.heal.bind(archer, 999, 0);
healArcher();
console.log("4: ", archer); // 4: { name: 'Robin Hood', health: 999 }
bind() and currying
// 利用 bind 重複利用程式碼
function multiply(a, b) {
return a * b;
}
let multiplyByTwo = multiply.bind(this, 2);
let multiplyByTen = multiply.bind(this, 10);
console.log(multiplyByTwo(3)); // 6
console.log(multiplyByTen(3)); // 30