【详解九章算法】详解JavaScript作用域与变量声明提升【翻译】

更新时间:2019-12-23    来源:网页配色    手机版     字体:

【www.bbyears.com--网页配色】

你知道下面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”,那么这些结果为何如此呢?确实这些看起来有点奇怪,危险,并且混乱,但这恰恰是说明JavaScript是一个强大和富有表现力的语言。我不知道这种特性的标准名称,但是我习惯叫它"提升“,这篇文章将尝试阐明这一机制,但首先我们需要先了解JavaScript的作用域。


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语言有块级作用域。当代码执行到一个块内,例如一对大括号内,新的变量声明在这个作用域内,不会影响大括号的外部。这是不同于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为函数作用域,这完全不同于类C语言的块级,比如在大括号内,它是不会创建新的作用域的。只有在函数才会。

而很多语言都使用的块级作用域,比如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中,一个变量进入作用域有四种基本途径:

    语言定义:全局作用域,默认情况下,有变量this和arguments。
    形参:函数可以有形参,它的作用域为整个函数内。
    函数声明:例如这种形式 function foo() {}
    变量声明:例如这种形式 var foo;

函数声明和变量声明会在解析JavaScript程序是进行内部“提升”,形参和全局变量已经存在了,意味着提升的是3,4两种变量类型,意味着代码在解析后会像这样:

function foo() {
    bar();
    var x = 1;
}
实际上解释后会像这样: 6 
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;
}


注意:当函数作为变量定义的值时声明不会被提升,这种情况只有变量名会被提升,这导致函数名提升了,但函数体没有被提升,但请记住函数声明有两种形式,考虑以下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!");
    }
    function bar() { // function declaration, given the name 'bar'
        alert("this will run!");
    }
}
test();


在这个例子中,只有包含函数体的函数声明会被提升到顶部,而变量"foo"被提升了,但是它的主体在右边,只有在语句执行到此时才会被分配。

这就是提升的基本概念,这样看起来也不是那么复杂和容易混淆了吧。当然在写JavaScript时,会遇到一些特殊情况会稍微复杂点。


变量名解析顺序

最重要的是记住在特殊情况下的变量名解析顺序。它们有四种方式进入命名空间,这个顺序根据列表从上到下一次进行,这个顺序列表在下面列出,在一般情况下,如果一个命名已经被定义,那么它不会被另一个同名的所覆盖,这就意味着一个函数声明要优先于变量声明,这并不等于分配新的命名无效,只是声明将被屏蔽。他们也有些例外:

    内置arguments的怪异情况,它看起来在函数声明之前形参已经被声明,这意味着一个形参名arguments将优先于内置的arguments,即使它没有被定义,这是一个不好的特征。不要使用arguments作为一个形参。
    如果定义this这个命名在一些地方,会导致语法错误。这是一个好的特性。
    如果多个形参有相同的名字,那么形参中最后一个同名的将被优先,哪怕它没有被定义。

函数命名表达式

你可以使用函数表达式的形式将函数定义赋给一个函数名,语法像函数定义,但它不同于函数声明,这个命名没有进入作用域,函数体也没有提升。这里有一些代码作为例子来说明其含义:

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"


如何使用这个知识来写代码

现在你了解了作用域与命名提升,但如何写JavaScript代码呢?一个非常重要的事情是在任何声明变量的时候都使用var.我强烈建议你在每一个作用域内的头部使用var定义变量,如果你总是如此,你将不会因为提升的特性而混乱。然而,这样做很难区分当前作用域下实际被声明的变量和以有的变量。我建议使用JSLint的onevar选项来执行这些。如果你准备这样做,那么你的代码应该看起来是这样:
/*jslint onevar: true [...] */
function foo(a, b, c) {
    var x = 1,
        bar,
        baz = "something";
}

规范怎么说明的

我发现经常查阅ECMAScript Starndard去理解这些特性是如何工作的是非常有用的。这里是它说明变量声明和作用域(最新版的12.2.2部分)

    如果变量声明在函数声明内部,那么这个变量被定义在当前函数作用域内,由10.1.3节所述。另外,他们都被定义在全局作用域(即,他们创建的是全局对象的成员,由10.1.3所述)的属性上,并具有属性的特性。变量被创建在当前执行的作用域内,一个块没法产生一个新的作用空间,只有程序和函数声明产生新的作用空间。变量被初始化时创建成一个undefined。一个变量真正被初始化是在使用表达式给变量分配一个存在的值。不是在变量被创建时。


作者原文


http://www.adequatelygood.com/JavaScript-Scoping-and-Hoisting.html

JavaScript Scoping and Hoisting

Do you know what value will be alerted if the following is executed as a JavaScript program?

var foo = 1;
function bar() {
    if (!foo) {
        var foo = 10;
    }
    alert(foo);
}
bar();

If it surprises you that the answer is “10”, then this one will probably really throw you for a loop:

var a = 1;
function b() {
    a = 10;
    return;
    function a() {}
}
b();
alert(a);

Here, of course, the browser will alert “1”. So what’s going on here? While it might seem strange, dangerous, and confusing, this is actually a powerful and expressive feature of the language. I don’t know if there is a standard name for this specific behavior, but I’ve come to like the term “hoisting”. This article will try to shed some light on this mechanism, but first lets take a necessary detour to understand JavaScript’s scoping.
Scoping in JavaScript

One of the sources of most confusion for JavaScript beginners is scoping. Actually, it’s not just beginners. I’ve met a lot of experienced JavaScript programmers who don’t fully understand scoping. The reason scoping is so confusing in JavaScript is because it looks like a C-family language. Consider the following C program:

#include
int main() {
    int x = 1;
    printf("%d, ", x); // 1
    if (1) {
        int x = 2;
        printf("%d, ", x); // 2
    }
    printf("%d\n", x); // 1
}

The output from this program will be 1, 2, 1. This is because C, and the rest of the C family, has block-level scope. When control enters a block, such as the if statement, new variables can be declared within that scope, without affecting the outer scope. This is not the case in JavaScript. Try the following in Firebug:

var x = 1;
console.log(x); // 1
if (true) {
    var x = 2;
    console.log(x); // 2
}
console.log(x); // 2

In this case, Firebug will show 1, 2, 2. This is because JavaScript has function-level scope. This is radically different from the C family. Blocks, such as if statements, do not create a new scope. Only functions create a new scope.

To a lot of programmers who are used to languages like C, C++, C#, or Java, this is unexpected and unwelcome. Luckily, because of the flexibility of JavaScript functions, there is a workaround. If you must create temporary scopes within a function, do the following:

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

This method is actually quite flexible, and can be used anywhere you need a temporary scope, not just within block statements. However, I strongly recommend that you take the time to really understand and appreciate JavaScript scoping. It’s quite powerful, and one of my favorite features of the language. If you understand scoping, hoisting will make a lot more sense to you.
Declarations, Names, and Hoisting

In JavaScript, a name enters a scope in one of four basic ways:

    Language-defined: All scopes are, by default, given the names this and arguments.
    Formal parameters: Functions can have named formal parameters, which are scoped to the body of that function.
    Function declarations: These are of the form function foo() {}.
    Variable declarations: These take the form var foo;.

Function declarations and variable declarations are always moved (“hoisted”) invisibly to the top of their containing scope by the JavaScript interpreter. Function parameters and language-defined names are, obviously, already there. This means that code like this:

function foo() {
    bar();
    var x = 1;
}

is actually interpreted like this:

function foo() {
    var x;
    bar();
    x = 1;
}

It turns out that it doesn’t matter whether the line that contains the declaration would ever be executed. The following two functions are equivalent:

function foo() {
    if (false) {
        var x = 1;
    }
    return;
    var y = 1;
}
function foo() {
    var x, y;
    if (false) {
        x = 1;
    }
    return;
    y = 1;
}

Notice that the assignment portion of the declarations were not hoisted. Only the name is hoisted. This is not the case with function declarations, where the entire function body will be hoisted as well. But remember that there are two normal ways to declare functions. Consider the following 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!");
    }
    function bar() { // function declaration, given the name 'bar'
        alert("this will run!");
    }
}
test();

In this case, only the function declaration has its body hoisted to the top. The name ‘foo’ is hoisted, but the body is left behind, to be assigned during execution.

That covers the basics of hoisting, which is not as complex or confusing as it seems. Of course, this being JavaScript, there is a little more complexity in certain special cases.
Name Resolution Order

The most important special case to keep in mind is name resolution order. Remember that there are four ways for names to enter a given scope. The order I listed them above is the order they are resolved in. In general, if a name has already been defined, it is never overridden by another property of the same name. This means that a function declaration takes priority over a variable declaration. This does not mean that an assignment to that name will not work, just that the declaration portion will be ignored. There are a few exceptions:

    The built-in name arguments behaves oddly. It seems to be declared following the formal parameters, but before function declarations. This means that a formal parameter with the name arguments will take precedence over the built-in, even if it is undefined. This is a bad feature. Don’t use the name arguments as a formal parameter.
    Trying to use the name this as an identifier anywhere will cause a SyntaxError. This is a good feature.
    If multiple formal parameters have the same name, the one occurring latest in the list will take precedence, even if it is undefined.

Named Function Expressions

You can give names to functions defined in function expressions, with syntax like a function declaration. This does not make it a function declaration, and the name is not brought into scope, nor is the body hoisted. Here’s some code to illustrate what I mean:

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

Now that you understand scoping and hoisting, what does that mean for coding in JavaScript? The most important thing is to always declare your variables with a var statement. I strongly recommend that you have exactly one var statement per scope, and that it be at the top. If you force yourself to do this, you will never have hoisting-related confusion. However, doing this can make it hard to keep track of which variables have actually been declared in the current scope. I recommend using JSLint with the onevar option to enforce this. If you’ve done all of this, your code should look something like this:

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

What the Standard Says

I find that it’s often useful to just consult the ECMAScript Standard (pdf) directly to understand how these things work. Here’s what it has to say about variable declarations and scope (section 12.2.2 in the older version):

    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.

I hope this article has shed some light on one of the most common sources of confusion to JavaScript programmers. I have tried to be as thorough as possible, to avoid creating more confusion. If I have made any mistakes or have large omissions, please let me know.



补充一篇 javascript变量声明提升(hoisting)

javascript的变量声明具有hoisting机制,JavaScript引擎在执行的时候,会把所有变量的声明都提升到当前作用域的最前面。

先看一段代码

var v = "hello";
(function(){
  console.log(v);
  var v = "world";
})();


这段代码运行的结果是什么呢?
答案是:undefined
这段代码说明了两个问题,
第一,function作用域里的变量v遮盖了上层作用域变量v。代码做少些变动

var v = "hello";
if(true){
  console.log(v);
  var v = "world";
}


输出结果为”hello”,说明javascript是没有块级作用域的。函数是JavaScript中唯一拥有自身作用域的结构。

第二,在function作用域内,变量v的声明被提升了。所以最初的代码相当于:

var v = "hello";
(function(){
  var v; //declaration hoisting
  console.log(v);
  v = "world";
})();


声明、定义与初始化

声明宣称一个名字的存在,定义则为这个名字分配存储空间,而初始化则是为名字分配的存储空间赋初值。


用C++来表述这三个概念

extern int i;//这是声明,表明名字i在某处已经存在了
int i;//这是声明并定义名字i,为i分配存储空间
i = 0;//这是初始化名字i,为其赋初值为0

javascript中则是这样

var v;//声明变量v
v = "hello";//(定义并)初始化变量v

因为javascript为动态语言,其变量并没有固定的类型,其存储空间大小会随初始化与赋值而变化,所以其变量的“定义”就不像传统的静态语言一样了,其定义显得无关紧要。

声明提升

当前作用域内的声明都会提升到作用域的最前面,包括变量和函数的声明

(function(){
  var a = "1";
  var f = function(){};
  var b = "2";
  var c = "3";
})();


变量a,f,b,c的声明会被提升到函数作用域的最前面,类似如下:

(function(){
  var a,f,b,c;
  a = "1";
  f = function(){};
  b = "2";
  c = "3";
})();


请注意函数表达式并没有被提升,这也是函数表达式与函数声明的区别。进一步看二者的区别:

(function(){
  //var f1,function f2(){}; //hoisting,被隐式提升的声明
 
  f1(); //ReferenceError: f1 is not defined
  f2();
 
  var f1 = function(){};
  function f2(){}
})();


上面代码中函数声明f2被提升,所以在前面调用f2是没问题的。虽然变量f1也被提升,但f1提升后的值为undefined,其真正的初始值是在执行到函数表达式处被赋予的。所以只有声明是被提升的。

名字解析顺序

javascript中一个名字(name)以四种方式进入作用域(scope),其优先级顺序如下:
1、语言内置:所有的作用域中都有 this 和 arguments 关键字
2、形式参数:函数的参数在函数作用域中都是有效的
3、函数声明:形如function foo() {}
4、变量声明:形如var bar;

名字声明的优先级如上所示,也就是说如果一个变量的名字与函数的名字相同,那么函数的名字会覆盖变量的名字,无论其在代码中的顺序如何。但名字的初始化却是按其在代码中书写的顺序进行的,不受以上优先级的影响。看代码:

(function(){
    var foo;
    console.log(typeof foo); //function
     
    function foo(){}
 
    foo = "foo";
    console.log(typeof foo); //string
})();

如果形式参数中有多个同名变量,那么最后一个同名参数会覆盖其他同名参数,即使最后一个同名参数并没有定义。

以上的名字解析优先级存在例外,比如可以覆盖语言内置的名字arguments。

命名函数表达式

可以像函数声明一样为函数表达式指定一个名字,但这并不会使函数表达式成为函数声明。命名函数表达式的名字不会进入名字空间,也不会被提升。

f();//TypeError: f is not a function
foo();//ReferenceError: foo is not defined
var f = function foo(){console.log(typeof foo);};
f();//function
foo();//ReferenceError: foo is not defined


命名函数表达式的名字只在该函数的作用域内部有效。

本文来源:http://www.bbyears.com/wangyezhizuo/83171.html