Module的语法
1.概述
历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的require
、Python 的import
,甚至就连 CSS 都有@import
,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
上面代码的实质是整体加载fs
模块(即加载fs
的所有方法),生成一个对象(_fs
),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
ES6 模块不是对象,而是通过export
命令显式指定输出的代码,再通过import
命令输入。
上面代码的实质是从fs
模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
ES6 模块有以下好处:
静态加载
不再需要
UMD
模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者
navigator
对象的属性。不再需要对象作为命名空间(比如
Math
对象),未来这些功能可以通过模块提供。
2.严格模式
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";
。
严格模式主要有以下限制。
变量必须声明后再使用
函数的参数不能有同名属性,否则报错
不能使用
with
语句不能对只读属性赋值,否则报错
不能使用前缀 0 表示八进制数,否则报错
不能删除不可删除的属性,否则报错
不能删除变量
delete prop
,会报错,只能删除属性delete global[prop]
eval
不会在它的外层作用域引入变量eval
和arguments
不能被重新赋值arguments
不会自动反映函数参数的变化不能使用
arguments.callee
不能使用
arguments.caller
禁止
this
指向全局对象不能使用
fn.caller
和fn.arguments
获取函数调用的堆栈增加了保留字(比如
protected
、static
和interface
)
尤其需要注意this
的限制。ES6 模块之中,顶层的this
指向undefined
,即不应该在顶层代码使用this
。
3. export 命令
模块功能主要由两个命令构成:export
和import
。export
命令用于导出模块,import
命令用于导入模块
export
的写法,除了像上面这样,还有另外一种。
上面代码在export
命令后面,使用大括号指定所要输出的一组变量。它与前一种写法(直接放置在var
语句前)是等价的,但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。
export
命令除了输出变量,还可以输出函数或类(class)。
通常情况下,export
输出的变量就是本来的名字,但是可以使用as
关键字重命名。
上面代码使用as
关键字,重命名了函数v1
和v2
的对外接口。重命名后,v2
可以用不同的名字输出两次。
需要特别注意的是,export
命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出 1,第二种写法通过变量m
,还是直接输出 1。1
只是一个值,不是接口。正确的写法是下面这样。
同样的,function
和class
的输出,也必须遵守这样的写法。
export
命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import
命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。
上面代码中,export
语句放在函数之中,结果报错。
4. import 命令
使用export
命令定义了模块的对外接口以后,其他 JS 文件就可以通过import
命令加载这个模块。
如果想为输入的变量重新取一个名字,import
命令要使用as
关键字,将输入的变量重命名。
import
命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。
上面代码中,脚本加载了变量a
,对其重新赋值就会报错,因为a
是一个只读的接口。但是,如果a
是一个对象,改写a
的属性是允许的。
上面代码中,a
的属性可以成功改写,并且其他模块也可以读到改写后的值。不过,这种写法很难查错,建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。
import
后面的from
指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js
后缀可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。
注意,import
命令具有提升效果,会提升到整个模块的头部,首先执行。
上面的代码不会报错,因为import
的执行早于foo
的调用。这种行为的本质是,import
命令是编译阶段执行的,在代码运行之前。
由于import
是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
上面三种写法都会报错,因为它们用到了表达式、变量和if
结构。在静态分析阶段,这些语法都是没法得到值的。
最后,import
语句会执行所加载的模块,但是不输入任何值。
如果多次重复执行同一句import
语句,那么只会执行一次,而不会执行多次。
5. 模块的整体加载
除了指定加载某个输出值,还可以使用整体加载,即用星号(*
)指定一个对象,所有输出值都加载在这个对象上面。
下面是一个circle.js
文件,它输出两个方法area
和circumference
。
逐一指定要加载的方法:
整体加载的写法:
注意,模块整体加载所在的那个对象(上例是circle
),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。
6. export default 命令
export default
命令,为模块指定默认输出。
其他模块加载该模块时,import
命令可以为该匿名函数指定任意名字。
export default
命令用在非匿名函数前,也是可以的。
上面代码中,foo
函数的函数名foo
,在模块外部是无效的。加载的时候,视同匿名函数加载。
export default
命令用于指定模块的默认输出,一个模块只能有一个默认输出
export default
命令其实只是输出一个叫做default
的变量,所以它后面不能跟变量声明语句。
有了export default
命令,输入模块时就非常直观了,以输入 lodash 模块为例。
如果想在一条import
语句中,同时输入默认方法和其他接口,可以写成下面这样。
对应上面代码的export
语句如下。
export default
也可以用来输出类。
7. export 和 import 的复合写法
如果在一个模块之中,先输入后输出同一个模块,import
语句可以与export
语句写在一起。
上面代码中,export
和import
语句可以结合在一起,写成一行。但需要注意的是,写成一行以后,foo
和bar
实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo
和bar
。
8. 模块的继承
假设有一个circleplus
模块,继承了circle
模块。
上面代码中的export *
,表示再输出circle
模块的所有属性和方法。注意,export *
命令会忽略circle
模块的default
方法。然后,上面代码又输出了自定义的e
变量和默认方法。
这时,也可以将circle
的属性或方法,改名后再输出。
加载上面模块的写法如下。
上面代码中的import exp
表示,将circleplus
模块的默认方法加载为exp
方法。
9. 跨模块的常量
const
声明的常量只在当前代码块有效。如果想设置跨模块的常量(即跨多个文件),或者说一个值要被多个模块共享,可以采用下面的写法。
如果要使用的常量非常多,可以建一个专门的constants
目录,将各种常量写在不同的文件里面,保存在该目录下。
然后,将这些文件输出的常量,合并在index.js
里面。
使用的时候,直接加载index.js
就可以了。
10. import()
1)简介
import()
函数,支持动态加载模块。
上面代码中,import
函数的参数specifier
,指定所要加载的模块的位置。import
命令能够接受什么参数,import()
函数就能接受什么参数,两者区别主要是后者为动态加载。
import()
返回一个 Promise 对象。下面是一个例子。
import()
函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。另外,import()
函数与所加载的模块没有静态连接关系,这点也是与import
语句不相同。import()
类似于 Node 的require
方法,区别主要是前者是异步加载,后者是同步加载。
2)使用场合
(1)按需加载
import()
可以在需要的时候,再加载某个模块。
(2)条件加载
import()
可以放在if
代码块,根据不同的情况,加载不同的模块。
上面代码中,如果满足条件,就加载模块 A,否则加载模块 B。
(3)动态的模块路径
import()
允许模块路径动态生成。
上面代码中,根据函数f
的返回结果,加载不同的模块。
3)注意点
import()
加载模块成功以后,这个模块会作为一个对象,当作then
方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。
上面代码中,export1
和export2
都是myModule.js
的输出接口,可以解构获得。
如果模块有default
输出接口,可以用参数直接获得。
上面的代码也可以使用具名输入的形式。
如果想同时加载多个模块,可以采用下面的写法。
import()
也可以用在 async 函数之中。
最后更新于
这有帮助吗?