理解 JavaScript(二)

Scoping & Hoisting

var a = 1;

function foo() {
    if (!a) {
        var a = 2;
    }
    alert(a);
};

foo();

上面这段代码在运行时会产生什么结果?javascript

尽管对于有经验的程序员来讲这只是小菜一碟,不过我仍是顺着初学者常见的思路作一番描述:html

  1. 建立了全局变量 a,定义其值为 1java

  2. 建立了函数 foo程序员

  3. foo 的函数体内,if 语句将不会执行,由于 !a 会将变量 a 转变成布尔的假值,也就是 false闭包

  4. 跳过条件分支,alert 变量 a,最终的结果应该是输出 1函数

嗯,看起来无懈可击的推理啊,但让人惊讶的是:答案居然是 2!为何?this

别着急,我会解释给你听。首先我要告诉你这不是什么错误,而是 JavaScript 语言解释器的一个(非官方的)特性,某人(Ben Cherry)把这个特性叫作:Hoisting(目前还没有有标准的翻译,比较常见的是提高)。翻译


声明与定义

为了理解 Hoisting,咱们先来看一个简单的状况:code

var a = 1;

你是否想过,上面这句代码在运行的时候到底发生了什么?
你是否知道,就这句代码而言,“声明变量 a” 和 “定义变量 a”这两个说法哪个才是正确的?htm

  • 下例叫作 “声明变量”:

var a;
  • 下例叫作 “定义变量”:

var a = 1;
  • 声明:是指你声称某样东西的存在,好比一个变量或一个函数;但你没有说明这样东西究竟是什么,仅仅是告诉解释器这样东西存在而已;

  • 定义:是指你指明了某样东西的具体实现,好比一个变量的值是多少,一个函数的函数体是什么,确切的表达了这样东西的意义。

总结一下:

var a;            // 这是声明
a = 1;            // 这是定义(赋值)
var a = 1;        // 合二为一:声明变量的存在并赋值给它

重点来了:当你觉得你只作了一件事情的时候(var a = 1),实际上解释器把这件事情分解成了两个步骤,一个是声明(var a),另外一个是定义(a = 1)。

这和 Hoisting 有何关系?

回到最开始的那个使人困惑的例子,我告诉你解释器是如何分析你的代码的:

var a;
a = 1;

function foo() {
    var a;        // 关键在这里
    if (!a) {
        a = 2;
    }
    alert(a);     // 此时的 a 并不是函数体外的那个全局变量
}

如代码所示,在进入函数体后解释器声明了新的变量 a,当时其值为 undefined,因而 if 语句条件判断结果为真,接着为新的变量 a 赋值为 2。你若不相信能够在函数体外面 alert(a),而后再执行 foo() 对比一下结果就知道了。


Scoping(做用域)

有人可能会问了:“为何不是在 if 语句内声明变量 a?”

由于 JavaScript 没有块级做用域(Block Scoping),只有函数做用域(Function Scoping),因此说不是看见一对花括号 {} 就表明产生了新的做用域,和 C 不同!

当解析器读到 if 语句的时候,它发现此处有一个变量声明和赋值,因而解析器会将其声明提高至当前做用域的顶部(这是默认行为,而且没法更改),这个行为就叫作 Hoisting

OK,你们都懂了,你懂了吗……

懂了不表明就会用了,就拿最开始的例子来讲,若是我就是想要 alert(a) 出那个 1 可咋整呢?

建立新的做用域

alert(a) 在执行的时候,会去寻找变量 a 的位置,它从当前做用域开始向上(或者说向外)一直查找到顶层做用域为止,如果找不到就报 undefined

由于在 alert(a) 的同级做用域里,咱们再次声明了本地变量 a,因此它报 2;因此咱们能够把本地变量 a 的声明向下(或者说向内)移动,这样 alert(a) 就找不到它了。

记住:JavaScript 只有函数做用域!

var a = 1;

function foo() {
    if (!a) {
        (function() {        // 这是上一篇说到过的 IIFE,它会建立一个新的函数做用域
            var a = 2;       // 而且该做用域在 foo() 的内部,因此 alert 访问不到
        }());                // 不过这个做用域能够访问上层做用域哦,这就叫:“闭包”
    };
    alert(a);
};

foo();

你或许在无数的 JavaScript 书籍和文章里读到过:“请始终保持做用域内全部变量的声明放置在做用域的顶部”,如今你应该明白为何有此一说了吧?由于这样能够避免 Hoisting 特性给你带来的困扰(我不是很情愿这么说,由于 Hoisting 自己并无什么错),也能够很明确的告诉全部阅读代码的人(包括你本身)在当前做用域内有哪些变量能够访问。可是,变量声明的提高并不是 Hoisting 的所有。在 JavaScript 中,有四种方式可让命名进入到做用域中(按优先级):

  1. 语言定义的命名:好比 this 或者 arguments,它们在全部做用域内都有效且优先级最高,因此在任何地方你都不能把变量命名为 this 之类的,这样是没有意义的

  2. 形式参数:函数定义时声明的形式参数会做为变量被 hoisting 至该函数的做用域内。因此形式参数是本地的,不是外部的或者全局的。固然你能够在执行函数的时候把外部变量传进来,可是传进来以后就是本地的了

  3. 函数声明:函数体内部还能够声明函数,不过它们也都是本地的了

  4. 变量声明:这个优先级其实仍是最低的,不过它们也都是最经常使用的

另外,还记得以前咱们讨论过 声明定义 的区别吧?当时我并无说为何要理解这个区别,不过如今是时候了,记住:

Hosting 只提高了命名,没有提高定义

这一点和咱们接下来要讲到的东西息息相关,请看:


函数声明与函数表达式的差异

先看两个例子:

function test() {
    foo();

    function foo() {
        alert("我是会出现的啦……");
    }
}

test();
function test() {
    foo();

    var foo = function() {
        alert("我不会出现的哦……");
    }
}

test();

同窗,在了解了 Scoping & Hoisting 以后,你知道怎么解释这一切了吧?

在第一个例子里,函数 foo 是一个声明,既然是声明就会被提高(我特地包裹了一个外层做用域,由于全局做用域须要你的想象,不是那么直观,可是道理是同样的),因此在执行 foo() 以前,做用域就知道函数 foo 的存在了。这叫作函数声明(Function Declaration),函数声明会连通命名和函数体一块儿被提高至做用域顶部。

然而在第二个例子里,被提高的仅仅是变量名 foo,至于它的定义依然停留在原处。所以在执行 foo() 以前,做用域只知道 foo 的命名,不知道它究竟是什么,因此执行会报错(一般会是:undefined is not a function)。这叫作函数表达式(Function Expression),函数表达式只有命名会被提高,定义的函数体则不会。

尾记:Ben Cherry 的原文解释的更加详细,只不过是英文而已。我这篇是借花献佛,主要是更浅显的解释给初学者听,若要看更多的示例,请移步原做,谢谢。

相关文章
相关标签/搜索