前言
我在使用tsup的过程中,发现tsup会把我写的export default编译成下面这种形式:
// export default SxfDeadfilePlugin;
export {
SxfDeadfilePlugin as default
};
我特别好奇,tsup为什么要这么做呢?这两个写法有什么区别呢?于是我跑去问了同事,同事给我发了这篇英文文章,我觉得写得挺好的,就翻译出来,分享到掘金社区~
正文
除了export default,JavaScript模块导出的值都是实时绑定的。
什么是实时绑定?
JavaScript是以实时绑定的方式导出值。实时绑定这个词意味着是对存储导出值的内存位置的引用。换句话说,导出的值是通过引用传递给导入它的模块的。除了实时绑定之外,变量在JavaScript中从来不通过引用传递。
实时绑定的属性
我使用两个简单的模块来演示实时绑定的属性。子模块child.js将variable作为其默认导出
// child.js
let variable = 'value';
export { variable as default };
父模块导入variable,并尝试给它分配一个新的值。
// parent.js
import variable from './child.js';
variable = 'test'; // 产生错误: Assignment to constant variable.
当父模块导入子模块导出的variable时,会在父模块内创建一个variable的常量,试图给variable赋一个新值会产生一个给常量赋值的错误。因此,一个子模块导出的变量在其父模块中会成为一个常量。
和其父模块不同,子模块可以修改变量的值,甚至在该变量被导出后。当子模块修改导出的变量时,导入的值(即父模块中的常量值)也会发生变化。为了说清楚这一个现象,我必须使用更复杂的模块代码。新版本的child.js中新增了改变变量值的异步任务。
// child.js
let variable = 'value';
setTimeout(() => variable = 'new value');
export { variable as default };
父模块打印了导入的variable的值并新增了一个异步任务,又打印了了一次variable的值。由于子模块先调度任务,所以父模块调度的任务肯定会在variable改变之后执行。
// parent.js
import variable from './child.js';
console.log(variable); // 打印 value
setTimeout(()=>console.log(variable)); // 打印 new value
父模块打印了同一个看似恒定不变的variable的两个不同值——首先是初始值,然后是最近的新值。
默认导出语法——导出一个变量的两种方式
一个变量可以使用下面两个语句导出作为默认导出:
export default variable;
或者
export {
variable as default
};
第一个语句显然代码量更少,那么,我为什么要用代码更多的默认导出语句形式来演示实时绑定呢?
export default和export as default之间的区别
export default variable和export { variable as default }产生的实时绑定不是相等的。为了讲清楚这个区别,我在child.js中使用更短的export default variable替换了export { variable as default }。
// child.js
let variable = 'value';
setTimeout(() => variable = 'new value');
export default variable;
现在父模块没有看到子模块修改了variable的值,父模块打印了两遍导入的variable的初始值:
// parent.js
import variable from './child.js';
console.log(variable); // 打印 value
setTimeout(() => console.log(variable)); // 仍然打印 value
这似乎没有创建实时绑定或者使用更简洁的语法export default variable不能像预期那样工作。导入的variable现在表现为一个真正的常量。
这是两种默认导出语法形式之间的主要区别。export { variable as default }导出了对被导出变量的引用,而使用export default variable,导入模块不会接收到对被导出变量的引用。
为什么export default不会导出对该变量的实时绑定?
首先,说一下术语。一个字面量是代表一个值的源码。字面量不是一个变量,它是被分配给一个变量。
export default是为了导出字面量和匿名声明。例如,一个模块导出一个匿名函数:
export default value => console.log(value);
或者一个模块导出一个字符串字面量:
export default 'value';
JavaScript引擎是如何将一个实时绑定导出到字面量的呢?它把字面量分配给一个辅助变量,并给这个自动创建的变量创建一个实时绑定。基本上,一行代码export default 'value'被转换成了这样的代码:
const automaticallyCreatedVariable = 'value';
export { automaticallyCreatedVariable as default };
合成变量automaticallyCreatedVariable不能被js代码访问到。
当export default被用来导出一个变量时,也会发生完全相同的情况。它的值被复制到另一个辅助变量中,这个变量的引用被暴露为一个实时绑定。
Rollup是如何复制默认导出的实时绑定?
在一些应用程序中,绑定模块是有意义的,Rollup是一个容易定制的用户友好的JavaScript模块捆绑器。
Rollup将上面的例子与export { variable as default }结合在一起,成为一个能复制实时绑定属性的捆绑器。
let variable = 'value';
setTimeout(() => variable = 'new value');
console.log(variable); // 打印 value
setTimeout(() => console.log(variable)); // 打印 new value
在export default variable的例子中,实时绑定似乎不起作用,它被组合在一个捆绑包中,该捆绑包还是使用上面描述的机制去复制结果。
let variable = 'value';
setTimeout(() => variable = 'new value');
var variable$1 = variable; // synthetic variable$1 is created
console.log(variable$1); // parent does not receive variable
setTimeout(() => console.log(variable$1));