JavaScript Scoping & Hoisting
var a = 1; function foo() { if (!a) { var a = 2; } alert(a); }; foo();
上面这段代码在运行时会产生什么结果?
尽管对于有经验的程序员来说这只是小菜一碟,不过我还是顺着初学者常见的思路做一番描述:
-
创建了全局变量
a
,定义其值为1
-
创建了函数
foo
-
在
foo
的函数体内,if
语句将不会执行,因为!a
会将变量a
转变成布尔的假值,也就是false
-
跳过条件分支,
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
,当时其值为 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 中,有四种方式可以让命名进入到作用域中(按优先级):
-
语言定义的命名:比如
this
或者arguments
,它们在所有作用域内都有效且优先级最高,所以在任何地方你都不能把变量命名为this
之类的,这样是没有意义的 -
形式参数:函数定义时声明的形式参数会作为变量被 hoisting 至该函数的作用域内。所以形式参数是本地的,不是外部的或者全局的。当然你可以在执行函数的时候把外部变量传进来,但是传进来之后就是本地的了
-
函数声明:函数体内部还可以声明函数,不过它们也都是本地的了
-
变量声明:这个优先级其实还是最低的,不过它们也都是最常用的
另外,还记得之前我们讨论过 声明 和 定义 的区别吧?当时我并没有说为什么要理解这个区别,不过现在是时候了,记住:Hosting 只提升了命名,没有提升定义
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),函数表达式只有命名会被提升,定义的函数体则不会。
原文发布时间为:2018年03月19日
原文作者:张泽立
本文来源:开源中国 如需转载请联系原作者
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
PHP执行慢分析工具xdebug + webgrind
经常碰到用户PHP网站打开速度慢,运维环境配置的锅?PHP代码的锅?当你发现程序变慢,但有时我们不知道具体是哪块代码执行慢(排除环境问题),或者哪个方法占用了太长的执行时间,这时我们就需要一个调试工具,来帮我们记录程序执行过程中的一些具体信息,包括类、方法名、执行时间、次数等信息,今天介绍的工具是xdebug以及他的结果分析展示工具webgrind。 xdebug安装cd /root/oneinstack/srcwget https://xdebug.org/files/xdebug-2.5.5.tgztar xzf xdebug-2.5.5.tgzcd xdebug-2.5.5/usr/local/php/bin/phpize./configure --with-php-config=/usr/local/php/bin/php-configmake && make installphp加载xdebug模块 cat > /usr/local/php/etc/php.d/ext-xdebug.ini << EOF[xdebug]zend_extensi...
- 下一篇
前端自动化构建工具之webpack入门——简单入门
写在前面 这篇博客也是参照别人的webpack入门,加上自己的一些操作,还有我在此过程中遇到的困难。如果是已经入门的或者这方面的高手,请绕行,也请勿吐槽。 正文开始 我们为什么要学习webpack 首先,我们为什么要学习前端自动化构建工具,我开始了解自动化构建工具的时候觉得很麻烦,要配环境,还要敲命令,就为了把那些文件都打包在一个叫bundle.js的文件里?那有什么意义? 看了半天文档,也没明白到底有啥好处。于是去参照了前辈的博客。 模块化,让我们可以把复杂的程序细化为小的文件; 类似于TypeScript这种在JavaScript基础上拓展的开发语言:使我们能够实现目前版本的JavaScript不能直接使用的特性,并且之后还能转换为JavaScript文件使浏览器可以识别; Scss,less等CSS预处理器等等。 好了,现在我们明白,为什么要使用他了。 webpack工作方式 Webpack的工作方式是:把你的项目当做一个整体,通过一个给定的主文件(如:index.js),Webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包为一个(或多个...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
-
Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
推荐阅读
最新文章
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS6,CentOS7官方镜像安装Oracle11G
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Hadoop3单机部署,实现最简伪集群