深入理解JavaScript系列(2)

2018-06-09 15:52 更新

本文是深入理解JavaScript系列的第篇讀文筆記,博客原文在這里

內(nèi)容簡(jiǎn)要

本文闡述了JavaScript中關(guān)于函數(shù)的兩個(gè)非常讓人混淆的東西,分別是函數(shù)表達(dá)式函數(shù)聲明

原文可能由于寫作的時(shí)間(2011-12-29)相距現(xiàn)在比較久遠(yuǎn),在那之前JScript與JavaScript正在爭(zhēng)奪瀏覽器腳本語言的霸主地位,所以大叔的文章中還提到了一些JScript的內(nèi)容以及SpiderMonkey的東西(誰讓那時(shí)候V8引擎還沒出來呢)。

我的讀文筆記中將會(huì)舍棄這些“糟粕”,說它們是糟粕并沒有貶低的意思,只是相對(duì)現(xiàn)在來說,它們已經(jīng)不太合適拿出來說了,沒什么意義,都是歷史的產(chǎn)物,它們存在的唯一價(jià)值就是推動(dòng)了標(biāo)準(zhǔn)的發(fā)展和統(tǒng)一。我是標(biāo)準(zhǔn)的鑒定擁護(hù)者。(語言有點(diǎn)激進(jìn),不喜輕噴)

所以,本篇讀文筆記的目的就是闡述清楚函數(shù)表達(dá)式函數(shù)聲明這兩個(gè)概念的含義以及一些常規(guī)誤區(qū)。

BACKBONE

函數(shù)表達(dá)式和函數(shù)聲明

那么,函數(shù)表達(dá)式和函數(shù)聲明究竟是啥玩意呢?

在ECMAScript中,創(chuàng)建函數(shù)的最常用的兩個(gè)方法就是函數(shù)表達(dá)式和函數(shù)聲明(這里不討論new Function()這種形式)。這兩者之間的區(qū)別不是很嚴(yán)謹(jǐn),因?yàn)镋CMAScript中僅僅定義了:函數(shù)聲明必須帶有標(biāo)識(shí)符(Identifier,其實(shí)就是函數(shù)名),而函數(shù)表達(dá)式則可以省略這個(gè)標(biāo)識(shí)符。

所以我們得出下面的語法,

函數(shù)聲明語法,

function function_name(args1, arg2, arg3) {
    // function body
}

這里的function_name顯然是不能忽略的,就是說在函數(shù)聲明中,你必須給函數(shù)起一個(gè)名字,當(dāng)然函數(shù)參數(shù)及其個(gè)數(shù)都是可選的。

函數(shù)表達(dá)式語法,

function [function_name](args1, arg2, arg3) {
    // function body
}

這里[function_name]的意思是表示function_name是可以被忽略的。

由此可見,如果不聲明函數(shù)名稱,它肯定是表達(dá)式,可如果聲明了函數(shù)名稱的話,如何判斷是函數(shù)聲明還是函數(shù)表達(dá)式呢?

ECMAScript是通過上下文來區(qū)分的,如果function foo(){}是作為賦值表達(dá)式的一部分的話,那它就是一個(gè)函數(shù)表達(dá)式,如果function foo(){}被包含在一個(gè)函數(shù)體內(nèi),或者位于程序的最頂部的話,那它就是一個(gè)函數(shù)聲明。

我們下面來看個(gè)例子來理解一下上面的解釋,

function foo(){} // 函數(shù)聲明,因?yàn)樗浅绦虻囊徊糠?var bar = function foo(){}; // 函數(shù)表達(dá)式,因?yàn)樗琴x值表達(dá)式的一部分
new function bar(){}; // 函數(shù)表達(dá)式,因?yàn)樗莕ew表達(dá)式
(function(){
    function bar(){} // 函數(shù)聲明,因?yàn)樗呛瘮?shù)體的一部分
})();

還有一種常見的表達(dá)式,大家可能見過的,如下

(function foo() {
    // foo body
})();

在后面的文章中我們將會(huì)專門對(duì)這種語法進(jìn)行說明。這就是在JavaScript中使用的比較廣泛的自執(zhí)行函數(shù),或者叫立即執(zhí)行函數(shù)。這里我們暫時(shí)不討論自執(zhí)行函數(shù),我們看這個(gè)函數(shù)由兩個(gè)()組成,第一個(gè)()中其實(shí)一個(gè)函數(shù),那么這里的foo是函數(shù)聲明還是函數(shù)表達(dá)式呢?

這里的foo函數(shù)是函數(shù)表達(dá)式。雖然這個(gè)函數(shù)有個(gè)名字foo,但是它是函數(shù)表達(dá)式。為什么呢?

因?yàn)檫@個(gè)foo函數(shù)是被()包裹起來的,而這個(gè)()在JavaScript中其實(shí)是一個(gè)操作符,叫做分組操作符,而且這個(gè)分組操作符內(nèi)部只能包含表達(dá)式。我們看幾個(gè)例子,

function foo(){} // 函數(shù)聲明
(function foo(){}); // 函數(shù)表達(dá)式:包含在分組操作符內(nèi)
try {
    (var x = 5); // 報(bào)錯(cuò),因?yàn)榉纸M操作符只能包含表達(dá)式而不能包含語句,這里的var就是語句
} catch(err) {
    // SyntaxError
}

你在chrome瀏覽器上可以快速體驗(yàn)下。打開F12,在console中輸入{'x': 10},然后回車chrome為告訴你

SyntaxError: Unexpected token :

如果我們這么輸入,({'x': 10}),回車之后你會(huì)得到一個(gè)Object。怎么樣,很神奇吧。

函數(shù)表達(dá)式和函數(shù)聲明的區(qū)別

函數(shù)表達(dá)式和函數(shù)聲明存在著十分微妙的差別。

函數(shù)聲明會(huì)在任何表達(dá)式被解析和求值之前先被解析和求值,即使你的聲明在代碼的最后一行,它也會(huì)在同作用域內(nèi)第一個(gè)表達(dá)式之前被解析或者求值。這其實(shí)跟深入理解JavaScript系列(1)-編寫高質(zhì)量JavaScript代碼的基本要點(diǎn)是很相似的。

來看個(gè)簡(jiǎn)單的例子,

alert(fn()); // I am fn!
alert(vfoo); // undefined
alert(foo()); // 報(bào)錯(cuò):foo is not defined
var vfoo = function foo() {
    return "I am foo!";
};
function fn() {
    return 'I am fn!';
}

這個(gè)例子中,我在代碼的注釋中給出了相應(yīng)的結(jié)果。不知道你有沒有答對(duì)呢。

這個(gè)例子雖然簡(jiǎn)單,但是有如下幾點(diǎn)需要說明,

  • 雖然fn函數(shù)是在alert之后聲明的,但是它是函數(shù)聲明,會(huì)優(yōu)先所有的表達(dá)式和語句解析和執(zhí)行,所以在alert語句執(zhí)行的時(shí)候fn函數(shù)其實(shí)已經(jīng)有定義了。所以執(zhí)行fn函數(shù)后順利的彈出了字符串。
  • 第二個(gè)彈出undefined是因?yàn)?code>vfoo變量是賦值之前就使用了,其值當(dāng)然是undefined
  • 第三個(gè)可能有人不太理解,代碼中明明聲明函數(shù)foo啊,咋還提示我foo沒有定義呢?

我這里解釋一下第三點(diǎn),JavaScript代碼是順序執(zhí)行的,也就是說,前面的代碼肯定是優(yōu)先后面的代碼執(zhí)行,有人會(huì)說,你這不是在打自己的臉么?之前還說JavaScript中會(huì)有預(yù)解析這種事情呢。其實(shí)預(yù)解析(pre-parse)和執(zhí)行時(shí)(runtime)是兩回事。一段JavaScript代碼在運(yùn)行的時(shí)候,是先經(jīng)歷預(yù)解析過程,這個(gè)過程JavaScript引擎會(huì)做一些變量和函數(shù)的命名,全局變量和局部變量語義表的構(gòu)建等等事情;然后就是正式的執(zhí)行過程。

JavaScript在運(yùn)行時(shí)是順序的。結(jié)合我們這個(gè)例子,JavaScript引擎在預(yù)解析階段,就會(huì)解析出來vfoo這個(gè)變量,此時(shí)這個(gè)vfoo變量就是聲明但是未賦值的狀態(tài),其值在JavaScript中就是undefined。而且這個(gè)function foo正是我們所說的函數(shù)表達(dá)式語法,他在預(yù)解析階段是不會(huì)做任何事情的。在正式的運(yùn)行時(shí)階段,JavaScript代碼是從上到下,一句一句的執(zhí)行的。很明顯這里的alert(foo())語句在下面的賦值表達(dá)式之前,在執(zhí)行alert時(shí),還沒有進(jìn)行賦值操作呢,而函數(shù)foo的定義其實(shí)是在賦值的時(shí)候才有定義的,所以這里會(huì)報(bào)錯(cuò),告訴你foo還沒有定義。

常見用法和誤區(qū)

ECMAScript中規(guī)定,函數(shù)表達(dá)式中函數(shù)的標(biāo)識(shí)符(就是函數(shù)的名字)是可以忽略的。那么,我到底是用函數(shù)聲明好呢,還是使用函數(shù)表達(dá)式好呢?如果使用了函數(shù)表達(dá)式,那我到底是省略函數(shù)名好呢,還是不省略函數(shù)名好呢?

使用函數(shù)聲明還是使用函數(shù)表達(dá)式是視情況而定的,至于函數(shù)表達(dá)式需不需要省略標(biāo)識(shí)符,一般的,如果你使用了函數(shù)表達(dá)式,我們是推薦你省略函數(shù)名的。但是在某些情況下,是推薦不省略函數(shù)名的。別暈,看我細(xì)細(xì)道來。

先來看一個(gè)例子,

if (true) {
    function foo() {
        return 'first';
    }
} else {
    function foo() {
        return 'second';
    }
}
foo();

這個(gè)foo執(zhí)行后到底會(huì)返回啥呢?有人說,肯定是first啊。其實(shí)是不一定的。函數(shù)聲明在條件語句內(nèi)雖然可以用,但是并沒有被標(biāo)準(zhǔn)化,也就是說不同的環(huán)境可能有不同的執(zhí)行結(jié)果。所以說我們將盡量避免這種情況。這種情況下,我們就應(yīng)該使用函數(shù)表達(dá)式了。如下,

var foo;
if (true) {
    foo = function() {
        return 'first';
    };
} else {
    foo = function() {
        return 'second';
    };
}
foo(); // first

我們?cè)賮砜匆粋€(gè)例子,

var f = function foo(){
    return typeof foo; // foo是在內(nèi)部作用域內(nèi)有效
};
// foo在外部用于是不可見的
typeof foo; // "undefined"
f(); // "function"

看到這個(gè)例子后,大家應(yīng)該很清楚了。函數(shù)表達(dá)式如果帶了函數(shù)名,那么這個(gè)函數(shù)名只在函數(shù)的內(nèi)部是可用的,在函數(shù)外部是未定義。這一點(diǎn)可以說是個(gè)小坑吧,注意下就行了。

另外一個(gè)值得一提是,給函數(shù)表達(dá)式添加函數(shù)名,可以在調(diào)試的時(shí)候?qū)σ娓佑押?,可以在調(diào)試堆棧上看到函數(shù)表達(dá)式的名字。這點(diǎn)大家知道就行,現(xiàn)代的JavaScript引擎都非常聰明,各種調(diào)試方法層出不窮,不必糾結(jié)這些黑暗技巧。

總結(jié)

這篇讀文筆記由于舍棄了一部分JScript的內(nèi)容,顯得比較單薄,不過沒關(guān)系,這篇筆記的主要目的就是為了闡述清楚函數(shù)聲明和函數(shù)表達(dá)式的愛恨情仇。我想我應(yīng)該把這兩者的方方面面都應(yīng)該說清楚了吧。而且函數(shù)在JavaScript中是第一等公民,其內(nèi)容遠(yuǎn)不止函數(shù)聲明和函數(shù)表達(dá)式這一點(diǎn)內(nèi)容,比如各種高階函數(shù),匿名函數(shù),多層閉包等等。要想成為一名合格的JavaScript程序員,那么JavaScript函數(shù)一定要信手拈來,這應(yīng)該是JavaScript中比較考驗(yàn)功力的一個(gè)內(nèi)容了吧。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)