JS - 闭包 & 作用域 & 内存泄漏

之前看了很多关于闭包概念的文章,看完后总是处于似懂非懂的状态,时间一长又忘了。最近重新找资料学习,在b站发现了后盾人这个教程视频:《第九章 这次把JS闭包给你讲得明明白白》,这个老师讲的很清晰,从作用域开始讲,到闭包的概念,再到闭包使用场景(商品排序),我觉得记牢一个知识点要联系它的使用场景,课程中的商品排序例子就是一个很好解释闭包的例子。挺推荐大家去观看学习的,特别是初学者、对闭包概念很模糊、看文章又看难理解的同学,有能力的同学去给老师投币哈哈哈!

闭包

1.学习闭包之前,先要了解JS的作用域,JS主要分为全局作用域、函数作用域

// 代码 1
<script>
  var a = 1;	// 全局

  function f () {
    var b = 2; // 局部
    console.log(a, b); // 函数内部可以访问全局环境中的变量
  }
  f();
  console.log(b); // Uncaught ReferenceError: b is not defined 全局环境不能访问局部环境的变量
</script>

以上代码产生的作用域图示: 

执行一个函数,会产生该函数的作用域,局部作用域可以访问全局作用域,反过来则不行

2.看一个失败的累加例子,利用这个例子再复习以下函数作用域的产生

// 代码 2
<script>
  function hd() {
    let n = 1;
    function sum() {
      console.log(++n);
    };
    sum();
  };
  hd(); // 2
  hd(); // 2
  hd(); // 2
</script>

这个例子不能达到n累加的效果,因为每次调用hd(),都会产生hd()函数作用域,以上代码产生的作用域图示: 

每次执行hd(),都会产生一个作用域,上述代码共产生了三个作用域,执行sum()时都是在累加自己作用域内的变量n,所以达不到累加效果。

3.利用闭包,改进上述代码的累加效果

利用闭包,可以使作用域A访问作用域B的内容,如全局环境访问局部环境的变量,可以理解为,这个局部环境的作用域被延长了!

// 代码 3
<script>
  function hd() {
    let n = 1;
    return function sum() { // 返回sum()
      console.log(++n);
    };
  };
  var a = hd(); // 变量a指向sum(),a属于全局环境,a通过sum()操作局部变量
  a(); // 2
  a(); // 3
  var b = hd();
  b(); // 2
  b(); // 3
</script>

以上代码产生的作用域图示: 

全局作用域中的变量a、b分别指向两个hd()作用域内的sum(),间接改变hd()作用域内的变量n,达到累加效果

代码 3中的hd()也可以改为返回对象,或创建实例的方式

// 代码 4
<script>
  function Hd() {
    let n = 1;
    function sum() {
      console.log(++n);
    }
    return {
      sum: sum
    }
  }
  let a = Hd();
  a.sum(); // 2
  a.sum(); // 3
</script>
// 代码 5
<script>
  function Hd() {
    let n = 1;
    this.sum = function () {
      console.log(++n);
    }
  }
  let a = new Hd();
  a.sum();
  a.sum();
</script>

再看一个复杂一点的例子,记住,局部作用域可以访问父作用域的内容,而反过来则不行(除非使用闭包)

// 代码 6
<script>
  function hd1() {
    let n = 1;
    return function sum() {
      let m = 1;
      return function show() {
        console.log('m: ', ++m);
        console.log('n: ', ++n);
      }
    }
  }
  // 执行hd1()返回sum(),执行hd1()()返回show()
  var a = hd1()();
  a(); // 执行show(),show()作用域内,可以访问到变量m、n
  a(); // 而且这个作用域才创建了一次,所以每次都访问这里面的内容
</script>

以上代码产生的作用域图示:

4.var的作用域

for循环中,var没有块的特性,存在于全局作用域中

// 代码 7
<script>
  for (var i = 1; i <= 3; i++) {
    console.log(i);  // 1, 2, 3
  }
  console.log(i); // 4
  console.log(window.i); // 4
</script>

加个setTimeout定时器,看看效果

// 代码 8
<script>
  for (var i = 1; i <= 3; i++) {
    setTimeout(function () {
      console.log(i); // 4 4 4
    }, 1000);
  }
  console.log(i); // 4
</script>

上述代码执行完,i的值等于4,定时器等待1000ms之后,寻找变量i,由于var没有块级作用域,只能逐层向上寻找,在全局作用域中找到i,此时i的值等于4,代码作用域图示:

改成let

// 代码 9
<script>
  for (let i = 1; i <= 3; i++) {
    setTimeout(function () {
      console.log(i); // 1, 2, 3
    }, 1000);
  }
  console.log(i); // Uncaught ReferenceError: i is not defined
</script>

代码作用域图示:

for产生三个块作用域,定时器结束时访问寻找到块内的变量i

* 使用立即执行函数产生作用域,实现上述操作

// 代码 10
<script>
  for (var i = 1; i <= 3; i++) {	// var没有块作用域
    (function (a) {	// 立即执行函数,也是一个独立的作用域
      setTimeout(function () {
        console.log(a); // 1, 2, 3
      }, 1000);
    })(i);
  }
  console.log(i); // 4
</script>

5.闭包的使用场景

教程中举例了一个闭包使用的场景,求数组区间,求商品价格区间,按价格排序等

// 代码 11
<script>
  var arr = [11, 22, 33, 21, 44, 99];
  var result = arr.filter((v) => {
    return v >= 20 && v <= 90
  })
  console.log(result);
</script>

上述代码求arr数组中,大于等于20并小于等于90的元素。这样写有一个弊端就是条件固定了20-90,不利于函数复用,使用闭包优化函数

<script>
  var arr = [11, 22, 33, 21, 44, 99];
  function between(a, b) {
    return function(v) {
      return v >= a && v <= b;
    }
  }
  console.log(arr.filter(between(10, 40)));
</script>

再看一个商品列表的例子,根据价格筛选商品

<script>
  var lessons = [
    {
      title: 'Flex布局详解',
      price: 99,
      click: 200
    },
    {
      title: 'React组件开发',
      price: 50,
      click: 100
    },
    {
      title: 'ES6入门教程',
      price: 89,
      click: 150
    }
  ];
  function betweenPrice(a, b) {
    return function (v) {
      return v.price >= a && v.price <= b;
    }
  }
  function order(field, type = 'asc') {
    console.log(type)
    return function (a, b) {
      if (type === 'asc')
        return a[field] > b[field] ? 1 : -1;
      else
        return a[field] > b[field] ? -1 : 1;
    }
  }
  // betweenPrice()返回的匿名函数,匿名函数引用a、b变量(a、b属于函数作用域内的变量)
  var filterL = lessons.filter(betweenPrice(10, 60));
  console.table(filterL )
  
  var sortL = lessons.sort(order('price', 'desc'));
  console.table(sortL);
</script>

2020/08/20更新

内存泄漏

内存不能正常的被垃圾回收机制回收,就称为“内存泄漏”

造成内容泄漏的例子:

1.局部环境中声明全局变量

function f() {
  a = 1;
}

2.定时器没有手动清除

clearTimeout()
clearInterval()

3.闭包

闭包在某些场景作用性很强,但也带来内存方面的问题。正常情况下,函数执行完之后的一段时间内,由垃圾回收机制回收内存,释放空间;使用闭包的情况下,由于全局环境一直保持对函数内部的引用,导致函数执行完不能被回收,占用内存空间

(下图为上述代码 6,变量a一直保持对show()的引用,导致整个hd1()函数都驻留在内存中)

4.DOM操作

再看一个获取HTML标签属性的例子,想达到的效果是:点击按钮,获取按钮的desc属性值

<body>
  <button desc="this is button 1">Button1</button>
  <button desc="this is button 2">Button2</button>

  <script>
    var btns = document.querySelectorAll('button')
    btns.forEach((item) => {
      item.addEventListener("click", function () {
        console.log(item.getAttribute('desc'))
        console.log(item)
      })
    })
  </script>
</body>

每次点击时,item.getAttribute('desc')一直保持对item的引用,item打印的结果是button标签,这个button将一直保留在内存中(如果标签很大,将长期占用大量内存)

改进代码:

<body>
  <button desc="this is button 1">Button1</button>
  <button desc="this is button 2">Button2</button>

  <script>
    var btns = document.querySelectorAll('button')
    btns.forEach((item) => {
      let desc = item.getAttribute('desc')
      item.addEventListener("click", function () {
        console.log(desc)
        item = null
        console.log(item)
      })
    })
  </script>
</body>

Vue文档:避免内存泄漏

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值