[翻译] JavaScript Scoping and Hoisting

原文链接:JavaScript Scoping and Hoisting

你晓得下面的JavaScript代码实行后会alert出什么值吗?

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

浏览器会alert“1”。那末,究竟是怎么了?只管这看起来有点新鲜、有点风险又有点令人疑心,但这事实上倒是这门言语一个强力的具有表现力的特征。我不晓得是不是是有个规范来定义这类行动,然则我喜好用”hoisting”来形貌。这篇文章试着去诠释这类机制,然则起首,让我们对JavaScript的scoping做一些必要的相识。

Scoping in JavaScript

关于JavaScript新手来讲scoping是最令人疑心的部份之一。事实上,不仅仅是新手,我碰到或许多有履历的JavaScript递次员也不能完整邃晓scoping。JavaScript的scoping云云庞杂的原因是它看上去异常像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
}

这段递次的输出是1,2,1。这是由于在C系言语有块级作用域(block-level scope),当进入到一个块时,就像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 scope)。这和C系言语是完整差别的。块,就像if语句,并不会建立一个新的作用域。只要函数才会建立新的作用域。

关于大部份熟习C,C++,C#或是Java的递次员来讲,这是意料之外而且不被待见的。荣幸的是,由于JavaScript函数的天真性,关于这个题目我们有一个解决方案。假如你必须在函数中建立一个暂时的作用域,请像下面如许做:

function foo() {
    var x = 1;
    if (x) {
        (function () {
            var x = 2;
            // some other code
        }());
    }
    // x is still 1.
}

这类方面确切异常天真,它运用在任何须要建立一个暂时作用域的处所,不仅仅是某个块中。然则,我猛烈发起你花点时候好好邃晓下JavaScript scoping。它实在是异常强力,而且它也是我最喜好的言语特征之一。假如你很好的邃晓了scoping,邃晓hoisting将会越发轻易。

Declarations, Names, and Hoisting

在JavaScript中,一个作用域(scope)中的称号(name)有以下四种:

  1. 言语本身定义(Language-defined): 一切的作用域默许都邑包含this和arguments。

  2. 函数形参(Formal parameters): 函数有名字的形参会进入到函数体的作用域中。

  3. 函数声明(Function decalrations): 经由过程function foo() {}的情势。

  4. 变量声明(Variable declarations): 经由过程var foo;的情势。

函数声明和变量声明老是被JavaScript诠释器隐式地提拔(hoist)到包含他们的作用域的最顶端。很明显的,言语本身定义和函数形参已处于作用域顶端。这就像下面的代码:

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;
}

注意到声明的赋值部份并没有被提拔(hoist)。只要声明的称号被提拔了。这和函数声明差别,函数声明中,全部函数体也都邑被提拔。然则请记着,声明一个函数一般来讲有两种体式格局。斟酌下面的JavaScript代码:

function test() {
    foo(); // TypeError "foo is not a function"
    bar(); // "this will run!"
    var foo = function () { // 函数表达式被赋值给变量'foo'
        alert("this won't run!");
    }
    function bar() { // 名为'bar'的函数声明
        alert("this will run!");
    }
}
test();

在这里,只要函数声明的体式格局会连函数体一同提拔,而函数表达式中只会提拔称号,函数体只要在实行到赋值语句时才会被赋值。

以上就包含了一切关于提拔(hoisting)的基本,看起来并没有那末庞杂或是令人疑心对吧。然则,这是JavaScript,在某些特殊情况下,总会有那末一点庞杂。

Name Resolution Order

须要记着的最最主要的惯例就是称号剖析递次(name resolution order)。记着一个称号进入一个作用域一共有四种体式格局。我上面列出的递次就是他们剖析的递次。总的来讲,假如一个称号已被定义了,他毫不会被另一个具有不必属性的同名称号掩盖。这就意味着,函数声明比变量声明具有更高的优先级。然则这却不意味着对这个称号的赋值无效,仅仅是声明的部份会被疏忽罢了。然则有下面几个破例:

内置的称号arguments的行动有些奇异。他似乎是在形参以后,函数声明之前被声明。这就意味着名为arguments的形参会比内置的arguments具有更高的优先级,纵然这个形参是undefined。这是一个不好的特征,不要运用arguments作为形参。
任何处所试图运用this作为一个标识都邑引发语法错误,这是一个好的特征。
假如有多个同名形参,那位于列表末了的形参具有最高的优先级,纵然它是undefined。

Name Function Expressions

你可以在函数表达式中给函数定义称号,就像函数声明的语句一样。但这并不会使它成为一个函数声明,而且这个称号也不会被引入到作用域中,而且,函数体也不会被提拔(hoist)。这里有一些代码可以申明我说的是什么意义:

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 () {}; // 匿名函数表达式('foo'被提拔)
function bar() {}; // 函数声明('bar'和函数体被提拔)
var baz = function spam() {}; // 定名函数表达式(只要'baz'被提拔)

foo(); // valid
bar(); // valid
baz(); // valid
spam(); // ReferenceError "spam is not defined"

How to Code With This Knowledge

如今你邃晓了作用域和提拔,那末这对编写JavaScript代码意味着什么呢?最主要的一条是声明变量时老是运用var语句。我猛烈的发起你在每一个作用域中都只在最顶端运用一个var。假如你强迫本身这么做,你永久也不会被提拔相干的题目搅扰。只管这么做会使的跟踪当前作用域现实声清楚明了哪些变量变得越发难题。我发起在JSLint运用onevar选项。假如你做了一切前面的发起,你的代码看起来会是下面如许:

/*jslint onevar: true [...] */
function foo(a, b, c) {
    var x = 1,
        bar,
        baz = "something";
}

What the Standard Says

我发明直接参考ECMAScript Standard (pdf)来邃晓这些东西是怎样运作的老是很有效。下面是关于变量声明和作用域的一段摘录(section 12.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.

我愿望这篇文章可以给JavaScript递次员最轻易疑心的部份一些启发。我全力写的周全,以避免引发更多的疑心。假如我写错了或是漏掉了某些主要的东西,请肯定让我晓得。

    原文作者:BetaRabbit
    原文地址: https://segmentfault.com/a/1190000004345355
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞