码文不易,转载请带上本文链接,感谢~ https://www.cnblogs.com/echoyya/p/14577243.html

在开发以及面试中,总是会遇到有关模块化相关的问题,始终不是很明白,不得要领,例如以下问题,回答起来也是模棱两可,希望通过这篇文章,能够让大家了解十之一二,首先抛出问题:

  • 导出模块时使用module.exports/exports或者export/export default;
  • 有时加载一个模块会使用require奇怪的是也可以使用import??它们之间有何区别呢?

于是有了菜鸟解惑的搜喽过程。。。。。。

模块化规范:即为 JavaScript 提供一种模块编写、模块依赖和模块运行的方案。

Script 标签

其实最原始的 JavaScript 文件加载方式,就是Script 标签,如果把每一个文件看做是一个模块,那么他们的接口通常是暴露在全局作用域下,也就是定义在 window 对象中,不同模块的接口调用都是一个作用域中,一些复杂的框架,会使用命名空间的概念来组织这些模块的接口。

缺点:

  1. 污染全局作用域
  2. 开发人员必须主观解决模块和代码库的依赖关系
  3. 文件只能按照script标签的书写顺序进行加载
  4. 在大型项目中各种资源难以管理,长期积累的问题导致代码库混乱不堪

默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。

如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞,用户会感觉到浏览器“卡死”了,没有任何响应。这显然是很不好的体验,所以浏览器允许脚本异步加载

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

<script>标签添加deferasync属性,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。

defer:要等到整个页面在内存中正常渲染结束,才会执行;多个脚本时,按顺序执行

async:一旦下载完,渲染引擎就会中断渲染,执行这个脚本再继续渲染。多个脚本时,不能保证按执行顺序

总结一句话:defer是“渲染完再执行”,async是“下载完就执行”。

CommonJS规范(同步加载模块)

  • Node.js 是目前 CommonJS 规范最热门的一个实现
  • 通过require方法同步加载所依赖的模块,通过exportsmodule.exports导出需要暴露的数据。
  • CommonJS 规范包括了模块(modules)、包(packages)、系统(system)、二进制(binary)、控制台(console)、编码(encodings)、文件系统(filesystems)、套接字(sockets)、单元测试(unit testing)等部分。

创建模块

在 Node.js 中,创建一个模块非常简单,一个文件就是一个模块

// module.js 模块
var name = "Echoyya"; // todo something...
exports.name = name

加载模块

使用require函数 加载模块(即被依赖模块的 module.exports对象)。

  1. 按路径加载模块
  2. 通过查找 node_modules 目录加载模块
  3. 加载缓存:Node.js 是根据实际文件名缓存,而不是 require() 提供的参数缓存的,如 require('express')require('./node_modules/express')加载两次,也不会重复加载,尽管两次参数不同,解析到的文件却是同一个。
  4. 核心模块拥有最高的加载优先级,换言之如果有模块与其命名冲突,Node.js 总是会加载核心模块。
  5. 更多关于require函数的用法和特点,博主此前另外总结过一篇博文,NodeJs 入门到放弃 — 入门基本介绍(一)

导出模块

exports.属性 = 值

exports.方法 = 函数

  • Node.js 为每个模块提供一个 exports 变量,指向 module.exports。相对于在每个模块头部,有一行这样的命令:var exports = module.exports;
  • exports对象 和 module.exports对象,指同一个内存空间, module.exports对象才是真正的暴露对象
  • exports对象 是 module.exports对象的引用,不能改变指向,只能添加属性和方法,若直接改变exports 的指向,等于切断了 exports 与 module.exports 的联系,返回空对象
  • console.log(module.exports === exports); // true
  • 更多关于exports函数的用法和特点,博主此前另外总结过一篇博文,NodeJs 入门到放弃 — 入门基本介绍(一)

另外的用法:

// singleobjct.js

function Hello() {
var name;
this.setName = function (thyName) {
name = thyName;
};
this.sayHello = function () {
console.log('Hello ' + name);
};
} exports.Hello = Hello;

此时获取 Hello 对象require('./singleobject').Hello,略显冗余,可以用下面方法简化。

// hello.js
function Hello() {
var name;
this.setName = function(thyName) {
name = thyName;
};
this.sayHello = function() {
console.log('Hello ' + name);
};
}
module.exports = Hello;

就可以直接获得这个对象:

// gethello.js
var Hello = require('./hello');
hello = new Hello();
hello.setName('Yu');
hello.sayHello();

CommonJS 特点

  1. 同步加载方式,适用于服务端,因为模块都放在服务器端,对于服务端来说模块加载较快,不适合在浏览器环境中使用,因为同步意味着阻塞加载。
  2. 所有代码都运行在模块作用域,不会污染全局作用域。
  3. 模块可以多次加载,但只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。
  4. 模块加载的顺序,按照其在代码中出现的顺序。

AMD(Asynchronous Module Definition)

采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。推崇依赖前置

require.js 是目前 AMD 规范最热门的一个实现

AMD 也采用 require语句加载模块,但是不同于 CommonJS,它要求两个参数:require([module], callback);

  • [module]:是一个数组,成员就是要加载的模块

  • callback:加载成功之后的回调函数;

require(['math'], function (math) {
math.add(2, 3);
});

创建模块

模块必须采用 define() 函数来定义。

  1. 若一个模块不依赖其他模块。可以直接定义在 define() 函数中。
// math.js

define(function (){
 var add = function (x,y){
  return x+y;
 };
 return {
  add: add
 };
});
  1. 若这个模块还依赖其他模块,那么 define() 函数的第一个参数,必须是一个数组,指明该模块的依赖性。当 require() 函数加载test模块时,就会先加载 myLib.js 模块。
// test.js

define(['myLib'], function(myLib){
 function foo(){
  myLib.doSomething();
 }
 return {
  foo : foo
 };
});

加载规范模块

// main.js

require(['math'], function (math){
 alert(math.add(1,1));
});

加载非规范的模块

理论上require.js加载的模块,必须是按照 AMD 规范define() 函数定义的模块。但实际上,虽然已经有一部分流行的函数库(比如 jQuery )符合 AMD 规范,更多的库并不符合。那么require.js 如何能够加载非规范的模块呢?

这样的模块在用 require() 加载之前,要先用 require.config()方法,定义它们的一些特征。

例如,underscore 和 backbone 这两个库,都没有采用 AMD 规范编写。如果要加载的话,必须先定义它们的特征。

require.config({
 shim: {
  'underscore': {
   exports: '_'
  },
  'backbone': {
   deps: ['underscore', 'jquery'],
   exports: 'Backbone'
  }
 }
});

require.config() 接受一个配置对象,这个对象有一个 shim 属性,专门用来配置不兼容的模块。每个模块要定义:

  • exports :输出的变量名,表示这个模块外部调用时的名称;

  • deps: 数组,表示该模块的依赖性。

如jQuery 的插件还可以这样定义:

shim: {
 'jquery.scroll': {
  deps: ['jquery'],
  exports: 'jQuery.fn.scroll'
 }
}

AMD特点

  1. AMD允许输出的模块兼容CommonJS
  2. 异步并行加载,不阻塞 DOM 渲染。
  3. 推崇依赖前置,也就是提前执行(预执行),在模块使用之前就已经执行完毕。

CMD(Common Module Definition)

  • CMD 是通用模块加载,要解决的问题与 AMD 一样,只不过是对依赖模块的执行时机不同 ,推崇就近依赖
  • sea.js 是 CMD 规范的一个实现代表库
  • 定义模块使用全局函数define,接收一个 factory 参数,可以是一个函数,也可以是一个对象或字符串;
  1. factory 是函数时有三个参数,function(require, exports, module):

    • require:函数用来获取其他模块提供的接口require(模块标识ID)

    • exports: 对象用来向外提供模块接口

    • module :对象,存储了与当前模块相关联的属性和方法

    // 定义 a.js 模块
    define(function(require, exports, module) { var $ = require('jquery.js') exports.price= 200;
    }); // b.js 加载模块
    const a = require('./a.js')
  2. factory 为对象、字符串时,表示模块的接口就是该对象、字符串。比如可以定义一个 JSON 数据模块:

    define({"foo": "bar"});
  3. 通过字符串模板定义模块:

    define('I am a template.My name is {{name}}.');

AMD 与 CMD 的区别

  1. AMD 是提前执行,CMD 是延迟执行

  2. AMD 是依赖前置,CMD 是依赖就近

    // AMD
    define(['./a', './b'], function(a, b) { // 在定义模块时 就要声明其依赖的模块
    a.doSomething()
    // ....
    b.doSomething()
    // ....
    }) // CMD
    define(function(require, exports, module) {
    var a = require('./a')
    a.doSomething()
    // ... var b = require('./b') // 可以在用到某个模块时 再去require
    b.doSomething()
    // ...
    })

UMD(Universal Module Definition)

  • UMD是AMD和CommonJS的糅合
  • UMD的实现很简单:
    1. 先判断是否支持Node.js模块(exports是否存在),存在则使用Node.js模块模式。
    2. 再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。
    3. 前两个都不存在,则将模块公开到全局(window或global)。
(function (window, factory) {
if (typeof exports === 'object') { module.exports = factory();
} else if (typeof define === 'function' && define.amd) { define([],factory);
} else { window.eventUtil = factory();
}
})(this, function () {
return {};
});

ES6模块化

​ ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。

ES6 中,import引用模块,使用export导出模块。通过babel项目将还未被宿主环境(各浏览器、Node.js)直接支持的 ES6 模块 编译为 ES5 的 CommonJS。因此Babel实际上是将不被支持的import/export翻译成目前已被支持的require/exports

// 导入
import Vue from 'vue'
import App from './App' // 导出
function v1() { ... }
function v2() { ... } export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
export function multiply() {...};
export var year = 2018;
export default ...

模块化规范大总结

CommonJS AMD CMD ES6
引用模块 require require require import
暴露接口 module.exports || exports define函数返回值 return exports export
加载方式 运行时加载,同步加载 并行加载,提前执行,异步加载 并行加载,按需执行,异步加载 编译时加载,异步加载
实现模块规范 NodeJS RequireJS SeaJS 原生JS
适用 服务器 浏览器 浏览器 服务器/浏览器

问题回归:"require"与"import"的区别

说了这么多,还是要回到文章一开始提到的问题,"require"与"import"两种引入模块方式,到底有神马区别,大致可以分为以下几个方面(可能总结的也不是很全面):

写法上的区别

require/exports 的用法只有以下三种简单的写法:

const fs = require('fs')
exports.fs = fs
module.exports = fs

import/export 的写法就多种多样:

import fs from 'fs'
import {default as fs} from 'fs'
import * as fs from 'fs'
import {readFile} from 'fs'
import {readFile as read} from 'fs'
import fs, {readFile} from 'fs' export default fs
export const fs
export function readFile
export {readFile, read}
export * from 'fs'

输入值的区别

require输入的变量,基本类型数据是赋值,引用类型为浅拷贝,可修改

import输入的变量都是只读的,如果输入 a 是一个对象,允许改写对象属性。

import {a} from './xxx.js'

a = {}; // Syntax Error : 'a' is read-only;

a.foo = 'hello'; // 合法操作

执行顺序

require:不具有提升效果,到底加载哪一个模块,只有运行时才知道。

const path = './' + fileName;
const myModual = require(path);

import:具有提升效果,会提升到整个模块的头部,首先执行。import的执行早于foo的调用。本质就是import命令是编译阶段执行的,在代码运行之前。

foo();

import { foo } from 'my_module';

import()函数:ES2020提案引入,支持动态加载模块。import()函数接受一个参数,指定所要加载的模块的位置,参数格式同import命令,两者区别主要是import()为动态加载。可用于按需加载条件加载动态的模块路径等。

它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块,返回一个 Promise 对象。import()加载模块成功以后,该模块会作为一个对象,当作then方法的参数。可以使用对象解构赋值,获取输出接口。

// 按需加载
button.addEventListener('click', event => {
import('./dialogBox.js')
.then({export1, export2} => { // export1和export2都是dialogBox.js的输出接口,解构获得
// do something...
})
.catch(error => {})
}); // 条件加载
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
} // 动态的模块路径
import(f()).then(...); // 根据函数f的返回结果,加载不同的模块。

使用表达式和变量

require:很显然是可以使用表达式和变量的

let a = require('./a.js')
a.add() let b = require('./b.js')
b.getSum()

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';
}

而require/exports 和 import/export 本质上的区别,实际上也就是CommonJS规范与ES6模块化的区别

它们有三个重大差异。

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

  3. CommonJS 模块的require()同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。

导致第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成

CommonJS运行时加载

  • 只能在运行时确定模块的依赖关系,以及输入和输出的变量,一个模块就是一个对象,输入时必须查找对象属性。
// CommonJS模块
let { stat, exists, readfile } = require('fs'); // 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

ES6编译时加载或者静态加载

  • ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
  • 可以在编译时就完成模块加载,引用时只加载需要的方法,其他方法不加载。效率要比 CommonJS 模块的加载方式高。
import { stat, exists, readFile } from 'fs';

一文彻底搞懂JS前端5大模块化规范及其区别的更多相关文章

  1. 一文搞懂 js 中的各种 for 循环的不同之处

    一文搞懂 js 中的各种 for 循环的不同之处 See the Pen for...in vs for...of by xgqfrms (@xgqfrms) on CodePen. for &quo ...

  2. 帮你彻底搞懂JS中的prototype、__proto__与constructor(图解)

    作为一名前端工程师,必须搞懂JS中的prototype.__proto__与constructor属性,相信很多初学者对这些属性存在许多困惑,容易把它们混淆,本文旨在帮助大家理清它们之间的关系并彻底搞 ...

  3. 让你彻底搞懂JS中复杂运算符==

    让你彻底搞懂JS中复杂运算符== 大家知道,==是JavaScript中比较复杂的一个运算符.它的运算规则奇怪,容易让人犯错,从而成为JavaScript中“最糟糕的特性”之一. 在仔细阅读了ECMA ...

  4. 彻底搞懂 JS 中 this 机制

    彻底搞懂 JS 中 this 机制 摘要:本文属于原创,欢迎转载,转载请保留出处:https://github.com/jasonGeng88/blog 目录 this 是什么 this 的四种绑定规 ...

  5. 一文彻底搞懂Java中的环境变量

    一文搞懂Java环境变量 记得刚接触Java,第一件事就是配环境变量,作为一个初学者,只知道环境变量怎样配,在加上各种IDE使我们能方便的开发,而忽略了其本质的东西,只知其然不知其所以然,随着不断的深 ...

  6. 彻底搞懂js __proto__ prototype constructor

    在开始之前,必须要知道的是:对象具有__proto__.constructor(函数也是对象固也具有以上)属性,而函数独有prototype 在博客园看到一张图分析到位很彻底,这里共享: 刚开始看这图 ...

  7. 搞懂:前端跨域问题JS解决跨域问题VUE代理解决跨域问题原理

    什么是跨域 跨域:一个域下的文档或脚本试图去请求另一个域下的资源 广义的跨域包含一下内容: 1.资源跳转(链接跳转,重定向跳转,表单提交) 2.资源请求(内部的引用,脚本script,图片img,fr ...

  8. 一文搞懂js中的typeof用法

    基础 typeof 运算符是 javascript 的基础知识点,尽管它存在一定的局限性(见下文),但在前端js的实际编码过程中,仍然是使用比较多的类型判断方式. 因此,掌握该运算符的特点,对于写出好 ...

  9. 【原创】一文彻底搞懂安卓WebView白名单校验

    前言 近两年公司端侧发现的漏洞很大一部分都出在WebView白名单上,针对这类漏洞安全编码团队也组织过多次培训,但是这种漏洞还是屡见不鲜.下面本人就结合产品中容易出现问题的地方,用实例的方式来总结一下 ...

随机推荐

  1. 英语能力考试 All In One

    英语能力考试 All In One 托福,雅思,托业 TOEIC 托业考试 Test of English for International Communication (TOEIC) 国际交流英语 ...

  2. H5 CSS 悬浮滚动条

    H5 CSS 悬浮滚动条 refs xgqfrms 2012-2020 www.cnblogs.com 发布文章使用:只允许注册用户才可以访问!

  3. how to tell a function arguments length in js

    how to tell a function arguments length in js JavaScript函数不对参数值(参数)执行任何检查 https://www.w3schools.com/ ...

  4. Flutter WillPopScope 拦截路由返回

    WillPopScope addScopedWillPopCallback 启用此路由以使用户否决尝试以将其关闭. 典型应用是如果用户尝试退出表单,则警告用户有关未保存的表单数据.连按两次返回键退出A ...

  5. NGK引入反量子加密系统来应对量子计算攻击

    当前,区块链和分布式账本技术已有了长足发展并广泛应用与多种场景中,原因在于其提供透明性,冗余性和问责性的能力.就区块链而言,此类特征是通过公钥加密和哈希函数提供的.但是,随着量子计算机技术的发展和量子 ...

  6. 内存包装类 Memory 和 Span 相关类型

    1. 前言 2. 简介 3. Memory<T>和Span<T>使用准则 3.1. 所有者, 消费者和生命周期管理 3.2. Memory<T> 和所有者/消费者模 ...

  7. 010_HTML5

    目录 初识HTML 什么是HTML HTML发展史 HTML5的优势 W3C标准 常见IDE IDEA开发HTML IDEA创建HTML文件,并用浏览器打开 配置浏览器 HTML基础 HTML基本结构 ...

  8. winform导出excel

    public void AllDataSetToExcel(DataSet ds) { string saveFileName = ""; bool fileSaved = fal ...

  9. WPF 之绘画(十一)

    一.WPF 绘画 WPF 可以绘制线段(Line).矩形(Rectange).椭圆(Ellipse).路径(Path).具体使用如下所示: <!--(1)线段:Line--> <Li ...

  10. Python爬虫系统学习(1)

    Python爬虫系统化学习(1) 前言:爬虫的学习对生活中很多事情都很有帮助,比如买房的时候爬取房价,爬取影评之类的,学习爬虫也是在提升对Python的掌握,所以我准备用2-3周的晚上时间,提升自己对 ...