这是我参与「掘金日新计划 · 4 月更文挑战」的第 4 天,点击查看活动详情
脚本与模块
引入脚本的问题
如果我们直接在script标签中引入一个js,那js是如何被引入的呢?
可以看到,如果是通过script标签直接引入一个js,浏览器在加载并解析这个文件时,会将它内部声明的变量会挂载到window对象上。
这样合理吗?当然不,这样会导致命名冲突。
如果我们引入两个js文件,这两个文件中有相同的变量声明呢?
可以看到,后面引入的js文件中的变量会覆盖前面引入的js文件中同名的变量。也就是两个文件中的变量声明都会被挂载到window上,如果有同名的就按照js的一般规则覆盖。
引入模块化之后
那如果我们要用es6中的模块化引入呢?
浏览器会报错,说不能够在模块的外部使用import声明。那就是说普通的script并不是一个模块,其实它是一个脚本,每一个js中定义的一切都共享一个全局作用域,而在浏览器中这个全局作用域就是window。
要区分script引入的是脚本还是模块,需要另外加一个标识module。可以看到之前的错误已经没有了,但是有另外一个报错,是关于跨域的。也就是现在使用file这样的本地文件协议是不能进行模块的加载操作的。为了方便直接用webstorm打开页面,它会将静态文件运行在一个服务上。
可以看到这时没有了跨域的问题。会发生此时引入的js文件中的变量已经不会被挂载在window上。它与引入脚本那种共享全局作用域相反,在模块顶部创建的变量不会自动添加到全局作用域。而且这个变量也只在当前模块的顶级作用域存在,它与外界隔离。如果外部模块想要访问这个模块,那么该模块必须显示导出想让外部访问的变量,这样外部模块才能够从该模块导出绑定。
如果试图访问它,会出现未定义的错误。
模块导出的合理性
为什么ES6的模块化需要使用export显示导出呢?我相信这个问题很多人没有思考过,因为ES6定义了模块导出导入的规范,那就根据它的规范和实现来就可以了。我也没有想过这个问题,但是直到一位同事问起我,才意识到这是个需要考虑的问题。
也许你会问,那不导出,怎么导出?如果这样想就陷入了一个误区,好像只能这样,不导出就不能导入。export的变量才能导出,也就是ES6的模块化规范提供了决定模块中的变量是否导出的能力,如果不提供,那完全可以默认全部都是导出的,import的时候也是默认全部导入的。还是用个例子说明会方便一点:
a.js
function fn(){};
function fn2(){};
b.js
// 引入模块
import Obj from './fn.js';
// 调用模块中的函数
Obj.fn();
Obj.fn2();
不在a.js中显式导出,默认全部导入,使用的时候我自己去控制和决定使用哪些和不使用哪些。确实没问题,但是如果我们的模块是提供给别人使用的,别人将我们的模块作为第三方模块引入,难道我们会默认其中的内容全部对使用者都可见吗?很多情况下是不会的,比如我们在一个模块中,可以会基于单一职责原则去编写代码,需要导出函数c,但是函数c需要调用函数a和函数b,而函数a和b只是为了更好的组织和复用代码,这个时候默认全部导出,那我们就做不到只暴露函数c,而是暴露了所有。反之,如果我们可以有export去显示导出什么,那就做到了模块内部可见权限的控制。即模块中任何未显示导出的变量
、函数、类都是模块私有的,无法从模块外部访问。
我认为这是关于一个比较合适的解释。默认导出全部,是将代码可见权限的控制权交给了使用者,而显式导出才能暴露给外部模块,是将代码可见权限的控制权掌握在了开发者手里。
总结
本文并不是为了介绍ES6的模块化机制,只是探讨了模块化存在的作用,以及为什么现在的设计是合理的。主要有两点,一是关于脚本和模块的区别,引入脚本的方式会使所有js共享同一个全局作用域,会存在非常多的问题,比如命名冲突、全局对象会过于庞大,查找速度也会下降等各种问题。于是引入了模块,而模块需要export显示导出,决定哪些要导出,这样设计的合理性在于将代码可见性的控制权掌握在了开发者手里,可以做到模块内部的私有化。
我从未像这样思考过这个问题,结论也是简单明了,但习以为常的却是需要思考的。