Javascript作用域和作用域链原理解析


Posted in Javascript onMarch 03, 2020

作用域和作用域链在Javascript和很多其它的编程语言中都是一种基础概念。但很多Javascript开发者并不真正理解它们,但这些概念对掌握Javascript至关重要。

正确的去理解这个概念有利于你去写更好,更高效和更简洁的代码,让你成为一个更优秀的Javascript开发者。

因此,在本文中,我将会向大家解释清楚什么是作用域和作用域链,以及Javascript引擎在内部是如何通过它们操作和查找变量的。

事不宜迟,正文开始 :)

什么是作用域

Javascript中的作用域说的是变量的可访问性和可见性。也就是说整个程序中哪些部分可以访问这个变量,或者说这个变量都在哪些地方可见。

为什么作用域很重要

作用域最为重要的一点是安全。变量只能在特定的区域内才能被访问,有了作用域我们就可以避免在程序其它位置意外对某个变量做出修改。

作用域也会减轻命名的压力。我们可以在不同的作用域下面定义相同的变量名。

作用域的类型

Javascript中有三种作用域:

  • 全局作用域;
  • 函数作用域;
  • 块级作用域;

1. 全局作用域

任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问。例如:

// 全局变量
var greeting = 'Hello World!';
function greet() {
 console.log(greeting);
}
// 打印 'Hello World!'
greet();

2. 函数作用域

函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问。例如:

function greet() {
 var greeting = 'Hello World!';
 console.log(greeting);
}
// 打印 'Hello World!'
greet();
// 报错: Uncaught ReferenceError: greeting is not defined
console.log(greeting);

3. 块级作用域

ES6引入了let和const关键字,和var关键字不同,在大括号中使用let和const声明的变量存在于块级作用域中。在大括号之外不能访问这些变量。看例子:

{
 // 块级作用域中的变量
 let greeting = 'Hello World!';
 var lang = 'English';
 console.log(greeting); // Prints 'Hello World!'
}
// 变量 'English'
console.log(lang);
// 报错:Uncaught ReferenceError: greeting is not defined
console.log(greeting);

上面代码中可以看出,在大括号内使用var声明的变量lang是可以在大括号之外访问的。使用var声明的变量不存在块级作用域中。

作用域嵌套

像Javascript中函数可以在一个函数内部声明另一个函数一样,作用域也可以嵌套在另一个作用域中。请看例子:

var name = 'Peter';
function greet() {
 var greeting = 'Hello';
 {
  let lang = 'English';
  console.log(`${lang}: ${greeting} ${name}`);
 }
}
greet();

这里我们有三层作用域嵌套,首先第一层是一个块级作用域(let声明的),被嵌套在一个函数作用域(greet函数)中,最外层作用域是全局作用域。

词法作用域

词法作用域(也叫静态作用域)从字面意义上看是说作用域在词法化阶段(通常是编译阶段)确定而非执行阶段确定的。看例子:

let number = 42;
function printNumber() {
 console.log(number);
}
function log() {
 let number = 54;
 printNumber();
}
// Prints 42
log();

上面代码可以看出无论printNumber()在哪里调用console.log(number)都会打印42。动态作用域不同,console.log(number)这行代码打印什么取决于函数printNumber()在哪里调用。

如果是动态作用域,上面console.log(number)这行代码就会打印54。

使用词法作用域,我们可以仅仅看源代码就可以确定一个变量的作用范围,但如果是动态作用域,代码执行之前我们没法确定变量的作用范围。

像C,C++,Java,Javascript等大多数编程语言都支持静态作用域。Perl 既支持动态作用域也支持静态作用域。

作用域链

当在Javascript中使用一个变量的时候,首先Javascript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。

如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错。

例如:

let foo = 'foo';
function bar() {
 let baz = 'baz';
 // 打印 'baz'
 console.log(baz);
 // 打印 'foo'
 console.log(foo);
 number = 42;
 console.log(number); // 打印 42
}
bar();

当函数bar()被调用,Javascript引擎首先在当前作用域下寻找变量baz,然后寻找foo变量但发现在当前作用域下找不到,然后继续在外部作用域寻找找到了它(这里是在全局作用域找到的)。

然后将42赋值给变量number。Javascript引擎会在当前作用域以及外部作用域下一步步寻找number变量(没找到)。

如果是在非严格模式下,引擎会创建一个number的全局变量并把42赋值给它。但如果是严格模式下就会报错了。

结论:当使用一个变量的时候,Javascript引擎会循着作用域链一层一层往上找该变量,直到找到该变量为止。

作用域和作用域链是如何工作的

以上内容已经讲解了作用域,作用域的类型,现在让我们看下Javascript引擎是如何确定变量的作用域链和如何去查找变量的。

要想理解Javascript是如何进行变量查找的,必须要了解Javascript中词法环境这个概念(请参考:理解Javascript中的执行上下文和执行栈)。

什么是词法环境

所谓词法环境就是一种标识符—变量映射的结构(这里的标识符指的是变量/函数的名字,变量是对实际对象[包含函数和数组类型的对象]或基础数据类型的引用)。

简单地说,词法环境是Javascript引擎用来存储变量和对象引用的地方。

注意:不要混淆了词法环境和词法作用域,词法作用域是在代码编译阶段确定的作用域(译者注:一个抽象的概念),而词法环境是Javascript引擎用来存储变量和对象引用的地方(译者注:一个具象的概念)。

一个词法环境就像下面这样:

lexicalEnvironment = {
 a: 25,
 obj: <ref. to the object>
}

只有当该作用域的代码被执行的时候,引擎才会为那个作用域创建一个新的词法环境。词法环境还会记录所引用的外部词法环境(即外部作用域)。例:

lexicalEnvironment = {
 a: 25,
 obj: <ref. to the object>
 outer: <outer lexical environemt>
}

Javascript引擎是如何进行变量查找的

现在我们已经知道了作用域,作用域链和词法环境的概念,现在让我们看下Javascript引擎是如何利用词法环境来确定作用域和作用域链的。

结合例子我们来理解上面的这些概念:

let greeting = 'Hello';
function greet() {
 let name = 'Peter';
 console.log(`${greeting} ${name}`); // Hello Peter
}
greet();
{
 let greeting = 'Hello World!'
 console.log(greeting); // Hello World!
}

上述代码加载后,首先会创建一个全局词法环境,其中包含在全局范围内声明的变量和函数。像下面这样:

globalLexicalEnvironment = {
 greeting: 'Hello'
 greet: <ref. to greet function>
 outer: <null>
}

这里的outer字段(也就是外部词法环境)被设置为了null,是因为全局词法环境已经是最顶层的词法环境了。

然后,我们调用了greet()函数,然后一个新的词法环境会被被创建:

functionLexicalEnvironment = {
 name: 'Peter'
 outer: <globalLexicalEnvironment>
}

这里的outer字段被设置为了globalLexicalEnvironment,是因为他的外部作用域就是全局作用域。

然后,执行console.log(`${greeting} ${name}`)这行代码,Javascript引擎首先在当前函数的词法环境中寻找变量greeting和name,但只找到了name,没找到greeting。然后继续在上层的词法环境中找greeting(这里是全局作词法环境)。最后在全局词法环境中找到了greeting。

紧接着执行那段在大括号里的代码,为这个块级创建一个新的词法环境。如下:

blockLexicalEnvironment = {
 greeting: 'Hello World',
 outer: <globalLexicalEnvironment>
}

然后执行console.log(greeting)这行代码,首先在本层词法环境中找greeting,OK,找到,结束。此时就不会再去外部作用域(这里是全局作用域)寻找该变量了。

注意:只有let和const声明变量才会创建一个新的词法环境存储,使用var声明的变量会被存储在当前块(大括号)所在的词法环境中(全局词法环境或是函数词法环境中)。

结论:当一个变量被使用时,Javascript引擎会首先在当前的词法环境中进行寻找,如果找不到就找上层词法环境中寻找,直到找到为止。

结论

作用域就是一个变量可访问和可见的区域,和函数一样,Javascript的作用域也可以嵌套,Javascript引擎会层层遍历作用域来寻找用到的变量。

Javascript使用词法作用域,这意味着变量的作用在编译阶段就会被确定。

Javascript引擎在程序执行期间使用词法环境来存储变量和函数。

作用域和作用域链是Javascript中的基础概念,理解作用域和作用域链能让你成为一个更优秀的Javascript开发者。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
将string解析为json的几种方式小结
Nov 11 Javascript
Extjs4 消息框去掉关闭按钮(类似Ext.Msg.alert)
Apr 02 Javascript
js去字符串前后空格5种实现方法及比较
Apr 03 Javascript
jQuery模拟超链接点击效果代码
Apr 21 Javascript
js原生appendChild的bug解决心得分享
Jul 01 Javascript
调试JavaScript中正则表达式中遇到的问题
Jan 27 Javascript
JavaScript Ajax编程 应用篇
Jul 02 Javascript
JavaScript九九乘法口诀表的简单实现
Oct 04 Javascript
JavaScript实现单击网页任意位置打开新窗口与关闭窗口的方法
Sep 21 Javascript
AngularJS修改model值时,显示内容不变的实例
Sep 13 Javascript
微信小程序设置全局请求URL及封装wx.request请求操作示例
Apr 02 Javascript
浅谈JS for循环中使用break和continue的区别
Jul 21 Javascript
JS数组方法reduce的用法实例分析
Mar 03 #Javascript
Javascript模拟实现new原理解析
Mar 03 #Javascript
JS面向对象编程——ES6 中class的继承用法详解
Mar 03 #Javascript
JS面向对象编程实现的拖拽功能案例详解
Mar 03 #Javascript
序列化模块json代码实例详解
Mar 03 #Javascript
JS常用排序方法实例代码解析
Mar 03 #Javascript
JS面向对象编程实现的Tab选项卡案例详解
Mar 03 #Javascript
You might like
下拉列表多级联动dropDownList示例代码
2013/06/27 PHP
IIS下PHP的三种配置方式对比
2014/11/20 PHP
解决phpcms更换javascript的幻灯片代码调用图片问题
2014/12/26 PHP
Zend Framework使用Zend_Loader组件动态加载文件和类用法详解
2016/12/09 PHP
jQuery自动切换/点击切换选项卡效果的小例子
2013/08/12 Javascript
JavaScript图片放大技术(放大镜)实现代码分享
2013/11/14 Javascript
js密码强度实时检测代码
2016/03/02 Javascript
jQuery表单事件实例代码分享
2016/08/18 Javascript
微信小程序前端源码逻辑和工作流
2016/09/25 Javascript
JavaScript结合HTML DOM实现联动菜单
2017/04/05 Javascript
Node.js利用js-xlsx处理Excel文件的方法详解
2017/07/05 Javascript
基于JavaScript实现数码时钟效果
2020/03/30 Javascript
JS解决position:sticky的兼容性问题的方法
2017/10/17 Javascript
360提示[高危]使用存在漏洞的JQuery版本的解决方法
2017/10/27 jQuery
详解vue-cli项目中用json-sever搭建mock服务器
2017/11/02 Javascript
Vue+Mock.js模拟登录和表格的增删改查功能
2018/07/26 Javascript
Vue2.0实现组件之间数据交互和通信操作示例
2019/05/16 Javascript
jQuery是用来干什么的 jquery其实就是一个js框架
2021/02/04 jQuery
[45:46]2014 DOTA2国际邀请赛中国区预选赛5.21 HGT VS DT
2014/05/23 DOTA
[01:00:06]加油DOTA_EP01_网络版
2014/08/09 DOTA
python相似模块用例
2016/03/04 Python
Python实现的朴素贝叶斯算法经典示例【测试可用】
2018/06/13 Python
Python数据可视化库seaborn的使用总结
2019/01/15 Python
python解释器spython使用及原理解析
2019/08/24 Python
在pycharm中使用pipenv创建虚拟环境和安装django的详细教程
2020/11/30 Python
python元组拆包实现方法
2021/02/28 Python
意大利中国电子产品购物网站:Geekmall.com
2019/09/30 全球购物
EJB需直接实现它的业务接口或Home接口吗,请简述理由
2016/11/23 面试题
幼儿园六一儿童节主持节目串词
2014/03/21 职场文书
幼儿园亲子活动总结
2014/04/26 职场文书
小学生迎国庆演讲稿
2014/09/05 职场文书
2014年党风廉政建设工作总结
2014/11/19 职场文书
收银员岗位职责
2015/02/03 职场文书
工作犯错保证书
2015/05/11 职场文书
Python字符串对齐方法使用(ljust()、rjust()和center())
2021/04/26 Python
鸿蒙3.0体验感怎么样? 鸿蒙3.0系统评测向
2022/08/14 数码科技