你將知道:
- 函數聲明和表達式
- 函數聲明和表達式之間的區別
- 什么是匿名函數
- 什么是 IIFE
- 命名函數表達式
this
關鍵字
函數是調用該函數時執行的代碼塊 。
函數聲明和表達式
讓我們回顧一下它的語法:
functionfunctionName(param1, param2, ..., paramN) {// Function's body
}
那么什么是函數聲明?
函數聲明是指作為獨立語句而不是表達式存在的函數定義。
// Standalone statement,
// hence a function declaration.
function sum(a, b) {return a + b;
}// Another function declaration.
function outerFn() {// A function declaration within outerFn().function innerFn() {console.log('Working with functions.')}}
這里 sum()
、 outerFn()
和 innerFn()
都是函數聲明,因為它們是獨立的語句:
在 JavaScript 中,還有另一種創建函數的方式,盡管其語法與函數聲明相同,但工作方式略有不同。我們稱之為函數表達式 。
函數聲明與函數表達式最重要的區別在于,后者可以定義一個沒有名稱的函數,這樣的函數通常被稱為匿名函數 。無論如何,假設函數被分配給一個變量,那么函數表達式在語法上看起來是這樣的:
var identifierName = function [functionName](param1, param2, ..., paramN) {// Function's body
}
請注意,使用函數表達式時,可以選擇在 function
關鍵字后包含函數名稱(即 functionName
),如上面的語法所示( functionName
周圍有 [ ]
)。
讓我們創建相同的函數 sum()
,這次使用函數表達式:
var sum = function(a, b) {return a + b;
}
這里發生的事情是,一個沒有名稱的函數被賦值給了變量 sum
。由于 sum
指向內存中的一個函數對象,所以我們可以使用表達式 sum()
。
下面我們將一個函數表達式作為參數傳遞給另一個函數:
function execute(fn) {fn();
}execute(function() { console.log('Hello World!'); })
這是 JavaScript 應用程序中常見的另一種編碼實踐。將一個函數作為參數傳遞給另一個函數,我們稱之為回調函數 ,或者簡稱為回調 。
function getLogger() {return function(val) {console.log('We are learning ' + val + '.');}
}var langIntro = getLogger();langIntro('JavaScript');
回調在 JavaScript 的事件 API 以及大量其他 API 中扮演著至關重要的角色。
在第 7 行,當調用 getLogger()
時,執行會轉到第 1 行中它的定義處,并退出并返回另一個函數的返回值。這個 “另一個” 函數接受一個參數 val
,并創建一個包含該參數和其他文本的日志。
由于 getLogger()
返回一個函數,因此 langIntro
中保存的值是對函數的引用——具體來說,是對第 2 行定義的匿名函數的引用。這意味著我們可以使用表達式 langIntro ()
來調用此函數。這正是我們在最后一行所做的。
從其他函數返回的函數構成了 JavaScript 中的另一個重要概念,稱為閉包 ,盡管這個概念不僅存在于從其他函數返回的函數中;它也存在于其他地方。
從核心執行層面來看,函數聲明和函數表達式之間沒有任何區別。調用時,兩者的執行方式相同。區別僅在于腳本編譯時解析它們的方式。
函數聲明和表達式之間的區別
函數聲明和函數表達式之間主要有兩個區別。
- 其中一個被 提升,而另一個則沒有。
- 一個可以創建匿名函數,而另一個則不能。
1. Hoisting 提升
提升是指在執行任何其他代碼之前處理各自范圍內的所有變量聲明的行為。
變量提升是以下代碼有效并且不會引發錯誤的原因:
// We think that x doesn't exist at this point, likewise
// the following statement would throw an error.
// However, that's not the case.
console.log(x);var x = 10;
console.log(x);
內部實現是這樣的:引擎首先在代碼中搜索所有變量聲明語句,并在運行任何其他代碼之前執行它們。完成后,它會從頭開始解析代碼。
這意味著在實際執行上述第 4 行之前,變量 x
已被有效聲明,因此可以訪問。由于 var x
將 x
初始化為 undefined
,因此在第 6 行賦值語句之前訪問 x
會返回 undefined
。
對于函數聲明,也會發生類似的情況。我們稱之為函數提升 (function hoisting) 。
整個聲明連同函數主體都被置于其范圍的最頂部。這意味著下面的代碼將完全正常工作:
console.log(sum(10, 20));function sum(a, b) {return a + b
}
在第 3 行中,函數 sum()
在實際定義之前就被訪問了。然而,由于函數提升,這次調用并沒有標記任何錯誤。
塊語句內的函數聲明
我們知道,所有函數聲明都會被提升到其作用域的頂部。然而,這個想法有一個小例外,事實上,不同瀏覽器之間存在不兼容性。
也就是說,如果函數聲明出現在塊語句內,那不是另一個函數的主體(也是一個塊語句),則函數聲明在大多數瀏覽器中不會正常提升 - 只有其名稱可以通過值 undefined
訪問。
這種行為有一個很好的理由,從下面的代碼中可以看出:
var userIsNew = false;if (userIsNew) {function greet() {console.log('Hello!');}
}
函數 greet()
被放在 if
語句塊內。該語句塊執行的條件是 userIsNew
為 true
。然而,事實顯然并非如此。因此,理想情況下,這段代碼的作者希望該函數不會出現在全局作用域中,因為它的出現條件尚未滿足。同樣,在這種情況下,大多數瀏覽器不會提升函數聲明,但函數名稱在相應的范圍內可用作 undefined
標識符。
這里要認識到的最重要的一點是,函數提升僅限于函數聲明 — — 而不是函數表達式 。
引擎在第一次遍歷一段代碼時僅查找函數聲明,而不是表達式,并將它們提升。
console.log(sum(10, 20));var sum = function(a, b) {return a + b
}//Uncaught TypeError: sum is not a function
我們有一個變量聲明語句 var sum
,同樣,它在運行任何其他代碼之前會被提升。接下來,從腳本開頭的第 1 行開始執行。此時, sum
等于 undefined
,因此無法調用;同樣, sum()
會拋出錯誤。
可能會認為可以通過在表達式中為函數添加名稱來緩解此問題。然而,這也行不通:
console.log(sum(10, 20));var sumRef = function sum(a, b) {return a + b
}
//Uncaught TypeError: sum is not a function
引擎只會在給定代碼片段中搜索函數聲明, 并最終將其提升。函數表達式,無論是命名的還是匿名的,都無關緊要。它們最終還是表達式,同樣會在提升階段被忽略。
2. 匿名函數
沒有名稱的函數稱為匿名函數 。
當 JavaScript 遇到函數聲明時,它期望也看到函數的名稱。否則會導致語法錯誤。
只有在函數表達式中才可以省略函數名稱,如下所示:
var sum = function(a, b) {return a + b;
}
匿名函數只能使用函數表達式來表示,是 JavaScript 中的一個重要特性。幾乎所有涉及事件和回調的程序都會用到它們.我們將廣泛使用匿名函數。
IIFE
在 JavaScript 中,有一種特殊情況,即函數表達式在定義后立即被調用。我們稱之為 IIFE ,即立即調用函數表達式 。
IIFE 是一個在定義后立即調用的函數表達式。
(function() {console.log('I am in an IIFE.')
})();
IIFE 用非常簡潔的語法解決了這個問題。它們將代碼封裝在本地環境中,不會干擾全局作用域。
最棒的是,這個過程非常快捷——我們無需命名這些函數,也無需擔心如何將它們賦值給任何標識符。只需創建一個匿名(或命名)函數,用一對括號將其封裝起來,將代碼放入其中,然后立即調用即可。 瞧!
每個庫的代碼都封裝在 IIFE 中,并且彼此之間完全互不干擾。 簡直太棒了。
this
關鍵字
JavaScript 中有很多令人困惑的概念,其中之一就是 this
的概念。本節我們將深入介紹并解釋 this 的核心。
首先, this
是 JavaScript 中的保留關鍵字,這意味著它不能用作變量的名稱。
回到 this
背后的想法,它是為了提供一種引用調用函數的對象的方式。這就是它被創建的初衷。事實上,編程語言 Java 無疑是 JavaScript 的影響源泉,它也包含 this
功能,本質上服務于非常相似的用途。
當在函數內部使用時, this
指的是調用該函數的對象 。
例如,考慮下面的對象 o
。它有一個屬性 x
和一個方法 f()
:
var o = {x: 10,f: function() { console.log(this.x); }
};var x = 20;
o.f();
一旦執行此代碼,我們就會在控制臺中記錄值 10
。
怎么樣? 我們來看看吧。
在第 7 行中,調用表達式 of()
在對象 o
上調用存儲在 of
中的函數,同樣,它的 this
也指向 o
。這僅僅意味著函數對象內部的表達式 this.x
轉換為 o.x
,等于值 10
,因此控制臺中打印的是 10
。
這是因為 this
會自動指向調用對象——我們不需要自己將其配置為所需的對象。相反,如果我們使用 ox
(而不是 this.x
),當 o
的名稱將來發生變化或被分配給其他標識符時,我們必須重寫這個表達式。理想情況下,只要有可能,就使用 this
關鍵字引用方法定義中的容器對象 - 而不是使用對象本身的名稱。
當直接調用函數對象(而不是作為對象的一部分)時,如果腳本在非嚴格模式下運行,則其 this
值將解析為全局 window
對象。
// The global context.
console.log(this);
Window {window: Window, self: Window, document: document, name: "", location: Location, …}