引题

  1. var x = [12, 23]
  2. function fn(y) {
  3. y[0] = 100;
  4. y = [100]
  5. y[1] = [200]
  6. }
  7. fn(x)
  8. console.log(x)

看其浏览器执行过程:

执行过程

1、ECStack 、GO、EC、VO、AO

浏览器会在计算机内存中分配一块内存,专门用来供代码执行的栈内存,称作执行环境栈(ECStack) 同时会创建一个全局对象(GO)
将内置的属性方法( isNaNsetIntervalsetTimeout… )存放到一块单独的堆内存空间,并且使用 window 指向全局对象

在执行代码前,还需要创建一个全局执行上下文( EC(G) ), 创建完成后,进入到栈内存中去执行( 进栈 ); 在当前全局执行上下文中,因为会创建很多变量并且赋值,所以会创建一个变量对象 VO(Variable Object) 来进行保存,在函数私有上下文中的变量对象叫做 活动对象 AO(Activation Object) (ps: 每个执行上下文都有一个单独的变量对象)
总结下这些陌生不常见的名词

  • 执行环境栈ECStack ( Execution Context Stack ):专门用来供代码执行的栈内存
  • 全局对象GO(Global Object):存放内置的属性方法,window 指向
  • 全局执行上下文EC(G) ( Execution Context(G) ):页面加载后进栈、销毁后出栈
  • 变量对象VO (Variable Object):存放当前执行上下文中创建的变量和值
  • 活动对象AO (Activation Object):函数私有上下文中的变量对象

3.2 - 浏览器底层机制 - 图1

2、全局代码执行

当一切准备就绪,就开始从上到下执行代码 (ps: 执行前还会涉及变量提升的问题,这里不进行展开)

  1. var x = [12, 23]
  2. 复制代码

js 在解析这段代码时,会按照以下 3 个步骤

  • 先创建一个值 [12, 23]
  • 再创建一个变量 x
  • 最后将变量与值进行关联

当创建的值是引用类型时,会在堆内存中开辟新的内存空间用了保存值,创建完成后,会将堆内存地址(通常是 16 进制 )保存到栈内存中; 如果创建的值是基本类型时,会直接保存到栈内存中
3.2 - 浏览器底层机制 - 图2继续向下执行,

  1. function fn(y) {
  2. y[0] = 100;
  3. y = [100]
  4. y[1] = [200]
  5. }
  6. 复制代码

函数也是属于引用类型,也需要开辟堆内存空间进行保存,不同于数组和对象保存的是键值对,JS会将函数体通过字符串包裹进行保存,同时也会保存函数相关的属性,例如 函数名 name: fn形参个数length: 1 等。同时,更重要的是,创建函数的时候,就定义了函数的作用域,也就是 等于当前创建函数的执行上下文。在这个例子中,函数fn 的作用域就是全局执行上下文,标识为 [[scope]]:EC(G)
3.2 - 浏览器底层机制 - 图3在函数创建好后,继续向下执行

  1. fn(x)
  2. 复制代码

通过上面的动图了解到,fn 和 x 都指向堆内存地址,所以,fn(x) 相当于 AAAFFF000(AAAFFF111) , 在执行函数体代码之前,我们需要知道的是:

  • 每次函数执行,都会创建一个函数私有执行上下文,创建完之后,需要进入到栈内存中去执行,此时,执行栈中的全局执行上下文就会被压入到栈底(压栈)
  • 同时,需要创建一个活动对象 AO 存放当前函数执行上下文中创建的变量和值等

3.2 - 浏览器底层机制 - 图4
再完成函数执行上下文入栈后,接下来会做以下几件事

  1. 初始化作用域链 scopeChain: 作用域链通常标记为 <当前执行上下文, 函数创建时的作用域> ,而作用域链是为了函数执行过程中,当活动对象中不存在某个变量时,会沿着作用域链向上找到
  2. 初始化 this 指向 : 本例子中,this 等于 window
  3. 初始化实参集合 arguments
  4. 形参赋值 y = x = AAAFFF111
  5. 执行函数体 紧接着就是执行函数体内容,在执行完成后,当前函数的执行上下文就会出栈,退出执行栈,而被压入栈底的全局执行上下文又被推到了栈顶,此时会继续执行全局上下文中的代码

3.2 - 浏览器底层机制 - 图5
至此,代码执行结束,最终输出的 x 是 [100, 23]

二、浏览器常用的垃圾回收机制(内存释放机制):

1、 查找引用方式(weblit内核)

浏览器有自动回收垃圾的机制,定期间隔某段时间,把所有没有被占用的内存回收释放(这种垃圾回收机制,比其它语言要完善一些)

-1).不会销毁的情况:

  • 创建一个堆(16进制地址):
    • 如果有变量或者其它东西存储了堆内存的地址,则当前堆内存被视为占用,也就不能释放销毁
  • 上下文进栈执行;
    • 如果当前上下文中的某些内容(一般也是当前上下文中创建的堆)被上下文以外的变量或者其它事务所占用,那么当前上下文就不能出栈释放(但是一般情况下,上下文中代码执行完,上下文自己就出栈释放了)

-2).堆内存释放

如果堆内存用完后,我们想去手动释放它,则取消所有的占用:赋值为NULL(NULL是空对象指针,也就是不指向任何的堆内存)

  1. //=>创建一个引用类型值,就会产生一个堆内存
  2. //如果当前创建的堆内存不被其它东西所占用了(浏览器会在空闲的时候,查找每一个内存的引用状况,不被占用的都会给回收释放掉),则会释放
  3. let obj = {
  4. name : 'xiaozhima'
  5. };
  6. let oop = obj;
  7. //此时obj和oop都占用着对象的堆内存,想要释放堆内存,需要手动解除变量和值的关联(null:空对象指针)
  8. obj = null;
  9. oop = null;

-3).栈内存释放

栈内存:

  • 打开浏览器形成的全局作用域是栈内存
  • 手动执行函数形成的私有作用域是栈内存
  • 基于ES6中的let/const形成的块作用域也是栈内存
  • ……

栈内存销毁:

  • 全局栈内存:关掉页面的时候才会销毁
  • 私有栈内存:
    • 1.一般情况下,函数只要执行完成,形成的私有栈内存就会被销毁释放掉(排除出现无限极递归、出现死循环的模式)
    • 2.但是一旦栈内存中的某个东西(一般都是堆地址)被私有作用域以外的事物给占用了,则当前私有栈内存不能立即被释放销毁(特点:私有作用域中的私有变量等信息也保留下来了=>这种函数执行形成不能被释放的私有栈内存,也叫做闭包)
  1. function fn(){
  2. //...
  3. }
  4. fn(); //=>函数执行形成栈内存,执行完成栈内存销毁
  5. function X(){
  6. return function(){
  7. //...
  8. }
  9. }
  10. let f=X(); //=>f占用了X执行形成的栈内存中的一个东西(返回小函数对应的堆),则X执行形成的栈内存不能被释放了

2、内存计数器方式(Trident内核)

当前内存被其它东西引用了,则给堆计数1(累加计数),取消占用后,则减1,当减到零之后,浏览器就可以把它释放了

浏览器渲染机制

渲染器进程 进入主线程后 接收的是HTML文件 开始DOM结构渲染
html 中会引入 css 图片等资源 这资源不会阻塞html的渲染 但遇到script标签会停止渲染去执行加载JS 因为script可以会修改Dom结构