/ 昌里大金猪的空间 / JavaScript 中的作用域与变量声明提升 [翻译]

JavaScript 中的作用域与变量声明提升 [翻译]

2011-04-11 posted in [javascript]

原文地址:http://www.adequatelygood.com/2010/2/JavaScript-Scoping-and-Hoisting
原文作者:ben cherry

在前篇小议javascript之全局对象创建译文中提到过JavaScript Hoisting概念,当时不甚了解。google之后发现ben cherry解释的最为明了,于是有了本文。

——以下为翻译——

JavaScript Scoping and Hoisting(JavaScript中的作用域与变量声明提升/声明时机提升)

你知道下面JavaScript代码的执行结果是什么吗?
var foo = 1
function bar() { 
    if (!foo) { 
        var foo = 10
    } 
    alert(foo); 
} 
bar();
如果你对foo的值实际上为"10"而感到诧异的话,再看一下下面这个例子:
var a = 1
function b() { 
    a = 10
    return
    function a() {} 
} 
b(); 
alert(a);

发现浏览器会弹出alert(1)。怎么回事呢?看起来很诡异,这恰好是JavaScript语言的特性。我不确定这种表现行为是否已经有一个标准的命名了,于是我将此称为变量声明提升。本文会对这种机制进行揭秘,先从作用域谈起好了。


Scoping in JavaScript(JavaScript中的作用域)

对于很多JavaScript语言初学者而言,作用域带来许多困惑。当然也不仅仅是初学者会遇到这些问题,不少资深JavaScript程序员也未能完全掌握作用域的精髓。之所以带来困惑,其中一个原因是由于JavaScript实在太像C风格的语言了。如下是一段C程序:
#include <stdio.h> 
int main() { 
    int x = 1
    printf("%d, ", x); // 1 
    if (1) { 
        int x = 2
        printf("%d, ", x); // 2 
    } 
    printf("%d\n", x); // 1 
}

程序输出结果是121。因为C风格的语言有块级作用域(block-level scope)。当函数鱼运行到 if 语句中时,当前作用域中的新变量会被声明,并且不会影响到外部作用域。但在JavaScript中情况并不是这样:
var x = 1
console.log(x); // 1 
if (true) { 
    var x = 2
    console.log(x); // 2 
} 
console.log(x); // 2

程序输出结果122。原因是JavaScript支持函数作用域(function-level scope),这个特性与C风格的语言格格不入。if 语句中的代码块并不会创建新的作用域,只有函数才会。

对于许多熟谙C\C++\C#\Java语言的程序员来说,JavaScript这个语言特性非常出乎意料。幸运的是,正由于JavaScript函数的灵活性,产生了一个变通方案。如果你必须在函数中创建一个临时作用域,可以这么做:
function foo() { 
    var x = 1
    if (x) { 
        (function () { 
            var x = 2
            // some other code 
        }()); 
    } 
    // x is still 1. 
}
这个函数非常灵活,不仅仅在块级语句内,需要时能在任何地方使用。然而我强烈建议你多花点时间来理解JavaScript的作用域。


Declarations, Names, and Hoisting(声明、名称以及变量声明提升/声明时机提升

当访问函数内的变量时,JavaScript会按照下面顺序查找:
  1. 语言级别:默认在所用作用域下会定义thisarguments
  2. 传入参数:函数命名的参数,作用域是当前函数体
  3. 函数声明:例如function foo() {}
  4. 变量声明:例如var foo;
//以上翻译地比较晦涩,建议直接看原文
函数声明与变量声明经常被JavaScript引擎隐式地提升到当前作用域的顶部,也就是说:
function foo() { 
    bar(); 
    var x = 1
}
实际上会被解释成:
function foo() { 
    var x; 
    bar(); 
    x = 1
}
也就是说,下面两种声明方式是等价的:
function foo() { 
    if (false) { 
        var x = 1
    } 
    return
    var y = 1
} 
function foo() { 
    var x, y; 
    if (false) { 
        x = 1
    } 
    return
    y = 1
}

可以发现,声明语句中的赋值部分并没有被提升声明,只有名称被提升了。两种函数声明方式:
function test() { 
    foo(); // TypeError "foo is not a function" 
    bar(); // "this will run!" 
    var foo = function () { // function expression assigned to local variable 'foo' 
        alert("this won't run!"); 
    } 
    function bar() { // function declaration, given the name 'bar' 
        alert("this will run!"); 
    } 
} 
test();
这个例子中,只有包含函数体的函数声明会被提升声明。foo虽然会被提升声明,但是函数体却在执行中被赋值。以上就是提升声明时机的基本概念,看起来一点也不复杂。


Name resolution order(名称解析顺序)


名称解析顺序有4步,一般来说,如果一个名称已经被定义了,它就不会被另一个具有同名称的属性所覆盖。这也就意味着,函数声明会比变量声明优先。并不是名称的赋值操作不会被执行,只是说声明部分被忽略了而已。有些例外:
  • 原生变量arguments特立独行,包含了传递到函数中的参数。如果自定义以arguments为命名的参数,将会阻止原生arguments对象的创建。所以勿使用arguments为名称的参数。
  • 胡乱使用this标识符会引起语法错误。
  • 如果多个参数具有相同的命名,那么最后一个参数会优先于先前的,即时这个参数未定义。


Named Function Expressions(函数命名表达式)

你可以通过函数表达式给函数命名,语法上看起来像是函数声明,实则不是。上一段代码:
foo(); // TypeError "foo is not a function" 
bar(); // valid 
baz(); // TypeError "baz is not a function" 
spam(); // ReferenceError "spam is not defined" 
 
var foo = function () {}// anonymous function expression ('foo' gets hoisted) 
function bar() {}// function declaration ('bar' and the function body get hoisted) 
var baz = function spam() {}// named function expression (only 'baz' gets hoisted) 
 
foo(); // valid 
bar(); // valid 
baz(); // valid 
spam(); // ReferenceError "spam is not defined"

How to Code With This Knowledge(如何编码)

文章到此你应该已经理解什么是作用域和提升声明时机了,那么如何在实战中运用呢?切记,使用var表达式创建变量,在此强烈建议在代码块顶部使用一个var表达式来创建变量。然而,这么做的同时也导致开发者很难对当前作用域下实际被声明的变量进行跟踪。我建议开发者使用JSLint来进行验证:
/*jslint onevar: true [...] */ 
function foo(a, b, c) { 
    var x = 1
        bar, 
        baz = "something"
}

What the Standard Says(看看规范上是怎么说的)

ECMAScript Standard 中的section12.2.2里写到:
If the variable statement occurs inside a FunctionDeclaration, the variables are defined with function-local scope in that function, as described in section 10.1.3. Otherwise, they are defined with global scope (that is, they are created as members of the global object, as described in section 10.1.3) using property attributes { DontDelete }. Variables are created when the execution scope is entered. A Block does not define a new execution scope. Only Program and FunctionDeclaration produce a new scope. Variables are initialised to undefined when created. A variable with an Initialiser is assigned the value of its AssignmentExpression when the VariableStatement is executed, not when the variable is created.//对ECMAScript不甚了解,故在此不作翻译

——翻译结束——
2011-04-11 17:44:39

参考资料:
作用域与命名空间