1

1. 什么是闭包

需要说到作用域、作用域链、变量提升、暂时性死区、执行上下文等

作用域:

  • 一套变量放在哪,如何访问、修改的规则
  • 在 js 编译阶段,遍历作用域查当前的变量是否存在,如果存在就忽略该变量,继续下一个。如果不存在就在现在的作用域创建一个
  • 在执行阶段,会在当前作用域查找该变量,如果找不到会向父作用域查找,直到最外层
  • JS 中共有 3种作用域:

    • 全局作用域:定义在全局的变量拥有该作用域
    • 函数作用域:定义在函数内的变量拥有该作用域
    • 块级作用域:

      • let、const 定义在 {} 中的变量拥有该作用域。
      • var 声明的变量,即便在块级作用域内定义也可以在块级作用域外面访问。
      • let、const 具有暂时性死区。既在定义他们的块级作用域内,在定义他们的前面是不能访问他们的。(var 可以,会被初始化为 undefiend)

        var me = 'xiuyan';
        
        {
        me = 'bear';
        let me;
        }
        
        报错原因:在块级作用域内有 let、const 定义的变量,在作用域开始旧会形成暂时性死区,不会去外层查找是否有该变量
  • 作用域链:
  • 作用域之间存在嵌套关系,就形成了作用域链

闭包:

  • 在一个函数内存在,既不是函数参数,又不是函数的局部变量的变量,这个函数就是闭包

词法作用域和动态作用域

  • 词法作用域:作用域链沿着定义的位置延伸,js 之这种
  • 动态作用域:作用域链沿着调用栈延伸
  • js 中如何修改

    • eval: 把一段 string 转为 js 代码执行
    • width:会单独创建一个作用域
    • 决定不要用他们写代码

2. 看代码说结果

for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

结过:4个5
原因:setTimout 延迟执行,定义的函数中没有 i,就会向外层作用域找,此时 i 已经是 5 了
如何改造:
-利用 setTimout 的第三个参数
for (var i = 0; i < 5; i++) {
    setTimeout(function(j) {
        console.log(j);
    }, 1000, i);
}
-利用函数缓存变量
for (var i = 0; i < 5; i++) {
    (function(j) {
      setTimeout(function() {
              console.log(i);
          }, 1000);
      })(i)
    
}
var a = 1;
function test(){
    a = 2;
    return function(){
        console.log(a);
    }
    var a = 3;
}
test()();

结果:2
原因:var a = 3 的定义被变量提升了,test 内部的函数会用它作用域内的 a 变量,所以是2.要注意是 function 定义的位置找,不是使用的地方往外层找

3. 开发中的应用

  • 实现私有变量:部分比较私密的变量,只想在对象内部生效,不能在外部访问
  • 实现偏函数、柯里化:可以实现将多个入参函数转为更少入参函数的方法

    function generateName(prefix, type, itemName) {
    return prefix + type + itemName
    }
    
    在使用时,第一个、第二个参数可能经常是固定的变量,这时就可以柯里化。
    function generateName(prefix) {  
    return function(type) {
        return function (itemName) {
            return prefix + type + itemName
        }    
    }
    }
    
    // 生成专属函数
    var countryName = generateName('中国')
    // 使用专属函数
    var cityName = countryName('广东')('广州')
    柯里化把所有的参数都分拆为一个,偏函数更加随意,可以差分为不同的个,例如:
    function generateName(prefix) {
    return function(type, itemName) {
        return prefix + type + itemName
    }
    }
    
    // 生成专属函数
    var countryName = generateName('中国')
    // 使用专属函数
    var cityName = countryName('广东','广州')
    

4. 是否会引起内存泄漏

正确使用不会引起内存泄漏,接下来介绍一下 js 的内存管理和内存泄漏

4.1. 内存管理

  • 分配内存:挖坑,要根据不同的类型分配不同的内容

    • js 基础类型:string、number、boolean、undefined、null、symbol。占用空间大小固定、体积轻量。放在栈内存中存储。
    • js 复杂类型:Object。占用空间大小不固定。放在堆内存中存储。
  • 读写内存:用坑

    • js 基础类型:获取时直接从栈中取走变量的值。
    • js 复杂类型:获取时,先从栈内存中获取对象的引用(堆内存中的地址),然后再去堆内存中查询,拿到具体的数据。
  • 释放内存:还坑,垃圾回收

    • 引用计数法:

      • 现代浏览器已经不用这种了
      • 内存中每一个变量都对应一个引用计数,当计数为0的时候释放内存
      • 存在的问题:无法删除循环引用的对象,容易引起内存泄漏
    • 标记清除法:

      • 从 window 对象开始,扫描所有可以通过根对象触达的变量,这些对象被标记为‘可触达’。
      • 遍历所有对象清除没标记的对象,并将标记恢复到默认。
      • 回收相应的空间
      • 缺点:回收的对象在地址上是不连续的,导致空间虽然被回收了,但是不连续,导致后续分配空间的不好使用,而浪费空间。而且不会立即回收垃圾对象
    • 标记整理法:

      • 标记阶段是一样的;
      • 清除之前会先整理空间,移动位置,使被释放的空间在一个连续的空间。
      • 新生代(存活时间短,占比小)+ 老生代。新生代(from + to),存活的从 from 移到 to,不存活的释放;然后互换位置;如果to中超过 25%,则放到老生代。老生代:将活的移动到一侧;清除边界外的所有;
    • 在实际中还做了一些优化:

      • 遍历一次对象的耗时可能比较久,在执行时存在一定的延迟。所以引擎会将垃圾回收分解为几个部分,各个部分分别执行,减小延迟。
      • 为了减少对执行的影响,垃圾回收只在 CPU 空闲的时候运行

    4.2. 内存泄漏

  • 意外的全局变量
  • 没有清除 setInterval、连续的 setTimout
  • 没有删除对 DOM 的引用

默_hjh
32 声望1 粉丝

« 上一篇
JWT