[ JS 基础 ] JS 模块化

JS 模块化

还是梳理一下模块化吧,以便排错

JS 最初的设计是没有模块化的,仅仅是单文件裸奔。。当然之前有很多模块化的实现方法,不过目前比较流行的模块化规范是 Nodejs 中 CommonJS 的模块化( 2019 年)以及 ES6 的模块化( 2015 年)

CommonJs

模块导出

关键字:module.exports exports

moduleexports都是对象类型

最终暴露给其他模块的对象是module.exportsmodule.exports exports两者指向的内存地址是一样的(同一个引用),其实在模块加载的最开始会执行:let exports = module.exports

导出例子:

// 导出内容作为属性
module.exports.name = ‘js‘;
module.exports.foo = () => console.log(‘foo‘);
exports.abc = 123;

// 整体导出
module.exports = {name: ‘js‘, foo: () => {}, abc: 123};
// exports = {name: ‘js‘, foo: () => {}, abc: 123};  // 不可以

注意:整体导出只能使用module.exports,因为一旦是整体导出,是用一个对象去替换module.exports 的指向。如果用exports来整体导出,并没有改变module.exports的指向,细品上面的粗体。

模块导入

关键字:require

导入例子:require(‘./moduleA‘)

导入规则

相对路径的情况下,在导入的目录下寻找moduleA

  • 无后缀名的情况按照 JavaScript 解析
  • moduleA.json按照 json 解析
  • moduleA.node按照加载的编译插件模块dlopen

如果同一级目录下没有moduleA文件,则会去找同级的moduleA目录

  • 如果有moduleA/package.json会找其中main指向的入口文件,如果寻找失败, fallback 到寻找下面的文件
  • moduleA/index.js
  • moduleA/index.json
  • moduleA/index.node

绝对路径的情况下,同上

没有开头路径的模块,比如vue

  • 首先判断是否是核心内置模块(比如:fs path),是就直接导入
  • 如果不是,会从当前项目中最近的node_module下找按照上面有路径的情况找文件。
  • 如果没找到 继续向父目录的node_modules中找

模块实现

Node的模块实际上可以理解为代码被包裹在一个函数包装器里面

**require wrapper: **(从知乎上抄的)

function wrapper (script) {
    return ‘(function (exports, require, module, __filename, __dirname) {‘ + 
        script +
     ‘\n})‘
}

function require(id) {
 var cachedModule = Module._cache[id];
  if(cachedModule){
    return cachedModule.exports;
  }

  const module = { exports: {} }

  // 这里先将引用加入缓存
  Module._cache[id] = module

  //当然不是eval这么简单
  eval(wrapper(‘module.exports = "123"‘))(module.exports, require, module, ‘filename‘, ‘dirname‘)


  return module.exports
}

类似源码( TODO )的简单实现吧,主要关键是会缓存模块

  • 模块只执行一次之后调用获取的 module.exports 都是缓存,哪怕这个 js 还没执行完毕(因为先加入缓存后执行模块)
  • 模块导出就是return这个变量的其实跟a = b赋值一样, 基本类型导出的是引用类型导出的是引用地址
  • exportsmodule.exports 持有相同引用,因为最后导出的是 module.exports, 所以对exports进行赋值会导致exports操作的不再是module.exports的引用

注意这个缓存,在多次导入同一个模块的时候

  1. 会先判断是否已经缓存过这个模块(对象)
  2. 如果是没有缓存,将 module 先加入缓存(拷贝的方式:注意引用类型),执行这个 module (初始化),返回module.exports对象。
  3. 总之要注意多次导入的其实是同一个缓存中的对象。

可以写一下试一试

// moduleA.js
let abc = {x: ‘abc‘}
console.log(‘before: ‘, abc)
setTimeout(() => {
  abc.x = ‘bbbbbbbbxxxxx‘
  console.log(‘after 3s: ‘, abc)
}, 3000);
module.exports = abc

// index.js
const abc = require(‘./moduleA‘)
console.log(‘from index: abc‘, abc);
setTimeout(() => {
  console.log(‘after 3.5s : abc‘, abc);
}, 3500);
setTimeout(() => {
  const abcc = require(‘./moduleA‘)
  console.log(‘next require: abc‘, abcc);
}, 5000);

P.S. CommonJS 的模块化是动态的,运行时导入模块的对象(那一套缓存)

ES6 Module

参考阮一峰老师:https://es6.ruanyifeng.com/#docs/module

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。

核心:静态

使用 CommonJS 模块是运行时加载

const { resolve } = require(‘path‘);

// 等价于
const _path = require(‘path‘);
const resolve = _path.resolve;

ES6 编译时加载(或者静态加载)

import { resolve } from ‘path‘;

这样以来不仅效率高了,而且仅加载一个方法。 不过这也导致了没法引用 ES6 模块本身,因为它不是对象。

严格模式( ES5 )

ES6 的模块自动采用严格模式

严格模式主要有以下限制。

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval不会在它的外层作用域引入变量
  • evalarguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.callerfn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protectedstaticinterface

export

将某个变量给外部开放使用接口

几种写法

// 
export var a = 1;
export function foo() {};

// 打包导出
const c = ‘123‘;
function b() {};
export {
	c as cc,
  b,
}
// 只能这样写 不然语法会报错

export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。

export var foo = ‘bar‘;
setTimeout(() => foo = ‘baz‘, 500);

最后,export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。

import

import { resolve } from ‘path‘;

接收大括号,对应模块中导出的变量名

当然也可以用 as 重新取个名字

所有import进来的都是 read-only 的,也就是 const 的,但是如果是引用类型,你知道可以怎么做吧。

注意:

  • import是会提升的(因为在编译阶段执行)

  • 不要在import的时候使用动态的构造,比如import {‘f‘ + ‘oo‘} from ‘foo‘,这些都是runtime的东西。。

  • 只写import xxx,只会执行这个模块,多次写也只会执行一次

  • import { foo } from ‘my_module‘;
    import { bar } from ‘my_module‘;
    
    // 等同于
    import { foo, bar } from ‘my_module‘;
    // 对应的是同一个 my_module
    
  • import是单例模式

export default

为模块指定默认输出,用户不需要知道这个模块有什么东西,一股脑的导入即可,可以任意命名。

// import-default.js
import customName from ‘./export-default‘;
customName(); // ‘foo‘

export default之后的有名字的函数都“名亡实存”了

注意:

  • 只能出现一次
  • 可以和普通export并存

本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。

export const e = ‘123123‘

export {
    e as default
}
// 等价于
// export default e
import $ from ‘lodash‘;  // 鲁大师也能是 dollar

其实通过转码之后的export default会编译为exports.default = xxxx

export 与 import 的复合写法

如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。

export { foo, bar } from ‘my_module‘;
// 向外转发了这两个接口 实际并没有导入到当前模块

// 可以简单理解为
import { foo, bar } from ‘my_module‘;
export { foo, bar };

用到这样的场景?

// 接口改名
export { foo as myFoo } from ‘my_module‘;

// 整体输出
export * from ‘my_module‘;

其他的情况见阮一峰

import()

import是在编译时静态处理(提升到最前处理),那么我们想动态的导入模块怎么办呢?

import命令叫做“连接” binding 其实更合适

为了提升动态性,ES2020 提案引入import()函数

import(specifier).then(module => {
  // ...
}).catch(err => {
  // ...
})

返回一个 Promise 。

import()函数与所加载的模块没有静态连接关系,这点也是与import语句不相同。import()类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载。

适用场景

  • 按需加载(懒加载): vue-router 的懒加载就传入一个 import 函数即可
  • 条件加载
  • 动态路径加载

注意

  • 加载后这个模块会作为一个对象

  • 可以用结构的方式获取属性

  • default属性获取默认导出的变量

  • 也可以在async函数里面用!

  • 多个模块导入可以用Promise.all()

对比

CommonJS 的模块化是动态的,运行时导入(那一套缓存)

小结

感觉有些方面模块化这种东西大同小异,比如类比 Python , __init__.py 文件就相当于是 JS 中的 index.js 所以一个目录都可以是一个 module 或者叫 package ,但是我还是觉得 Python 中用 . 作为子模块的连接比较舒服(前提是所有路径都是模块)。

[ JS 基础 ] JS 模块化

上一篇:<%@ include file=""%>与区别


下一篇:.NET Core请求控制器Action方法正确匹配,但为何404?