闭包

86 阅读6分钟

前奏曲:了解闭包,先了解前两项

1、环境和作用域

话不多说,直接看代码:

    ...
    <button>测试闭包</button>
    ...
    
    ...
    let title = '鼓浪屿'
    document.querySelector('button').addEventListener('click',()=>{
      console.log(title);
    })
    
    function add(){
      console.log(title)
    }
    add()
    ...

Tips:全局环境中的数据,如上:title和add不会被卸载掉(不被回收),除非人为的回收,比如:数据置空、关闭浏览器等。

    function add() {
      let num = 1;
      console.log(++num);
    }
    add(); // 2
    add(); // 2

Tips:调用两次函数,每调用一次函数,就生成不一样的内存地址,上文中的num的数据是不会共享的,即不会互相影响。

    function name1() {
      let num = 1;
      console.log(++num);
    }
    let fn1 = name1 // 这里是函数名的改变,并没有开辟新的空间
    console.log(name1, fn1); //都是:ƒ name1() {let num = 1; console.log(++num);}
    console.log(fn1 === name1,'one'); // true 'one' ,表面指向同一个函数地址
    fn1() // 2
    fn1() // 2

    function name2() {
      let num = 1;
      return function () {
        console.log(++num);
      };
    }
    let fn2 = name2(); //返回出去的函数储存在fn中,这个num变量一直在使用,所以不会被销毁
    console.log(name2, fn2);// ƒ name2() { let num = 1; return function () { console.log(++num) }} 和  ƒ () {console.log(++num);}
    console.log(fn2 === name2(),'two'); // false 'two' ,表面指向不同的函数地址
    fn2(); // 2
    fn2(); // 3

2、块级作用域和函数作用域

letvar都具备函数作用域:

// var是有函数作用域的,let也是有函数作用域
function add() {
  a = 1; // 在window中声明可以取到
  var b = 2
  let c = 3
}
add();
console.log(a); // 1
console.log(b); // ReferenceError: b is not defined
console.log(c); // ReferenceError: c is not defined

不易被发现的函数作用域:

// 1.
let arr = []
for (var i = 0; i < 3;  i++) {
  // console.log(i);
  arr.push(()=> i) // 函数是有函数作用域的
}
console.log(arr[0]()); // 3

// 2.
let brr = []
for (let i = 0; i < 3;  i++) {
  // console.log(i);
  brr.push(()=> i) // 函数是有函数作用域的
}
console.log(brr[0]()); // 0

image.png Tips:第一次用了var,不存在块级作用域,直接取得全局window的i;第二次用了let,存在块级作用域,直接取用块级作用域的数据

// 问题现象,如下:
let arr = [1, 2, 3, 4];
for (var i = 0; i < arr.length; i++) {
    setTimeout(() => {
      console.log(i); // 4、4、4、4
    }, 0);
}

// 解决方案1:let
for (let i = 0; i < arr.length; i++) {
  setTimeout(() => {
    console.log(i); // 0、1、2、3
  }, 0);
}

// 解决方案2:用函数模拟出,let的“块”的行为
for (var i = 0; i < arr.length; i++) {
  (function(j) {
    setTimeout(() => {
      console.log(j); // 0、1、2、3
    }, 0);
  })(i);
}

3.闭包特性及优缺点(本文的重点)

特性:

1、函数套函数;

2、内部函数可以直接使用外部函数的局部变量或参数;

3、变量或参数不会被垃圾回收机制回收 GC;

优点:

1、变量长期保存在内存中;

2、避免被全局变量污染;

3、私有成员的存在;

缺点:

1、由于闭包中的变量长期存储在内存中,或造成内存消耗,会造成内存泄漏;

2、引用的变量可能发生变化;

最简单的数据应用,如下:

// 如何取出数组中,任意区间的数据?
let arr = [1, 2, 3, 45, 50, 51, 90, 100, 600];
function fn(prev, next) { // fn函数的作用:为了传递prev和next的形参
  return function(item) { // 这个匿名函数才是filter真正的回调函数
    return item > prev && item < next;
  };
}

let res = arr.filter(fn(50, 100));
console.log(res); // [ 51, 90 ]

稍微复杂点的数据应用,如下:

// 创建一个累积和数组
const accumulate = arr =>
  arr.map(
    (function(sum) {
      return function(value) {
        return sum += value;
      };
    })(0)
  );
let res = accumulate([1, 2, 3, 4]); // [1, 3, 6, 10]
console.log(res);

内存泄露和垃圾回收

不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)

常见的内存泄露,如下:

  1. 错误使用全局变量
function fn() {
  a = "gulangyu"; //a成为一个全局变量,不会被回收
	this.b = 'gulangyu'// 函数自身发生调用,this指向全局对象window
}
// JS对未声明的变量都会挂载到全局对象上。只有在页面刷新或者关闭时才会释放内存。解决方案:使用let、const声明变量或者使用严格模式
  1. console方法的不删除
console.log(a) // 开发工具中可以查看这个信息,表明存在内存泄露
  1. 闭包中,返回的函数使用父级的变量
function add(name){
  return function(){
    return name
  }
}
let res = add('gulangyu')
console.log(res()); // gulangyu
// 因为add()函数被全局变量res引用,所以在add()函数内部的匿名函数不会被回收,也就是内层函数私有作用域不会销毁,处于随时被调用的状态;
// 由于闭包会携带其函数作用域,所以比普通函数占用更大的内存,减少闭包的使用;
// 如果闭包如果使用不当,可以导致环形引用(circular reference),类似于死锁.
** 值得一提的是:闭包不一定导致内存泄露!!! **
  1. dom泄露,元素存在变量中没释放
var a = document.getElementById('id');
document.body.removeChild(a);
// 虽然removeChild移除了a的dom元素,但由于存在变量a对它的引用,即DOM元素还在内存里面;
// 浏览器中的dom采用的是渲染引擎,而js采用的是v8引擎 1.浏览器的JS引擎与DOM引擎共享一个主线程。当调用DOM时,会将JS引擎挂起然后再启动DOM引擎。调用结束后,重启JS引擎继续执行。这种上下文的切换很耗性能。 2.很多调用DOM操作涉及重排和重绘,以确保返回值的准确,更消耗性能。
  1. 计时器,链式setTimeout和setInterval未使用clear清除计时器
    // setInterval, 不用多说,不clearInterval就会持续性执行。
    

谷歌浏览器怎么直观的查看内存泄露的情况呢?

image.png 图解:

图片左上角①:Record,点击后会记录一个时间段,下方JS Heap会形成一个内存占用的梯形图
图片左上角②:Starting profiling and reload page(开始分析并重新加载页面),重载,其他同上
图片左上角③:Clear,清空

   let a = 123

如下图: image.png

    let b = 1
    setInterval(() => {
      b++
    }, 500);

如下图: image.png

Tips:梯形图在一定时间内,不断的在上升,说明:一直有新的内存出现,即内存发生泄露

    // 代码段1
    function add() {
      let num = 100
      return function () {
        return num++
      }
    }
    let res = add()
    console.log(res()); // 100

如下图: image.png

    // 代码段2
    function add() {
      return function () {
        return 100
      }
    }
    let res = add()
    console.log(res()); // 100

如下图: image.png

Tips:可以看在上面的代码在浏览器中看到一个细节,把代码段1改成代码段2,梯形图并不会变成"水平线",只有当把浏览器关闭重新打开才能直接变成水平线,可见关闭浏览器可以强制清空内存的占用。

垃圾回收机制:

内存生命周期:

1.内存分配:当我们申明变量、函数、对象的时候,系统会自动为他们分配内存
2.内存使用:即读写内存,也就是使用变量、函数等
3.内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存

垃圾回收

原理:垃圾收集器会按照固定的时间间隔, 周期性的找出不再继续使用的变量,然后释放其占用的内存
策略:标记清除 和 引用计数