本文基於 Bilibili - 自由的加百利 前置條件: 需掌握函數的編寫、傳參、返回、調用 理解作用域、掌握定時器的用法 知道引用類型和基本數據類型的區別 知道函數也是引用類型 聽說過同步非同步的概念 瞭解類和對象的關係 匿名函數 來看一下一個函數的基本屬性: 匿名函數的自運行 我們可以將一個普通函 ...
本文基於 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)