函数执行-作用域链-内存管理

函数执行,作用域链

全局代码执行过程(函数)


var name='xxx'
foo()
function foo(num){
    console.log(m)
    var m=1
    var n=2
    console.log('foo')

}

这段代码执行之前先创建全局对象GO(global object),创建GO的时候先会创建一些默认的项(window等),除此之外在编译阶段还会将我们写的name属性放入GO,此时的name属性值为undefined(编译阶段),也就是如果我们在声明name变量之前打印name变量得到的值为undefined

但是同理,如果我们在声明foo函数之前,调用foo函数并不会得到undefined,这也是因为执行过程的机制。
当js引擎编译到第三行的时候,发现声明了一个函数,这时候会把函数名(foo)存储到GO中,同时js引擎会在内存里面新创建一个函数存储空间(函数对象)用于存储foo函数,存储在GO中的foo属性对存储foo的函数空间进行引用

存储函数空间

存储函数空间在内存中开辟,会生成自己的内存地址(如“0xa00”),这个存储函数空间的内存地址就会保存在全局对象的GO中的foo属性中,也就是这个时候GO中的foo属性值为对应函数存储空间的内存地址。
存储函数空间中会存储下面两个东西:
1,scope父级作用域(函数的上一层作用域,这里就是全局作用域VO,这里的VO又和GO等价)
2,函数的执行体(代码块)

函数执行期上下文(FEC,Functional Execution Context)

当通篇的编译工作结束以后就会开始执行,这个时候执行foo()函数,就会从GO中找到foo变量和其中存储的foo函数存储空间地址,找到后进行执行。
在上文中提到过,只要是代码需要执行就必须进入调用栈(ECStack)中,同理foo函数在执行的时候也需要进入调用栈,同理函数在调用栈中也无法直接执行,和全局代码在全局中需要进入全局执行上下文(GEC)进入GEC后执行类似,函数也有自己的函数执行期上下文FEC

活跃对象(Activation Object,AO)

GEC一样,FEC中也有VO,但是这里的VO指向就不再是GO了,而是会在内存中生成一,AO(Activation Object)代表活跃对象,AO也是对象,AO不同于GOAO在执行后会被销毁,GO则不会被销毁。

AO里面会存储定义在函数中的变量,以及函数接收的参数,在函数实际执行之前,会先创建一个AO,并将上述的参数(函数内变量,实参)全部放入AO,并赋值上undefined,如上文例子中函数在执行之前就会把num,m,n放入AO中,且值均为undefined。

创建完AO之后才会真正开始执行函数内的代码,首先执行的时候如果携带了实参,如foo(123),这时候,AO内的num值就会改变为123,随后运行函数体内的第一行代码,console.log(m),和全局变量中的原理类似,此时的m虽然已经在AO中可以被找到了,但是值为undefined,所以打印结果也为undefined

一旦函数内所有语句执行完毕,foo函数对应的函数执行上下文FEC)就会从调用栈中移除,同样该FECVO指向的AO由于不再被引用,所以也会被销毁

当再次执行foo()函数的时候则会重复上述步骤,重新创建一个新的FEC,以及与其对应的AO

为什么函数可以在定义流程之前就被调用?
因为js引擎在进行编译期会扫描所有函数并进行预编译


var name='xxx'
foo()
function foo(num){
    console.log(m)
    var m=1
    var n=2
    var name='yyy'
    console.log(name)
}

这段函数代码在执行的时候通过上文我们可以很轻易的知道其流程了
1,编译,GO识别到该函数后将其放入函数存储空间,并将其在空间内的地址存入GO
2,执行前的准备工作,执行foo函数的时候创建FEC(函数执行上下文),并将FEC放入ECstack中(调用栈)中等待被调用,在FEC中生成VO,并将VO指向AOAO中存入函数的形参和变量,这里就是num,m,n,name并同时给这些全部赋值上undefined
3,执行函数,第一行打印m,此时的m为undefined,后面分别给m,n,name赋值替换掉undefined,最后打印name,就是打印AO中的name,这时已经经过var name的赋值从undefined修改成为了’yyy’,所以打印结果毫无疑问是yyy
4,foo()函数执行完毕后,该函数的FECFEC中的VO指向的AO
5,再次调用的时候重新创建FEC…重复上述步骤
6?如果删除给name赋值的语句再打印name会得到什么结果呢?答案是会打印外部的xxx

为什么打印的是外部的name?
js中在使用变量的时候会先进行寻找,在所有他能找的地方都找不到该变量的时候才会报错

js变量的查找规则

当我们查找一个变量时候,真实的查找路径是沿着作用域链(scope chain)一层层往上查找的

FEC(函数执行上下文)中,除了存在VO以外还同时存放了作用域链,作用域链是由自己当前的VO加上父级作用域(parent scope)组成的,父级作用域在函数被编译的时候就已经决定了,在函数的编译的时候,函数的父级作用域就已经确定了,并与函数的执行体(代码块)一同存放进了函数存储空间。

在这里的话函数foo本身的父级作用域是全局GO,所以这里的实际作用域链就是foo函数的VO(foo函数的AO)加上父级作用域的VO(这里是全局变量GO),当在AO中查找某个变量找不到的时候,就会自动顺着作用域链往上找,这里就找到了GO

当有多个函数嵌套的时候,函数的父级作用域就是另一个函数的AO了,这时候寻找某个变量可能就要一层一层的向上层寻找

var name='xxx'
foo()
function foo(num){
    console.log(m)
    var m=1
    var n=2

    function bar(){
       console.log(name)
    }
   
    bar()
}

这段代码的执行流程,先在调用栈中生成foo函数的执行上下文,在foo函数执行之前会先创建foo函数对应AO,同时在AO中注册函数中需要使用的变量。此时发现存在bar函数,而且此时的bar函数是没有被编译的(v8引擎的preparse流程)不执行的时候只做了预编译,为了节省性能

当函数foo在创建AO的时候,bar函数才被真正进行编译,需要为bar函数在内存里面开拓bar函数存储空间,并为其存储父级作用域scope和函数体存储bar函数,同时将bar函数的存储空间的地址保存到foo函数AO的bar属性中
上述这些流程发生在foo函数在编译时期

编译完成foo函数后开始执行foo函数,执行到bar()语句的时候,为bar()函数在调用栈中生成bar函数的执行上下文(同时生成bar函数的AO,bar函数自己的AO是空的,因为bar不存在任何变量,所以AO解析为空;作用域链,bar函数的作用域链就是自己的AO加上他外部的foo函数的AO

回到bar函数的FEC,确认了AO和作用域链以后执行bar函数代码,打印name的时候首先在自己的AO上找,发现找不到name变量,所以顺着作用域链往上查找,这里的作用域链由于bar函数定义在foo函数内,所以bar函数的作用域链是自身的AO+外层foo函数的AO

此时foo函数的AO中也不存在name变量,所以沿着foo函数的作用域链继续向上查找找到GO身上的name变量,最终打印的就是GO中的name

此时bar函数执行完毕,bar函数对应的FEC就会从调用栈中被弹出,同时foo函数也完成执行,foo函数的FEC也从调用栈中被弹出

作用域链

沿着作用域关系逐层查找父级作用域,一直上至少到GO,当在GO中也查找不到的时候才会报错,需要注意的是,函数的父级作用域,也是函数的执行上下文的中的作用域链,在编译阶段就已经确定了,和函数的调用位置无关

变量环境和记录

上面的所有都是基于早期的ECMA规范,规范正文如下:
每一个执行上下文(包含GEC全局执行上下文,用于执行全局代码;FEC函数执行上下文,用于执行函数代码)都会关联一个环境变量VO,在源代码中的变量和函数声明会被作为属性添加到VO中,对于函数来说,参数也会被添加到VO
在最新的ECMA规范中对一些描述进行了修改如下:
每个执行上下文都会关联到一个变量环境(variable environment)(从环境变量修改为了变量环境),在执行代码中变量和函数的声明都会作为 环境记录(Enviroment Record)添加到变量环境中,对于函数来说,参数也会被作为环境记录添加到变量环境

一些js引擎的特殊处理

1,
function foo(){
m=100
}
foo()
console.log(m)
这里的m并不是在函数体内var的,这种写法无从得知m是在哪里声明的,所以严格意义上是语法错误,但是js引擎会对其进行特殊处理,将其视为在全局上var了一个m并赋值100,所以这里是可以正常打印100的

2,
function foo(){
var a=b=10
}
foo()
console.log(a)
console.log(b)
函数体内代码可以转换为var a=10,b=10,最终访问不到a,但是可以访问b,因为js引擎对b=10这种语句进行了特殊处理,将其视为在全局上声明了b=10,而a则是生命在foo的AO中,所以全局上访问不到

内存管理

不管什么样的编程语言,在代码的执行过程中都是需要给他分配内存的,不同的是某些编程语言需要手动管理内存,某些编程语言会帮助我们自动管理内存
计算机的核心硬件,硬盘,内存,cpu,代码一般从磁盘加载到内存中,再由cpu执行内存中的代码,执行过程中会继续在内存里开拓空间(如声明一个对象需要在内存中为这个对象开辟空间并记录其地址)
不管以什么样的方式来管理内存,内存的管理都会有如下的生命周期
1,分配申请你需要的内存,申请
2,使用分配的内存(存放一些东西,如对象等)
3,不需要使用的时候,对其进行释放
不同的编程语言在1,3步骤是有不同实现的,分为手动管理和自动管理内存
手动管理内存,比如c,c++,包括早期的oc,都是需要手动管理内存的申请和释放的(malloc和free函数)
自动管理内存,比如java,js,python等,会自动帮助我们管理内存,会自动分配/释放

js的内存管理
js会在定义变量的时候自动为我们分配内存
内存的分配方式存在差异,总得可以分为两种,
1,js对于基本数据类型内存的分配会在执行的时候直接在栈空间进行分配
2,js对于复杂数据类型(引用类型)内存的分配会在堆空间中开辟一块空间,并且将这块空间的指针返回值变量引用
也就是栈结构存放原始值,堆结构存放引用值

js的垃圾回收

因为内存的大小是有限的,所以当内存不再使用的时候我们将其进行释放,以便腾出更多的内存空间
在手动管理内存的语言中,我们需要通过一些方式释放自己不需要再存储的内存,但是这种管理方式很低效,需要单独编写管理内存的代码。

所以大部分现代的编程语言都有自己的垃圾回收机制(GC),对于不再使用的对象,我们都称其为垃圾,他需要被回收,以释放更多的内存空间,GC想知道哪些对象不再使用了,需要用到GC算法,GC算法,常见GC算法有引用计数和标记清除

引用计数算法

在被引用的对象内部会存在一个引用计数器(retain count),每有一个引用指向该对象,被对象内的计数器就加一,每减少一个引用计数器就减一,当引用计数器的值为0的时候,这块空间(堆空间内存放该对象的那块空间)就会被回收掉。

但是引用计数存在一个很大的弊端,如果有循环引用,永远相互指向就会内存泄露

标记清除

js引擎广泛采用的就是标记清除算法,当然V8引擎等还在其基础上进行了优化,还会结合一些其他算法

这个算法设置一个根对象,垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于哪些没有引用到的对象就认为是不可引用的对象,不可引用对象在下次垃圾回收的时候就会被回收掉,这个算法可以很好的解决循环引用的问题。

这张图中,黄色部分就是可达的,会被认为是可达(有引用的对象),而绿色部分的M,N则是不可达的,尽管他们互相引用了,但是从根对象开始找不到他们,所以还是会被判定为不可达,这个就是标记清除算法
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值