摘要:標識符有效性正是導致函數語句與函數表達式不同的關鍵所在下一小節我們將會展示命名函數表達式的具體行為。歸根結底,只有給函數表達式取個名字,才是最穩妥的辦法,也就是使用命名函數表達式。
前言
網上還沒用發現有人對命名函數表達式進去重復深入的討論,正因為如此,網上出現了各種各樣的誤解,本文將從原理和實踐兩個方面來探討JavaScript關于命名函數表達式的優缺點。
簡單的說,命名函數表達式只有一個用戶,那就是在==Debug==或者==Profiler==分析的時候來描述函數的名稱,也可以使用函數名實現遞歸,但很快你就會發現其實是不切實際的。當然,如果你不關注調試,那就沒什么可擔心的了,否則,如果你想了解兼容性方面的東西的話,你還是應該繼續往下看看。
我們先開始看看,什么叫函數表達式,然后再說一下現代調試器如何處理這些表達式,如果你已經對這方面很熟悉的話,請直接跳過此小節。
本文中后半部分說了好多JScript,基本上是過時的東西,我覺得直接略過就行
在ECMAScript中,創建函數的最常用的兩個方法是函數表達式和函數聲明,兩者期間的區別是有點暈,因為ECMAScript規范只明確了一點:函數聲明必須帶有標示符(==Identifier==)(就是大家常說的函數名稱),而函數表達式則可以省略這個標示符:
函數聲明:
function 函數名稱 (參數:可選){ 函數體 }
函數表達式:
function 函數名稱(可選)(參數:可選){ 函數體 }
所以,可以看出,如果不聲明函數名稱,它肯定是表達式,可如果聲明了函數名稱的話,如何判斷是函數聲明還是函數表達式呢?==ECMAScript==是通過上下文來區分的,如果function foo(){}是作為賦值表達式的一部分的話,那它就是一個函數表達式,如果function foo(){}被包含在一個函數體內,或者位于程序的最頂部的話,那它就是一個函數聲明。
function foo(){} // 聲明,因為它是程序的一部分 var bar = function foo(){}; // 表達式,因為它是賦值表達式的一部分 new function bar(){}; // 表達式,因為它是new表達式 (function(){ function bar(){} // 聲明,因為它是函數體的一部分 })();
還有一種函數表達式不太常見,就是被括號括住的(function foo(){}),他是表達式的原因是因為括號 ()是一個分組操作符,它的內部只能包含表達式,我們來看幾個例子:
function foo(){} // 函數聲明 (function foo(){}); // 函數表達式:包含在分組操作符內 try { (var x = 5); // 分組操作符,只能包含表達式而不能包含語句:這里的var就是語句 } catch(err) { // SyntaxError }
你可以會想到,在使用eval對JSON進行執行的時候,JSON字符串通常被包含在一個圓括號里:eval("(" + json + ")"),這樣做的原因就是因為分組操作符,也就是這對括號,會讓解析器強制將JSON的花括號解析成表達式而不是代碼塊。
try { { "x": 5 }; // "{" 和 "}" 做解析成代碼塊 } catch(err) { // SyntaxError } ({ "x": 5 }); // 分組操作符強制將"{" 和 "}"作為對象字面量來解析
表達式和聲明存在著十分微妙的差別,首先,函數聲明會在任何表達式被解析和求值之前先被解析和求值,即使你的聲明在代碼的最后一行,它也會在同作用域內第一個表達式之前被解析/求值,參考如下例子,函數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語句構成的)。
在ECMAScript的語法擴展中,有一個是函數語句,目前只有基于Gecko的瀏覽器實現了該擴展,所以對于下面的例子,我們僅是抱著學習的目的來看,一般來說不推薦使用(除非你針對Gecko瀏覽器進行開發)。
一般語句能用的地方,函數語句也能用,當然也包括Block塊中:
if (true) { function f(){ } }else { function f(){ } }
函數語句可以像其他語句一樣被解析,包含基于條件執行的情形
if (true) { function foo(){ return 1; } }else { function foo(){ return 2; } } foo(); // 1 // 注:其它客戶端會將foo解析成函數聲明 // 因此,第二個foo會覆蓋第一個,結果返回2,而不是1
函數語句不是在變量初始化期間聲明的,而是在運行時聲明的——與函數表達式一樣。不過,函數語句的標識符一旦聲明能在函數的整個作用域生效了。標識符有效性正是導致函數語句與函數表達式不同的關鍵所在(下一小節我們將會展示命名函數表達式的具體行為)。
// 此刻,foo還沒用聲明 typeof foo; // "undefined" if (true) { // 進入這里以后,foo就被聲明在整個作用域內了 function foo(){ return 1; } }else { // 從來不會走到這里,所以這里的foo也不會被聲明 function foo(){ return 2; } } typeof foo; // "function"
不過,我們可以使用下面這樣的符合標準的代碼來模式上面例子中的函數語句:
var foo; if (true) { foo = function foo(){ return 1; }; }else { foo = function foo() { return 2; }; }
函數語句和函數聲明(或命名函數表達式)的字符串表示類似,也包括標識符:
if (true) { function foo(){ return 1; } } String(foo); // function foo() { return 1; }
另外一個,早期基于Gecko的實現(Firefox 3及以前版本)中存在一個bug,即函數語句覆蓋函數聲明的方式不正確。在這些早期的實現中,函數語句不知何故不能覆蓋函數聲明:
// 函數聲明 function foo(){ return 1; } if (true) { // 用函數語句重寫 function foo(){ return 2; } } foo(); // FF3以下返回1,FF3.5以上返回2 // 不過,如果前面是函數表達式,則沒用問題 var foo = function(){ return 1; }; if (true) { function foo(){ return 2; } } foo(); // 所有版本都返回2
再次強調一點,上面這些例子只是在某些瀏覽器支持,所以推薦大家不要使用這些,除非你就在特性的瀏覽器上做開發。
命名函數表達式函數表達式在實際應用中還是很常見的,在web開發中友個常用的模式是基于對某種特性的測試來偽裝函數定義,從而達到性能優化的目的,但由于這種方式都是在同一作用域內,所以基本上一定要用函數表達式:
// 該代碼來自Garrett Smith的APE Javascript library庫(http://dhtmlkitchen.com/ape/) var contains = (function() { var docEl = document.documentElement; if (typeof docEl.compareDocumentPosition != "undefined") { return function(el, b) { return (el.compareDocumentPosition(b) & 16) !== 0; }; } else if (typeof docEl.contains != "undefined") { return function(el, b) { return el !== b && el.contains(b); }; } return function(el, b) { if (el === b) return false; while (el != b && (b = b.parentNode) != null); return el === b; }; })();
提到命名函數表達式,理所當然,就是它得有名字,前面的例子var bar = function foo(){};就是一個有效的命名函數表達式,但有一點需要記住:這個名字只在新定義的函數作用域內有效,因為規范規定了標示符不能在外圍的作用域內有效:
var f = function foo(){ return typeof foo; // foo是在內部作用域內有效 }; // foo在外部用于是不可見的 typeof foo; // "undefined" f(); // "function"
既然,這么要求,那命名函數表達式到底有啥用啊?為啥要取名?
正如我們開頭所說:給它一個名字就是可以讓調試過程更方便,因為在調試的時候,如果在調用棧中的每個項都有自己的名字來描述,那么調試過程就太爽了,感受不一樣嘛。
如果一個函數有名字,那調試器在調試的時候會將它的名字顯示在調用的棧上。有些調試器(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 (?)() // 這里可是問號哦 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()
OK,又學了一招吧?不過在高興之前,我們再看看不同尋常的JScript吧。
JScript這一部分講的全都是JScript而不是Javascript這兩個真不是一種東西
netscape開發了在Navigator中使用的LiveScript語言,后改名為JavaScript
Microsoft發行jscript用于internet explorer.
最初的jscript和javascript差異過大,web程序員不得不痛苦的為兩種瀏覽器編寫兩種腳本。于是誕生了ECMAScript,是一種國際標準化的javascript版本。現在的主流瀏覽器都支持這種版本。
javascript是一個通用的名稱,所有瀏覽器都認識,而jscript只有IE認識。
其他語言細節上的區別,不是一兩下能說完的。編程時最好遵循ECMAscript標準。這樣可以保證兼容性。
順便說一下,javascript原來叫Livescript,后來Sun的java風頭正盛的時候netscape就把名字改成javascript。
個人感覺這一段基本上可以忽略了 但為了尊重作者我還是把它整理了一下。
JScript的Bug比較惡的是,IE的ECMAScript實現JScript嚴重混淆了命名函數表達式,搞得現很多人都出來反對命名函數表達式,而且即便是最新的一版(IE8中使用的5.8版)仍然存在下列問題。
下面我們就來看看IE在實現中究竟犯了那些錯誤,俗話說知已知彼,才能百戰不殆。我們來看看如下幾個例子:
例1:函數表達式的標示符泄露到外部作用域
var f = function g(){}; typeof g; // "function"
上面我們說過,命名函數表達式的標示符在外部作用域是無效的,但JScript明顯是違反了這一規范,上面例子中的標示符g被解析成函數對象,這就亂了套了,很多難以發現的bug都是因為這個原因導致的。
==注:IE9貌似已經修復了這個問題==
例2:將命名函數表達式同時當作函數聲明和函數表達式
typeof g; // "function" var f = function g(){};
特性環境下,函數聲明會優先于任何表達式被解析,上面的例子展示的是JScript實際上是把命名函數表達式當成函數聲明了,因為它在實際聲明之前就解析了g。
這個例子引出了下一個例子。
例3:命名函數表達式會創建兩個截然不同的函數對象!
var f = function g(){}; f === g; // false f.expando = "foo"; g.expando; // undefined
看到這里,大家會覺得問題嚴重了,因為修改任何一個對象,另外一個沒有什么改變,這太惡了。通過這個例子可以發現,創建2個不同的對象,也就是說如果你想修改f的屬性中保存某個信息,然后想當然地通過引用相同對象的g的同名屬性來使用,那問題就大了,因為根本就不可能。
再來看一個稍微復雜的例子:
例4:僅僅順序解析函數聲明而忽略條件語句塊
var f = function g() { return 1; }; if (false) { f = function g(){ return 2; }; } g(); // 2
這個bug查找就難多了,但導致bug的原因卻非常簡單。首先,g被當作函數聲明解析,由于JScript中的函數聲明不受條件代碼塊約束,所以在這個很惡的if分支中,g被當作另一個函數function g(){ return 2 },也就是又被聲明了一次。然后,所有“常規的”表達式被求值,而此時f被賦予了另一個新創建的對象的引用。由于在對表達式求值的時候,永遠不會進入“這個可惡if分支,因此f就會繼續引用第一個函數function g(){ return 1 }。分析到這里,問題就很清楚了:假如你不夠細心,在f中調用了g,那么將會調用一個毫不相干的g函數對象。
你可能會文,將不同的對象和arguments.callee相比較時,有什么樣的區別呢?我們來看看:
var f = function g(){ return [ arguments.callee == f, arguments.callee == g ]; }; f(); // [true, false] g(); // [false, true]
可以看到,arguments.callee的引用一直是被調用的函數,實際上這也是好事,稍后會解釋。
還有一個有趣的例子,那就是在不包含聲明的賦值語句中使用命名函數表達式:
(function(){ f = function f(){}; })();
按照代碼的分析,我們原本是想創建一個全局屬性f(注意不要和一般的匿名函數混淆了,里面用的是帶名字的生命),JScript在這里搗亂了一把,首先他把表達式當成函數聲明解析了,所以左邊的f被聲明為局部變量了(和一般的匿名函數里的聲明一樣),然后在函數執行的時候,f已經是定義過的了,右邊的function f(){}則直接就賦值給局部變量f了,所以f根本就不是全局屬性。
了解了JScript這么{{BANNED}}以后,我們就要及時預防這些問題了,首先防范標識符泄漏帶外部作用域,其次,應該永遠不引用被用作函數名稱的標識符;還記得前面例子中那個討人厭的標識符g嗎?——如果我們能夠當g不存在,可以避免多少不必要的麻煩哪。因此,關鍵就在于始終要通過f或者arguments.callee來引用函數。如果你使用了命名函數表達式,那么應該只在調試的時候利用那個名字。最后,還要記住一點,一定要把命名函數表達式聲明期間錯誤創建的函數清理干凈。
對于,上面最后一點,我們還得再解釋一下。
WebKit的displayNameWebKit團隊在這個問題采取了有點兒另類的策略。介于匿名和命名函數如此之差的表現力,WebKit引入了一個“特殊的”displayName屬性(本質上是一個字符串),如果開發人員為函數的這個屬性賦值,則該屬性的值將在調試器或性能分析器中被顯示在函數“名稱”的位置上。Francisco Tolmasky詳細地解釋了這個策略的原理和實現。
在ECMAScript-262第5版引入了嚴格模式(strict mode)。開啟嚴格模式的實現會禁用語言中的那些不穩定、不可靠和不安全的特性。據說出于安全方面的考慮,arguments.callee屬性將在嚴格模式下被“封殺”。因此,在處于嚴格模式時,訪問arguments.callee會導致TypeError(參見ECMA-262第5版的10.6節)。而我之所以在此提到嚴格模式,是因為如果在基于第5版標準的實現中無法使用arguments.callee來執行遞歸操作,那么使用命名函數表達式的可能性就會大大增加。從這個意義上來說,理解命名函數表達式的語義及其bug也就顯得更加重要了。
// 此前,你可能會使用arguments.callee (function(x) { if (x <= 1) return 1; return x * arguments.callee(x - 1); })(10); // 但在嚴格模式下,有可能就要使用命名函數表達式 (function factorial(x) { if (x <= 1) return 1; return x * factorial(x - 1); })(10); // 要么就退一步,使用沒有那么靈活的函數聲明 function factorial(x) { if (x <= 1) return 1; return x * factorial(x - 1); } factorial(10);致謝
理查德· 康福德(Richard Cornford),是他率先解釋了JScript中命名函數表達式所存在的bug。理查德解釋了我在這篇文章中提及的大多數bug,所以我強烈建議大家去看看他的解釋。我還要感謝Yann-Erwan Perio和道格拉斯·克勞克佛德(Douglas Crockford),他們早在2003年就在comp.lang.javascript論壇中提及并討論NFE問題了。
約翰-戴維·道爾頓(John-David Dalton)對“最終解決方案”提出了很好的建議。
托比·蘭吉的點子被我用在了“替代方案”中。
蓋瑞特·史密斯(Garrett Smith)和德米特里·蘇斯尼科(Dmitry Soshnikov)對本文的多方面作出了補充和修正。
英文原文:http://kangax.github.com/nfe/
參考譯文:連接訪問 (SpiderMonkey的怪癖之后的章節參考該文)
關于本文本文轉自TOM大叔的深入理解JavaScript系列本文有大量刪減,查看原文
【深入理解JavaScript系列】文章,包括了原創,翻譯,轉載,整理等各類型文章,原文是TOM大叔的一個非常不錯的專題,現將其重新整理發布。謝謝大叔。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/78411.html
摘要:前言大家學的時候,經常遇到自執行匿名函數的代碼,今天我們主要就來想想說一下自執行。其實,前面兩個例子里的變量,也可以換成,因為和外面的不在一個作用于,所以不會出現問題,這也是匿名函數閉包的威力。 前言 大家學JavaScript的時候,經常遇到自執行匿名函數的代碼,今天我們主要就來想想說一下自執行。 在詳細了解這個之前,我們來談了解一下自執行這個叫法,本文對這個功能的叫法也不一定完全對...
摘要:關鍵詞必須是小寫的,并且必須以與函數名稱相同的大小寫來調用函數。當調用函數時,這些標識符則指代傳入函數的實參。函數表達式其實是忽略函數名稱的,并且不可以使用函數名這種形式調用函數。注意構造函數無法指定函數名稱,它創建的是一個匿名函數。 一、關于函數 JavaScript函數是指一個特定代碼塊,可能包含多條語句,可以通過名字來供其他語句調用以執行函數包含的代碼語句。 比如我們有一個特定的...
摘要:所有變量聲明由名稱和對應值組成一個變量對象的屬性被創建如果變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性。 介紹 JavaScript編程的時候總避免不了聲明函數和變量,以成功構建我們的系統,但是解釋器是如何并且在什么地方去查找這些函數和變量呢?我們引用這些對象的時候究竟發生了什么? 原始發布:Dmitry A. Soshnikov 發布時間:2009-...
摘要:編寫高質量的要點深入理解系列一知識點最小全局變量全局變量命名易與第三方的腳本引起沖突所以盡可能少的使用全局變量是很重要的相關策略有命名空間模式或是函數立即自動執行,但是要想讓全局變量少最重要的還是始終使用來聲明變量。 Title: 編寫高質量Javascript的要點-Review深入理解Javascript系列(一)date: 2017-6-9 14:14:20 status: p...
摘要:訪問全局對象在瀏覽器中,全局對象可以通過屬性在代碼的任何位置訪問除非你做了些比較出格的事情,像是聲明了一個名為的局部變量。 前言 才華橫溢的Stoyan Stefanov,在他寫的由O’Reilly初版的新書《JavaScript Patterns》(JavaScript模式)中,我想要是為我們的讀者貢獻其摘要,那會是件很美妙的事情。具體一點就是編寫高質量JavaScript的一些要素...
閱讀 2847·2021-09-10 10:51
閱讀 2215·2021-09-02 15:21
閱讀 3206·2019-08-30 15:44
閱讀 869·2019-08-29 18:34
閱讀 1652·2019-08-29 13:15
閱讀 3322·2019-08-26 11:37
閱讀 2697·2019-08-26 10:46
閱讀 1107·2019-08-26 10:26