在使用es6的export时,我们可能时常遇到这样的疑问:
- export default const x = 9; 为啥会报语法错误?
- export default function f(){}; export default class C{}; 为啥这又可以呢?
- 文件a:export let x = 8; 文件b:import { x } from './a' 两个x是同一个x吗?是一回事儿吗?
问题一:export default const x = 9; 为啥会报语法错误?
ecma-262 6.0(es6)的标准说明书中规定,export default后面只能是这几种形式:
- HoistableDeclaration(函数声明,generator声明,未来不仅限于前面这两个)
- ClassDeclaration(类声明)
- AssignmentExpression(赋值表达式)
所以从标准看,是不支持变量声明定义的语法的,但是我们的疑问是,为什么标准不支持这种写法呢,挺方便的挺常需要的东西。
其实是基于这样的考虑:export default 被设计成模块的默认导出方式,这个默认值只会有一个,但是const可以支持这种形式:const x = 8, y = 10, z = 5; 所以开发人员可能会这样去写export default const x = 8, y = 5, z=99; 这显然是自相矛盾的,不是一个好的语法设计。所以这种形式的语法干脆就被禁止掉了,可以用以下形式替代:
const x = 9;
export default x;
问题二:export default function f(){}; export default class C{}; 为啥这又可以呢?
其实第一个问题已经解释了原因。
funciton和class不可以一口气定义好几个,一次只能定义一个,这个和export default一个默认值的形式一致的,所以这种语法是行得通的。
问题三:文件a:export let x = 8; 文件b:import { x } from './a' 两个x是同一个x吗?是一回事儿吗?
这个问题有点意思,值得我们好好探究一下。
我们先来看一个例子:
文件a
import { x } from './b';
setTimeout(() => {
console.log('x', x);
}, 5000);
文件b
export let x = 8; // 会绑定
setTimeout(() => x = 9, 1000);
这个是时候运行的话,x会打印多少呢?
一种可能是,x被导出时,被赋值给新的变量:a的x = b的x; 此时b的x变化不会导致a的x变化,x打印为 8
另一种可能是,a的x和b的x是同一个变量,b的x变化,a的x也会变,x打印为9
用ts-node直接执行或者webpack打包后在浏览器执行,均得到了9
所以两个文件的x实际上是一回事儿,这个就是es6标准说的binding,也就是说这两个x实际上是绑定关系。
那该怎么实现这个es6的标准的呢,有一种方式可以实现这种效果,将b中的x变成o.x,类似于:
function () {
let o = {};
o.x = 8;
setTimeout(() => o.x = 9, 1000);
return o;
}
这样作为transpiler需要将代码中所有用到x的地方变成o.x。
webpack实现的方式比较巧妙一些,是这么做的:
a文件:
var _web_sub__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./b.js");
setTimeout(() => {
console.log('x', _web_sub__WEBPACK_IMPORTED_MODULE_0__.x);
}, 5000);
b文件:
__webpack_require__.d(__webpack_exports__, {
"x": () => x
});
let x = 8;
setTimeout(() => x = 9, 1000);
重点看b文件,
__webpack_exports__是一个空对象
__webpack_require__.d利用defineObjectProperty方法将将x以getter形式定义到__webpack_exports__上
最终__webpack_exports__会被返回给a文件,被赋值给_web_sub__WEBPACK_IMPORTED_MODULE_0__
执行_web_sub__WEBPACK_IMPORTED_MODULE_0__.x时,会触发 () = x 此getter
可以看到,webpack用了闭包的方式,在闭包中直接访问要导出的变量,这样a文件的x和b文件的x实际上是同一个,x并没有被赋值给其他中间变量。
webpack便是用的此方式实现的live binding(动态绑定),挺简单的一套操作。
不过我们再来看另外一个例子:
文件a:
import x from './b';
setTimeout(() => {
console.log('x', x);
}, 5000);
文件b:
let x = 8;
export default x; // 不会绑定
setTimeout(() => x = 9, 1000);
按照上面动态绑定的说法,x应该打印9
然而却不是,事实打印的是8
这又是为什么呢?难道default就不动态绑定了?es6也太混蛋了,太坑人了。
其实不然,大家可以在回顾一下上面标准里说的export default后面应该接啥,例子里的情况实际上属于AssignmentExpression的情况,也就是说例子里export default后面其实是个可以作为右值的表达式,而表达式可以是各种形式:
export default x;
export default x + 1;
export default 1 + 2;
这种情况下,其实并没有一个实际可绑定的变量,因为他是一个表达式,因此也就无所谓绑定了。
我们可以看一下webpack是如何实现这种场景的:
__webpack_require__.d(__webpack_exports__, {
"default": () => __WEBPACK_DEFAULT_EXPORT__
});
let x = 8;
const __WEBPACK_DEFAULT_EXPORT__ = (x);
setTimeout(() => x = 9, 1000);
x被作为表达式赋值给了__WEBPACK_DEFAULT_EXPORT__
我们再看另外一个例子:
文件a:
import x from './b';
setTimeout(() => {
console.log('x', x);
}, 5000);
文件b:
export default function f() {
};
setTimeout(() => f = 9, 1000);
这种情况,x打印了9,而不是空函数
为什么这次export default又可以绑定了呢,其实根据上面对表达式的解释很好推断,
function f() {}是一个很明确的函数定义,有一个明确的名字:f
f可以被绑定,class的情况其实是一样的
在webpack中,被绑定的变量会被放到一个闭包中返回,依赖此变量的模块在访问该变量时触发getter,闭包将绑定变量返回。
其实可以总结一下:只要export后面是一个可以明确获取到绑定变量的情况,该变量都会被绑定。
可以看一下es6标准中给出的用法,有ExportName和LocalName的概念
到此,export的常规只是部分已经结束。
然后我们还有一个思考,为什么es6要采用变量绑定的形式呢?
确实发现了一个好处,就是可以解决循环引用的问题。
咱们先说一下为什么es6可以支持循环引用。
大家都知道在深层遍历一个具有循环引用的变量时,如果不去主动检测规避的话,会形成死循环。那es6中为啥不会出现栈溢出或者没完没了的死循环呢?
我们先看标准中怎么说的:
ResolveExport是幂等的、无副作用,一个实现可以选择预先计算或者缓存住Exports的结果
ResolveExport是用来处理imported的,是幂等的、无副作用的。
那我们看看webpack是怎么实现的:
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
if(__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
模块导出的东西会存到__webpack_module_cache__中,等下次再重复import时,会直接读取缓存__webpack_module_cache__。这就是ResolveExport的实现。
有了这个之后,某模块第二次被重复引用时,并不会重复的去执行模块的代码,而是直接将上次导出的值返回给了需要的模块。这样当我们循环引用时,某模块第二次被引用,由于并不会重复执行模块代码,因此循环引用被切断,不会没完没了的引用下去(间接循环和直接循环都是如此)
举例如下:
文件util.js:
import { ajax } from './network';
console.log('ajax', ajax);
export function getPermission(id) {
const url = 'https://x.com/id';
return ajax(url);
}
export function parseUrl(url) {
}
getPermission(123); // 可成功运行
文件network.js:
import { parseUrl } from './util';
export function ajax(url) {
console.log('ajax is running');
parseUrl(url);
}
从两个文件上方的import可以看到是相互依赖,循环引用。
咱们可以结合webapck的__webpack_require__函数简单捋一下执行流程:
先调用__webpack_require__,传入util.js
=> 1. 生成__webpack_module_cache__ ,一个空的exports对象
=> 2. 执行util.js ,将导出变量以getter函数的形式加到exports对象上
=> 3. 由于依赖network.js,所以转为先执行network.js
=> 4. 发现network.js依赖util.js,转为执行__webpack_require__('./util.js')
=> 5. 发现__webpack_module_cache__中有utils.js的导出缓存:__webpack_module_cache__[moduleId].exports,直接将该缓存return,并没有继续去执行util.js(这一步是关键)
=> 6. network.js拿到缓存的util.js导出的数据继续往下执行
=> 7. network.js执行完毕后,将导出的数据返回给util.js,util.js继续执行
=> 结束
可以看到步骤5终止了无限循环
上面总体说明了为什么es6中循环引用不会导致死循环,但是另外有一个问题,就是步骤5直接reutrn了util.js的导出缓存,但此时util.js还没有开始执行,又哪里来的导出值呢?
答案是,在第5步的那一刻,util.js确实没有导出值,只有等utils执行完了,才能有真正的具有业务意义的值,但是第5步的导出数据已经返回给了network.js,network.js如果当时立即就访问该值,此时会触发上面说的getter函数,getter函数会直接访问导出的变量,如果该变量是个函数,可以进行提升,则可以正常运行,但是如果是个不能提升的普通变量,则会直接报错,这样的话即使es6允许循环引用,那也失去了意义,因为无法获取依赖的导出数据。
这时该我们的binding(绑定)出场了,由于es6的导出值和被依赖模块中的export值是绑定关系,network.js只需要等util.js执行完,就可以获取到util.js的导出数据了。
这种场景非常适合network中函数对导出数据依赖的情况
举例如下:
util.js:
import { ajax } from './network';
console.log('ajax', ajax);
export function getPermission(id) {
const url = 'https://x.com/id';
return ajax(url);
}
export function parseUrl(url) {
}
export const xxxxxx = 8;
getPermission(123); // 可成功运行
会报错的network.js
import { parseUrl, xxxxxx } from './util';
console.log('parseUrl', parseUrl); // 由于parseUrl是函数,有变量提升,可以正常访问
console.log('xxxxxx', xxxxxx); // 此处触发() = xxxxxx,会报错
export function ajax(url) {
console.log('ajax is running');
parseUrl(url);
}
正确的network.js
import { parseUrl, xxxxxx } from './util';
console.log('parseUrl', parseUrl); // 由于parseUrl是函数,有变量提升,可以正常访问
// console.log('xxxxxx', xxxxxx); // 此处触发() = xxxxxx,会报xxxxxx未定义
export function ajax(url) {
console.log('ajax is running');
console.log('xxxxxx', xxxxxx); // 打印出8,此时util的xxxxxx已经赋值完成
parseUrl(url);
}
到此结束