es6 之 import, export 以及 commonjs和es6的循环依赖比较

named import 和 default import

1、现象

在给公共业务组件单独打包的时候,碰到一个须要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

改为javascript

import SelectMixin from '../common/index.js'
复制代码
的确是有值,可是输出后发现,值的内容以下

image


那么问题来了。

2、为何能获取到这个导出的对象却没法解构到想要到值。

一篇简书上看到这么说的。
export default {
    SelectMixin,
    AuthProviderMixin,
    RequestUtil
}
复制代码
通过webpack 和 babel 转换成了
module.exports.default = {
    SelectMixin,
    AuthProviderMixin,
    RequestUtil
}
复制代码

因此 SelectMixin 取不到值是正常的。瞬间感受他说的颇有道理的样子。结果后面就没了后来。真是一顿操做猛如虎。。。

后来一大佬过来了,他能懂。。。他说这是规范,就像平时的小括号和函数里的小括号同样同样的。好像是这么回事哈。。顶礼膜拜。。。

不过最后文章中还提到了一句

import 语句中的"解构赋值"并非解构赋值,而是 named imports,语法上和解构赋值很像,但仍是有所差异html

这里我就直接去Google “import语法上和解构赋值的差异 ”,另外一名大佬就看到了 named imports ,这差距从小学语文就能看出来了。。扎心了。


3、named imports

在说named imported 以前先看看常常会碰到的下面的代码。这是在这个文件里写了3个函数而后导出供其余文件使用。而咱们使用的时候则如右图。

image

小问题: 这里引用 defaultExport 这个js 文件必定要这么写吗?

上面的问题能够想着先,而后咱们来看看下面这几个知识点以后再回来讲这个问题。 上面的代码用了 export default ,而对应的import的在这个时候被称做 default import。他们是成对使用的。因此用了 export default 则必定要用到 default import。这里须要记住的知识点有如下几点。java

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'
复制代码
  1. 一个模块只能有一个default exportsnode

  2. default imports 只对 default export 有用, default exports 须要用 default imports 去获取。webpack

  3. 在 default imports 中,导出的时候,能够为其任意命名,由于 default export 是匿名的。在下面的例子里,A, MyA, Something 都是内容相同的。只是其名字不一样而已,这个语法就是获取./A值的同时,为其匿名的对象取个名字git

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

接下来咱们再来看看这两段代码。

image

小问题: 这里引用 namedExport 这个js 文件必定要这么写吗?

上面的问题能够想着先。而后咱们来看看下面这几个知识点以后再回来讲这个问题。上面的代码用了 named default ,而对应的import的在这个时候被称做 named import。他们是成对使用的。因此用了 named default 则必定要用到 named import。这里须要记住的知识点有如下几点。es6

// this is named export
// A.js 
export const A = 42
复制代码
// this is named import
// B.js
import { A } from './A'
复制代码
  1. 一个模块能够有多个 named exports
  2. named imports 只对 named export 有用, named exports 须要用 named imports 去获取
  3. 这里没法像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 。

image

在es6解构里能够用冒号为解构的变量重命名,在named import里也能够,使用的是as 。上面的代码能够这么写。
// B.js
import anyThing, { myA as myX, Something as XSomething } from './A'
复制代码

补充

咱们能够把 Default export 当成一个特殊的 named export ,其实default export 也能够像这样来解析。只是他默认是叫default的一个对象。切默承认以有任意一个名字去覆盖他的匿名。github

import { default as anything } from './A'
复制代码

不过这样写也是不被容许的,毕竟default 是一个保留字段。系统会直接报错,不过讲道理,抛去这个保留字段问题,这个写法按道理也能获取到default exportweb

import { default } from './A'
复制代码

4、延伸--模块的循环引用

在了解named import 过程当中碰到一个有意思的点就是 模块点循环引用。文献里说到es6 对循环引用支持比CommonJs更好。特对这个进行了一番了解。segmentfault

循环引用

"循环引用"(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命令,也不会再次执行该模块,而是到缓存之中取值。

咱们先来看一个例子,考虑一下答案。

images
这例子出自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的循环引用的原理。接下来咱们再来看另一个例子。

images
例子出自此: 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属性。他们之间的关系以下图,都指向一块{}内存区域。

images

再看个例子

从上面能够看出,其实require导出的内容是module.exports的指向的内存块内容,并非exports的。

简而言之,区分他们之间的区别就是 exports 只是 module.exports的引用,辅助后者添加内容用的。


5、因此两种循环引用的关键仍是在对模块引用时的处理方式不一样。

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

相关文章
相关标签/搜索