还在因为搞不懂js闭包底层原理而沮丧吗,一文带你解决js闭包大boss

0 阅读4分钟

前言

初学 JS 的小伙伴,多半都被闭包绕得晕头转向。明明代码看着不长,作用域变量却捉摸不透;函数嵌套调用后数据莫名留存,销毁时机始终摸不清;想用它封装数据、缓存信息,稍不留意就出现内存隐患,越调试越一头雾水。

本文抛开抽象晦涩的专业术语,用通俗唠嗑的方式拆解闭包本质。理清作用域嵌套、变量留存核心逻辑,梳理常见使用场景与避坑要点。在深入理解js闭包前,我们已经有了v8预编译执行与类型的基础,这对于我们理解闭包也有重要作用。聊闭包之前,我们先说一个前沿知识---作用域链。

作用域链

每一个执行上下文的变量环境中都存在一个 outer 指针,用来指向外部的执行上下文, 当v8 在查找一个变量时, 会先在当前执行上下文的变量环境中查找,如果不存在,就会沿着 outer 指针指向的那个执行上下文查找,依次类推直到找到全局为止, 我们把这个查找的链条称作作用域链。下面给一个代码和图示,供你参考。

function bar(){
    console.log(myname);

}
function foo(){
    var myname = '张三';
    bar();
}
var myname = '李四';
foo();

输出结果为

image.png

为什么呢?接下来我给出该段函数执行的图示加以理解:

image.png 代码执行时,v8会顺着红色箭头寻找myname的值,先找bar执行上下文,找不到,就顺着outer指针指向的全局上下文中找到myname为“李四”,输出。

闭包

前言知识

根据v8的特性,一个函数执行完毕后,他的执行上下文会被销毁那么我们用一段代码来具体了解这一特点

function foo() {
    var myName = '赛哥'
    var age = 18
 function bar(){
    console.log(myName);
 }
return bar
}
var baz = foo()
baz()

image.png 接下来,我们依然用图解方法来直观了解v8函数执行完毕后,他的执行上下文会被销毁这一特性

image.png 那么问题来了,foo函数执行上下文被删除了,那我bar函数执行上下文中的outer指针将何去何从,这就有必要引出今日boss---闭包来解决问题了。

概念

根据作用域的查找规则,内部函数一定可以访问外部函数中的变量 当一个外部函数中的内部函数被拿到外部函数之外来执行,哪怕外部函数执行完毕被函数调用栈销毁,被内部函数引用的那部分变量依然需要被保留,这部分变量集合称为闭包。

image.png 如图所示,v8会在会在销毁foo执行上下文在被销毁时,会留下自己最后的火种:一个装了变量myName的小包裹。那这样bar函数的outer指针就可以指向这个闭包。这样设计有得有失:

  • 缺点·内存泄漏
  • 优点·定义私有模块,防止全局变量污染
  • 优点·延迟执行
  • 优点·事件绑定

闭包应用

接下来到达面试题拷打环节,

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)} ()
}

该段代码输出什么呢,聪明如你肯定知道,11行for循环输出arr[]其实是执行function() {console.log(i)} ()函数,而第三行for循环到第五次时,i=6,而函数从上往下执行,到十一行时,数组长度为五,执行五次function,而function中输出为6,因此结果应为五个6.

image.png 那么面试官问你如何做到不动11行for循环如何做到让代码输出1,2,3,4,5呢? 那么,要解决这个问题,我们就要从function出发,思考他为什么一直是输出6,对了,究其根本,还是因为每次循环后i的值不能保存。那么我们想想今天的主角闭包有什么优点呢?归根结底会出现5个6,是因为所有的i都指向了全局的i,那么看

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)} ()
}
  1. 循环执行时 :每次循环都会立即调用 fn(i) ,将当前的 i 值作为参数传入
  2. 函数参数传递 :JavaScript 中函数参数是 按值传递 的,每次调用 fn(i) 时, j 会获得当前 i 的副本(1, 2, 3, 4, 5)
  3. 闭包形成 : arr.push 中的匿名函数捕获了 fn 函数作用域中的 j 变量
  4. 结果 :每个匿名函数都绑定了各自循环迭代时的 j 值 输出结果:1, 2, 3, 4, 5

image.png