浏览器中堆栈内存中的底层处理

378 阅读8分钟

浏览器创建代码的过程:

/*

  • 编译器-------------------把代码解析成浏览器懂得结构
  •  词法解析
    
  •  AST抽象语法树
    
  •  构建出浏览器能能够执行的代码
    
  • 编译器做完这些事之后交给了
  • 引擎(V8 webkit内核)
  •  变量提升
    
  •  作用域/闭包
    
  •  变量对象
    
  •  堆栈内存
    
  •  GO/VO/AO/EC/EC Stack
    
  •  ......
    

*/

JS引擎想要执行代码,一定会创建一个执行栈,也就是创建一个栈内存(EC Stack =>执行上下文环境栈)

栈内存:提供环境,用来执行代码,并且可以存储基本类型的值

EC:执行上下文,指的是当执行代码的时候,会形成一个全局的执行上下文(EC(G)global量),在里面也可以创建局部的执行上下文(EC(...),例如函数环境)。

注:某个域下的代码执行都有自己的执行上下文。

把压缩的上下文压缩到栈中执行 =>进栈

执行完有的上下文就没用了 =>出栈

有的还有用,会把其压缩到栈底 =>闭包

GO(全局对象):global object 在全局的执行上下文中,会有一个全局的对象,在浏览器端(不是node),这个对象有一些属性和方法(onload,setTimeout...)存放在了堆内存(Heap),然后又把全局对象赋值给window={xxx:xxx...},所以我们可以直接调用window.onload(),window.setTimeout()...,

我们还可以创建全局变量,并且给变量赋值

例1:
    let a = 12 ;
    let b = a;
    b = 13;
    console.log(a);
    a = 12 变量赋值的三操作
        创建变量(声明 declare)
        创建值:基本值直接在栈中创建和存储即可
        让变量和值关联起来(赋值) 也叫定义defined
        所以当单独创建一个变量没有赋值的时候,是undefined
        
    上面例子中
        a指向了12,a————————12(指向也可以叫指针,所有编程语言都有指针的概念)
        b指向了同一个12,
        b又重新指向了13,此时b的指向变了,但是a没变,
        所以a还是12

除了给变量赋值,我们还可以赋对象

例2:
    let a = {n:12} ;
    let b = a;
    b['n'] = 13;
    console.log(a.n);  

由于引用值是复杂的结构,所以特殊处理-->开辟一个存储对象中键值对(或存储函数中代码)的内存空间,我们把这个空间叫堆内存,所有堆内存都有一个可被后续查找的16进制地址,而这个16进制地址是存放在栈内存当中的 后续关联赋值的时候,是把堆内存地址给予变量操作的,所以叫“引用值”

上面例子中

    a指向了一个对象的16进制地址(假设为:AAAFFF000),
    b指向了同一个对象的16进制地址,
    b把该对象的"n"改成了13,
    由于a和b指向的是同一个对象的地址
    所以a.n也变成了13
    
再比如:
例3:
    let a = {n:12} ;
    let b = a;
    b = {n:13} ;
    console.log(a.n);  
        a指向了一个对象的16进制地址,
        b指向了同一个对象的16进制地址,
        b又指向了另一个对象的地址,此时a的指向没有变
        所以a.n还是12

到此为止,只要a或者b的指向没有变,那么该对象就不会被销毁,如果想销毁,那么就改变a或者b的指向,那怎么样才能做到既把对象销毁了,又不会重新创建一个堆内存或者栈内存呢?

a = null;
b = null;
null代表空对象指针,是不会占用内存的,
null可以理解为意料之中,代表已知即将赋值,但是还没有赋值,可以先占个坑
undefined可以理解为意料之外,代表不知道会不会赋值

另:

例4:
    let a = {n:12} ;
    let b = a;
    b.m = b = {n:20} ;
    console.log(a);  
    console.log(b);  
    单赋值的时候a = {n:12},是从右往左执行
    而b.m = b = {n:20},属性赋值比变量赋值优先级高,所以b.m赋值完b又重新赋值
    所以a为{n:12,m:{n:20}},b为{n:20}

VO:在全局的执行上下文中,除了有赋值给window的全局对象以外,还会有我们自己创建的变量,我们把存储这个上下文当中的变量的地方,叫做"变量对象VO(G)"。

例5:
    let x = [12,23] ;
    function fn(y){
        y[0] = 100;
        y = [100];
        y[1] = 200;
        console.log(y);
    }; 
    fn(x);
    console.log(x); 

不难看出,x存储在了我们上面说的VO(G)变量对象,x指向了一个数组,而数组也属于对象,所以该数组也占用了一个堆内存,数组内部与对象一样,也有很多键值对,其中把各个项的下标当做了键:

0:12
1::2
length:2
__proto__:.......

x存储在了VO(G)变量对象中,那fn呢?是不是也在其中呢?

答案是:是的!

函数也属于变量,和let和var创建的变量本质是一样的,区别是存储的值是个函数类型的值 创建函数的方式:

function fn(){}、let fn = function(){} 函数表达式

函数表达式的用处:

xxx.onclick = function(){}
xxx.addEventListener('click',function(){});

还有自调函数(function(){})()
~function(){}()
+/-/!function(){}()
但是带~/+/-/!的自调函数不可以自传参

数组里面有键值对,那函数呢?

创建函数的时候就定义了函数的作用域=>当前函数所在的上下文[[scope]]:EC(G)

由于函数也属于引用类型,所以该函数也占用了一个堆内存,其内部为

形参:y
函数内部的内容的代码字符串
"y[0]=100;y=[100]..."

也有对象有的存储键值对
length:1 形参个数
name:"fn"
prototype:...原型
...

AO:每一个函数执行都会形成一个全新的局部上下文,那此时在函数内部创建的变量,都会存在这个局部上下文中的变量对象,我们把它叫做"活动的变量对象(active object:AO)",可以理解为函数内部的变量对象,用来存储局部变量

由此我们可知,y为AO载体,

那么函数在执行内部代码之前,会先做几件事:

1.'初始化作用域链(scopeChain):<EC(fn)--EC(G)>'
2.初始化this指向:window
3.会有一个arguments,它是一个类数组,里面存放的实参的值或者16进制地址
  arguments = {0:AAAFFF000}
4.会把形参y指向实参的值或者16进制地址
  y = AAAFFF000

然后才代码开始执行

(1).因y指向了全局上下文中的x,所以改y[0]就是改x[0] = 100;
(2).把y又指向了另一个对象,那此时指针发生了改变,指向了[100]这个对象BBBFFF000,
(3).这时又把y[1]改变为200,此时改变的是BBBFFF000
(4).所以上面例子中结果为[100,200],[100,23]
由于fn函数上下文执行完毕后,里面的内容没有被占用,所以该函数出栈,被压在底部的EC(G)会弹到栈顶继续执行

注:非严格模式下,形参和实参会建立映射机制,严格模式下不会,而且ES6箭头函数中没有arguments实参集合 例如:

fuinction fn(x,y){
    /*
        arguments = {0:10,1:20}
        x = 10
        y = 20
        此时,非严格模式下,arguments会与形参建立映射机制
        x = 10 -------> 0:10
        y = 20 -------> 1:20
        所以arguments里改了,形参的值也变了
    */
    console.log(x,y,arguments) // 10,20,[10,20,caller...]
    argument[0] = 100;
    y = 200
    console.log(x,y,arguments)// 100,200,[100,200,caller...]
}
fn(10,20);

如果是严格模式下
"use strict"
fuinction fn(x,y){
    /*
        arguments = {0:10,1:20}
        x = 10
        y = 20
        此时,严格模式下,会阻断arguments与形参的映射机制
    */
    console.log(x,y,arguments) // 10,20,[10,20,caller...]
    argument[0] = 100;
    y = 200
    console.log(x,y,arguments)// 10,200,[100,20,caller...]
}
fn(10,20);

关于 逻辑与和逻辑或的优先级,以及在实际项目的几种写法:

例6:
    var x = 10;
    ~function(x){
        console.log(x);
        x = x||20 && 30||40;
        console.log(x)
    }();
    console.log(x);
自调函数也会有"创建+执行"的过程,只是我们平时把他们当成一步了

知识点:
    A||B
      A为真,返回A,否则返回B
    A&&B
      A为真,返回B,否则返回A
      
    &&优先级高于||
使用过程中:
  逻辑或:
    //=>ES6中可以直接赋值初始值
    function fn(x=0){};
    //=>传统方式
    function fn(x){
        if(typeof x==='undefined'){
            x = 0
        }
        或者
        x = x || 0;
    };
    
  逻辑与:
    typeof fn==='function'?fn():null;
    或者
    fn && fn(); //该写法不严谨,但是可以执行
所以上方代码结果为:30  10

谷歌浏览器和IE浏览器对"垃圾回收机制"处理的区别

例7:
    let x = [1,2];
        y = [3,4];
    ~function(x){
        x.push('A');
        x = x.slice(0);
        //slice()会返回一个新数组,所以x会指向新数组的16进制地址,由于slice()里只有一个参数,所以截取到末尾,等于是重新克隆了一份给x
        x,push('B');
        x = y; //此时y是全局下的y
        x.push('C');
        console.log(x,y);
    }(x);
    console.log(x,y);
  输出:[3,4,'C'],  [3,4,'C']
        [1,2,'A'],  [3,4,'C']

此时,x,y被占用着,slice()新返回的数组没有被占用,所以跟着自调函数一起出栈

谷歌浏览器的"垃圾回收机制"(内存释放机制):
=>浏览器会在空闲的时候,把所有不被占用的堆内存,进行释放和销毁

IE浏览器的机制:
=>当前堆被占用一次,记数字1,再被占用一次,数字累加,当然取消占用,数字减去1,一直减到0则销毁