JavaScript的发展史 JavaScript的发展壮大 在JavaScript诞生之初只是作为一个脚本语言来使用,主要是做一些简单的表单校验等,因为代码量不多,所以是直接跟html写在一个文件里面,并且用script标签包裹
1 2 3 4 5 // index.html<script > var name = 'shiyuq' var age = 18 </script >
但是随着业务越来越复杂,尤其在ajax出现后代码量飞速增长,开发者们纷纷将JavaScript代码写到单独的js文件中,与html文件解耦,如下:
1 2 3 4 5 6 <script src='./index.js' ></script>var name = 'shiyuq' var age = 18
再后来,多个开发者都将自己的js文件引入到一个html文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <script src='./index.js' ></script><script src ='./index-shiyuq1.js' > </script > <script src ='./index-shiyuq2.js' > </script > var name = 'shiyuq' var age = 18 var name = 'faker' var age = 28 var name = (name ) => { return `hello ${name} ` }var age = (age ) => { return age + 1 }
不难发现,问题已经稍有眉头了,此时用哪个文件中的变量完全就取决于谁引用在最下面(在调用它之前),如果在不同文件中变量的类型还不一致,就会导致程序直接崩溃,这种从某种程度上来讲也属于全局变量污染 ,开发者的噩梦就此来临
模块化的出现 为了解决全局变量污染的问题,开发者开始使用命名空间的方法,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <script src='./index.js' ></script><script src ='./index-shiyuq1.js' > </script > <script src ='./index-shiyuq2.js' > </script > app.module = {} app.module .name = 'shiyuq' app.module .age = 18 app.moduleA = {} app.moduleA .name = 'faker' app.moduleA .age = 28 app.moduleB = {} app.moduleB .name = (name ) => { return `hello ${name} ` } app.moduleB .age = (age ) => { return age + 1 }
此时,已经有隐隐约约的模块化的概念了,只不过是用命名空间来实现的。但还是有个隐性问题,index-shiyuq1.js的文件作者可以很方便的通过app.module.name来获取到模块index.js中的name,当然也可以很方便的去修改它,但是修改却让index.js毫不知情。这是不允许发生的!
接着,聪明的开发者们又想到了JavaScript的函数作用域,振臂一呼,用闭包 可以解决现在的问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <script src="./index.js" ></script><script src ="./index-shiyuq1.js" > </script > <script src ="./index-shiyuq2.js" > </script > app.module = (function ( ) { var name = 'shiyuq' var age = 18 return { getName : () => name, getAge : () => age } })() app.moduleA = (function ( ) { var name = 'faker' var age = 28 return { getName : () => name, getAge : () => age } })() app.moduleB = (function (name, age ) { var name = name var age = age return { getName : () => name, getAge : () => age } })('hello shiyuq2' , 28 + 1 )
现在index-shiyuq2.js可以通过app.moduleA.getName()来获取到模块A中的名字,但是各个模块的名字都保存在各自的函数里面,无法修改,但是由于模块的加载有先后顺序,模块B可以访问模块A,但是模块A却无法访问模块B
所以模块化,不仅要处理全局变量污染,数据的保护,还要解决模块间的依赖关系
CommonJS应运而生 为了解决上面出现的一系列问题,所以需要制定模块化的规范,CommonJS就是新的规范,下面来讲解以下CommonJS大致的作用
概述 node应用是由模块组成,采用了CommonJS规范,每个文件就是一个模块,有自己的作用域,且在一个文件中定义的变量、函数和类都是私有的,对其他文件不可见
1 2 3 var age = 28 var getAge = (val ) => val - 10
上面的age和getAge都是当前index.js文件私有的,但是如果你想要在多个文件中分享变量,可以使用global关键字
虽然但是,不推荐!
CommonJS规范规定:每个模块内部有两个变量可以使用,分别是require和module
require:用来加载一些模块
module:代表的是当前模块,是一个对象且上面保存了当前模块的信息。然后它上面有一个exports属性,保存着当前模块要导出的接口或者变量,使用require加载其他模块获取的值其实就是module上面的exports属性
1 2 3 4 5 6 7 8 9 10 var name = 'shiyuq' var age = 18 module .exports .name = 'shiyuq' module .exports .age = 18 var a = require ('a.js' ) console .log (a.name ) console .log (a.age )
CommonJS——exports 为了方便,nodejs在实现CommonJS规范的时候,为每个模块都提供了一个exports的私有变量,指向了module.exports,于是你可以理解为exports和module.exports都指向了一个内存地址,所以你给exports中添加属性的同时,module.exports的数据也会同步变化,相当于在每个模块开始的地方,加入了下面一行代码:
1 var exports = module .exports
所以上面的代码也可以这么写:
1 2 3 4 5 var name = 'shiyuq' var age = 18 exports .name = 'shiyuq' exports .age = 18
warning :
由于exports是模块内部的私有局部变量,它是指向了module.exports的地址,所以你直接对他赋值是不可取的,这相当于改变了此变量的内存地址!!!
1 2 3 4 var name = 'shiyuq' var age = 18 exports = name
请看下面的代码:
1 2 3 4 5 6 7 8 9 var module = {exports : {}}var exports = module .exports console .log (exports ) console .log (module .exports ) var name = 'shiyuq' var age = 18 exports = nameconsole .log (exports ) console .log (module .exports )
同样,如果你对module.exports重新赋值,也是需要注意的地方
1 2 3 4 5 var name = 'shiyuq' exports .name = nameconsole .log (module .exports ) module .exports = 'hello shiyuq' console .log (module .exports )
建议 :可以只使用一种导出的方式,建议使用module.exports,写在每个模块的结尾
CommonJS的实现 我们先了解以下CommonJS的一些模块的特点:
1:所有的代码都运行在模块的作用域,不会污染全局作用域
2:模块可以多次加载,但只会在第一次加载时运行一次,之后缓存运行结果,以后再次加载,直接读取缓存结果,想要让模块再次运行,需要清除缓存
3:模块加载的顺序,按照其在代码中出现的顺序
在了解了CommonJS的一些关键规范和特点后,我们不难发现CommonJS的主要使用的技术,离不开三个关键字,就是exports,module和require
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var name = 'shiyuq' var age = 18 exports .name = nameexports .age = agevar a = require ('a.js' )console .log (a.name ) console .log (a.age ) var name = 'jenny' var age = 17 exports .name = nameexports .age = agevar b = require ('b.js' )console .log (b.name )
所以结合第一部分咱们对于Javascript的解析后,不难写出CommonJS的简易实现(使用立即执行函数),将require、exports、module三个参数传入,再把模块代码放入立即执行函数中,模块的导出值放在module.exports中,这样就实现了模块的加载,如下:
1 2 3 4 5 6 7 8 9 10 11 (function (module , exports , require ) { var a = require ('a.js' ) console .log (a.name ) console .log (a.name ) var name = 'jenny' var age = 17 exports .name = name exports .age = age })(module , module .exports , require )
知道了CommonJS的实现原理后,就很容易可以把规范的项目代码转换成浏览器支持的代码,例如咱们熟知的webpack,咱们以webpack为例,看看使用webpack构建的时候,主要做了哪些工作?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 (function (modules ) { })({ 'a.js' : function (module , exports , require ) { }, 'b.js' : function (module , exports , require ) { }, 'c.js' : function (module , exports , require ) { } })
接下来,我们需要按照CommonJS的规范,实现模块管理中的内容,然后,我们知道加载过的模块会被缓存,所以我们需要一个对象来缓存加载过的模块,然后需要一个require函数来加载模块,在加载的时候需要生成一个module,并且module上需要有一个exports属性,用来接收模块导出的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 (function (modules ) { var cachedModules = {} var require = function (moduleName ) { if (cachedModules[moduleName]) return cachedModules[moduleName].exports var module = { moduleName, exports : {} } cachedModules[moduleName] = module modules[moduleName].call (module .exports , module , module .exports , require ) return module .exports } return require ('a.js' ) })({ 'a.js' : function (module , exports , require ) { }, 'b.js' : function (module , exports , require ) { }, 'c.js' : function (module , exports , require ) { } })
所以上面基本已经实现了CommonJS的核心规范
require文件的加载流程 咱们上面已经讲过了require文件的时候,文件会有缓存,那么咱们一般在开发的时候,会有三种类型,通常是核心模块、文件模块以及第三方模块,那么require在加载的时候,遵循什么规律呢?
核心模块:像fs、http、path等等,都会被识别为nodejs的核心模块
文件模块:像是使用./、../作为相对路径的文件模块,/作为绝对路径的文件模块
第三方模块:非路径形式并且也是非核心模块的模块,被称为第三方模块,比如咱们常用的lodash、moment等等
其中核心模块的加载速度最快,因为已经被编译成二进制代码;而文件模块由于第一次加载会被缓存,所以第二次加载的时候也会很快;所以咱们需要关注的是第三方模块的加载,加载顺序如下:
首先是在当前的node_modules目录查找
如果没有,在父级目录的node_modules中查找,如果没有,继续向上查找
沿着路径递归,直至根目录下的node_modules
如果没有,提示模块未找到(module *** not found)
那既然知道了require是如何引入模块的,那么我们来看下面这个问题:循环引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var b = require ('b.js' )console .log ('我是a文件' )exports .sayHi = () => { console .log (b ()) }var a = require ('a.js' )console .log ('我是b文件' )var userInfo = { name : 'shiyuq' , age : 18 }module .exports = () => userInfovar a = require ('a.js' )var b = require ('b.js' )console .log ('我是入口文件' )
接下来大家可以在终端中输入node main.js,运行结果如下:
所以从上面的运行结果,不难看出CommonJS在分析模块的加载阶段,采用的是深度优先遍历,执行的顺序是父->子->父,但是要注意的是在b.js模块还没有加载完成的时候,此时在b.js模块中是没有a模块的sayHi方法的,因为a模块还没有导出sayHi方法【如果你循环引用了,就会出现这样的问题,这在我们的工作中,已经是令人非常头疼的存在】
其他模块化方案 我们学习了CommonJS的模块化规范,知道了它的模块加载机制是同步的,这在服务端是可行的,但是在浏览器端,难免会出现页面假死,阻塞后续代码执行,为了解决这个问题,所以后面又发展了一些其他的模块化规范
后端
CommonJS
Node.js
前端
AMD
RequireJS
CMD
Sea.js
前后端
ES6 Modules
ES6
因为本来对于前端没有进行深入的研究,个人看法是一开始出现的RequireJS其实就是为了解决CommonJS规范不能用于浏览器的问题,而AMD就是RequireJS在推广过程中对模块定义规范化的产出;而后面出现的sea.js,是由于有人觉得AMD规范是异步的,不够自然和垂直,所以创造了sea.js,大家可以像nodejs一样书写模块代码,随之而然形成了CMD规范
CommonJS是服务于服务端的,AMD和CMD是服务于客户端的,但是他们都有一个共同点,就是只有在代码运行后才能确定导出的内容,所以从es6开始,ES6 Module将会取代其他规范,成为两端通用的模块解决方案
ES6 Module 从ES6开始,在语言标准的层面,就实现了模块化功能,具体可以看阮一峰ES6 Module
ES6模块的设计思想是尽可能的静态化,是的编译的时候就能确定模块的依赖关系,输入和输出的变量
1 2 3 4 5 6 const {stat, exists, readFile} = require ('fs' )const fs = require ('fs' )const {stat, exists, readFile} = fs
实际CommonJS是去加载了整个模块,然后生成了一个对象,最后再从该对象上取到我们需要的方法,这种也叫做运行时加载,也就是在程序运行起来的时候才能得到这个对象,而ES6模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入
1 2 import {stat, exists, readFile} from 'fs'
export和import export:规定模块的对外接口
import:输入其他模块提供的功能
1 2 3 4 5 6 7 8 9 10 11 export var name = 'shiyuq' export var age = 20 var name = 'shiyuq' var age = 20 exports { name, age }
你还可以重命名
1 2 3 4 5 6 7 8 9 10 11 export function getName (name) { return 'hello' + name }function getAge () {}export { getAge as getAgeNew }
但是你需要特别注意,export命令规定必须与模块内部的变量建立一一对应的关系
1 2 3 4 5 export 1 var name = 'shiyuq' export name
上面实际导出的都是一个值,没有对应关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 export var name = 'shiyuq' var name = 'shiyuq' export {name}var name = 'shiyuq' export {name as nameNew}function f ( ) {}export fexport function f ( ) {}function f ( ) {}export {f}
需要注意的是,export可以出现在模块的任何位置,只要处于模块的顶层,如果处于块级作用域,将会报错
使用了export命令定义了模块的对外接口后,其他的模块可以通过import命令加载这个模块
1 2 3 4 5 import {name, age} from 'a.js' import {name as nameNew, age} from 'a.js'
要注意,import命令具有提升效果,会放到整个模块的顶部,首先执行
1 2 console .log (name)import {name} from 'a.js'
因为import命令是编译阶段执行的,在代码运行之前
由于import是静态执行,所以不可以使用表达式和变量(他们只能在代码运行的时候才有具体的结果)
我们还可以整体加载
1 2 import * as a from 'a.js' console .log (a.name )
从上面可以看出使用import的时候,用户需要知道所要加载的变量名或者函数名,否则无法加载,所以为了方便用户,我们可以使用export default命令
1 2 3 4 export default function ( ) { console .log ('shiyuq' ) }
上面是默认输出的一个匿名函数,我们可以在其他模块中这么使用
1 2 import a from 'a.js' a ()
所以我们常常会在源码中看到以下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import _ from 'lodash' import _, {each} from 'lodash' export default function (obj ) {}export function each (obj, iterator, context ) {}export {each as forEach}export default 18
模块加载的实质 CommonJS模块输出的是一个值的浅拷贝,而ES6模块输出的是值的引用,它在遇到import命令时,不去执行模块,而是生成一个动态的只读引用,等真的需要用到的时候再去模块中取值,所以ES6时动态引用,并不会缓存值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 let name = 'shiyuq' const exports = {name : name} console .log (exports .name ) exports .name = 'jenny' console .log (exports .name ) console .log (name) let module = {exports : {}}const moduleA = (function (module , exports , require ) { let name = 'shiyuq' const setName = (n ) => name = n const getName = ( ) => name module .exports = { name, setName, getName } return module .exports })(module , module .exports , {})console .log (moduleA) moduleA.getName () moduleA.name moduleA.setName ('jenny' ) moduleA.getName () moduleA.name
现在大家应该了解了为什么内部的变化不会影响到外部的值了,但是这个也仅仅只是针对的原始值,如果是引用值那就会跟着变化了
但是ES6的模块运行机制和CommonJS不一样,它在遇到import时,只是生成一个动态的只读引用,有点像linux里面的软链接,如果原始值变了,import输入的值也会跟着变化
1 2 3 4 5 6 7 8 9 10 11 export let counter = 3 export function incCounter ( ) { counter++ }import {counter, incCounter} from 'a.js' console .log (counter) incCounter ()console .log (counter)
可见ES6模块不会缓存运行结果,而是动态获取
ES6模块的循环加载 ES6模块是动态引用,所以那些变量并不会被缓存,而是成为一个指向被加载模块的引用,只要你作为开发者能确保它真的能取到值
1 2 3 4 5 6 7 8 9 10 11 import {bar} from 'b.js' console .log ('a.js' )console .log (bar)export const foo = 'foo' import {foo} from 'a.js' console .log ('b.js' )console .log (foo)export const bar = 'bar'
上面a模块中引用b,b模块中引用a,造成循环引用,现在执行node a.js查看运行结果
1 2 b .js ReferenceError: foo is not defined
这是因为在执行a模块的时候,其中引入了b模块,去b模块中加载,首先打印b.js,然后打印foo,但是此时a模块中并未输出foo接口,所以报错
大家也可以写一个 简单的demo,测试一下ES6打包后的代码,这样可以更加清晰的了解ES6和CommonJS的不同之处