es6 javascript的模块module(上)

ES6 的 Class 只是面向对象编程的语法糖, 升级了 ES5 的构造函数的原型链继承的写法, 并没有解决模块化问题。 Module 功能就是为了解决这个问题而提出的。
历史上, JavaScript 一直没有模块( module) 体系, 无法将一个大程序拆分成互相依赖的小文件, 再用简单的方法拼装起来。 其他语言都有这项功能,比如 Ruby 的require、 Python 的import, 甚至就连 CSS 都有 @import, 但是 JavaScript 任何这方面的支持都没有, 这对开发大型的、 复杂的项目形成了巨大障碍。
在 ES6 之前, 社区制定了一些模块加载方案, 最主要的有 CommonJS 和 AMD 两种。 前者用于服务器, 后者用于浏览器。 ES6 在语言规格的层面上, 实现了模块功能, 而且实现得相当简单, 完全可以取代现有的 CommonJS 和 AMD 规范, 成为浏览器和服务器通用的模块解决方案。

ES6 模块的设计思想, 是尽量的静态化, 使得编译时就能确定模块的依赖关系, 以及输入和输出的变量。 CommonJS 和 AMD 模块, 都只能在运行时确定这些东西。 比如, CommonJS 模块就是对象, 输入时必须查找对象属性。

// CommonJS 模块
let {
    stat,
    exists,
    readFile
} = require('fs');
//  等同于
let _fs = require('fs');
let stat = _fs.stat,
    exists = _fs.exists,
    readfile = _fs.readfile;
上面代码的实质是整体加载fs模块( 即加载fs的所有方法), 生成一个对象( _fs), 然后再从这个对象上面读取 3 个方法。 这种加载称为“ 运行时加载”, 因为只有运行时才能得到这个对象, 导致完全没办法在编译时做“ 静态优化”。
ES6 模块不是对象, 而是通过export命令显式指定输出的代码, 输入时也采用静态命令的形式。

// ES6 模块
import {
    stat,
    exists,
    readFile
} from 'fs';
上面代码的实质是从fs模块加载 3 个方法, 其他方法不加载。 这种加载称为“ 编译时加载”, 即 ES6 可以在编译时就完成模块加载, 效率要比 CommonJS模块的加载方式高。 当然, 这也导致了没法引用 ES6 模块本身, 因为它不是对象。
由于 ES6 模块是编译时加载, 使得静态分析成为可能。 有了它, 就能进一步拓宽 JavaScript 的语法, 比如引入宏( macro) 和类型检验( type system)
这些只能靠静态分析实现的功能。
除了静态加载带来的各种好处, ES6 模块还有以下好处。
不再需要 UMD 模块格式了, 将来服务器和浏览器都会支持 ES6 模块格式。 目前, 通过各种工具库, 其实已经做到了这一点。
将来浏览器的新 API 就能用模块格式提供, 不再必要做成全局变量或者navigator对象的属性。
不再需要对象作为命名空间( 比如Math对象), 未来这些功能可以通过模块提供。
浏览器使用 ES6 模块的语法如下。

<script type = "module" src = "foo.js" > < /script>
上面代码在网页中插入一个模块foo.js, 由于type属性设为module, 所以浏览器知道这是一个 ES6 模块。
Node 的默认模块格式是 CommonJS, 目前还没决定怎么支持 ES6 模块。 所以, 只能通过 Babel 这样的转码器, 在 Node 里面使用 ES6 模块。


1 严格模式

ES6 的模块自动采用严格模式, 不管你有没有在模块头部加上 "use strict";。


2 export 命令

模块功能主要由两个命令构成: export和import。 export命令用于规定模块的对外接口, import命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件。 该文件内部的所有变量, 外部无法获取。 如果你希望外部能够读取模块内部的某个变量, 就必须使用export关键字输出该变量。 下面是一个 JS 文件, 里面使用export命令输出变量。

// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
上面代码是profile.js文件, 保存了用户信息。 ES6 将其视为一个模块, 里面用export命令对外部输出了三个变量。
export的写法, 除了像上面这样, 还有另外一种。

// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {
    firstName,
    lastName,
    year
};
上面代码在export命令后面, 使用大括号指定所要输出的一组变量。 它与前一种写法( 直接放置在var语句前) 是等价的, 但是应该优先考虑使用这种写法。 因为这样就可以在脚本尾部, 一眼看清楚输出了哪些变量。

export 命令除了输出变量, 还可以输出函数或类( class)。

export function multiply(x, y) {
    return x * y;
};
上面代码对外输出一个函数multiply。
通常情况下, export输出的变量就是本来的名字, 但是可以使用as关键字重命名。

function v1() {...
}

function v2() {...
}
export {
    v1 as streamV1,
    v2 as streamV2,
    v2 as streamLatestVersion
};
上面代码使用as关键字, 重命名了函数v1和v2的对外接口。 重命名后, v2可以用不同的名字输出两次。
需要特别注意的是, export命令规定的是对外的接口, 必须与模块内部的变量建立一一对应关系。

//  报错
export 1;
//  报错
var m = 1;
export m;
上面两种写法都会报错, 因为没有提供对外的接口。 第一种写法直接输出 1, 第二种写法通过变量m, 还是直接输出 1。 1 只是一个值, 不是接口。 正确的写法是下面这样。

//  写法一
export var m = 1;
//  写法二
var m = 1;
export {
    m
};
//  写法三
var n = 1;
export {
    n as m
};
上面三种写法都是正确的, 规定了对外的接口m。 其他脚本可以通过这个接口, 取到值1。 它们的实质是, 在接口名与模块内部变量之间, 建立了一一对应的关系。
同样的, function和class的输出, 也必须遵守这样的写法。

//  报错
function f() {}
export f;
//  正确
export function f() {};
//  正确
function f() {}
export {
    f
};
另外, export语句输出的接口, 与其对应的值是动态绑定关系, 即通过该接口, 可以取到模块内部实时的值。

export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
上面代码输出变量foo, 值为bar, 500 毫秒之后变成baz。
这一点与 CommonJS 规范完全不同。 CommonJS 模块输出的是值的缓存, 不存在动态更新, 详见下文《 ES6 模块加载的实质》 一节。
最后, export命令可以出现在模块的任何位置, 只要处于模块顶层就可以。 如果处于块级作用域内, 就会报错, 下一节的import命令也是如此。 这是因为处于条件代码块之中, 就没法做静态优化了, 违背了 ES6 模块的设计初衷。

function foo() {
    export default 'bar' // SyntaxError
}
foo()
上面代码中, export语句放在函数之中, 结果报错。


3 import 命令

使用export命令定义了模块的对外接口以后, 其他 JS 文件就可以通过import命令加载这个模块( 文件)。

// main.js
import {
    firstName,
    lastName,
    year    
} from './profile';

function setName(element) {
    element.textContent = firstName + ' ' + lastName;
}
上面代码的import命令, 就用于加载profile.js文件, 并从中输入变量。 import命令接受一个对象( 用大括号表示), 里面指定要从其他模块导入的变量名。 大括号里面的变量名, 必须与被导入模块( profile.js) 对外接口的名称相同。
如果想为输入的变量重新取一个名字,import 命令要使用as关键字, 将输入的变量重命名。

import {
    lastName as surname
} from './profile';
注意, import命令具有提升效果, 会提升到整个模块的头部, 首先执行。

foo();
import {
    foo
} from 'my_module';
上面的代码不会报错, 因为import的执行早于foo的调用。
如果在一个模块之中, 先输入后输出同一个模块, import语句可以与export语句写在一起。

export {
    es6 as
    default
}
from './someModule';
//  等同于
import {
    es6
} from './someModule';
export default es6;
上面代码中, export和import语句可以结合在一起, 写成一行。 但是从可读性考虑, 不建议采用这种写法, 而应该采用标准写法。
另外, ES7 有一个提案, 简化先输入后输出的写法, 拿掉输出时的大括号。

//  提案的写法
export v from 'mod';
//  现行的写法
export {
    v
}
from 'mod';
import语句会执行所加载的模块, 因此可以有下面的写法。

import 'lodash';
上面代码仅仅执行lodash模块, 但是不输入任何值。


4 模块的整体加载

除了指定加载某个输出值, 还可以使用整体加载, 即用星号( * )指定一个对象, 所有输出值都加载在这个对象上面。
下面是一个circle.js文件, 它输出两个方法area和circumference。

    // circle.js
export function area(radius) {
    return Math.PI * radius * radius;
}
export function circumference(radius) {
    return 2 * Math.PI * radius;
}
现在, 加载这个模块。

// main.js
import {
    area,
    circumference
} from './circle';
console.log(' 圆面积: ' + area(4));
console.log(' 圆周长: ' + circumference(14));
上面写法是逐一指定要加载的方法, 整体加载的写法如下。

import * as circle from './circle';
console.log(' 圆面积: ' + circle.area(4));
console.log(' 圆周长: ' + circle.circumference(14));


5 export default 命令

从前面的例子可以看出, 使用import命令的时候, 用户需要知道所要加载的变量名或函数名, 否则无法加载。 但是, 用户肯定希望快速上手, 未必愿意阅读文档, 去了解模块有哪些属性和方法。
为了给用户提供方便, 让他们不用阅读文档就能加载模块, 就要用到export default命令, 为模块指定默认输出。

// export-default.js
export default function() {
    console.log('foo');
}
上面代码是一个模块文件export - default.js, 它的默认输出是一个函数。
其他模块加载该模块时, import命令可以为该匿名函数指定任意名字。

// import-default.js
import customName from './export-default';
customName(); // 'foo'
上面代码的import命令, 可以用任意名称指向export -default.js输出的方法, 这时就不需要知道原模块输出的函数名。 需要注意的是, 这时import命令后面, 不使用大括号。
export default命令用在非匿名函数前, 也是可以的。

// export-default.js
export default function foo() {
    console.log('foo');
}
//  或者写成
function foo() {
    console.log('foo');
}
export default foo;
上面代码中, foo函数的函数名foo, 在模块外部是无效的。 加载的时候, 视同匿名函数加载。
下面比较一下默认输出和正常输出。

//  输出
export default function crc32() {
    // ...
}
//  输入
import crc32 from 'crc32';
//  输出
export function crc32() {
    // ...
};
//  输入
import {
    crc32
} from 'crc32';
上面代码的两组写法, 第一组是使用export default时, 对应的import语句不需要使用大括号; 第二组是不使用export default时, 对应的import语句需要使用大括号。
export default命令用于指定模块的默认输出。 显然, 一个模块只能有一个默认输出, 因此export deault命令只能使用一次。 所以, import命令后面才不用加大括号, 因为只可能对应一个方法。
本质上,export default就是输出一个叫做default的变量或方法, 然后系统允许你为它取任意名字。 所以, 下面的写法是有效的。

// modules.js
function add(x, y) {
    return x * y;
}
export {
    add as
    default
};
//  等同于
// export default add;
// app.js
import {
    default as xxx
} from 'modules';
//  等同于
// import xxx from 'modules';
正是因为export default命令其实只是输出一个叫做default的变量, 所以它后面不能跟变量声明语句。

//  正确
export var a = 1;
//  正确
var a = 1;
export default a;
//  错误
export default
var a = 1;
上面代码中,export default a的含义是将变量a的值赋给变量default。 所以, 最后一种写法会报错。
有了export default命令, 输入模块时就非常直观了, 以输入 jQuery 模块为例。

import $ from 'jquery';
如果想在一条 import 语句中, 同时输入默认方法和其他变量, 可以写成下面这样。

import customName, {
    otherMethod
} from './export-default';
如果要输出默认的值, 只需将值跟在export default之后即可。

export default 42;
export default也可以用来输出类。
// MyClass.js
export default class {...
}
// main.js
import MyClass from 'MyClass';
let o = new MyClass();


6 模块的继承

模块之间也可以继承。
假设有一个circleplus模块, 继承了circle模块。

// circleplus.js
export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
    return Math.exp(x);
}
上面代码中的export * ,表示再输出circle模块的所有属性和方法。 注意,export * 命令会忽略circle模块的default方法。 然后, 上面代码又输出了自定义的e变量和默认方法。
这时, 也可以将circle的属性或方法, 改名后再输出。

// circleplus.js
export {
    area as circleArea
}
from 'circle';
上面代码表示, 只输出circle模块的area方法, 且将其改名为circleArea。
加载上面模块的写法如下。

// main.js
import * as math from 'circleplus';
import exp from 'circleplus';
console.log(exp(math.e));
上面代码中的import exp表示, 将circleplus模块的默认方法加载为exp方法。


7 ES6 模块加载的实质

ES6 模块加载的机制, 与 CommonJS 模块完全不同。 CommonJS 模块输出的是一个值的拷贝, 而 ES6 模块输出的是值的引用。
CommonJS 模块输出的是被输出值的拷贝, 也就是说, 一旦输出一个值, 模块内部的变化就影响不到这个值。 请看下面这个模块文件lib.js的例子。

    // lib.js
var counter = 3;

function incCounter() {
    counter++;
}
module.exports = {
    counter: counter,
    incCounter: incCounter,
};
上面代码输出内部变量counter和改写这个变量的内部方法incCounter。 然后, 在main.js里面加载这个模块。

    // main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
上面代码说明, lib.js模块加载以后, 它的内部变化就影响不到输出的mod.counter了。 这是因为mod.counter是一个原始类型的值, 会被缓存。 除非写成一个函数, 才能得到内部变动后的值。

// lib.js
var counter = 3;

function incCounter() {
    counter++;
}
module.exports = {
    get counter() {
        return counter
    },
    incCounter: incCounter,
};
上面代码中, 输出的counter属性实际上是一个取值器函数。 现在再执行main.js, 就可以正确读取内部变量counter的变动了。

$ node main.js
3
4
ES6 模块的运行机制与 CommonJS 不一样, 它遇到模块加载命令import时, 不会去执行模块, 而是只生成一个动态的只读引用。 等到真的需要用到时, 再到模块里面去取值, 换句话说, ES6 的输入有点像 Unix 系统的“ 符号连接”, 原始值变了, import输入的值也会跟着变。 因此, ES6 模块是动态引用, 并且不会缓存值, 模块里面的变量绑定其所在的模块。
还是举上面的例子。

// lib.js
export let counter = 3;
export function incCounter() {
    counter++;
}
// main.js
import {
    counter,
    incCounter
} from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
上面代码说明, ES6 模块输入的变量counter是活的, 完全反应其所在模块lib.js内部的变化。
再举一个出现在export一节中的例子。

// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
// m2.js
import {
    foo
} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);
上面代码中, m1.js的变量foo, 在刚加载时等于bar, 过了 500 毫秒, 又变为等于baz。
让我们看看, m2.js能否正确读取这个变化。

$ babel - node m2.js
bar
baz
上面代码表明, ES6 模块不会缓存运行结果, 而是动态地去被加载的模块取值, 并且变量总是绑定其所在的模块。
由于 ES6 输入的模块变量, 只是一个“ 符号连接”, 所以这个变量是只读的, 对它进行重新赋值会报错。

// lib.js
export let obj = {};
// main.js
import {
    obj
} from './lib';
obj.prop = 123; // OK
obj = {}; // TypeError
上面代码中, main.js从lib.js输入变量obj, 可以对obj添加属性, 但是重新赋值就会报错。 因为变量obj指向的地址是只读的, 不能重新赋值, 这就好比main.js创造了一个名为obj的 const 变量。

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

// mod.js
function C() {
    this.sum = 0;
    this.add = function() {
        this.sum += 1;
    };
    this.show = function() {
        console.log(this.sum);
    };
}
export let c = new C();
上面的脚本mod.js, 输出的是一个C的实例。 不同的脚本加载这个模块, 得到的都是同一个实例。

    // x.js
import {
    c
} from './mod';
c.add();
// y.js
import {
    c
} from './mod';
c.show();
// main.js
import './x';
import './y';
现在执行main.js, 输出的是 1。
$ babel - node main.js
1
这就证明了x.js和y.js加载的都是C的同一个实例。


  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值