你真的了解闭包吗?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 barfunction foo 以及 var myName 都会被提升到全局作用域的顶部。函数 barfoo 会完整地被提升,而 myName 变量虽被提升但初始值为 undefined
  2. 变量赋值:代码开始执行,myName被赋值为"掘金前端"
  3. foo执行:创建foo的执行上下文,var myName 会被提升,然后被赋值为 "掘金"。不过这个 myNamefoo 函数内部的局部变量。
  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在惰性解析的过程中,会使用预加载器对函数进行分析,分析出函数中是否引用了外部的函数中的变量,如果有就将这个变量复制到堆中,这样再函数执行完之后,也不会释放该变量。

结尾

  • 新手前端,请多多指教。
相关推荐
mldong6 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排7 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60617 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角8 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!8 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰9 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi10 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen11 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马11 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒12 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端