克服JS 的奇怪部分 #3:當 function 被呼叫時
- Published on
- understanding-the-weird-part
上一篇提到 function 是裝著程式碼的容器,有 code property 屬性可以呼叫,當它被呼叫之後、會建立新的執行環境,執行環境將決定裡面的程式碼如何執行。
創造階段,JS 幫你設定的東西:
Variable Environment
:函式中變數所存放的地方,EC 建立時會順便建立自己的變數環境this
:依據函式被呼叫的方式來決定,如果是以 object 形式呼叫的話,JS 引擎會自動幫你指向包含它的物件Outer Environment
:函式的外部環境參考,取決於函式被寫在什麼地方(在自己的變數環境找不到變數時,會順著 Scope Chain 往外面找、直到全域)- arguments:包含了所有你傳入 function 的變數
this 變數
一般情況,在 function 裡面呼叫 this 是指向全域 Window
function a() {
console.log(this)
this.newvariable = 'hello' // 在全域物件 Window 新增屬性
}
var b = function () {
console.log(this)
}
a() // Window
b() // Window
console.log(newvariable) // hello
當函數是連結到物件的方法時,事情就不一樣了!
this => 物件
var c = {
name: 'The c object',
log: function () {
this.name = 'Update c object'
console.log(this)
},
}
c.log() // {name: "Update c object", log: ƒ}
這時,裡面新增一個 setName 函數呼叫時會改變 this.name 的值。
但實際上卻沒有改到 this.name 的值,反而在 Window 物件中多出來一個 name 屬性,代表剛剛 setName
函數裡面的 this 指向的是全域物件、而不是 c object!
var c = {
name: 'The c object',
log: function () {
this.name = 'Update c object'
console.log(this)
var setName = function (newname) {
this.name = newname
}
setName('Update again!') // 🔴 not work!
},
}
c.log() // {name: "Update c object", log: ƒ}
這是因為,setName
執行時是作為一般函式,而不是作為某某物件的方法執行,因此它的 this 變數會是 JS 引擎預設的 this,指向全域 Window 物件。
如果要解決 this 的問題,通常會在物件方法裡面先寫一行 var self = this
,因為物件是 by reference 來設定值,self 會指向跟目前 this 一樣的記憶體位置,這樣之後要用到同一變數,就不需要考慮 this 指向被 JS 改變的問題,仍然指向整個 c 物件。
var c = {
name: 'The c object',
log: function () {
var self = this // ✅ 最開始就指定,確保用到對的物件
self.name = 'Update c object'
console.log(self)
var setName = function (newname) {
self.name = newname // 順著 scope chain 尋找
}
setName('Update again!')
},
}
c.log() // {name: "Update again!", log: ƒ}
arguments
過去的 arguments
一般以為 arguments 就是 parameter 的另一稱呼,但在 JS 裡面幫你設定了一個叫 arguments
關鍵字,包含了所有你傳入的值
- 執行 function statement 時已經先預留這些值的記憶體空間(hoisting)
- 可以省略傳入或部分
- arguments 類似於陣列(array-like),有一些陣列的方法
greet('zoe')
function greet(first, second, third) {
console.log(first)
console.log(second)
console.log(third)
console.log(arguments) // ['zoe'] 因為只有傳入一個參數
}
ES6 之後,可以在參數設定給預設值
function greet(first, second, third = 'en') {
// # 檢查是否有帶參數
if (arguments.length === 0) {
console.log('Missing parameters!')
return // return statement 將離開函式
}
console.log(first)
console.log(second)
console.log(third)
console.log(arguments)
}
greet() // Missing parameters!
greet('John') // John undefined en ['John']
greet('John', 'Doe', 'es') // John Doe ess ['John', 'Doe', 'es']
spread parameter
在 ES6 之後可以透過 ...
取得沒有直接寫出來的參數,會是一個陣列來存這些
function greet(first, second, third, ...others) {
console.log(others) // ["hello", "zoe"]
}
greet('John', 'Doe', 'es', 'hello', 'zoe')
最後補充一個面試常考到的 IIFE
IIFE
立即呼叫的函式表示式(Immediately-invoked Function Expressions)
利用 function expression 創造函數物件之後,馬上呼叫
// function expression
var greet = (function (name) {
return 'hello ' + name
})()
console.log(greet) // hello undefined
不像 function statement 沒有函式名稱時會報錯
JS 在解析下面這段程式碼時,預期它是 function statement,因為用了 function 作為開頭,當沒有給出名稱時就會拋出錯誤
function (name) { // 🔴 Unexpected token!
return 'hello' + name
}
用括號包著,可以讓它變成 function expresstion
📔 JS 認為用 ()
包著的東西是一個表示式,會以 expression 方式來讀取。而 IIFE 指的就是透過 function expression 來建立函式,立即執行它,在同一時間創造和執行
;(function (name) {
return 'hello' + name
})(
// IIFE
function (name) {
var greeting = 'Hello'
return greeting + name
}
)('John') // 用 () 呼叫
使用立即函式的好處,除了可以立即執行程式碼,省略多餘呼叫,還可以用來避免汙染全域執行環境的東西,減少開發時因相同命名相衝的 bug
以下圖為例,可以把 IIFE 的變數鎖定在 Execution Context
把東西包在 IIFE 裡面,就不會跟其他 Execution Context 衝突到