JavaScript中的执行环境和作用域链


Posted in Javascript onSeptember 04, 2020

前言

JS 中的执行环境和作用域链是非常重要的概念,它们是 JS 引擎在处理 JS 代码的时候对变量和函数的处理方式,这两个概念的正确理解能够帮助我们更好地理解和预测代码的行为。

执行环境

执行环境定义了变量或者函数有权访问的数据集合,每一个执行环境都有一个与之关联的变量对象,该执行环境中定义的所有变量和函数都保存在这个对象中。我们无法直接访问这个对象,这个对象只是在解析器处理数据的时候使用。

我们平时说的全局变量就是在最外围的一个执行环境中定义的变量,全局执行环境根据 ECMAScript 的不同实现而有不同的表示,在 Web 浏览器中,全局执行环境就是 window 对象,所有的全局变量和函数就是作为 window 对象的属性和方法创建的。在 nodejs 的实现中,全局执行环境就是global 对象。
除了全局执行环境,每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中,而函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。也就是说某个执行环境中的代码全部执行完毕之后,该环境就被销毁,保存在其中的所有变量和函数定义也随之销毁,全局执行环境直到应用程序额推出——例如网页或浏览器被关闭时才被销毁。

作用域链

前面说到每个执行环境都有一个变量对象来保存环境中定义的变量和函数,环境是层层嵌套的,所以当代码进入到一个新的环境开始执行时,会创建变量对象的一个作用域链,把嵌套的执行环境之间的变量对象做一个有序的联系。作用域链最主要的作用是确保当前执行环境有权访问的变量和函数,并且有序地查找。在作用域链的最前端始终是当前正在执行的代码所处的执行环境的变量对象,如果这个环境是一个函数,就把函数的活动对象作为其变量对象,在函数中没有定义新的变量时,这个活动独享就是函数的 arguments 对象。作用域链的下一个变量都西昂来自于当前执行环境的包含环境,依次类推,逐层嵌套,知道全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个都对象。

当我们的代码在执行的时候,遇到的每一个标识符解析都会沿着作用域链一级一级地进行搜索,从作用域链的前端(当前执行环境的变量对象)逐级向后回溯,知道找到标识符为止,如果在作用域链上没有找到这个标识符,通常会导致错误。我们经常遇到的 Uncaught ReferenceError: x is not defined 就是这个错误在浏览器中的表现。

JS解释器在执行时会将变量和函数进行声明提前,在声明函数的时候,会给函数一个 [[scope]] 属性,这个属性中包含了当前函数所有包含环境的变量对象,也就是我们的函数在声明提前的时候就已经生成了他的包含环境的作用域链了,然后当函数执行的时候会把自己的 arguments 和内部定义的函数和变量打包成一个变量对象加到 scope chain 的最后。

函数参数也被当做变量来对待,因此起访问规则与执行环境中的其他变量相同。

作用域链的这种特性理解起来其实也是比较直观的,但是在实际的代码中由于情况非常多,有时候有些行为还是比较反直觉或者说容易产生误解的。比如下面的情况:

作用域链看的是函数定义的位置而不是执行的位置

var x = 10
bar()
function foo() {
 console.log(x)
}
function bar(){
 var x = 30
 foo()
}

在这个例子里面,可能会有人误以为 bar() 会输出 30,我们只要理解函数其实是保存在堆中,我们给函数命名只是一个指向函数堆中地址的一个引用,当我们执行函数的时候根据这个引用去堆中找对应的函数执行。所以无论我们在哪里执行函数,函数的位置都是不变的,我们看作用域链也是,我们确定作用域链不是看函数是在哪里执行,而是要看函数是在哪里定义,作用域链可以认为是函数声明时就已经生成了。

个人认为 ECMAScript 这样处理作用域链是为了作用域链能够保持不变而不用一直维护,并且根据环境的嵌套保持一致性。

闭包

除了全局执行环境的变量对象是始终存在的,其他局部函数的变量对象都只在函数的执行过程中存在,一般来讲,函数执行完毕之后,局部活动对象就被销毁了,内存中仅仅保存全局执行环境的变量对象,但是闭包的情况是不同的。

闭包指的是有权访问另一个函数作用域中的变量的函数,比如下面这样:

function outer(){
   var scope = "outer";
   return function (){
    return scope;
   }
 }
var fn = outer();
fn();

在一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到他的作用域链中,因此在 outer 函数内部定义的匿名函数(我们下面把这个匿名函数称为 inner 函数)的作用域链中,实际上会包含外部函数 outer() 的活动对象,下图可以看书当代码执行时,outer inner 函数的作用域链。

JavaScript中的执行环境和作用域链

当匿名函数从 outer() 中被返回后,inner() 函数仍然可以访问在 outer() 中定义的所有变量,也就是说,当 outer() 函数执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链依然在引用这个活动对象。换句话说,当 outer() 函数执行完毕返回后,其执行环境和作用域链都被销毁,但它的活动对象依然保存在内存中,如果匿名函数不销毁,则这个活动对象会一直存在于内存中。

js中的对象都是保存在堆中,我们在代码中写的都是对对象的引用,作用域链中也是,所以上面说的 outer() 函数执行完毕后作用域链被销毁但是对象还存在,其实销毁的只是引用, js 中的垃圾处理机制的一种策略是引用计数,当某个变量或对象的引用次数为 0 的时候内存会被收回。outer 函数的变量对象的引用有两个一个是 outer 的作用域链和匿名函数的作用域链,所以只要匿名函数不被销毁,这个引用就一直存在,outer() 的活动对象也会一直存在。

轮子哥在知乎给过一个比较容易理解的说法:“闭”的意思不是封闭内部状态,而是封闭外部状态,一个函数如何能够封闭外部状态呢,当外部状态的 scope 失效的时候,它自己还保留了一份。

JavaScript中的执行环境和作用域链

由于闭包会携带包含它的函数的作用域,因此回避其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,只在必要的时候使用闭包。

总结

任何一种编程语言都有作用域的概念,我们的程序是围绕着变量操作的,那么在设计语言的时候,变量如何储存,储存到哪里,我们的程序如何找到对应的变量就是一个首先要解决的问题。而作用域就是语言设计者针对这个问题编写的一套设计良好的规则来存储并搜索对象,这就是作用域的概念。而 JS 中的这个规则就是作用域链,我们在编写程序的时候也需要知道我们的变量(以及函数)是如何储存,以及 JS 引擎在遇到标识符解析的时候是按照什么规则来搜索变量或者函数的,只有这样我们才能写出更可靠的代码。

以上就是JavaScript中的执行环境和作用域链的详细内容,更多关于JavaScript执行环境和作用域链的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
javascript 复杂的嵌套环境中输出单引号和双引号
May 26 Javascript
js监听键盘事件示例代码
Jul 26 Javascript
javascript事件冒泡详解和捕获、阻止方法
Apr 12 Javascript
jquery插件jquery.nicescroll实现图片无滚动条左右拖拽的方法
Aug 10 Javascript
微信小程序 Windows2008 R2服务器配置TLS1.2方法
Dec 05 Javascript
JavaScript代理模式原理与用法实例详解
Mar 10 Javascript
es6函数之rest参数用法实例分析
Apr 18 Javascript
深入解读VUE中的异步渲染的实现
Jun 19 Javascript
浅谈JavaScript中等号、双等号、 三等号的区别
Aug 06 Javascript
vue实现同时设置多个倒计时
May 20 Vue.js
javascript数组includes、reduce的基本使用
Jul 02 Javascript
html5 录制mp3音频支持采样率和比特率设置
Jul 15 Javascript
Vue 禁用浏览器的前进后退操作
Sep 04 #Javascript
详解JavaScript数据类型和判断方法
Sep 04 #Javascript
vue将data恢复到初始状态 && 重新渲染组件实例
Sep 04 #Javascript
详解JavaScript中new操作符的解析和实现
Sep 04 #Javascript
我所理解的JavaScript中的this指向
Sep 04 #Javascript
JS运算符优先级与表达式示例详解
Sep 04 #Javascript
vue中的循环对象属性和属性值用法
Sep 04 #Javascript
You might like
日本十大科幻动漫 宇宙骑士垫底,第一已成经典
2020/03/04 日漫
php4的彩蛋
2006/10/09 PHP
php基础知识:函数基础知识
2006/12/13 PHP
php获取网页请求状态程序示例
2014/06/17 PHP
laravel ORM关联关系中的 with和whereHas用法
2019/10/16 PHP
用jQuery实现检测浏览器及版本的脚本代码
2008/01/22 Javascript
jquery插件制作 手风琴Panel效果实现
2012/08/17 Javascript
javascript中SetInterval与setTimeout的定时器用法
2015/08/24 Javascript
js文字横向滚动特效
2015/11/11 Javascript
jquery动态切换背景图片的简单实现方法
2016/05/14 Javascript
jquery获取form表单input元素值的简单实例
2016/05/30 Javascript
基于Nodejs利用socket.io实现多人聊天室
2017/02/22 NodeJs
在小程序中集成redux/immutable/thunk第三方库的方法
2018/08/12 Javascript
JS 获取文件后缀,判断文件类型(比如是否为图片格式)
2020/05/09 Javascript
VSCode launch.json配置详细教程
2020/06/18 Javascript
DWR内存兼容及无法调用问题解决方案
2020/10/16 Javascript
解决ant Design中Select设置initialValue时的大坑
2020/10/29 Javascript
[15:58]DOTA2国际邀请赛采访专栏:Tongfu.Sansheng&KingJ,DK.rOtk
2013/08/08 DOTA
Python中模拟enum枚举类型的5种方法分享
2014/11/22 Python
在Python中利用Pandas库处理大数据的简单介绍
2015/04/07 Python
详解Django中Request对象的相关用法
2015/07/17 Python
python利用不到一百行代码实现一个小siri
2017/03/02 Python
详解用Python处理HTML转义字符的5种方式
2017/12/27 Python
详解如何为eclipse安装合适版本的python插件pydev
2018/11/04 Python
把django中admin后台界面的英文修改为中文显示的方法
2019/07/26 Python
Django用户认证系统如何实现自定义
2020/11/12 Python
HTML5网页录音和上传到服务器支持PC、Android,支持IOS微信功能
2019/04/26 HTML / CSS
法院信息化建设方案
2014/05/21 职场文书
党的群众路线教育实践活动个人自我剖析材料
2014/10/07 职场文书
2014年药品销售工作总结
2014/12/16 职场文书
士兵突击观后感
2015/06/16 职场文书
论语读书笔记
2015/06/26 职场文书
出纳2015年度工作总结范文
2015/10/14 职场文书
人民调解协议书
2016/03/21 职场文书
低版本Druid连接池+MySQL驱动8.0导致线程阻塞、性能受限
2021/07/01 MySQL
SpringCloud项目如何解决log4j2漏洞
2022/04/10 Java/Android