likes
comments
collection
share

浏览器工作原理与实践(二)——浏览器中的JavaScript执行机制

作者站长头像
站长
· 阅读数 19

js执行顺序

当一段代码被执行时,js引擎会先对其编译,并创建执行上下文。

  1. 当执行全局代码时,会编译全局代码并创建全局执行上下文,整个页面生存周期内,全局上下文只有一份。
  2. 调用到函数时,函数体内代码编译并创建函数执行上下文,函数结束后,下下文会被销毁。

栈遵守后进先出的原则,比如一条死胡同,进去的多人,只能最后进来的人先出。

var a = 2;
function add(b,c){ return b+c }
function addAll(b,c){
     var d = 10,
     result = add(b,c);
     return a + result + d
}
addAll(3,6)
  1. 创建全局上下文,放最底下:先进。变量环境:a = undefined, add = function(){...}, addAll = function(){...}
  2. 执行全局代码,a = 2赋值,上下文变量的a = 2
  3. 调入addAll()js编译addAll函数,并创建其函数执行上下文,压入栈中。参数列表,d = undefined, result = undefined
  4. 执行d = 10,把上下文赋值
  5. 执行到add(b,c)时,创建函数执行上下文,压入栈中。
  6. add函数返回时,add函数上下文从栈顶弹出,将result设为add函数的返回值,add上下文销毁。
  7. addAll执行相加操作后返回,addAll执行上下文从栈顶弹出,销毁。只剩全局上下文,结束。

call stack调用栈,函数里加console.trace()打印函数调用关系。当栈的执行上下文超过一定数据会栈溢出。let const 有块级作用域,当执行到块级时,会被存放在执行上下文的词法环境。词法环境也是一个栈的结构,最外层变量在栈底,一个块级执行完毕,该作用域的信息就会从栈顶弹出,销毁。

闭包的执行顺序

function foo() { 
    var myName = "极客时间" ;
    let test1 = 1 ;
    const test2 = 2 ;
    var innerBar = { 
        getName:function(){ 
            console.log(test1) 
            return myName ;
        }, 
        setName:function(newName){ 
            myName = newName ;
        } 
    } 
    return innerBar;
}
var bar = foo();
bar.setName("极客邦");
bar.getName();
console.log(bar.getName());

浏览器工作原理与实践(二)——浏览器中的JavaScript执行机制

outer:外部引用,指向上级作用域;变量环境:var,function。词法环境:let,const

innerBar是个对象,包含seeNamegetName这2个方法,这2个方法是在foo函数内部定义的,用了foomyNametest12个变量。

词法作用域规则,内部是函数总是可以访问它们外部函数的变量。虽然foo已经结束,其执行上下文也已经弹出去销毁了,但由于其变量myNametest1被使用,所以这2个变量依然保存在内存中,是专属于里面的闭包的,其它任何地方无法访问。在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。当执行到 bar.setName 方法中的myName = "极客邦"这句代码时,JavaScript 引擎会沿着“当前执行上下文–>foo 函数闭包–> 全局执行上下文”的顺序来查找 myName 变量浏览器工作原理与实践(二)——浏览器中的JavaScript执行机制

闭包回收

如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。如果引用闭包的函数是个局部变量,等闭包函数销毁后,下次js引擎回收时判断不再用,会回收这块内存。注意一个原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。

var myObj={
    name:'思否',
    showThis:function(){
        this.name='思否1'
        console.log(this)
    }
}
var foo=myObj.showThis;//相当于赋值指向同一块内存地址,但是调用对象变了,
//foo在全局执行上下文指向window,所以foo函数this指向window
foo()//foo在window全局里

js8种类型:Boolean,Null,Undefined,Number,BigInt,String,Symbol,Object7种原始类型是存在调用栈中。Object引用类型真实存在堆中,栈中存的是堆的引用地址。

垃圾回收

调用栈的执行上下文调完了,会弹出调用栈,所占用的内存也会被其它占用。而调用栈中引用地址指向的堆空间就需要用到 JavaScript 中的垃圾回收器了。V8把堆分成新生代和老生代,新生代容量1-8M,生存时间短的对象。其它则在老生代。

  1. 标记空间中活动对象和非活动对象
  2. 回收非活动对象所占据的内存,就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。
  3. 内存整理

主要是标识对象是否在使用,标记完全回收清理非活动对象。由于非活动对象分布在不同的内存地址,大小也不同,回收之后会有很多内存碎片。新生代有对象区域和空闲区域,新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。回收剩下的对象放到空闲区域,空闲区域变成了对象区域。老行生代的标记清除,会循环调用栈找对象地址的引用 ,找不到就是要回收。回收之后让所有剩下对象移向一端集合在一起,内存碎片就变成大空间。js和回收交叉进行。