作為 typeof
都要給上幾分面子的 function
,果然還是有一篇獨立來介紹一下比較好。
儘管在前面的文章裡,我們早已散散落落的在各處提到它,用到它,討論它,函式就是一個這麼重要的角色。
這篇會盡量涵蓋前面提過的部分,順便當作一個複習確認一下這些特性都被記住了。
函式是一個具可複用性的程式碼區塊,具對應的作用域,可以接受參數的傳入與回傳參數。
基礎的函式定義方式通過 function
關鍵字定義。
console.log(typeof foo);//"function"
console.log(typeof foo.__proto__.__proto__)//"object"
console.log(foo());//"bar"
function foo(){
console.log('bar');
}
這樣 foo
就是一個最基本的函式,儘管 typeof
會針對函式回傳 function
,那是因為便於讓使用者針對函示做特別的處理,因為函式實在是太廣泛使用了。
實際上跟著原型鏈往上看,會發現一如前面說的除了原始型別,所有的複合型別都是物件,function
也不例外。
透過 ()
便能夠執行函式。
函式會被提升到該作用域的頂端,即使語句順序上先使用後宣告也沒問題。
上面的例子也同時展示了這個特性。
有了提升,讓我們在多函式時無需特別費心處理函式宣告順序排放。
除了上面的基本宣告方法,還有很多方式可以宣告函式,首先,既然函式是個物件,當然他也可以被作為 RHS 的賦值對象。
console.log(typeof a);//"undefined"
try {console.log(typeof b);}
catch (error) {console.log(`${error.name}: ${error.message}`);}
//"ReferenceError: Cannot access 'b' before initialization"
var a = function foo(){
console.log('bar');
}
let b = function foo2(){
console.log('bar2');
}
a();//"bar"
b();//"bar2"
但透過這種方式創建的函式,提升行為變成宣告接受的變數容器一致,如果是 var
就是提升宣告,如果是 let
就是 TDZ。
函式不一定要有名稱,像上面我們定義了很多個由 foo
來命名的函式。
但我們可能會想要做一些運算,立即執行,為種理由使用 一次性的 函式,或是 函式名稱並不重要的函式(例如賦值給了變數,之後透過該變數呼叫而不是函式名稱)。
(function (){
console.log('anonymous');
})();//"anonymous"
上面的語法透過 ()
包住 function
物件,接著在後面接上一個 ()
會讓前面 ()
內的函式物件立刻被執行,稱作立即執行函式,英文全名 Immediately Invoked Function Expression,通常會簡稱 IIFE
(四個字的字首)。
好處就是簡潔,適當使用的話省去不必要的命名的功夫,減少命名衝突,且 IIFE
一旦執行完就會釋放對應使用的內部變數,語句,不會污染全域變數。
當然不當使用就會變成多次重複定義,沒有名稱的函式意圖較為不明顯,倚賴賦值方式來被重複呼叫等等反向缺點,總之就是使用上要斟酌。
ES 6 引入了一種新的匿名函式宣告方式,稱作箭頭函式,因語法中有 ()=>
箭頭而得名。
來看個箭頭函示的範例:
function foo(){
return (a)=>{
console.log(this.a);
};
}
let obj1 = {a : 'first a'};
let obj2 = {a : 'second a'};
let obj1foo = foo.call(obj1);
obj1foo.call(obj2);//"first a"
function fooOld(){
return function(){console.log(this.a);};
}
let obj1fooOld = fooOld.call(obj1);
obj1fooOld.call(obj2);//"second a"
foo
函式的宣告最後就回傳了一個箭頭函式。
透過箭頭函式指向的物件,會保有指向箭頭函式調用時綁定的 this
,且該 this
無法被任何方法覆蓋,連 new 都不行。
這就是昨天 this
文章最後說的 this
的一個特例。
obj1foo
存的是一個將回傳箭頭函式的 this
指向 obj1
的函式。
儘管嘗試使用 .call
明確綁定也覆蓋不掉 this
(一般通過 .call
的語法能將 this
改為指向 obj2),仍然印出了 obj1 上的 'first a'
。
如果回傳的是一般的函式寫法,則可以看到 obj1fooOld
被 call(obj2)
的明確綁定給覆寫了 this
。
在 JS 中,函式的傳入參數允許是不定量的,即使函式僅定義了一個或未定義參數,實際上仍能從函式內部訪問傳入的內容。
function foo(a){
console.log(arguments);
//Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
console.log(Array.isArray(arguments));//false
console.log(a);//1
console.log(arguments[0]);//1
}
foo(1,2,3);
以這個例子 a 是 foo 的一個函數,而我們透過 foo(1,2,3)
傳入了三個參數,這樣並不會引發語法錯誤。
實際上,所有的函式 -- 除了箭頭函式以外都有 argument 這個類陣列的物件(不是陣列,所以要用陣列上的方法得用 Array.from()
來做轉型),可以透過數字索引來訪問各個值,注意,即使你有定義參數去接傳入,依然從第一個參數來算 index 0
。
如上面 a
參數接下了 1
,但你要從 argument
中找到 1
,那你需要訪問的索引值依然是 0
而非 1
。
另外,嚴格模式下,後續對參數 a
的操作並不會反映到 argument[0]
上,儘管非嚴格模式下會,但可能會造成混淆,盡量不要混用。
箭頭函式無法使用的原因跟他的特殊詞法作用域(雖然我們是第一次提到這個詞,但在有優良寫法的情況下,大部分情況都會與作用域具相等的表現,優良寫法指不使用 eval 之類的函數來改變執行時的行為)有關,箭頭函式的 this
如上所說會綁定至定義時的外部作用域,argument
也是同理,使用的話也會拿到外部的作用域參數而非箭頭函式自身的參數。這樣的實現避免了過度混淆 this
的指向。
而 ES 6 推出了新的語法 ...args
(rest parameter),更好的處理了未被接到的傳入參數的行為。
function foo(a,...args){
console.log(args);//[2,3]
console.log(Array.isArray(args));//true
console.log(a);//1
}
foo(1,2,3);
使用方式是函式的最後一個變數命名以 ...
開頭,則他會被視為一個陣列。
如果有寫過其他語言的人可能會對這樣的功能很熟悉,在函式定義的時候先給定各個參數預設值,若函式被執行的時候沒有接到對應的參數,則採用預設值。
function foo(a = 1,b = 2,c = 3, d = c){
console.log(a,b,c,d);
}
foo(4,5,6);//4,5,6,6
foo();//1,2,3,3
預設值也能用其他參數來做為預設值,但參數的定義初始化是從左到右,且這個 ()
內是獨有的一個作用域,無法引用被定義在其他作用域的值,即使是 function
內部定義的 var
也不行。
另外,默認參數變數的初始化是在呼叫時被初始化,而不是宣告時,借用 MDN 的例子:
function append(value, array = []) {
array.push(value);
return array;
}
append(1); // [1]
append(2); // [2], not [1, 2]
第一行的 append
和第二行的 append
並無關聯,他們的 array
都在被呼叫時被初始化為 []
。
我們區分一下,當我說回傳表示回傳的具體行為,當我要指涉的是return
關鍵字,我會使用return
來做代表。
return
關鍵字主要作用於函式作用域中,他的目的是給予函式一個回傳值,當有人以函式調用的結果作為值的來源的時候,便會寫入這個回傳值。
回傳的同時也會終止目前函式的執行,後續部分不會再被執行。在 IDE 中,如果一個無條件的 return
語句後面還有語句,往往能看到被那些語句倍反灰表示不會被碰到。
function foo(returnTrue){
console.log('first');
if(returnTrue) return true;
console.log('second');
}
let a = foo(true);//"first"
console.log(a);//true
let b = foo();//"first" "second"
console.log(b);//"undefined"
如果沒有給予 return
,但使用該函式的調用結果來賦值,則會拿到 undefined
。
在同一個函式作用域下,return
沒有被使用的次數限制,只是一旦任何一個 return
被觸及,則會立刻終止函式。
如果是單行的箭頭函式,也可以這樣寫來省略 return
關鍵字:
let foo = (a,b)=>a+b;
console.log(foo(1,2));
重載指的是用同一函式名稱,依據傳入的參數不同,執行對應的實作。
先說結論,JS 中並不支持函式的重載。
function foo(a,b){
return a + b;
}
function foo(a, b, c){
return a - b - c;
}
console.log(foo(1,2));//NaN
在 JS 中,函式也是物件的一種,同名物件被重複宣告會發生什麼事?後宣告者會覆蓋前面的宣告,自然就無法處理「重載」。
如上面例子,實際上目前的 foo
一詞僅指 function foo(a,b,c)
函式,function foo(a,b)
已經被覆蓋掉了。
在 JS 中,若要實現重載,可以使用文中提到的 argument
或 ...arg
關鍵字,透過人為判定傳入的參數來手動執行不同的邏輯。