最近在写【重拾前端】系列,下面有几个快速通道,大家自取

【重识前端】原型/原型链和继承

【重识前端】闭包与模块

【重识前端】全面攻破this

【重识前端】一次搞定JavaScript的执行机制

【重识前端】什么是BFC、IFC、GFC 和 FFC

【重识前端】深入内存世界

前言

回忆我前几年的时光,大量使用 JavaScript 但却完全不理解闭包是什么。总是感觉这门语 言有其隐蔽的一面,如果能够掌握将会功力大涨,但讽刺的是我始终无法掌握其中的门 道。

JavaScript中闭包无处不在,你只需要能够识别并拥抱它。

最后你恍然大悟:原来在我的代码中已经到处都是闭包了,现在我终于能理解它们了。理 解闭包就好像 Neo3 第一次见到矩阵 4 一样。

定义

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用 域之外执行。

function foo() { var a = 2;function bar() { console.log( a ); // 2}bar();
}
foo();

这样可能还不够直观,我们再修改一下。

function foo() { var a = 2;function bar() { console.log( a ); // 2}return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,这就是闭包的效果。

其实,在正常情况下,我们调用完foo之后,JavaScript检测到后续不会再用到他了, 所以他的内部作用域就会被回收♻️。(涉及到垃圾回收机制,后续的文章我们会说到。)

闭包的神奇之处就在于他可以阻止♻️的发生。因为事实上内部作用域已然存在,那么又有谁在使用呢?哦~是我们的bar在使用。

拜 bar() 所声明的位置所赐,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一 直存活,以供 bar() 在之后任何时间进行引用。

bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。

所以我说说我自己的理解:

闭包是一个运行时的概念,如果只是声明或者定义。即使他符合闭包的要求那他也不能成为一个真正的闭包。为什么?因为内存地址并没有开始分配,闭包只是分配之后那一块地址没有被回收而已。

再回到我们最开始的函数:

function foo() { var a = 2;function bar() { console.log( a ); // 2}bar();
}
foo();

一个函数foo内有一个函数bar。并且函数bar用到了函数foo的变量。那么我们就可以称这个引用过程为“闭包”。但是我们这个函数foo必须被调用,所以我们的例子地下调用了一下函数foo。

再回到我们那个较为清晰的例子

function foo() { var a = 2;function bar() { console.log( a ); // 2}return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,这就是闭包的效果。

这个应该是大众认知上非常经典的一个闭包了吧?但是假如说这个foo函数没有被调用,那么我认为他就不是一个闭包。

实践

常见的闭包

前面的代码片段有点死板,并且为了解释如何使用闭包而人为地在结构上进行了修饰。你已经写过的代码中一定到处都是闭包的身影。 现在让我们来搞懂这个事实。

function wait(message) {setTimeout( function timer() { console.log( message );}, 1000 );
}
wait( "Hello, closure!" );

这个例子非常常见,代码很简单,就是1秒钟之后输出我们传入的参数。

但是我们仔细看就能看见闭包的影子,一秒钟之后,message还是可以被timer访问到。并没有被回收♻️掉。

这就是闭包

本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一 级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使 用了回调函数,实际上就是在使用闭包!

闭包与循环

说到这里肯定有人马上就想到了最经典的面试题:请写出以下的输出内容

for (var i=1; i<=5; i++) { setTimeout( function timer() {console.log( i ); }, i*1000 );
}

正常情况下,我们对这段代码行为的预期是分别输出数字 1~5,每秒一次,每次一个。

但实际上,这段代码在运行时会以每秒一次的频率输出五次 6。

这是为什么?

因为等到执行console.log的时候,那个时候的i已经在for的调教下变成了6了,所以他们去取只能取到6;

(事实上, 当定时器运行时即使每个迭代中执行的是setTimeout(…, 0),所有的回调函数依然是在循 环结束后才会被执行,因此会每次输出一个 6 出来。这个涉及到时间循环,后面的文章会具体的展开来讲)

通过我们刚刚的学习知道,如果我们创造了一个闭包,这样的话这个i不就不会被回收了吗?

ok,我们试试

for (var i=1; i<=5; i++) { (function() {setTimeout( function timer() { console.log( i );}, i*1000 );})();
}

这样能行吗?试试吧,在Chrome或者node里面试试吧,我等着你。

我不卖关子了。这样不行。但是为什么呢?我们现在显然拥有更多的词法作用域了。的确 每个延迟函数都会将 IIFE 在每次迭代中创建的作用域封闭起来。

如果作用域是空的,那么仅仅将它们进行封闭是不够的。仔细看一下,我们的 IIFE 只是一 个什么都没有的空作用域。它需要包含一点实质内容才能为我们所用。

它需要有自己的变量,用来在每个迭代中储存 i 的值


for (var i=1; i<=5; i++) { (function() {var j = i;setTimeout( function timer() {console.log( j ); },j *1000 );})();
}

行了!它能正常工作了!。

可以对这段代码进行一些改进:

for (var i=1; i<=5; i++) { (function(j) {setTimeout( function timer() {console.log( j ); },j *1000 );})(i);
}

当然,这些 IIFE 也不过就是函数,因此我们可以将 i 传递进去,如果愿意的话可以将变量

名定为 j,当然也可以还叫作 i。无论如何这段代码现在可以工作了。
在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的

作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。 问题解决啦!

当然了,肯定有用块级作用域let的,那样其实是更完美的。

for (let i=1; i<=5; i++) { setTimeout( function timer() {console.log( i ); },1000 );
}

闭包与模块

很早之前,我们引用的js文件内部的变量其实都是挂载在window上面的,久而久之变量名很可能会重复=>覆盖=>BUG=>难以追寻的BUG

所以很需要模块的这个概念来解决。

模块的发展我分为三块:上古时代,近现代,现代

上古时代的模块

闭包最强大的功能=>《模块》

看个例子

function a(){var q = 1;var w = [1, 2];function getQ(){console.log(q)}function getW(){console.log(w)}return {getQ: getQ,getW: getW,}
}var f = a();
f.getQ(); // 1
f.getW(); // [1, 2]

我们仔细的剖析一下这段代码, a是一个函数,我们用a创建了一个模块实例。注意一下:如果不执行 getQ 或者 getW 这个闭包就无法创建,因为在函数a内部的getQ和getW都只是对变量q和变了w的引用声明;没有实实在在的调用就没有创建作用域,也没有开辟内存空间。

引用一句《你不知道的JavaScript-上》中的一句话:

如果不执行 外部函数,内部作用域和闭包都无法被创建。

其次,a() 返回一个用对象字面量语法 { key: value, … } 来表示的对象。这 个返回的对象中含有对内部函数而不是内部数据变量的引用。我们保持内部数据变量是隐 藏且私有的状态。可以将这个对象类型的返回值看作本质上是模块的公共 API。

这个对象类型的返回值最终被赋值给外部的变量 f,然后就可以通过它来访问 API 中的 属性方法,比如 f.getW()。

从模块中返回一个实际的对象并不是必须的,也可以直接返回一个内部函 数。jQuery 就是一个很好的例子。jQuery 和 $ 标识符就是 jQuery 模块的公 共 API,但它们本身都是函数(由于函数也是对象,它们本身也可以拥有属 性)。

其实我们这样每次调用一次a函数相当于创建了一个新的模块实例。

假如,此时我们希望只要一个实例,那咋办呢?

天降猛男----IIFE(立即执行函数)

=》

var f = (function a(){var q = 1;var w = [1, 2];function getQ(){console.log(q)}function getW(){console.log(w)}return {getQ: getQ,getW: getW,}
})(); f.getQ(); // 1
f.getW(); // [1, 2]

这个模块只属于f一个人,要修改只能找f帮忙了。

既然模块也是一个函数,那是不是也可以传参呀?

是的,稍作修改就好了。

function a(id){var q = 1;var w = [1, 2];function getQ(){console.log(q)}function getW(){console.log(w)}function getId(){console.log(id)}return {getQ: getQ,getW: getW,getId: getId,}
}var f = a(99);
f.getQ(); // 1
f.getW(); // [1, 2]
f.getId(); // 99

那我想修改模块内部的东西咋办捏?

安排!

function a(id){var q = 1;var w = [1, 2];function getQ(){console.log(q)}function getW(){console.log(w)}function getId(){console.log(id)}function change(){q = q + 1}return {getQ: getQ,getW: getW,getId: getId,change: change,}
}var f = a(99);
f.getQ(); // 1
f.getW(); // [1, 2]
f.getId(); // 99
f.change(); // 99
f.getQ(); // 2

通过在模块实例的内部保留对公共 API 对象的内部引用,可以从内部对模块实例进行修 改,包括添加或删除方法和属性,以及修改它们的值。

近现代的模块

AMD(很少用到了)

AMD 即Asynchronous Module Definition,中文名是异步模块定义的意思。它是一个在浏览器端模块化开发的规范

由于不是JavaScript原生支持,使用AMD规范进行页面开发需要用到对应的库函数,也就是大名鼎鼎RequireJS,实际上AMD 是 RequireJS 在推广过程中对模块定义的规范化的产出

requireJS主要解决两个问题

1、多个js文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器
2、js加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应时间越长
看一个使用requireJS的例子

/* *  a.js *  创建一个名为“a”的模块*/define('a', function(require, exports, module) {   exports.getTime = function() {   return new Date();   }  });/* *  b.js *  创建一个名为“b”的模块,同时使用依赖require、exports和名为“a”的模块:*/define('b', ['require', 'exports', 'a'], function(require, exports, a) {   exports.test = function() {return {   now: a.getTime()    };   }  });/* main.js */require(['b'], function(b) {   console.log(b.test());  });

语法

requireJS定义了一个函数 define,它是全局变量,用来定义模块

define(id?, dependencies?, factory);

  1. id:可选参数,用来定义模块的标识,如果没有提供该参数,脚本文件名(去掉拓展名)
  2. dependencies:是一个当前模块依赖的模块名称数组
  3. factory:工厂方法,模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值
    在页面上使用require函数加载模块

require([dependencies], function(){});
require()函数接受两个参数

  1. 第一个参数是一个数组,表示所依赖的模块
  2. 第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块

require()函数在加载依赖的函数的时候是异步加载的,这样浏览器不会失去响应,它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。

CMD(很少用到了)

CMD 即Common Module Definition通用模块定义,CMD规范是国内发展出来的,就像AMD有个requireJS,CMD有个浏览器的实现SeaJS,SeaJS要解决的问题和requireJS一样,只不过在模块定义方式和模块加载(可以说运行、解析)时机上有所不同
语法
Sea.js 推崇一个模块一个文件,遵循统一的写法
define(id?, deps?, factory)
因为CMD推崇

  1. 一个文件一个模块,所以经常就用文件名作为模块id
  2. CMD推崇依赖就近,所以一般不在define的参数中写依赖,在factory中写

factory是一个函数,有三个参数,function(require, exports, module)

  1. require 是一个方法,接受 模块标识 作为唯一参数,用来获取其他模块提供的接口:require(id)
  2. exports 是一个对象,用来向外提供模块接口
  3. module 是一个对象,上面存储了与当前模块相关联的一些属性和方法

看个例子:

/* *  a.js *  一个文件就是一个模块*/define(function(require, exports, module) {   exports.getTime = function() {   return new Date();   }  });/* main.js */define(function(require, exports, module) {  /* 按需加载a.js */ var a = require('./a');    console.log(a.getTime());  });
AMD和CMD的区别

最明显的区别就是在模块定义时对依赖的处理不同

1、AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块
2、CMD推崇就近依赖,只有在用到某个模块的时候再去require
这种区别各有优劣,只是语法上的差距,而且requireJS和SeaJS都支持对方的写法

AMD和CMD最大的区别是对依赖模块的执行时机处理不同,注意不是加载的时机或者方式不同

很多人说requireJS是异步加载模块,SeaJS是同步加载模块,这么理解实际上是不准确的,其实加载模块都是异步的,只不过AMD依赖前置,js可以方便知道依赖模块是谁,立即加载,而CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块,这也是很多人诟病CMD的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到可以忽略

为什么我们说两个的区别是依赖模块执行时机不同,为什么很多人认为ADM是异步的,CMD是同步的(除了名字的原因。。。)

同样都是异步加载模块,AMD在加载模块完成后就会执行改模块,所有模块都加载执行完后会进入require的回调函数,执行主逻辑,这样的效果就是依赖模块的执行顺序和书写顺序不一定一致,看网络速度,哪个先下载下来,哪个先执行,但是主逻辑一定在所有依赖加载完成后才执行

CMD加载完某个依赖模块后并不执行,只是下载而已,在所有依赖模块加载完成后进入主逻辑,遇到require语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是完全一致的

这也是很多人说AMD用户体验好,因为没有延迟,依赖模块提前执行了,CMD性能好,因为只有用户需要的时候才执行的原因

现代的模块

CommonJS

CommonJS 最早是 Node 在使用,目前也仍然广泛使用,比如在 Webpack 中你就能见到它,当然目前在 Node 中的模块管理已经和 CommonJS 有一些区别了。

// a.js
module.exports = {a: 1
}
// or
exports.a = 1// b.js
var module = require('./a.js')
module.a // -> log 1

因为 CommonJS 还是会使用到的,所以这里会对一些疑难点进行解析

先说 require

var module = require('./a.js')
module.a
// 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {a: 1
}
// module 基本实现
var module = {id: 'xxxx', // 我总得知道怎么去找到他吧exports: {} // exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法相似的原因
var exports = module.exports
var load = function (module) {// 导出的东西var a = 1module.exports = areturn module.exports
};
// 然后当我 require 的时候去找到独特的
// id,然后将要使用的东西用立即执行函数包装下,over

另外虽然 exportsmodule.exports 用法相似,但是不能对 exports 直接赋值。因为 var exports = module.exports 这句代码表明了 exportsmodule.exports 享有相同地址,通过改变对象的属性值会对两者都起效,但是如果直接对 exports 赋值就会导致两者不再指向同一个内存地址,修改并不会对 module.exports 起效。

ES Module

ES Module 是原生实现的模块化方案,与 CommonJS 有以下几个区别

  • CommonJS 支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
  • CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
  • CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
  • ES Module 会编译成 require/exports 来执行的
// 引入模块 API
import XXX from './a.js'
import { XXX } from './a.js'
// 导出模块 API
export function a() {}
export default function() {}

总结

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时 就产生了闭包。

索引:https://juejin.im/post/5a422b036fb9a045211ef789

【重识前端】闭包与模块相关推荐

  1. 【重识前端】什么是BFC、IFC、GFC 和 FFC

    最近在写[重拾前端]系列,下面有几个快速通道,大家自取 [重识前端]原型/原型链和继承 [重识前端]闭包与模块 [重识前端]全面攻破this [重识前端]一次搞定JavaScript的执行机制 [重识 ...

  2. 【重识前端】深入内存世界

    最近在写[重拾前端]系列,下面有几个快速通道,大家自取 [重识前端]原型/原型链和继承 [重识前端]闭包与模块 [重识前端]全面攻破this [重识前端]一次搞定JavaScript的执行机制 [重识 ...

  3. 判断字符串 正则_(重学前端 - JavaScript(模块一)) 14、引用类型之 RegExp (正则)(详述)...

    上一篇文章介绍了 JavaScript 中的 Date 类型,从地理方面的原理知识开始入手,如果大家认真看过上一篇文章,相信 JavaScript 中的 Date 类型已经难不住大家了!!! 但是今天 ...

  4. 从0开始的微服务架构:(一)重识微服务架构

    2019独角兽企业重金招聘Python工程师标准>>> 导语 虽然已经红了很久,但是"微服务架构"正变得越来越重要,也将继续火下去. 各个公司与技术人员都在分享微 ...

  5. Re:从0开始的微服务架构:(一)重识微服务架构--转

    原文地址:http://www.infoq.com/cn/articles/micro-service-architecture-from-zero?utm_source=infoq&utm_ ...

  6. 【重识 HTML + CSS】知识点目录

    重识 HTML + CSS 前言 基本 HTML 标签 基本 CSS 属性 CSS 选择器 CSS 特性 HTML 列表.表格.表单 HTML 元素类型 盒子模型相关知识点 Photoshop 简单使 ...

  7. Re:从0开始的微服务架构:(一)重识微服务架构

    导语 虽然已经红了很久,但是"微服务架构"正变得越来越重要,也将继续火下去. 各个公司与技术人员都在分享微服务架构的相关知识与实践经验,但我们发现,目前网上的这些相关文章中,要么上 ...

  8. 【重识云原生】计算第2.4节——主流虚拟化技术之KVM

    <重识云原生系列>专题索引: 第一章--不谋全局不足以谋一域 第二章计算第1节--计算虚拟化技术总述 第二章计算第2节--主流虚拟化技术之VMare ESXi 第二章计算第3节--主流虚拟 ...

  9. winter重学前端——训练营预习课

    重学前端读书笔记 javascript javascript 类型 javascript对象 面向对象与基于对象 模拟类 JavaScript 中的对象分类 javascript 执行 Promise ...

最新文章

  1. mysql dump 影响业务_mysqldump原理3
  2. netty权威指南学习笔记五——分隔符和定长解码器的应用
  3. laravel 中添加自定义辅助函数helpers.php
  4. C语言学习之1到5的乘积1到N的乘积
  5. UDT协议实现分析——bind、listen与accept
  6. SAP S/4HANA装到Docker里的镜像有多大
  7. java slf4j_SLF4J 使用手册
  8. localdb 安装_如何安装Microsoft SQL Server Express LocalDB
  9. [转载] python的next()函数
  10. java本地方法如何调用其他程序函数,方法详解
  11. 基于双生视界的live2d提取与查看方法
  12. 深入浅出设计模式① —— 设计模式之路
  13. 微星组件环境linux,微星笔记本常用系统环境组件下载集合
  14. 倒向随机微分方程(BSDE)解对终端值的依赖性
  15. 吐血推荐|2万字总结Mac所有应用程序、软件工具和相关资料
  16. VB语言和C语言有什么区别
  17. 反弹shell的各种姿势
  18. 萨班斯-奥克斯利法案 (SOX) 特权账号管理
  19. Fibonacci Sum(二项式求和)
  20. 2021-05-13 python样条插值(一)

热门文章

  1. win7 安装SQL Server2008R2 提示文件格式错误的处理
  2. hadoop搭建伪分布式集群(centos7+hadoop-3.1.1)
  3. bypass分流抢票(火车、高铁)
  4. 如何群发邮件?群发邮件让发收件人互相不知道?邮箱群发邮件技巧
  5. 手机(局域网)远程连接 windows电脑
  6. 第15篇 2D绘图(五)绘制图片
  7. 职场必备两款高效率管理工具,大有用处!
  8. ogg19.1.0.0.4打补丁
  9. 拼多多Temu如何批量养国外买家账号进行拉新?
  10. redis set设置有效时间问题