你真的了解闭包吗?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在惰性解析的过程中,会使用预加载器对函数进行分析,分析出函数中是否引用了外部的函数中的变量,如果有就将这个变量复制到堆中,这样再函数执行完之后,也不会释放该变量。

结尾

  • 新手前端,请多多指教。
相关推荐
Mike_jia几秒前
一篇文章带你了解一款强大的本地镜像库系统---Harbor
前端
_一条咸鱼_几秒前
Vue 框架组件模块之弹窗组件深度剖析(四)
前端
某哈压力大1 分钟前
制作一个简单的水印组件
前端·vue.js
小old弟2 分钟前
Git简明指南:从入门到基本操作
前端·git
Cutey9163 分钟前
解决在 UniApp 中,deep不生效的问题
前端·javascript·面试
阿丽塔~3 分钟前
React.memo()和 useMemo()的用法是什么,有哪些区别
前端·javascript·react.js
光阴独白3 分钟前
Apple Login for JavaScript
前端·apple
应该算是高级了吧4 分钟前
问问你:vue3中ref和reactive的底层实现逻辑一样吗?
前端·vue.js
习惯灬4 分钟前
ES6对象新增了哪些扩展?
前端·javascript
LTPP9 分钟前
自动化 Rust 开发的革命性工具:lombok-macros
前端·后端·github