彻底搞懂JS闭包:从作用域链、形成条件到优缺点

我们都知道函数包函数,内部函数可以访问外部变量 ,那是因为作用域的规则在这里,但是如果你了解了作用域链 ,你就会发现,好像有点bug 在这里,所以这个bug 就是我们今天要一起探讨学习的闭包了。

一、作用域链

想要知道什么是闭包,那你一定得先搞明白什么是作用域链。

1.执行上下文 & outer指针

我们知道在 v8 执行每段JS代码的时候都会生成执行上下文,每一个执行上下文都有两个部分,一个是变量环境(存放 var 定义的变量和引用复杂类型的变量),一个是词法环境(存放 let 与 const 定义的变量),其中还有一个重要的东西-outer 。它是一个指针,永远指向当前代码的外层执行上下文,全局执行上下文的outer是null。

outer 复制代码
var myName = "游总"
function bar (){
    console.log(myName);
}
function foo (){
    var myName = '刘局'
    bar ()
}
foo ()

代码的调用栈我们如图所示,我们先来看看outer 的指向,首先是第一个函数bar ,它定义在全局内,所以outer指向全局 ,第二个函数foo ,它也是定义在全局内,outer也指向全局

图1.1-outer指向

2.作用域链的查找规则

v8 在查找一个变量时,在当前执行上下文中没有找到,就会顺着 outer 所指向的那个执行上下文 查找,以此类推,直到找到全局为止,我们把这个查找的链条称为作用域链,如图1.1所示。

二、闭包 closure

1.两个核心规则

  1. 一个函数执行完毕之后,它的执行上下文会被销毁;
  2. 内层函数可以通过作用域链访问外层变量。
闭包 复制代码
function foo() {
  var myname = '张老师'
  var age = 18
  function bar() {
    console.log(myname);
  }
  return bar
}
var baz = foo()
baz()

我们先画出这段代码的调用栈,因为函数bar 定义在函数foo 里面,所以bar 函数执行上下文的outer 指向foo函数的执行上下文foo 函数执行上下文的outer 指向全局 。在 v8 运行完 var baz = foo() 后,baz 被赋值为函数 bar ,所以baz()其实是调用函数bar ,但是函数foo 的执行上下文被删除了,那函数bar 就找不到myname 了,所以 v8 为了不造成这样的结果就留下来一个闭包

图2.1-闭包调用栈
当一个外部函数中的内部函数被拿到外部函数之外的区域执行的时候,哪怕外部函数已经执行完毕,被内部函数引用的那部分变量依然需要被保留,我们把这部分变量的集合称之为闭包。

2.形成条件

  1. 函数嵌套(内外两层)
  2. 内层应用外层局部变量
  3. 内层函数在外部被持续使用

三个条件缺一不可。

3.优点

  1. 实现了私有变量:外部无法直接修改内部数据
  2. 避免了全局变量污染:所有的变量封存在函数内部 不过闭包还存在一些缺点:闭包会持续持有外层变量,滥用会导致内存越来越大、卡顿,甚至爆栈。

三、例题

例题 复制代码
var arr = []
for (var i = 1; i <= 5; i++) {
    arr.push(function() {
      console.log(i);})
}
for (let n = 0; n < arr.length; n++) {
  arr[n]()  // function() {console.log(i)} ()
}

1.let方法

如上述代码所示,我们该怎么只修改第一个for循环里面的内容,从而使输出结果是1 2 3 4 5,我们可以知道现在代码执行结果是 6 6 6 6 6 。我们有第一个方法,直接把 var i = 1 变成 let i = 1 ,这种方法是利用了词法环境作用域 ,如果是 var 定义,就会提升到全局作用域,每一次执行for循环就会被覆盖成新的,如果是let定义,JS 引擎会为每一轮迭代生成一套全新独立的词法环境,每一轮的 i 都保存在各自专属的词法环境中,互不覆盖,数组内的函数形成闭包,各自绑定创建时对应的词法环境,所以结果就是 1 2 3 4 5

2.函数方法

我们把函数内容改成,

闭包 复制代码
var arr = []
for (var i = 1; i <= 5; i++) {
  function fn(j) {
    arr.push(function() {
      console.log(j);
    })
  }
  fn(i)
}
for (let n = 0; n < arr.length; n++) {
  arr[n]()  // function() {console.log(i)} ()
}

采用函数嵌套的方式,使每一次函数循环都会留下一个存储变量的闭包,使得在后面循环调用函数的时候可以输出相对应的值。

结语

若其中内容有误或过浅,欢迎大家批评指正,Ciallo~ (∠・ω< )⌒★

相关推荐
di24k24k4 分钟前
多个 el-form 共用同一 ref 导致表单校验部分失效
前端·javascript·vue.js·elementui
JKooll29 分钟前
WPS 表格 DISPIMG 函数实战:用 JavaScript 将图片真正嵌入单元格
javascript·wps
晴天161 小时前
Rust 快速入门 (从 JavaScript 开发者视角)-Day09
开发语言·javascript·rust
其美杰布-富贵-李1 小时前
第 6 篇:相机与 OrbitControls
前端·javascript·three.js
Hilaku3 小时前
当 AI 一天写完一周的代码,我们还剩什么优势?
前端·javascript·程序员
无责任此方_修行中3 小时前
换个版本号就能升级?可没那简单:pnpm 11 升级踩坑记
javascript·后端·npm
尘世中一位迷途小书童3 小时前
无人机航线里的云台角度怎么调?Cesium 做了个视锥 + 鹰眼联动
前端·javascript·数据可视化
张元清3 小时前
React useMeasure Hook:用 ResizeObserver 测量 DOM 元素 (2026)
javascript·react.js
A24207349303 小时前
Vue3 + TypeScript:后端数据在表格内渲染后进行增删改的完整实现步骤
前端·javascript·typescript
他们叫我 悦儿遥遥雨3 小时前
.NET 8 Web开发入门(六):Blazor 全栈开发——告别 JavaScript 焦虑
前端·javascript·.net