前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Es6中的模块化Module,导入(import)导出(export)

Es6中的模块化Module,导入(import)导出(export)

作者头像
itclanCoder
发布2020-10-28 11:36:22
2.4K0
发布2020-10-28 11:36:22
举报
文章被收录于专栏:itclanCoderitclanCoder

前言

在Es6之前,javascript没有模块系统,它无法将一个大程序拆分成若干个互相依赖的小文件,然后在用简单的方法拼装起来.为了做到模块化,在Es6之前,引入了AMD(Asynchronous module definition)与CMD(common module definition)

前者典型代表是requireJS(外国人搞出来的),后者是seajs(国内)

共同点:都是对模块定义的不同规范,都是异步加载模块,并且解决文件之间的依赖重命名冲突等问题。

不同点:模块定义的方式和模块加载机制是不同的,前者AMD(requirejs)是将所有文件同时加载,一次性引入,推崇依赖前置,也就是在定义模块时要先声明其依赖的模块,加载完模块后会立马执行该模块(运行时加载)

CMD(seajs)强调的是一个文件一个模块,可按需引入,推崇依赖就近,加载完某个模块后不会立即执行,而是等遇到了require语句的时候在执行 .

两者的使用加载机制不同,也就导致了AMD(requirejs)模块会提前执行,用户体验好,而CMD(seajs)性能好,因为只有在需要时候才执行,在服务器端,nodejs使用的就是cmd规范,也就是需要什么包,就引入什么包,按需加入(编译时加载)

而在Es6的语言规格中引入了模块化功能,也就很好的取代了之前的commonjs和AMD规范了,成为了浏览器和服务器的通用的模块解决方案,在现今(vuejs,ReactJS)等框架大行其道中,都引入了Es6中的模块化(Module)机制,一些自动化打包工具webpack或者微信小游戏中也同样如此

您将在本文中学习到什么是模块,以及模块的导入导出,理解了这个,在一些基于脚手架搭建的项目里或者自动化构建工具中,就不觉得写法怪怪和迷路了的

正文从这里开始~

什么是模块?

在Es6中引入let,const定义变量是解决访问变量的全局作用域问题,从而引入块级作用域,解决命名冲突,同名全局污染,安全等问题

模块可以理解为函数代码块的功能,是封装对象的属性和方法的javascript代码,它可以是某单个文件,变量或者函数,

在Es6模块中,无论有没有加"use strict",都会自动采用严格模式,而且在模块顶部创建的变量不会自动被添加全局作用域中,这个变量仅在模块的顶级作用域中存在,而且模块必须导出一些外部代码可以访问的元素,如变量或者函数,模块也可以从其他模块导入绑定

在模块与模块之间的特性与作用域关系不大(例如微信小程序或者小游戏中的各个文件就是不同的模块,在该文件定义的变量或者函数只在该文件内作用),但也很重要,在模块的顶部,this的值是undefined,另外,模块不支持HTML风格的代码注释

模块实质上是对业务逻辑分离实现低耦合高内聚,也便于代码管理而不是所有功能代码堆叠在一起,模块真正的魔力所在是仅导出和导入你需要的绑定,而不是将所有的东西都放到一个文件

引入模块与引入脚本是有区别的,前者更多是按需引入加载,后者而是无论有没有用,全部一次性引入和加载,类似于通过script标签引入jQuery等库都是一次性载入

Node中模块的导出与导入

在Node模块中,采用的是commonjs规范,也就是使用require方式引入模块,而使用module.exports导出接口,在node中,例如如下代码example.js,当然你也是可以把属性值定义到外面去的,把下面这段代码存储脚本为example

代码语言:javascript
复制
/*
*  通过module.exports将数据进行对外暴露
*/
module.exports = {
       name:"随笔川迹",
       funA:function(){
          return `我是${this.name}`
       }
 }
// 或者把变量函数值定义在外面,例如,与上面等价,以下是常见写法
var name = "随笔川迹";
var funA = function(){
    return `我是${name}`
}
module.exports = {
   name:name,  // 至于前面的变量名可以任意,但是在另外一模块中引入时要与该变量名保持一致,否则就会报错,也可以只写一个name
   funA:funA  // 也可以只写一个funA
}

而在另外一文件命名requireExample.js中使用require方式引入

代码语言:javascript
复制
/*
*
*  通过require()的方式将外部模块引入
* 

 */
 var  m =  require("./requireExample.js");
 console.log(m.name);  // 随笔川迹 
 console.log(m.funA()); // 我是随笔川迹

执行结果如下图所示

以上代码是在node中,通过module.exports对外暴露变量对象,函数等常见方式,而通过require()的方式引入本地模块或者导入包

这个module.exports是node提供的一个私有全局变量属性,而require也是node提供的一个私有全局方法,那么在Es6模块中并没有采用node中require导入模块的方式

在微信小程序中,暂不支持Es6中的export和import模块导出与导入的语法,它依然采用的是类似node中对外暴露数据用module.exports方式,而引入数据则用require的方式

注意:小程序中用import方式引入外部wxss是可以的,但在微信小游戏中却已经支持来Es6中的export与import模块语法

如下为小游戏测试:Es6中export与import的使用,但遗憾的是在小程序暂且还不支持Es6中模块的写法,对外暴露数据仍然采用module.export 的方式而引入模块采用require的方式,与在node中使用相似

(微信小游戏是支持Es中的模块的)

如何检测node.js对Es6的支持情况

命令行终端下全局安装 es-checker

代码语言:javascript
复制
npm install -g es-checker

安装后,在命令行中执行 es-checker命令

代码语言:javascript
复制
es-checker

在命令行终端就会有一个Es6在该node版本中支持结果:如下图所示,红色的表示是暂不支持的

另外一种检测Es6的方法是:在node的repl环境中测试,如果不支持就会报错,运行正常就说明支持Es6写法

还有一种检测方法就是:参考官方文档Es6对Node或者浏览器的支持情况具体可Ecmascript6 compatibility Table(https://kangax.github.io/compat-table/es6/),微信不支持访问外链,直接将地止复制到浏览器访问即可

Es6中模块导出的基本语法

模块的导出,export关键字用于暴露数据,暴露给其他模块

使用方式是,可以将export放在任何变量,函数或类声明的前面,从而将他们从模块导出,而import用于引入数据,例如如下所示

将下面这些js存储到exportExample.js中,分别导出的是数据,函数,类

代码语言:javascript
复制
/*
* 
* @authors 随笔川迹 (itclanCode@163.com) 
* @date    2018-07-07 18:01:23
* @desc:导出数据
* */


// 导出数据
 export var  name = "随笔川迹"; // 导出暴露name变量
 export let  weChatPublic = "itclanCoder"; // 暴露weChatPublic
 export const time = 2018; // 暴露time


// 导出函数
 export function sum(num1,num2){
      return num1+num2;
 }
/*
*
* 以上等价于
* function sum(num1,num2){
*    return num1+num2;
* }
* export sum;
* 
 */

 // 导出类
export class People{
      constructor(name,age){
         this.name = name;
         this.age = age;
      }
      info(){
         return `${this.name}${this.age}岁了`;
      }
 }

若将上面代码进行拆分

1. 导出数据,变量前面加上export关键字

代码语言:javascript
复制
export var  name = "随笔川迹";
 export let  weChatPublic = "itclanCoder";
 export const time = 2018;
// 上面的等价于下面的写法,以下这种是常见写法

var name = "随笔川迹";
let weChatPublic = "itclanCoder";
const time = 2018;

export {name,weChatPublic,time}

2. 导出函数,函数前面加上export关键字

代码语言:javascript
复制
export function sum(num1,num2){
      return num1+num2;
}

也可以这样:在定义它时没有马上导出它,由于不必总是导出声明,可以导出引用,因此下面这段代码也是可以运行的

代码语言:javascript
复制
function sum(num1,num2){
       return num1+num2;
}
// 之后将其导出
export sum;

注意:一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取,同样,任何未显示导出的变量,函数或类都是模块私有的,若没有用export对外暴露,是无法从模块外部访问的 例如:

代码语言:javascript
复制
function countResult(num1,num2){
       return num1-num2;
 }
// 没有通过export关键字导出,在外部是无法访问该模块的变量或者函数的

3. 导出类,类前面加上export关键字

代码语言:javascript
复制
export class People{
      constructor(name,age){
         this.name = name;
         this.age = age;
      }
      info(){
         return `${this.name}${this.age}`
      }
}

对应在另一个模块中通过import导入如下所示,模块命名为importExample.js

代码语言:javascript
复制
/*
*
* @desc:从exportExample模块中导入数据,通过import的方式
* @说明:由于我在node环境中测试,因为node暂且不支持Es6中的module语法,所以得先把es6代码通过babel转化成Es5代码,方可在node环境中执行该脚本,from后面具体路径引入的应该是通过Es6转化为Es5的代码
* 

 */

import { name, weChatPublic,time,sum,People} from "../modelTest1/exportExampleEs5.js"

var people = new People("小美",18); // 实例化perople对象
console.log(name);
console.log(weChatPublic);
console.log(time);
console.log(sum(1,2));
console.log(people.info());

注意1:在上面的示例中,除了export关键字外,每一个声明与脚本中的一模一样,因为导出的函数和类声明需要有一个名称,所以代码中的每一个函数或类也确实有这个名称,除非用default关键字,否则不能用这个语法导出匿名函数或类

注意2:因为在现今node版本环境中,目前还不直接支持export和import语法,也就是说在node环境中,直接写Es6的模块代码,用node执行js脚本,会抛出错误,所以得先把Es6转换成Es5版本的代码,然后在node环境下运行该脚本才不会报错,这种转换方式可以通过babel进行转化

安装babel如下所示:命令行终端下通过npm全局安装babel-cli

代码语言:javascript
复制
npm install --global babel-cli
npm install --save babel-preset-es2015

然后在当前目录下新建配置文件.babelrc,注意存储的位置不要带有中文路径,否则使用babel命令时会抛出错误

代码语言:javascript
复制
{
  "presets":["es2015"]
}

在编写好es6代码后通过 babel Es6源脚本 -o Es5脚本 这里的-o或--out-file指的从Es6标准格式转化生成的输出Es5文件

(在node环境中babel进行转化为Es5代码执行)

让我们对比看一下,其实在node中Es6中的export通过babel编译后Es5中代码是以exports方式进行导出的,而Es6中的import导入模块通过babel编译后是通过转变为require的方式引入的:

如下对比所示:Es6中export导出模块代码

代码语言:javascript
复制
/*
* 
* @authors 随笔川迹 (itclanCode@163.com) 
* @date    2018-07-07 18:01:23
* @desc:导出数据
* */


// 导出数据
 export var  name = "随笔川迹"; // 导出暴露name变量
 export let  weChatPublic = "itclanCoder"; // 暴露weChatPublic
 export const time = 2018; // 暴露time


// 导出函数
 export function sum(num1,num2){
      return num1+num2;
 }
/*
*
* 以上等价于
* function sum(num1,num2){
*    return num1+num2;
* }
* export sum;
* 
 */
function multiply(num1,num2){
   return num1+num2;
}
export multiply;
 // 导出类
export class People{
      constructor(name,age){
         this.name = name;
         this.age = age;
      }
      info(){
         return `${this.name}${this.age}岁了`;
      }
 }

通过babel编译转变为Es5代码:

代码语言:javascript
复制
"use strict";

Object.defineProperty(exports, "__esModule", {
    value: true
});

var _createClass = function() {
    function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i];
            descriptor.enumerable = descriptor.enumerable || false;
            descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true;
            Object.defineProperty(target, descriptor.key, descriptor); } } return function(Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

exports.sum = sum;

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

/*
 * 
 * @authors 随笔川迹 (itclanCode@163.com) 
 * @date    2018-07-07 18:01:23
 * @desc:导出数据
 * */

// 导出数据
var name = exports.name = "随笔川迹"; // 导出暴露name变量
var weChatPublic = exports.weChatPublic = "itclanCoder"; // 暴露weChatPublic
var time = exports.time = 2018; // 暴露time
var flag = true;



// 导出函数
function sum(num1, num2) {
    return num1 + num2;
}
/*
 *
 * 以上等价于
 * function sum(num1,num2){
 *    return num1+num2;
 * }
 * export sum;
 * 
 */

// 导出类

var People = exports.People = function() {
    function People(name, age) {
        _classCallCheck(this, People);

        this.name = name;
        this.age = age;
    }

    _createClass(People, [{
        key: "info",
        value: function info() {
            return "" + this.name + this.age + "\u5C81\u4E86";
        }
    }]);

    return People;
}();

而在另一个模块中importExample.js中,这里是Es6中import导入模块的代码

代码语言:javascript
复制
/*
*
* @desc:从exportExample模块中导入数据,通过import的方式
* 

 */

import { name, weChatPublic,time,sum,People} from "../modelTest1/exportExampleEs5.js"

var people = new People("小美",18); // 实例化perople对象
console.log(name);
console.log(weChatPublic);
console.log(time);
console.log(sum(1,2));
console.log(people.info());

在node中通过babel编译转化为Es5代码后,import相当于require的作用,但是他们两者是不同的,前者是按需引入,而后者是一次性全部引入

代码语言:javascript
复制
"use strict";

var _exportExampleEs = require("../modelTest1/exportExampleEs5.js");

var people = new _exportExampleEs.People("小美", 18); // 实例化perople对象
/*
*
* @desc:从exportExample模块中导入数据,通过import的方式
* 

 */

console.log(_exportExampleEs.name);
console.log(_exportExampleEs.weChatPublic);
console.log(_exportExampleEs.time);
console.log((0, _exportExampleEs.sum)(1, 2));
console.log(people.info());

Es6中模块导入的基本语法·

如果想从一个文件(模块)访问另一个文件(模块)的功能,则需要通过import关键字在另一个模块中引入数据,import语句的两个部分组成分别是:要导入的标识符标识符应当从那个模块导入,另外,导入的标识符的顺序可以是任意位置,但是导入的标识符(也就是大括号里面的变量)与export暴露出的变量名应该是一致的 具体的写法如下:

代码语言:javascript
复制
import {identifer1,indentifer2}  from "./example.js"  // import {标识符1,标识符2} from "本地模块路径"

import后面的双大括号表示从后面给定的模块导入的绑定,关键字from表示从哪个模块导入给定的绑定,该模块由表示模块路径的字符串指定(被称为模块说明符),如果在浏览器中,使用路径格式与<script>元素的相同,也就是说,必须把文件拓展名也加上

注意:在nodejs中,区分加前缀和不加前缀,不加路径前缀的表示的是包,而加入路径前缀的表示本地文件,例如:require("http")引入的是一个包;而require("./http.js")引入的是一个本地文件

注意:导入绑定的列表看起来与解构对象很相似,但两者不是一样的

当从模块中导入一个绑定时,它就好像使用了const定义的一样,也就是自动默认使用严格模式,你无法定义另一个同名变量(包括导入另一个同名绑定),也无法在import语句前使用标识符或改变绑定的值

1. 导入单个绑定

假设前面的实例在一个名为ExportExample.js的模块当中,我们可以导入并以多种方式使用这个模块中的绑定,可以只导入一个标识符:例如:

代码语言:javascript
复制
// 只导入一个
  import {sum} from "./example.js"

  console.log(sum(1,2));  // 3
  sum = 1; // 抛出一个错误,是不能对导入的绑定变量对象进行改写操作的

尽管ExportExample.js导出的函数不止一个,但这个示例导入的却只有sum()函数,如果尝试给sum赋新值,那么就会抛出一个错误,因为不能给导入的绑定重新赋值 为了兼容多个浏览器和Nodejs坏境,一定要在字符串之前包含/,./或../来表示要导入的文件

2. 导入多个绑定

如果想从示例模块中导入多个绑定,与单个绑定相似,多个绑定值之间用逗号隔开即可

代码语言:javascript
复制
// 导入多个
import {sum,multiply,time} from "./exportExample.js"
console.log(sum(1,2)); // 3
console.log(multiply(1,2)); // 3
console.log(time);  // 2018

在这段代码中,从exportExample.js模块导入3个绑定,sum,multiply和time之后使用它们,就像使用本地定义的一样 等价于下面这个: 不管在import语句中把一个模块写了多少次,该模块将只执行一次,导入模块的代码执行后,实例化过的模块被保存在内存中,只要另一个import语句使用它就可以重复使用它

代码语言:javascript
复制
import {sum} from "./exportExample.js"
import {multiply} from "./exportExample.js"
import {time} from "./exportExample.js

3. Es6中导入整个模块

特殊情况下,可以导入整个模块作为一个单一的对象,然后所有的导出都可以作为对象的属性使用,例如:

代码语言:javascript
复制
// 导入一整个模块
import * as example from "./exportExample.js"
console.log(example.sum(1,example.time));
consoole.log(example.multiply(1,2));// multiply与sum函数功能一样

在上面这段代码中,从本地模块的exportExample.js中导出的所有绑定被加载到一个被称作为example的对象中,指定的导出sum()函数,multiply()函数和time之后作为example的属性被访问,这种导入格式被称为命名空间导入,因为exportExample.js文件中不存在example对象,所以它被作为exportExample.js中所有导出成员的命名空间对象而被创建

Es6中模块语法的限制

export和import的一个重要的限制是,他们必须在其他语句和函数之外使用,例如,下面的代码会给出一个语法错误

代码语言:javascript
复制
if(flag){
      export flag; // 语法错误
 }

下面以在微信小游戏中测试为证

(export必须在函数其他语句之外使用否则会报错)

export和import的一个重要的限制是,他们必须在其他语句和函数之外使用,例如,下面的代码会给出一个语法错误

export语句不允许出现在if语句中,不能有条件导出或以任何方式动态导出,也就是说export命令规定的是对外的接口,必须与模块内部的变量建立一一对应的关系,不能这样写: export 5;或者 var num = 5; export num;必须得加上大括号 {变量名}去暴露它 模块语法存在的一个原因是要让javascipt引擎静态的确定哪些可以导出,因此,只能在模块顶部使用export

同样,不能在一条语句中使用import,只能在顶部使用它(这也是为什么很多框架在业务逻辑代码之前,需要什么插件,都得提前引入),如下代码所示,import语句也不能放在一条语句当中

代码语言:javascript
复制
function testImport(){
      import flag  from "./ExportExample.js"   // 语法错误
  }

下面时在微信小游戏中测试可证

(import也不能在其他语句或者函数内使用,否则会报错)

由于同样的原因,不能动态的导入或导出绑定,export和import关键字被设计成静态的 以上这种通过import导入模块与require的写法的具体区别是:

import 导入的方式更加灵活随意一些,要想用哪个变量,函数,模块就导入哪一个,按需加载,现在想想在使用框架当中,使用某个UI库里面的某单个组件,使用import导入单个组件而非全部一次性引入的原因了.

而使用require是全部都引入了的若想要更加效率的话,那么推崇import导入的方式

例1:全局完整引入,没有大括号,从element-ui库中引入Element,当然在vue中,还得Vue.use(插件名)全局注册一下

代码语言:javascript
复制
import Element from 'element-ui';
Vue.use(Element);

例2:从element-ui库中导入两个Button,Select组件

代码语言:javascript
复制
import { Button, Select } from 'element-ui
Vue.use(Button);
Vue.use(Select);

Es6中如何给导入导出时标识符重命名

从一个模块导入变量,函数或者类时,我们可能不希望使用他们的原始名称,就是导入导出时模块内的标识符(变量名,函数,或者类)可以不用一一对应,保持一致,可以在导出和导入过程中改变导出变量对象的名称

使用方式: 使用as关键字来指定变量,函数,或者类在模块外应该被称为什么名称 例如如下一函数

代码语言:javascript
复制
function sum(num1,num2){
     return num1+num2;
}

export {sum as add} // as后面是重新指定的函数名

如上代码,函数sum是本地名称,add是导出时使用的名称,换句话说,当另一个模块要导入这个函数时,必须使用add这个名称

若在importExample.js一模块中,则导入的变量对象应是add而不是sum,是由它导出时变量对象决定的

代码语言:javascript
复制
import  {add} from "./exportExample.js"

如果模块想使用不同的名称来导入函数,也可以使用as关键字

代码语言:javascript
复制
import {add as sum} from "./exportExample.js"
 console.log(sum(1,2)); // 3
 console.log(typeof add); // undefined

如上代码导入add函数时使用了一个导入名称来重命名sum函数,注意这种写法与前面导出export时的区别,使用import方式时,重新命名的标识符在前面,as后面是本地名称,但是这种方式,即使导入时改变函数的本地名称,即使模块导入了add函数,在当前模块中也没有add()标识符,如上对add的类型检测就是很好的验证

Es6中导入绑定时的一个注意点,导入定义时的变量无法更改

在Es6中的import语句为变量,函数,类创建的目的是只读绑定所要导入的对象,并不是像正常变量一样简单的引用原始绑定,标识符只有在被导出的模块中可以修改(也就是只能在export模块中修改),当导入绑定的模块后,它是无法更改绑定的值的(在import中无法对已导入绑定的变量作修改),from前面的就是绑定的变量对象,例如:如下代码所示

代码语言:javascript
复制
import {name,setName}  from "./exportExample.js"  // from前面双大括号中的变量对象是不可以被修改的,想尝试修改就会报错

console.log(name);  // 随笔川迹,此时访问name是全局变量
setName("好好先生");
console.log(name);  // 好好先生,函数内的同名变量会覆盖全局变量
name = "itclanCoder"  // 抛出错误,此处的name并非导入时name

当想尝试更改导入时变量对象的名称时,就会抛出错误

(在导入模块中,修改导入变量对象是会抛出错误的,不允许被修改,想修改,应当滚回导出模块中修改变量对象的值)

如上代码:当调用setName("好好先生")时会回到导出setName()的模块中去执行,并将name设置为好好先生,通过import导入的name标识符是export导出时的name标识符本地名称

总结

本文主要从什么是模块,Node中模块的导出与导入,如何检测node.js对Es6的支持情况 ,以及在Node中通过babel将es6代码转化为Es5代码在Node中执行,模块的导出(导出数据,函数和类)模块的导入(单个导入,多个导入,导入整个)

模块中在用export关键字导出所要暴露的对象和用import关键字导入暴露的对象中,导入的变量对象需要和导出的保持一致,当然也可以通过as关键字进行重命名,并且模块导入的变量对象无法被改写,如果要改写,那么需要到export所暴露对象的模块中进行改写

在模块化项目里,从模块中导出导入是如今非常常见的做法,如果在不给导出的标识符(变量,函数,类)呢,那么可以通过导出default关键字指定单个变量,函数或者类,预知后文详情,可持续关注了

关于模块化处理在未来的标准中非常重要,为什么初次看到那些脚手架生成的代码,文件导出导入,有些摸不透的,不知所云的,就是因为加入了Es6中的模块化知识,与AMD比较也好,还是CMD也罢,是有些区别的,本人初学者笔记学习心得,如果文章中有误导的地方,欢迎路过的老师多提意见和指正

作者:川川,一个靠前排的90后帅小伙,具有情怀的代码男,路上正追逐斜杠青年的践行者,愿做你耳朵旁边的枕男,眼睛笔尖下的窗户

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-07-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 itclanCoder 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 1. 导入单个绑定
相关产品与服务
云开发 CloudBase
云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档