理解 JavaScript Scoping & Hoisting(二)


Posted in Javascript onNovember 18, 2015

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

Javascript 相关文章推荐
Extjs TriggerField在弹出窗口显示不出问题的解决方法
Jan 08 Javascript
javascript中使用css需要注意的地方小结
Sep 01 Javascript
自定义一个jquery插件[鼠标悬浮时候 出现说明label]
Jun 27 Javascript
js 数组去重的四种实用方法
Sep 09 Javascript
jQuery实现不断闪烁文字的方法
May 15 Javascript
js实现超简单的展开、折叠目录代码
Aug 28 Javascript
d3.js实现简单的网络拓扑图实例代码
Nov 06 Javascript
理解javascript中的Function.prototype.bind的方法
Feb 03 Javascript
jQuery实现文章图片弹出放大效果
Apr 06 jQuery
JS实现的A*寻路算法详解
Dec 14 Javascript
Vue.js构建你的第一个包并在NPM上发布的方法步骤
May 01 Javascript
浅谈vue中使用编辑器vue-quill-editor踩过的坑
Aug 03 Javascript
js立即执行函数: (function ( ){})( ) 与 (function ( ){}( )) 有什么区别?
Nov 18 #Javascript
z-blog SyntaxHighlighter 长代码无法换行解决办法(基于jquery)
Nov 18 #Javascript
JavaScript如何获取数组最大值和最小值
Nov 18 #Javascript
原生js模拟淘宝购物车项目实战
Nov 18 #Javascript
JavaScript统计网站访问次数的实现代码
Nov 18 #Javascript
javascript实现添加附件功能的方法
Nov 18 #Javascript
Jquery Mobile 自定义按钮图标
Nov 18 #Javascript
You might like
自制汽车收音机天线:收听广播的技巧和方法
2021/03/02 无线电
PHP加速 eAccelerator配置和使用指南
2009/06/05 PHP
PHP类中Static方法效率测试代码
2010/10/17 PHP
PHP实现自动登入google play下载app report的方法
2014/09/23 PHP
php使用PDO方法详解
2014/12/27 PHP
PHP实现的简单AES加密解密算法实例
2017/05/29 PHP
文本框获得焦点和失去焦点的判断代码
2012/03/18 Javascript
关于js new Date() 出现NaN 的分析
2012/10/23 Javascript
JS中的this变量的使用介绍
2013/10/21 Javascript
JavaScript获得页面base标签中url的方法
2015/04/03 Javascript
Bootstrap基础学习
2015/06/16 Javascript
jQuery插件datalist实现很好看的input下拉列表
2015/07/14 Javascript
用原生js统计文本行数的简单示例
2016/08/19 Javascript
Angularjs 设置全局变量的方法总结
2016/10/20 Javascript
vue展示dicom文件医疗系统的实现代码
2018/08/27 Javascript
详解Vue源码之数据的代理访问
2018/12/11 Javascript
小程序云开发教程如何使用云函数实现点赞功能
2019/05/18 Javascript
详解Vue中组件传值的多重实现方式
2019/08/16 Javascript
对python中return和print的一些理解
2017/08/18 Python
Python中将dataframe转换为字典的实例
2018/04/13 Python
用python3教你任意Html主内容提取功能
2018/11/05 Python
Python  Django 母版和继承解析
2019/08/09 Python
在Python中字符串、列表、元组、字典之间的相互转换
2019/11/15 Python
python-OpenCV 实现将数组转换成灰度图和彩图
2020/01/09 Python
浅谈ROC曲线的最佳阈值如何选取
2020/02/28 Python
关于Python turtle库使用时坐标的确定方法
2020/03/19 Python
基于HTML5 FileSystem API的使用介绍
2013/04/24 HTML / CSS
俄罗斯外国汽车和国产汽车配件网上商店:Движком
2020/04/19 全球购物
软件缺陷的分类都有哪些
2014/08/22 面试题
婚庆司仪主持词
2014/03/15 职场文书
中国入世承诺
2014/04/01 职场文书
小学生关于梦想的演讲稿
2014/08/22 职场文书
学习教师敬业奉献模范事迹材料思想汇报
2014/09/19 职场文书
青年教师个人总结
2015/02/11 职场文书
英语辞职信范文
2015/02/28 职场文书
小英雄雨来观后感
2015/06/09 职场文书