--- tags: JavaScript, 六角筆記王 title: 函式 --- # 函式 若要有效的處理資料,就一定會用到函式,這篇記錄函式有怎樣的特性與運用概念。目前有整理到的如下: - 函式型別 - 立即函式 - 函式的參數使用,不同的函式會有不同的內建關鍵字,常見的有 - `arguments` (ES6箭頭函式則無此參數) - `this` (ES6箭頭函式則無此參數) - 自訂參數 - 閉包 - 範圍鏈 - 函式與提升 (Hoisting) - 函式與變數利用 - 閉包的私有函式 ## 宣告函式 - 使用 `function` 來宣告一個函式。 - 具名函式,宣告函式後,給定一個名稱以利後續呼叫。 - 匿名函式,宣告函式後,不須給定名稱,常用於立即函式與變數賦予值上。 - 名稱要給也是可以,但通常不會這麼做。 - ES6中可以使用箭頭函式。 - 雖然型別為 `function`,但它其實是物件的一種。 ```javascript /* 具名函式 */ 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 的結果中往內層展開並找到新增的屬性 ```javascript /* 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 = '新增屬性值' // } // } // } ``` ## 立即函式 - 一般函式需要呼叫它才會執行 - `函式名稱()` - 立即函式會立刻執行,給定名稱沒有意義 - `( 名稱(){} )()` - `( (){} )()` ```javascript /* 一般函式 */ function fn(a){ console.log(a) } /* 立即函式 */ // 它會立刻執行,所以也可以寫成匿名函式 (function (a){ console.log(a) })('函式') /* 箭頭與立即 */ ( a => {console.log(a)} )('函式') ``` ## 函式與參數 一段函式中,常見的內建參數有以下這些 : - arguments - this - 自訂參數 ```javascript var a = '全域物件中的變數' function fn(a){ console.log(a, arguments , this.a) } fn('自訂參數') // '自訂參數' // ['自訂參數'],它為類陣列 // '全域物件中的變數',為什麼不是 '自訂參數',在後面 this會有說明 ``` ### 參數關鍵字 arguments - 能接收所有傳入的值 - 它為類陣列,也就說它沒有陣列的原型方法可以使用 - 其型別為 `"object"` ```javascript 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(上)](https://hsuan777.github.io/2020/11/02/javascript/ES6%E7%AE%AD%E9%A0%AD%E5%87%BD%E5%BC%8F%E8%88%87this%E4%B8%8A/) - [箭頭函式與 this(下) - call、apply、bind](https://hsuan777.github.io/2020/11/02/javascript/ES6%E7%AE%AD%E9%A0%AD%E5%87%BD%E5%BC%8F%E8%88%87this%E4%B8%8B/) #### this 與簡易呼叫 還記得 `this` 只看在哪裡被呼叫嗎? 又怎樣算是簡易呼叫(simple call)? 來看以下例子 : ```javascript 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` 還是指向全域? 沒關係,繼續往下看另一個例子。 ```javascript 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呢 ? 來改寫上面的例子, ```javascript 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的單一元素 ```javascript console.dir('某個標籤元素') ``` #### this 與 call、apply、bind - call與 apply會立刻執行,bind則需要呼叫。 - **非嚴格模式傳入 `null`、`undefined`,則會指向全域物件**。 - `this` 為物件型別,也就是說傳入後會變成包裹物件(建構式)。 - 若傳入的是數字,會變成 `Number()` - 若傳入的是字串,會變成 `String()` ```javascript 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`。 ```javascript 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 ``` ### 函式與自訂參數 #### 參數傳入值 - 當函式執行時,只會根據位置傳入值 - 在函式中,參數所運用的地方才會是對應的 ```javascript function fn(a, b, d, e){ console.log(d, e, a, b) } fn('a', 'b', 'c', 'd') // "c" "d" "a" "b" ``` #### 參數傳入物件 當利用函式內的程式碼修改傳入的物件時,會依據傳參考特性修改原物件的屬性值,若物件在函式內做了深層複製的話則不會。 #### 參數傳入函式( callback function ) 除了可以傳入值、物件、陣列,也可以將另一個函式傳入。 ```javascript // 函式參數傳入函式 function callSomeone(name, a){ console.log(name, '你好',a) } function fnB(fn){ fn('小明', 1) } fnB(callSomeone) ``` ## 參考來源 >1. [六角學院 - JS核心篇] >2. [cythilya - 你懂 JavaScript 嗎?#4 型別(Types)](https://ithelp.ithome.com.tw/articles/10200841) >3. [Kuro Hsu - 重新認識 JavaScript: Day 10 函式 Functions 的基本概念](https://ithelp.ithome.com.tw/articles/10191549) >4. [Ray - 有點長的淺談 JavaScript function 函式](https://w3c.hexschool.com/blog/cb6e361) >5. [Huli - 淺談 JavaScript 頭號難題 this:絕對不完整,但保證好懂](https://blog.techbridge.cc/2019/02/23/javascript-this/)