--- title: 'JS 核心 32 - ES6 章節:箭頭函式' tags: JS 核心 ,JS , JavaScript, ES6 description: 2021/02/24 --- JS 核心 -- ES6 章節:箭頭函式 === ## 箭頭函式簡介 > 「箭頭函式」也是 ES6 的新增語法 關於 ES6 的新增語法 * 讓原有 JS 撰寫更為精簡 (語法糖),但運作邏輯和傳統 JS 觀念上是一樣的。 * 讓 JS 撰寫觀念更為直覺、簡便 (新方法),但運作邏輯和傳統 JS 觀念上會有不同。 ## 箭頭函式如何縮寫 ### 先來看一段函式表達式 先宣告一個變數 callName,後面接函式的內容。把此函式賦予到變數 callName 上。 ``` const callName = function(someone){ // 會傳入一個參數 return '我是' + someone; // 會 return 一個值 } console.log(callName('小明')); // 我是小明 ``` ### 縮寫,改成箭頭函式 1. 把 function 此關鍵字移除 2. 在「參數」的**右邊**補上「=>」 ``` const callName = (someone) => { return '我是' + someone; } console.log(callName('小明')); // 我是小明 ``` ### 還可以縮寫,縮寫條件為 <span class="red">"如果程式碼內容為表達式時" 就可縮寫</span> > 表達式就是可以回傳一個值。 > 以上例來說,函式內沒有其他內容,直接回傳一個值,就可做縮寫。 1. 把程式碼改成「單行」 2. 把「大括號」去掉 3. 把「return」拿掉 : 會自動「return」回傳箭頭後面的結果 ``` const callName = (someone) => '我是' + someone; console.log(callName('小明')); // 我是小明 ``` 當只有<span class="red">**一個參數**</span>時,可以把小括號拿掉。 ``` const callName = someone => '我是' + someone; console.log(callName('小明')); // 我是小明 ``` 當<span class="red">**沒有參數**</span>時,此小括號不可省略。 ``` const callName = () => '小括號不可省略'; console.log(callName()); // 小括號不可省略 ``` 當<span class="red">**二個參數**</span>時,此小括號不可省略。 ``` const callName = (a, b) => '我是' + a + b; console.log(callName('林', '小花')); // 我是林小花 ``` --- ## 與傳統函式不同之處 - 箭頭函式沒有 arguments 這個參數 箭頭函式屬於新增的方法,和傳統函式會有不同。 ### 先來看看「傳統函式」裡的 arguments 參數 * 傳統函式在執行時會自動帶上 arguments 這個參數 * arguments 參數會將我們傳入的參數一一列出 * <span class="red">arguments 參數為「**類陣列**」</span> ,和一般的陣列不一樣 ``` const nums = function(){ console.log(arguments); } nums(10, 50, 100, 50, 5, 1, 1, 1, 500); // 看到 arguments 物件,裡面帶入的值就是我們所傳入的參數 ``` ![](https://i.imgur.com/cm4MW5R.png) 把上例改成「箭頭函式」: 因為<span class="red">**「箭頭函式」沒有 arguments 這個參數,所以跳錯。**</span> ``` const nums = () => { console.log(arguments); // 跳錯,ReferenceError: arguments is not defined } nums(10, 50, 100, 50, 5, 1, 1, 1, 500); // ReferenceError (無法去存取一個不存在的變數) ``` ![](https://i.imgur.com/eqftz8b.png) Q : 但有些時候必須把未列出的參數也取出,那該怎麼做 ? A : 使用「其餘參數」 ### 「其餘參數」用法 前面加上「3個點」,後面接「變數名稱」 ``` const nums = (...arg) => { console.log(arg); } nums(10, 50, 100, 50, 5, 1, 1, 1, 500); ``` ![](https://i.imgur.com/EmApvLG.png) ## 與傳統函式不同之處 - <span class="red">「箭頭函式」沒有自己的 this</span> ### this 的綁定和 "如何呼叫" 有很大的關係 * this 與函式如何宣告沒有關係,僅與**呼叫方法**有關 * 物件的方法調用時,僅需要關注**是在哪個物件下**呼叫的 ```typescript= var myName = '全域' var person = { myName: '小明', callName: function(){ console.log('1', this.myName); // 1 小明 setTimeout(function(){ // 簡易呼叫, this 指向 window console.log('2', this.myName); // 2 全域 console.log('3', this); // 3 (this 指向 window) }, 10); } } person.callName(); // 在 person 物件下呼叫 callName 函式時,若 callName 函式內有 this,this 就會指向 person ``` * 把上例程式碼中的 setTimeout 改成「箭頭函式」。 因為「箭頭函式」沒有自己的 this,若在「箭頭函式」裡看到 this 就先當作不存在,<span class="red">**this 會使用外層作用域的 this**</span> (指向外層的 person 物件)。 > 目前 setTimeout 在 callName 函式裡,改成「箭頭函式」就變成套用外層 person 物件的 this。 ``` var myName = '全域' var person = { myName: '小明', callName: function(){ console.log('1', this.myName); // 1 小明 setTimeout(() => { // 箭頭函式,this 指向外層作用域的 person 物件 console.log('2', this.myName); // 2 小明 console.log('3', this); // this 指向外層作用域的 person 物件 }, 10); } } person.callName(); ``` 接著,再把 callName 函式也變成「箭頭函式」 > 目前 callName 在 person 物件裡,改成「箭頭函式」就變成套用外層 window 環境的 this。 ``` var myName = '全域' var person = { myName: '小明', callName: () => { // 箭頭函式,沒有自己的 this,this 指向 window console.log('1', this.myName); // 1 全域 setTimeout(() => { // 箭頭函式,沒有自己的 this,this 指向 window console.log('2', this.myName); // 2 全域 console.log('3', this); // this 指向 window }, 10); } } person.callName(); ``` ### this 不同,導致 DOM 的 this 也會指向不同位置 #### 「傳統函式」: 監聽器會綁定 this HTML 部分 ``` <p>這裡具有一段話</p> ``` JS 部分 : 透過 click 方式把 DOM 位置取出 ``` const ele = document.querySelector('p'); ele.addEventListener('click', function(){ // 透過 click 方式取出 DOM 的位置 console.log(this); // <p>這裡具有一段話</p> (這裡 this 指的是 HTML 裡的 DOM 元素) this.style.background = 'yellow'; // 點擊 DOM 元素即變色 }); ``` #### 若改成「箭頭函式」: this 指向全域 window ``` const ele = document.querySelector('p'); ele.addEventListener('click', () => { // 透過 click 方式取出 DOM 的位置 console.log(this); // 指向全域 window (「箭頭函式」沒有自己的 this) // this.style.background = 'yellow'; }); ``` ### 「箭頭函式」沒有自己的 this,也無法透過 call, apply, bind 重新給予 this 先前章節有提到可以透過 call 的方式把另外一個物件傳入,並作為函式執行的 this 使用。 ``` const family = { myName: '小明家', } const fn = function(para1, para2) { console.log(this, para1, para2); } fn.call(family, '小明', '杰倫'); // {myName: "小明家"} "小明" "杰倫" ``` 若改成「箭頭函式」 ``` const family = { myName: '小明家', } const fn = (para1, para2) => { console.log(this, para1, para2); } fn.call(family, '小明', '杰倫'); // 全域 window "小明" "杰倫" ``` ## 與傳統函式不同之處 -「箭頭函式」無法作為「建構函式」使用 假設有兩段「建構函式」,一個是用「傳統函式」撰寫、另一個是用「箭頭函式」撰寫。 ``` const Fn = function (a) { this.name = a; // this 指向傳入的參數 (作為建構函式用) } const ArrowFn = (a) => { // 無法作為建構函式使用 this.name = a; } ``` 來看一下「傳統函式」、「箭頭函式」撰寫的「建構函式」prototype 長怎樣 ~ ``` console.log(Fn.prototype, ArrowFn.prototype); // {constructor: ƒ} undefined ``` **結果 :** * 傳統函式有 prototype,可以做為「建構函式使用」 * 「箭頭函式」沒有 prototype,不能做為「建構函式使用」 #### 再來使用 new 方式來新增物件實體 ``` const a = new Fn('a'); // 可以另外傳入參數作為物件的屬性用 console.log(a); // Fn {name: "a"} const b = new ArrowFn('b'); // 跳錯,(ArrowFn is not a constructor) // ArrowFn 並不是一個建構函式,他沒有 prototype ``` --- ## 常見問題 -「箭頭函式」實作中常見的錯誤 ### :writing_hand: 「箭頭函式」若不加<span class="red">大括號和return</span>,可以直接回傳後面的值 ``` const arrFn = () => 1; console.log(arrFn()) // 1 ``` ### :writing_hand: 「箭頭函式」不能直接回傳<span class="red">物件實字 (要加上小括號)</span> 假設預期要回傳一個物件 {data: 1},為什麼卻回傳了 undefined ? ``` const arrFn = () => {data: 1}; // 預期要回傳此物件 {data: 1} console.log(arrFn()) // undefined ``` 整理一下程式碼發現 : {data: 1} 其中的大括號其實是做為「箭頭函式」裡的程式碼片段範圍使用,並不是物件實字裡所使用的大括號。 ``` const arrFn = () => { // 此大括號為「箭頭函式」裡的程式碼片段範圍使用 data: 1 // 無法回傳 }; console.log(arrFn()) // undefined ``` 若在實作中,想要回傳物件內容該如何做調整 ? 在**物件外層**再加一對小括號,就能正確回傳物件內容。 ``` const arrFn = ()=> ({data: 1}); // 在 {data: 1} 兩邊加上小括號,即可回傳物件內容 console.log(arrFn()); // {data: 1} ``` ### :writing_hand: <span class="red">「判斷式」後面不能搭配「箭頭函式」使用,結構上會有錯誤 (SyntaxError)</span> ### <span class="red">修正錯誤 : 加上小括號</span> **「傳統函式」範例 :** (第 2 行) 由判斷式得知 1. function(){ return 1 } 為「函式表達式」。 2. 因為 num 為 0 是"假值",所以會把「函式表達式」直接賦予到變數 numFn 上。 ```typescript= let num = 0; // 先宣告一個數字 const numFn = num || function(){ return 1 }; console.log(numFn()); // 1 ``` **「箭頭函式」範例 :** 結構上會有錯誤,錯誤碼 (SyntaxError) ``` let num = 0; const numFn = num || () => 1; console.log(numFn()); // 跳錯 (SyntaxError) ``` **「箭頭函式」範例 :** 修正以上問題,要加上小括號 ``` let num = 0; const numFn = num || (() => 1); console.log(numFn()); // 1 ``` ### :writing_hand: 若物件內的方法必須取用 this 時,注意「箭頭函式」的 this 可能無法取到預期中的物件。 ### <span class="red">取用物件內的方法(其中方法為「箭頭函式」),this 為指向外層作用域的 this。</span> 定義一個物件,裡面有一個 callName 方法(使用「箭頭函式」)。 > 使用「箭頭函式」要注意 this 的指向。 ``` const person = { myName: '小明', callName: () => { // 「箭頭函式」沒有自己的 this,這個 this 指向全域 console.log(this.myName); // undefined }, } person.callName(); // this 指向全域,無法正確取到 person 物件內的 myName ``` 解決方法 : 使用「傳統函式」 ``` const person = { myName: '小明', callName: function () { console.log(this.myName); // 小明 }, } person.callName(); ``` ### :writing_hand: 物件裡若要搭配「箭頭函式」,注意指向是不同的,也會影響運行結果。 <span class="red">Vue 的生命週期 created : created 是當我們的 Vue 執行到這個**元件**時,在一開始就優先執行這段程式碼。</span> 使用「傳統函式」可以正確在 Vue 的元件裡取得自己的資料內容。 ``` const app = new Vue({ // 將 Vue 實體建立出來 data: { num: 1 }, created: function(){ // 傳統函式 console.log(this.num); // 1 (可以取到自己的 data) } }) ``` 若把 created 改成「箭頭函式」就會出錯 ``` const app = new Vue({ // 將 Vue 實體建立出來 data: { num: 1 }, created: () => { console.log(this.num); // undefined } }) ``` ps : 此段範例需載入 vue 的 CDN 才能實作 ``` <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script> ``` ### :writing_hand: 「箭頭函式」無法作為「建構函式」使用 下面一段程式碼,使用「傳統函式」做為「建構函式」使用 ``` const Fn2 = function (a) { this.name = a; } ``` 在 Fn2「建構函式」的原型下,使用「箭頭函式」來新增原型方法。 此段程式碼裡的<span class="red">**箭頭函式 this 指向不同,指向全域**</span>。無法正確的指到 Fn2「建構函式」物件本身。 ``` Fn2.prototype.protoFn = () => { return this.name; } ``` 使用 new 把 Fn2「建構函式」實體化 ``` const newObj = new Fn2('函式'); // 實體化 console.log(newObj); ``` 可以在 newObj 的物件實體下找到新增的原型方法。 ![](https://i.imgur.com/2DlVSfJ.png) 但是可能在執行時會出錯,找不到資料。 無法得到 newObj 物件裡的 name。 ``` console.log(newObj.protoFn()); // 得到空值 ``` 原因是「箭頭函式」this 指向和「傳統函式」不一樣。 this 其實是指向「全域」,無法正確的指到 Fn2「建構函式」物件本身。 --- ## :memo: 學習回顧 :::info * <span class="red"></span> ::: ## :+1: 相關參考文件 :::info ::: <style> .red { color: red; } .green { color: green; } </style>