内存管理和内存泄露(闭包、作用域链)(一)

简介: 内存管理和内存泄露(闭包、作用域链)

全局作用域(var)

基于早期ECMA的版本规范:


GEC(global excution context)全局执行上下文:执行全局代码


FEC(functional excution context)函数执行上下文:执行函数代码


每一个执行上下文会被关联到一个变量环境(variable object,VO),在源代码中的变量和函数声明会被作为属性添加到VO中


对于函数来说,参数也会被添加到VO中


  • 解释
  • 不管执行的是哪个,都会关联到VO对象(variable object),只是这个VO对象所代表的东西不一样而已
  • 参数被添加到VO中,是形参的那个地方


代码执行过程

  • foo函数
function foo(){
    //foo函数并没有特殊的含义,是编程约定俗成的一种习惯(定义我们不知道要取什么名字的东西)
}


  • 函数变量提升
function foo(){
    console.log("小余")
}
foo()
//会在控制台打印出"小余"


foo()
function foo(){
    console.log('小余')
}
//一样在控制台能够打印出来'小余'


全局函数执行过程

编译阶段是js->AST的时候就确立了

变量对象的定义:变量对象是一个特殊的对象,并且与执行上下文息息相关,VO(变量对象)里面会存有下列内容:

  • variables(var,variableDeclaration);
  • function declarations(FD)
  • function formal parameters

AO对象
函数执行的前一刻,会创建一个称为执行期上下文的内部对象(AO)。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕时,它所产生的执行上下文会被销毁。

  • 讲人话就是:AO对象在函数执行上下文里面

  • 当我们创建了函数的时候,js引擎会重新开辟一块空间来进行存储(编译阶段)
  1. 保存父级作用域(函数的上一层作用域)
  2. 保存函数的执行体(就是执行的代码块)


  • 开辟的内存空间一般是0x开头
  • 我们在GlobalObject(全局对象)中一般是放入了我们函数的名字,例如foo。
  • 然后在全局对象中的foo再引用了对应存储函数的内存空间,也就是保存的是指向该函数的内存地址的指针
  • foo()的()是调用的意思,执行之后就会放入函数的调用栈中(调用栈会再创建出来一个函数执行上下文(Functional Execution Context),在里面会有一个类似GOglobal object的东西,叫做AOActivation Object
  • 在执行函数之前会先创建AO对象,会将函数的内容提升到AO对象中,此时函数里面的内容都是undefined,当我们在AO开始执行函数代码的时候,函数内的undefined逐渐被替换掉执行的内容
  • 函数里所有代码执行完了之后,函数的执行上下文就会被弹出栈,执行上下文就会销毁掉。此时AO如果没有人指向它的话,也会跟着一起销毁掉
  • 如果后续在后面又调用了一遍foo(),然后没准还传了一些参数进去,那刚刚的过程又会重复执行了一遍

作用域链

  • 当我们查找一个变量时,真实的查找路径是沿着作用域链来查找的
  • 作用域链组成
  • VO(variable Object 变量对象)
  • Parent(父级作用域)
  • 只有函数才会产生作用域,父级作用域在编译阶段就被确认了

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

  • 函数里面如果嵌套函数的话,这个时候进行执行的时候,嵌套函数是没有被编译的,而是预编译。等AO对象被创建的时候,它才会被正式编译
var name="why"
foo(123)
function foo(num){
  console.log("1",m);
    var n = 10
    var m = 20
    
    function bar(){
        console.log("2",name)
    }
    bar()
}
//结果如下
1 undefined
2 why
  • 此时如果我们把var name="why"注销掉,会发现控制台的2 why并没有消失
  • 因为windows的属性上面本来就有name这个属性,当我们注销掉的时候,值已经保存在windows的name里面了,控制台打印一下windows能找到

image.png

  • 所以最好的办法就是我们给name换一个变量名,比如XiaoYu
  • 然后重复上面操作注销掉XiaoYu这个变量,此时就出现我们想要看到的结果:找不到了

image.png

函数调用函数执行过程

  • 函数调用函数的作用域查找
//message打印出来的是哪里的内容
var message = "小余"
function foo(){
    console.log(message)
}
function bar(){
    var message = "大余"
    foo()
}
bar()
//打印结果:小余


  • bar()执行的时候会调用一个AO,也就是函数执行上下文
  • AO里面是一个message,此时还是undefined。下一步往里面填入"大余"
  • 再下一步执行foo(),这个时候就又创建出来一个执行上下文的空间用来执行bar里面的foo函数。这个执行上下文就又是一个AO,但此时这个AO里面是空的
  • 此时要取找message,是按照作用域链去查找的,查找的话首先是从自己身上查找,foo自己身上是没有message的,这里的执行上下文是空的,那下一个就是父级作用域,注意这里的父级作用域,不是看你foo()在哪调用的上一级,而是你函数写在哪的上一级,foo函数跟bar函数是并列的,他们的上一级都是全局作用域了,在全局作用域上面的也就只有var message="小余",所以打印的结果为小余
  • 对于函数来说AO其实就是VO

以上的都是ES6之前的概念(也就是ES5)

新的ECMA标准-变量环境和环境记录

在最新的ECMA的版本规范中,对一些词汇进行了修改

每一个执行上下文会关联到一个变量环境(Variable Environment)中,在执行代码中变量和函数的声明会作为环境记录(Environment Record)添加到变量环境中。


对于函数来说,参数会被作为环境记录添加到变量环境中


  • 解释
  • VO对象(对应AO)被Variable Environment(变量环境)替代
  • VO对象(对应GO)被VE,也就是Variable Environment,都是变量环境进行了替代
  • 变量环境里的是环境记录


作用域提升面试题

面试题1

在foo()里面没有声明n,自己没有就会向父级作用域去找,在父级作用域找到var n = 100,然后进行了覆盖操作,此时我们再打印n出来就是200了


我们打印的时候,console.log(n)的n其实不是值,而是存放指向值存放的内存空间的指针

var n = 100
function foo(){
    n = 200
}
foo()
console.log(n)
//答案是200

面试题2

函数内的是foo对象,函数外面的是go(global Object)对象、下面全部统一称呼go对象


流程步骤:


  1. 最先触发var m = "大余",go对象:{m:undefined} => {m:"大余"}
  2. 然后执行foo()函数,创造出一个AO对象(执行上下文),此时还处于编译状态,AO对象:{m:undefined},注意了,这个时候查找顺序是从自身开始查找,自身没有才去父级查找,而大余这个变量信息是在foo()的父级作用域中,所以之后才会去查找。此时执行上下文里面要去打印console.log(m),先在自己的作用域链中查找,而此时m已经在自身作用域中被赋值了一个undefined,但为什么不往父级找呢?首先这边执行上下文并没有结束,而在下面的时候就找到了var m = "小余"了,所以他就不会在将AO对象的执行上下文都执行结束后再去父级作用域寻找
  3. 最后执行了执行上下文中var m = "小余",将AO对象里的m的内容替代成了小余,,然后继续往下执行打印,打印出结果
function foo(){
    console.log(m)
    var m = "小余"
    console.log(m);
}
var m = "大余"
foo()
//结果如下
//undefined
//小余

面试题3

跟前面已经大同小异了


  1. foo1函数的执行结果,如果自身作用域内没有找到n,就会沿着父级作用域寻找,然后foo1是在foo2函数内调用的,父级作用域并不取决于在哪调用,而取决于你函数体处于哪里,foo1的作用域是跟foo2的作用域平级的,他们的父级作用域都是最外层的全局作用域。
  2. 然后foo2内部首先自己创建出来一个AO对象,再AO对象里创建一个执行上下文,里面先对编译阶段的{n:undefined}进行赋值200,然后通过console.log进行了打印,接着调用了foo1()函数,这foo1()函数答案为一百,在上一步中我们已经进行分析了
  3. 接着就是调用了foo2(),先打印了foo2中赋值的200,再打印foo1中的100。最后打印了最外层的n,100。这里最外层的打印只能打印100,100如果注销掉就报错,因为显而易见的,全局作用域基本上已经是最大的作用域了,再往上就找不到了,而这个是不会向函数内部去往下找的,且函数执行完后,他的执行上下文就销毁掉了
var n = 100
function foo1(){
    console.log("这是foo1内部",n);
}
function foo2(){
    var n = 200
    console.log("这是foo2内部",n);
    foo1()
}
foo2()
console.log("这是最外层",n);
//执行结果顺序如下
//这是foo2内部 200
//这是foo1内部 100
//这是最外层 100

面试题4

  1. 首先最外层,一个GO对象(Global Object):{a:undefined,foo:0xa00},foo的0xa00是内存地址,然后a被赋值为100
  2. 然后到foo函数部分,生成AO对象,AO对象里面是执行上下文,首先a的内容肯定是先为undefined,接着就return了,后面的var a = 100都还没生效foo函数就结束了,在编辑器中会给出提示:检测到无法访问的代码。但是还是请注意,这个执行上下文中还是出现了a这个变量,虽然完全没有用上,但是他意味着我们的执行上下文中还是出现了a这个变量,阻止了我们向父级作用域继续寻找的道路,所以我们访问不到全局作用域的100
  3. 最后就只能返回undefined了
var a = 100
function foo(){
    console.log(a)
    return
    var a = 200
}
foo()
//undefined

面试题5

var a = b = 10会转化为两行代码


  • var a = 10
  • b = 10(没错,b没有被var声明),从右向左,先给b赋值


所以很显然,外面作用域是访问不到a,但是能访问到b的,不然你把console.log(a)注释掉,就可以正常显示控制台信息的b为10了

function foo(){
    var a = b = 10
}
foo()
console.log(a);
console.log(b);
//会报错

作用域补充

没有声明直接使用,严格来说,语法都错了,应该要报错的,因为我们甚至不知道这个变量是怎么来的,但是JavaScript的语法太灵活了,他允许了这种写法,但是最好不要这样写,就当作了解就行

function foo(){
    m = 200
}
foo()
console.log(m);
//200

内存管理

认识内存管理

  1. 不管什么样的编程语言,在代码的执行过程中都是需要给它分配内存的,不同的是某些编程语言需要我们自己手动的管理内存某些编程语言自动帮助我们管理内存
  2. 不管以什么样的方式来管理内存,内存的管理都会有如下的生命周期
  • 第一步:跟配申请你需要的内存(申请)
  • 第二步:使用分配的内存(存放一些东西,比如对象等)
  • 第三步:不需要使用时,对其进行释放


  1. 不同的编程语言对于第一步和第三步会有不同的实现
  • 手动管理内存:比如C、C++,包括早起的OC,都是需要手动来管理内存的申请和释放的(malloc和free函数);
  • 自动管理内存:比如Java、JavaScript、python、Swift、Dart等,它们有自动帮助我们管理内存
  • 通常情况下JavaScript是不需要手动管理的,JavaScript会在定义变量的时候为我们分配内存


//创建对象
//Java代码
Person p = new Person()
//JavaScript代码
var obj = {name:"why"}

内存分配方式

  • JavaScript对于基本数据类型内存的分配会在执行时,直接在栈空间进行分配
  • JavaScript对于复杂数据类型内存的分配会在堆内存中开辟一块空间,并且将这块空间的指针返回值变量引用。我们一般也称呼这个为引用类型
  • 栈空间存放的是地址,真正的对象实例存放在堆空间中

知识点补充:

  1. 简单类型和复杂类型
    简单类型
    简单类型又叫做基本数据类型或者值类型
  • 值类型: 简单数据类型/基本数据类型,在存储变量中存储的是值本身,因此叫做值类型
    String、number、Boolean、undefined、null
  • 复杂类型
    复杂类型又叫做引用类型
  • 引用类型: 复杂数据类型,在存储变量中存储的仅仅是地址(引用),因此叫做引用数据类型,通过new关键字创建的对象(系统对象、自定义对象),如Object、Array、Data等

JavaScript的垃圾回收

  • 因为内存的大小是有限的,所以当内存不再需要的时候,我们需要对其进行释放,以便腾出更多的内存空间
  • 在手动管理内存的语言中,我们需要通过一些方式自己来释放不再需要的内存,比如free函数
  • 但是这种管理的方式其实非常的低效,影响我们编写逻辑的代码的效率
  • 并且这种方式对开发者的要求也很高,并且一不小心就会产生内存泄露


  • 所以现在大部分现代的编程语言都是有自己的垃圾回收机制
  • 垃圾回收的英文是Garbage Collection,简称GC
  • 对于那些不再使用的对象,我们都称之为垃圾,它需要被回收,以释放更多的内存空间
  • 而我们的语言允许环境,比如Java的运行环境JVM,JavaScript的运行环境js引擎都会内存(内置) 垃圾回收器
  • 垃圾回收器我们也是简称GC,所以在很多地方你看到的GC其实是指垃圾回收器


  • 关键问题:GC怎么知道那些对象不再使用的呢?
  • 涉及到GC的算法 =>作为子标题内容进行扩展


常见的GC算法 - 引用计数

对象里面有一个专门的空间,叫做 retain count,专门记录有多少个指针指向自己的retain count(一个指向加1),默认为0,但通常最少是1,因为你在栈里面存放的地址已经就指向堆内存了),这个计数器(retain count)是实时更新的,当这个计数器为0的时候,垃圾回收机制就知道这个对象已经没有人在使用了,就会触发回收机制销毁掉


引用计数弊端:


  • 循环引用
var obj1 = {friend:obj2}
var obj2 = {friend:obj1}
//这样互相引用如果不obj1 = null结束的话,会产生内存泄漏的


var obj = {name:"小余"}
var info = {name:"大余",friend:obj}
var p = {name:"超大余",friend:obj}


内存管理和内存泄露(闭包、作用域链)(二)https://developer.aliyun.com/article/1470366

目录
相关文章
|
30天前
|
JavaScript 前端开发 Java
内存管理和内存泄露(闭包、作用域链)(三)
内存管理和内存泄露(闭包、作用域链)
24 0
|
30天前
|
自然语言处理 JavaScript 前端开发
内存管理和内存泄露(闭包、作用域链)(二)
内存管理和内存泄露(闭包、作用域链)
28 0
|
3月前
|
存储 JavaScript 前端开发
|
5月前
|
存储 编译器 C语言
深入理解C++内存管理:指针、引用和内存分配(下)
深入理解C++内存管理:指针、引用和内存分配
|
1月前
|
存储
内存管理之内存释放函数
内存管理之内存释放函数
15 0
|
3月前
|
存储 安全 架构师
内存泄漏专题(9)内存池陷阱
内存泄漏专题(9)内存池陷阱
25 0
|
8月前
|
存储 编译器 C++
【C/C++】 静态内存分配与动态内存分配
C/C++ 中静态内存分配与动态内存分配相关内容,区别与比较
193 0
|
9月前
|
存储 JavaScript 前端开发
JS进阶(三) 闭包,作用域链,垃圾回收,内存泄露
闭包,作用域链,垃圾回收,内存泄露 1、函数创建 创建函数 1、开辟一个堆内存(16进制的内存地址) 2、声明当前函数的作用域(再哪个上下文创建的,它的作用域就是谁) 3、把函数体内的代码当作字符串存储在堆内存当中(所以不执行没有意义) 4、把函数的堆内存地址类似对象一样放到栈中供对象调用 执行函数 1、会形成一个全新的私有上下文(目的是供函数中的代码执行),然后进栈执行 2、在私有上下文中有一个存放私有变量的变量对象 AO(xx) 3、在代码执行之前要做的事情 - 初始化它的作用域链<自己的上下文,函数的作用域> - 初始化this (箭头函数没有this) - 初始化Arguments实参
65 0
|
5月前
|
存储
内存管理函数
内存管理函数
|
5月前
|
存储 Java 编译器
深入理解C++内存管理:指针、引用和内存分配(上)
深入理解C++内存管理:指针、引用和内存分配