Scoping & Hoisting

例:

var a = 1;

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

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

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

  1. 创建了全局变量 a,定义其值为 1
  2. 创建了函数 foo
  3. 在 foo 的函数体内,if 语句将不会执行,因为 !a 会将变量 a 转变成布尔的假值,也就是 false
  4. 跳过条件分支,alert 变量 a,最终的结果应该是输出 1

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

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


声明与定义

为了理解 Hoisting,我们先来看一个简单的情况:

var a = 1;

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

  • 下例叫做 “声明变量”:
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,而无论 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 的原文解释的更加详细,只不过是英文而已。我这篇是借花献佛,主要是更浅显的解释给初学者听,若要看更多的示例,请移步原作,谢谢。

转载地址:http://segmentfault.com/a/1190000000348228

JS函数声明与定义,作用域,函数声明与表达式的区别的更多相关文章

  1. Scala类型声明与定义、函数定义、流程控制、异常处理

    Scala源代码被编译成Java字节码,所以它可以运行于JVM之上,并可以调用现有的Java类库. Scala的基础类型基本与javascript一致. Scala的数据类型全部相同于Java中,具有 ...

  2. 一个经典的js中关于块级作用域和声明提升的问题

    function functions(flag) { if (flag) { function getValue() { return 'a'; } } else { function getValu ...

  3. JS基础语法---函数---介绍、定义、函数参数、返回值

    函数: 把一坨重复的代码封装,在需要的时候直接调用即可 函数的作用: 代码的重用 函数需要先定义,然后才能使用 函数名字:要遵循驼峰命名法 函数一旦重名,后面的会把前面的函数覆盖 Ctrl +鼠标左键 ...

  4. js 面向对象中,定义一个函数的过程

    定义一个函数做的两件事:1: 实例化一个Function对象:2: 实例化一个Object对象,并给该函数扩展prototype属性指向这个构造函数 大致过程如图所示: 每一种引用类型(函数,对象,数 ...

  5. JS基础语法---函数的其他定义方式

    函数的其他定义方式 函数声明 函数表达式:把一个函数给一个变量,此时形成了函数表达式 函数调用 函数的自调用   命名函数:函数如果有名字,就是命名函数 匿名函数:函数如果没有名字,就是匿名函数   ...

  6. JavaScript函数的多种定义方法

    缘起 javascript和其他编程语言相比比较随意,所以javascript代码中充满各种奇葩的写法,有时雾里看花,当然,能理解各型各色的写法也是对 javascript语言特性更进一步的深入理解, ...

  7. 关于C++的变量和类的声明和定义

    什么是变量?变量或者叫对象,是一个有具名的.可以供程序操作的存储空间.这里具名是指变量是有名字的,可供操作是指能进行加减乘除或者输入输出等操作,存储空间则是指有一块属于它的内存空间. 为了便于说明,标 ...

  8. C++中声明和定义的区别

    声明 这有一个与这个名字相关的东西,并且它是这个类型的,告诉编译器我要使用它,并期待它定义在某一个地方. 定义 定义是指提供所有必要的信息(占用内存大小),使其能够创建整个实体. 我们必须明白的: 一 ...

  9. C语言中声明和定义详解(待看。。

    变量声明和变量定义 变量定义:用于为变量分配存储空间,还可为变量指定初始值.程序中,变量有且仅有一个定义. 变量声明:用于向程序表明变量的类型和名字. 定义也是声明,extern声明不是定义 定义也是 ...

  10. C++内联函数与宏定义

    用内联取代宏: 1.内联可调试: 2.可进行类型安全检查或自动类型转换: 3.可访问成员变量. 另外,定义在类声明中的成员函数自动转化为内联函数. 文章(一) 内联函数与宏定义 在C中,常用预处理语句 ...

随机推荐

  1. 使用TheFolderSpy监控文件夹的变化-邮件通知

    一.概述 当我们的文档或者代码文件发布在公网.共享文件夹中,其他用户具备访问或修改的权限时,就存在文档被覆盖或删除的分享.另外一个典型的场景,发布在Web服务器上的网页文件,在网站版本不更新的时间,服 ...

  2. Socket通信的Demo

    https://blog.csdn.net/shankezh/article/details/70763579

  3. 腾讯大数据之TDW计算引擎解析——Shuffle

    转自 https://www.csdn.net/article/2014-05-19/2819831-TDW-Shuffle/1 摘要:腾讯分布式数据仓库基于开源软件Hadoop和Hive进行构建,T ...

  4. Python环境搭建详解(Window平台)

    前言 Python,是一种面向对象的解释型计算机程序设计语言,是纯粹的自由软件,Python语法简洁清晰,特色是强制用空白符作为语句缩进,具有丰富和强大的库,它常被称为胶水语言. Python是一种解 ...

  5. AndroidStudio环境安装与配置

    前言 大家好,给大家带来AndroidStudio环境安装与配置的概述,希望你们喜欢 AndroidStudio IDE下载 我们选择用Android Studio开发Android的App,Andr ...

  6. iOS开发总结——项目目录结构

    1.前言 清晰的项目目录结构有利于项目的开发,同时也是软件架构的一部分,所以,项目开发之初搭建项目的目录结构很重要.刚转iOS时,自己并不知道如何搭建App的项目目录,在参与开发两个应用后,结合Web ...

  7. Devops流程规范

    芯盾时代_Devops_Docker操作说明及使用规范 北京芯盾时代科技有限公司 2019年1月 修订记录 版本号 修订人 修订日期 修订描述 v0.1 芯盾 2019/1/15 初次创建 v0.2 ...

  8. Linux入门搭建可视化桌面环境小合集virtual box centOS7.10

    常用命令: 关联主机与虚拟机(方便文件共享): # sudo mount -t vboxsf share(主机文件夹名) /usr/share(虚拟机内自创) Linux shell进入root模式: ...

  9. [EXP]McAfee ePO 5.9.1 - Registered Executable Local Access Bypass

    # Exploit Title: McAfee ePO 5.9.1 Registered Executable Local Access Bypass # Date: 2019-03-07 # Exp ...

  10. 基于 CGLIB 库的动态代理机制

    之前的文章我们详细的介绍了 JDK 自身的 API 所提供的一种动态代理的实现,它的实现相对而言是简单的,但是却有一个非常致命性的缺陷,就是只能为接口中的方法完成代理,而委托类自己的方法或者父类中的方 ...