JavaScript Scoping and Hoisting

你知道如下的JavaScript代码被执行后,会弹出什么?

var foo = 1;

function bar() {

    if (!foo) {

    var foo = 10;

    }

    alert(foo);

}

bar();

如果你对弹出的结果是“10”感到惊讶,下面的这段代码弹出的结果会让你感到震惊。

var a = 1;

function b() {

    a = 10;

    return;

    function a() {}

}

b();

alert(a);

当然,上面的代码会让浏览器弹出“1”。那么这中间究竟发生了什么?虽然这看起来似乎让人感到陌生,危险,困惑,但是这就是JavaScript语言的强大并富有表现力的特征。我不知道对这个特殊的行为是否有标准的名称,但是我喜欢用“hoisting”来标识它。这边文章将会尝试揭示为什么会这样,但是我们先要绕个路,来了解下JavaScript的作用域(scoping)。

JavaScript中的作用域(scoping)

对于JavaScript初学者来说最让人困惑的来源之一就是作用域(scoping)。事实上,不仅是初学者,我也遇到许多有经验的JavaScript程序员,他们也不是完全了解作用域。在JavaScript中的作用域是如此的让人感到困惑,究其原因是JavaScript看起来像是C家族的语言。考虑下列的C程序:

#include

int main() {

    int x = 1;

    printf("%d, ", x); // 1

    if (1) {

        int x = 2;

        printf("%d, ", x); // 2

    }

    printf("%d\n", x); // 1

}

这个程序的输出结果是“1,2,1”。之所以输出这样的结果是因为C和其它的C家族语言都有着“block-level”作用域。当 控制(control)进入block(比如if声明)后,在if的作用域中就可以声明新的变量,而不影响外层的作用域。但是这却不适用于JavaScript。在Firebug中测试如下的代码:

var x = 1;

console.log(x); // 1

if (true) {

    var x = 2;

    console.log(x); // 2

}

console.log(x); // 2

在这个例子中,Firebug将会显示“1,2,2”。这是因为JavaScript中只有function-level(函数作用域)。这就是和C语言的区别。Blocks(比如if声明)不会创建一个新的作用域。只有函数才会创建新的作用域。

对于许多熟悉C,C++,C#,Java的程序员来说,这是出乎意料的和不收欢迎的。值得庆幸的是,由于JavaScript中函数的灵活性,可以找到一个变通方法。如果你一定要在函数中创建一个临时的作用域,可以尝试像下面这样做:

function foo() {

    var x = 1;

    if (x) {

        (function () {

            var x = 2;

            // some other code

        }());    //(function(){}())

    }    //if(x)

// x is still 1.

}

事实上,这个方法非常灵活,可以在任何你需要临时作用域的地方进行使用,不仅仅是在block声明之内。然而,我强烈建议你花点时间来理解下JavaScript的作用域。它是如此的强大,并且是我喜爱的语言特征之一。如果你理解了作用域,hoisting(提前)对你来说会好理解许多。

声明,命名,和Hoisting

在JavaScript中,一个名字可以用四种方式中的其中之一进入作用域:

Language-defined:  默认情况下,所有的函数作用域都被传递了this和arguments这2个参数。

Formal parameters(作为形参): 就像其它语言中的形参那样。

Function declarations(函数声明):函数声明具有function foo() {}这样的形式。


Variable declarations(变量声明):变量声明采取var foo这样的形式。

函数声明和变量声明被JavaScript的interpreter(解释器)隐式的移动到它们作用域的顶部。函数形参和Language-difined(语言定义的)名字 很明显已经在顶部了。这意味着像这样的代码:

function foo() {

    bar();

    var x = 1;

}

被解释为这样:

function foo() {

    var x;

    bar();

    x = 1;

}

不管包含声明的那行代码是否会被执行。如下的2个函数式等价的:

function foo() {

    if (false) {

        var x = 1;

    }

    return;

    var y = 1;

}

function foo() {

    var x, y;

    if (false) {

        x = 1;

    }

    return;

    y = 1;

}

注意声明的赋值部分并没有被hoisted,只有声明部分被hoisted。这不同于函数声明(函数声明会将整个函数体也hoist)。但是要记得有2种常用方式来声明函数。思考如下的JavaScript代码:

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!");

    }//var foo = function(){}

    function bar() { // function declaration, given the name 'bar'

        alert("this will run!");

    }//  function bar() {}  

}

test();

在这个例子中,bar的函数声明及其函数体被提前到顶部。变量foo的声明被提前,但是其右侧的匿名函数及其函数体并没有提前,被留下来  等待在执行时赋值给foo。

上述阐述覆盖了hoisting的基本情况,事实上并不像看起来的那样复杂。当然,JavaScript中的this指针,在某些特殊的场合下,是有点复杂的。

Name Resolution Order(名称解析顺序)

需要谨记的最重要的特殊情况是name resolution order。有4种方式供名称进入给定的作用域。我列出它们的顺序就是它们被解析的顺序。总的来说,如果一个名称已经被定义了,它不会被另一个同名的property覆盖。这意味着函数声明的优先级高于变量声明。这并不意味着对那个名称的赋值会不起作用,仅仅是(=右边的)声明部分会被忽略。

这儿有一些例外:bulit-in(内建的)arguments 举止有些古怪。它似乎是在形参后声明的,但是在函数声明前。这意味着如果形参的名称被取为arguments,那么它的优先级高于内建的arguments,即使它是undefined。这是个不好的特性,所以不要形参不要命名为arguments。

尝试使用this作为标识符会导致SyntaxError(语法错误)。这是个好的特性。

如果多个形参的名字相同的话,最后出现的那个会高于其它的,即使它是undefined。

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(在编码时如何运用这些知识?)

既然你已经了解了作用域和hoisting,那么在JavaScript中对于编写代码,它们(作用域和hoisting)意味着什么?最重要的事情是“在声明你所有的变量时,只使用一个‘var statement’ ”。我强烈建议你在每个作用域内只使用一个var statement,并且把它(var statement)放到作用域顶部。如果你强迫自己这样做的话,你永远不会有hoisting相关的困惑。然而,这样做可能会使得追踪‘哪些变量是在当前作用域中声明的’变得困难。我建议在JSLint中设置onevar选项来强制达到这点。如果你按照我要求的去做的话,你的代码看起来应该像这样:

/*jslint onevar: true [...] */

function foo(a, b, c) {

    var x = 1,

        bar,

        baz = "something";

}

What the Standard Says(那么具体的标准是如何的?)

我发现,想要了解这些‘事情(scoping,hoisting)’是如何运作的 ,直接查阅ECMAScript Standard (pdf)往往是最有帮助的。如下是ECMAScript Standard (pdf)关于变量声明和作用域的描述:

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.

如果变量声明出现在函数声明之内,那么这些变量就被定义在那个函数的函数作用域内,像章节10.1.3中描述那样。否则,这些变量通过使用property attributes{DontDelete}被定义在全局作用域(即,这些变量被作为global object的成员被创建,像章节10.1.3中描述的那样)。变量在进入作用域时被创建。一个block不会定义一个新的作用域。只有程序和函数声明会创建一个新的作用域。变量在创建时被初始化为undefined。带有初始值的变量在变量声明被执行时,会被赋予它的赋值表达式的值。而不是变量被创建时。

我希望这篇文章已经揭示了,对JavaScript程序员来说,最困惑的根源之一(scoping,hoisting)。我尽可能的透彻地阐述这件事,并避免在阐述这件事时 制造更多的困惑。如果有什么错误或者大的疏忽,请告知我。

本文翻译自:http://www.adequatelygood.com/JavaScript-Scoping-and-Hoisting.html

转载请注明出处

你可能感兴趣的:(JavaScript Scoping and Hoisting)