Module的语法
历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的require、Python 的import,甚至就连 CSS 都有@import,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
// CommonJS模块
let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
// ES6模块
import { stat, exists, readFile } from 'fs';
上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为编译时加载或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
Module的加载实现
1、ES6 模块与 CommonJS 模块的差异
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
下面重点解释第一个差异。
CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个模块文件lib.js的例子。
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
// main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
面代码说明,lib.js模块加载以后,它的内部变化就影响不到输出的mod.counter了。这是因为mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。
ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
再比如:
// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);
// bar
// baz
注意export,import等都是ES6语法,在目前浏览器及终端里并不被直接支持,因此必须有babel等转码器,才可以运行。
本地利用babel编译es6至es5
- 初始化仓库
npm init
- 配置
.babelrc
只有配置了相关的预处理插件,babel才知道将高级语法转译到什么类型,若不配,则原样输出
{
"presets": [
// 预处理的版本,需要安装对应的插件
"env",
],
}
- 安装.babelrc配置的预处理版本及babel
npm i -D babel-cli babel-preset-env
- 将含有
m1.js,m2.js
的文件夹编译打包,配置package.json这里你或许会问,直接在项目里运行babel命令不就好了,为何还要写在这里?因为你安装的babel-cli只是项目内,并没有全局安装,因此为提示:command not found : bebel
{ "scripts": { "build": "babel src -d dist" }, }
- 终端运行编译后的文件
node m2.js
,即可看到先打印bar,500ms后打印baz
然后编译后生成的代码如下
// m1.js
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var foo = exports.foo = 'bar';
setTimeout(function () {
return exports.foo = foo = 'baz';
}, 500);
// m2.js
'use strict';
var _m = require('./m1.js');
console.log(_m.foo);
setTimeout(function () {
return console.log(_m.foo);
}, 500);
除了babel转码方式,还有以下两种:
- es6-module-transpiler转码器
- 使用 SystemJS(但后台调用的是google的Traceur转码器)
2、CommonJS 模块的加载原理
CommonJS 的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。
{
id: '...',
exports: { ... },
loaded: true,
...
}
上面代码就是 Node 内部加载模块后生成的一个对象。该对象的id属性是模块名,exports属性是模块输出的各个接口,loaded属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,这里都省略了。
以后需要用到这个模块的时候,就会到exports属性上面取值。即使再次执行require命令,也不会再次执行该模块,而是到缓存之中取值。也就是说,CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
3、ES6模块加载CommonJS模块
CommonJS 模块的输出都定义在module.exports这个属性上面。Node 的import命令加载 CommonJS 模块,Node 会自动将module.exports属性,当作模块的默认输出,即等同于export default xxx。如下一个CommonJS模块
// a.js
module.exports = {
foo: 'hello',
bar: 'world'
};
// 等同于
export default {
foo: 'hello',
bar: 'world'
};
export default function() {}
// 等效于:
function a() {};
export {a as default};
import a from './d';
// 等效于,或者说就是下面这种写法的简写,是同一个意思
import {default as a} from './d';
import命令加载上面的模块,module.exports会被视为默认输出,即import命令实际上输入的是这样一个对象{ default: module.exports }。
所以,一共有三种写法,可以拿到 CommonJS 模块的module.exports。
// 写法一
import baz from './a';
// baz = {foo: 'hello', bar: 'world'};
// 写法二
import {default as baz} from './a';
// baz = {foo: 'hello', bar: 'world'};
// 写法三
import * as baz from './a';
// baz = {
// get default() {return module.exports;},
// get foo() {return this.default.foo}.bind(baz),
// get bar() {return this.default.bar}.bind(baz)
// }
上面代码的第三种写法,可以通过baz.default拿到module.exports。foo属性和bar属性就是可以通过这种方法拿到了module.exports。
注意:由于 ES6 模块是编译时确定输出接口,CommonJS 模块是运行时确定输出接口,所以采用import命令加载 CommonJS 模块时,不允许采用下面的写法。
// 不正确
import { readFile } from 'fs';
上面的写法不正确,因为fs是CommonJS格式,只有在运行时才能确定readFile接口,而import命令要求编译时就确定这个接口。解决方法就是改为整体输入。
// 正确的写法一
import * as express from 'express';
const app = express.default();
// 正确的写法二
import express from 'express';
const app = express();
4、exports/require & import/export
require/exports 的用法只有以下三种简单的写法:
const fs = require('fs')
exports.fs = fs
module.exports = fs
而 import/export 的写法就多种多样:
import fs from 'fs'
import {default as fs} from 'fs'
import * as fs from 'fs'
// 以下几种错误
// import {readFile} from 'fs'
// import {readFile as read} from 'fs'
// import fs, {readFile} from 'fs'
export default fs
export const fs
export function readFile
export {readFile, read}
export * from 'fs'
5、webpack配置里的require与vue文件里的import
在vue文件里之所以可以使用import,因为webpack有相关的loader(如babel)处理这些CommonJs模块,但webpack本身却没有相关的loader来处理,因此仍然需要遵循CommonJs的引入规范,即require(‘’)
6、如何区分模块是es6模块还是CommonJs模块
exports/import & module.exports/require区别
- require: node 和 es6 都支持的引入
- export / import : 只有es6 支持的导出引入
- module.exports / exports: 只有 node 支持的导出
1、在node模块里
Node里面的模块系统遵循的是CommonJS规范。CommonJS规范就是对模块的定义:
CommonJS定义的模块分为: 模块标识(module)、模块定义(exports) 、模块引用(require)
在node执行一个文件时,会给这个文件内生成一个 exports和module对象,而module又有一个exports属性。他们之间的关系为exports -> 内存 <- module.exports
,都指向一块{}内存区域。
exports = module.exports = {}
//utils.js
let a = 100;
console.log(module.exports); // {}
console.log(exports); // {}
exports.a = 200; //此时 module.exports 的内容也为 {a : 200}
exports = '指向其他内存区'; //这里把exports的指向指走
//test.js
var a = require('/utils');
console.log(a) // 打印为 {a : 200}
从上面可以看出,其实require导出的内容是module.exports的指向的内存块内容,并不是exports的。 简而言之,区分他们之间的区别就是 exports 只是 module.exports的引用,辅助后者添加内容用的。 其实就是,exports只辅助module.exports操作内存中的数据,操作完数据,结果到最后真正被require出去的内容还是module.exports的。
注意下面情况下,因为module.exports指向了其他的对象,导致module.exports与exports断开了连接,因此再导出得到的module.exports并没有a属性,即为undefined
//utils.js
module.exports = {} // 虽然赋值空对象,但module.exports与exports已经断开连接
exports.name = 'error';
//test.js
var a = require('/utils');
console.log(a.name) // undefined
如下便可以:
//utils.js
exports = module.exports = {} //
exports.name = 'perfect';
//test.js
var a = require('/utils');
console.log(a.name) // 'perfect'
2、在ES模块里导入导出
- export与export default均可用于导出常量、函数、文件、模块等
- 在一个文件或模块中,export、import可以有多个,export default仅有一个
- 通过export方式导出,在导入时要加{ },export default则不需要
- export能直接导出变量表达式,export default不行。
// es6里默认就是严格模式
'use strict'
//导出变量
export const a = '100';
//导出方法
export const dogSay = function(){
console.log('wang wang');
}
//导出方法第二种
function catSay(){
console.log('miao miao');
}
export { catSay };
//export default导出
const m = 100;
export default m;
//export defult const m = 100;// 这里不能写这种格式。
动态import()
- 模块都是单例的,引入后只会被调用一次
- 动态导入在静态导入之前被调用
- 动态导入的脚本不按他们在代码中出现的顺序执行
// type="module"的脚本默认延迟加载(dom解析完毕后,按顺序加载)
<script type="module" src="static.js"></script>
<script src="dynamic.js"></script>
- 动态的import()提供一个基于promise的api
- import()遵循es模块规则:singleton,说明符,cors等
- import()可以在经典脚本和模块脚本中使用
-
在代码中使用import()顺序与他们被解析的顺序没有什么共同关系
- 可以使用动态导入进行延迟或条件加载以及依赖用户的操作
- 动态的import()可以在脚本的任何地方使用
- import()接受字符串文字,你可以根据你的需要构造说明符
动态导入使用promise 的api,因此可以并行加载多个脚本(promise.all),还可以坚持那个promise被首先resolved或reject(promise.race) 还可以用这个api检测哪个cdn的速度更快。
注意:动态导入的脚本不按他们在代码中出现的顺序执行,虽然静态导入保证您按顺序执行脚本。因为动态导入都是由它自己生成的,并且和其他的完成没有关系,同时也不会等待其他的完成。
注意:
- import()说明符传入的路径总是相对于调用它的文件,也就是说哪个文件使用import,就相对于该文件
- 可以在浏览器开发工具控制台使用动态导入(便于调试)
现在,import()几乎没有浏览器支持。 Node.js正在考虑添加这个功能,可能看起来像require.import()。 可以用以下代码,检测是否在特定浏览器或node.js中受支持
let dynamicImportSupported = false;
try{
Function('import("")');
dynamicImportSupported = true;
}catch(err){};
console.log(dynamicImportSupported);
关于polyfills,还可以用以下代码以提供类似import()的功能
function importModule(url) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
const tempGlobal = "__tempModuleLoadingVariable" +
Math.random().toString(32).substring(2);
script.type = "module";
script.textContent = `import * as m from "${url}"; window.${tempGlobal} = m;`;
script.onload = () => {
resolve(window[tempGlobal]);
delete window[tempGlobal];
script.remove();
};
script.onerror = () => {
reject(new Error("Failed to load module script with URL " + url));
delete window[tempGlobal];
script.remove();
};
document.documentElement.appendChild(script);
});
}
总结
动态import()为我们提供了异步方式使用es模块的额外功能,根据我们的需求动态或有条件地加载他们,这使我们能够更快,更好地创建更多优势应用程序。
本地ECMAScript模块与webpack模块的新特性和区别
- ECMAScript modules are static(es模块是完全静态的,你必须指定在编译时导入和导出什么,并且不能再运行时对更改做出反应) ```js // 考虑如下导入,导入的静态结构在语法上有两种强制方式 import * as someModule from ‘./dir/someModule.js’ // 首先,这个导入声明只能出现在模块的顶层。这样可以防止在if语句或事件处理程序中导入模块 // 其次,模块说明符./dir/someModule.js是固定的,不能在运行时计算它(比如函数调用)
const moduleSpecifier = ‘./dir/someModule.js’; import(moduleSpecifier) .then(someModule => someModule.foo()); // 首先,该参数是一个带有模块说明符的字符串,其格式与用于导入声明的模块说明符相同,但参数可以是其结果可以强制为字符串的任何表达式。 // 其次,另外函数调用的结果是一个promise,一旦模块完全加载,promise就完成了。 // 然后,尽管工作方式与函数类似,但import()是一个操作符,为了解析相对于当前模块的模块说明符,它需要知道从哪个模块调用它。正常函数没有直接的方法来发现这一点。
// 用例1 – 按需加载 button.addEventListener(‘click’, event => { import(‘./dialogBox.js’) .then(dialogBox => { dialogBox.open(); }) .catch(error => { /* Error handling */ }) });
// 用例2 – 条件加载 if (isLegacyPlatform()) { import(···) .then(···); }
// 用例3 – 计算模块说明符(也就是动态计算模块说明符)
import(messages_${getLocale()}.js
)
.then(···);
// 技巧1 – 解构赋值法导出 import(‘./myModule.js’) .then(({export1, export2}) => { ··· });
// 技巧2 – 访问默认导出 import(‘./myModule.js’) .then(myModule => { console.log(myModule.default); });
// 技巧3 – 并行动态加载多个 Promise.all([ import(‘./module1.js’), import(‘./module2.js’), import(‘./module3.js’), ]) .then(([module1, module2, module3]) => { ··· });
// 技巧4 – 组合async使用 async function main() { const myModule = await import(‘./myModule.js’); const {export1, export2} = await import(‘./myModule.js’); const [module1, module2, module3] = await Promise.all([ import(‘./module1.js’), import(‘./module2.js’), import(‘./module3.js’), ]); } main();
#### **宏**
与其它类Lisp语言不同,不支持宏是 JavaScript 与生俱来的一个问题,这是因为宏会在编译时操作语法树,而这在像 JavaScript 这样的语言中几乎是不可能的。LispyScript是一种采用 Lispy 语法、支持宏的 JavaScript 实现。
当我们说宏的时候我指的是可以定义一个小东西,它能被语法分析,并且转成代码。
对于宏,JavaScript 引擎在编译之前执行一个预处理步骤: 如果解析器生成的令牌流中的令牌序列与宏的模式部分匹配,那么它将被通过宏体生成的令牌替换。 只有当您能够静态地查找宏定义时,预处理步骤才会起作用。 因此,如果希望通过模块导入宏,那么它们必须具有静态结构。
#### **类及继承**
***1、在es5中***<br/>
```js
//构造函数People
function People (name,age){
this.name = name;
this.age = age
}
People.prototype.sayName = function(){
return '我的名字是:'+this.name;
}
//创建新的子类p1
let p1 = new People('harrisking',23);
2、在es6中
class People{
//构造方法constructor就等于上面的构造函数People
constructor(name,age){
this.name = name;
this.age = age;
}
sayName(){
return '我的名字是:'+this.name;
}
}
//创建新的子类p1
let p1 = new People('harrisking',23);
// 如果要想实现继承,则如下
class Sun extends People{
constructor(name,age,sex){
super(name,age);//调用父类的constructor(name,age)
this.sex = sex;
}
haha(){
return this.sex + ' ' + super.sayName();//调用父类的sayName()
}
}
let littleSun = new Sun(1,2,3)
littleSun.haha()
3、new运算符
var o = new Foo();
// 等价于
var o = new Object(); //1、新建空对象
o.__proto__ = Foo.prototype; //2、建立连接
let returnVal = Foo.call(o) //3、执行
if(typeof returnVal === 'object'){//4、判断返回值
return returnVal;
} else {
return o
}
// Object.create(proto[, propertiesObject])
// 创建一个新对象,使用提供的对象来提供给新对象的__proto__
// 其实就是 新对象.__proto__ = proto
// 写一个函数实现 new运算符
function _new(fn, ...arg) {
const obj = Object.create(fn.prototype);
const ret = fn.apply(obj, arg);
return ret instanceof Object ? ret : obj;
}
4、数据类型
在 javascript 的最初版本中,使用的 32 位系统,为了性能考虑使用低位存储了变量的类型信息:
- 000:对象
- 1:整数
- 010:浮点数
- 100:字符串
- 110:布尔 有 2 个值比较特殊:
- undefined:用 -2^{30} (−2^30)表示。
- null:对应机器码的 NULL 指针,一般是全零。 ```js function testDataType(item){ return Object.prototype.toString.call(item).slice(8,-1) }
testDataType(null) // “Null” typeof null // “object”
testDataType([]) // “Array” typeof [] // “object”
`Object.prototype.toString.call(obj)` 和 `({}).toString.call(obj)` 的区别是什么?哪个好?
({})可以节省字节,但会在浏览器回收垃圾前保有这么个新建的对象,后者先创建一个Object对象,然后调用Object原型上的toString方法,而前者是直接利用Object原型上方法,不需创建
另外之所以用括号,是因为不用括号相当于一个代码块,而用括号相当于一个对象
另外还需要注意:涉及到多帧 DOM 环境中的脚本编写时,问题就出现了。 简而言之,在一个 iframe 中创建的 Array 对象不会与在另一个 iframe 中创建的数组共享[[[ Prototype ]]]。 它们的构造函数是不同的对象,所以 instanceof 和 constructor 检查都会失败,如下:
```js
var myFrame = document.createElement('iframe');
document.body.appendChild(myFrame);
var myArray = window.frames[window.frames.length-1].Array;
var arr = new myArray(a,b,10); // [a,b,10]
// instanceof will not work correctly, myArray loses his constructor
// constructor is not shared between frames
arr instanceof Array; // false
arr instanceof myArray; // true
修饰器
1、类的修饰
许多面向对象的语言都有修饰器(Decorator)函数,用来修饰类的行为。如下:
@testable
class MyTestableClass {
// ...
}
function testable(target) {
target.isTestable = true;
}
MyTestableClass.isTestable // true
上面代码中,@testable就是一个修饰器,它修改了MyTestableClass这个类的行为,为它加上了静态属性isTestable,testable函数的参数target是MyTestableClass类本身。
基本上,修饰器的行为就是下面这样:
@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A;
也就是说,修饰器是一个对类进行处理的函数,修饰器函数的第一个参数,就是所要修饰的目标类。