彻底弄懂Module及其进化过程

ES6的出现,使得我们对Module的理解停留在exportimport的使用上,但整个JavaScript历史长河里,Module是怎样一个演变过程呢,下面就Module的进化过程做一个简单介绍,涉及的内容较多,每个模块涉入不深,供大家简单的了解。文中有什么说的不对的地方,欢迎提出指正。觉得内容还行的欢迎点赞,您的鼓励,是我前进的动力。

模块化诞生的初衷:web系统的庞大、复杂、团队的分工协作,使得后期维护的成本越来越高,而模块化是用于保持代码块之间相互独立而普遍使用的设计模式。

什么是Module呢?
Module就是将一个复杂的程序依据一定的规则(规范)封装成几个块(文件)并进行组合在一起。块的内部数据/实现是私有的,只是向外部暴露一些接口(方法)与外部其它模块通信。

历史上,JavaScript一直没有模块体系,直到CommonJSAMD的出现,下面来看下在ES6之前,是怎样将大程序拆分成互相依赖的小文件的。

模块化进化史

  1. 全局function模式 ----- 将不同的功能封装成不同的函数
// module1.js
let data = 'module1';
function foo() {
    console.log(`foo():${data}`);
}
function bar() {
    console.log(`bar():${data}`);
}

// module2.js
let data2 = 'module2';
function foo() {  //与另一个模块中的函数冲突了
    console.log(`foo():${data2}`);
}
// index.html
<body>
  <script type="text/javascript" src="module1.js"></script>
  <script type="text/javascript" src="module2.js"></script>
  <script type="text/javascript">
    foo(); // foo():module2    foo()函数值的输出与js文件的加载顺序有关
    bar(); // bar():module1
</script>
</body>

缺点:global变量被污染,容易造成命名冲突。

2.namespace模式 ----- 简单对象封装,减少了全局变量

// module1.js
let myModule1 = {
  data: 'module1.js',
  foo() {
    console.log(`foo() ${this.data}`)
  },
  bar() {
    console.log(`bar() ${this.data}`)
  }
}

// module2.js
let myModule2 = {
  data: 'module2.js',
  foo() {
    console.log(`foo() ${this.data}`)
  },
  bar() {
    console.log(`bar() ${this.data}`)
  }
}

// index.html
<body>
  <script type="text/javascript" src="module1.js"></script>
  <script type="text/javascript" src="module2.js"></script>
  <script type="text/javascript">
      myModule1.foo()
      myModule1.bar()

      myModule2.foo()
      myModule2.bar()

      myModule1.data = 'other data' //能直接修改模块内部的数据
      myModule1.foo();   //  输出other data
</script>
</body>

缺点:模块数据缺乏独立性,外部可更改内部数据。

3.IIFE模式(立即调用函数表达式)-----匿名函数自调用
优点:数据是私有的,外部只能通过暴露的方法操作
问题:如果当前模块依赖另一个模块怎么办?

 // module.js
    (function (window) {
        let data = 'module';   // 数据
        // 操作数据的函数
        function foo() { // 用于暴露有函数
            console.log(`foo():${data}`);
        }
        function bar() { // 用于暴露有函数
            console.log(`bar():${data}`)
            otherFun(); // 内部调用
        }
        function otherFun() { //内部私有的函数
            console.log('otherFun()');
        }
        //暴露行为
        window.myModule = {foo, bar}
    })(window)

// index.html
<body>
    <script type="text/javascript" src="module.js"></script>
    <script type="text/javascript">
        myModule.foo();             // foo():module
        myModule.bar();             // bar():module otherFun()
        //myModule.otherFun()       // TypeError:myModule.otherFun is not a function
        console.log(myModule.data); // undefined:不能访问模块内部数据
        myModule.data = 'xxxx';     // 不能修改的模块内部的data
        myModule.foo();             // foo():module,没有改变
    </script>
</body>

4.IIFE增强模式----引入依赖(现代模块化实现的基石)

// module.js
    (function (window, $) {
        let data = 'NBA'; // 数据
        // 操作数据的函数
        function foo() { // 用于暴露有函数
            console.log(`foo():${data}`);
            $('body').css('background', 'red');
        }
        function bar() { // 用于暴露有函数
            console.log(`bar() ${data}`);
            otherFun(); // 内部调用
        }
        function otherFun() { // 内部私有的函数
            console.log('otherFun()');
        }
        // 暴露行为
        window.myModule = {foo, bar};
    })(window, jQuery)

// index.html
<body>
    // 引入的js必须有一定顺序
    <script type="text/javascript" src="jquery.js"></script>
    <script type="text/javascript" src="module.js"></script>
    <script type="text/javascript">
        myModule.foo()
    </script>
</body>

常见的模块化规范

ES6之前,社区制定了一些模块加载方案,最主要的有CommonJSAMD两种。前者用于服务器,后者用于浏览器。由于ES6模块化的出现,CommonJSAMD规范渐渐很少被人使用。下面简单了解下两种规范如何使用。
1.CommonJS----用于服务器端,模块的加载是运行时同步加载的

  • 定义暴露模块:exports
    (1) exports.xxx = value;
    (2) module.exports = value;
  • 引入模块:require
    (1) 第三方模块:var module = require('xxx模块名');
    (2) 自定义模块:var module = require('模块文件相对路径');

2.AMD--- 专门用于浏览器端,模块的加载是异步的

  • 定义暴露模块:define()
    (1) 定义没有依赖的模块
define(function() {
     // 代码块
    return 模块;
})

(2) 定义有依赖的模块

define(['module1', 'module2'], function(m1,m2) {
        // 代码块
        return 模块;
})
  • 引入使用的模块
require(['module1', 'module2'], function(m1, m2) {
        // 使用模块1和模块2
 })
 // 或者使用requirejs引入模块
 requirejs(['module1', 'module2'], function(m1, m2) {
        // 使用模块1和模块2
 })

ES6模块化

ES6模块的设计思想是尽量静态化,使得在编译时就能确定模块之间的依赖关系,以及输入和输出的变量。ES6模块功能主要有两个命令构成:exportimport
下面简单介绍下这两个命令的使用方式及注意事项,更多详细介绍请参考阮一峰Module的语法

export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
ES6模块中默认采用严格模式,不管你头部有没有写"use strict"

1.export
导出export:作为一个模块,它可以选择性地给其它模块暴露(提供)自己的属性和方法,供其它模块使用。

// profile.js
// 写法一
export var firstName = 'zxy';
export var yesr = '2020';
//写法二  推荐写法
var firstName = 'zxy';
var year = '2020';
export {firstName, year}

注意事项:

  • export命令除了输出变量,也可输出函数或类;
  • export输出的变量就是本来的名字,也可通过as关键字重命名;
function v1() { ... }
function v2() { ... }
export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};
  • export语句输出的接口与其对应的值是动态绑定关系,即通过该接口可以取到模块内部实时的值。
  • export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错。import命令亦是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了ES6模块的设计初衷。
export var foo = 'bar'; 
setTimeout(() => foo = 'baz', 500); // 输出变量foo,值为bar,500ms之后变为baz

function foo() {
    export default 'bar';  //  SyntaxError
}

2.import
导入import:作为一个模块,可以根据需要,引入其它模块提供的属性或者方法,供自己模块使用。

// main.js
import { firstName, year } from './profile.js';
function setName(element) {
  element.textContent = firstName ;
}
  • 大括号中的变量名必须与被导入模块(profile.js)对外接口的名称相同,位置顺序无要求。
  • import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径
  • 如果想为输入的变量重新取一个名字,要在import命令中使用as关键字,将输入的变量重命名。
  • import命令具有提升效果,会提升到整个模块的头部并首先执行。这种行为的本质是,import命令是编译阶段执行的,在代码运行之前。
import {firstName as name} from './profile.js';

foo();
import {foo} from 'module';
  • 由于import是静态执行的,所以不能使用表达式和变量,只有在运行时才能得到结果的的语法结构。
// 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;
// 报错
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}
  • 如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。
    import 'loadsh';
    import loadsh;
  • 导入不存在的变量,值为undefined
// module1.js
export var name = 'jack';  

// module2.js
import {height} from './module1.js';
console.log(height); // 输出结果:undefined

  • 声明的变量,对外都是只读的。请注意下方解释
// module1.js
export var name = 'jack';  

// module2.js
import {name} from './module1.js';
name="修改字符串变量";   // 亲试,不会报错,输出:修改字符串变量

这里的对外只读并不是指import声明的变量都是只读的,引入后不可改写,而是当定义的接口或变量为只读时(通过Object.defineProperty()),import引入的变量就不可被更改。

3.模块的整体加载

  • 除了指定加载某个输出值,还可以使用整体加载(即星号*)来指定一个对象,所有输出值都加载在这个对象上。
//  moduleA.js
var name='zxy';
var age = '18';
var say = function() {
 console.log('say hello');
}
export {name, age, say}

// moduleB.js
import * as obj from './moduleA.js';
obj.name;   // zxy
obj.age;   // 18
obj.say();  // say hello

4.export default命令
export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以import命令后面不用加大括号,因为只可能对应一个方法。

// moduleA.js
export default function() {
      console.log('zxy')
}

// moduleB.js
import sayDefault from './moduleA.js';
sayDefault();  // zxy
  • export default就是输出一个叫作default的变量或方法,然后系统允许我们为它取任意名字。使用as关键字。
  • 因为export default命令其实是输出一个叫default的变量,因此它后面不能跟变量声明语句。
export var a = 1;  // 正确
export default var a = 1;  // 错误
var a = 1;  export default a;  // 正确

同样地,因为export default命令的本质是将后面的值,赋给default变量,所以可以直接将一个值写在export default之后。

export default 1;  // 正确
export 1;   // 错误

上面代码中,后一句报错是因为没有指定对外的接口,而前一句指定对外接口为default

  1. import()方法
    -import()函数用途完成动态加载。import()返回一个Promise对象。
function initEntity() {
    return Promise.all([
        import('./A.js'),
        import('./B.js');  
]).then([transDefine] => {
        // code
})
  • 按需加载
btn.addEventListener('click', event =>{
        import('./dialogBox.js');
}).then(dialogBox => {
        dialogBox.open();
}).catch(error => {
      // code
})
  • 条件加载
if(condition) {
      import('moduleA');
} else {
      import('moduleB');
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容

  • 概述 历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用...
    emmet7life阅读 617评论 0 0
  • 上一章介绍了模块的语法,本章介绍如何在浏览器和 Node 之中加载 ES6 模块,以及实际开发中经常遇到的一些问题...
    emmet7life阅读 2,742评论 0 1
  • ES6模块机制 commonjs 在node环境下跑 ES6 esModule 前段使用为主 webpack co...
    叶戏尘阅读 817评论 0 2
  • 模块通常是指编程语言所提供的代码组织机制,利用此机制可将程序拆解为独立且通用的代码单元。所谓模块化主要是解决代码分...
    MapleLeafFall阅读 1,169评论 0 0
  • 【ES6脚丫系列】模块Module 第一节:Module基本概念 【01】过去使用CommonJS和AMD,前者用...
    吃码小妖阅读 261评论 0 0