Learning Objectives
- 用函式宣告、函式運算式與箭頭函式定義函式,並說明函式宣告的提升
- 使用參數預設值與其餘參數,說明 return 與回傳 undefined 的情況
- 把函式當成值傳遞(callback),並使用立即執行函式
- 依呼叫方式判斷 this,並用 call、apply、bind 指定 this
- 說明箭頭函式沒有自己的 this 與 arguments
函式:把一段程式包起來重複使用
函式(function)把一段完成特定工作的程式包起來,取一個名字,需要時再呼叫。好處是同樣的程式不必寫很多次, 要修改時也只要改一個地方。
function square(number) { // square 是名稱,number 是參數
return number * number; // 大括號裡是函式的本體
}
square(5); // 呼叫函式,5 是引數,得到 25
- 宣告函式不會執行它,只是告訴 JavaScript「被呼叫時要做什麼」;呼叫
square(5)時才真的執行 - 定義時括號裡的名稱叫參數(parameter),呼叫時傳進去的值叫引數(argument)
return結束函式,並把後面的值交回給呼叫的地方;沒有return(或return;後面沒有值)時,函式回傳undefined- JavaScript 不檢查引數的個數:少給的參數是
undefined,多給的引數被忽略
宣告、呼叫與回傳
第一步就看右邊:程式還沒執行到第 4 行,square、greet、show 已經在變數區了,這是函式宣告的提升(hoisting),所以第 2 行可以先呼叫 square。呼叫函式時,右邊會多出一個框架,回傳之後就消失。
三種定義函式的寫法
| 寫法 | 例子 | 提升 |
|---|---|---|
| 函式宣告(function declaration) | function cube(n) { return n ** 3; } |
整個函式被提升,可以在宣告之前呼叫 |
| 函式運算式(function expression) | const cube = function (n) { return n ** 3; }; |
跟著變數的規則:const/let 在宣告前不能使用 |
| 箭頭函式(arrow function) | const cube = n => n ** 3; |
同函式運算式 |
函式是值:函式運算式把一個函式(通常沒有名字,叫匿名函式)存進變數,就和把數字存進變數一樣。
箭頭函式的寫法:
const double = n => n * 2; // 只有一個參數:小括號可以省略
const add = (a, b) => a + b; // 本體只有一個運算式:不寫大括號,自動回傳它的值
const add2 = (a, b) => { // 寫了大括號:要自己寫 return
const sum = a + b;
return sum;
};
const point = (x, y) => ({ x, y }); // 要直接回傳物件,外面加小括號,否則大括號會被當成本體
const hello = () => '你好'; // 沒有參數:寫空的小括號
投影片用 var 宣告函式運算式,在宣告之前呼叫會得到 TypeError: square is not a function(變數已提升但值是 undefined);
改用 const 時得到的是 ReferenceError(變數在宣告之前不能使用)。兩種都會出錯,結論一樣:函式運算式要先定義再呼叫。
函式運算式與箭頭函式
cube、double、add、makePoint 都是存在變數裡的函式。注意第 17 行在 later 宣告之前呼叫它,會丟出錯誤,被 catch 接住。
參數預設值與其餘參數
預設值:在參數後面寫 = 值,沒有傳入(或傳入 undefined)時就使用預設值。
function price(amount, discount = 1) { return amount * discount; }
price(100); // 100
price(100, 0.8); // 80
其餘參數(rest parameter):最後一個參數前面加 ...,把多出來的引數收集成一個真正的陣列,可以使用 length、join、reduce 等方法。
function sum(...nums) { return nums.reduce((t, n) => t + n, 0); }
sum(1, 2, 3, 4); // 10
舊的程式會用每個一般函式裡都有的 arguments:它包含所有傳入的引數,有 length 和索引,但不是陣列,沒有陣列的方法
(這種東西叫「類陣列」)。箭頭函式沒有自己的 arguments。現在請一律使用其餘參數。
預設值、其餘參數與 arguments
看呼叫 sum(1, 2, 3, 4) 時右邊的框架:nums 是一個陣列 [1, 2, 3, 4]。first 的第一個引數給了 label,剩下的才進入 rest。
函式是值:callback 與立即執行函式
JavaScript 的函式是一級物件(first-class object):可以存進變數、放進物件或陣列、當成引數傳給別的函式, 也可以被函式回傳。
把函式當成引數傳進去,讓對方在適當的時機呼叫,這個函式就叫 callback。你在上一課用過的 forEach、map、filter,
以及之後的 setTimeout、addEventListener,都是收 callback 的函式。
函式回傳另一個函式的用法(閉包)在第 12 課〈作用域與閉包〉說明。
立即執行函式(IIFE,Immediately Invoked Function Expression):用小括號把函式包起來,後面再加一組小括號馬上呼叫:
(function () {
const secret = 42; // 只活在這個函式裡
})();
在還沒有 let、const 與模組的年代,IIFE 是避免變數跑到全域、互相干擾的主要方法,舊程式裡很常見。
callback 與 IIFE
repeat 收到的 action 是一個函式,第 4 行每呼叫一次,右邊就多一個框架。第 19 行的 IIFE 定義完立刻以 8、7 呼叫;它裡面的 secret 在外面看不到。
想一想:定義與參數
下面的程式會印出什麼?
console.log(typeof hello);
function hello() {
return 'hi';
}
下面的程式會印出什麼?
const f = (a, b) => {
a + b;
};
console.log(f(1, 2));
下面的程式會印出什麼?
function tag(name, ...classes) {
return name + ':' + classes.length;
}
console.log(tag('div'), tag('p', 'a', 'b'));
this:看函式「怎麼被呼叫」
一般函式裡的 this 不是在定義時決定,而是在呼叫時依呼叫方式決定:
| 呼叫方式 | this 是 |
|---|---|
obj.method() |
點前面的物件 obj |
fn()(單獨呼叫) |
非嚴格模式是全域物件(瀏覽器裡是 window);嚴格模式('use strict'、模組、class)是 undefined |
fn.call(obj, a, b) |
第一個引數 obj,後面的引數一個一個給 |
fn.apply(obj, [a, b]) |
第一個引數 obj,引數放在陣列裡 |
fn.bind(obj) |
不會呼叫,而是回傳一個新函式,之後怎麼呼叫,this 都固定是 obj |
new Fn() |
新建立的物件(第 27 課〈prototype〉說明) |
| 事件處理函式 | 觸發事件的元素(第 19 課〈事件處理〉說明) |
最常見的錯誤是把方法拿出來單獨呼叫:const f = amy.hello; f(); 這時 this 已經不是 amy 了。
需要把方法交給別人呼叫時(例如當成 callback),可以先用 bind 綁定。
同一個函式,不同的 this
hello 只寫了一次,但第 8 行和第 12 行呼叫時,右邊框架裡的 this 分別是 amy 和 ben。後半段用 call、apply、bind 指定 intro 的 this。
箭頭函式沒有自己的 this
箭頭函式沒有自己的 this(也沒有自己的 arguments)。它裡面的 this,就是外層函式的 this,
不管它被誰、用什麼方式呼叫,call、apply、bind 也改不了。
這讓箭頭函式很適合當作方法裡的 callback:
const timer = {
seconds: 0,
start() {
[1, 2].forEach(() => { this.seconds++; }); // this 是 start 的 this,也就是 timer
},
};
反過來,不要用箭頭函式寫物件的方法:{ hello: () => this.name } 裡的 this 是物件外面的 this,不是這個物件。
一般函式與箭頭函式當 callback
withFunction 裡交給 forEach 的是一般函式,它被 forEach 單獨呼叫,this 不是 timer。withArrow 交出去的是箭頭函式,this.seconds++ 改到的正是 timer.seconds,看右邊的值從 0 變成 2。
想一想:this
下面的程式會印出什麼?
const cat = {
name: 'Mimi',
say() {
return this.name;
},
};
const dog = { name: 'Lucky', say: cat.say };
console.log(cat.say(), dog.say());
下面的程式會印出什麼?
function full(sep) {
return this.first + sep + this.last;
}
const p = { first: 'Ada', last: 'Lovelace' };
const f = full.bind(p);
console.log(f(' '), full.call(p, '-'));
關於箭頭函式,下列哪一個說法正確?
實作:其餘參數與 callback
完成兩個函式:average 用箭頭函式與其餘參數,收任意個數字,回傳平均(沒有數字時回傳 0);applyTo 對 list 的每個元素呼叫 fn,把回傳值收集成新陣列回傳(自己寫迴圈,不要用 map)。
重點精華回顧
- 函式宣告
function f() {}會整個被提升;函式運算式與箭頭函式存在變數裡,要先定義再呼叫。 - 沒有
return的函式回傳undefined;少給的參數是undefined,多給的引數被忽略。 - 參數預設值
(a, b = 1);其餘參數(...rest)是真正的陣列;舊式的arguments是類陣列,箭頭函式沒有它。 - 箭頭函式:單一運算式不寫大括號就自動回傳;寫了大括號要自己
return;回傳物件要加小括號。 - 函式是值:可以當成 callback 傳給別的函式;IIFE 定義完立刻執行,避免變數跑到全域。
this由呼叫方式決定:obj.method()是 obj;call/apply指定 this 並立刻呼叫;bind回傳綁定好 this 的新函式。- 箭頭函式沒有自己的 this,用的是外層函式的 this;適合當方法裡的 callback,不適合拿來寫物件的方法。