1. 先谈谈闭包
全局变量会污染环境, 这个都清楚
加入你定义的一个数组变量叫 arr, 你能确保别人的数组变量不再用这个名字了么?
如果不想污染全局环境, 只能将变量做成局部的
function addToArr(element) {
var arr = [];
arr.push(element);
return element + " added to " + arr;
}
var firstPass = addToArr("a");
var secondPass = addToArr("b");
console.log(firstPass); // a added to a
console.log(secondPass); // b added to b
但是这样写的话, 就会有另外一个问题,
虽然 arr 变量不会污染全局环境了, 但是, 每次调用 addToArr 函数的时候, arr 都是一个新的空数组, 不能持续!!!
所以 我在这想达到的效果就是, 即让 arr 做成局部的, 又要让 arr 不丢之, 不被重置.
此时, 我们可以把 内部的 push 动作包装成一个函数并返回, 所以就有了下面的代码:
function addToArr() {
var arr = [];
return function push(element) {
arr.push(element);
console.log(arr);
};
}
var result = addToArr();
result("a"); // [ 'a' ]
result("b"); // [ 'a', 'b' ]
在此, 可以把这种情景称为 "闭包": 一个能够记住其环境变量的函数!!!
内部函数必须是一个封闭(外部)函数的返回值。这种也称为工厂函数。
下一步, 我们再把内部函数匿名一下:
function addToArr() {
var arr = [];
return function(element) {
arr.push(element);
return element + " added to " + arr;
};
}
var closure = addToArr();
console.log(closure("a")); // a added to a
console.log(closure("b")); // b added to a,b
ok, 这样做就解决我们的需求了
但是js中闭包的真正目的是什么呢?
- 提供私有的全局变量
- 在函数调用之间保存变量(状态)
JS中闭包最有趣的应用程序之一是模块模式。在ES6之前,除了将变量和方法封装在函数中之外,没有其他方法可以模块化JS代码并提供私有变量与方法。
闭包与立即调用的函数表达式相结合 是至今通用解决方案。堪称完美☺
var Person = (function(){
// do something
})()
接下来说一下如何"私有化"变量和方法:
var Person = (function() {
var person = {
name: "",
age: 0
};
function setName(personName) {
person.name = personName;
}
function setAge(personAge) {
person.age = personAge;
}
})();
很棒, 外部再也不能访问 person 变量 和 setName 和 setAge 这些东西了, 真是真真正正的实现的"私有化"
如果你想公开谁, 就可以返回谁
如果想公开多个, 可以返回一个包含对私有方法引用的对象。
var Person = (function() {
var person = {
name: "",
age: 0
};
function setName(personName) {
person.name = personName;
}
function setAge(personAge) {
person.age = personAge;
}
return {
setName: setName,
setAge: setAge
};
})();
好了 你很棒, 到此为止, 模块的雏形已经被你掌握了, 剩下的, 可以再来聊点关键字问题了 😁
模块
JS模块化的不足
在实应用中,JavaScript
的表现能力取决于宿主环境中的API支持程程。在Web 1.0时,只有对DOM
、BOM
等基本的支持。随着Web 2.0的推进 ,HTML5崭露头角,它将Web网页带进Web应用的时代,在浏览器中出现了更多、更强大的API供JavaScript
调用。但是这些过程发生在前端,后端JavaScript
的规范却远远落后。对于JavaScript
自身而言,它的规范是薄弱的,还有以下缺陷。
JS没有模块系统,不支持封闭的作用域和依赖管理
没有标准库,没有文件系统和IO流API
没有标准接口
没有包管理系统
因此,社区也为JavaScript
定了相应的规范,其中的CommonJS
是最为重要的里程程。
CommonJS
规范涵盖了模块、二进制、Buffer、字符集编码、I/O流、单元测试、文件系统、进程环境、包管理等等。Node
能以一中比较成熟的姿态出现,离不开CommonJS
规范的影响。
下图是Node
与浏览器以及w3c组织
、CommonJS
组织、ECMAScript
之间的关系:
模块的优点吧
- 模块化开发中,通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数,并且可以按需加载。
- 依赖自动加载,按需加载。
- 提高代码复用率,方便进行代码的管理,使得代码管理更加清晰、规范。
- 减少了命名冲突,消除全局变量。
- 目前流行的js模块化规范有CommonJS、AMD、CMD以及ES6的模块系统
常见模块化规范
- CommonJs (Node.js)
- AMD (RequireJS)
- CMD (SeaJS)
CommonJS(Node.js)
CommonJS
是服务器模块的规范,Node.js
采用了这个规范。
根据 CommonJS
规范,一个单独的文件就是一个模块,每一个模块都是一个单独的作用域,在一个文件定义的变量(还包括函数和类),都是私有的,对其他文件是不可见的。
CommonJS
规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。
CommonJS
中,加载模块使用 require
方法。该方法读取一个文件并执行,最后返回文件内部的 exports
对象。
Node.js
主要用于服务器编程,加载的模块文件一般都已经存在本地硬盘,加载起来较快,不用考虑异步加载的方式,所以CommonJS
的同步加载模块规范是比较适用的。
但如果是浏览器环境,要从服务器加载模块,这是就必须采用异步模式。所以就有了AMD
,CMD
等解决方案。
// math.js
var x = 5;
var addX = function(value) {
return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
// 也可以改写为如下
module.exports = {
x: x,
addX: addX,
};
let math = require('./math.js');
console.log('math.x',math.x);
console.log('math.addX', math.addX(4));
NodeCommonJS
借鉴CommonJS
的的Modules
规范实现了一套非常易用的模块系统,NPM
对Packages
规范的完好支持使得Node
应用在开发过程中事半功倍。下面我们就以Node
的模块和包的实现展开说明。
-
CommonJS
的规范
CommonJS
对模块的定义十分简单,主要分为模块引用、模块定义、模块标识3部分。
在
node.js
里,模块划分所有的功能,每个JS都是一个模块 实现require
方法,NPM
实现了模块的自动加载和安装依赖
(function(exports,require,module,__filename,__dirname){
exports = module.exports={}
exports.name = 'zfpx';
exports = {name:'zfpx'};
return module.exports;
})
//往下会实现一个简单的require方法
- 模块分类
原生模块 http path fs util events 编译成二进制,加载速度最快,原生模块通过名称来加载
-
文件模块 在硬盘的某个位置,加载速度非常慢,文件模块通过名称或路径来加载 . 文件模块的后缀有三种:
后缀名为.js的JavaScript脚本文件,需要先读入内存再运行
后缀名为.json的JSON文件,fs 读入内存 转化成JSON对象
后缀名为.node的经过编译后的二进制C/C++扩展模块文件,可以直接使用一般自己写的通过路径来加载,别人写的通过名称去当前目录或全局的node_modules下面去找
-
第三方模块
- 如果require函数只指定名称则视为从node_modules下面加载文件,这样的话你可以移动模块而不需要修改引用的模块路径
- 第三方模块的查询路径包括module.paths和全局目录
全局目录
window如果在环境变量中设置了NODE_PATH变量,
并将变量设置为一个有效的磁盘目录,
require在本地找不到此模块时向在此目录下找这个模块。
UNIX操作系统中会从
$HOME/.node_modules $HOME/.node_libraries 目录下寻找
Node
模块的加载策略
Node.js模块分为两类(第三方模块这里暂时不提),一类是核心模块(即原生模块),一类是文件模块(我们自己写的)。
原生模块加载速度最快,而文件模块是动态加载的,加载速度比原生模块慢。
但Node对两类模块都会进行缓存,所以在第二次调用require的时候,是不会重复调用的。
在文件模块中,又分3类:.js .json .node
AMD (RequireJS) 异步模块定义
- AMD = Asynchronous Module Definition,即 异步模块定义。
-
AMD
规范加载模块是异步的,并允许函数回调,不必等到所有模块都加载完成,后续操作可以正常执行。 -
AMD
中,使用require
获取依赖模块,使用exports
导出 API。
//规范 API
define(id?, dependencies?, factory);
define.amd = {};
// 定义无依赖的模块
define({
add: function(x,y){
return x + y;
}
});
// 定义有依赖的模块
define(["alpha"], function(alpha){
return {
verb: function(){
return alpha.verb() + 1;
}
}
});
- 异步加载和回调
require([module], callback) 中 callback 为模块加载完成后的回调函数
//加载 math模块,完成之后执行回调函数
require(['math'], function(math) {
 math.add(2, 3);
});
RequireJS
RequireJS 是一个前端模块化管理的工具库,遵循 AMD 规范,RequireJS 是对 AMD 规范的阐述。
RequireJS 基本思想为, 通过一个函数来将所有所需的或者所依赖的模块装载进来,然后返回一个新的函数(模块)。后续所有的关于新模块的业务代码都在这个函数内部操作。
RequireJS 要求每个模块均放在独立的文件之中,并使用 define 定义模块,使用 require 方法调用模块。
按照是否有依赖其他模块情况,可以分为 独立模块 和 非独立模块。
- 独立模块,不依赖其他模块,直接定义
define({
method1: function(){},
method2: function(){}
});
//等价于
define(function() {
return {
method1: function(){},
method2: function(){}
}
});
- 非独立模块,依赖其他模块
define([ 'module1', 'module2' ], function(m1, m2) {
...
});
//等价于
define(function(require) {
var m1 = require('module1');
var m2 = require('module2');
...
});
-
require
方法调用模块
require(['foo', 'bar'], function(foo, bar) {
foo.func();
bar.func();
});
CMD (SeaJS)
CMD = Common Module Definition,即 通用模块定义。
CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。
CMD规范和AMD类似,都主要运行于浏览器端,写法上看起来也很类似。
主要是区别在于 模块初始化时机:
- AMD中只要模块作为依赖时,就会加载并初始化
- CMD中,模块作为依赖且被引用时才会初始化,否则只会加载。
- CMD 推崇依赖就近,AMD 推崇依赖前置。
- AMD 的 API 默认是一个当多个用,CMD 严格的区分推崇职责单一。
例如,
AMD 里 require 分全局的和局部的。
CMD里面没有全局的 require,提供 seajs.use() 来实现模块系统的加载启动。
CMD 里每个 API 都简单纯粹。
//AMD
define(['./a','./b'], function (a, b) {
//依赖一开始就写好
a.test();
b.test();
});
//CMD
define(function (requie, exports, module) {
//依赖可以就近书写
var a = require('./a');
a.test();
...
//软依赖
if (status) {
var b = requie('./b');
b.test();
}
});
Sea.js
使用Sea.js,在书写文件时,需要遵守CMD(Common Module Definition)模块定义规范。一个文件就是一个模块。
用法
- 通过
exports
暴露接口。这意味着不需要命名空间了,更不需要全局变量。这是一种彻底的命名冲突解决方案。 - 通过
require
引入依赖。这可以让依赖内置,开发者只需关心当前模块的依赖,其他事情Sea.js
都会自动处理好。对模块开发者来说,这是一种很好的 关注度分离,能让程序员更多地享受编码的乐趣。 - 通过
define
定义模块.
示例
- 例如,对于下述util.js代码
var org = {};
org.CoolSite = {};
org.CoolSite.Utils = {};
org.CoolSite.Utils.each = function (arr) {
// 实现代码
};
org.CoolSite.Utils.log = function (str) {
// 实现代码
};
可以采用SeaJS重写为:
define(function(require, exports) {
exports.each = function (arr) {
// 实现代码
};
exports.log = function (str) {
// 实现代码
};
});
通过 exports
就可以向外提供接口。通过 require('./util.js')
就可以拿到 util.js 中通过 exports
暴露的接口。这里的 require
可以认为是 Sea.js 给 JavaScript 语言增加的一个语法关键字,通过 require
可以获取其他模块提供的接口。
define(function(require, exports) {
var util = require('./util.js');
exports.init = function() {
// 实现代码
};
});
TypeScript中的模块
TypeScript 也遵循了 import 和 export 的机制
基础
导出一个常量:export const D = "";
导入一个常量:import { D } from "xx";
导出关键字和导入关键字都适用于重命名,如:
import { D as Dell } from "xx"
const C = "";
export { C as D }
我们也可以将所有的导出都导入到一个变量中,如:
export const A = "";
export const B = "";
import * as CONST from "xx";
每一个模块都可以使用一个默认的导出,如:
export default function reducers () {}
import reducers from "xx";
范例
在目前的 Web 前端工程中,大部分情况下我们都会在 JavaScript 引入样式,如:import "./style.css"
当这样的范例有时候编译器并不能编译通过,也许我们可以定义一个通用的模块,如:declare module "*.css";
当然,你也可以使用 require 语句来导入一个样式文件。
由于在我们的工作中,会引用到很多第三方的模块,由于历史的原因,可能很多第三方模块,用了 AMD 或者 commonjs,或者它们压根就没有 TypeScript 的声明文件,重要的是我们都可以定义一个自己的描述文件,起码可以让程序 Run 起来。
举例:
小明 在解析一个 url 时用了一个第三方包来完成,但是当他将第三方包引入到工程中时发现报错了,于是他自己定义了一个模块声明来处理这个错误,如:
declare module "url" {
export function parse(urlStr: string) => string;
}
我们可以在工程中创建一个 typing 目录,编译器会自动的读取这个声明文件。
但是当工程中的错误,还是没有解决,因为它是一个 commonjs
的模块,由于 commonjs
中定义了 module.exports
和 exports
,根据官方给出的建议是使用 import d = require("xxx")
,但是对于有风格洁癖的人来说,这很不好看,但是我们可以从中遵循出来,由于 exports
的关系,以及现代编译器编译的规则,假设是导出了主模块,那么我们可以使用:
import g from "xx";
g.default()
假设,我们的 commonjs
是使用的 exports
,那么我们可以使用:import * as g from "xx";
来解决这个问题。
于是 小明 很愉快的解决了错误,对于高效的程序员来说,老板又给 小明 涨薪了。
我们来看一看最终编译之后的代码:
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const url_1 = __importDefault(require("browser-used/lib/url"));
console.log(url_1.default);
JS中模块的发展史
0.早期:用script来引入js模块
<script type="text/javascript" src="a.js"></script>
<script type="text/javascript" src="b.js"></script>
<script type="text/javascript" src="c.js"></script>
<script type="text/javascript" src="d.js"></script>
缺点:
(1)加载的时候会停止渲染网页,引入的js文件越多,网页失去响应的时间越长;
(2)会污染全局变量;
(3)js文件之间存在依赖关系,加载是有顺序的,依赖性最大的要放到最后去加载;当项目规模较大时,依赖关系变得错综复杂。
(4)要引入的js文件太多,不美观,代码难以管理。
1.CommonJS 规范
是服务器端模块的规范,由nodejs推广使用。
该规范的核心思想是:允许模块通过require方法来同步加载所要依赖的其他模块,然后通过 exports 或module.exports 来导出需要暴露的接口。
CommonJS 还可以细分为 CommonJS1 和 CommonJS2,区别在于 CommonJS1 只能通过 exports.xx = xx 的方式导出,CommonJS2 在 CommonJS1 的基础上加入了module.exports = xx 的导出方式。 CommonJS 通常指 CommonJS2。
采用CommonJS 规范导入导出:
// 导出
module.exports = moduleA.someFunc;
// 导入
const moduleA = require('./moduleA');
实例:
//math.js
var num = 0;
function add(a, b) {
return a + b;
}
module.exports = {
//需要向外暴露的变量、函数
num: num,
add: add
}
可以这样加载:
//引入自定义的模块时,参数包含路径,可省略.js
//引入核心模块时,不需要带路径,如var http = require("http");
var math = require('./math');
math.add(1, 2)//3
实际上,从上面的例子就可以看出,math.add(1,2)
必须要等待math.js
加载完成,即require
是同步的。
在服务器端,模块文件保存在本地磁盘,等待时间就是磁盘的读取时间。
但对于浏览器而言,由于模块都放在服务器端,等待时间取决于网上的快慢。
因此更合理的方案是异步加载模块。
缺点:
(1)不能并行加载模块,会阻塞浏览器加载;
(2)代码无法直接运行在浏览器环境下,必须通过工具转换成标准的ES5;
2.AMD和require.js
AMD:异步模块定义。
上面已经介绍过,CommonJS
是服务器端模块的规范,主要是为了JS在后端的表现制定的,不太适合前端。
而AMD就是要为前端JS的表现制定规范。
由于不是JavaScript
原生支持,使用AMD规范进行页面开发需要用到对应的库函数,也就是require.js(还有个js库:curl.js)。
实际上AMD 是 require.js在推广过程中对模块定义的规范化的产出。
AMD采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
require.js
也采用require()
语句加载模块,但是不同于CommonJS
: