JavaScript高级笔记-coderwhy版本(二)

本文涉及的产品
云解析 DNS,旗舰版 1个月
云解析DNS,个人版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: JavaScript高级笔记-coderwhy版本

JavaScript高级笔记-coderwhy版本(一)https://developer.aliyun.com/article/1469637

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对象都会被用上

image.png

  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()

闭包的执行过程

以下是我们已经非常熟悉的闭包过程,这次我们来看下他是怎么进行执行的,这次会解开我们之前还不了解的,为什么闭包会让本该执行完的执行上下文的自由变量不会被销毁掉

执行之前一样是非常熟悉的流程,直接上图啦

image.png

当foo开始执行之后

image.png

当foo执行完了之后:这个时候,bar的内存地址已经存放到fn中了(也就是fn已经指向bar了),并且在后续被fn()给调用了,所以不管foo的函数执行上下文有没有被销毁,都不会影响到bar的函数对象了(因为GO根对象的fn已经指向了bar函数对象了上面有介绍JavaScript的垃圾回收,也就是标记清除部分,让bar函数对象不被销毁),然后bar函数对象连锁反应又跟foo的AO对象相互进行引用了(最关键的是bar指向foo的AO对象,这是可达的部分),所以foo的AO对象也不会被销毁。这就是为什么bar引用的父级自由变量会得以保留的原因

image.png

我们接下来就要继续执行fn的函数执行上下文(bar的)了

image.png

image.png

当bar的执行上下文被销毁掉的时候,也不会影响闭包,因为根对象依旧指向着fn,也就是bar的函数对象,而bar函数对象的父级作用域parentScope指着foo的AO对象,所以脱离了捕捉时的上下文,它也能照常运行。自由变量依旧存在而没有被销毁

image.png

function foo(){
    var name = "xiaoyu"
    var age = 20
    function test(){
        console.log("这是我的名字",name);
        console.log("这是我的年龄",age);
    }
    return test
}
var fn = foo()
fn()
//这是我的名字 xiaoyu
//这是我的年龄 20

04_函数执行作用域链和深入闭包

闭包的内存泄漏

从上面的代码块中,我们可以知道,当bar函数不被销毁的时候,foo的AO对象就永远不会被销毁,因为我们bar要访问foo的AO对象里面的内容


  • 目前因为在全局作用域下fn变量对0xb00的函数对象有引用,而0xb00的作用域中AO(0x200)有引用,所以会造成这些内存都是无法被释放的


但如果我们的bar函数只执行一次,后面就再也不需要了,那这个AO对象一直保存着就没有意义了,该销毁的却一直保留着,我们就叫这个是内存泄漏

闭包内存泄漏案例

image.png

image.png

  • 只要arrayFns数组不被销毁,则createFnArray函数也会一直保留着不被销毁

V8引擎源码可以看到对数字的处理:(是在后面回顾的时候进行补充说明的)

image.png

function createFnArray(){
    // 创建一个长度为1024*1024的数组,往里面每个位置填充1.观察占了多少的内存空间(int类型,整数1占4个字节byte)
    //4byte*1024=4kb,再*1024为4mb,占据的空间是4M × 100 + 其他的内存 = 400M+
    //在js里面不管是整数类型还是浮点数类型,看起来都是数字类型,这个时候占据的都是8字节,但是js引擎为了提高空间的利用率,对很多小的数字是用不到8个字节(byte)的,8字节 = 2的64次方,所以8字节是很大的,现在的js引擎大多数都会进行优化,对小的数字类型,在V8中称为Smi,小数字 2的32次方
    var arr = new Array(1024*1024).fill(1)
    
    return function(){
        console.log(arr.length);
    }
}
//var arrayFn = createFnArray()
//arrayFn()
var arrayFns = []
for(var i = 0 ; i<100 ; i++){
    //createFnArray()//我们通过for循环不断调用createFnArray这个函数,我们没有使用任何函数去接收他,所以当他创建进入下一个循环之后就会马上被销毁掉
    arrayFns.push(createFnArray())
}

image.png

内存泄漏解决方法

image.png

//内存泄漏解决方法
function foo(){
    var name = "xiaoyu"
    var age = 20
    function test(){
        console.log("这是我的名字",name);
        console.log("这是我的年龄",age);
    }
    return test
}
var fn = foo()
fn()
fn = null//将fn指向null,null的内存地址为0x0。此时fn指向bar的指针就会断开了,AO对象跟bar函数对象就形成了一个对于根对象的不可达的对象,将再下次被销毁掉。注意,你把它置为null之后,不会马上回收的,会在发现之后的下一轮进行回收

AO不使用的属性

  • 我们来研究一个问题:AO对象不会被销毁时,是否里面的所有属性都不会被释放?
  • 下面代码中的name属于闭包的父作用域里面的变量
  • 我们知道形成闭包之后count一定不会被销毁掉,那么name是否会被销毁掉呢?会,没有被使用到的会销毁掉,V8引擎做的优化
function makeAdder(count){
    let name ="why" 
    return function (num){
        debugger
        return count + num
    }
}
const add10 = makeAdder(10)
console.log(add10(5));
console.log(add10(8));
//15
//18

闭包的内存泄漏测试

内存回收案例测试如下

image.png

如果我们连foo函数对象都不想要了,我们也来个foo = null,断掉了foo与根对象GO的联系,那下次foo函数也会被销毁,或者说垃圾回收掉


image.png 回收一半的内存

JS闭包引用的自由变量销毁

当我们除了声明了fn来接收foo()之外,又声明了baz同样子接收foo(),这个时候是又执行了一遍foo函数里面的bar部分,fn跟baz不是同时指向同一个地方,而是又创建了一个新的foo的AO对象跟bar的函数对象,当我们将fn指向null,将内存进行回收时的时候,销毁的也只是fn对应的bar函数对象跟foo()对象,而对baz产生的bar函数对象跟foo的AO对象没有任何的影响,毕竟baz是又重新走了一遍流程,baz跟fn是互相独立的(PS:foo的AO对象是由bar的父级作用域内存地址指向而产生出来的)

image.png

  1. foo的AO对象有bar在指向着,因为bar函数内含父级作用域foo的AO对象的内存地址且正处于引用状态,这个内存地址指向着AO对象,让AO对象不会被销毁掉,但是我们只是引用name这个自由变量,age并没有使用到,按照ECMA规范,正规AO对象都不会被销毁,当然也就包含了我们没有用上的age变量了
  2. 但是js引擎是非常灵活的,为了提高内存的利用率,这个可能永远使用不上的age属性是会被回收掉的,从而提高空余的内存空间,提高性能

image.png

闭包引用的AO对象属性销毁

通过debugger我们可以看到未使用的父级作用域的变量会被js引擎回收掉,如果引用了则不会

function foo(){
    var name = "why"
    var age = 18
    function bar(){
        debugger
        console.log(name)
    }
    return bar
    
}
var fn = foo()
fn()

image.png


JavaScript高级笔记-coderwhy版本(三)https://developer.aliyun.com/article/1469639

目录
相关文章
|
18小时前
|
JavaScript 前端开发
JavaScript高级四、高阶技巧
JavaScript高级四、高阶技巧
|
1月前
|
JavaScript 前端开发 Java
深入ES6:解锁 JavaScript 类与继承的高级玩法
深入ES6:解锁 JavaScript 类与继承的高级玩法
|
1月前
|
前端开发 JavaScript 安全
解锁 JavaScript ES6:函数与对象的高级扩展功能
解锁 JavaScript ES6:函数与对象的高级扩展功能
|
1月前
|
前端开发 JavaScript 安全
高级前端开发需要知道的 25 个 JavaScript 单行代码
1. 不使用临时变量来交换变量的值 2. 对象解构,让数据访问更便捷 3. 浅克隆对象 4. 合并对象 5. 清理数组 6. 将 NodeList 转换为数组 7. 检查数组是否满足指定条件 8. 将文本复制到剪贴板 9. 删除数组重复项 10. 取两个数组的交集 11. 求数组元素的总和 12. 根据指定条件判断,是否给对象的属性赋值 13. 使用变量作为对象的键 14. 离线状态检查器 15. 离开页面弹出确认对话框 16. 对象数组,根据对象的某个key求对应值的总和 17. 将 url 问号后面的查询字符串转为对象 18. 将秒数转换为时间格式的字符串 19.
37 3
高级前端开发需要知道的 25 个 JavaScript 单行代码
|
1月前
|
JavaScript 前端开发 算法
Box2D(现在通常称为Box2D.js或者其WebAssembly版本,Emscripten Box2D)是一个流行的2D物理引擎,用于模拟刚体动力学、碰撞检测与响应以及关节约束等物理现象
【6月更文挑战第16天】Box2D.js,基于C++的2D物理引擎,经Emscripten编译为WebAssembly,用于JavaScript游戏中的真实物理模拟,包含碰撞检测和关节约束。它提供高效的碰撞检测,易于扩展和定制物理属性。使用步骤涉及初始化世界、创建刚体、添加碰撞形状、模拟物理及处理碰撞事件。物理引擎虽提升逼真度,但也增加复杂性和性能需求。其他选项如p2.js、matter.js和ammo.js也可供选择。
60 8
|
2月前
|
前端开发 JavaScript
JavaScript新科技:PostCSS的安装和使用,2024年最新2024网易Web前端高级面试题总结
JavaScript新科技:PostCSS的安装和使用,2024年最新2024网易Web前端高级面试题总结
|
2月前
|
JavaScript 前端开发
web前端JS高阶面试题(1),高级开发工程师面试
web前端JS高阶面试题(1),高级开发工程师面试
|
2月前
|
JSON JavaScript 前端开发
js的版本
【5月更文挑战第7天】js的版本
31 1
|
2月前
|
JavaScript 前端开发 Linux
|
2月前
|
JavaScript 前端开发
JavaScript高级主题:什么是 ES6 的解构赋值?
【4月更文挑战第13天】ES6的解构赋值语法简化了从数组和对象中提取值的过程,提高代码可读性。例如,可以从数组`[1, 2, 3]`中分别赋值给`a`, `b`, `c`,或者从对象`{x: 1, y: 2, z: 3}`中提取属性值给同名变量。
24 6