前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >你为什么学不好闭包

你为什么学不好闭包

作者头像
用户6901603
发布于 2024-06-07 12:46:59
发布于 2024-06-07 12:46:59
13300
代码可运行
举报
文章被收录于专栏:不知非攻不知非攻
运行总次数:0
代码可运行

因为,你没有遇到一个好老师,告诉你闭包的本质。本质的东西往往都是通俗易懂的,你只需要花费几分钟,就能彻底消化你自己独立摸索了几年都掌握不好的一个小知识。

这篇文章,我先用用一个简短的章节,来通过本质学习闭包,看看你能学好不。

0、案例

在 JavaScript 中,大多数情况下,函数就是一个作用域。在理解闭包之前,我们需要对作用域有一个非常准确的认知。

如果没有作用域,那么我们所有声明的变量、函数等都在全局中了,此时在多人协作或者复杂的项目中,不同的功能之间极大概率会出现命名重复,相互干扰等重大问题。因此,我们需要引入作用域机制,用于隔离不同功能块,避免他们相互影响。

作用域的本质就是隔离

但是,这种隔离是一种绝对隔离,如下所示,函数 a 与函数 b 之间,他们的内部变量是无法相互访问的。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function a() {}

function b() {}

问题就在于,这种绝对隔离覆盖不了所有场景。许多时候,我们又希望作用域之间的变量,是可以相互访问的。因此,我们需要隔离,我们也需要共享。那我们应该怎么办呢?

一个简单的办法就是把你希望他们相互访问的变量放到全局去

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var m = 20

function a() {
  console.log(m)
}

function b() {
  console.log(m)
}

这样,他们就能共同访问同一个变量。但是,我们需要尽量避免往全局中放变量,因为依然可能会相互干扰,所以把变量往全局放不是一个靠谱的方案。那又该怎么办?

一个简单的方案,就是再给他们套一个父级函数,并将他们想要共同访问的变量定义在父级作用域。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function p() {
  let m = 20
  function a() {
    console.log(m)
  }

  function b() {
    console.log(m)
  }
}

这样,我们就达到了目标。这,就是闭包。我们用一句话来总结一下这个现象:闭包的本质就是:局部数据共享

局部:指的是函数作用域,非全局。

数据:指的是 m

共享:指的是不同作用域之间能共同访问。

这样理解,闭包就非常简单了,以后,我们凡事看到有局部数据共享的地方,就一定是闭包在起作用。

闭包本身就是为了解决作用域隔离的前提下,需要局部数据共享的场景而出现的一个技术方案

因此,我们再来看看如下几个例子巩固一下这个结论。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function init() {
  var name = "Mozilla"; // name 是一个被 init 创建的局部变量
  function displayName() {
    alert(name); // 使用了父函数中声明的变量
  }
  displayName();
}
init();

这个例子中,作用域 init 与作用域 displayName 共享数据 name,因此闭包存在。

再来一个例子

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function makeFunc() {
  var name = "Mozilla";
  function displayName() {
    alert(name);
  }
  return displayName;
}

var myFunc = makeFunc();
myFunc();

这个例子中,作用域 makeFunc 与作用域 displayName 共享数据 name,因此闭包存在

我们要特别注意,特别容易理解错的地方是,闭包是否存在,只与是否存在局部数据共享有关,与函数是否存在返回值,返回方式,返回函数的调用方式无关。

最后一个例子,makeAdder 与 匿名函数共享数据 x,因此闭包存在。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function makeAdder(x) {
  return function (y) {
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2)); // 7
console.log(add10(2)); // 12

我们把刚才最开始那个共享 m 的例子稍作改动,感受一下是不是很熟悉,你有信心在这个基础之上把他扩展成一个发布订阅模块吗?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function p() {
  var count = 0
  
  function get() {
    return count
  }
  
  function set(value) {
    count = value
  }
  
  return { get, set }
}

const {get, set} = p()

最后,我们再来分析一下 MDN 上,针对闭包的定义

  • 闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

一个函数,表示一个作用域。

周边环境的状态,他这里特别说明了是词法环境,在 js 中,词法环境,就是词法作用域,所以这里表达的是另外一个作用域。

用通俗一点的方式来说,就是,一个函数,与另外一个函数的引用组合在一起,就是闭包。这里讲的就是局部数据共享

因此,总结下来说就是,作用域是为了隔离,闭包是为了在隔离的基础之上实现局部数据共享。

通过我这样的讲解,你理解了没?理解了之后,接下来再开始闭包的基础学习吧!

0、

在 JavaScript 中闭包是一个特殊的对象。

凡是没有将闭包,定义为对象的说法,都是错误的。

1、词法作用域

词法作用域并非 JavaScript 特有的说法,通常我们也直接称之为作用域。词法作用域表达的是一个静态关系,通常情况下,我们在代码编写时,语法规范就已经确定了词法作用域的作用范围。它具体体现在代码解析阶段,通过词法分析确认。

JavaScript 的词法作用域通过函数的 [[Scopes]] 属性来具体体现。而函数的 [[Scopes]] 属性,是在预解析阶段确认。

词法作用域是为了明确的告诉我们,当前的上下文环境中,能够访问哪些变量参与程序运行。在函数的执行上下文中,除了自身上下文中能够直接访问的声明之外,还能从函数体的 [[Scopes]] 属性访问其他作用域中的声明。

一个简单的例子

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const g = 10

function foo() {
  let a = 10;
  let b = 20;

  function bar() {
    a = a + 1;
    const c = 30;

    return a + b + c;
  }  

  console.dir(bar)
  return bar
}

foo()

仅从语法上,我们就可以知道,函数 bar 能访问的声明,除了自身声明的变量 c 之外,还能访问 foo 声明的变量 a 与 b,以及全局声明的变量 g。最后还能访问整个全局对象。

能够访问自身的变量 c,具体体现为当前函数上下文中创建的 Local 对象。而其他的,则全部都体现在函数的 [[Scopes]] 属性中。如图。

2、闭包

在上面例子里,函数 bar 的 [[Scopes]] 中,有一个特殊的对象,Closure,就是我们将要学习的闭包对象。

从概念上来说,闭包是一个特殊的对象,当函数 A 内部创建函数 B,并且函数 B 访问函数 A 中声明的变量等声明时,闭包就会产生。

例如上面的例子中,函数 foo 内部创建了函数 bar,并且在 bar 中,访问了 foo 中声明的变量 a 与 b,此时就会创建一个闭包。闭包是基于词法作用域的规则产生,让函数内部可以访问函数外部的声明。闭包在代码解析时就能确定。

从具体实现上来说,对于函数 bar 而言,闭包对象「Closure (foo)」的引用存在于自身的 [[Scopes]] 属性中。也就是说,只要函数体 bar 在内存中持久存在,闭包就会持久存在。而如果函数体被回收,闭包对象同样会被回收。

「此处消除一个大多数人的误解:认为闭包在内存中永远都不会被回收,实际情况并不是这样的」

通过前面的函数调用栈章节我们知道,在预解析阶段,函数声明会创建一个函数体,并在代码中持久存在。但是并非所有的函数体都能够持久存在。上面的例子就是一个非常典型的案例。函数 foo 的函数体能够在内存中持久存在,原因在于 foo 在全局上下文中声明,foo 的引用始终存在。因此我们总能访问到 foo。而函数 bar 则不同,函数 bar 是在函数 foo 的执行上下文中声明,当执行上下文执行完毕,执行上下文会被回收,在 foo 执行上下文中声明的函数 bar,也会被回收。如果不做特殊处理,foo 与 bar 产生的闭包对象,同样会被回收。

微调上面的案例,多次调用 foo 的返回函数 bar 并打印 a 的值。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const g = 10

function foo() {
  let a = 10;
  let b = 20;

  function bar() {
    a = a + 1;
    console.log(a)
    const c = 30;
   
    return a + b + c;
  }  
 console.dir(bar)
  return bar
}

// 函数作为返回值的应用:此时实际调用的是 bar 函数
foo()()
foo()()
foo()()
foo()()

分析一下执行过程。

当函数 foo 执行,会创建函数体 bar,并作为 foo 的返回值。foo 调用完毕,则对应创建的执行上下文会被回收,此时 bar 作为 foo 执行上下文的一部分,自然也会被回收。那么保存在 foo.[[Scopes]] 上的闭包对象,自然也会被回收。

因此,多次执行 foo()(),实际上是在创建多个不同的 foo 执行上下文,中间与 bar 创建的闭包对象,始终都没有被保存下来,会随着 foo 的上下文一同被回收。因此,多次执行 foo()(),实际上创建了不同的闭包对象,他们也不会被保留下来,相互之间也不会受到影响。如图

这个过程,也体现了 JavaScript 边执行边解析的特性

而当我们使用一些方式,保留了函数体 bar 的引用,情况就会发生变化,微调上面的代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const g = 10

function foo() {
  let a = 10;
  let b = 20;

  function bar() {
    a = a + 1;
    console.log(a)
    const c = 30;
   
    return a + b + c;
  }  
 console.dir(bar)
  return bar
}

// 在全局上下文中,保留 foo 的执行结果,也就是 内部函数 bar 的引用
var bar = foo()

// 多次执行
bar()
bar()
bar()

分析一下,微调之后,代码中,在全局上下文使用新的变量 bar 保存了 foo 的内部函数 bar 的引用。也就意味着,即使 foo 执行完毕,foo 的上下文会被回收,但是由于函数 bar 有新的方式保存引用,那么即使函数体 bar 是属于 foo 上下文的一部分,它也不会被回收,而会在内存中持续存在。

因此,当 bar 多次执行,其实执行的是同一个函数体。所以函数体 bar 中的闭包对象「Closure (foo)」也是同一个。那么在 bar 函数内部修改的变量 a,就会出现累加的视觉效果。因为他们在不停的修改同一个闭包对象。

再次微调

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const g = 10

function foo() {
  let a = 10;
  let b = 20;

  function bar() {
    a = a + 1;
    console.log(a)
    const c = 30;
   
    return a + b + c;
  }  
 console.dir(bar)
  return bar
}

// 在全局上下文中,保留 foo 的执行结果,也就是 内部函数 bar 的引用
var bar1 = foo()

// 多次执行
bar1()
bar1()
bar1()

// 在全局上下文中,保留 foo 的执行结果,也就是 内部函数 bar 的引用
var bar2 = foo()

// 多次执行
bar2()
bar2()
bar2()

调整之后我们观察一下。

虽然 bar1 与 bar2 都是在保存 foo 执行结果返回的 bar 函数的引用。但是他们对应的函数体却不是同一个。foo 每次执行都会创建新的上下文,因此 bar1 和 bar2 是不同的 bar 函数引用。因此他们对应的闭包对象也就不同。所以执行结果就表现为:

03、小结

闭包的产生非常简单,只需要在函数内部声明函数,并且内部函数访问上层函数作用域中的声明就会产生闭包

闭包对象真实存在于函数体的 [[Scopes]] 属性之中

闭包对象是在代码解析阶段,根据词法作用域的规则产生

闭包对象并非不能被垃圾回收机制回收,仍然需要视情况而定

透彻理解闭包的真实体现,要结合引用数据类型,作用域链,执行上下文和内存管理一起理解

接下来我们要继续修改上面的例子,来进一步理解闭包。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function foo() {
  let a = 10;
  let b = 20;

  function bar() {
    a = a + 1;
    console.log('in bar', a)
    let c = 30;

    function fn() {
      a = a + 1;
      c = c + 1
      console.log('in fn', a)
    }

    console.dir(fn)
    return fn
  }

  console.dir(bar)
  return bar()
}

var fn = foo()
fn()
fn()
fn()

函数 foo 中声明函数 bar, 函数 bar 中声明函数 fn。

函数 bar 中访问 函数 foo 中声明的变量 a。显然,此时能生成闭包对象 「Closure (foo)」 函数 fn 中访问 函数 foo 中声明的变量 a,与函数 bar 中声明的变量 c。此时也能生成闭包对象「Closure (foo)」与 「Closure (bar)」

我们会发现,bar.[[Scopes]] 中的闭包对象「Closure (foo)」与 fn.[[Scopes]] 中的闭包对象 「Closure (foo)」是同一个闭包对象。

输入结果如下图所示:

闭包对象 foo 中的变量 a 的值,受到 bar 与 fn 操作共同影响。

4、思考题

下面这些例子中,是否有闭包产生

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function add(x) {
  return function _add(y) {
    return x + y;
  }
}

add(2)(3); // 5
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var name = "window";

var p = {
  name: 'Perter',
  getName: function () {
    return function () {
      return this.name;
    }
  }
}

var getName = p.getName();
var _name = getName();
console.log(_name);
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-06-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 这波能反杀 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
深入理解JavaScript闭包之什么是闭包
在看本篇文章之前,可以先看一下之前的文章 深入理解JavaScript 执行上下文 和 深入理解JavaScript作用域,理解执行上下文和作用域对理解闭包有很大的帮助。
木子星兮
2020/07/27
8890
apply/call/bind、作用域/闭包、this指向(普通,箭头,JS/Vue的this)
这里要跟this指向区分开。(这里说的作用域先简单理解为,只是变量的作用域,跟this没关系)
用户4396583
2024/10/08
1590
前端冲刺必备指南-执行上下文/作用域链/闭包/一等公民
前言 大家好,我是吒儿?,每天努力一点点?,就能升职加薪?当上总经理出任CEO迎娶白富美走上人生巅峰?,想想还有点小激动呢?。 这是我的第11期文章内容✍,我并不希望把?这篇文章内容成为笔记去记,或者
达达前端
2020/05/20
8550
js函数、作用域和闭包
2.1 用function命令声明函数 function命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数,函数体放在大括号里面
bamboo
2019/01/29
1.5K0
js函数、作用域和闭包
浏览器工作原理 - 浏览器中的 JavaScript
函数 foo() 是一个完整的函数声明,没有涉及赋值操作;第二个函数,先声明了变量 bar,再把 function () {} 赋值给 bar。可以理解为:
Cellinlab
2023/05/17
6320
浏览器工作原理 - 浏览器中的 JavaScript
定义闭包
函数和对其词法环境lexical environment的引用捆绑在一起构成闭包,也就是说,闭包可以让你从内部函数访问外部函数作用域。在JavaScript,函数在每次创建时生成闭包。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
WindRunnerMax
2022/05/06
2750
JavaScript闭包
函数和对其词法环境lexical environment的引用捆绑在一起构成闭包,也就是说,闭包可以让你从内部函数访问外部函数作用域。在JavaScript,函数在每次创建时生成闭包。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
WindRunnerMax
2020/10/20
1.1K0
浏览器原理学习笔记02—浏览器中的JavaScript执行机制
执行上下文(Execution context)是 JavaScript 执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的 this、变量、对象以及函数等。
CS逍遥剑仙
2020/05/02
1.2K1
前端基础进阶(五):JavaScript 闭包详细图解
初学JavaScript时,我在闭包上,走了很多弯路。而这次重新回过头来对基础知识进行梳理,要讲清楚闭包,也是一个非常大的挑战。
一只图雀
2020/07/07
7401
前端基础进阶(五):JavaScript 闭包详细图解
深入理解变量对象、作用域链和闭包
执行上下文、执行栈、作用域链、闭包,这其实是一整套相关的东西,之前转载的文章也有讲到这些。下面两篇文章会更加详细地解释这些概念。
Chor
2019/11/07
7560
闭包
React Hooks是React 16.8引入的一个新特性,其出现让React的函数组件也能够拥有状态和生命周期方法,其优势在于可以让我们在不编写类组件的情况下,更细粒度地复用状态逻辑和副作用代码,但是同时也带来了额外的心智负担,闭包陷阱就是其中之一。
WindRunnerMax
2023/05/26
5010
面试最爱问的闭包问题!!!!
这里先来看一下闭包的定义,分成两个:在计算机科学中和在JavaScript中。在计算机科学中对闭包的定义(维基百科):
zayyo
2023/12/10
3011
JS_基础知识点精讲
今天,我们继续「前端面试」的知识点。我们来谈谈关于「JavaScript」的相关知识点和具体的算法。
前端柒八九
2022/12/19
1.2K0
JS_基础知识点精讲
兄台:JS闭包了解一下
同时, 在 JS 中,对象的值可以是「任意类型」的数据。(在JS篇之数据类型那些事儿简单的介绍了下基本数据类型分类和判断数据类型的几种方式和原理,想了解具体细节,可移步指定文档)
前端柒八九
2022/08/25
7750
兄台:JS闭包了解一下
《现代Javascript高级教程》Javascript执行上下文与闭包
JavaScript中的闭包源于计算机科学中的一种理论概念,称为“λ演算”(Lambda Calculus)。λ演算是计算机科学的基础之一,1930年由Alonzo Church提出,它是一种用于描述计算过程的数学抽象模型,也是函数式编程语言的基础。
linwu
2023/07/27
2060
深入理解JS作用域链与执行上下文
这段代码,很意外地简单,我们的到了想要的结果,在控制台打印出了:Hello JavaScript hoisting 。
loveX001
2022/10/06
5160
关于 JS 闭包看这一篇就够了
LHS (Left-hand Side) 和 RHS (Right-hand Side) ,是在代码执行阶段 JS 引擎操作变量的两种方式,字面理解就是当变量出现在赋值操作左侧时进行LHS查询,出现在右侧时进行RHS查询。更准确的来说,LHS是为了找到变量的容器本身从而可以进行赋值,而RHS则是获取某个变量的值。
用户8921923
2022/10/24
4690
作用域与作用域链
通常来说,一段程序代码中所用到的名字并不总是有效或可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域scope。当一个方法或成员被声明,他就拥有当前的执行上下文context环境。在有具体值的context中,表达式是可见也都能够被引用。如果一个变量或者其他表达式不在当前的作用域,则将无法使用。作用域也可以根据代码层次分层,以便子作用域可以访问父作用域,通常是指沿着链式的作用域链查找,而不能从父作用域引用子作用域中的变量和引用。
WindRunnerMax
2020/08/27
2.1K0
[第18期] 一文搞清 Javascript 中的「上下文」
上下文是 Javascript 中的一个比较重要的概念, 可能很多朋友对这个概念并不是很熟悉, 那换成「作用域」 和 「闭包」呢?是不是就很亲切了。
皮小蛋
2020/03/02
4470
《你不知道的JavaScript》-- 闭包(笔记)
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
爱学习的程序媛
2022/04/07
3410
《你不知道的JavaScript》-- 闭包(笔记)
相关推荐
深入理解JavaScript闭包之什么是闭包
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档