跟我學(xué)習(xí)javascript的函數(shù)和函數(shù)表達式
1、函數(shù)聲明與函數(shù)表達式
在ECMAScript中,創(chuàng)建函數(shù)的最常用的兩個方法是函數(shù)表達式和函數(shù)聲明,兩者期間的區(qū)別是有點暈,因為ECMA規(guī)范只明確了一點:函數(shù)聲明必須帶有標(biāo)示符(Identifier)(就是大家常說的函數(shù)名稱),而函數(shù)表達式則可以省略這個標(biāo)示符:
函數(shù)聲明:function 函數(shù)名稱 (參數(shù):可選){ 函數(shù)體 }
函數(shù)表達式:function 函數(shù)名稱(可選)(參數(shù):可選){ 函數(shù)體 }
所以,可以看出,如果不聲明函數(shù)名稱,它肯定是表達式,可如果聲明了函數(shù)名稱的話,如何判斷是函數(shù)聲明還是函數(shù)表達式呢?ECMAScript是通過上下文來區(qū)分的,如果function foo(){}是作為賦值表達式的一部分的話,那它就是一個函數(shù)表達式,如果function foo(){}被包含在一個函數(shù)體內(nèi),或者位于程序的最頂部的話,那它就是一個函數(shù)聲明。
function foo(){} // 聲明,因為它是程序的一部分 var bar = function foo(){}; // 表達式,因為它是賦值表達式的一部分 new function bar(){}; // 表達式,因為它是new表達式 (function(){ function bar(){} // 聲明,因為它是函數(shù)體的一部分 })();
表達式和聲明存在著十分微妙的差別,首先,函數(shù)聲明會在任何表達式被解析和求值之前先被解析和求值,即使你的聲明在代碼的最后一行,它也會在同作用域內(nèi)第一個表達式之前被解析/求值,參考如下例子,函數(shù)fn是在alert之后聲明的,但是在alert執(zhí)行的時候,fn已經(jīng)有定義了:
alert(fn()); function fn() { return 'Hello world!'; }
另外,還有一點需要提醒一下,函數(shù)聲明在條件語句內(nèi)雖然可以用,但是沒有被標(biāo)準(zhǔn)化,也就是說不同的環(huán)境可能有不同的執(zhí)行結(jié)果,所以這樣情況下,最好使用函數(shù)表達式: 因為在條件語句中沒有塊級作用域這個概念
// 千萬別這樣做! // 因為有的瀏覽器會返回first的這個function,而有的瀏覽器返回的卻是第二個 if (true) { function foo() { return 'first'; } } else { function foo() { return 'second'; } } foo(); // 相反,這樣情況,我們要用函數(shù)表達式 var foo; if (true) { foo = function() { return 'first'; }; } else { foo = function() { return 'second'; }; } foo();
函數(shù)聲明的實際規(guī)則如下:
函數(shù)聲明只能出現(xiàn)在程序或函數(shù)體內(nèi)。從句法上講,它們 不能出現(xiàn)在Block(塊)({ … })中,例如不能出現(xiàn)在 if、while 或 for 語句中。因為 Block(塊) 中只能包含Statement語句, 而不能包含函數(shù)聲明這樣的源元素。另一方面,仔細(xì)看一看規(guī)則也會發(fā)現(xiàn),唯一可能讓表達式出現(xiàn)在Block(塊)中情形,就是讓它作為表達式語句的一部分。但是,規(guī)范明確規(guī)定了表達式語句不能以關(guān)鍵字function開頭。而這實際上就是說,函數(shù)表達式同樣也不能出現(xiàn)在Statement語句或Block(塊)中(因為Block(塊)就是由Statement語句構(gòu)成的)。
2、命名函數(shù)表達式
提到命名函數(shù)表達式,理所當(dāng)然,就是它得有名字,前面的例子var bar = function foo(){};就是一個有效的命名函數(shù)表達式,但有一點需要記?。哼@個名字只在新定義的函數(shù)作用域內(nèi)有效,因為規(guī)范規(guī)定了標(biāo)示符不能在外圍的作用域內(nèi)有效:
var f = function foo(){ return typeof foo; // function --->foo是在內(nèi)部作用域內(nèi)有效 }; // foo在外部用于是不可見的 typeof foo; // "undefined" f(); // "function"
既然,這么要求,那命名函數(shù)表達式到底有啥用?。繛樯兑∶??
正如我們開頭所說:給它一個名字就是可以讓調(diào)試過程更方便,因為在調(diào)試的時候,如果在調(diào)用棧中的每個項都有自己的名字來描述,那么調(diào)試過程就太爽了,感受不一樣嘛。
tips:這里提出一個小問題:在ES3中,命名函數(shù)表達式的作用域?qū)ο笠怖^承了 Object.prototype 的屬性。這意味著僅僅是給函數(shù)表達式命名也會將 Object.prototype 中的所有屬性引入到作用域中。結(jié)果可能會出人意料。
var constructor = function(){return null;} var f = function f(){ return construcor(); } f(); //{in ES3 環(huán)境}
該程序看起來會產(chǎn)生 null, 但其實會產(chǎn)生一個新的對象。因為命名函數(shù)表達式在其作用域內(nèi)繼承了 Object.prototype.constructor(即 Object 的構(gòu)造函數(shù))。就像 with 語句一樣,這個作用域會因 Object.prototype 的動態(tài)改變而受到影響。幸運的是,ES5 修正了這個錯誤。
這種行為的一個合理的解決辦法是創(chuàng)建一個與函數(shù)表達式同名的局部變量并賦值為 null。即使在沒有錯誤地提升函數(shù)表達式聲明的環(huán)境中,使用 var 重聲明變量能確保仍然會綁定變量 g。設(shè)置變量 g 為 null 能確保重復(fù)的函數(shù)可以被垃圾回收。
var f = function g(){ return 17; } var g =null;
3、調(diào)試器(調(diào)用棧)中的命名函數(shù)表達式
剛才說了,命名函數(shù)表達式的真正用處是調(diào)試,那到底怎么用呢?如果一個函數(shù)有名字,那調(diào)試器在調(diào)試的時候會將它的名字顯示在調(diào)用的棧上。有些調(diào)試器(Firebug)有時候還會為你們函數(shù)取名并顯示,讓他們和那些應(yīng)用該函數(shù)的便利具有相同的角色,可是通常情況下,這些調(diào)試器只安裝簡單的規(guī)則來取名,所以說沒有太大價值,我們來看一個例子:不用命名函數(shù)表達式
function foo(){ return bar(); } function bar(){ return baz(); } function baz(){ debugger; } foo(); // 這里我們使用了3個帶名字的函數(shù)聲明 // 所以當(dāng)調(diào)試器走到debugger語句的時候,F(xiàn)irebug的調(diào)用棧上看起來非常清晰明了 // 因為很明白地顯示了名稱 baz bar foo expr_test.html()
通過查看調(diào)用棧的信息,我們可以很明了地知道foo調(diào)用了bar, bar又調(diào)用了baz(而foo本身有在expr_test.html文檔的全局作用域內(nèi)被調(diào)用),不過,還有一個比較爽地方,就是剛才說的Firebug為匿名表達式取名的功能:
function foo(){ return bar(); } var bar = function(){ return baz(); } function baz(){ debugger; } foo(); // Call stack baz bar() //看到了么? foo expr_test.html()
然后,當(dāng)函數(shù)表達式稍微復(fù)雜一些的時候,調(diào)試器就不那么聰明了,我們只能在調(diào)用棧中看到問號:
function foo(){ return bar(); } var bar = (function(){ if (window.addEventListener) { return function(){ return baz(); }; } else if (window.attachEvent) { return function() { return baz(); }; } })(); function baz(){ debugger; } foo(); // Call stack baz (?)() // 這里可是問號哦,顯示為匿名函數(shù)(anonymous function) foo expr_test.html()
另外,當(dāng)把函數(shù)賦值給多個變量的時候,也會出現(xiàn)令人郁悶的問題:
function foo(){ return baz(); } var bar = function(){ debugger; }; var baz = bar; bar = function() { alert('spoofed'); }; foo(); // Call stack: bar() foo expr_test.html()
這時候,調(diào)用棧顯示的是foo調(diào)用了bar,但實際上并非如此,之所以有這種問題,是因為baz和另外一個包含alert(‘spoofed')的函數(shù)做了引用交換所導(dǎo)致的。
歸根結(jié)底,只有給函數(shù)表達式取個名字,才是最委托的辦法,也就是使用命名函數(shù)表達式。我們來使用帶名字的表達式來重寫上面的例子(注意立即調(diào)用的表達式塊里返回的2個函數(shù)的名字都是bar):
function foo(){ return bar(); } var bar = (function(){ if (window.addEventListener) { return function bar(){ return baz(); }; } else if (window.attachEvent) { return function bar() { return baz(); }; } })(); function baz(){ debugger; } foo(); // 又再次看到了清晰的調(diào)用棧信息了耶! baz bar foo expr_test.html()
好的,整個文章結(jié)束,大家對javascript的認(rèn)識又近了一步,希望大家越來越喜歡小編為大家整理的文章,繼續(xù)關(guān)注跟我學(xué)習(xí)javascript的一系列文章。
相關(guān)文章
JavaScript中16進制顏色與rgb顏色互相轉(zhuǎn)換的示例代碼
這篇文章主要介紹了JavaScript中16進制顏色與rgb顏色互相轉(zhuǎn)換的示例代碼,通過示例代碼介紹了JS 顏色16進制、rgba相互轉(zhuǎn)換問題,感興趣的朋友一起看看吧2024-01-01Javascript中replace方法與正則表達式的結(jié)合使用教程
replace方法是javascript涉及到正則表達式中較為復(fù)雜的一個方法,嚴(yán)格上說應(yīng)該是string對象的方法,下面這篇文章主要給大家介紹了關(guān)于Javascript中replace方法與正則表達式的結(jié)合使用的相關(guān)資料,需要的朋友可以參考下2022-09-09JavaScript實現(xiàn)函數(shù)重載的代碼示例
在JavaScript中并沒有直接支持函數(shù)重載的機制,但是可以通過一些技巧來模擬函數(shù)重載的效果,比如使用參數(shù)判斷,使用默認(rèn)參數(shù),對象參數(shù),這些方法都可以實現(xiàn)類似函數(shù)重載的效果,所以本文就給大家介紹一下JavaScript如何實現(xiàn)函數(shù)重載,需要的朋友可以參考下2023-08-08原生js仿jq判斷當(dāng)前瀏覽器是否為ie,精確到ie6~8
這篇文章主要介紹了原生js仿jq判斷當(dāng)前瀏覽器是否為ie,精確到ie6~8,需要的朋友可以參考下2014-08-08js實現(xiàn)類似菜單風(fēng)格的TAB選項卡效果代碼
這篇文章主要介紹了js實現(xiàn)類似菜單風(fēng)格的TAB選項卡效果代碼,通過javascript鼠標(biāo)事件及頁面元素遍歷實現(xiàn)tab切換的功能,非常簡單實用,需要的朋友可以參考下2015-08-08Bootstrap導(dǎo)航條學(xué)習(xí)使用(二)
這篇文章主要為大家詳細(xì)介紹了Bootstrap導(dǎo)航條的使用方法第二篇,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-02-02