重新认识一下“闭包”

本文深入探讨了JavaScript中闭包的概念,包括其定义、使用场景及如何通过闭包实现私有变量、回调函数等设计模式。同时,文章详细解析了闭包在执行上下文和作用域链中的工作原理,以及如何利用闭包解决常见的变量访问问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

也可在我的个人博客查看:个人博客——闭包

闭包

闭包就是能够读取另一个(外部)作用域变量的函数

使用场景

用于实现私有变量,回调函数,还有一些设计模式等等。

引入

在全局下声明一个变量 name 和函数 sayHello,并执行函数,那么实质该作用域就是一个闭包,而特殊的是其外部是全局作用域(唯一且持久)

var name = "dilomen";
function sayHello() {
  console.log("Hello " + name);
}
sayHello();

全局可能不是那么直观,那么就来看看局部作用域下,sayHello 方法依旧能访问到 person 内的变量

function person() {
  var name = "dilomen";

  function inner() {
    console.log("Hello " + name);
  }
  return inner;
}
var sayHello = person();
sayHello();

为什么变量在作用域消失之后还能被访问到呢?
在外部函数中声明内部函数时,在定义了函数的同时,还会创建一个闭包。闭包会包含函数的声明和该作用域下的所有变量,如上述例子中的 inner 和 name,所以当调用内部函数时,即便声明时的作用域已经消失了,但是还是可以通过闭包来获取声明作用域下的所有变量。由于会存储和引用额外的信息到内存,所以会导致一些性能的降低,所以不能过多使用闭包。

闭包会保存声明内部函数(inner)时作用域下的所有变量和函数声明:

在这里插入图片描述
构造函数也是同理,声明一个变量,这种方式往往用于私有变量的使用,然后通过声明内部函数,同时产生了闭包,在之后的实例中就可以通过调用内部函数来调取闭包内保存的变量

function Person() {
  var name = "dilomen";
  this.sayHello = function() {
    console.log("Hello " + name);
  };
}
var person = new Person();
person.sayHello();
!!!趁热打铁

当我们需要对一个函数内的变量进行每次累加更改时,我们就会使用到闭包,那么根据之前的知识能够解释这个原理吗?

function compute() {
  var num = 1;

  function add() {
    num++;
  }

  function get() {
    console.log(num);
  }
  return {
    add,
    get,
  };
}
var a = compute();
a.add();
a.add();
a.get(); // 4
var b = compute();
b.get(); // 1

首先闭包是在内部函数声明时产生的,所以当我们调用 compute 方法时,第一次声明了内部函数,也就是产生了一个闭包,而之后的 add 和 get 都是对这个闭包内的变量进行操作。当我们再次调用一个新的 compute 方法时,会产生一个新的闭包,因此变量也是作用域下的初始值。

执行上下文

要深入理解闭包,就必须先了解执行上下文和作用域(词法环境)的概念

执行上下文

  • 全局执行上下文:程序开始执行时,就创建了全局执行上下文,有且只有一个
  • 函数执行上下文:每次调用函数时就会创建一个函数执行上下文

js 执行机制
由于 JavaScript 是单线程的,所以只能在某一刻执行某个代码,而不能同时执行。
一旦函数发生调用当前的执行上下文就必须停止,并创建新的函数执行上下文,直到函数执行完成,将函数执行上下文销毁,返回到调用的执行上下文。所以被称为调用栈,是一种先进后出的数据结构。

function outer() {
  console.log("outer");
  inner();
}
function inner() {
  console.log("inner");
}
outer();
  1. 程序开始,就会创建一个全局执行上下文
  2. 首先全局调用了 outer 函数,所以创建了 outer 函数执行上下文
  3. 在 outer 里又调用了 inner 函数,所以有创建了 inner 函数上下文
  4. inner 函数执行完成,回到 outer 函数执行上下文
  5. outer 函数执行完成,回到全局执行上下文
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SaUhAGD8-1589005836060)(/JavaScript/js调用栈.png)]

同样也可以在 chrome 浏览器的调试中观察到
在这里插入图片描述

作用域

又称为词法环境,和执行上下文一样,作用域也分全局作用域和局部作用域。

  • ES6 之前,在 JS 中是没有块状作用域,不像别的语言{}包裹就是一个块状作用域,但是函数是有作用域的。try-catch 中的 catch 也是具有作用域的
  • ES 推出了新的变量声明,let,const 会创建一个块状作用域

执行上下文和作用域的区别

  • 执行上下文是指调用函数时触发的,主要是和当前对象的指向相关,所以会存在谁调用,this 就指向谁的问题

  • 作用域是函数声明或者变量声明(let,const)时就已经确定了的,主要是与变量的访问相关

    箭头函数就是指向声明时的作用域下的,所以不管谁调用都不会改变,说明作用域是声明时就确定的,而不是执行时

function outer() {
  console.log("outer");
  inner();
}
function inner() {
  console.log("inner");
}
outer();

所以以上代码,在函数声明时,outer 和 inner 就已经创建了函数作用域,这样,在每次调用时,都是相互独立的

那么由于函数有作用域,正常情况下,就只能内部获取外部的变量(通过执行上下文生成的作用域链来获取),那么如何才能让外部访问到内部的变量呢?这个时候我们就能通过闭包来实现。

作用域链
作用域链就是指程序在执行是未在当前作用域内找到对应变量,那么它就会去父级作用域上找,直到全局作用域,这种向上的作用域关系就是作用域链。

每一个通过闭包访问变量的函数都具有一个作用域链,作用域包含闭包的全部信息。

!!!趁热打铁

经过了执行上下文和作用域知识的完善,就可以解释以下这道经典闭包问题了

function test() {
  for (var i = 0; i < 5; i++) {
    setTimeout(() => {
      console.log(i);
    });
  }
}
test(); // 5 5 5 5 5

由于 for 循环没有作用域,所以 setTimeout 中的 i 根据作用域链得到的是 test 作用域下的 i,而这个 i 通过 for 循环已经变成了 5(同步代码在异步代码前)

或者之后调用也是一样,arr 各子项被赋值了,但是函数并未执行,所以此时程序并不关心函数中 i 的值,只有当该函数被调用时,那么程序会去找函数被定义时的作用域链,也就是 test,而此时 i 已经变成 5 了

function test() {
  var arr = [];
  for (var i = 0; i < 5; i++) {
    arr[i] = function() {
      console.log(i);
    };
  }
  return arr;
}

var myArr = test();
for (var i = 0; i < 5; i++) {
  myArr[i](); // 5 5 5 5 5
}

解决方式:就是添加一层作用域,将 test 的 i 变量保存在闭包中

包裹一层作用域,这样每个 for 循环都会产生一个闭包,这个闭包会记录每一个 i 值,异步代码就可以通过闭包访问到正确的 i 值
为解决上述问题,我们就可以通过闭包将每次的 i 保存下来

function test() {
  for (var i = 0; i < 5; i++) {
    (function(t) {
      setTimeout(() => {
        console.log(t);
      });
    })(i);
  }
}
test(); // 0 1 2 3 4

使用块状变量声明 let

function test() {
  for (let i = 0; i < 5; i++) {
    setTimeout(() => {
      console.log(t);
    });
  }
}
test(); // 0 1 2 3 4

补充

为什么闭包会造成内存泄露

  • 首先闭包的信息是存储在内存中的。
  • js 在函数结束时,会进行垃圾回收机制,但是由于闭包被引用了,那么根据标记清除法,它就不会在函数结束后回收。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值