- 简述
- 无副作用(No Side Effects)
- 高阶函数(High-Order Function)
- 柯里化(Currying)
- 闭包(Closure)
-- JavaScript 作用域
-- 面向对象关系
-- this调用规则
-- 配置多样化的构造重载
-- 更多对象关系维护——模块化
-- 流行的模块化方案- 不可变(Immutable)
- 惰性计算(Lazy Evaluation)
- Monad
前言
在一个封闭的执行空间中(如函数),调用执行时会在内存中创建其执行上下文。而在上下文执行完毕时,本应当销毁的执行结果并没有销毁,保留了下来由调用者继续引用。这就是闭包(Closure)。
闭包的表现形式可以是多样的,最稀松常见的便是返回一个对象,由调用主函数获取该对象这种方式。当然,也可以是非常复杂的过程构建,比如构建一个模块化的交互环境。
JavaScript 模块化表现
早些年间,JavaScript
并不是很受人待见。所有的执行环境都在一个页面中混为一体,每一个 jser
都需要考虑自己写的外部 .js
文件如何能够不被其他的插件所干扰。很多相似的代码片段重复出现,大量的造轮子,搞的一个页面环境十分笨重。让人觉得 JavaScript
只能用来玩一玩,根本无谈工程化,上不了台面。
终于,前辈们不堪其扰,决心杀出一条血路,为构建更优质的 js 环境而不懈努力。他们要解决的问题是:
- 命名冲突:相似意义的变量或函数的命名冲突,可能会导致全局其他地方的引用产生歧义;
- 功能解耦:一个功能强大的 js 插件往往包含诸多内容,致使插件设计者在维护时不堪重负。需要将一个复杂的问题解耦成若干简单的问题,并且互相协作引用。
- 功能依赖:简单的诸多小的功能点可以被多处重复引用依赖,最终形成复杂的应用。因此需要配备成熟的依赖方式。
- 工程维护:在设计上解构了一个复杂的应用,在人员配备方面便可以做到分工明确。由独立的人或团队负责某一个或一部分模块的设计,在接口引用的过程中只要符合标准,则开发效率也会大大提升,利于进一步维护。
AMD 规范
AMD 即 Asynchronous Module Definition,中文名是“异步模块定义”的意思。它是一个在浏览器端模块化开发的规范。模块将被异步加载,模块加载不影响后面语句的运行。所有依赖某些模块的语句均放置在回调函数中。典型的实现是 requirejs。
requirejs 中通过 define(id?, dependencies?, factory) 函数定义模块,参数表示的含义是:
id [可选]:定义中模块的名字。如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字。如果提供了该参数,模块名在应用环境中不允许重复。
dependencies [可选]:当前模块依赖的其他模块标识(模块名)所组成的数组字面量。如果忽略此参数,则默认为["require", "exports", "module"]。
factory : 模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值。
require.js 示例
1. 目录结构
// 当前示例的目录结构
├─ js
│ ├─ require.js
│ ├─ a.js
│ ├─ b.js
│ └─ c.js
├─ index.html
2. 页面引用
在引用 require.js
时,我们标注了其他模块的加载主入口是 a.js
,而无需把所有的模块都引入页面。
<body>
<script src="js/require.js" type="text/javascript" charset="utf-8" data-main="js/a.js"></script>
<script type="text/javascript">
console.log('page loaded..');
</script>
</body>
3. 模块定义
// 文件: a.js
// 假设模块 a 依赖于模块 c 和 b
define('a', ['c', 'b'], function(c, b){
// 模块 a 的执行环境
console.log('a被加载...');
c.printC('a');
b.printB('a');
console.log('a加载完毕...');
});
之后是模块 c 和 b 的定义
// 文件: c.js
define('c', function(){
console.log('c被加载...');
// 定义函数 printC
const printC = function(who){
console.log(who + ' print C!!!');
}
// 导出调用对象
return {
printC
};
});
// 文件: b.js
define('b', function(){
console.log('b被加载...');
// 定义函数 printC
const printB = function(who){
console.log(who + ' print B!!!');
}
// 导出调用对象
return {
printB
};
});
4. 执行结果
requirejs
以异步的方式进行模块定义,并未阻塞程序主线程。因此其他的 js
活动可以正常运行。而在加载模块依赖的过程中,则会根据声明的依赖标识逐一加载。此时,并无关联性的三个 js
文件就可以互相引用,完成了功能解构。
CMD 规范
CMD 即 Common Module Definition,通用模块定义,是国内发展起来的一套 js
模块化规范,所解决的问题与 AMD
规范相同,只不过对于模块的定义方式和加载时机略有不同。代表产品有 Alibaba 的玉伯所设计的 Sea.js
。与 AMD
规范不同的是 AMD
规范推崇依赖加载前置,而 CMD
规范推崇依赖就近。从依赖调用过程中我们就能看出他们之间的差别。
sea.js 示例
1. 目录结构
// 当前示例的目录结构
├─ js
│ ├─ sea.js
│ ├─ aa.js
│ ├─ bb.js
│ └─ cc.js
├─ index.html
2. 页面引用
使用 sea.js
的过程中,我们可以并不指定任何其他模块的显式导入,而是在需要使用时再手动调用:
<script src="js/sea.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
// 通过 seajs 对象调用 use() 方法获取其他模块的引用
seajs.use('./js/aa.js', function (aa) {
aa.printBB();
aa.printCC();
});
// 未阻塞的页面其他 js 语句
console.log('page loaded...');
</script>
3. 模块定义
// aa.js
define(function (require, exports, module) {
console.log('aa被加载...');
// 在需要时引用的其他模块,而非定义时描述模块
const cc = require('./cc');
const bb = require('./bb');
// 通过 module.exports 导出模块
module.exports = {
printCC: cc.print,
printBB: bb.print
};
});
其他的依赖模块 bb.js
和 cc.js
:
// bb.js
define(function (require, exports, module) {
console.log('bb被加载...');
// 导出模块内容
module.exports = {
print: function(){
console.log('bb 执行 print()!');
}
};
});
// cc.js
define(function (require, exports, module) {
console.log('cc被加载...');
// 导出模块内容
module.exports = {
print: function(){
console.log('cc 执行 print()!');
}
};
});
4. 执行结果
与 AMD
规范最大的不同,便是对于模块的加载时机:aa
首先被加载,在 aa
模块执行的过程中按需加载了 cc
和 bb
模块。
不过值得一提的是,随着 ES6
中对模块化的定义以及普及,民间的优秀规范也渐渐被弃用。Sea.js
上一次的更新时间是 2014 年,已停止维护。但他们都是非常优秀的闭包案例,值得学习。
CommonJS 规范
CommonJS 规范 是 node.js
中的模块化规范,其应用方式与 CMD
规范十分相似,但要简化一些。CommonJS 规范
的实现方式是:
(function(exports, require, module, __filename, __dirname){
return module.exports;
});
熟悉 node.js
环境的同学能看到在模块参数中,node为我们传递了全局变量 __filename
和 __dirname
以便于更方便的引用。而我们自己在写基于 node.js
环境的 js
源码时,便可以很方便的直接这样定义:
let fs = require('fs');
// 定义一个读取文件返回 Promise 对象的异步函数
let readFile = (txtOrig) => new Promise((resolve, reject) => {
fs.readFile(txtOrig, {encoding: 'utf8'}, (err, data) => {
if(err) reject(err);
resolve(data);
});
});
module.exports = {
readFile
}
模块定义的语法被隐去了,留给开发者的是可以更加关注业务流程,减少了代码冗余。
ES6 模块规范
ES6 模块规范 是官方拟定的一套模块加载规范,其模块引用方式和语法都与前者略有不同。
模块导出
ES6 通过 export default
和 export
两种语法进行模块导出,导出的结果是一个模块对象,而两种语法会分别将导出结果作为这个模块对象的属性。他们之间的差别是:
export default:导出的值作为模块对象的
default
属性,可由导入方进行重命名。一个模块中只允许出现一个export default
。
export:导出的语法必须是一个声明语句,声明的标识符(变量名或函数名)会作为导出的模块对象的属性进行动态绑定。一个模块中可以出现多个export
语法,并且可以与export default
语法共存。
示例:
// 模块 foo
export const num1 = 10;
export const num2 = 19;
export default function(a, b){
return a + b;
}
模块导入
接收默认导入:
// 模块 bar
// 默认导入时可以为导入的结果重新命名
import calc from './foo';
calc(2, 3); // 结果: 5
接收声明导入:
// 模块 bar
// 通过 export 导出的声明,必须以原命名进行导入
import {num1, num2} from './foo';
num1; // 结果: 10
num2; // 结果: 19
全部导入:
// 模块 bar
import calc, {num1, num2} from './foo';
calc(num1, num2); // 结果: 29
通配符导入:
// 模块 bar
import calc, * as Num from './foo';
calc(Num.num1, Num.num2); // 结果: 29
总结:闭包是一个保护执行环境、缓存执行结果的设计方式。在各种支持函数式编程的语言中我们都能看到闭包的身影,它的概念很模糊,却无处不在。