跳至主要内容

[JS] Function基本介紹-3

TL;DR

本篇介紹:

  1. 函式的參數
    1. arguments
    2. this
    3. 區域變數以及透過範圍鏈取得的外部變數
  2. 將函式視為參數傳入(即callback function)

參考資料

相關連結


函式的所有參數

透過簡單的範例,我們先來了解一個函式內,可以運用的參數有哪些。

const globalVariable="全域變數";
const obj={
fn(param){
const localVaribale="區域變數";
console.log('param',param);
console.log('localVaribale',localVaribale);
console.log('globalVariable',globalVariable);
console.log('arguments',arguments);
console.log('this',this);
}
}

obj.fn('作為參數傳入的字串1','作為參數傳入的字串2',3,4,null,undefined,false,true);

// param 作為參數傳入的字串1
// localVaribale 區域變數
// globalVariable 全域變數
// arguments
// Arguments(8) ['作為參數傳入的字串1', '作為參數傳入的字串2', 3, 4, null, undefined, false, true]
// this
// {fn: ƒ}

執行結果圖示

上方範例程式碼執行結果

參數與區域變數Hoisting

我們都知道,javascript在宣告變數的時候具有Hoisting的特性。
透過以下範例來釐清在函式內所定義的變數的hoisting,與外部傳入參數的順序。

結論
  1. 外部傳入參數的創造階段先於內部變數的創造階段
  2. 參數的變數名稱可以透過重新賦值覆蓋掉外部傳入的內容

首先先驗證參數變數名稱可以透過重新賦值的方式來覆蓋:

function callName(myName) {
console.log(myName);

var myName;
console.log(myName);
myName='杰倫';
console.log(myName);
}
callName('小明');
// 小明
// 小明
// 杰倫

中間的var myName其實沒有產生任何影響,直接拿掉運作結果也會相同:

function callName(myName) {
console.log(myName);

myName='杰倫';
console.log(myName);
}
callName('小明');
// 小明
// 杰倫
note

如果中間var改為let則會被視為重複宣告變數。而改為const除了檢查是否重複宣告以外,還需要給定初始值。

所以建議都使用ES6let以及const來宣告變數,可以避免許多錯誤。

再來看另一個範例
因為使用function所宣告的函式,在hoisting時會移動到作用域的最前方,那麼我們來確認一下是否會先於參數變數:

function callName(myName) {
console.log(myName);

function myName(){}

myName='杰倫';
console.log(myName);
}
callName('小明');

// ƒ myName(){}
// 杰倫

如果function的hoisting早於參數,那麼印出來的就還會是小明。可以看到funciont myName移動到第一個console.log之前,但是仍然是在參數傳入之後,所以印出來的會是f myName(){}

傳入參數不足的狀況

如果呼叫函式時傳入的參數少於定義函式時的參數數量,則不足的部分會是undefined。且傳入變數名稱與內容或外部變數名稱無關,只與位置有關。

function callMore(d,c,b,a) {
console.log(d,c,b,a);
}
const a='a';
const b='b';
const c='c';
callMore(a,b,c);
// a b c undefined

參數預設值

如果傳遞的參數數量不足時,預設會是undefined。在ES6以前,我們需要透過以下的方式來檢查我們的參數,避免在執行函式過程中出現錯誤:

function count(a,b) {
a = a || 2;
b = b || 3;
return a + b;
};

console.log(count(6,3)); // 9
console.log(count()); // 5 因為沒有代入參數,所以會出現預設的a=2,b=3

上方程式碼其實還有一個問題。當我們使用短路求值時,||前方的參數如果為falsy,則會回傳後方的值。也就是說,如果我們傳入的數值為0,也會被當作falsy

count(0,2) // 4 

所以為了避免這個問題我們還需要額外再處理0的狀況...

function count(a, b) {
a = Number(a) || 2;
b = Number(b) || 3;
return a + b;
}

或是改用三元運算子的方法解決

function count(a, b) {
a = (typeof a === 'number') ? a : 2;
b = (typeof b === 'number') ? b : 3;
return a + b;
}

還沒開始寫function的內容頭就昏了!

好在ES6之後有一個很好用的新語法-預設參數。我們可以簡單的將上方的函式改寫為:

function count(a = 2, b = 3) {
return a + b;
}

只要我們有傳遞參數時,就會使用我們所傳遞的參數。而我們沒傳遞參數時就會使用預設值代替。

info

需要注意如果我們直接將undefined給傳遞進去,仍然會被替代為預設參數

function count(a = 2, b = 3) {
return a + b;
}
count(0,2) //2
count(undefined,2) //4

這邊不敘述太多關於預設參數的特性,如果需要參考可以直接查看MDN文件。

透過參數傳遞物件

透過參數在傳遞物件的時候,仍然是傳參考(call by reference),所以當函式去調整物件內的內容時,外層的物件也會被調整。

function callObject(obj) {
obj.name= '杰倫家'
}
const family={
name: '小明家'
}
callObject(family);
console.log(family); // {name: '杰倫家'}

Callback function

因為JS具有一級函式的特性,所以一個函式可以視為參數傳遞到另一個函式之中,就是我們很常聽到的Callback cunction。

基本範例

首先先看一個簡單的範例,來理解什麼是callback function:

function foo(bar) {
bar('小明'); //實際執行傳入匿名函式的位置在這邊
}
foo(function(param){
console.log(param);
})

// 小明

在執行foo的時候,將一個匿名函式透過參數bar傳入。接著直接執行傳入的匿名函式,並且將參數(小明)給傳入。

基本範例延伸

我們也可以預先定義好一個函式,並且直接將該函式當作參數傳入

function callSomeone(myName,id) {
console.log(`我的名字是${myName}`,`我的學號是${id}`)
}
function foo(bar) {
bar('小明',7) //實際執行位置,參數在function boo內才給定
}
foo(callSomeone) //將具名函式直接傳入,因為不需要執行所以這邊不需要加上小括號

// 我的名字是小明 我的學號是7號

arguments

在本篇一開始的範例中,有介紹arguments這個變數。主要會將所有傳入的參數都放在arguments這個變數內。

function callArg(foo) {
console.log(foo,arguments)
}
callArg('bar',1,2,3,'foobar')
// bar
// Arguments(5) ['bar',1,2,3,'foobar']

需要注意arguments並不是陣列,而是一個類陣列,所以並沒有forEach等等的方法可以使用。

// 使用for可以正確運作
function callArg(foo) {
console.log(foo,arguments);
for(let index=0; index<arguments.length;index++){
console.log(arguments[index]);
}
}
callArg('bar',1,2,3,'foobar')
// bar
// Arguments(5) ['bar',1,2,3,'foobar']
// bar
// 1
// 2
// 3
// foobar
// 不具有forEach方法所以會拋出錯誤
function callArg(foo) {
console.log(foo,arguments);
arguments.forEach(function(){})
}
callArg('bar',1,2,3,'foobar')
// bar
// Arguments(5) ['bar',1,2,3,'foobar']
// Uncaught TypeError: arguments.forEach is not a function