跳至主要内容

JavaScript 基礎

Execution Context

當 JavaScript Engine 執行時,
會先產出全域執行上下文(Global Execution Context)
其中內建 Global Object 與 this。
此後每次調用函式就會產生一個新的執行上下文,將之送入堆疊。
Global Object 在瀏覽器稱為 window,在 Node.js 稱為 global。

備註

所有 JS 代碼都是在全域執行上下文當中執行。
全域變數屬於 Global Object。

Lexical Environment

每個執行上下文有其所屬的 Lexical Environment(詞法環境、語彙環境)。

sample.js
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!");
});

變數宣告與提升

用 var 宣告變數會觸發提升
console.log(a); // undefined
var a = 1;
相當於上述代碼
var a = undefined;
console.log(a);
a = 1;
用 let 宣告變數不會觸發提升
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

Function 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
// 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

this 指向函式隸屬的物件
function a() {
console.log(this);
}

a(); // 在瀏覽器執行時印出 window 物件,在 Node.js 執行時印出 global 物件。
this 與嚴謹模式
function a() {
"use strict"; // 這會使 this 不指向全域物件
console.log(this);
}

a(); // undefined

this 的存在意義

  1. 使物件內的方法可以存取其所屬的物件
  2. 讓多個物件可以執行相同的代碼
this 範例代碼
// 藉由 this 存取函式所屬的物件,減少代碼重複。
const obj = {
name: "Billy",
sing: function () {
return "la~ la~ la~ " + this.name;
},
singAgain() {
return this.sing() + "!";
},
};

console.log(obj.singAgain());
this 範例代碼
// 善用 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();
方法二:使用 bind
const obj = {
name: "Billy",
sing() {
console.log("a", this);
var anotherFunc = function () {
console.log("b", this);
}.bind(this);
return anotherFunc();
},
};

obj.sing();
方法三:提前將 this 儲存於 self 變數
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