理解 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 相关文章推荐
鼠标右击事件代码(asp.net后台)
Jan 27 Javascript
js实现Select列表各项上移和下移的方法
Aug 14 Javascript
JS实现下拉菜单赋值到文本框的方法
Aug 18 Javascript
RequireJS入门一之实现第一个例子
Sep 30 Javascript
整理JavaScript创建对象的八种方法
Nov 03 Javascript
js推箱子小游戏步骤代码解析
Jan 10 Javascript
如何去除富文本中的html标签及vue、react、微信小程序中的过滤器
Nov 21 Javascript
基于Vue+elementUI实现动态表单的校验功能(根据条件动态切换校验格式)
Apr 04 Javascript
JavaScript遍历查找数组中最大值与最小值的方法示例
May 24 Javascript
浅谈v-for 和 v-if 并用时筛选条件方法
Nov 07 Javascript
15分钟学会vue项目改造成SSR(小白教程)
Dec 17 Javascript
JavaScript 函数用法详解【函数定义、参数、绑定、作用域、闭包等】
May 12 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
一个改进的UBB类
2006/10/09 PHP
安装PHP可能遇到的问题“无法载入mysql扩展” 的解决方法
2007/04/16 PHP
PHP查询数据库中满足条件的记录条数(两种实现方法)
2013/01/29 PHP
CodeIgniter生成网站sitemap地图的方法
2013/11/13 PHP
[转]JS宝典学习笔记
2007/02/07 Javascript
js 创建快捷方式的代码(fso)
2010/11/19 Javascript
JQuery获取文本框中字符长度的代码
2011/09/29 Javascript
jQuery插件原来如此简单 jQuery插件的机制及实战
2012/02/07 Javascript
让页面上两个div中的滚动条(滑块)同步运动示例
2013/08/07 Javascript
JS性能优化笔记搜索整理
2013/08/21 Javascript
JQuery伸缩导航练习示例
2013/11/13 Javascript
javascript实现跨域的方法汇总
2015/06/25 Javascript
Javascript 基础---Ajax入门必看
2016/07/06 Javascript
轻松掌握JavaScript策略模式
2016/08/25 Javascript
简易的JS计算器实现代码
2016/10/18 Javascript
Bootstrap基本组件学习笔记之分页(12)
2016/12/08 Javascript
JavaScript实现的商品抢购倒计时功能示例
2017/04/17 Javascript
vue富文本编辑器组件vue-quill-edit使用教程
2018/09/21 Javascript
vue-cli脚手架搭建的项目去除eslint验证的方法
2018/09/29 Javascript
JavaScript设计模式之代理模式实例分析
2019/01/16 Javascript
小程序使用分包的示例代码
2020/03/23 Javascript
js点击事件的执行过程实例分析【冒泡与捕获】
2020/04/11 Javascript
vue实现购物车功能(商品分类)
2020/04/20 Javascript
[00:56]PWL开团时刻DAY8——追追追追追!
2020/11/09 DOTA
利用Python和OpenCV库将URL转换为OpenCV格式的方法
2015/03/27 Python
python binascii 进制转换实例
2019/06/12 Python
Python求两点之间的直线距离(2种实现方法)
2019/07/07 Python
Python requests.post方法中data与json参数区别详解
2020/04/30 Python
简述 Python 的类和对象
2020/08/21 Python
Expedia西班牙:预订酒店、机票、旅行和廉价度假套餐
2019/04/10 全球购物
Linux中如何用命令创建目录
2016/12/02 面试题
中专生学习生活的自我评价分享
2013/10/27 职场文书
校长竞聘演讲稿
2014/05/16 职场文书
民事诉讼答辩状范文
2015/05/21 职场文书
2016清明节森林防火广播稿
2015/12/17 职场文书
Vue中foreach数组与js中遍历数组的写法说明
2021/06/05 Vue.js