V8角度看闭包
别再从JS角度看闭包了,我教你用别的眼看
JavaScript中函数的特性
JavaScript的函数一等公民与函数即对象
JS中的函数类型可以随意添加属性,这是其他语言无法实现的
function foo1(){}
foo1.a=1
foo1.b=2
console.dir(foo1)
ƒ foo1()
a: 1
b: 2
arguments: null
caller: null
length: 0
name: "foo1"
prototype: {constructor: ƒ}
__proto__: ƒ ()
[[Scopes]]: Scopes[1]
如果一个编程语言中函数可以做数据类型一样的事,那在这编程语言中函数就是一等公民
对象能做的事函数一样能做,函数还可以调用。 所以说函数即对象。

V8是如何保持函数的可调用性的
函数即对象,那V8如何区别函数与对象的区别呢?
函数的可调用性是与对象最大的区别 v8中是如何处理函数的可调用性的呢?
function foo1(){}
foo1.a = 1
var foo2 = { a : 1 }
都会有a:1这个属性
V8在处理函数时不仅会在foo1中添加a这个属性
还会添加两个隐藏属性
name:'foo1' //保存函数的名称,匿名函数那么会是默认的anonymous
code:'function foo1(){}' //保存函数执行代码,以字符串的形式保存在堆内存中
你定义一个函数会发现默认就会有name这个属性,这只是方便用户调用的,你改了也没用。
你能调用函数时,V8已经处理完了函数。生成了一个函数对象,并且作用域链也已经确定好了。所有当执行函数时,V8虚拟机会执行函数的隐藏属性code,来执行这个函数。
函数即对象
知道了函数即对象,也知道了V8如何区分函数和对象。
可以开始聊闭包了
在JavaScript中函数可以在函数中定义,可以作为一个返回值。函数内部又可以访问外部的属性,V8该如何维持这个关系呢?
作用域链
作用域链的知识大家可以看冴羽大佬的文章 juejin.cn/post/684490…
执行上下文
在执行JS代码时,v8会进行惰性解析。
只会执行JS的顶层代码,生成对应的中间代码。
V8的编译原理大概说一下
V8会解析JS代码生成中间代码,执行时交于解释器执行。
对于热点代码会进过编译器生成二进制代码并缓存,直接交由CPU执行。

在解析中间代码时如果遇到函数,那么会跳过函数,但是会预解析(这是闭包的关键,后面说)
function foo1(){...}
foo1.a = 1
var foo2 = { a : 1 }
比如这段代码,只会执行顶层代码
初始化 函数foo1和对象foo2
之后赋值
对于函数foo1中的代码并不会执行,但会预编译
在执行顶层代码时会创建一个执行上下文并存入一个栈中,这个栈就是执行上下文栈。
当你的顶层代码中执行函数后,在执行函数时也会生成一个执行上下文压入栈中。
为什么用栈来保存函数的调用呢?
1,因为栈的特性最符合函数的执行顺序
最外层函数生命周期最长,越里面的生命周期越短
function foo1(){
function foo2(){
function foo3(){
}
foo3()
}
foo2()
}
foo1()
老千层饼了
foo1
foo1->foo2
foo1->foo2->foo3
foo1->foo2
foo1
根据先入后出的原则,栈就十分适合执行上下文栈
2,栈的内存是连续的
function foo1(){
var a=1
function foo2(){
var b=2
function foo3(){
var c=3
}
foo3()
}
foo2()
}
foo1()
栈的顺序就是
1001f3 c 3
1001f2 b 2
1001f1 a 1
至于V8是如何维护这个栈的(栈顶指针和栈帧指针),就不说了。有兴趣的自己去翻翻V8.dev
栈的删除与新增数据时很快的,只需要移动指针就可以完成。
这两个特性使得栈十分适合执行上下文
用栈保存执行上下文的问题
连续的内存是很难得的,而且你俄罗斯套娃套的越深。栈所占用的连续内存就越多。
所有浏览器会有一个执行上下文栈的最大值,防止你无限套娃。
闭包
好了,看了这么多废话终于到关键点了。
V8如何处理闭包,如何保持作用域链。
function foo1(){
var a=1
return function foo2(){
console.log(a)
}
}
var foo3 = foo1()
foo3()
简简单单一个闭包
在var foo3 = foo1()阶段之后
foo1的执行上下文被销毁栈中的数据被销毁,那foo2又是如何保存foo1作用域,并访问里面的属性的呢?
预检测
上面说过,在执行顶层代码是如果遇到函数会进行预检测
- 对函数中语法进行检测
- 生成VO变量对象,进行函数提升,确认祖先作用域(JS是词法作用域,也是依靠预检测绑定好祖先作用域)
子函数内部使用了父函数的属性
如果子函数中使用了父函数的属性,就会将这个属性保存到堆内存中,并在[[Scopes]]中增加一层对象,地址就是保存的堆内存。
function foo1(){
var a=1
var b=2
return function foo2(){
console.log(a)
}
}
var foo3 = foo1()
foo3()

子函数内部没有使用父函数的属性
如果子函数内部没有用到父函数的属性呢
function foo1(){
var a=1
var b=2
return function foo2(){
}
}
var foo3 = foo1()
foo3()

父函数的属性循环引用呢
function foo1() {
let obj1 = {}
let obj2 = { b: obj1 }
obj1.a = obj2
return function foo2() {
console.log(obj1)
}
}
var foo3 = foo1()
foo3()
console.dir(foo3)

总结
V8来看闭包
执行上下文中的变量是存在栈中的,但在预解析过程中子函数有使用到父函数中的属性,会直接把属性存放在堆内存中。
这样即使栈销毁了,只要保持引用,属性存放在堆内存依旧可以访问。
它将调用到的父函数属性,单独存在了堆内存中。
资料来源
极客时间《图解 Google V8》适合大家V8入门康康
https://v8.dev和v8源码适合深入学习
趁着Vue3.0要中旬才正式上线,到时候去读源码。这段时间把V8学习了。
大家给个三连吧
白嫖的
