8. Module语法

javascript语法之前一直存在一个问题就是没有一个模块加载方案,即使在CSS中都存在@import来让我们可以加载其它模块的代码,这对于我们开发大型项目是很不友好的,很容易导致我们需要去重复写一些重复的代码,而在在ES6中有一个比较大的变动就是引入了模块加载方案,而且实现的相当简单

在ES6中我们通过export命令显式指定输出代码,再通过import来对代码进行引入

//a.js
let a=1,
    b=2;
export {a,b}
//b.js
import {a,b} from './a'

以上就是ES6中基本的Module语法,如果我们之前有使用或读过Common.js/AMD/CMD,那么对这个语法应该是比较容易接受的,但是这两者之间还是有区别的

ES6模块的设计思想,是尽量的静态化,是的编译时就能确定模块的依赖关系,以及输出和输入的变量,而在Common中,都是在运行时才能确定这些东西的

let { stat, exists, readFile } = require('fs');

在Common中模块就是对象,输入时必须查找对象属性,Common中我们加载一个模块时的实质是整体加载了该模块,生成一个对象,然后再从这个对象上读取方法,这种加载称为运行时加载,因为只有运行时才能得到这个对象

在ES6中,模块不是对象,而是通过export命令显式指定输出的代码,再通过import对象输入

import {stat,exists,readFile} from 'fs'

上面的代码实质上是从fs模块加载三个方法,其它方法不加载,这种加载称为编译时加载/静态加载,也就是说ES6可以在编译时就完成模块加载,这样就提高了加载时的效率,但是同时也无法引用ES6模块本身,因为它不是对象

ES6中还有一个特别需要注意的与Common的区别不同在于,Common中输出的值是缓存的值,模块内部的变化不会动态更新到外部引用了该模块的模块,影响已经而ES6中模块输出的值是动态更新的,我们在模块内的更改会影响到外部引用了该模块的模块。栗子:

//a.js
var cpunter = 3;
function incCounter(){
  counter++;
}
module.exports={
  counter,
  incCounter
}
//b.js
var mod=require('./a');

console.log(mod.counter);//3
mod.incCounter();
console.log(mod.conunter);//3

以上是在CommonJS规则下运行的结果,那么将该代码放在ES6的规则下

//a.js
var cpunter = 3;
function incCounter(){
  counter++;
}
export {cpunter incCounter };
//b.js
import {cpunter incCounter} from './a'

console.log(cpunter)//3
incCounter()
console.log(cpunter)//4

其实也很好理解,之前我们也说过,在Common规则下模块接收的是一个对象,该对象会被缓存,我们是从该对象上获取某个值,而ES6模块化的规则则是在JS引擎对脚本进行静态分析的时候,遇到加载命令import就会生成一个制度引用,等到脚本真正执行时再根据这个只读引用去加载模块内的值,可以理解为是对模块内变量/函数的堆地址的引用,所以可以动态的获取变量的变化

在Common中我们如果要动态获取原始类型的值,一般这么写

var counter=3;
function incCounter(){
  counter++;
}
module.exports={
  get counter(){
    return counter
  },
  incCounter:incCounter
}

我们将counter作为一个取值函数对外暴露,这样在强迫每次重新获取counter的值

在ES6中接收的其他模块变量,由于只是一个”符号链接“的关系,所以这个变量是只读的,允许我们对它进行添加属性的操作,对它进行重新赋值操作会报错,因为重新赋值相当于在我们引用的模块中创建新的属性,这是不被允许的

使用ES6模块需要注意,它会自动采用严格模式,不管我们是否添加了"use strict",所以需要我们注意一些问题

  1. arguments不会自动反映函数参数的变化
  2. 不能使用argument.callee
  3. 不能使用argument.caller
  4. 禁止this指向全局,唔,需要注意,在非严格模式下,如果this指向undefined那么会指向window,但是在严格模式中该指向会指向undefined
  5. 函数参数不能有同名属性

export命令

模块功能主要由两个命令构成:export和import,export用于规定模块的对外接口,import用于接收其他模块提供的功能

一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取,如果我们希望某个变量能被外部使用,那么必须使用exprot输出该变量

//1.
export let a=1;
export let b=2;

//2.
var a=1,
    b=2;
export {a,b}

上面是两种输出代码的方式,更加推荐下面的一种,可读性更高

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

export function a() {
    //...
}

一般情况我们对外输出的就是函数/变量本来的名字,但也可以通过关键字as对接口重新命名

function a() { }
function b() { }

export {
    a as get,
    a as getName,
    b as set
}

如上,在我们使用了as对a函数分别命名了get和getName后,a函数可以用这两个不同的名字在其它引用的模块中输出两次

需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应的关系,也就是说我们要向外暴露的必须是一个对象或者是一个定义的变量/函数

var a=1;
export a;//错误

export 1;//错误

function b() {};
export b;//错误
//以上的写法都是直接输出值而不是接口,正确的写法如下
var a=1;
export{a}//正确

export var a=1;
//函数同理

export命令可以出现在模块内的任何位置,只要保证是顶层作用域就可以,如果处于块级作用域内会报错,import命令也是如此

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

import命令

js文件可以通过import命令来接收使用了export命令向外暴露了接口的模块的变量/函数

import {first,last} from './a'
console.log(first+'|'+last)

import命令接受一对大括号,里面指定要从其它模块导入的变量名,大括号里面的变量名,必须与导入模块的对外暴露的接口的名称相同

如果想要使用一个新名字,那么在import命令中使用关键字as来为导入的变量/函数重新命名

import { last as child } from './a'

import后面的from用来指定模块文件的位置,可以是相对路径或者是绝对路径。文件后缀可以省略,如果只是模块名,不带有路径那么必须配置文件中告诉Javascript引擎该模块的位置

import {myMethod} from 'util'; //这里util是模块的文件名,由于不带有路径,必须通过配置告诉引擎怎么读取到这个模块

注意,import命令具有提升效果,会提升整个模块的头部,首先执行

foo()

import { foo } from './a'

上面的写法是完全可行的,因为在与解析时会执行import命令,早于foo的执行

同时由于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 { foo } from './a'
import { bar } from './a'
//等同于
import { foo,bar } from './a'

模块的整体加载

除了指定加载某个输出值,还可以使用整体加载,也就是使用通配符(*)制定一个对象,所有输出值都加载在这个对象上面

import { foo,bar } from './a'
foo()
bar()
//等同于
import * as fn from './a'
fn.foo()
fn.bar()

注意,同样,也是静态生成的,所以不允许在运行时改变,以下写法会报错

fn.foo=function(){...}

export default命令

也许之前我们没有看过ES6的语法而是直接使用Vue等框架配合webpack做模块化开发的话,我们会发现其实我们最常使用的是我们现在介绍的这个命令,这个命令可以说是ES6设计中比较贴心的,在上面的写法中我们必须知道所加载的变量名或函数名,否则无法加载,但是这需要我们去阅读我们要使用的模块的文档,浪费我们大量的时间,为了为用户提供方便,让我们不用阅读文档就能加载模块,就要用到export default

该命令用于为模块指定默认输出,其它模块在加载该模块时可以为该模块指定任意的名字,并且不需要包裹在大括号内

//a.js
export default function(){
  console.log('foo')
}
//b.js
import foo from './a'
foo()

以上代码a.js中默认输出的是一个函数,在b.js中使用foo来接收a.js模块对外暴露的函数

当然,export default可以对外暴露命名函数,匿名函数,变量,对象,但是在向外暴露时,函数/对象/变量本身的名字会被忽略,按照匿名函数/对象/变量来向外暴露

//a.js
function foo (){
  ...
}
export default foo;//这个样子也是可以的

//b.js
import fn from './a'
fn()

注意,一个模块只允许有一个export default命令所以import后才不需要加大括号,因为只有可能对应一个方法

其实本质上export default就是输出一个叫做dafault的变量或方法,然后系统允许你为它取任意名字,但是同时需要注意,因为在export default输出的其实是一个叫做default的变量,所以它后面不能跟变量声明语句,这一点和export是相反的

//正确
var a=1;
export default a;
//错误
export default var a=1;

上面代码中,export default a的含义是将变量a的值赋值给变量defaul,所以,最后一种写法会报错

同样的,因为export default的本质是将该命令后面的值赋值给default,default就是我们设置的对外的接口,所以可以将一个值直接写在export default之后

export default 42; //正确

expotr 42;//报错

在向外暴露接口时我们可以同时设定export default和export接口

//a.js
var function foo(){...}
var  a=1;
export { foo a };
export default{
  b:2,
  bar:function(){
    console.log("success")
  }
}
//b.js
import { foo a } from './a';
import fn from './a'

export与import的复合写法

唔,这种写法主要是用来实现我们在一个模块中先导入一个模块,然后再将该模块暴露出去,这个时候我们需要将import语法和export语法进行混写

import { foo,bar } form './a';
export { foo as fn ,bar };
//以上写法等同于
export { foo as fn,bar } from './a'

整体输出我们导入的模块采用以下写法

export * from './a'
//这里需要注意export * 命令会忽略a模块中的default方法

默认接口写法如下

export { default } from 'foo';

跨模块常量

我们都知道在ES6中规定了const声明的常量只在当前代码块优先,如果想设置跨模块常量,可以采用如下写法

//a.js
export const A=1;
export const B=3;
//b.js
import { A,B } from './a'
//或者
import * as constants from './a'

console.log(A)
console.log(B)

你可能感兴趣的:(8. Module语法)