surprisingly, a topic of named function expressions doesn’t seem to be covered well enough on the web. this is probably why there are so many misconceptions floating around. in this article, i’ll try to summarize both - theoretical and practical aspects of these wonderful javascript constructs; the good, bad and ugly parts of them.
在互聯(lián)網(wǎng)上很難找到一篇對有名函數(shù)表達(dá)式講述的比較全面的文章。這也可能是為什么對此問題有那么多的誤解的原因。在這篇文章中,我將試圖從理論和實(shí)踐兩個(gè)方面總結(jié)這些精彩的javascript結(jié)構(gòu),他的精華、雞肋和糟粕。
in a nutshell, named function expressions are useful for one thing only - descriptive function names in debuggers and profilers. well, there is also a possibility of using function names for recursion, but you will soon see that this is often impractical nowadays. if you don’t care about debugging experience, you have nothing to worry about. otherwise, read on to see some of the cross-browser glitches you would have to deal with and tips on how work around them.
簡而言之,有名函數(shù)表達(dá)式只對一件工作有用,在調(diào)試器和性能測試器中描述函數(shù)的名稱。當(dāng)然還有可能在遞歸調(diào)用中使用函數(shù)名稱,但是你將很快看到在當(dāng)下他通常不大實(shí)用。如果你不擔(dān)心調(diào)試的問題,那么你就不用擔(dān)心了,甚至不去使用有名函數(shù)表達(dá)式。否則,繼續(xù)讀下去,你將看到你可能已經(jīng)遇到的跨瀏覽器問題和一些解決這些問題的方法。
i’ll start with a general explanation of what function expressions are how modern debuggers handle them. feel free to skip to a final solution, which explains how to use these constructs safely.
下面我們先來介紹一下什么是函數(shù)表達(dá)式,和現(xiàn)代的調(diào)試器如何處理他們呢。你也可以隨時(shí)跳轉(zhuǎn)到最后的解決方案,那里將說明如何安全的使用這些結(jié)構(gòu)
one of the two most common ways to create a function object in ecmascript is by means of either function expression or function declaration. the difference between two is rather confusing. at least it was to me. the only thing ecma specs make clear is that function declaration must always have an identifier (or a function name, if you prefer), and function expression may omit it:
在ecmascript中有兩種簡單的創(chuàng)建函數(shù)對象的方法,一種是函數(shù)表達(dá)式,一種是函數(shù)聲明。兩者的區(qū)別讓人比較困惑,至少對我是這樣。ecms規(guī)范中唯一澄清了的是函數(shù)聲明必須含有一個(gè)標(biāo)示符(如果你喜歡,可以稱他為函數(shù)名),但是函數(shù)表達(dá)式可以省略他。
functiondeclaration :
function identifier ( formalparameterlist opt ){ functionbody }functionexpression :
function identifier opt ( formalparameterlist opt ){ functionbody }
we can see that when identifier is omitted, that “something” can only be an expression. but what if identifier is present? how can one tell whether it is a function declaration or a function expression - they look identical after all? it appears that ecmascript differentiates between two based on a context. if a function foo(){} is part of, say, assignment expression, it is considered a function expression. if, on the other hand, function foo(){} is contained in a function body or in a (top level of) program itself - it is parsed as a function declaration.
我們可以看到,當(dāng)標(biāo)示符被省略的時(shí)候,這樣的結(jié)構(gòu)只能是一個(gè)函數(shù)表達(dá)式。但是當(dāng)標(biāo)示符出現(xiàn)的時(shí)候情況是怎樣的呢。他究竟是一個(gè)函數(shù)聲明還是一個(gè)函數(shù)表達(dá)式呢,他們看上去是完全一樣的。但是ecmascript是通過上下文來區(qū)分兩者的。如果function foo(){}是一個(gè)賦值表達(dá)式的一部分,他就被認(rèn)為是函數(shù)表達(dá)式。相反,如果function foo(){}包含在一個(gè)函數(shù)體中,或者在程序的最上層的代碼中,他就被解釋成為一個(gè)函數(shù)聲明。
function foo(){}; // 聲明,因?yàn)樽鳛樽钌蠈映绦虻囊徊糠謉eclaration, since it's part of a program
var bar = function foo(){}; // 表達(dá)式,因?yàn)槭菑?fù)制表達(dá)式的一部分expression, since it's part of an assignmentexpression
new function bar(){}; // 表達(dá)式,因?yàn)樗莕ew表達(dá)式的一部分。expression, since it's part of a newexpression
(function(){
function bar(){}; // 聲明,因?yàn)槭呛瘮?shù)圖的一部分declaration, since it's part of a functionbody
})();there’s a subtle difference in behavior of declarations and expressions.
聲明和表達(dá)式在作用上有這細(xì)微的差別。
first of all, function declarations are parsed and evaluated before any other expressions are. even if declaration is positioned last in a source, it will be evaluated foremost any other expressions contained in a scope. the following example demonstrates how fn function is already defined by the time alert is executed, even though it’s being declared right after it:
首先,函數(shù)聲明是在所有表達(dá)式之前被解析和求值。即使聲明被放在代碼的最后,他也會在包含它的作用域內(nèi)的所有表達(dá)式之前被求值。下面的例子將說明即使fn函數(shù)在alert執(zhí)行之后才被聲明,但是在alert被執(zhí)行的時(shí)候fn函數(shù)已經(jīng)被定義了。
alert(fn());
function fn() {
return 'hello world!';
}
another important trait of function declarations is that declaring them conditionally is non-standardized and varies across different environments. you should never rely on functions being declared conditionally and use function expressions instead.
函數(shù)聲明的另外一個(gè)重要的特點(diǎn)是,在條件語句中聲明他們的情況在ecma標(biāo)準(zhǔn)中是沒有說明的,并且在不同的環(huán)境中(也就是不同的瀏覽器)中是不同的。因此你不能依賴函數(shù)的條件聲明,而是應(yīng)該使用函數(shù)表達(dá)式來代替。
// 千萬不要這樣做never do this!
// 有些瀏覽器會聲返回“first”的foo函數(shù)some browsers will declare `foo` as the one returning 'first',
// 有些瀏覽器則會聲明返回“second”的foo函數(shù)while others - returning 'second'
if (true) {
function foo() {
return 'first';
}
}
else {
function foo() {
return 'second';
}
}
foo();
//作為替代你可以使用函數(shù)表達(dá)式 instead, use function expressions:
var foo;
if (true) {
foo = function() {
return 'first';
}
}
else {
foo = function() {
return 'second';
}
}
foo();
function expressions can actually be seen quite often. a common pattern in web development is to “fork” function definitions based on some kind of a feature test, allowing for the best performance. since such forking usually happens in the same scope, it is almost always necessary to use function expressions. after all, as we know by now, function declarations should not be executed conditionally:
函數(shù)表達(dá)式可能更常見一些。在web開發(fā)中的一個(gè)普通的模式就是根據(jù)一些某種功能測試的情況來fork函數(shù)定義,允許達(dá)到最好的性能。既然這個(gè)fork通常發(fā)生在同一個(gè)作用域內(nèi),所以他需要使用函數(shù)表達(dá)式。畢竟,如我們所知,函數(shù)定義不應(yīng)該被條件執(zhí)行
var contans = (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;
}
})();quite obviously, when a function expression has a name (technically - identifier), it is called a named function expression. what you’ve seen in the very first example - var bar = function foo(){}; - was exactly that - a named function expression with foo being a function name. an important detail to remember is that this name is only available in the scope of a newly-defined function; specs mandate that an identifier should not be available to an enclosing scope:
非常顯然那,當(dāng)一個(gè)函數(shù)表達(dá)式含有函數(shù)名(從技術(shù)上講就是一個(gè)標(biāo)示符),他就被稱作有名函數(shù)表達(dá)式。在你看到的第一個(gè)例子中 var bar=function foo(){}就是這樣,他是一個(gè)以foo作為函數(shù)名的有名函數(shù)表達(dá)式。最重要的一個(gè)細(xì)節(jié)就是這個(gè)函數(shù)名只有在新定義的函數(shù)的作用域內(nèi)才可訪問,ecma要求一個(gè)標(biāo)示符不應(yīng)該被一個(gè)封閉的作用域所訪問。
var f = function foo(){
return typeof foo; // “foo”在內(nèi)部作用域可以被訪問 "foo" is available in this inner scope
};
//‘foo’在外部不可見 `foo` is never visible "outside"
typeof foo; // "undefined"
f(); // "function"so what’s so special about these named function expressions? why would we want to give them names at all?
it appears that named functions make for a much more pleasant debugging experience. when debugging an application, having a call stack with descriptive items makes a huge difference.
因此,為什么這些有名函數(shù)表達(dá)式如此特別?為什么我們想要給他們名字呢。
可以看到有命名的函數(shù)在調(diào)試過程中可能更加方便一些。當(dāng)調(diào)試一個(gè)應(yīng)用程序的時(shí)候擁有一個(gè)含有描述項(xiàng)的調(diào)用棧會有很大的不同
|||
when a function has a corresponding identifier, debuggers show that identifier as a function name, when inspecting call stack. some debuggers (e.g. firebug) helpfully show names of even anonymous functions - making them identical to names of variables that functions are assigned to. unfortunately, these debuggers usually rely on simple parsing rules; such extraction is usually quite fragile and often produces false results.
當(dāng)一個(gè)函數(shù)有一個(gè)相應(yīng)的標(biāo)示符的時(shí)候,當(dāng)檢查調(diào)用堆棧的時(shí)候調(diào)試器把這個(gè)標(biāo)示符顯示為函數(shù)名。有些調(diào)試器(例如firebug)將顯示甚至是匿名函數(shù)的名稱,使他們的名字和函數(shù)被賦值給的變量的名字一致。不幸的是,這些調(diào)試器通常依賴于簡單的解釋規(guī)則;這些提取出來的名稱通常是非常不穩(wěn)定的,而且通常產(chǎn)生錯(cuò)誤的結(jié)果
let’s look at a simple example:
讓我們看一個(gè)簡單的例子
function foo(){
return bar();
}
function bar(){
return baz();
}
function baz(){
debugger;
}
foo();
// 這里我們用函數(shù)聲明來定義所有3個(gè)函數(shù)here, we used function declarations when defining all of 3 functions
// 當(dāng)調(diào)試器停止在`debugger`語句的時(shí)候when debugger stops at the `debugger` statement,
//調(diào)用堆棧看上去非常具有描述性的 the call stack (in firebug) looks quite descriptive:
baz
bar
foo
expr_test.html()we can see that foo called bar which in its turn called baz (and that foo itself was called from the global scope of expr_test.html document). what’s really nice, is that firebug manages to parse the “name” of a function even when an anonymous expression is used:
我們看以看到,foo調(diào)用bar,bar調(diào)用baz,foo本身是從expr_test.html的全局作用域中被調(diào)用的。firebug非常突出的一點(diǎn)是即是使用一個(gè)匿名的表達(dá)式,他都試圖去解釋函數(shù)名
function foo(){
return bar();
}
var bar = function(){
return baz();
}
function baz(){
debugger;
}
foo();
// 調(diào)用堆棧call stack
baz
bar()
foo
expr_test.html()what’s not very nice, though, is that if a function expression gets any more complex (which, in real life, it almost always is) all of the debugger’s efforts turn out to be pretty useless; we end up with a shiny question mark in place of a function name
但是調(diào)試器還不是很完美,當(dāng)函數(shù)表達(dá)式變得非常復(fù)雜的時(shí)候,這些調(diào)試器就很難去解釋這些函數(shù)名,通常是以一個(gè)問號來代替函數(shù)名
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()another confusion appears when a function is being assigned to more than one variable:
另外一個(gè)令人迷惑的事情是,當(dāng)一個(gè)函數(shù)被賦值給不止一個(gè)變量
function foo(){
return baz();
}
var bar = function(){
debugger;
};
var baz = bar;
bar = function() {
alert('spoofed');
}
foo();
// call stack:
bar()
foo
expr_test.html()you can see call stack showing that foo invoked bar. clearly, that’s not what has happened. the confusion is due to the fact that baz was “exchanged” references with another function - the one alerting “spoofed”. as you can see, such parsing - while great in simple cases - is often useless in any non-trivial script.
你可以看到調(diào)用棧顯示foo調(diào)用bar。顯然,剛才發(fā)生地不是這樣。引起這種困惑是因?yàn)閎az引用了另外一個(gè)函數(shù)---提示 “spoofed”的那個(gè)函數(shù)。就如你所看到的,這些解釋雖然在簡單的情況下非常有用,但是在復(fù)雜的腳本中卻基本上沒什么用。
what it all boils down to is the fact that named function expressions is the only way to get a truly robust stack inspection. let’s rewrite our previous example with named functions in mind. notice how both of the functions returning from self-executing wrapper, are named as bar:
歸根結(jié)底,有名函數(shù)表達(dá)式實(shí)際上是得到真正的調(diào)用棧檢查的唯一方法。讓我們用有名函數(shù)表達(dá)式來重寫我們前面的例子。請注意,兩個(gè)從自執(zhí)行包裹中返回的函數(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();
// and, once again, we have a descriptive call stack!
baz
bar
foo
expr_test.html()before we start dancing happily celebrating this holy grail finding, i’d like to bring a beloved jscript into the picture.
在我們開始跳舞慶祝找到這個(gè)方法之前,我們還是先來看看jscript中的情況是如何的
unfortunately, jscript (i.e. internet explorer’s ecmascript implementation) seriously messed up named function expressions. jscript is responsible for named function expressions being recommended against by many people these days.
不幸的是,jscript(也就是ecmascript的ie實(shí)現(xiàn))處理有名函數(shù)表達(dá)式卻非常糟糕。jscript應(yīng)對有名函數(shù)表達(dá)式不被很多人推薦負(fù)責(zé)
let’s look at what exactly is wrong with its broken implementation. understanding all of its issues will allow us to work around them safely. note that i broke these discrepancies into few examples - for clarity - even though all of them are most likely a consequence of one major bug.
讓我們來看看這個(gè)ecmascript的不好的實(shí)現(xiàn)中都有什么問題。理解所有這些問題將使我們可以安全的使用它。雖然下面的這些例子都很可能是一個(gè)主要的bug的結(jié)果,但是為了清晰起見,我把這些差異分為了幾個(gè)例子。
|||
實(shí)例1:函數(shù)表達(dá)式標(biāo)示符滲進(jìn)了外圍作用域
var f = function g(){};
typeof g; // "function"remember how i mentioned that an identifier of named function expression is not available in an enclosing scope? well, jscript doesn’t agree with specs on this one - g in the above example resolves to a function object. this is a most widely observed discrepancy. it’s dangerous in that it inadvertedly pollutes an enclosing scope - a scope that might as well be a global one - with an extra identifier. such pollution can, of course, be a source of hard-to-track bugs.
我剛才提到過一個(gè)有名函數(shù)表達(dá)式的標(biāo)示符不能在外部作用域中被訪問。但是,jscript在這點(diǎn)上和標(biāo)準(zhǔn)并不相符,在上面的餓例子中g(shù)卻是一個(gè)函數(shù)對象。這個(gè)是一個(gè)可以廣泛觀察到的差異。這樣它就用一個(gè)多余的標(biāo)示符污染了外圍作用域,這個(gè)作用域很有可能是全局作用域,這樣是很危險(xiǎn)的。當(dāng)然這個(gè)污染可能是一個(gè)很難去處理和跟蹤的bug的根源
實(shí)例2:有名函數(shù)表達(dá)式被進(jìn)行了雙重處理,函數(shù)表達(dá)式和函數(shù)聲明
typeof g; // "function"
var f = function g(){};
as i explained before, function declarations are parsed foremost any other expressions in a particular execution context. the above example demonstrates how jscript actually treats named function expressions as function declarations. you can see that it parses g before an “actual declaration” takes place.
正如我前面解釋的,在一個(gè)特定的執(zhí)行環(huán)境中,函數(shù)聲明是在所有的表達(dá)式之前被解釋。上面的例子說明jscript實(shí)際上把有名函數(shù)表達(dá)式作為一個(gè)函數(shù)聲明來對待。我們可以看到他在一個(gè)實(shí)際的聲明之前就被解釋了。
this brings us to a next example:
在此基礎(chǔ)上我們引入了下面的一個(gè)例子。
實(shí)例3:有名函數(shù)表達(dá)式創(chuàng)建兩個(gè)不同的函數(shù)對象。
var f = function g(){};
f === g; // false
f.expando = 'foo';
g.expando; // undefinedthis is where things are getting interesting. or rather - completely nuts. here we are seeing the dangers of having to deal with two distinct objects - augmenting one of them obviously does not modify the other one; this could be quite troublesome if you decided to employ, say, caching mechanism and store something in a property of f, then tried accessing it as a property of g, thinking that it is the same object you’re working with.
在這里事情變得更加有趣了,或者是完全瘋掉。這里我們看到必須處理兩個(gè)不同的對象的危險(xiǎn),當(dāng)擴(kuò)充他們當(dāng)中的一個(gè)的時(shí)候,另外一個(gè)不會相應(yīng)的改變。如果你打算使用cache機(jī)制并且在f的屬性中存放一些東西,只有有試圖在g的屬性中訪問,你本以為他們指向同一個(gè)對象,這樣就會變得非常麻煩
let’s look at something a bit more complex.
讓我們來看一些更復(fù)雜的例子。
實(shí)例4:函數(shù)聲明被順序的解釋,不受條件塊的影響
var f = function g() {
return 1;
};
if (false) {
f = function g(){
return 2;
}
};
g(); // 2an example like this could cause even harder to track bugs. what happens here is actually quite simple. first, g is being parsed as a function declaration, and since declarations in jscript are independent of conditional blocks, g is being declared as a function from the “dead” if branch - function g(){ return 2 }. then all of the “regular” expressions are being evaluated and f is being assigned another, newly created function object to. “dead” if branch is never entered when evaluating expressions, so f keeps referencing first function - function g(){ return 1 }. it should be clear by now, that if you’re not careful enough, and call g from within f, you’ll end up calling a completely unrelated g function object.
像這樣的一個(gè)例子可能會使跟蹤bug非常困難。這里發(fā)生的問題卻非常簡單。首先g被解釋為一個(gè)函數(shù)聲明,并且既然jscript中的聲明是和條件塊無關(guān)的,g就作為來自于已經(jīng)無效的if分支中的函數(shù)被聲明function g(){ return 2 }。之后普通的表達(dá)式被求值并且f被賦值為另外一個(gè)新創(chuàng)建的函數(shù)對象。當(dāng)執(zhí)行表達(dá)式的時(shí)候,由于if條件分支是不會被進(jìn)入的,因此f保持為第一函數(shù)的引用 function g(){ return 1 }。現(xiàn)在清楚了如果不是很小心,而且在f內(nèi)部調(diào)用g,你最終將調(diào)用一個(gè)完全無關(guān)的g函數(shù)對象。
you might be wondering how all this mess with different function objects compares to arguments.callee. does callee reference f or g? let’s take a look:
你可能在想不從的函數(shù)對象和arguments.callee相比較的結(jié)果會是怎樣呢?callee是引用f還是g?讓我們來看一下
var f = function g(){
return [
arguments.callee == f,
arguments.callee == g
];
};
f(); // [true, false]as you can see, arguments.callee references same object as f identifier. this is actually good news, as you will see later on.
我們可以看到arguments.callee引用的是和f標(biāo)示符一樣的對象,就像稍后你會看到的,這是個(gè)好消息
looking at jscript deficiencies, it becomes pretty clear what exactly we need to avoid. first, we need to be aware of a leaking identifier (so that it doesn’t pollute enclosing scope). second, we should never reference identifier used as a function name; a troublesome identifier is g from the previous examples. notice how many ambiguities could have been avoided if we were to forget about g’s existance. always referencing function via f or arguments.callee is the key here. if you use named expression, think of that name as something that’s only being used for debugging purposes. and finally, a bonus point is to always clean up an extraneous function created erroneously during nfe declaration.
既然看到了jscript的缺點(diǎn),我們應(yīng)該避免些什么就非常清楚了。首先,我們要意識到標(biāo)示符的滲出(以使得他不會污染外圍作用域)。第二點(diǎn),我們不應(yīng)該引用作為函數(shù)名的標(biāo)示符;從前面的例子可以看出g是一個(gè)問題多多的標(biāo)示符。請注意,如果我們忘記g的存在,很多歧義就可以被避免。通常最關(guān)鍵的就是通過f或者argument.callee來引用函數(shù)。如果你使用有名的表達(dá)式,記住名字只是為了調(diào)試的目的而存在。最后,額外的一點(diǎn)就是要經(jīng)常清理有名函數(shù)表達(dá)式聲明錯(cuò)誤創(chuàng)建的附加函數(shù)
i think last point needs a bit of an explanation:
我想最有一點(diǎn)需要一些更多解釋
being familiar with jscript discrepancies, we can now see a potential problem with memory consumption when using these buggy constructs. let’s look at a simple example:
熟悉了jscript和規(guī)范的差別,我們可以看到當(dāng)使用這些有問題的結(jié)構(gòu)的時(shí)候,和內(nèi)存消耗相關(guān)的潛在問題
var f = (function(){
if (true) {
return function g(){};
}
return function g(){};
})();we know that a function returned from within this anonymous invocation - the one that has g identifier - is being assigned to outer f. we also know that named function expressions produce superfluous function object, and that this object is not the same as returned function. the memory issue here is caused by this extraneous g function being literally “trapped” in a closure of returning function. this happens because inner function is declared in the same scope as that pesky g one. unless we explicitly break reference to g function it will keep consuming memory.
我們發(fā)現(xiàn)從匿名調(diào)用中返回的一個(gè)函數(shù),也就是以g作為標(biāo)示符的函數(shù),被復(fù)制給外部的f。我們還知道有名函數(shù)表達(dá)式創(chuàng)建了一個(gè)多余的函數(shù)對象,并且這個(gè)對象和返回的對象并不是同一個(gè)函數(shù)。這里的內(nèi)存問題就是由這個(gè)沒用的g函數(shù)在一個(gè)返回函數(shù)的閉包中被按照字面上的意思捕獲了。這是因?yàn)閮?nèi)部函數(shù)是和可惡的g函數(shù)在同一個(gè)作用域內(nèi)聲明的。除非我們顯式的破壞到g函數(shù)的引用,否則他將一直占用內(nèi)存。
var f = (function(){
var f, g;
if (true) {
f = function g(){};
}
else {
f = function g(){};
}
//給g賦值null以使他不再被無關(guān)的函數(shù)引用。
//null `g`, so that it doesn't reference extraneous function any longer
g = null;
return f;
})();note that we explicitly declare g as well, so that g = null assignment wouldn’t create a global g variable in conforming clients (i.e. non-jscript ones). by nulling reference to g, we allow garbage collector to wipe off this implicitly created function object that g refers to.
注意,我們又顯式的聲明了g,所以g=null賦值將不會給符合規(guī)范的客戶端(例如非jscirpt引擎)創(chuàng)建一個(gè)全局變量。通過給g以null的引用,我們允許垃圾回收來清洗這個(gè)被g所引用的,隱式創(chuàng)建的函數(shù)對象。
when taking care of jscript nfe memory leak, i decided to run a simple series of tests to confirm that nulling g actually does free memory.
當(dāng)考慮jscript的有名函數(shù)表達(dá)式的內(nèi)存泄露問題時(shí),我決定運(yùn)行一系列簡單的測試來證實(shí)給g函數(shù)null的引用實(shí)際上可以釋放內(nèi)存
|||
the test was simple. it would simply create 10000 functions via named function expressions and store them in an array. i would then wait for about a minute and check how high the memory consumption is. after that i would null-out the reference and repeat the procedure again. here’s a test case i used:
這個(gè)測試非常簡單。他將通過有名函數(shù)表達(dá)式創(chuàng)建1000個(gè)函數(shù),并將它們儲存在一個(gè)數(shù)組中。我等待了大約一分鐘,并查看內(nèi)存使用有多高。只有我們加上null引用,重復(fù)上述過程。下面就是我使用的一個(gè)簡單的測試用例
function createfn(){
return (function(){
var f;
if (true) {
f = function f(){
return 'standard';
}
}
else if (false) {
f = function f(){
return 'alternative';
}
}
else {
f = function f(){
return 'fallback';
}
}
// var f = null;
return f;
})();
}
var arr = [ ];
for (var i=0; i<10000; i++) {
arr[i] = createfn();
}results as seen in process explorer on windows xp sp2 were:
結(jié)果是在windows xp sp2進(jìn)行的,通過進(jìn)程管理器得到的
ie6:
without `null`: 7.6k -> 20.3k
with `null`: 7.6k -> 18k
ie7:
without `null`: 14k -> 29.7k
with `null`: 14k -> 27k
the results somewhat confirmed my assumptions - explicitly nulling superfluous reference did free memory, but the difference in consumption was relatively insignificant. for 10000 function objects, there would be a ~3mb difference. this is definitely something that should be kept in mind when designing large-scale applications, applications that will run for either long time or on devices with limited memory (such as mobile devices). for any small script, the difference probably doesn’t matter.
結(jié)果在一定程度上證實(shí)了我的假設(shè),顯示的給無用的參考以null值確實(shí)會釋放內(nèi)存,但是在內(nèi)寸的消耗的區(qū)別上貌似不是很大。對于1000個(gè)函數(shù)對象,大約應(yīng)該有3m左右的差別。但是有一些是明確的,在設(shè)計(jì)大規(guī)模的應(yīng)用的時(shí)候,應(yīng)用要不就是要運(yùn)行很長時(shí)間的或者要在一個(gè)內(nèi)存有限的設(shè)備上(例如移動設(shè)備)。對于任何小的腳本,差別可能不是很重要。
you might think that it’s all finally over, but we are not just quite there yet :) there’s a tiny little detail that i’d like to mention and that detail is safari 2.x
你可以認(rèn)為這樣就可以結(jié)束了,但是還沒到結(jié)束的時(shí)候。我還要討論一些小的細(xì)節(jié),而且這些細(xì)節(jié)是在safari 2.x下的
even less widely known bug with nfe is present in older versions of safari; namely, safari 2.x series. i’ve seen some claims on the web that safari 2.x does not support nfe at all. this is not true. safari does support it, but has bugs in its implementation which you will see shortly.
雖然沒有被人們發(fā)現(xiàn)在早期的safari版本,也就是safari 2.x版本中有名函數(shù)表達(dá)式的bug。但是我在web上看到一些聲稱safari 2.x根本不支持有名函數(shù)表達(dá)式。這不是真的。safari的確支持有名函數(shù)表達(dá)式,但是稍后你將看到在它的實(shí)現(xiàn)中是存在bug的
when encountering function expression in a certain context, safari 2.x fails to parse the program entirely. it doesn’t throw any errors (such as syntaxerror ones). it simply bails out:
在某些執(zhí)行環(huán)境中遇到函數(shù)表達(dá)式的時(shí)候,safari 2.x 將解釋程序整體失敗。它不拋出任何的錯(cuò)誤(例如syntaxerror)。展示如下
(function f(){})(); // <== 有名函數(shù)表達(dá)式 nfe
alert(1); //因?yàn)榍懊娴谋磉_(dá)式是的整個(gè)程序失敗,本行將無法達(dá)到, this line is never reached, since previous expression fails the entire programafter fiddling with various test cases, i came to conclusion that safari 2.x fails to parse named function expressions, if those are not part of assignment expressions. some examples of assignment expressions are:
在用一些測試用例測試之后,我總結(jié)出,如果有名函數(shù)表達(dá)式不是賦值表達(dá)式的一部分,safari解釋有名函數(shù)表達(dá)式將失敗。一些賦值表達(dá)式的例子如下
// 變量聲明part of variable declaration
var f = 1;
//簡單的賦值 part of simple assignment
f = 2, g = 3;
// 返回語句part of return statement
(function(){
return (f = 2);
})();
this means that putting named function expression into an assignment makes safari “happy”:
這就意味著把有名函數(shù)表達(dá)式放到賦值表達(dá)式中會讓 safari非常“開心”
(function f(){}); // fails 失敗
var f = function f(){}; // works 成功
(function(){
return function f(){}; // fails 失敗
})();
(function(){
return (f = function f(){}); // works 成功
})();
settimeout(function f(){ }, 100); // failsit also means that we can’t use such common pattern as returning named function expression without an assignment:
這也意味著我們不能使用這種普通的模式而沒有賦值表達(dá)式作為返回有名函數(shù)表達(dá)式
//要取代這種safari2.x不兼容的情況 instead of this non-safari-2x-compatible syntax:
(function(){
if (featuretest) {
return function f(){};
}
return function f(){};
})();
// 我們應(yīng)該使用這種稍微冗長的替代方法we should use this slightly more verbose alternative:
(function(){
var f;
if (featuretest) {
f = function f(){};
}
else {
f = function f(){};
}
return f;
})();
// 或者另外一種變形or another variation of it:
(function(){
var f;
if (featuretest) {
return (f = function f(){});
}
return (f = function f(){});
})();
/*
unfortunately, by doing so, we introduce an extra reference to a function
which gets trapped in a closure of returning function. to prevent extra memory usage,
we can assign all named function expressions to one single variable.
不幸的是 這樣做我們引入了對函數(shù)的另外一個(gè)引用
他將被包含在返回函數(shù)的閉包中
為了防止多于的內(nèi)存使用,我們可以吧所有的有名函數(shù)表達(dá)式賦值給一個(gè)單獨(dú)的變量
*/
var __temp;
(function(){
if (featuretest) {
return (__temp = function f(){});
}
return (__temp = function f(){});
})();
...
(function(){
if (featuretest2) {
return (__temp = function g(){});
}
return (__temp = function g(){});
})();
/*
note that subsequent assignments destroy previous references,
preventing any excessive memory usage.
注釋:后面的賦值銷毀了前面的引用,防止任何過多的內(nèi)存使用
*/
if safari 2.x compatibility is important, we need to make sure “incompatible” constructs do not even appear in the source. this is of course quite irritating, but is definitely possible to achieve, especially when knowing the root of the problem.
如果safari2.x的兼容性非常重要。我們需要保證不兼容的結(jié)構(gòu)不再代碼中出現(xiàn)。這當(dāng)然是非常氣人的,但是他確實(shí)明確的可以做到的,尤其是當(dāng)我們知道問題的根源。
it’s also worth mentioning that declaring a function as nfe in safari 2.x exhibits another minor glitch, where function representation does not contain function identifier:
還值得一提的是在safari中聲明一個(gè)函數(shù)是有名函數(shù)表達(dá)式的時(shí)候存在另外一個(gè)小的問題,這是函數(shù)表示法不含有函數(shù)標(biāo)示符(估計(jì)是tostring的問題)
var f = function g(){};
// notice how function representation is lacking `g` identifier
string(g); // function () { }this is not really a big deal. as i have already mentioned before, function decompilation is something that should not be relied upon anyway.
這不是個(gè)很大的問題。因?yàn)橹拔乙呀?jīng)說過,函數(shù)反編譯在任何情況下都是不可信賴的。
var fn = (function(){
//聲明一個(gè)變量,來給他賦值函數(shù)對象 declare a variable to assign function object to
var f;
// 條件的創(chuàng)建一個(gè)有名函數(shù) conditionally create a named function
// 并把它的引用賦值給f and assign its reference to `f`
if (true) {
f = function f(){ }
}
else if (false) {
f = function f(){ }
}
else {
f = function f(){ }
}
//給一個(gè)和函數(shù)名相關(guān)的變量以null值 assign `null` to a variable corresponding to a function name
//這可以使得函數(shù)對象(通過標(biāo)示符的引用)可以被垃圾收集所得到this marks the function object (referred to by that identifier)
// available for garbage collection
var f = null;
//返回一個(gè)條件定義的函數(shù) return a conditionally defined function
return f;
})();finally, here’s how we would apply this “techinque” in real life, when writing something like a cross-browser addevent function:
最后,當(dāng)我么一個(gè)類似于跨瀏覽器addevent函數(shù)的類似函數(shù)時(shí),下面就是我們?nèi)绾卧谡鎸?shí)的應(yīng)用中使用這個(gè)技術(shù)
// 1) 用一個(gè)分離的作用域封裝聲明 enclose declaration with a separate scope
var addevent = (function(){
var docel = document.documentelement;
// 2)聲明一個(gè)變量,用來賦值為函數(shù) declare a variable to assign function to
var fn;
if (docel.addeventlistener) {
// 3) 確保給函數(shù)一個(gè)描述的標(biāo)示符 make sure to give function a descriptive identifier
fn = function addevent(element, eventname, callback) {
element.addeventlistener(eventname, callback, false);
}
}
else if (docel.attachevent) {
fn = function addevent(element, eventname, callback) {
element.attachevent('on' + eventname, callback);
}
}
else {
fn = function addevent(element, eventname, callback) {
element['on' + eventname] = callback;
}
}
// 4)清除通過jscript創(chuàng)建的addevent函數(shù) clean up `addevent` function created by jscript
// 保證在賦值之前加上varmake sure to either prepend assignment with `var`,
// 或者在函數(shù)頂端聲明 addevent or declare `addevent` at the top of the function
var addevent = null;
// 5)最后通過fn返回函數(shù)的引用 finally return function referenced by `fn`
return fn;
})();
|||
it’s worth mentioning that there actually exist alternative ways of having descriptive names in call stacks. ways that don’t require one to use named function expressions. first of all, it is often possible to define function via declaration, rather than via expression. this option is only viable when you don’t need to create more than one function:
需要說明,實(shí)際上純在一個(gè)種使得在調(diào)用棧上顯示描述名稱(函數(shù)名)的替代方法。一個(gè)不需要使用有名函數(shù)表達(dá)式的方法。首先,通常可以使用聲明而不是使用表達(dá)式來定義函數(shù)。這種選擇通常只是適應(yīng)于你不需要創(chuàng)建多個(gè)函數(shù)的情況。
var hasclassname = (function(){
// 定義一些私有變量define some private variables
var cache = { };
//使用函數(shù)定義 use function declaration
function hasclassname(element, classname) {
var _classname = '(?:^|//s+)' + classname + '(?://s+|$)';
var re = cache[_classname] || (cache[_classname] = new regexp(_classname));
return re.test(element.classname);
}
// 返回函數(shù)return function
return hasclassname;
})();this obviously wouldn’t work when forking function definitions. nevertheless, there’s an interesting pattern that i first seen used by tobie langel. the way it works is by defining all functions upfront using function declarations, but giving them slightly different identifiers:
這種方法顯然對于多路的函數(shù)定義不適用。但是,有一個(gè)有趣的方法,這個(gè)方法我第一次在看到tobie langel.在使用。這個(gè)用函數(shù)聲明定義所有的函數(shù),但是給這個(gè)函數(shù)聲明以稍微不同的標(biāo)示符。
var addevent = (function(){
var docel = document.documentelement;
function addeventlistener(){
/* ... */
}
function attachevent(){
/* ... */
}
function addeventasproperty(){
/* ... */
}
if (typeof docel.addeventlistener != 'undefined') {
return addeventlistener;
}
elseif (typeof docel.attachevent != 'undefined') {
return attachevent;
}
return addeventasproperty;
})();while it’s an elegant approach, it has its own drawbacks. first, by using different identifiers, you loose naming consistency. whether it’s good or bad thing is not very clear. some might prefer to have identical names, while others wouldn’t mind varying ones; after all, different names can often “speak” about implementation used. for example, seeing “attachevent” in debugger, would let you know that it is an attachevent-based implementation of addevent. on the other hand, implementation-related name might not be meaningful at all. if you’re providing an api and name “inner” functions in such way, the user of api could easily get lost in all of these implementation details.
雖然這是一個(gè)比較優(yōu)雅的方法,但是他也有自己的缺陷。首先,通過使用不同的標(biāo)示符,你失去的命名的一致性。這是件好的事情還是件壞的事情還不好說。有些人希望使用一支的命名,有些人則不會介意改變名字;畢竟,不同的名字通常代表不同的實(shí)現(xiàn)。例如,在調(diào)試器中看到“attachevent”,你就可以知道是addevent基于attentevent的一個(gè)實(shí)現(xiàn)。另外一方面,和實(shí)現(xiàn)相關(guān)的名字可能根本沒有什意義。如果你提供一個(gè)api并用如此方法命名內(nèi)部的函數(shù),api的使用者可能會被這些實(shí)現(xiàn)細(xì)節(jié)搞糊涂。
a solution to this problem might be to employ different naming convention. just be careful not to introduce extra verbosity. some alternatives that come to mind are:
解決這個(gè)問題的一個(gè)方法是使用不同的命名規(guī)則。但是注意不要飲用過多的冗余。下面列出了一些替代的命名方法
`addevent`, `altaddevent` and `fallbackaddevent`
// or
`addevent`, `addevent2`, `addevent3`
// or
`addevent_addeventlistener`, `addevent_attachevent`, `addevent_asproperty`
another minor issue with this pattern is increased memory consumption. by defining all of the function variations upfront, you implicitly create n-1 unused functions. as you can see, if attachevent is found in document.documentelement, then neither addeventlistener nor addeventasproperty are ever really used. yet, they already consume memory; memory which is never deallocated for the same reason as with jscript’s buggy named expressions - both functions are “trapped” in a closure of returning one.
這種模式的另外一個(gè)問題就是增加了內(nèi)存的開銷。通過定義所有上面的函數(shù)變種,你隱含的創(chuàng)建了n-1個(gè)函數(shù)。你可以發(fā)現(xiàn),如果attachevent在document.documentelement中發(fā)現(xiàn),那么addeventlistener和addeventasproperty都沒有被實(shí)際用到。但是他們已經(jīng)消耗的內(nèi)存;和jscript有名表達(dá)式bug的原因一樣的內(nèi)存沒有被釋放,在返回一個(gè)函數(shù)的同時(shí),兩個(gè)函數(shù)被‘trapped‘在閉包中。
this increased consumption is of course hardly an issue. if a library such as prototype.js was to use this pattern, there would be not more than 100-200 extra function objects created. as long as functions are not created in such way repeatedly (at runtime) but only once (at load time), you probably shouldn’t worry about it.
這個(gè)遞增的內(nèi)存使用顯然是個(gè)嚴(yán)重的問題。如果和prototype.js類似的庫需要使用這種模式,將有另外的100-200個(gè)多于的函數(shù)對象被創(chuàng)建。如果函數(shù)沒有被重復(fù)地(運(yùn)行時(shí))用這種方式創(chuàng)建,只是在加載時(shí)被創(chuàng)建一次,你可能就不用擔(dān)心這個(gè)問題。
新聞熱點(diǎn)
疑難解答
圖片精選