gistfile1.txt /** * Created by Administrator on 2017/11/13. */class Point{ constructor(x,y){ this.x = x; this.y = y; }; toString(){ return '('+this.x+','+this.y+')'; }}// 生成类的实例对象的写法,与 ES5 完全一样,也是使用
/** * Created by Administrator on 2017/11/13. */ class Point{ constructor(x,y){ this.x = x; this.y = y; }; toString(){ return '('+this.x+','+this.y+')'; } } // 生成类的实例对象的写法,与 ES5 完全一样,也是使用new命令。前面说过,如果忘记加上new,像函数那样调用Class,将会报错。 // 与 ES5 一样,类的所有实例共享一个原型对象。 var p1 = new Point(2,3); var p2 = new Point(3,2); console.log(p1.__proto__ == p2.__proto__) //true //true // __proto__ 并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用 Object.getPrototypeOf 方法来获取实例对象的原型,然后再来为原型添加方法/属性。 // 与函数一样,类也可以使用表达式的形式定义。 const MyClass = class Me{ getClassName(){ return Me.name //上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是MyClass而不是Me,Me只在 Class 的内部代码可用,指代当前类。 } } // 采用 Class 表达式,可以写出立即执行的 Class。 let person = new class { constructor(name) { this.name = name; } sayName() { console.log(this.name); } }('张三'); person.sayName(); // "张三" // Class 的取值函数(getter)和存值函数(setter) class MyClass2{ constructor(){} get prop(){ console.log('getter') } set prop(value){ console.log(value) } } let inst = new MyClass2() // inst.prop(123) //error inst.prop = 123 //这样传参数的 inst.prop //getter // 注意,如果静态方法包含this关键字,这个this指的是类,而不是实例。 class Foo { static classMethod() { return 'hello'; } } ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";。 严格模式主要有以下限制。 变量必须声明后再使用 函数的参数不能有同名属性,否则报错 不能使用with语句 不能对只读属性赋值,否则报错 不能使用前缀 0 表示八进制数,否则报错 不能删除不可删除的属性,否则报错 不能删除变量delete prop,会报错,只能删除属性delete global[prop] eval不会在它的外层作用域引入变量 eval和arguments不能被重新赋值 arguments不会自动反映函数参数的变化 不能使用arguments.callee 不能使用arguments.caller 禁止this指向全局对象 不能使用fn.caller和fn.arguments获取函数调用的堆栈 增加了保留字(比如protected、static和interface) 上面这些限制,模块都必须遵守。由于严格模式是 ES5 引入的,不属于 ES6,所以请参阅相关 ES5 书籍,本书不再详细介绍了。 其中,尤其需要注意this的限制。ES6 模块之中,顶层的this指向undefined,即不应该在顶层代码使用this。 如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞,用户会感觉到浏览器“卡死”了,没有任何响应。这显然是很不好的体验,所以浏览器允许脚本异步加载,下面就是两种异步加载的语法。 上面代码中, 上面代码在网页中插入一个模块foo.js,由于type属性设为module,所以浏览器知道这是一个 ES6 模块。 浏览器对于带有type="module"的 如果网页有多个 一旦使用了async属性, 对于外部的模块脚本(上例是foo.js),有几点需要注意。 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。 模块脚本自动采用严格模式,不管有没有声明use strict。 模块之中,可以使用import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口。 模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的。 同一个模块如果加载多次,将只执行一次。 ES6 模块与 CommonJS 模块的差异 § ⇧ 讨论 Node 加载 ES6 模块之前,必须了解 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, }; 上面代码输出内部变量counter和改写这个变量的内部方法incCounter。然后,在main.js里面加载这个模块。 // main.js var mod = require('./lib'); console.log(mod.counter); // 3 mod.incCounter(); console.log(mod.counter); // 3 上面代码说明,lib.js模块加载以后,它的内部变化就影响不到输出的mod.counter了。这是因为mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。 // lib.js var counter = 3; function incCounter() { counter++; } module.exports = { get counter() { return counter }, incCounter: incCounter, }; 上面代码中,输出的counter属性实际上是一个取值器函数。现在再执行main.js,就可以正确读取内部变量counter的变动了。 $ node main.js 3 4 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 上面代码说明,ES6 模块输入的变量counter是活的,完全反应其所在模块lib.js内部的变化。 再举一个出现在export一节中的例子。 // 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); 上面代码中,m1.js的变量foo,在刚加载时等于bar,过了 500 毫秒,又变为等于baz。 让我们看看,m2.js能否正确读取这个变化。 $ babel-node m2.js bar baz 上面代码表明,ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。 由于 ES6 输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错。 // lib.js export let obj = {}; // main.js import { obj } from './lib'; obj.prop = 123; // OK obj = {}; // TypeError 上面代码中,main.js从lib.js输入变量obj,可以对obj添加属性,但是重新赋值就会报错。因为变量obj指向的地址是只读的,不能重新赋值,这就好比main.js创造了一个名为obj的const变量。 最后,export通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例。 // mod.js function C() { this.sum = 0; this.add = function () { this.sum += 1; }; this.show = function () { console.log(this.sum); }; } export let c = new C(); 上面的脚本mod.js,输出的是一个C的实例。不同的脚本加载这个模块,得到的都是同一个实例。 // x.js import {c} from './mod'; c.add(); // y.js import {c} from './mod'; c.show(); // main.js import './x'; import './y'; 现在执行main.js,输出的是1。 $ babel-node main.js 1 这就证明了x.js和y.js加载的都是C的同一个实例。、 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' }; 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。 下面是一些例子。 // b.js module.exports = null; // es.js import foo from './b'; // foo = null; import * as bar from './b'; // bar = { default:null }; 上面代码中,es.js采用第二种写法时,要通过bar.default这样的写法,才能拿到module.exports。 // c.js module.exports = function two() { return 2; }; // es.js import foo from './c'; foo(); // 2 import * as bar from './c'; bar.default(); // 2 bar(); // throws, bar is not a function 上面代码中,bar本身是一个对象,不能当作函数调用,只能通过bar.default调用。 CommonJS 模块的输出缓存机制,在 ES6 加载方式下依然有效。 // foo.js module.exports = 123; setTimeout(_ => module.exports = null); 上面代码中,对于加载foo.js的脚本,module.exports将一直是123,而不会变成null。 由于 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(); CommonJS 模块加载 ES6 模块 CommonJS 模块加载 ES6 模块,不能使用require命令,而要使用import()函数。ES6 模块的所有输出接口,会成为输入对象的属性。 // es.mjs let foo = { bar: 'my-default' }; export default foo; foo = null; // cjs.js const es_namespace = await import('./es'); // es_namespace = { // get default() { // ... // } // } console.log(es_namespace.default); // { bar:'my-default' } 上面代码中,default接口变成了es_namespace.default属性。另外,由于存在缓存机制,es.js对foo的重新赋值没有在模块外部反映出来。 下面是另一个例子。 // es.js export let foo = { bar:'my-default' }; export { foo as bar }; export function f() {}; export class c {}; // cjs.js const es_namespace = await import('./es'); // es_namespace = { // get foo() {return foo;} // get bar() {return foo;} // get f() {return f;} // get c() {return c;} // }