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

转载请注明出处

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容