javascript闭包深入理解

124 阅读5分钟

.浏览器渲染原理

  • 第一步:HTML树+CSS树 = render tree
  • 第二步:render tree layout布局:文档流、盒模型、计算大小和位置
  • 第三步:render tree paint绘制:边框背景颜色,文字颜色,阴影等
  • 第四步:render tree compose:根据层叠关系展示画面

2. V8引擎的架构

V8引擎本身的源码非常复杂,大概有超过100w行C++代码,通过了解它的架构,我们可以知道它是如何对JavaScript执行的:

  • Parse模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码;
    • 如果函数没有被调用,那么是不会被转换成AST的;
  • Ignition是一个解释器,会将AST转换成ByteCode(字节码)
    • 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
    • 如果函数只调用一次,Ignition会执行解释执行ByteCode;
  • TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码;
    • 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提高代码的执行性能;
    • 但是,机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是umber类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;

3. 代码执行流程

3.1 概述

如果把全局当作一个函数(main函数,入口函数),相当于先调用的是全局执行函数,后面调用的哪个函数就执行哪个函数,执行过程几乎一样:

  • 函数未执行时,其对应的堆内存中仅有父级作用域和函数执行体,函数每次被调用时,才会创建函数执行上下文(全局执行上下文),每一个执行上下文都会包含一个VO对象
  • 在调用函数,执行函数体之前,会先创建函数执行上下文(环境),然后把执行环境压缩(推入)到ECStack执行环境栈中。
  • 在该环境中创建对应的活动对象AO,函数执行环境中把活动对象AO作为自己的变量对象,函数中定义的变量都是保存在AO中的。搜集变量和函数,变量赋值为undefined,函数绑定对应的堆内存地址值,该过程称为函数的预解析,先提升变量,再提升函数
  • 绑定VO为AO(GO),绑定this,绑定arguments
  • 开始逐行执行代码,一行一行执行代码完毕后,该环境弹出(出栈)
  • 销毁函数执行上下文,此时AO不一定销毁
3.2 全局代码执行过程
  • 第零步,创建函数调用栈(ECStack),创建全局执行上下文(Global Execution Context)
  • 第一步,在堆内存中创建一个全局对象:Global Object(GO)
    • 里面有String,Date,setTimeOut等属性和方法
    • 另外还有一个window属性,指向GO对象
    • 还有变量和函数的预处理,为GO对象添加属性和方法,普通属性为undefined,函数则为地址值,此过程称为变量和函数的作用域提升
  • 第二步,绑定VO为GO,执行代码
3.3 ECStack执行函数代码

函数在被调用前,其对应的堆内存中仅有父级作用域和函数执行体,函数每次被调用时,才会创建函数执行上下文

  • 根据函数体创建一个函数执行上下文(Functional Execution Context,简称FEC),并且压入到ECStack中。
  • 函数执行上下文包含三个部分:
    • 第一部分:在解析函数成为AST树结构时,会创建一个Activation Object(AO),AO中包含形参、arguments、函数定义和指向函数对象、定义的变量;
    • 第二部分:作用域链:由VO(在函数中就是AO对象)和父级VO组成,查找时会一层层查找;
    • 第三部分:this绑定的值
    • 第四部分:执行函数体

4. 闭包

作用:缓存数据,时间上可以延长内部数据的生命周期,空间上可以让外部访问内部数据

缺点:可能会有内存泄漏

正常函数执行结束后,其函数执行上下文出栈后,对应的AO对象会被释放掉,但如果该函数内部有嵌套的函数或者对象未被释放,可以通过某种方式访问到,那么子函数中会有父级作用域指向该AO对象,使得该对象无法被销毁,形成狭义上的闭包。

  • 一个普通的函数function,如果它可以访问外层作用于的自由变量,那么这个函数就是一个闭包;
  • 从广义的角度来说:JavaScript中的函数都是闭包;
  • 从狭义的角度来说:JavaScript中一个函数,如果访问了外层作用域的变量,那么它是一个闭包;
    • 产生时间:内部函数被定义时产生
    • 销毁时间:内部函数对象被销毁时
  • 如果外部函数内部嵌套内部函数,那么调用几次外部函数,就会创建几个单独的内部函数以及单独的AO对象
function outer() {
	var a = 1;
	function inner() {
		a++;
		console.log(a);
	}
	return inner;
}
// 每执行一次outer函数,都会声明一次变量a,和对应的函数inner
var fn1 = outer();
var fn2 = outer();

fn1();//2
fn1();//3
fn1();//4
fn2();//2