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

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

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

常见的GC算法 - 标记清除

  1. 这个算法是设置一个根对象(root object),其实就是GO(Global Object),垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于哪些没有引用到的对象,就认为是不可用(不可达)的对象
  2. 这个算法可以很好的解决循环引用的问题(因为被认为不可用的对象会在下一回中被回收掉)
  3. JS引擎笔记广泛的采用就是标记清除算法,当然类似V8引擎为了进行更好的优化,它在算法的实现细节上也会结合一些其他的算法

image.png

03_JS的内存管理和闭包

让人迷惑的闭包

  • 闭包是JavaScript中一个很难的知识点

JS中函数是一等公民

一等公民:当前的这个东西非常灵活且非常重要的,例如可以作为另外一个函数的参数或者返回值来使用


比如Java的对象就是一等公民

  • 在JavaScript中,函数是非常重要的,并且是一等公民:
  • 那么久意味着 函数的使用是非常灵活的
  • 函数可以作为另一个函数的参数,也可以作为另外一个函数的返回值来使用
  • 自己编写高阶函数
  • 使用内置的高阶函数
  • 把一个函数如果接受另外一个函数作为参数,或者该函数回返回另外一个函数作为返回值的函数,那么这个函数就称为一个高阶函数
  • 底下案例的封装函数小案例就是一个高阶函数
  • 函数作为参数、返回值使用
  • Vue3+react
  • vue3 composition api:setup函数->代码(函数hook,定义函数);
  • react:class->function->hooks
//作为另一个函数的参数,js语法允许函数内部再定义函数
function foo(){
    function bar(){
        console.log("小余的bar");
    }
    return bar
}
var fn = foo()
fn()
//小余的bar
//也可以作为另外一个函数的返回值来使用
function foo (aaaa){
    console.log(aaaa);
}
foo(123)
function bar(bbbb){
    return bbbb + "刚吃完午饭"
}
foo(bar("小余"))
// 123
// 小余刚吃完午饭
  • add5 = makeAdder(10)的意思是将10传入形参count中,然后你再调用add5(本质上这个时候add5调用的已经是add函数了),然后在add5中传入的数值将会传入num中。
  • 固定了第一层的数据,并没有完全写死,完全可以在add5中进行定义,不止是add5,我们可以定义var add100 = makeAdder(100),都是可以的,这样就可以去定制一些函数
  • 为什么我们makeAdder都调用完了,count为什么不会销毁,这就是闭包的使用
function makeAdder(count){
    function add(num){
        return count + num
    }
    return add
}
var add5 = makeAdder(10)
console.log(add5(6));
console.log(add5(66));
//16
//76

封装函数小案例

//封装小案例
function calc(num1,num2,calcFn){
    console.log(calcFn(num1,num2));
}
function add(num1,num2){
    return num1 + num2
}
function sub(num1,num2){
    return num1 - num2
}
function mul(num1,num2){
    return num1 * num2
}
calc(10,10,add)
calc(10,10,sub)
calc(10,10,mul)
//20
//0
//100

数组中的5个常用高阶函数使用

挑选偶数的方式

//普通使用
var nums = [2,4,5,8,12,45,23]
var newNums = []
for(var i = 0;i<nums.length;i++){
    var num = nums[i]
    if(num % 2 === 0){
        newNums.push(num)
    }
}
console.log(newNums)
//[ 2, 4, 8, 12 ]

filter过滤器

//高阶函数filter过滤器的使用
//filter,对数组进行过滤,是数组中的一个方法,传入三个参数(第一个是数组中的值,第二个是数组的下标,第三个是我们当前数组的引用=>就是整个数组传进来),返回值是另外一个新的数组
var nums = [2,4,5,8,12,45,23]
var newNums = nums.filter((item,index,array)=>{
    return item % 2 === 0
})
console.log(newNums);
//[ 2, 4, 8, 12 ]

map映射

//高阶函数map映射的使用
//map:映射
var newNums2 = nums.map((item)=>{
    return item % 2 === 0 ? '偶数是女生' : '基数是男生'
})
console.log(newNums2);
//[ '偶数是女生', '偶数是女生', '基数是男生', '偶数是女生', '偶数是女生', '基数是男生', '基数是男生' ]

forEech:迭代

//forEech:迭代,没有返回值,通常就用来打印一些东西
var nums = [2,4,5,8,12,45,23]
nums.forEach((item)=>{
    console.log(item);
})
// 2
// 4
// 5
// 8
// 12
// 45
// 23

find:查找

//find:查找的意思,有返回值
var nums = [2,4,5,8,"小余",12,45,23]
var item = nums.find((item)=>{
    return  item === "小余"
})
console.log(item);
//小余
------
var item = nums.find((item)=>{
    return  item === "小余不见了"
})
console.log(item);
//undefined
------
var friend = [
    {name:"小余",age:18},
    {name:"大余",age:20},
    {name:"小满",age:23},
    {name:"喜多川",age:22},
    {name:"老鱼皮",age:23}
]
const findFriend = friend.find((item)=>{
    return item.name = "小余"
})
console.log(findFriend);
//{ name: '小余', age: 18 }
//findIndex,找到对象在数组在对象中对应的索引值
const findFriend = friend.findIndex((item)=>{
    return item.name === "小余"
})
console.log(findFriend);
//0

reduce:累加

//reduce:对我们原来的数组进行一些累加或者统计的操作
//普通实现方式
var nums = [2,4,5,8,12,45,23]
var total = 0
for(var i = 0;i<nums.length;i++){
    total += nums[i]
}
console.log(total);
//99
--------
//高阶函数reduce的使用
//reduce接收参数,第一个参数:上一个函数的返回值(例如我们数组中有7个数字,那就调用7次函数,第一个参数每次都调用上一次的内容)
//那第一次调用的时候没有上一个函数怎么办?我们可以在回调函数后面定义初始化的值,例如0
//prevValue(上一次的值):0 , item:2  prevValue是previousValue的简写
//prevValue(上一次的值):2 , item:4
//不停的将上一次的值跟下一次的值做一个处理,直到全部处理结束带着结果进行返回
var num = nums.reduce((preValue,item)=>{
    return  preValue + item
},0)
console.log(num);
//99

函数(Function)与方法(Method)的区别

  • 一般来说,其实是指同一个东西。
  • 函数(Function):独立的Funtion,称之为一个函数
  • 方法(Method):当我们的一个函数属于某一个对象时,我们称这个函数是这个对象的方法
  • 方法更像是定义在一些特殊地方的函数,函数包含得更大
var obj = {
    
    foo:function(){
        
    }
}
//这个foo就是一个属于obj对象的方法
//调用的时候
obj.foo()


闭包流程

闭包定义:

  • 闭包定义分为两个:在计算机科学中(因为闭包不是JavaScript特有的,在其他语言中也是有的)和在JavaScript中
  • 在计算机科学中队闭包的定义:
  • 闭包(Closure),又称词法闭包(Lexical Closure)或者函数闭包(function closures)
  • 是在支持头等函数的变成语言中,实现词法绑定的一种技术
  • 头等函数是指在程序设计语言中,函数被当作一等公民。这意味着,函数可以作为别的函数的参数、函数的返回值,赋值给变量或存储在数据结构中
  • 解析函数的时候,就会确定它的上层作用域,这是在词法解析的时候进行确定的
  • 闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(关联的自由变量)(相当于一个符号查找表)
  • 这个结构体在C语言中就是指一个结构
  • 但在JavaScript中,它其实是指一个对象,对象里面存储着一个函数和一个关联环境(想表达是一个整体)
  • 闭包跟函数最大的区别在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即时脱离了捕捉时的上下文,它也能照常运行(闭包核心观念)
  • 自由变量:假如在全局中定义了变量a,在函数中使用了这个a,这个a就是自由变量,可以这样理解,凡是跨了自己的作用域的变量都叫自由变量。
  • 脱离捕捉的上下文:在你函数的上下文之外的地方调用,你脱离了这个作用域范围能够调用,证明了本来该被销毁的自由变量却得以保存
  • 闭包的概念最早出现于60年代,最早实现闭包的程序是Scheme的,那么我们就可以理解为什么JavaScript中有闭包:
  • 因为JavaScript中有大量的设计来源于Scheme的。(Scheme是最早实现闭包的语言)
  • MDN对JavaScript闭包的解释:
  • 一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包
  • 换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。
  • 在 JavaScript 中,每当创建一个函数,闭包会随着函数的创建而被同时创建
  • 概括就是有函数就有闭包
  • 之所以会有函数就有闭包是因为,当函数被创建出来的时候,定义在最外层,它的上层作用域就是全局作用域,如果在函数内引用了全局作用域的内容,那也是形成了一个闭包
  • 理解总结:
  • 一个普通的函数function,如果它可以访问外层作用域的自由变量,那么这个函数就是一个闭包
  • 从广义的角度来说,JavaScript的函数都是闭包
  • 从狭义的角度来说,JavaScript中一个函数,如果访问了外层作用域的变量,那么它是一个闭包

高阶函数执行过程

function foo(){
    //预解析,前面有讲过
    function bar(){
        console.log("小余");
    }
    return bar
}
var fn = foo()
fn()
//小余

流程图

一旦我们想要调用函数,它会在执行栈里面创建一个函数的执行上下文


  • 这个时候不会马上调用函数执行的上下文,会先创建一个AO对象(函数执行之前创建)
  • 为什么不每个函数都创建AO对象呢?因为如果你如果每个都创建,当数量一多,就会创建很多个AO对象出来,当你都放着不调用,那岂不是就很浪费,所以设置当我们即将调用的前一刻会将AO对象创建出来,这样每个创建出来的AO对象都会被用上


  1. AO对象里面有一个bar,也就是我们刚刚上面代码块中的bar,在foo函数里面进行了return
  2. 这个bar存放的其实只是一个地址,原本全局对象GO(global object)里面的fn是underfined,现在变成bar的内存地址(类似0Xb00之类的东西)了
  3. 执行完之后,执行上下文会销毁掉
  4. 然后我们执行了fn(),此时我们应该注意到fn里面的内容其实已经是bar的内存地址了,所以我们执行的时候fn其实是通过bar的内存地址去进行指针指向执行
  5. 然后指向的对应ECStack调用栈的全局执行上下文又会创建出来一个函数执行上下文进行执行内容,执行完之后就会把这个函数执行上下文进行一个销毁
  6. 然后fn()就会打印出bar中的内容

闭包到底是什么

  1. 从var fn = foo()开始,这个时候在GO对象中,fn还是一个undefined
  2. 一样的,在执行foo的时候,会先创建出来一个foo的AO执行对象 => 里面有一个name为undefined跟一个预解析的bar函数,bar函数里面存放的是函数指针的一个引用(指向了bar函数创建出来的函数对象0xb00地址,0xb00是一个举例,不一定就是这个)
  3. 下一刻中将name中的内容填入,取代undefined,然后就是function bar(){xxx}并不执行,而是直接跳到return bar中,这里return返回的bar其实就是0xb00地址,所以在fn = foo()的fn就能拿到你返回的0xb00地址(fn = 0xb00)。这个时候foo函数内的东西就都执行结束了,那这个对应的函数执行上下文就会销毁掉
  4. 在GO对象中的fn也会对应的替换成bar的指针地址0xb00
  5. 最后执行fn(),这个又是一个函数的执行,这个时候我们又会创建出来一个函数的执行上下文,但是这次的函数执行上下文,其实就是bar的执行上下文,在第3点中我们已经能感受到替换成bar的过程了。创建bar的AO对象,然后有创建对应的执行上下文,首先里面是VO,VO对应的是AO,接着执行里面的内容,一个控制台打印命令,"小余"是字符串,能够直接被打印出来,但是,这个时候,里面引用了一个name,这个时候name应该要沿着作用域链去查找(VO+parentScope),VO里面没有找到,在父级foo对象中找到了name,foo对象在定义的时候就已经确定了。我们在bar函数对象0xb00中除了包含了代码执行体之外,还包含了parentScope:foo的AO对象(就是上面闭包定义中说的词法解析的时候),所以能够打印出来name的内容


  • 当我们在调用fn函数的时候,就已经形成闭包了,因为我们在var fn = foo()执行的时候,foo函数就已经执行完了,然后return返回了bar这个内容,按道理来说,这个时候name就需要随着foo的函数执行上下文销毁掉了,但我们根据结果却依旧能够进行访问到name。这就是js内部帮我们实现的功能
  • 结论:
  • 闭包是两部分组成的,函数+可以访问的自由变量(bar本身加上它内部引用的自由变量形成闭包)


function foo(){
    var name = "小满不穿裤子"
    function bar(){
        console.log("小余",name);
    }
    return bar
}
var fn = foo()
fn()
//小余 小满不穿裤子
//可以访问name:test算闭包
//有访问到:test不算闭包
var name = "放寒假了"
function test(){
    console.log(name);
}
test()

补充:执行上下文跟作用域的区别:


当我们要执行函数的时候,就会创建出来一个环境,环境叫做执行上下文,执行上下文有我们的作用域还有作用域链

函数的执行过程的内存

image.png


foo的执行上下文销毁前后对比:

image.png

image.png

  1. 我们写了foo函数跟test函数,从foo()开始执行,这个时候会先创建出foo函数的函数对象(0xa00内存地址),然后函数对象里面包括了parentScope父级作用域跟函数执行体。
  2. 然后foo函数这个父级作用域parentScope在下面的代码块中指GO(0x100内存地址),没错,parentScope是指向一个内存地址(根据上图,我们能知道他们其实是一个互相引用的关系)。test函数 同理
  3. 然后foo执行的时候同理的创建出来对应的函数执行上下文,在执行上下文中,我们知道VO其实就是指AO,存放的AO其实也是内存地址,会对应的去进行引用,接着按顺序将name跟age进行了一次输出,覆盖掉了AO对象中name、age原本默认输出的undefined。输出完了内容之后,一样的会销毁掉执行上下文VO
function foo(){
    var name = "xiaoyu"
    var age = 20
}
function test(){
    console.log("test");
}
foo()
test()


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

目录
相关文章
|
1月前
|
JavaScript 前端开发 Java
内存管理和内存泄露(闭包、作用域链)(三)
内存管理和内存泄露(闭包、作用域链)
24 0
|
3月前
|
存储 JavaScript 前端开发
|
1月前
|
存储 JavaScript 前端开发
内存管理和内存泄露(闭包、作用域链)(一)
内存管理和内存泄露(闭包、作用域链)
58 0
|
1月前
|
存储
内存管理之内存释放函数
内存管理之内存释放函数
16 0
|
2月前
|
缓存 自然语言处理 JavaScript
10分钟带你深入理解JavaScript的执行上下文和闭包机制
JavaScript中的闭包源于计算机科学中的一种理论概念,称为“λ演算”(Lambda Calculus)。λ演算是计算机科学的基础之一,1930年由Alonzo Church提出,它是一种用于描述计算过程的数学抽象模型,也是函数式编程语言的基础。
|
9月前
|
存储 JavaScript 前端开发
JS进阶(三) 闭包,作用域链,垃圾回收,内存泄露
闭包,作用域链,垃圾回收,内存泄露 1、函数创建 创建函数 1、开辟一个堆内存(16进制的内存地址) 2、声明当前函数的作用域(再哪个上下文创建的,它的作用域就是谁) 3、把函数体内的代码当作字符串存储在堆内存当中(所以不执行没有意义) 4、把函数的堆内存地址类似对象一样放到栈中供对象调用 执行函数 1、会形成一个全新的私有上下文(目的是供函数中的代码执行),然后进栈执行 2、在私有上下文中有一个存放私有变量的变量对象 AO(xx) 3、在代码执行之前要做的事情 - 初始化它的作用域链<自己的上下文,函数的作用域> - 初始化this (箭头函数没有this) - 初始化Arguments实参
65 0
|
10月前
|
存储 JavaScript 前端开发
从执行上下文和作用域链理解闭包
从执行上下文和作用域链理解闭包
74 0
从执行上下文和作用域链理解闭包
|
存储 JavaScript 前端开发
一篇文章带你搞定javaScript变量作用域和内存问题(变量,作用域,垃圾收集,管理内存)
一篇文章带你搞定javaScript变量作用域和内存问题(变量,作用域,垃圾收集,管理内存)
58 0
|
JavaScript Java
面试题:闭包、作用域链、内存泄漏
面试题:闭包、作用域链、内存泄漏
什么是闭包?闭包的用途是什么?闭包的缺点是什么?
变量的作用域有两种:全局变量和局部变量; 函数内部可以直接读取全局变量; 在函数外部无法读取函数内的局部变量。 能够读取其他函数内部变量的函数,就是闭包
88 0

热门文章

最新文章