关于import的思考

前言

起因是因为一次来回引入引发的问题。例如A引入了B的方法,B引入了A的方法,然后在调用时发现遇到了问题,于是决定做一篇关于模块引入的整理。


Module语法

历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。

在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

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

require和import本质上就是commonjs和ES6module的区别。一个是JavaScript社区指定的野生规范,一个是后续指定的官方规范。

// 常规用法
// CommonJS模块
let { stat, exists, readFile } = require('fs');

// ES6模块
import { stat, exists, readFile } from 'fs';

import的一些注意事项

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资源这种说法。

import会执行所加载模块。例如:import 'lodash',仅仅执行lodash模块,但是不输入任何值。

require('core-js/modules/es6.symbol');
require('core-js/modules/es6.promise');
import React from 'React';

目前阶段,通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令,可以写在同一个模块里面,但是最好不要这样做。因为import在静态解析阶段执行,所以它是一个模块之中最早执行的。下面的代码可能不会得到预期结果。

关于import()函数,其诞生的目的是为了让import也有动态加载的能力,实现require先前拥有的基本功能。


关于require和import本质区别

寸志:形式上看上去五花八门,本质可以涵盖为以下三点。

  1. CommonJS 还是 ES6 Module 输出都可以看成是一个具备多个属性或者方法的对象
  2. default 是 ES6 Module 所独有的关键字,export default fs 输出默认的接口对象,import fs from 'fs' 可直接导入这个对象;
  3. ES6 Module 中导入模块的属性或者方法是强绑定的,包括基础类型;而 CommonJS 则是普通的值传递或者引用传递

第3点具体该如何理解?

// counter.js
exports.count = 0
setTimeout(function () { ++exports.count }, 500)

// commonjs.js
const {count} = require('./counter')
setTimeout(function () {
  console.log('read count after 1000ms in commonjs is', count)
}, 1000)
// 输出结果:read count after 1000ms in commonjs is 0

//es6.js
import {count} from './counter'
setTimeout(function () {
  console.log('read count after 1000ms in es6 is', count)
}, 1000)
// 输出结果:read count after 1000ms in es6 is 1

count为number类型,因此commonjs中是值传递,传递0之后后续原来的参数发生变化并不会影响到引用方的count值。而es6中为强绑定,因此后续也跟着发生了改变。


关于循环引用

解答这个问题需要先了解commonjs和es6底层的加载原理

commonjs

CommonJS 的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。

{
  id: '...', // 模块名
  exports: { ... }, // 模块输出的各个接口
  loaded: true, // 表示是否执行完毕
  ...
}

执行一次后,再次执行只会去exports上取值,返回第一次运行的结果。
CommonJS 模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。所以,输入变量的时候,必须非常小心。

es6

ES6 模块是动态引用,如果使用import从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';

// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';

直接抛出这个典型的例子。执行a.mjs后,引用b,已知它从a输入了foo接口,这时不会去执行a,而是认为这个接口已经存在了。因此执行到第三行时发现foo并没有定义ReferenceError
可以通过将bar,foo写成函数的形式,这是因为函数具有提升作用,在执行import {bar} from './b'时,函数foo就已经有定义了,所以b.mjs加载的时候不会报错。这也意味着,如果把函数foo改写成函数表达式,也会报错。


心得体会

关于import的讨论,先前分析过它预加载、动态加载的行为。本次熟悉了语法用法和加载的机制,尽量通过函数的形式去返回可能存在循环调用的模块,在发生循环调用时也要尤为警惕避免产生不可预见的bug。


参考

Module 的语法 - 阮老师的es6教程
Module 的加载实现 - 阮老师的es6教程
require,import区别?-知乎

你可能感兴趣的:(关于import的思考)