ES6 —(Module 的加载实现)

1、浏览器加载

(1)传统方法
  在 HTML 网页中,浏览器通过 <script> 加载 JavaScript 脚本。

<script type="text/javascript">
    // module code
<script>
<script type="text/javascript" src="path/test.js"></script> 

上述代码中,由于浏览器脚本的默认语言是 JavaScript,因此 type="text/javascript" 可以省略。
  默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到 <script> 标签就会停下来,执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。
  同步可能会造成浏览器堵塞,所以浏览器允许脚本异步加载,如下

<script src="test.js" defer></script>
<script src="test.js" async></script>

<script> 标签中使用 defer 或者 async 属性,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。
  defer : 要等到整个页面正常渲染结束才会执行,“渲染完再执行”。
  async : 一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染,“下载完就执行”。

(2)模块加载规则
  浏览器加载 ES6 模块,也使用 <script> 标签,但是要加入 type="module" 属性。

<script type="module" src="foo.js"></script>
<script type="module">
    import utils from "./util.js";
    // module code
<script> 

  浏览器对于带有 type="module"<script> 都是异步加载的,不会造成浏览器堵塞,默认使用 defer 属性,即等到整个页面渲染完,再执行模块脚本。如果想使用 asyn 则需要显式给出。

<script type="module" src="foo.js" async></script>

对于外部的模块脚本(如上面的 foo.js ),需注意:
  (1)代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
  (2)模块脚本自动采用严格模式,不管有没有声明 use static。参见严格模式限制
  (3)模块之中,可以使用 import 命令加载其他模块(.js 后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用 export 命令输出对外接口。
  (4)模块之中,顶层的 this 关键字返回 undefined ,而不是指向 window 。也就是说,在模块的顶层使用 this 关键字,无意义。
  (5)同一个模块如果加载多次,只执行一次。

2、ES6 模块与 CommonJS 模块的差异

  ES6 模块与 CommonJS 模块完全不同:

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用(不可修改)。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  • CommonJS 模块的顶层 this 指向当前模块,ES6 模块的顶层 this 指向 undefined

  第二个差异是因为 CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完成才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
  CommonJS 模块输出的是值的拷贝,会被缓存。也就是说,一旦输出一个值,模块内部的变化不会影响到该值。

// libComjs.js
var counter = 3;
var colors = ['red'];
function incCounter (){
    counter++;
    colors = ['green'];
    // 给 colors 重新赋值,colors 所代表的地址内容发生变化,
    // 指向了 ['green'] 数组。
    return counter;
}
module.exports = {
    counter: counter,
    colors: colors,  
    incCounter: incCounter
}

//main.js
var mod = require('./libComjs.js');
console.log(mod.counter);   // 3
console.log(mod.colors);   // ['red']
console.log(mod.incCounter());  // 4
console.log(mod.counter);   // 3
console.log(mod.colors);    // ['red']

如果想取到变化后的值,应输出取值器函数,如将模块输出代码改为:

module.exports = {
    get counter(){
        return counter;
    },
    get color(){
        return color;
    },
    incCounter: incCounter
}

改为上述代码后,在运行 main.js 就能得到内部变动后的值。
  import 生成的是一个只读的引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面取值。换句话说,import 加载的值会跟着原始值变。因此,ES6模块是动态引用,并且不会缓存值。模块里面的变量绑定其所有的模块。

// libES6.js
var counter = 3;
var color = ['red'];
function incCounter(){
    counter++;
    color = ['green'];
    return counter;
}

export {counter, color, incCounter};

//main.js
var mod = require('./libES6.js');
console.log(counter);   // 3
console.log(colors);   // ['red']
console.log(incCounter());  // 4
console.log(counter);   // 4
console.log(colors);    // ['green']

注意:
  (1)ES6 输入模块不会缓存运行结果,而是动态地去被加载模块取值,并且变量总是绑定其所在的模块。
  (2)ES6 输入的模块变量,只是一个“符号连接符”,所以这个变量是只读的,对它进行重新赋值会报错。如果是引用类型,变量指向的地址是只读的,但是可以为其添加属性或成员。

color.push('blue');  // 正确,可以为其添加成员
color = ['blue'];    // 报错

  (3)export 通过接口,输出的是同一个值,不同的脚本加载这个接口,得到的都是同样的实例。

3、Node 加载

(1)概述
  Node 中 CommonJS 模块与 ES6 模块不兼容,所以需要将两者分开各自加载。
  在静态分析阶段,一个模块脚本只要有一行 import 或 export 语句,Node 就会认为该脚本为 ES6 模块,否则就为 CommonJS 模块。如果不输出任何接口,但是希望被 Node 认为是 ES6 模块,可以在脚本中输出一个空对象,export {};
  如果不指定绝对路径,Node 加载 ES6 模块会依次寻找以下脚本,与 require() 规则一致。

import './foo';
// 依次寻找
//   ./foo.js
//   ./foo/package.json
//   ./foo/index.js

import 'baz';
// 依次寻找
//   ./node_modules/baz.js
//   ./node_modules/baz/package.json
//   ./node_modules/baz/index.js
// 寻找上一级目录
//   ../node_modules/baz.js
//   ../node_modules/baz/package.json
//   ../node_modules/baz/index.js
// 再上一级目录

(2)import 命令加载 CommonJS 模块
  Node 采用 CommonJS 模块格式,模块的输出都定义在 module.exports 这个属性上面。在 Node 环境中,使用 import 命令加载 CommonJS 模块,Node 会自动将 module.exports 属性,当作模块的默认输出,即等同于 export default

// a.js
module.exports = {
    foo: 'hello',
    bar: 'world'
}
// 等同于
export default {
    foo: 'hello',
    bar: 'world'
}

// main.js
import baz from './a'; 
或 import {default as baz} from './a';
或 import * as baz from './a';

注意:
  1)CommonJS 模块的缓存机制,在 ES6 加载方式下依然有效。
  2)由于 ES6 模块是编译时确定输出接口,而 CommonJS 模块是运行时确定输出接口,所以采用 import 命令加载 CommonJS 模块时,不允许使用以下写法:

import {readfile} from 'fs'; // 错误

(3)require 命令加载 ES6 模块
  采用 require 命令加载 ES6 模块时,ES6 模块的所有输出接口,会成为输入对象的属性,default 接口会变成 default 属性,其余的接口属性名为原来变量名(或函数名)。并且,依然存在缓存机制。

// default.js
let foo = {bar: 'my-default'};
export default foo;
foo = null;

// main.js
const default_prop = requrie('./default.js');
console.log(default_prop.default);  // {bar: 'my-default'}

// es.js
export let foo = {bar:'my-default'};
export {foo as bar};
export function f() {};
export class c {};

// cjs.js
const es_namespace = require('./es');
// es_namespace = {
//   get foo() {return foo;}
//   get bar() {return foo;}
//   get f() {return f;}
//   get c() {return c;}
// }

4、循环加载

  循环加载指的是 A 脚本的执行依赖 B 脚本,而 B 脚本的执行又依赖 A 脚本。通常,循环加载表示存在强耦合,如果处理不好,还可能导致递归加载,使程序无法执行,因此避免出现。
(1)CommonJS 模块的循环加载
  CommonJS 的一个模块,就是一个脚本文件。 require 命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。

{
    id: 
    exports:
    loaded:
    ....
}

上面代码是 Node 内部加载模块后生成的一个对象,id 属性为模块名,exports 属性是模块输出的各个接口,loaded 属性是个布尔值,表示该模块的脚本是否执行完毕,其他属性此处省略。以后需要用到这个模块时,就会到 export 属性上面取值。即使再次执行 require 命令,也不会再次执行该模块,而是到缓存中取值。
  CommonJS 模块的重要特性是加载时执行,即脚本代码在 require 时就会全部执行。一旦出现某个模块被循环加载,就只输出已经执行的部分,还未执行的部分不会输出。

// a.js 
exports.done = false;  // 2
var b = require('./b.js');  // 3
console.log('在 a.js 之中,b.done = %j', b.done); // 9
exports.done = true; // 10
console.log('a.js 执行完毕'); // 11

// b.js
exports.done = false;  // 4
var a = require('./a.js');  // 5 
// 此时 a.js 只执行了 exports.done = false; 
// 所以后面输出的是 a.done = false
console.log('在 b.js 之中,a.done = %j', a.done); // 6
exports.done = true; // 7
console.log('b.js 执行完毕'); // 8

// main.js
var a = require('./a.js');  // 1
var b = require('./b.js');  // 12
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done); // 13

// 输出
在 b.js 之中,a.done = false
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true

上述代码,说明两件事,一是,在 b.js 中,a.js 没有执行完毕,而是只执行了第一句,即返回当前已经执行的部分值,而不是代码全部执行后的值;二是,main.js 执行到第二行时,不会再次执行 b.js 而是输出缓存的 b.js 的执行结果,即 b.js 的第四行。
注意:因为 CommonJS 遇到循环加载时,返回当前已经执行的部分值,因此,以下两种写法在遇到循环加载时会有差异。

var a = require('a'); // 安全写法
var foo = require('a');.foo; // 危险写法

(2)ES6 模块的循环加载
  ES6 模块是动态引用,如果使用 import 从一个模块加载变量,那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

// a.js如下
import {bar} from './b.js';
console.log('a.js');
console.log(bar);
export let foo = 'foo';

// b.js
import {foo} from './a.js';
console.log('b.js');
console.log(foo);
export let bar = 'bar';

// main.js
import * as all from './a.js';
// 输出
b.js
undefined
a.js
bar

上述代码中,由于 a.js 第一行加载 b.js,所以先执行的是 b.js ,而 b.js 的第一行又是 a.js ,这时由于 a.js 已经开始执行了,所以不会重复执行,而是继续往下执行 b.js ,所以第一个输出的是 b.js

5、ES6 模块的转码

  浏览器目前还不支持 ES6 模块,为了现在就能使用,可以将其转为 ES5 的写法。可以用 Babel 、 ES6 module transpiler 和 SystemJS来转码。

阮一峰:ECMAScript 6入门

### 回答1: ES6模块和CommonJS的区别在于它们的语法和实现方式不同。ES6模块ES6规范中定义的一种模块化方式,它使用import和export关键字来导入和导出模块。而CommonJS是Node.js中使用的一种模块化方式,它使用require和module.exports来导入和导出模块ES6模块支持静态分析,可以在编译时确定模块的依赖关系,从而实现更好的性能和可靠性。而CommonJS模块动态加载的,需要在运行时才能确定模块的依赖关系,因此在性能和可靠性方面不如ES6模块。 此外,ES6模块还支持命名导入和导出,可以更灵活地控制模块的导入和导出。而CommonJS模块只支持默认导入和导出,无法实现命名导入和导出。 ### 回答2: ES6 module和CommonJS是两种不同的模块化解决方案,虽然两者主要目的都是为了让JavaScript代码更容易组织和重用,但是它们在具体实现上存在很大的区别,本文将从以下四个方面来介绍它们的区别: 1.语法区别 ES6 module使用了新的关键字来声明模块中的变量、方法和类等例如export、import等,而CommonJS则用require()和module.exports。ES6 module的语法更加简洁易读且与JavaScript同步加载,CommonJS的语法比较复杂,且需要异步加载模块。 2.模块加载方式不同 ES6模块在编译的时候会根据import引用的模块信息,去异步加载依赖的模块,这种方式叫做静态加载。而CommonJS的模块则是在运行时同步加载,也就是说CommonJS会将所有依赖的模块全部加载完成,然后再运行代码,这种方式比较适合服务器端的开发。 3.ES6模块有作用域 ES6模块和CommonJS中的模块是有作用域的,但是ES6模块中的作用域是静态的,而CommonJS中的模块作用域是动态的,也就是说在CommonJS中,模块中变量的值在代码运行期间是可变的,而在ES6模块中则不是。 4.ES6模块支持循环依赖 ES6模块中是支持循环依赖的,它会先给所有需要依赖的模块都分配一个空间,然后再去填充它们,这样就避免了循环依赖的问题。而CommonJS中的模块是不支持循环依赖的,因为在CommonJS中,模块加载是同步的,所以不能把某个模块加载工作放在另外一个模块加载完毕之后再执行。 总之,ES6 module是一种新的、更加先进和可靠的模块化方案,并且支持静态加载和循环依赖等功能,虽然目前不被所有浏览器支持,但是它的优点是显而易见的。相比之下,CommonJS更适用于服务器端的开发,但是因为它还需要异步加载模块作用域的问题等,所以在实际的开发过程中并不是很方便和实用。 ### 回答3: ES6模块和CommonJS模块JavaScript中两种主要的模块化规范,它们有一些明显的区别。 1. 语法不同 ES6模块的导入和导出是基于语法的,也就是使用import和export关键字来操作。而CommonJS模块则是使用require()函数来导入模块,使用module.exports(或exports的简写)来导出模块。 2. 执行方式不同 ES6模块是在代码编译时进行解析的,也就是在代码执行前进行模块解析和编译。而CommonJS模块是在代码运行时才会进行解析。 3. 是否支持静态分析 ES6模块支持静态分析,也就是可以在编译时对模块进行分析和优化,这在性能上有很大的优势。而CommonJS模块不支持静态分析,这在性能上有些影响。 4. 值拷贝与动态绑定不同 在ES6模块中,导入的变量是值拷贝,也就是说,导入的变量与导出的变量不是同一个引用,它们是两个相互独立的对象。而在CommonJS模块中,导入的变量是动态绑定,也就是说,导入的变量与导出的变量是同一个引用。这可能会导致意外的副作用。 5. 是否支持异步加载 ES6模块原生支持异步加载,也就是可以在代码中使用import()函数来异步加载模块。而CommonJS模块不支持异步加载,必须在同步代码中使用require()来加载模块。 总体来说,ES6模块在语法、执行方式、静态分析、值拷贝与动态绑定、支持异步加载等方面都有优势,这也是为什么越来越多的JavaScript开发者选择使用ES6模块。不过,在Node.js环境下,CommonJS模块仍然是主要的模块化规范。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值