缘起
写这篇文章的契机,是最近有看 react 源码的想法。先是看了这篇文章 如何阅读大型前端开源项目的源码 学习了一波姿势,然后开始下载 react 工程鼓捣起来。源码打断点,构建 umd 代码,然后看函数调用栈,执行到这一步,看到了这个页面,发现都是我不熟悉的东西。比如这个 Scope,虽然大体知道是作用域,但是具体是按照什么规则得到的,就不是很清楚了。所以决定先搞清一下 js 的作用域问题。
js 的作用域,是一个老生常谈的问题了,也是面试常考的内容。以前也通过看 js 高程,你不知道的 js 这些书对此有过据较为深入的学习,刷一些面试题的时候也背了一些“标准答案”,但是时间一长,也忘掉大半了。现在要我真说道说道,大概是说不出个所以然来的。今天搜索相关信息时看到这个博客的一一系列博文讲的真不错 https://blog.csdn.net/q1056843325/category_9267514.html ,自己对这一块其实挺欠缺的。纸上得来终觉浅,觉知此事要躬行。这次主要借鉴这位博主的文章内容,借助 Chrome 的开发者工具好好探究一下这个问题。
探究
预编译,变量提升与 let,const
脚本执行时,js 引擎会做下面三件事。
- 语法分析 引擎检查代码是否包含低级语法错误
- 预编译 在内存中开辟一些空间,存放变量与函数
- 解释执行
语法分析很简单,就是引擎检查你的代码有没有什么低级的语法错误
解释执行顾名思义便是执行代码了
预编译简单理解就是在内存中开辟一些空间,存放一些变量与函数
预编译的发生时间
我之前也以为预编译主要就是做了变量提升这件工作,也就是在脚本解释执行前执行一次。但实际上,在函数执行前都会进行预编译。
先看一下我之前理解的预编译,在 v8 引擎中时怎么做的。
在脚本第一行执行前插入了 debugger 语句,这个时候预编译已经完成。我们看到 Scope 的 Global 作用域(浏览器中的 window 对象)中已经有了以下的属性,符合我们的认知,对于 let 声明的属性,预编译的时候是没有处理的。
1 | { |
然后继续执行,直到执行完了,我们看到 Scope 中多了一个 Script 的标签,其中保存了 aa 的值。这个按我的理解,也可以把它当作全局作用域,与 Global 不同的是,你没法在 window 对象上访问它。
接下来就要执行函数了,在此之前,需要插入一点另外的知识
函数的 [[Scope]]
属性
函数是特殊的可执行对象。既然是对象,就可以拥有属性。函数中存在这一个内部属性[[Scope]]
(我们不能使用,供 js 引擎使用)函数被创建时,这个内部属性就会包含函数被创建的作用域中对象的集合。这个集合呈链式链接,被称为函数的作用域链。按照我的理解,这和词法作用域是一个概念。这个 [[Scope]]
是会随着代码执行动态变化的。在这个例子中,b 函数的 [[Scope]]
一开始是没有 Script 的,在 let 声明执行后,Script 就被添加上了。
函数的执行环境
在函数执行时,会创建一个叫做执行环境/执行上下文(execution context)的内部对象,它定义了一个函数执行时的环境。函数每次执行时的执行环境独一无二,多次调用函数就多次创建执行环境,并且函数执行完毕后,执行环境就会被销毁,执行环境有自己的作用域链,用于解析标识符。
[[Scope]]
和执行期上下文虽然保存的都是作用域链,但不是同一个东西。[[Scope]]
属性是函数创建时产生的,会一直存在,而执行上下文在函数执行时产生,函数执行结束便会销毁。
预编译发生在每次函数执行前这个创建执行上下文的过程
预编译生成了下图中的 Local 作用域。而 Script 和 Global 作用域,则是函数属性 [[Scope]]
的拷贝。
这个博主的文章,里面多次提到了 AO 活动对象的概念,实际上这是 ES3 的说法了,还是以开发者工具的展现为准,这样更好理解一点。
至此,关于预编译的问题解决。
性能优化
js 引擎查找作用域链是为了解析标识符,变量在执行环境作用域链的位置越深,读写速度就越慢,因此应该多用局部变量(缓存)。
闭包
以前看过这样一个对于闭包的解释。当在一个函数中返回一个函数的时候,这个被返回的时候就可以保有当前函数作用域的变量的引用,这就形成了闭包。
但这样的理解是不充分的。实际上,只要是在函数的作用域内定义了函数,这个被定义的函数就具有闭包作用域。
因此闭包的一大用途,就是实现私有变量。我们知道函数如果返回一个对象,在对它进行构造调用时,结果返回的就是该对象。不过使用这种方式,this 赋值的属性和原型绑定就失效了。
可以看到,当执行以这种方式返回的函数时,可以通过闭包的方式访问执行构造调用时的 Local 作用域。
再回想一下之前说过的,函数每次执行时的执行环境独一无二,多次调用函数就多次创建执行环境,和 settimeout 打印的经典问题,顿时感觉豁然开朗了啊。