关于闭包和模块的一些东西

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时,只有对DOMBOM等基本的支持。随着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之间的关系:

image.png
模块的优点吧
  • 模块化开发中,通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数,并且可以按需加载。
  • 依赖自动加载,按需加载。
  • 提高代码复用率,方便进行代码的管理,使得代码管理更加清晰、规范。
  • 减少了命名冲突,消除全局变量。
  • 目前流行的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 的同步加载模块规范是比较适用的。
但如果是浏览器环境,要从服务器加载模块,这是就必须采用异步模式。所以就有了 AMDCMD 等解决方案。

// 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规范实现了一套非常易用的模块系统,NPMPackages规范的完好支持使得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方法
  • 模块分类
    1. 原生模块 http path fs util events 编译成二进制,加载速度最快,原生模块通过名称来加载

    2. 文件模块 在硬盘的某个位置,加载速度非常慢,文件模块通过名称或路径来加载 . 文件模块的后缀有三种:
      后缀名为.js的JavaScript脚本文件,需要先读入内存再运行
      后缀名为.json的JSON文件,fs 读入内存 转化成JSON对象
      后缀名为.node的经过编译后的二进制C/C++扩展模块文件,可以直接使用

       一般自己写的通过路径来加载,别人写的通过名称去当前目录或全局的node_modules下面去找
      
    3. 第三方模块

      • 如果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


image.png

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.exportsexports,根据官方给出的建议是使用 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,589评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,615评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,933评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,976评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,999评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,775评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,474评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,359评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,854评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,007评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,146评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,826评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,484评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,029评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,153评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,420评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,107评论 2 356