ES6模块化import export的用法

1,753 阅读11分钟

提到importexport这两个导入和导出的关键字, 我相信诸位前端开发工程师一定不陌生, js的模块化从一开始社区里孵化的各种方案, 到最后一统江湖的ES6官方的模块化方案, 从百家争鸣到天下一统, 此后我们在进行模块化开发的时候一直用的就都是ES6这个模块化的语法了, 也就是我们今天要聊的importexport

本文会直接对importexport进行一个探讨和交流, 会涉及到ES6相关的知识, 如有需要了解ES6相关的知识, 可以看看阮一峰老师的ES6 入门教程

同时需要注意的是, 要使用importexport关键字需要满足以下两个条件中的一个:

  1. 使用webpack等打包工具处理
  2. 将代码写在<script type="module"></script>

本文将采用第一种方式来处理这个问题, 毕竟模块化的写在<script type="module"></script>中是个人非常不推荐的做法, 一来这个标签的module的兼容性堪忧, 二来我们的模块化开发都是基于nodejs webpack来做的, 这样才能最大限度发挥模块化的意义, 而webpack的配置就不再熬述, 毕竟不是本文的重点, 推荐大家使用现成的脚手架工具来帮我们完成webpack的配置, 给我们一个完备的开发环境, 比如国内的react开发人员常用的一个脚手架工具: 阿里的UmiJS

import和export default

导入和导出无法孤立 拆开来看, 二者是一起出现的, 那么首先, 我们来聊一聊importexport default, 我们建立两个文件叫im.jsex.js, 对的, 顾名思义, 就是我的前任的意思, 就是导入和导出这两个单词importexport的前两个字母, 方便我们理解这两个文件具体的职责: im.js负责导入文件, ex.js则负责导出

import xxx

接着我们在这两个文件中写入如下内容:

ex.js:

const func = () => {
  console.log('func执行了');
};

export default func;

im.js:

import func from './ex';

func();

运行正常, 输出结果如下:

func执行了

那如果我们导入的时候更改一下名字呢, ex.js中代码不变, im.js修改为如下:

import foo from './ex';

foo();

程序执行的结果和上面一样, 至于为何, 我们先按下不表, 接着往下探索

import { xxx }

相信很多朋友还见过这样的导入方式:

import { useState } from 'react';

还是上面的两个文件, 这回我们做一个修改:

im.js:

import { func } from './ex';

func();

es.js文件中的代码保持不变, 但为了方便阅读这里也再写一遍:

ex.js:

const func = () => {
  console.log('func执行了');
};

export default func;

此时我们发现报错了:

Object(...) is not a function

那为何报错了呢?我们可以一起来分析一下看看:

import { xxx }的写法很像ES6解构的写法, 首先解构的方式获取值, 我们叫解构赋值, 比如我们会有这样的写法:

const obj = {
  a: 1,
  b: 2,
  c: 3
};

const { a } = obj;
//...

也就是从一个对象中读取他的某个字段/属性, 相信大家并不陌生, 那再回到我们的导入语句:

import { func } from './ex';

我们就可以理解为: 从./ex中读取它的属性func, 那么首先就需要确定两个问题:

  1. 这个./ex是不是一个对象
  2. 它里面有没有一个属性叫func

要回答这两个问题, 我们就要回到上面提到的改了名字之后依旧能执行的那段代码:

import foo from './ex';

foo();

还有一开始的代码:

import func from './ex';

func();

我们对比来看就会发现, 这两个foofunc都是函数, 都能在末尾加上()来调用, 而显然它们都不是函数名, 因为我们ex.js中定义的函数并不叫foo, 也不叫func, 里面其实是定义了一个叫func的变量, 然后将一个函数赋值给它, 因此, foofunc其实是两个变量, 两个被赋值了的变量, 它们的值是一个函数, 那如何证明呢?我们可以做如下的修改:

im.js保持不变:

import func from './ex';

func();

ex.js:

function func() {
  console.log('func执行了');
}

export default func;

这里的ex.js中就不使用函数表达式了, 而是直接使用关键字function来声明一个函数, 此时程序执行依旧正常, 再次修改:

im.js:

import foo from './ex';

foo();

ex.js保持不变:

function func() {
  console.log('func执行了');
}

export default func;

修改之后程序依旧能正常执行, 到这, 我们就可以回答上面的那两个问题了:

  1. 这个./ex是不是一个对象

不是

  1. 它里面有没有一个属性叫func

没有

export default小结

这个关键字导出的是值, 这个值在import的时候可以使用任意的变量去接收

import和export

import xxx

接下来我们来看看importexport, 注意是export, 而不是export default, 修改文件内容如下:

im.js:

import func from './ex';

func();

ex.js:

export const func = () => {
  console.log('func执行了');
};

此时报错:

Object(...) is not a function

在探讨这个问题之前要先聊一下这个问题: 如果导出的时候这么写会发生什么问题:

ex.js:

const func = () => {
  console.log('func执行了');
};

export func;

此时会报语法的错误:

Unexpected token, expected "{"

这是由于ES6的特性导致的, export后面只能写关键字或者字面量, 诸如export const ..., 或者export {...}

回到那个报错的问题, 之前的

import { func } from './ex';

也是报这个错, 这是因为导出的时候使用了export default, 导出的是值, 而不是object, 应该使用import xxx from './ex';的方式

而这里我们使用了import xxx from './ex';反而报错, 也就是说:

export default + import { func } from './ex'; => 报错

export const + import func from './ex'; => 报错

还都是同一个错Object(...) is not a function, 而:

export default + import func from './ex'; => 运行正常

这是因为export default导出的是值, import xxx from的时候可以用任意变量来接收, 而从报错的结果我们看到我们导入的却是一个object

那我们是不是可以大胆的猜一猜: export const + import { func } from './ex'; => 是不是就能正常运行呢?如果导出的是一个object, 然后里面还有一个方法叫func呢?

import { xxx }

试试看:

im.js:

import { func } from './ex';

func();

ex.js:

export const func = () => {
  console.log('func执行了');
};

此时我们发现程序运行正常了, 也就是说我们的猜测是正确的, 同时可以想到似乎当我们使用exprot const的时候导出的确实是一个object, 但, 真的是吗? 我们再试一次: im.js:

import { func } from './ex';

func();

这里ex.js里做一个修改: ex.js:

const func = () => {
  console.log('func执行了');
};

export { func };

这个写法也是可以的, 可以正常运行, 那如果导出的时候多写几个函数呢:

im.js:

import { func, func2 } from './ex';

func();
func2();

ex.js:

export const func = () => {
  console.log('func执行了');
};

export const func2 = () => {
  console.log('func2执行了');
};

或者:

ex.js:

const func = () => {
  console.log('func执行了');
};

const func2 = () => {
  console.log('func2执行了');
};

export { func, func2 };

这两种导出的写法都是可以的

export小结

到这, 我们就可以确定了: export导出的是模块的变量, 我们在使用import导入的时候需要指定变量的名称

其他用法

export default + export

相信大家在平时日常的开发中会看到这样的语句:

import xxx, { x1, x2 } from 'xxxxx';

比如:

import React, { useState, useEffect } from 'react';

通过前面的探讨我们知道, 这样的导入语句是导入了一个值, 同时还导入了两个变量, 对应的导出就是export default + export, 那我们尝试写一下:

im.js:

import deFunc, { func, func2 } from './ex';

func();
func2();
deFunc();

ex.js:

export const func = () => {
  console.log('func执行了');
};

export const func2 = () => {
  console.log('func2执行了');
};

const defaultFunc = () => {
  console.log('defaultFunc执行了');
};

export default defaultFunc;

同时需要注意的是, 一个文件中只能有一个export default否则会报错, 如果我们这样写:

export const func = () => {
  console.log('func执行了');
};

export const func2 = () => {
  console.log('func2执行了');
};

const defaultFunc = () => {
  console.log('defaultFunc执行了');
};

const defaultFunc2 = () => {
  console.log('defaultFunc2执行了');
};

export default defaultFunc;
export default defaultFunc2;

那就会报错:

Only one default export allowed per module.

取别名

有的同学可能还看多过这样的写法:

import * as xxx from 'xxxx';

这个as关键字就是一个取别名的用法, 这里我们可以根据之前的结论来大胆推断一下这样的导入形式对应的导出代码应该怎么写

首先, export default一个模块中只允许有一个, 而且导出的是值, 是可以让我们用任意变量接收的值, 此时取别名就没有意义了, 因为都能用任意变量名来接收了, 我们想要什么变量名都是可以的, 再取个别名完全就是多此一举

既然如此, 那么就只剩export了, 接下来就验证一下我们的推测:

im.js:

import * as actions from './ex';

actions.func();
actions.func2();

ex.js:

export const func = () => {
  console.log('func执行了');
};

export const func2 = () => {
  console.log('func2执行了');
};

这个写法我第一次看到是在学习react的时候, react中有个一个action的概念, 一个文件中有多个action, 那么把这些零散的action取一个别名, 然后再导入使用, 当然了, 我们知道还可以这么写:

import { func, func2 } from './ex';

我个人倾向于在导入的时候就做一个指定变量名的操作, 这样的话代码看起来会更加清晰, 同时我们需要哪一个时候导入哪一个即可, 不需要取个别名一次全部导入

行文至此, 主要的内容都已经和大家聊完了, 最后的最后再提亿一点点, 关于这个一些不是太常用的用法, 就是它的转发的用法, 具体是在哪遇到的我忘了, 只记得我自己做了一个探索和记录, 在这里一并和大家聊一聊

其他用法2: 转发

这里我们再创建一个文件: forward.js, 转发的意思

转发变量

im.js:

import { func } from './forward';

func();

forward.js:

export { func } from './ex';

ex.js:

export const func = () => {
  console.log('func执行了');
};

export const func2 = () => {
  console.log('func2执行了');
};

使用expor from即可完成我们的转发的操作

转发值

既然有转发变量, 那自然也有转发值的操作

im.js:

import deFunc from './forward';

deFunc();

forward.js:

export default from './ex';

ex.js:

export const func = () => {
  console.log('func执行了');
};

export const func2 = () => {
  console.log('func2执行了');
};

const defaultFunc = () => {
  console.log('defaultFunc执行了');
};

export default defaultFunc;

这个是转发值的语法, 那么保留我们的export const对程序的运行也是没有影响的, 只是这个forward.js中的写法:

export default from './ex';

会让vs code误以为这是ts, 然后会提示你有语法错误, 这时我们做一个修改即可:

export { default } from './ex';

这么写就行了

替代写法

转发的操作实际上就是导入加上重新导出, 也就是说可以通过导入语法加上导出语法完成, 比如可以这么写:

转发变量:

forward.js:

import { func } from './ex';

export const a = func;

引入的时候:

im.js:

import { a } from './forward';

a();

转发值的时候:

forward.js:

import deFunc from './ex';

export default deFunc;

引入的时候:

im.js:

import deFunc from './forward';

deFunc();

更多详细的用法可参考如下的文章:

  1. export const vs. export default in ES6

  2. ECMAScript Proposal: export default from

  3. Module的语法_阮一峰ECMAScript 6 入门

另外需要注意的是, import { xxx } from 'a.js';的语法和ES6中的解构的语法很像, 但并不是一回事, 在ES6的模块化语法中, 我们使用export关键字导出的是模块的变量, 因此在使用import { xxx } from 'a.js';做导入操作的时候, 导入的也是模块a.js的变量, 比如这里的变量名就是xxx. 以及a.js是一个文件(模块), 而非对象, 故import { xxx } from 'a.js';并不是ES6中的解构的语法, 更详细的解释大家可以看一下上面的第三个文章, 里面阮一峰老师对ES6的模块进行了更详细, 更深入地讲解


  • 2021年09月15日更新: ex.js:
export const func = () => {
  console.log('func执行了');
};

export const func2 = () => {
  console.log('func2执行了');
};

const func3 = () => {
  console.log('func3执行了');
}

export default func3;

im.js:

import * as actions from './ex';

更新一下, 如果是这样的情况, 那么此时这个actions里会是这样的:

{
  default: Function,
  func: Function,
  func2: Function
}

其中里面的default就是我们export default导出的那个函数func3, 当然还可以这么使用:

im.js:

import func3, { func, func2 } from './ex';

这个写法相对比较常见, 比如在使用reactjs进行开发的过程中就会看到:

import React, { PureComponent } from 'react';

这就是因为react中使用了上面ex.js的导出方式

发现这个的契机是因为在做其他的开发工作, 发现某个库使用了这样的引入方式:

import * as React from 'react';

因此让我产生了上述一开始的疑问, 然后想到了这篇文章, 于是在自己尝试之后来这里做一个更新