named import 和 default import
一、现象
在给公共业务组件单独打包的时候,碰到一个需要export 2个mixin和一个报错函数的场景。当时就直接这么些写的。
// common/index.js
// 初始化SelectMixin
let selectUrl = '/example/common/getSelectBykeys'
let setSelectUrlPrefix = () => {}
const SelectMixin = select(selectUrl, setSelectUrlPrefix)
// 初始化AuthProviderMixin
let authUrl = '/example/common/authUrl'
let transferAuthResult = () => {}
const AuthProviderMixin = authProvider(authUrl, transferAuthResult)
// 初始化request
let handleRequestError = () => {}
const RequestUtil = request(handleRequestError)
export default {
SelectMixin,
AuthProviderMixin,
RequestUtil
}
复制代码
然后在一个页面引入SelectMixin,代码如下
import { SelectMixin } from '../common/index.js'
复制代码
结果是提示
"export 'SelectMixin' was not found in '../common/index.js'
复制代码
在控制台输出SelectMixin的时候也是undefined
改成
import SelectMixin from '../common/index.js'
复制代码
的确是有值,但是输出后发现,值的内容如下
那么问题来了。
二、为什么能获取到这个导出的对象却无法解构到想要到值。
在一篇简书上看到这么说的。
export default {
SelectMixin,
AuthProviderMixin,
RequestUtil
}
复制代码
经过webpack 和 babel 转换成了
module.exports.default = {
SelectMixin,
AuthProviderMixin,
RequestUtil
}
复制代码
所以 SelectMixin 取不到值是正常的。瞬间感觉他说的很有道理的样子。结果后面就没了后来。真是一顿操作猛如虎。。。
后来一大佬过来了,他能懂。。。他说这是规范,就像平时的小括号和函数里的小括号一样一样的。好像是这么回事哈。。顶礼膜拜。。。
不过最后文章中还提到了一句
import 语句中的"解构赋值"并不是解构赋值,而是 named imports,语法上和解构赋值很像,但还是有所差别
这里我就直接去Google “import语法上和解构赋值的差别 ”,另一名大佬就看到了 named imports ,这差距从小学语文就能看出来了。。扎心了。
三、named imports
在说named imported 之前先看看经常会碰到的下面的代码。这是在这个文件里写了3个函数然后导出供其他文件使用。而我们使用的时候则如右图。
小问题: 这里引用 defaultExport 这个js 文件一定要这么写吗?
上面的问题可以想着先,然后我们来看看下面这几个知识点之后再回来说这个问题。 上面的代码用了 export default ,而对应的import的在这个时候被称作 default import。他们是成对使用的。所以用了 export default 则一定要用到 default import。这里需要记住的知识点有以下几点。
3.1 default exports 和 default imports
// this is default export
// A.js
export default 42
复制代码
// this is default import
// B.js
import A from './A'
复制代码
-
一个模块只能有一个default exports
-
default imports 只对 default export 有用, default exports 需要用 default imports 去获取。
-
在 default imports 中,导出的时候,可以为其任意命名,因为 default export 是匿名的。在下面的例子里,A, MyA, Something 都是内容相同的。只是其名字不同而已,这个语法就是获取./A值的同时,为其匿名的对象取个名字
import A from './A'
import MyA from './A'
import Something from './A'
复制代码
这里第3点回答了上面的一个问题。这种导出的时候,是匿名的,所以引入的时候的import后面接的是为这个匿名的对象取的一个名字,这个名字是任意的。所以不用一定要取文件的名字。
再回来看第2点。default imports 只对 default export 有用, default exports 需要用 default imports 去获取。这一句解释了在文中一开始提到问题。
第一点:一个模块只能有一个default exports在一个文件里写多个export default 是错误的。会报错的。规则是不允许的。就记住匿名导出每个文件有且只能有一个。当然,不用匿名导出也是可以的。export default关键词后面可以跟任何值:一个函数、一个类、一个对象,所有能被命名的变量
3.2 named exports 和 named imports
接下来我们再来看看这两段代码。
小问题: 这里引用 namedExport 这个js 文件一定要这么写吗?
上面的问题可以想着先。然后我们来看看下面这几个知识点之后再回来说这个问题。上面的代码用了 named default ,而对应的import的在这个时候被称作 named import。他们是成对使用的。所以用了 named default 则一定要用到 named import。这里需要记住的知识点有以下几点。
// this is named export
// A.js
export const A = 42
复制代码
// this is named import
// B.js
import { A } from './A'
复制代码
- 一个模块可以有多个 named exports
- named imports 只对 named export 有用, named exports 需要用 named imports 去获取
- 这里无法像default import 一样,给导出的对象任意取名,需要一一对应。当然可也提供了给 named import其他写法,给named export 重新命名的机会。
// B.js
import { A } from './A'
import { myA } from './A' // Doesn't work!
import { Something } from './A' // Doesn't work!
复制代码
- 但是一个模块导出多个named exports的时候,可以像上面那般import,不过也可以像解构一样,写在一起如下面这样。
// A.js
export const A = 42
export const myA = 43
export const Something = 44
复制代码
// B.js
import { A, myA, Something } from './A'
复制代码
这里第3点回答了上面的一个问题。这种导出的时候,是具名的,所以要按名字去解析对应的导出。不过这种named import 给了两种其他的引入方式,可以为命名的函数重新修改名字。也可把所以具名模块合成一个对象使用。
再回来看第2点。named imports 只对 named export 有用, named exports 需要用 named imports 去获取。这一句解释了在文中一开始提到问题。所以对于named export 只能用named import。
你可以export任何的顶级变量:function、class、var、let、const。
另外还发现一个有意思的,忍不住想飙一句英文: amazing ,default export 和 named export 可以混合使用,default import 和 named import 。
在es6解构里可以用冒号为解构的变量重命名,在named import里也可以,使用的是as 。上面的代码可以这么写。
// B.js
import anyThing, { myA as myX, Something as XSomething } from './A'
复制代码
补充
我们可以把 Default export 当成一个特殊的 named export ,其实default export 也可以像这样来解析。只是他默认是叫default的一个对象。切默认可以有任意一个名字去覆盖他的匿名。
import { default as anything } from './A'
复制代码
不过这样写也是不被允许的,毕竟default 是一个保留字段。系统会直接报错,不过讲道理,抛去这个保留字段问题,这个写法按道理也能获取到default export
import { default } from './A'
复制代码
四、延伸--模块的循环引用
在了解named import 过程中碰到一个有意思的点就是 模块点循环引用。文献里说到es6 对循环引用支持比CommonJs更好。特对这个进行了一番了解。
循环引用
"循环引用"(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。
通常,”循环引用"表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。
但是实际上,这是很难避免的,尤其是依赖关系复杂的大项目,很容易出现a依赖b,b依赖c,c又依赖a这样的情况。这意味着,模块加载机制必须考虑”循环引用”的情况。即使在设计初期通过很好的结构设计避免了,但是代码一重构,”循环引用”还是很容易就出现的。所以”循环引用”的情况是不可能避免的。
4.1 CommonJS模块的加载原理
这里不讨论其他静态文件只考虑脚本文件,CommonJS的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。如下:
{
id: '',
exports: '',
parent: '',
filename: null,
loader: false,
children: [],
paths: ''
// ...
}
复制代码
这个对象里,id属性是模块名,exports属性是模块输出的各个接口,loaded属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,省略。 具体可以去看 www.ruanyifeng.com/blog/2015/1… 也可以参考node源码 github.com/nodejs/node…
当代码用到这个模块的时候,就会到exports属性上面取值。即使再次执行require命令,也不会再次执行该模块,而是到缓存之中取值。
我们先来看一个例子,考虑一下答案。
这例子出自node官网,可以移步此处 nodejs.org/api/modules…不说答案,我们直接说这个在运行c.js时当过程。上边代码之中,c.js先引入了a.js。则按照required的原理,会执行a.js整个脚本。
a.js脚本先输出一个done变量,然后加载另一个脚本文件b.js。注意,此时a.js代码就停在这里,等待b.js执行完毕,再往下执行。 再看b.js的代码
b.js执行到第二行,就会去加载a.js,这时,就发生了”循环引用”。(CommonJs的循环引用的重要原则:一旦出现某个模块被”循环引用”,就只输出已经执行的部分,还未执行的部分不会输出。) 这里有得小伙伴会认为是去执行a.js之前没执行完的代码。但是规则不是这么定义的。因为a.js触发了循环引用,则a.js会返回已经执行的部分代码。
系统会去a.js模块对应对象的exports属性取值,a.js虽然还没有执行完,但是其exports里确实有值的,从exports属性取回已经执行的部分的值,而不是最后的值。 a.js已经执行的部分,只有一行,即 exports.done = false;
所以对于b.js来说,引入的a.js值为false。然后,b.js接着往下执行,等到全部执行完毕,再把执行权交还给a.js。于是,a.js接着往下执行,直到执行完毕。
c.js就第一句就将a.js和b.js全部加载完毕了。a.js和b.js最终返回的都是true。
所以最终会选择答案B。
上面是commonjs的循环引用的原理。接下来我们再来看另外一个例子。
例子出自此: exploringjs.com/es6/ch_modu…执行a.js 之后的结果是右边两种情况。这个比前面一个例子好理解。我们先来看一下es6 modules 的加载原理。
4.2 es6 modules 的加载原理
ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import时,不会去执行模块,而是只生成一个引用。 等到真的需要用到时,再到模块里面去取值。
因此,ES6模块是动态引用,不存在缓存值的问题,而且模块里面的变量,绑定其所在的模块。
那再看上面的例子。a.js之所以能够执行,原因就在于ES6加载的变量,都是动态引用其所在的模块。只要引用是存在的,代码就能执行。
如果按照CommonJS规范,上面的代码是没法执行的。a先加载b,然后b又加载a,这时a还没有任何执行结果,所以输出结果为null,即对于b.js来说,变量foo的值等于null,后面的foo()就会报错。
4.3 CommonJs补充
在commonJs里经常会看到exports 和 module.exports 。这个会比较混淆。
CommonJS定义的模块分为: 模块标识(module)、模块定义(exports) 、模块引用(require)。
在一个node执行一个文件时,会给这个文件内生成一个 exports和module对象, 而module又有一个exports属性。他们之间的关系如下图,都指向一块{}内存区域。
再看个例子
从上面可以看出,其实require导出的内容是module.exports的指向的内存块内容,并不是exports的。简而言之,区分他们之间的区别就是 exports 只是 module.exports的引用,辅助后者添加内容用的。
五、所以两种循环引用的关键还是在对模块引用时的处理方式不同。
5.1 commonJs
-
对于基本数据类型,属于复制。即会被模块缓存。同时,在另一个模块可以对该模块输出的变量重新赋值。
-
对于复杂数据类型,属于浅拷贝。由于两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改时会影响另一个模块。
-
当使用require命令加载某个模块时,就会运行整个模块的代码。
-
当使用require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
-
循环加载时,属于加载时执行。即脚本代码在require的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
5.2 Es6
-
ES6模块中的值属于【动态只读引用】。
-
对于只读来说,即不允许修改引入变量的值,import的变量是只读的,不论是基本数据类型还是复杂数据类型。当模块遇到import命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
-
对于动态来说,原始值发生变化,import加载的值也会发生变化。不论是基本数据类型还是复杂数据类型。
-
循环加载时,ES6模块是动态引用。只要两个模块之间存在某个引用,代码就能够执行。
参考
www.jianshu.com/p/ba6f582d5…
stackoverflow.com/questions/3…
hackernoon.com/import-expo…
2ality.com/2014/09/es6… exploringjs.com/es6/ch_modu…
www.cnblogs.com/unclekeith/…
www.ruanyifeng.com/blog/2015/1…
www.ruanyifeng.com/blog/2015/0…
github.com/nodejs/node…
segmentfault.com/a/119000001…
zhuanlan.zhihu.com/p/27159745