代码编织梦想

前言

  • 首先引用书籍你不知道的JavaScript 中的一段话:

回忆我前几年的时光,大量使用JavaScript但却完全不理解闭包是什么。总是感觉言有其隐的一面,如果能够掌握将 会功力大涨,但其讽刺的是我始终无法掌握其中的门道。还记得我曾经大量阅读早期框架的源码,试图能够理解闭包的工作原理,现在还能回忆起我的脑海中第一次浮现出关于 "模块模式" 相关概念时的激动心情。

  • 在理解闭包之前,你需要先了解的是执行上下文,当然了你了解V8执行过程就更好了。你可以通过上一篇的文章中学习相关知识,这里详细讲解了V8的执行过程以及执行上下文是如何创建的。

什么是闭包?

  • 首先看看维基百科对闭包的定义
  1. 闭包(Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。
  2. 闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。
  3. 闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。
  • 再来看一下MDN对JavaScript闭包的解释
  1. 闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合
  2. 闭包能让开发者可以从内部函数访问外部函数的作用域。
  3. 。在 JavaScript 中,闭包会随着函数的创建而被同时创建。
  • 总的来说,闭包就是能够读取其他函数内部变量的函数。

LHS查询和RHS查询

  • 在开始闭包之前,我们再来补充一下两个概念,就是什么是## LHS查询和RHS查询,待会要用到。
  1. LSH查询;LHS查询会在当前作用域链进行查询,如果当前作用域查询不到,就会沿着作用域链一层一层,找到的话就会将值赋值给这个变量,如果到达作用域顶端仍然找不到,就会在作用域链顶端创建这个变量。
  2. RHS查询;RHS查询会在当前作用域链进行查询,如果当前作用域查询不到,就会沿着作用域链一层一层,,找到的话就会取得这个值并返回,如果到达作用域顶端仍然找不到,就会抛出错误(比如TypeError、ReferenceError)。

闭包的产生

  • JavaScript三大特性,而闭包产生的原因也正是因为这些特性:
  1. 可以在JavaScript函数内部定义新的函数;
  2. 内部函数中访问函数中的定义;
  3. 在JavaScript中,函数是一等公民,所以函数中既可以传入一个函数又可以作为参数返回一个函数。
  • 话不多说,先上代码:
function foo() {
  var moment = 18;
  var test = 111;

  function bar() {
    const may = moment + 777;
    return may;
  }

  console.log(test);

  return bar;
}

var baz = foo();

baz(); // 嗨,朋友,这就是闭包

复制代码
  • 通过观察上面的代码,我们在foo函数中定义了bar函数,并返回bar函数,同时在bar函数中访问了foo函数中的变量moment。

  • 上面的代码大概的执行流程为:

  1. 当调用 foo 函数时,foo 函数会将它的内部函数 bar 返回给全局变量 baz
  2. 等到 foo 函数执行结束时,执行上下文会被 V8 销毁;
  • 按照正常的情况来说,变量 moment 已经被 V8 销毁了,因为我们知道 V8 引擎有垃圾回收期用来释放不再使用的内存空间,但是由于存活的函数 bar 依然引用了 foo 函数作用域中的变量 moment,这样就会带来两个问题:
  1. 当 foo 执行结束时,变量 moment 该不该被销毁?如果不应该被销毁,那么他应该在什么时候销毁,而又应该采用什么策略?
  2. 我们都知道 V8 引擎采用的是惰性解析的方案,那么当执行到 foo 函数时, V8 只会解析 foo 函数,并不会解析内部的 bar函数,仅仅知识对 bar函数进行了标记,在这时 V8 引擎并不知道 bar 函数中是否引用了 外层函数作用域中的变量 moment;
  • 由于 JavaScript 是一门基于堆和栈的语言。在执行全局代码时当执行,V8 会将全局执行上下文压入到调用栈中,然后进入执行 foo 函数的调用过程。
  • 这时候 V8 引擎会为 foo 函数创建执行上下文,执行上下文中包括了变量 moment,然后将 foo 函数的执行上下文压入栈中,foo 函数执行结束之后,foo 函数执行上下文从栈中弹出,这时候 foo 执行上下文中的变量 moment 也随之被销毁。
  • 正常的处理方式应该是 foo 函数的执行上下文被销毁了,但是 bar 函数引用的 foo 函数中的变量却不能被销毁。
  • 在执行 foo 函数的阶段,虽然采取了惰性解析,不会解析和执行 foo 函数中的的 bar 函数中的 bar 函数,但是 V8还是需要判断 bar 函数是否引用了 foo 函数中的变量。
  • V8 引擎引入了预解析器,当解析顶层代码的时候,遇到了一个函数,那么预解析器并不会直接跳过该函数,而是对该书做一次快速的预解析,其中主要的目的主要有两个:
  1. 判断当前函数是否是不是存在一些语法上的错误,如果发现语法错误,那么就会向 V8 抛出语法错误;
  2. 判断 foo 函数是否有被 bar 函数引用的变量,如果有,就会把该变量复制一份到堆内存中,同时 bar 函数本身也是一个对象,也会被存放到内存当中,这样即使 foo 函数即使执行完成,内存被释放以后,bar 函数在执行的时候,依然可以从堆内存中访问复制过来的变量;
  • 第二点钟说的复制一个变量,实际上是复制了一个闭包函数(Closure (foo)),但是此函数只有被 bar 函数引用的值,foo 函数中的 test 变量并没有被复制过去,如下图所示:

Other Example

function foo() {
  var moment = 777;

  function baz() {
    console.log(moment);
  }

  bar(baz);
}

function bar(fn) {
  fn(); // 这也是一个闭包
}

foo();
复制代码
  • 把内部函数 baz 传递给 bar 函数,当调用这个内部函数时(这个时候叫作 fn),它涵盖的 foo()内部作用域的闭包就可以观察到了,因为他能够访问。
function wait(message) {
  setTimeout(function timer() {
    console.log(message);
  }, 1000);
}

wait("hello world");
```js
function wait(message) {
  setTimeout(function timer() {
    console.log(message);
  }, 1000);
}

wait("hello world"); // 这也是一个闭包
复制代码
  • 将一个内部函数 timer 传递给settimeout(...)。timer函数依然保存有wait(...)作用域的闭包。
  • 在引擎内部,内置的工具函数settimeout(...)持有对一个参数的引用,这个参数也许叫作fn或者func,又或者其他类型的名字。引擎会调用这个函数,在这个例子中就是内部的timer函数,而词法作用域在这个过程中保持完整。

经典永不过时

for (var i = 0; i <= 5; i++) {
  setTimeout(() => {
    console.log(i);
  }, i * 10000);
}
复制代码
  • 正常情况下,我们对这段代码行为的预期分别是输出1~5,每秒一次,每次一个。但实际上,这段代码在运行时会以每秒一次输出的频率输出五次6。因为这个循环的终止条件是 i 不再<=5,条件时 i 的值为6,因此输出显示的是循环结束时 i 的最终值。

  • 这是因为 setTimeout 是异步的,而for循环是同步的,延迟函数的回调会在循环结束时才执行,当循环结束时 i 已经是 6了,所有的回调函数才会开始执行,因此会每次输出一个 6 来。

  • 那么有什么办法可以让这个循环一次输出数字呢? 用 let 关键字代替 var? 答案当然是可以的 , 你会看到 0 1 2 3 4 5 成功输出。

for (let i = 0; i <= 5; i++) {
  setTimeout(() => {
    console.log(i); // 0 1 2 3 4 5 成功输出
  }, 1000);
}
复制代码
  • 如果不用 let,用立即执行函数(IIFE)呢?
for (var i = 0; i <= 5; i++) {
  (function () {
    setTimeout(() => {
      console.log(i); // 输出 6 次 6
    }, 1000);
  })();
}
复制代码
  • 这样明显是不行的,为什么呢?虽然我们拥有了跟多的词法作用域了,每个延迟函数都会将 IIFE 在每次迭代中创建的作用域封闭起来。但是该错用域是空的,所以 IIFE只是一个什么都没有的空作用域。
for (var i = 0; i <= 5; i++) {
  (function (j) {
    setTimeout(() => {
      console.log(j); // 0 1 2 3 4 5 成功输出
    }, 1000);
  })(i);
}
复制代码
  • 在这里我们把 i 作为参数传递给 立即执行函数 ,j 就是传进来的参数,这个时候 立即执行函数 就有自己的作用域变量 j 了,问题就迎刃而解了。这就是闭包的力量。

用闭包模拟私有方法

function Person() {
  var friends = 0;

  this.getFriends = function () {
    return friends;
  };

  this.friend = function () {
    friends++;
  };
}

const student = new Person();

student.friend();
student.friend();
console.log(student.getFriends());
复制代码
  • 在上面的代码中,我们创建了一个构造函数 Person ,定义了一个变量 friends 用于保存状态。由于JavaScript的作用域规则的限制,因此只能在构造函数内部访问该变量。
  • 我们可以通过方法读写私有变量,但是不能直接对 friends 变量直接进行读写,这就实现了私有变量了。

闭包的优缺点

  • 优点
  1. 可以将一个变量长期储存在内存中,用于缓存;
  2. 可以避免全局变量的污染;
  3. 加强封装性,是实现了对变量的隐藏和封装,让 JavaScript 也能支持私有变量;
  • 缺点
  1. 因为函数外部引用的变量不会被销毁,所以会导致内存消耗很大,增加了内存消耗量,影响网页性能出现问题;
  2. 而且过度的使用闭包可能会导致内存泄漏,或程序加载运行过慢卡顿等问题的出现。所以我们可以在退出函数之前将不使用的局部变量进行删除;

虽然闭包带给我们一定的好处,但是处理不好,很可能给我们带来严重的灾害。如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。。所以在开发中应尽量避免使用闭包。正所谓代码千万条,规范第一条,代码不规范,开发两行泪。

闭包就好像从 JavaScript 中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人才能够到达那里。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_42981560/article/details/127823318

深入贯彻闭包思想,全面理解js闭包形成过程-爱代码爱编程

谈起闭包,它可是JavaScript两个核心技术之一(异步和闭包),在面试以及实际应用当中,我们都离不开它们,甚至可以说它们是衡量js工程师实力的一个重要指标。下面我们就罗列闭包的几个常见问题,从回答问题的角度来理解和定义你们心中的闭包。 问题如下: 1.什么是闭包? 2.闭包的原理可不可以说一下? 3.你是怎样使用闭包的? 闭包的

js之深入理解闭包问题。_毛瑞彬的博客-爱代码爱编程

最近一段时间准备好好学习下机器学习,所以了解了很多机器学习方面的知识。抓紧补救了下自己曾经遗忘的理论知识(高数、统计学、概率论、微积分等),发现还有点吃力了,很多东西都快忘得差不多。不过,咱程序员,还是很擅长围魏救赵滴^_^

深入理解javascript闭包_中二青年阿欢的博客-爱代码爱编程

什么是闭包? 当一个函数能够记住并访问到其所在的词法作用域及作用域链,特别强调是在其定义的作用域外进行的访问,此时该函数和其上层执行上下文共同构成闭包。 需要明确的几点: 1.闭包一定是函数对象2.闭包和词法

带你快速了解闭包_哆来a梦没有口袋的博客-爱代码爱编程_常见闭包

1.闭包的理解 1.怎么产生闭包: 当一个嵌套的内部函数引用了外部函数的变量(函数)时。 2.产生的条件:(1)函数嵌套                          (2)内部函数引用了外部函数的数据和变量; 3.什么是闭包:存在于嵌套函数中,包含了被引用变量(函数)的对象。 4.常见的闭包:(1)将函数作为另一个函数的返回值 fu

js面试题之闭包-爱代码爱编程

一、什么是闭包? 前言 百度百科定义:闭包就是能够读取其他函数内部变量的函数。 创建闭包的通常方式,是在一个函数内部创建另一个函数 在解释之前,得先讲讲作用域。先来看下面这个示例: var a = 1; f

深入浅出讲解Javascript闭包问题+经典面试题-爱代码爱编程

目录 什么是闭包闭包的作用闭包应用(含经典面试题)思考题 什么是闭包 闭包指有权访问另一个函数作用域中的变量的函数。 —Javascript高级程序设计 在这句话中,要抓住重点:闭包就是函数 简单理解就是:一个作用域可以访问另一个函数内部的局部变量 被访问的这个局部变量所在的函数,就是闭包函数 function fn() {

JS常见面试题之 详解js闭包-爱代码爱编程

一、什么是闭包? 前言 百度百科定义:闭包就是能够读取其他函数内部变量的函数。创建闭包的通常方式,是在一个函数内部创建另一个函数 在解释之前,得先讲讲作用域。先来看下面这个示例: 示例中包含了两种作用域,一种是属于全局的全局作用域,另一种是属于函数f的局部作用域。由于Javascript这种链式作用域(父作用域是可以被其子作用域

android安装教程!深入理解Flutter动画原理,大厂面试题汇总-爱代码爱编程

背景 知乎客户端中有一个自己维护的 Hybrid 框架,在此基础上开发了一些 Hybrid 页面,当需要前端或者客户端开发接口的时候,就涉及到联调的问题。 和一般的 前端 <=> 服务端,或者 客户端 <=> 服务端 类似,前端 <=> 客户端也会出现联调的各种问题,但是往往 Hybrid 开发相关的调试工具并不是那

最新前端面试中常见的面试题(附答案)-爱代码爱编程

前端面试笔记 前言一、HTML篇1.语义话的目的是什么?⭐2.HTML5新特征⭐⭐⭐3.cookie与sessionStorage和localStorage的区别⭐⭐⭐二、CSS篇1.css有哪些基本的选择器,执行先后顺序?⭐2.垂直居中DIV⭐⭐⭐3.两栏布局左边固定右边自适应⭐⭐3.三栏布局左右固定中自适应⭐⭐4.常用的块与行属性内标签有哪些?

深入C++面试题总结-爱代码爱编程

C++基础知识框架: 写在开篇:总结了更为深入的各大厂的C++面试题,干货满满,快收藏起来呀~~ 目录: C++基础知识框架:1. RAII:2. 大端和小端:3. 生成可执行文件过程及各个过程完成的事情:4. 静态库与动态库:5. 编译型语言和解释型语言的区别:6. static、extern、全局变量:7. volatile:8. assert

前端经典面试题 | 闭包的作用和原理_coderhing的博客-爱代码爱编程

🖥️ 前端经典面试题 专栏:闭包的作用和原理(详解) 🧑‍💼 个人简介:一个不甘平庸的平凡人🍬 ✨ 个人主页:CoderHing的个人主页 🍀 格言: ☀️ 路漫漫其修远兮,吾将上下而求索☀️ 👉 你的一键三连是我更新的最大动力❤️ 目录 一、回答点 二、深入回答 作用域 什么是闭包 一、回答点 作用域、函