你真的了解闭包吗?look in my eyes!

首先先看下概念:

红宝书:闭包 指的是那些引用 了另一个函数作用域 中变量的函数。

MDN: 闭包 是由捆绑起来(封闭的)的函数和函数周围状态(词法环境)的引用组合而成。换言之,闭包让函数能访问它的外部作用域。

csharp 复制代码
function func(){
  const a = 1;
  return function foo(){
    return a + 2;
  }
}

const bar = func();
bar()

理解闭包,我们需要先从作用域链来入手。

作用域链

先看一道例题,这段代码输出什么呢?

javascript 复制代码
function bar() {
  console.log(myName)
}
function foo() {
  var myName = "掘金"
  bar()
}
var myName = "掘金前端"
foo()

聪明的你肯定会知道这段代码输出的是"掘金前端";

那JS在查询这段代码的时候,是如何查找的呢? 查找的这个顺序 也就是作用域链。这个作用域链是由词法作用域决定的。

词法作用域

词法作用域是指作用域 是由代码中函数声明的位置决定的。和函数怎么调用是没有关系的。

了解了词法作用域之后, 就可以解释上面这个例题了。

  1. 变量提升 :function bar、function foo 以及 var myName 都会被提升到全局作用域的顶部。函数 bar 和 foo 会完整地被提升,而 myName 变量虽被提升但初始值为 undefined。
  2. 变量赋值:代码开始执行,myName被赋值为"掘金前端"。
  3. foo执行:创建foo的执行上下文,var myName 会被提升,然后被赋值为 "掘金"。不过这个 myName 是 foo 函数内部的局部变量。
  4. bar执行:创建bar的执行上下文,先在当前作用域中查找,找不到沿着作用域链向上查找, 根据词法作用域的解释,作用域是声明位置 决定的,所以bar函数的上层作用域是全局作用域 ,和foo没有关系 。所以找到了全局作用域中的"掘金前端"

前置知识了解完我们就可以看看开篇的例题了。

csharp 复制代码
function func(){
  const a = 1;
  return function foo(){
    return a + 2;
  }
}

const bar = func();
bar()

根据词法作用域的规则,内部函数foo总能访问外部函数func中的变量。所以虽然func函数执行完成之后,他的执行上下文弹出,但是由于foo总是需要使用变量a,所以这个变量依然存在内存中。

右边 Scope 项就体现出了作用域链的情况:Local 就是当前的 foo 函数的作用域,Closure(func) 是指 func 函数的闭包,最下面的 Global 就是指全局作用域,从"Local-->Closure(func)-->Global"就是一个完整的作用域链。

这样我们就可以理解了闭包的概念: 指的是那些引用 了另一个函数作用域 中变量的函数。

闭包了解到这里还远远不够。开始下一话题:

v8是如何实现闭包的呢?

首先是JavaScript的三个特性,这让闭包实现有了可能

  • JS允许在函数内部定义新的函数。
  • 可以在内部函数访问父函数中定义的变量。
  • 函数可以作为返回值。

我们再回过头来看,V8执行JS代码的时候,需要先编译再执行。但是V8并不会一次性将所有的JS解析为要执行的代码。如果一次性将所有代码解析完,第一是可能会造成卡顿,第二会占用很大的内存。

所以v8实现了惰性解析。

惰性解析

指再解析的过程中,如果遇到函数声明就会跳过函数内部的代码,不会为函数内部代码进行AST转换。

是不是看完这句话内心中有疑问了:你说函数会跳过,那我问你,如果函数解析被跳过了,那V8是如何知道这个函数有没有对外部的变量有引用呢?Tell me why? look in my eyes!

虽然采用了惰性解析,但是V8还是判断了当前函数是否引用了外部函数的变量, 这个事情是交给与解析器来做了。

预解析器

当开始解析代码的时候,预解析器做了两件事情:

  • 判断是否有语法错误
  • 检查函数内部是否引用了外部变量,如果有引用,就会将栈中的变量复制到堆中。

闭包有哪些作用呢?

封装私有化数据

ini 复制代码
function createCounter() {
    let count = 0;
    return {
        increment: function () {
            count++;
            console.log(count);
        }
    };
}
const counter = createCounter();
counter.increment(); // 输出 1
counter.increment(); // 输出 2

解决异步带来的问题

javascript 复制代码
for (var i = 0; i <= 5; i++) {
  setTimeout(() => {
      console.log(i); 
    }, 1000);
}
// 6 6 6 6 6 6


for (var i = 0; i <= 5; i++) {
  (function (j) {
    setTimeout(() => {
      console.log(j);
    }, 1000);
  })(i);
}
// 0 1 2 3 4 5 成功输出

闭包的注意事项

  1. 由于闭包可以创建私有变量,如果私有变量占的内存很大,或者闭包函数很多,会增大内存的消耗量。
  2. 可能会造成内存泄漏,
javascript 复制代码
function createLargeArray() {
    // 创建一个包含大量元素的数组
    const largeArray = new Array(1000000).fill(0);
    return function() {
        console.log(largeArray)
    };
}

let closureFunction = createLargeArray();

const button = document.querySelector('button');

button.addEventListener('click',closureFunction,{
  once:true
})

上面这段代码,执行完一次之后,createLargeArray就不会再被访问到了,所以对于我们来说这段代码是不会在使用到了,也就变成了垃圾,但是不会被回收。

改进

ini 复制代码
closureFunction = null

面试官:说一说对闭包的理解

闭包的是指那些引用了另一个函数作用域中变量的函数。表现为调用一个函数,返回了一个函数。因为JS的词法作用域,这样就让闭包可以存储私有变量,带来的问题是可能造成内存增加或者内存泄漏。之所以V8可以实现闭包,首先因为JS的特性,允许函数中声明函数,允许函数作为参数,允许函数作为返回值。其次JS在惰性解析的过程中,会使用预加载器对函数进行分析,分析出函数中是否引用了外部的函数中的变量,如果有就将这个变量复制到堆中,这样再函数执行完之后,也不会释放该变量。

结尾

  • 新手前端,请多多指教。
相关推荐
卸任4 小时前
Vibe coding工具使用心得
前端·ai编程
brien95 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance5 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M5 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写5 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李5 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易6 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马6 小时前
前沿视界
前端
kyriewen6 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易6 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员