JavaScript的作用域和提升机制

你知道下面的JavaScript代码执行时会输出什么吗?

1
2
3
4
5
6
7
8
var foo = 1;
function bar() {
    if (!foo) {
        var foo = 10;
    }
    alert(foo);
}
bar();

----相当于:

var foo = 1;

function(){

var foo;

if(!foo){

foo=10;

}

alert(foo);

}

bar();

答案是“10”,吃惊吗?那么下面的可能会真的让你大吃一惊:

1
2
3
4
5
6
7
8
var a = 1;
function b() {
    a = 10;
    return;
    function a() {}
}
b();
alert(a);

-----相当于:

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

这里浏览器会弹出“1”。怎么回事?这似乎看起来是奇怪,未知,让人混淆的,但这实际上是这门语言一个强大和富有表现力的特性。我不知道这一特性行
为是否有标准名字,但我喜欢这个术语“提升(hoisting)”。本文试图揭示这一特性的机制,但首先让我们链接JavaScript的作用域。

JavaScript中的作用域(scope)

JavaScript初学者最容易混淆的地方是作用域。实际上,不只是初学者。我遇到过许多经验丰富的JavaScript程序员,却不完全明白作用域。JavaScript的作用域如此容易混淆的原因是它看起来很像C家族的语言(类C语言)。考虑下面的C程序:

1
2
3
4
5
6
7
8
9
10
#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和C家族的语言有块级作用域(block-level scope)。当控制流进入一个块,比如if语句,新的变量会在块作用域里声明,不会对外面作用域产生印象。这不适用于JavaScript。在Firebug里运行下面的代码:

1
2
3
4
5
6
7
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函数的灵活性,有一个解决方案。你若你必须要在函数内部创建一个临时作用域,像下面这样做:

1
2
3
4
5
6
7
8
9
10
function foo() {
    var x = 1;
    if (x) {
        (function () {
            var x = 2;
            // 此处省略一万个字
        }());
    }
    // x 仍然是 1.
}

这方法实际上相当灵活,可以在你需要临时作用域的时候随意使用,不局限于块级语句内部。然而,我强烈建议你花时间去了解和欣赏JavaScript的作用域。它非常强大,是这门语言中我最喜欢的特性之一。如果你了解作用域,将更容易理解提升。

声明,名字和提升(Hoisting)

在JavaScript中,作用域中的名字(属性名)有四种基本来源:

  1. 语言定义:默认所有作用域都有属性名this和arguments。
  2. 形参:函数可能有形式参数,其作用域是整个函数体内部。
  3. 函数声明:类似于function foo() {}这种形式。
  4. 变量声明:var foo;这种形式的代码。

函数声明和变量声明总是被JavaScript解释器无形中移动到(提升)包含他们的作用域顶部。函数参数和语言定义的名称明显总是存在。这意味着像下面的代码:

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

实际上被解释为像下面这样:

1
2
3
4
5
function foo() {
    var x;
    bar();
    x = 1;
}

无论包含声明的代码行是否会被执行,上面的过程都会发生。下面的两个函数是等价的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo() {
    if (false) {
        var x = 1;
    }
    return;
    var y = 1;
}
 
function foo() {
    var x, y;
    if (false) {
        x = 1;
    }
    return;
    y = 1;
}

注意变量声明中赋值的过程不会被提升。仅仅变量名字被提升了。这不适用于函数声明,整个函数体也会提升。但不要忘记有两种声明函数的方法。考虑下面的JavaScript代码:

1
2
3
4
5
6
7
8
9
10
11
function test() {
    foo();                     // 类型错误 “foo 不是一个函数”
    bar();                     // “这能运行”
    var foo = function () {    // 将函数表达式赋值给本地变量“foo”
        alert("this won't run!");
    }
    function bar() {           //  'bar'函数声明,分配“bar”名字
        alert("this will run!");
    }
}
test();

在这种情况下,仅仅函数声明的函数体被提升到顶部。名字“foo”被提升,但后面的函数体,在执行的时候才被指派。
这是全部的基本提升,看起来并不复杂和让人混淆。当然,这是JavaScript,在某些特殊性况下会更复杂一点。

名字解析顺序

需要记住的最重要的特殊情况是名字的解析顺序。记住作用域中的名字有四种来源。上面我列出他们的顺序是他们被解析的顺序。一般来说,如果一个名字已
经被定义过,那么它不会在被其他有相同名字的属性重写。这意味着函数声明优先于变量声明。这并不意味着为名字赋值的过程将不工作,仅仅声明的过程会被忽
略。有几个例外情况:

  • 函数的内置变量arguments比较奇怪。它看起来是在普通的函数参数之后才声明,其实是在函数声明之前。如果参数里面有名称为arguments的参数,它会比内置的那个优先级高,即使它是undefined。所以不要使用arguments作为为函数参数的名称。
  • 尝试使用this作为标示符的地方都会造成一个语法错误。这是一个很好的特性。
  • 如果多个参数具有相同的名字,那么最后一个参数会优先于先前的,即使它是undefined。

命名函数表达式

你可以在函数表达式给中给函数命名,用这样的语法不能完成一个函数声明,下面有一些代码来说明我的意思:

1
2
3
4
5
6
7
8
9
10
11
12
13
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"

编码时如何使用这些知识

现在你应该理解了作用域和提升(hoisting),那么我们在编写JavaScript的时候应该怎么做呢?最重要的事情就是始终用var表达式来声明你的变量。我强烈建议你使用单var模式(single var)。如果你强迫自己做到这一点,你将永远不会遇到任何与变量提升相关的混乱的问题。但是这样做也让我们很难跟踪那些在当前作用域中实际上已经声明的变量。我建议你使用JSLint和声明一次原则来进行实际操作,如果你这样做了,你的代码应该会看起来像这样:

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

标准给出的解释

我翻了翻ECMAScript标准,想直接了解这些东西是如何工作的,发现效果不错。这里我不得不说关于变量声明和作用域(第12.2.2节)的内容:

如果在一个函数中声明变量,这些变量就被定义在了在该函数的函数作用域中,见第10.1.3所述。不然它们就是被定义在全局的作用域内(即,它们被
创建为全局对象的成员,见第10.1.3所述),当进入执行环境的时候,变量就被创建。一个语句块不能定义一个新的作用域。只有一个程序或者函数声明能够
产生一个新的作用域。创建变量时,被初始化为undefined。如果变量声明语句里面带有赋值操作,则赋值操作只有被执行到声明语句的时候才会发生,而
不是创建的时候。

我希望这篇文章阐明了对JavaScript程序员来说最常见的迷惑问题,我试图讲的尽可能详尽,以避免造成更多的迷惑,如果我说错了或者有大的遗漏,请通知我。

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

JavaScript的作用域和提升机制的更多相关文章

  1. JavaScript 函数作用域的“提升”现象

    在JavaScript当中,定义变量通过var操作符+变量名.但是不加 var 操作符,直接赋值也是可以的.例如 : message = "hello JavaScript ! " ...

  2. 浅谈JavaScript 函数作用域当中的“提升”现象

    在JavaScript当中,定义变量通过var操作符+变量名.但是不加 var 操作符,直接赋值也是可以的. 例如 : message = "hello JavaScript ! " ...

  3. 基础系列(1)之干掉JavaScript变量作用域

     今天去某顺公司面试,发现一些基础知识都不记得了,于是乎决定把js基础系列的全部梳理一遍,今天就整理下js变量作用域的相关基础知识点,配合最常遇到的笔试题阐述. 题一: var g = "a ...

  4. javasrcipt的作用域和闭包(二)续篇之:函数内部提升机制与Variable Object

    一个先有鸡还是先有蛋的问题,先看一段代码: a = 2; var a; console.log(a); 通常我们都说JavaScript代码是由上到下一行一行执行,但实际这段代码输出的结果是2.但这段 ...

  5. 解读JavaScript中的Hoisting机制(js变量声明提升机制)

    hoisting机制:javascript的变量声明具有hoisting机制,JavaScript引擎在执行的时候,会把所有变量的声明都提升到当前作用域的最前面. 知识点一:javascript是没有 ...

  6. 漫谈JavaScript中的提升机制(Hoisting)

    前言 刚接触到JavaScript的时候,便知道JavaScript是按顺序执行的,是如浏览器的解析DOM树一样的流程,解析DOM结构的时候,如果遇到JS脚本或者外联脚本便会停止解析,继续下载脚本之后 ...

  7. JavaScript的作用域与作用域链

    作用域 作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期.可以说,变量和函数在什么时候可以用,什么时候被摧毁,这都与作用域有关. JavaScript中,变量的作用域有全局 ...

  8. Javascript的作用域、作用域链以及闭包

    一.javascript中的作用域 ①全局变量-函数体外部进行声明 ②局部变量-函数体内部进行声明 1)函数级作用域 javascript语言中局部变量不同于C#.Java等高级语言,在这些高级语言内 ...

  9. JavaScript之作用域与作用域链

    今天是2016的第一天,我们得扬帆起航踏上新的征程了.此篇阐述JavaScript中很重要的几个概念:作用域与作用域链及相关知识点. 我们先从变量与作用域的行为关系开始讨论. 变量作用域 JavaSc ...

随机推荐

  1. Linux计划任务,自动删除n天前的旧文件【转】

    转自:http://blog.csdn.net/jehoshaphat/article/details/51244237 转载地址:http://yaksayoo.blog.51cto.com/510 ...

  2. 分析Linux内核创建一个新进程的过程【转】

    转自:http://www.cnblogs.com/MarkWoo/p/4420588.html 前言说明 本篇为网易云课堂Linux内核分析课程的第六周作业,本次作业我们将具体来分析fork系统调用 ...

  3. 织梦系统中出现DedeTag Engine Create File False提示原因及解决方法

    今天更新网站时dedecms系统时,遇到一个问题:DedeTag Engine Create File False  出现这样的提示. 其实这也不算是什么错误,我个人觉得最重要的一点就是根目录下没有给 ...

  4. 给我发邮件(qq)| 和我联系

    qq邮箱开放平台(只能是qq对qq): 简单点的发邮件: 和我联系

  5. [ios]离屏渲染优化

    原文链接:https://mp.weixin.qq.com/s?__biz=MjM5NTIyNTUyMQ==&mid=2709544818&idx=1&sn=62d0d2e9a ...

  6. 【转】启动 Eclipse 弹出“Failed to load the JNI shared library jvm.dll”错误的解决方法! .

    转载地址:http://blog.csdn.net/zyz511919766/article/details/7442633 原因1:给定目录下jvm.dll不存在. 对策:(1)重新安装jre或者j ...

  7. EFsql笔记

    like的语法 string[] cities = { "London", "Madrid" }; IQueryable<Customer> cus ...

  8. Wireless Network

    Wireless Network Time Limit: 10000MS Memory Limit: 65536K Total Submissions: 19626 Accepted: 8234 De ...

  9. Dungeon Master 分类: 搜索 POJ 2015-08-09 14:25 4人阅读 评论(0) 收藏

    Dungeon Master Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 20995 Accepted: 8150 Descr ...

  10. 第十二届浙江省大学生程序设计大赛-Beauty of Array 分类: 比赛 2015-06-26 14:27 12人阅读 评论(0) 收藏

    Beauty of Array Time Limit: 2 Seconds Memory Limit: 65536 KB Edward has an array A with N integers. ...