1. Class 的基本语法
JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子。
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过 class 关键字,可以定义类。
基本上,ES6 的 class 可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
1. 定义类
//定义类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
ES6 的类,完全可以看作构造函数的另一种写法。
class Point {
// ...
}
typeof Point // "function"
Point === Point.prototype.constructor // true
构造函数的 prototype 属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的 prototype 属性上面。
class Point {
constructor() {
// ...
}
toString() {
// ...
}
toValue() {
// ...
}
}
// 等同于
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
};
类的内部所有定义的方法,都是不可枚举的(non-enumerable)。
class Point {
constructor(x, y) {
// ...
}
toString() {
// ...
}
}
Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
2 Class 表达式
与函数一样,类也可以使用表达式的形式定义。
// 类名为MyClass
// Me 只在 Class 内部有定义。
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
// 第二种。 如果没有用到直接可以省略
const MyClass = class { /* ... */ };
3. 不存在变量提升
new Foo(); // ReferenceError
class Foo {}
4. 私有方法
私有方法是常见需求,但 ES6 不提供,只能通过变通方法模拟实现。
1. 命名加判断;
2. 将私有方法移出到Class之外;
class Widget {
foo (baz) {
}
}
function bar(baz){
return this.snaf = baz;
}
3. 利用 Symbol 值的唯一性,将私有方法的名字命名为一个 Symbol 值。
4. 可以使用代理Proxy
5. #方法
Symbol实现私有方法:
const bar = Symbol('bar');
const snaf = Symbol('snaf');
export default class myClass{
// 公有方法
foo(baz) {
this[bar](baz);
}
// 私有方法
[bar](baz) {
return this[snaf] = baz;
}
// ...
};
5. 私有属性
与私有方法一样,ES6 不支持私有属性。目前,有一个提案,为 class 加了私有属性。方法是在属性名之前,使用 # 表示。
class Point {
#x;
constructor(x = 0) {
#x = +x; // 写成 this.#x 亦可
}
get x() { return #x }
set x(value) { #x = +value }
}
6. this 的指向
2. 模块
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 模块本身,因为它不是对象。
2.1 ES6 模块还有以下好处
1. 不再需要 UMD 模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
2. 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者 navigator 对象的属性。
3. 不再需要对象作为命名空间(比如 Math 对象),未来这些功能可以通过模块提供。
2.2 自动采用严格模式
严格模式主要有以下限制。
变量必须声明后再使用
函数的参数不能有同名属性,否则报错
不能使用 with 语句
不能对只读属性赋值,否则报错
不能使用前缀0表示八进制数,否则报错
不能删除不可删除的属性,否则报错
不能删除变量 delete prop ,会报错,只能删除属性 delete global[prop]
eval 不会在它的外层作用域引入变量
eval 和 arguments 不能被重新赋值
arguments 不会自动反映函数参数的变化
不能使用 arguments.callee
不能使用 arguments.caller
禁止 this 指向全局对象
不能使用 fn.caller 和 fn.arguments 获取函数调用的堆栈
增加了保留字(比如 protected 、 static 和 interface )
2.3 export 命令
模块功能主要由两个命令构成:** export 和 import **。 export 命令用于规定模块的对外接口, import 命令用于输入其他模块提供的功能。
两种写法
// 第一种 profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
// 第二种 profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};
// 导出函数 + 别名
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
注意事项:
export 命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
// 报错
export 1;
// 报错
var m = 1;
export m;
// ===== 正确的写法
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
// ====== 导出函数
// 报错
function f() {}
export f;
// 正确
export function f() {};
// 正确
function f() {}
export {f};
它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系。
export 语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
上面代码输出变量 foo ,值为 bar ,500毫秒之后变成 baz 。
这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新,详见下文《Module 的加载实现》一节。
2.4 import 命令
// 加载+别名
import { lastName as surname } from './profile';
注意, import 命令具有提升效果,会提升到整个模块的头部,首先执行。
// 报错
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
因为它们用到了表达式、变量和 if 结构。在静态分析阶段,这些语法都是没法得到值的。
2.5 export default 命令
加载某个模块,直接调用函数、属性可以采用 export default 命令。
// export-default.js
export default function () {
console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'
第二个例子
// 第一组
// 使用 export default 时,对应的 import 语句不需要使用大括号
export default function crc32() { // 输出
// ..
}
import crc32 from 'crc32'; // 输入
// 第二组
// 不使用 export default 时,对应的 import 语句需要使用大括号。
export function crc32() { // 输出
// ...
};
import {crc32} from 'crc32'; // 输入
export default 命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此 export default 命令只能使用一次。所以, import 命令后面才不用加大括号,因为只可能对应一个方法。
本质上,export default 就是输出一个叫做 default 的变量或方法,然后系统允许你为它取任意名字。
// modules.js
function add(x, y) {
return x * y;
}
export {add as default};
// 等同于
// export default add;
// app.js
import { default as xxx } from 'modules';
// 等同于
// import xxx from 'modules';
正是因为 export default 命令其实只是输出一个叫做 default 的变量,所以它后面不能跟变量声明语句。
// 正确
export var a = 1;
// 正确
var a = 1;
export default a;
// 错误
export default var a = 1;
export default 也可以用来输出类。
// MyClass.js
export default class { ... }
// main.js
import MyClass from 'MyClass';
let o = new MyClass();
2.6 export 与 import 的复合写法
如果在一个模块之中,先输入后输出同一个模块, import 语句可以与 export 语句写在一起。
export { foo, bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module';
export { foo, bar };
上面代码中, export 和 import 语句可以结合在一起,写成一行。
模块的接口改名和整体输出,也可以采用这种写法。
网友评论