【重识前端】闭包与模块
最近在写【重拾前端】系列,下面有几个快速通道,大家自取
【重识前端】原型/原型链和继承
【重识前端】闭包与模块
【重识前端】全面攻破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);
- id:可选参数,用来定义模块的标识,如果没有提供该参数,脚本文件名(去掉拓展名)
- dependencies:是一个当前模块依赖的模块名称数组
- factory:工厂方法,模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值
在页面上使用require函数加载模块
require([dependencies], function(){});
require()函数接受两个参数
- 第一个参数是一个数组,表示所依赖的模块
- 第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块
require()函数在加载依赖的函数的时候是异步加载的,这样浏览器不会失去响应,它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。
CMD(很少用到了)
CMD 即Common Module Definition通用模块定义,CMD规范是国内发展出来的,就像AMD有个requireJS,CMD有个浏览器的实现SeaJS,SeaJS要解决的问题和requireJS一样,只不过在模块定义方式和模块加载(可以说运行、解析)时机上有所不同
语法
Sea.js 推崇一个模块一个文件,遵循统一的写法
define(id?, deps?, factory)
因为CMD推崇
- 一个文件一个模块,所以经常就用文件名作为模块id
- CMD推崇依赖就近,所以一般不在define的参数中写依赖,在factory中写
factory是一个函数,有三个参数,function(require, exports, module)
- require 是一个方法,接受 模块标识 作为唯一参数,用来获取其他模块提供的接口:require(id)
- exports 是一个对象,用来向外提供模块接口
- 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
另外虽然 exports
和 module.exports
用法相似,但是不能对 exports
直接赋值。因为 var exports = module.exports
这句代码表明了 exports
和 module.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
【重识前端】闭包与模块相关推荐
- 【重识前端】什么是BFC、IFC、GFC 和 FFC
最近在写[重拾前端]系列,下面有几个快速通道,大家自取 [重识前端]原型/原型链和继承 [重识前端]闭包与模块 [重识前端]全面攻破this [重识前端]一次搞定JavaScript的执行机制 [重识 ...
- 【重识前端】深入内存世界
最近在写[重拾前端]系列,下面有几个快速通道,大家自取 [重识前端]原型/原型链和继承 [重识前端]闭包与模块 [重识前端]全面攻破this [重识前端]一次搞定JavaScript的执行机制 [重识 ...
- 判断字符串 正则_(重学前端 - JavaScript(模块一)) 14、引用类型之 RegExp (正则)(详述)...
上一篇文章介绍了 JavaScript 中的 Date 类型,从地理方面的原理知识开始入手,如果大家认真看过上一篇文章,相信 JavaScript 中的 Date 类型已经难不住大家了!!! 但是今天 ...
- 从0开始的微服务架构:(一)重识微服务架构
2019独角兽企业重金招聘Python工程师标准>>> 导语 虽然已经红了很久,但是"微服务架构"正变得越来越重要,也将继续火下去. 各个公司与技术人员都在分享微 ...
- Re:从0开始的微服务架构:(一)重识微服务架构--转
原文地址:http://www.infoq.com/cn/articles/micro-service-architecture-from-zero?utm_source=infoq&utm_ ...
- 【重识 HTML + CSS】知识点目录
重识 HTML + CSS 前言 基本 HTML 标签 基本 CSS 属性 CSS 选择器 CSS 特性 HTML 列表.表格.表单 HTML 元素类型 盒子模型相关知识点 Photoshop 简单使 ...
- Re:从0开始的微服务架构:(一)重识微服务架构
导语 虽然已经红了很久,但是"微服务架构"正变得越来越重要,也将继续火下去. 各个公司与技术人员都在分享微服务架构的相关知识与实践经验,但我们发现,目前网上的这些相关文章中,要么上 ...
- 【重识云原生】计算第2.4节——主流虚拟化技术之KVM
<重识云原生系列>专题索引: 第一章--不谋全局不足以谋一域 第二章计算第1节--计算虚拟化技术总述 第二章计算第2节--主流虚拟化技术之VMare ESXi 第二章计算第3节--主流虚拟 ...
- winter重学前端——训练营预习课
重学前端读书笔记 javascript javascript 类型 javascript对象 面向对象与基于对象 模拟类 JavaScript 中的对象分类 javascript 执行 Promise ...
最新文章
- mysql dump 影响业务_mysqldump原理3
- netty权威指南学习笔记五——分隔符和定长解码器的应用
- laravel 中添加自定义辅助函数helpers.php
- C语言学习之1到5的乘积1到N的乘积
- UDT协议实现分析——bind、listen与accept
- SAP S/4HANA装到Docker里的镜像有多大
- java slf4j_SLF4J 使用手册
- localdb 安装_如何安装Microsoft SQL Server Express LocalDB
- [转载] python的next()函数
- java本地方法如何调用其他程序函数,方法详解
- 基于双生视界的live2d提取与查看方法
- 深入浅出设计模式① —— 设计模式之路
- 微星组件环境linux,微星笔记本常用系统环境组件下载集合
- 倒向随机微分方程(BSDE)解对终端值的依赖性
- 吐血推荐|2万字总结Mac所有应用程序、软件工具和相关资料
- VB语言和C语言有什么区别
- 反弹shell的各种姿势
- 萨班斯-奥克斯利法案 (SOX) 特权账号管理
- Fibonacci Sum(二项式求和)
- 2021-05-13 python样条插值(一)