JavaScript的运行过程
Excerpt
本节内容会先讲解 JavaScript 的执行过程,再来学习作用域、作用域链等核心概念。
当理解了这些内容后,就可以很好的理解闭包、闭包的内存泄漏等。
一. 全局代码的执行过程
1.1. ECMA 的版本说明
在 ECMA 早期的版本中(ECMAScript3),代码的执行流程的术语和 ECMAScript5 以及之后的术语会有所区别:
目前网上大多数流行的说法都是基于 ECMAScript3 版本的解析,并且在面试时问到的大多数都是 ECMAScript3 的版本内容;
但是 ECMAScript3 终将过去, ECMAScript5 必然会成为主流,所以最好也理解 ECMAScript5 甚至包括 ECMAScript6 以及更好版本的内容;
事实上在 TC39( ECMAScript5 )的最新描述中,和 ECMAScript5 之后的版本又出现了一定的差异;
那么我们课程按照如下顺序学习:
通过 ECMAScript3 中的概念学习 JavaScript 执行原理、作用域、作用域链、闭包等概念;
通过 ECMAScript5 中的概念学习块级作用域、let、const 等概念;
事实上,它们只是在对某些概念上的描述不太一样,在整体思路上都是一致的。
1.2. 全局代码初始化
假如我们有下面一段代码,它在 JavaScript 中是如何被执行的呢?
1 | var name = "why"; |
1.2.1. 初始化全局对象
js 引擎会在执行代码之前,会在堆内存中创建一个全局对象:
Global Object(GO) 该对象 所有的作用域(scope)都可以访问;
里面会包含 Date、Array、String、Number、setTimeout、setInterval 等等;
其中还有一个 window 属性指向自己;
1.2.2. 执行上下文(EC)
在 JavaScript 引擎内部,有一个执行上下文栈(Execution Context Stack,简称 ECS),它是用于执行代码的调用栈,存储着当前正在执行的执行上下文。
当 JavaScript 引擎开始执行代码时,它会先创建一个全局执行上下文(Global Execution Context,简称 GEC),并将其推入 ECS 中,表示当前正在执行全局的代码块。
GEC 包含了两部分内容:
第一部分是在代码执行前,JavaScript 引擎会扫描全局作用域下的变量和函数,将它们添加到全局对象(Global Object)中,并分配内存空间,但是并不会赋值。这个过程称为变量的作用域提升(hoisting)。
第二部分是在代码执行中,JavaScript 引擎会对变量进行赋值,并执行其他的函数。
1.3. 全局代码的执行
1.3.1. VO 和 GO 的理解
每一个执行上下文会关联一个 VO(Variable Object,变量对象),变量和函数声明会被添加到这个 VO 对象中。
变量对象是一个抽象的概念,在 JavaScript 引擎内部,它被实现为一个与当前执行上下文相关联的对象。
在全局执行上下文中,变量对象是全局对象(Global Object);
在函数执行上下文中,变量对象是一个称为活动对象(Activation Object)的对象。
当全局代码被执行的时候,VO 就是 GO 对象了
当全局代码被执行时,变量对象(Variable Object,简称 VO)就是全局对象(Global Object,简称 GO)了。
全局对象是在 JavaScript 引擎内部创建的一个唯一对象,它在全局作用域下被访问,并且可以存储全局变量、函数、内置对象和浏览器提供的 API 等等。
1.3.2. 全局代码执行过程(执行前)
GO 对象在执行全局代码之前就已经被创建,并且包含了一些内置对象和方法,例如 Object、Array、String、Number、Math、JSON、setTimeout、setInterval 等等。
当全局执行上下文被创建时,变量对象 VO 会被设置为 GO 对象,并且全局作用域下的变量和函数会被添加到变量对象中。
1.3.3. 全局代码执行过程(执行后)
代码执行后就会给变量进行复制操作:
二. 函数代码的执行过程
2.1. 函数代码的执行过程
2.1.1. 函数执行的概念描述
在 JavaScript 中,当函数被调用时,会创建一个函数执行上下文(Functional Execution Context,简称 FEC),并将其压入执行上下文栈(Execution Context Stack,简称 ECS)中,表示当前正在执行该函数的代码块。
函数执行上下文关联的变量对象(Variable Object,简称 VO)是一个称为活动对象(Activation Object,简称 AO)的对象。
在进入函数执行上下文时,JavaScript 引擎会创建一个 AO 对象,并将其关联到当前的执行上下文中。
AO 对象是一个包含函数的参数、函数声明和变量声明的列表。
它的初始值使用函数的参数列表作为参数并进行初始化。
这个 AO 对象会作为执行上下文的变量对象(VO)来存储变量的初始化。
当函数执行完毕后,其执行上下文会从 ECS 中弹出,AO 对象会被垃圾回收,所以不能在函数执行完毕后再访问该函数的 AO 对象。
2.1.2. 函数代码执行过程(执行前)
2.1.3. 函数代码执行过程(执行后)
2.2. 作用域、作用域链
2.2.1. 作用域、作用域链的理解
在 JavaScript 中,当进入一个执行上下文时,执行上下文也会关联一个作用域链(Scope Chain)。
作用域链是一个对象列表,用于变量标识符的查找和求值。
当进入一个执行上下文时,这个作用域链被创建,并且根据代码类型,添加一系列的对象;
作用域链是由当前执行上下文的变量对象(VO)和所有外部环境的作用域链组成的。
具体内容可以查看视频中的讲解过程。
2.2.2. 作用域、作用域链面试题
接下来我们可以利用变量的提升过程,做一些面试题:
1 | // 1.面试题一: |
2.3. 闭包和闭包的内存泄漏
2.3.1. 闭包的迷惑之处
闭包是 JavaScript 中一个非常容易让人迷惑的知识点:
有同学在深入 JS 高级的交流群中发了这么一张图片(这张图来自你不知道的 JavaScript);
并且闭包也是群里面大家讨论最多的一个话题;
闭包确实是 JavaScript 中一个很难理解的知识点,接下来我们就对其一步步来进行剖析,看看它到底有什么神奇之处。
2.3.2. 闭包的概念定义
这里先来看一下闭包的定义,分成两个:在计算机科学中和在 JavaScript 中。
在计算机科学中对闭包的定义(维基百科):
闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures);
是在支持 头等函数 的编程语言中,实现词法绑定的一种技术;
闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表);
闭包跟函数最大的区别在于,当捕捉闭包的时候,它的 自由变量 会在捕捉时被确定,这样即使脱离了捕捉时的上下文,它也能照常运行;
闭包的概念出现于 60 年代,最早实现闭包的程序是 Scheme,那么我们就可以理解为什么 JavaScript 中有闭包:
- 因为 JavaScript 中有大量的设计是来源于 Scheme 的;
我们再来看一下 MDN 对 JavaScript 闭包的解释:
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure);
也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域;
在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来;
那么我的理解和总结:
一个普通的函数 function,如果它可以访问外层作用于的自由变量,那么这个函数就是一个闭包;
从广义的角度来说:JavaScript 中的函数都是闭包;
从狭义的角度来说:JavaScript 中一个函数,如果访问了外层作用于的变量,那么它是一个闭包;
2.3.3. 闭包的形成过程
当我们的代码没有闭包时,往往代码会有非常大的局限性:
- 如果一个函数需要使用到外面作用域的变量,那么必须将所有的变量全部传入进去。
1 | var name = "why"; |
有了闭包之后,我们可以进行如下代码的操作:
1 | function createAdder(count) { |
2.3.4. 闭包的内存泄漏
使用闭包可能会导致内存泄漏:
因为闭包会引用外部函数的变量对象,如果这个闭包被长期保存,那么外部函数的变量对象就会一直存在内存中,无法被垃圾回收。
因此,在使用闭包时,需要注意内存的管理。
那么我们为什么经常会说闭包是有内存泄露的呢?
在上面的案例中,如果后续我们不再使用 add10 函数了,那么该函数对象应该要被销毁掉,并且其引用着的父作用域 AO 也应该被销毁掉;
但是目前因为在全局作用域下 add10 变量对 0xb00 的函数对象有引用,而 0xb00 的作用域中 AO(0x200)有引用,所以最终会造成这些内存都是无法被释放的;
所以我们经常说的闭包会造成内存泄露,其实就是刚才的引用链中的所有对象都是无法释放的;
那么,怎么解决这个问题呢?
因为当将 add8 设置为 null 时,就不再对函数对象 0xb00 有引用,那么对应的 AO 对象 0x200 也就不可达了;
在 GC 的下一次检测中,它们就会被销毁掉;
1 | function createAdder(count) { |
文章转载于coderwhy | JavaScript 高级系列(三) - JavaScript 的运行过程