關于 JS 函數的一切
本文基于: Bilibili - 自由的加百利
前置條件:
- 需掌握函數的編寫、傳參、返回、調用
- 理解作用域、掌握定時器的用法
- 知道引用類型和基本數據類型的區別
- 知道函數也是引用類型
- 聽說過同步異步的概念
- 了解類和對象的關系
匿名函數
來看一下一個函數的基本屬性:

匿名函數的自運行
我們可以將一個普通函數去掉它的名字,這樣就成功的創建了一個匿名函數,并且編譯器不會報錯。

那么這個函數既然沒有名字,我們又該怎么調用它呢?這時只需要使用一個小括號包裹住整個函數,再在函數體的末尾添加一個小括號就可以在創建函數之后立即執行這個函數。

這種寫法,也叫作 匿名函數的自運行
其與直接在外部書寫函數體內部的語句相比,優點就是不會造成變量污染,會在匿名函數內形成一個 封閉的作用域
小括號的作用
在匿名函數的外部加上一個小括號,實際的作用是 將該函數的聲明變成了一個優先計算的表達式
( function(){...} )()
而表達式的運算結果就是這個 匿名函數 本身。拿到了函數本身之后,就可以在其后面加上一個小括號來調用它了。
把函數變成表達式?
既然小括號的作用是將函數的聲明變成表達式,那么在函數周圍加上運算符會不會有同樣的效果呢?
+function(){...}()
!function(){...}()
~function(){...}()
void function(){...}()
delete function(){...}()
以上的幾種寫法都可以成功執行匿名函數,而且使用 +function(){...}() 這種方式執行函數自運行的效率是最高的。
遞歸函數
遞歸函數 是指一個函數直接或間接的調用自身,并在特定的情況下結束并放回運行結果
這里我們舉一個 階乘 的例子:
function F(N) {
return N * F(N - 1);
}
表面看上去,這個函數可以接收一個參數,并計算出這個數的階乘。但是仔細想想就會發現不對勁,當 N = 1 時函數并沒有停止自身的繼續傳遞,也就是說這個函數沒有停止條件,最終便會陷入一個死循環。結果就是 會在某一時刻,大量的函數將內存空間占滿導致內存溢出。
也就是說我們上面寫的這個函數,只有 遞 沒有 歸
改造遞歸
我們嘗試改變一下上面的 遞歸函數
首先要弄清楚,我們需要計算的是一個數 它的階乘是多少。計算一個數字的階乘便是讓這個數每次乘以比他自身小 1 的數,直到乘到1。(說得不是很清楚,大家自行理解)
那么關鍵點就在于這個 直到
我們不能讓它無止境的傳遞下去,在上面的例子中,參與遞歸的 N 為 1 時還在繼續向內傳遞,0, -1, -2, -3... 我們所要做的就是當函數傳遞到 N = 1 時停止向內傳遞,直接返回 1 自身,將其自己交給外部的函數來調用,代碼更改如下:
function F(N) {
if (N == 1) return 1;
return N * F(N - 1);
}
上面 if 語句的作用是:當 N 為 1 時,直接返回 1
這時運行一下就會發現,函數不報錯了,而且也得到了我們想要的結果。
回調函數
回調函數,并不是指一種特殊的函數,而是指函數的使用方式
看一下下面的代碼:
function f1(){
console.log(111);
}
function f2(){
console.log(222);
}
f1();
f2();
輸出結果的順序自然是先輸出 111,再輸出 222
但是如果我們給 f1() 添加一個定時器呢?
function f1(){
setTimeout(function(){
console.log(111);
}, 1000)
}
function f2(){
console.log(222);
}
f1();
f2();
這時便會先輸出 222,一秒后輸出 111。這種含有異步操作的函數就被稱為 異步函數 ,異步函數最大的特點就是 后續的代碼不需要排隊,異步函數時可以和后續的代碼并行的。f1() 就是一個典型的異步函數,你無法知道 f1() 和 f2() 哪一個會先結束。
回調函數引出
那么在有異步函數的情況下,如果我希望先輸出 111,再輸出222,要怎么做呢?
目前看來,唯一的辦法是 把函數 f2() 放在 f1() 的內部調用
function f1(){
setTimeout(function(){
console.log(111);
f2();
}, 1000)
}
function f2(){
console.log(222);
}
f1();
假設有這樣一個場景,項目組里有小白、小黃、小綠三個人,有一個工具函數 getToken()
function getToken(){
//異步函數......
}
它是一個異步函數,大家都在使用這個函數完成自己的業務,并且每個人都希望在 getToken() 結束后執行自己的代碼,于是它們將函數寫成了下面這樣:

但是這種寫法顯然是錯誤的,因為異步函數保證不了函數的執行順序。那么現在只能想辦法將自己所寫的函數放在異步函數內部,才有機會在其后面執行。
首先,我們給 getToken() 函數增加一個參數 callback
function getToken(callback){
//異步函數......
}
之后,三個人的代碼就可以改成這樣:

把自己的函數傳進去,最后在 getToken() 的最后調用這個 callback
function getToken(callback){
//異步函數......
callback();
}
現在,所有人的代碼都會在異步函數最后執行,這極大的提高了代碼的可復用性,降低了開發維護的成本。
這種函數調用的方式就叫回調
字面意思就是:把自己的函數交給別人,回頭再調。
構造函數
- 這一節需要理解 什么是面向對象
一個函數除了可以被當作函數,還可以被當作
class
function fn(){
}
let obj = new fn();
console.log( typeof obj );
我們可以直接使用 new 關鍵字來聲明一個對象,這個時候,我們就說 fn() 是一個構造函數
那么 fn() 明明是一個空函數,這個對象是怎么來的呢?
構造函數的執行流程
問題的關鍵就在于這個 new 關鍵字。當你調用函數時在前面加上了 new 關鍵字,瀏覽器就會啟動 構造函數 的執行流程:
function fn(){
this = {}
// 創建一個空對象,將其保存在this關鍵字中
...... //your code
return this;
}
let obj = new fn();
當然了,上面部分代碼是不可見的。一個函數到底是普通函數還是構造函數,取決于你來怎么使用它。
但是通常,按照習慣,我們會將構造函數的首字母大寫,普通函數的首字母小寫。也就是說,如果你看到一個函數的首字母是大寫的,在絕大多數的時候,它不應該被直接調用。
function User() {
......
}
let user = User(); ×
let user = new User(); √
在最新版的 JavaScript 已經支持了 class 關鍵字,你可以像 Java 一樣定義一個類,并通過構造方法來生成對象。

閉包函數
function a(){
let x = 1;
function b(){
console.log(x);
}
}
函數 b() 是一個定義在函數 a() 內部的函數,所以其可以訪問到變量 x ,變量 x 相對于函數 b() 來說就是一個全局變量。
如果我們把函數 b() 作為函數 a() 的返回值:
function a(){
let x = 1;
return function b(){
console.log(x);
}
}
let c = a();
c();
我們已知,函數 c() 就是函數 b() ,有由于函數 c() 是全局變量,因此,相當于在全局范圍調用了函數 b() ,打破了函數 b() 只能在局部使用的限制,最終我們打印出了變量 x

在這里,函數 a() 所形成的作用域,叫做 閉包,函數 b() 被稱作 閉包函數
函數的柯里化
這一節來源于知乎:https://zhuanlan.zhihu.com/p/163838720#:~:text=函數柯里化,就是,后,才執行原函數
function add(a, b) {
return a + b
}
function curry(fn) {
return function (a) {
return function (b) {
return fn(a, b)
}
}
}
let fn = curry(add)(1)(2)

浙公網安備 33010602011771號