Es6的那些高级特性

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

  1. 初始化仓库 npm init
  2. 配置.babelrc

只有配置了相关的预处理插件,babel才知道将高级语法转译到什么类型,若不配,则原样输出

{
	"presets": [
		// 预处理的版本,需要安装对应的插件
		"env",
	],
}
  1. 安装.babelrc配置的预处理版本及babel
npm i -D babel-cli babel-preset-env
  1. 将含有m1.js,m2.js的文件夹编译打包,配置package.json

    这里你或许会问,直接在项目里运行babel命令不就好了,为何还要写在这里?因为你安装的babel-cli只是项目内,并没有全局安装,因此为提示:command not found : bebel

    {
     "scripts": {
         "build": "babel src -d dist"
     },
    }
    
  2. 终端运行编译后的文件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区别

参考:exports与export的区别

  • 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模块里导入导出

  1. export与export default均可用于导出常量、函数、文件、模块等
  2. 在一个文件或模块中,export、import可以有多个,export default仅有一个
  3. 通过export方式导出,在导入时要加{ },export default则不需要
  4. export能直接导出变量表达式,export default不行。

import与export异同

// 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()

  1. 模块都是单例的,引入后只会被调用一次
  2. 动态导入在静态导入之前被调用
  3. 动态导入的脚本不按他们在代码中出现的顺序执行
// type="module"的脚本默认延迟加载(dom解析完毕后,按顺序加载)
<script type="module" src="static.js"></script>
<script src="dynamic.js"></script>
  1. 动态的import()提供一个基于promise的api
  2. import()遵循es模块规则:singleton,说明符,cors等
  3. import()可以在经典脚本和模块脚本中使用
  4. 在代码中使用import()顺序与他们被解析的顺序没有什么共同关系

  5. 可以使用动态导入进行延迟或条件加载以及依赖用户的操作
  6. 动态的import()可以在脚本的任何地方使用
  7. import()接受字符串文字,你可以根据你的需要构造说明符

动态导入使用promise 的api,因此可以并行加载多个脚本(promise.all),还可以坚持那个promise被首先resolved或reject(promise.race) 还可以用这个api检测哪个cdn的速度更快。

注意:动态导入的脚本不按他们在代码中出现的顺序执行,虽然静态导入保证您按顺序执行脚本。因为动态导入都是由它自己生成的,并且和其他的完成没有关系,同时也不会等待其他的完成。

注意:

  1. import()说明符传入的路径总是相对于调用它的文件,也就是说哪个文件使用import,就相对于该文件
  2. 可以在浏览器开发工具控制台使用动态导入(便于调试)

现在,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模块的新特性和区别

  1. 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;

也就是说,修饰器是一个对类进行处理的函数,修饰器函数的第一个参数,就是所要修饰的目标类。

最近的文章

CSS世界笔记

参考资料CSS世界、盒尺寸四大家族深入理解contentcontent与替换元素根据外在盒子是内联还是块级,我们把元素分为内联与块级元素,而根据是否具有可替换内容,我们可将元素分为替换元素和非替换元素。替换元素,顾名思义,内容可以被替换,如:<img src="xx1.png">我们可以把上面的xx1.png替换为xx2.png,图片就替换了?这种通过修改某个属性值呈现的内容就可以被替换的元素称为替换元素,因此img、object、video、iframe、textarea、i...…

继续阅读
更早的文章

框架思想

框架框架分好多种,比如说 ui 框架负责渲染 ui 层面,而像react,vue是数据到视图的映射,而angular不但有数据到视图的映射,还有自己的路由等。。。每种框架做的东西不同,但各有特点,需要根据业务需要来选择。像我们常说的react和vue,他们核心虽然只解决一个很小的问题,但他们有各自的生态圈及配套的可选工具,当把这些工具一一加进来的时候,就可以组合成非常强大的栈,就可以涵盖其他的那些更完整的框架所涵盖的问题。MVVM 由来在html5还没火起来的时候,MVC作为web应用的最...…

继续阅读