1、函數宣告與函數表達式
在ECMAScript中,創建函數的最常用的兩個方法是函數表達式和函數聲明,兩者期間的區別是有點暈,因為ECMA規範只明確了一點:函數聲明必須帶有標示符(Identifier )(就是大家常說的函數名稱),而函數表達式可以省略這個標示符:
函數宣告:function 函數名稱 (參數:可選){ 函數體 }
函數表達式:function 函數名稱(可選)(參數:可選){ 函數體 }
所以,可以看出,如果不宣告函數名稱,它肯定是表達式,可如果聲明了函數名稱的話,如何判斷是函數宣告還是函數表達式呢? ECMAScript是透過上下文來區分的,如果function foo(){}是作為賦值表達式的一部分的話,那它就是一個函數表達式,如果function foo(){}被包含在一個函數體內,或者位於程式的最頂端的話,那它就是一個函數宣告。
function foo(){} // 声明,因为它是程序的一部分 var bar = function foo(){}; // 表达式,因为它是赋值表达式的一部分 new function bar(){}; // 表达式,因为它是new表达式 (function(){ function bar(){} // 声明,因为它是函数体的一部分 })();
表達式和宣告存在著十分微妙的差別,首先,函數宣告會在任何表達式被解析和求值之前先被解析和求值,即使你的聲明在程式碼的最後一行,它也會在同作用域內第一個表達式之前被解析/求值,參考如下例子,函數fn是在alert之後聲明的,但是在alert執行的時候,fn已經有定義了:
alert(fn()); function fn() { return 'Hello world!'; }
另外,還有一點需要提醒一下,函數宣告在條件語句內雖然可以用,但是沒有被標準化,也就是說不同的環境可能有不同的執行結果,所以這樣情況下,最好使用函數表達式: 因為條件語句中沒有區塊級作用域這個概念
// 千万别这样做! // 因为有的浏览器会返回first的这个function,而有的浏览器返回的却是第二个 if (true) { function foo() { return 'first'; } } else { function foo() { return 'second'; } } foo(); // 相反,这样情况,我们要用函数表达式 var foo; if (true) { foo = function() { return 'first'; }; } else { foo = function() { return 'second'; }; } foo();
函數宣告的實際規則如下:
函數宣告只能出現在程式或函數體內。從句法上講,它們 不能出現在Block(區塊)({ … })中,例如不能出現在 if、while 或 for 語句中。因為 Block(區塊) 中只能包含Statement語句, 而不能包含函數宣告這樣的來源元素。另一方面,仔細看規則也會發現,唯一可能會讓表達式出現在Block(區塊)中情形,就是讓它作為表達式語句的一部分。但是,規範明確規定了表達式語句不能以關鍵字function開頭。而這其實就是說,函數表達式同樣也不能出現在Statement語句或Block(區塊)中(因為Block(區塊)就是由Statement語句構成的)。
2、命名函數表達式
提到命名函數表達式,理所當然,就是它得有名字,前面的例子var bar = function foo(){};就是一個有效的命名函數表達式,但有一點要記住:這個名字只在新定義的函數作用域內有效,因為規範規定了標示符不能在外圍的作用域內有效:
var f = function foo(){ return typeof foo; // function --->foo是在内部作用域内有效 }; // foo在外部用于是不可见的 typeof foo; // "undefined" f(); // "function"
既然,這麼要求,那命名函數表達式到底有啥用啊?為啥要取名?
正如我們開頭所說:給它一個名字就是可以讓調試過程更方便,因為在調試的時候,如果在調用棧中的每個項目都有自己的名字來描述,那麼調試過程就太爽了,感受不一樣嘛。
tips:這裡提出一個小問題:在ES3中,命名函數表達式的作用域物件也繼承了 Object.prototype 的屬性。這意味著僅僅是為函數表達式命名也會將 Object.prototype 中的所有屬性引入作用域。結果可能會出人意料。
var constructor = function(){return null;} var f = function f(){ return construcor(); } f(); //{in ES3 环境}
程式看起來會產生 null, 但其實會產生一個新的物件。因為命名函數表達式在其作用域內繼承了 Object.prototype.constructor(即 Object 的建構子)。就像 with 語句一樣,這個作用域會因 Object.prototype 的動態改變而受到影響。幸運的是,ES5 修正了這個錯誤。
這種行為的一個合理的解決方案是創建一個與函數表達式同名的局部變數並賦值為 null。即使在沒有錯誤地提升函數表達式宣告的環境中,使用 var 重宣告變數能確保仍然會綁定變數 g。設定變數 g 為 null 能確保重複的函數可以被垃圾回收。
var f = function g(){ return 17; } var g =null;
3、调试器(调用栈)中的命名函数表达式
刚才说了,命名函数表达式的真正用处是调试,那到底怎么用呢?如果一个函数有名字,那调试器在调试的时候会将它的名字显示在调用的栈上。有些调试器(Firebug)有时候还会为你们函数取名并显示,让他们和那些应用该函数的便利具有相同的角色,可是通常情况下,这些调试器只安装简单的规则来取名,所以说没有太大价值,我们来看一个例子:不用命名函数表达式
function foo(){ return bar(); } function bar(){ return baz(); } function baz(){ debugger; } foo(); // 这里我们使用了3个带名字的函数声明 // 所以当调试器走到debugger语句的时候,Firebug的调用栈上看起来非常清晰明了 // 因为很明白地显示了名称 baz bar foo expr_test.html()
通过查看调用栈的信息,我们可以很明了地知道foo调用了bar, bar又调用了baz(而foo本身有在expr_test.html文档的全局作用域内被调用),不过,还有一个比较爽地方,就是刚才说的Firebug为匿名表达式取名的功能:
function foo(){ return bar(); } var bar = function(){ return baz(); } function baz(){ debugger; } foo(); // Call stack baz bar() //看到了么? foo expr_test.html()
然后,当函数表达式稍微复杂一些的时候,调试器就不那么聪明了,我们只能在调用栈中看到问号:
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 (?)() // 这里可是问号哦,显示为匿名函数(anonymous function) foo expr_test.html()
另外,当把函数赋值给多个变量的时候,也会出现令人郁闷的问题:
function foo(){ return baz(); } var bar = function(){ debugger; }; var baz = bar; bar = function() { alert('spoofed'); }; foo(); // Call stack: bar() foo expr_test.html()
这时候,调用栈显示的是foo调用了bar,但实际上并非如此,之所以有这种问题,是因为baz和另外一个包含alert(‘spoofed')的函数做了引用交换所导致的。
归根结底,只有给函数表达式取个名字,才是最委托的办法,也就是使用命名函数表达式。我们来使用带名字的表达式来重写上面的例子(注意立即调用的表达式块里返回的2个函数的名字都是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(); // 又再次看到了清晰的调用栈信息了耶! baz bar foo expr_test.html()
好的,整个文章结束,大家对javascript的认识又近了一步,希望大家越来越喜欢小编为大家整理的文章,继续关注跟我学习javascript的一系列文章。