0%

JS核心篇-函式

若要有效的處理資料,就一定會用到函式,這篇記錄函式有怎樣的特性與閉包運用概念。
目前有整理到的如下 :

  • 函式型別
  • 立即函式
  • 函式的參數使用,不同的函式會有不同的內建關鍵字,常見的有
    • arguments (ES6箭頭函式則無此參數)
    • this (ES6箭頭函式則無此參數)
    • 自訂參數
  • 閉包
    • 範圍鏈
    • 函式與提升 (Hoisting)
    • 函式與變數利用
    • 閉包的私有方法

函式

  • 使用 function 來宣告一個函式。
  • 具名函式,宣告函式後,給定一個名稱以利後續呼叫。
  • 匿名函式,宣告函式後,不須給定名稱,常用於立即函式與變數賦予值上。
    • 名稱要給也是可以,但通常不會這麼做。
  • ES6中可以使用箭頭函式。
  • 雖然型別為 "function",但它其實是物件的一種。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 具名函式 */
function fnA(){
return '具名函式'
}
fnA()

/* 匿名函式 */
var fnB = function (){
return '匿名函式'
}
fnB()

/* ES6箭頭函式 */
let fnC = () => {
return '箭頭函式'
}
console.log(typeof fnA) // "function"

函式與型別

通常我們會利用 typeof() 來檢查目前值為哪一種型別,若對函式做檢查,會得到 "function",但很多技術文章都說,函式其實也是一種物件,那要怎麼知道它的確是物件的一種呢?

  • 利用物件原型方式看子型別為何,會得到 "[object Function]"
  • 將函式當作物件新增屬性,console.log 可以正確取值。
  • 將函式使用 new 這個運算子做為物件,可以在 console.log 的結果中往內層展開並找到新增的屬性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* typeof 型別檢查為 funciton */
const fn = () => { return 1 }
console.log(typeof fn) // 'function'

/* 利用物件原型來看函式屬於哪一種子型別 */
console.log(Object.prototype.toString.call(fn))
// "[object Function]"

console.log(fn.prototype) // Object { 許多屬性 }

/* 函式可以新增屬性 */
fn.b = '新增屬性值'
console.log(fn.b) // '新增屬性值'

/* 將函式以 new這個運算子,成為一個建構式函式 */
// 可以在原型物件(__proto__)底下的 constructor中,發現新增的屬性 b
console.log(new fn)

// fn{
// __proto__: {
// constructor: fn {
// b = '新增屬性值'
// }
// }
// }

立即函式

  • 一般函式需要呼叫它才會執行
    • 函式名稱()
  • 立即函式會立刻執行,給定名稱沒有意義
    • ( 名稱(){} )()
    • ( (){} )()
1
2
3
4
5
6
7
8
9
10
11
12
13
/* 一般函式 */
function fn(a){
console.log(a)
}

/* 立即函式 */
// 它會立刻執行,所以也可以寫成匿名函式
(function (a){
console.log(a)
})('函式')

/* 箭頭與立即 */
( a => {console.log(a)} )('函式')

函式與參數

一段函式中,常見的內建參數有以下這些 :

  • arguments
  • this
  • 自訂參數
1
2
3
4
5
6
7
8
var a = '全域物件中的變數'
function fn(a){
console.log(a, arguments , this.a)
}
fn('自訂參數')
// '自訂參數'
// ['自訂參數'],它為類陣列
// '全域物件中的變數',為什麼不是 '自訂參數',在後面 this會有說明

參數關鍵字 arguments

  • 能接收所有傳入的值
  • 它為類陣列,也就說它沒有陣列的原型方法可以使用
  • 其型別為 "object"
1
2
3
4
5
6
7
8
9
10
11
function sum(a, b) {
console.log(arguments)
console.log(typeof(arguments))
}
sum(2, 4, 6)
// Arguments{
// [2, 4, 6],
// callee: f sum(),
// length: 3
// }
// "object"

參數關鍵字 this

參考自己整理 this筆記,this在不同位置中會指向誰。

this 與簡易呼叫

還記得 this 只看在哪裡被呼叫嗎? 又怎樣算是簡易呼叫(simple call)?
來看以下例子 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var a = '全域物件'

function fnA(){
console.log(this.a)
}

function fnB(){
var a = '函式內變數'
function fnC(){
console.log(this.a)
}
fnC()
}
fnA() // '全域物件'
fnB() // '全域物件'

為什麼兩個都是指向全域?

  • fnA() 在全域環境中被呼叫,且函式在全域環境下被定義,這點很容易看出來,就算把 this 拿掉,也會尋找外層的變數 a。
  • fnB() 被呼叫後,執行了 fnC() ,它明明在 fnB() 內被執行,為什麼 this 還是指向全域? 沒關係,繼續往下看另一個例子。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var a = '全域物件'
var obj = {
a: '物件屬性值',
fnA: function(){
console.log(this.a)
},
fnB: function(){
var a = '函式內變數'
function fnC(){
console.log(this.a)
}
fnC()
}
}

obj.fnA() // "物件屬性值"
obj.fnB() // "全域物件"

為什麼執行物件中的兩個函式,結果會不一樣,照理說要指向物件中的屬性 obj.a 吧?

  • 物件中的 fnA()this 正確指向了該物件
  • 物件中的 fnB() 執行後,再執行 fnC()this 卻指向了全域物件?
    • 執行 obj.fnA() ,是由 obj 呼叫 fnA()
    • 執行 obj.fnB() ,是由 obj 呼叫 fnB(),接著執行 fnC()
      fnC() 被誰呼叫? 沒有,它是直接被執行的,所以它的 this 指向了全域。
    • 若將 fnC() 內的 this.a 改為 a,則會因為範圍鏈的關係往外層尋找,就會是 '函式內變數'

再來複習一下 this 與簡易呼叫。

  • 簡易呼叫是甚麼?
    • 直接呼叫,未透過任何方式取用執行。
  • 簡易呼叫為全域物件下的一種?
    • 否,是 this 會指向全域,而不是該函式被建立在在全域下
  • 那些例子屬於簡易呼叫?
    • callback function
    • 立即函式
    • 原型方法,例如陣列的 forEach,此為繼承共用的原型方法,屬於直接執行。

那如果要將 fnC() ,正確的指向物件屬性 a呢 ? 來改寫上面的例子,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var a = '全域物件'
var obj = {
a: '物件屬性值',
fnA: function(){
console.log(this.a)
},
fnB: function(){
var a = '函式內變數'
// 這裡的 this 會指向該物件
var vm = this
function fnC(){
// 閉包的原理,讓還需用到的變數,將它關在函式中,不被釋放
console.log(vm.a)
}
fnC()
}
}

obj.fnA() // "物件屬性值"
obj.fnB() // "物件屬性值"

為什麼 fnB()this 賦予給一個變數時,就不會指向全域,而是會指向物件呢?

  • fnB() ,是由物件 obj 所呼叫的,就跟 fnA() 一樣,他們的 this 都會指向該物件。
  • fnB() 內宣告一個變數,將指向物件的 this 賦予給該變數,fnC() 再利用此變數時,就會正確的指向物件 obj
  • 後面的段落會說明閉包的概念。

this 與DOM

  • console.dir 可以看到它原本的物件有哪些屬性
  • 可以使用 this 指向 DOM的單一元素
1
console.dir('某個標籤元素')

this 與 call、apply、bind

  • call與 apply會立刻執行,bind則需要呼叫。
  • 非嚴格模式傳入 nullundefined,則會指向全域物件
  • this 為物件型別,也就是說傳入後會變成包裹物件(建構式)。
    • 若傳入的是數字,會變成 Number()
    • 若傳入的是字串,會變成 String()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var c = 5
function fn(a, b){
console.log(this)
console.log(this+a)
console.log(this+b)
console.log(this.c)
}

fn.call(1, 2, '3')
// Number 1
// 3
// "13"
// undefined

fn.call(null, 2, '3')
// Window
// "[object Window]2"
// "[object Window]3"
// 5

嚴格模式

  • 加入 use strict
  • 不會影響不支援嚴格模式的瀏覽器
  • 可依據執行環境設定 use strict
  • 透過拋出錯誤的方式消除ㄧ些安靜的錯誤(消除小錯誤)
  • 禁止使用ㄧ些有可能被未來版本 ECMAScript定義的語法
  • 在這些方法中的 this ,預設值是 undefined,若未傳入值又去呼叫它,就會是 undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function fn(a, b){
'use strict'
// 在嚴格模式下的 call、apply、bind 不會變成建構式物件
console.log(this)
console.log(this+a)
console.log(this+b)
console.log(this.c)
}

fn.call(null, 2, '3')
// null
// 2
// "null3"
// Cannot read property 'c' of null

函式與自訂參數

參數傳入值

  • 當函式執行時,只會根據位置傳入值
  • 在函式中,參數所運用的地方才會是對應的
1
2
3
4
5
function fn(a, b, d, e){
console.log(d, e, a, b)
}
fn('a', 'b', 'c', 'd')
// "c" "d" "a" "b"

參數傳入物件

當利用函式內的程式碼修改傳入的物件時,會依據傳參考特性修改原物件的屬性值,若物件在函式內做了深層複製的話則不會。

參數傳入函式( callback function )

除了可以傳入值、物件、陣列,也可以將另一個函式傳入。

1
2
3
4
5
6
7
8
// 函式參數傳入函式
function callSomeone(name, a){
console.log(name, '你好',a)
}
function fnB(fn){
fn('小明', 1)
}
fnB(callSomeone)

閉包

閉包為範圍鏈、記憶體空間、表達式以及函式等觀念總合的概念,重複執行函式時,其變數值就會累加上去。要利用閉包的概念前,先來複習一下函式還有怎樣的特性。

  • 範圍鏈
  • 函式與提升
  • 函式與記憶體空間

函式與範圍鏈

在函式中的變數,若沒有宣告,則往外層尋找直到全域,若全域也沒有,則報錯。

靜態作用域

跟在哪裡被呼叫無關,跟位置有關。
來看以下範例,函式的位置會影響最後的結果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var a = 1
function fnA(){
console.log(a)
}
function fnB(){
var a = 2
fnA()
}
fnB() // 1
// fnA()內本身沒有 a變數,則向外層找
// 它也不在 fnB()內,於是找到了全域物件中的變數 a

function fnC(){
var a = 3
function fnD(){
console.log(a)
}
fnD()
}
fnC() // 3
// fnD()本身沒有 a變數,則向外層找
// 在 fnC()找到已宣告的變數 a

動態作用域

跟在哪裡使用無關,跟在哪裡被呼叫有關,最常見的就是 this 的運用。

函式與提升(Hoisting)

在函式中,

  • 將自訂參數重新宣告不會有任何作用
  • 函式中的函式,不會提升至外層
  • 若在函式中不將變數宣告或者非自訂參數,則可能依據呼叫環境在全域物件中建立變數
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function fn(a){
console.log(a)

// 將自訂參數重新宣告不會有任何作用
var a
console.log(a)

// 賦予新值
a = 1
console.log(a)

// 若在裏頭插入函式,它不會提升到外層
function fnB(){
console.log(a)
}
fnB()
b = 2
}
fn('我是函式')
console.log(a)
console.log(b)
// '我是函式'
// '我是函式'
// 1
// 1
// a is not defined
// 2,b未正式宣告且因為範圍鏈(向外層尋找)的關係,在全域物件中建立了變數

函式與記憶體空間

先看下面的例子,函式每次執行時,都會加 1。

1
2
3
4
5
6
7
8
9
10
var a = 0
function fn(){
a += 1
console.log(a)
}
fn() // 1
fn() // 2
fn() // 3
fn() // 4
fn() // 5

如果將變數 a,移動到函式內,重複執行函式時,就會因為記憶體空間被釋放掉,又重新宣告而無法累計。

1
2
3
4
5
6
7
8
9
10
function fn(){
var a = 0
a += 1
console.log(a)
}
fn() // 1
fn() // 1
fn() // 1
fn() // 1
fn() // 1

接下來,改寫上述範例,假設今天去夜市套圈圈,老闆的每個籃子裡總共有五個圈圈,規則是每次只能丟一個圈圈。今天有兩個玩家,都另開新局,他們都能有五個圈圈可投。

  • return 一個函式,此函式包含著變數,該變數會視為還須利用而不會被上層的函式釋放掉
  • 分別宣告變數並賦予函式,函式內部所宣告的變數不會互相影響
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function game(){
    var times = 5
    return function(){
    times -= 1
    return `圈圈剩餘 ${times} 個`
    }
    }
    var personA = game()
    console.log(personA()) // 圈圈剩餘 4個
    console.log(personA()) // 圈圈剩餘 3個
    console.log(personA()) // 圈圈剩餘 2個
    console.log(personA()) // 圈圈剩餘 1個
    console.log(personA()) // 圈圈剩餘 0個

    var personB = game()
    console.log(personB()) // 圈圈剩餘 4個
    console.log(personB()) // 圈圈剩餘 3個
    console.log(personB()) // 圈圈剩餘 2個
    console.log(personB()) // 圈圈剩餘 1個
    console.log(personB()) // 圈圈剩餘 0個

閉包進階與函式工廠

這次老闆使用機器人自動丟五個圈圈出去,每次丟一個就紀錄一次剩餘多少個圈圈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function game(){
var times = 5
var gameLog = []
for (var x = 0 ; x < 5 ; x++){
times -= 1
gameLog.push(`第 ${x+1} 次丟出圈圈剩餘 ${times} 個`)
}
return gameLog
}
var personA = game()
console.log(personA)
// ["第 1 次丟出圈圈剩餘 4 個",
// "第 2 次丟出圈圈剩餘 3 個",
// "第 3 次丟出圈圈剩餘 2 個",
// "第 4 次丟出圈圈剩餘 1 個",
// "第 5 次丟出圈圈剩餘 0 個"]

閉包進階的私有方法

讓我們再度強化機器人的可用性,基礎次數五次,每丟一次就記錄一次到陣列中,利用閉包的原理,將函式 return一個物件。
但是,這台機器人卻有個地方無法正常顯示,該怎麼修復它呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function game(){
var times = 5
var gameLog = []

return {
log: gameLog,
nowTimes: `目前剩餘次數為 ${times} 次`,
addTimes: function(number){
times += number
return `目前剩餘次數為 ${times} 次`
},
start: function(){
for (var x = 0 ; x < 5 ; x++){
times -= 1
gameLog.push(`第 ${x+1} 次丟出圈圈剩餘 ${times} 個`)
}
return '開始自動執行遊戲'
}
}
}

var personA = game()

console.log(personA.addTimes(25)) // 30
console.log(personA.nowTimes) // 卻還是顯示 5次

函式 personA return的物件,其包含許多函式與變數,而單純變數值卻無法修改? 我們來檢查看看,

1
2
3
4
5
6
7
8
9
// 節錄 personA片段
console.log(personA)
// Object {
// nowTimes: `目前剩餘次數為 5 次`,
// addTimes: function(number){
// times += number
// return `目前剩餘次數為 ${times} 次`
// },
// }

可以看到有的直接變成字串,而待在函式裡的變數則維持變數的樣子,讓我們再回想閉包的原理,若該函式內的變數有需要用到,則記憶體空間不會被釋放掉,也就是說,屬性值為單純的變數,被使用後就釋放掉了,待在函式內的則不會。那麼,我們來修正機器人,將 addTimes 這個屬型值改為函式,這樣就能正確顯示了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function game(){
var times = 5
var gameLog = []

return {
log: gameLog,
nowTimes: function(){
return `目前剩餘次數為 ${times} 次`
},
addTimes: function(number){
times += number
return `目前剩餘次數為 ${times} 次`
},
start: function(){
for (var x = 0 ; x < 5 ; x++){
times -= 1
gameLog.push(`第 ${x+1} 次丟出圈圈剩餘 ${times} 個`)
}
return '開始自動執行遊戲'
}
}
}

var personA = game()

console.log(personA.addTimes(25)) // 30
console.log(personA.nowTimes()) // 30

參考來源

  1. 六角學院 - JS核心篇
  2. cythilya - 你懂 JavaScript 嗎?#4 型別(Types)
  3. Kuro Hsu - 重新認識 JavaScript: Day 10 函式 Functions 的基本概念
  4. Ray - 有點長的淺談 JavaScript function 函式
  5. Huli - 淺談 JavaScript 頭號難題 this:絕對不完整,但保證好懂
  6. Huli - 所有的函式都是閉包:談 JS 中的作用域與 Closure
  7. OneJar - 你不可不知的 JavaScript 二三事#Day5:湯姆克魯斯與唐家霸王槍—變數的作用域(Scope) (1)