ES6-新内容(四)class、Decorator、Module

一、Class

1.与传统构造方法对比

传统构造方法:

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 Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}
var p = new Point(1, 2);

constructor方法,这就是构造方法,而this关键字则代表实例对象。使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。

构造函数的prototype属性,在ES6的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。

constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。

2.Class表达式

示例:

const MyClass = class Me {
  getClassName() {
    return Me.name;
  }
};

需要注意的是,这个类的名字是MyClass而不是Me,Me只在Class的内部代码可用,指代当前类。

如果类的内部没用到的话,可以省略Me,也就是可以写成下面的形式:

const MyClass = class { /* ... */ };

立即执行的Class:

let person = new class {
  constructor(name) {
    this.name = name;
  }

  sayName() {
    console.log(this.name);
  }
}('张三');

person.sayName(); // "张三"

3.私有方法

私有方法是常见需求,但ES6不提供,只能通过变通方法模拟实现。

  1. 一种做法是在命名上加以区别:在私有方法名前加下划线,表示是私有方法。
  2. 另一种方法就是索性将私有方法移出模块,因为模块内部的所有方法都是对外可见的。以下示例中,bar()方法就是一个私有方法。
class Widget {
  foo (baz) {
    bar.call(this, baz);
  }
}
function bar(baz) {
  return this.snaf = baz;
}
  1. 还有一种方法是利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。
const bar = Symbol('bar');
const snaf = Symbol('snaf');

export default class myClass{
  foo(baz) {// 公有方法
    this[bar](baz);
  }
  [bar](baz) {// 私有方法
    return this[snaf] = baz;
  }
};

4.this的指向

类的方法内部如果含有this,它默认指向类的实例。
但是包含this的方法不能直接使用,否则会报错,解决方法如下:

  1. 在构造方法中绑定this。
class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }
  printName(name = 'there') {
    this.print(`Hello ${name}`);
  }
  print(text) {
    console.log(text);
  }
}
  1. 使用箭头函数。
class Logger {
  constructor() {
    this.printName = (name = 'there') => {
      this.print(`Hello ${name}`);
    };
  }
  printName(name = 'there') {
    this.print(`Hello ${name}`);
  }
  print(text) {
    console.log(text);
  }
}
  1. 使用Proxy,获取方法的时候,自动绑定this。

5.严格模式

类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。

6.name属性

name属性总是返回紧跟在class关键字后面的类名。

7.class的继承

1)extends关键字

示例:

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

ColorPoint的父类是Point类。
extends关键字后面可以跟多种类型的值。

class B extends A {}

上面代码的A,只要是一个有prototype属性的函数,就能被B继承。由于函数都有prototype属性(除了Function.prototype函数),因此A可以是任意函数。
三种特殊情况:

  1. 子类继承Object类
  2. 不存在任何继承
  3. 子类继承null
2)super关键字

super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。

ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。

在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。

super这个关键字,既可以当作函数使用,也可以当作对象使用:

  1. 作为函数调用时,代表父类的构造函数。
  2. super作为对象时,指向父类的原型对象。
3)类的prototype属性和__proto__属性

Class作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链:
(1) 子类的__proto__属性,表示构造函数的继承,总是指向父类。
(2) 子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

实例的__proto__属性:子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类的原型的原型,是父类的原型。

4)Object.getPrototypeOf()

可以用来从子类上获取父类。

Object.getPrototypeOf(ColorPoint) === Point
// true
5)原生构造函数的继承

原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript的原生构造函数大致有下面这些:

Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()

以前,这些原生构造函数是无法继承的,因为子类无法获得原生构造函数的内部属性。

ES6允许继承原生构造函数定义子类,因为ES6是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。

下面是一个继承Array的例子:

class MyArray extends Array {
  constructor(...args) {
    super(...args);
  }
}

var arr = new MyArray();
arr[0] = 12;
arr.length // 1

arr.length = 0;
arr[0] // undefined

注意,继承Object的子类,有一个行为差异,NewObj继承了Object,但是无法通过super方法向父类Object传参。这是因为ES6改变了Object构造函数的行为,一旦发现Object方法不是通过new Object()这种形式调用,ES6规定Object构造函数会忽略参数。

8.Class的取值函数(getter)和存值函数(setter)

与ES5一样,在Class内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
示例:

class MyClass {
  constructor() {}
  get prop() {
    return 'getter';
  }
  set prop(value) {
    console.log('setter: '+value);
  }

存值函数和取值函数是设置在属性的descriptor对象上的。

9.Class的静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

父类的静态方法,可以被子类继承。

静态方法也是可以从super对象上调用的。

10.Class的静态属性和实例属性

静态属性指的是Class本身的属性,即Class.propname,而不是定义在实例对象(this)上的属性。
写法:

class Foo {}

Foo.prop = 1;
Foo.prop // 1

上面的写法为Foo类定义了一个静态属性prop。目前,只有这种写法可行,因为ES6明确规定,Class内部只有静态方法,没有静态属性。

ES7写法:
ES7有一个静态属性的提案,目前Babel转码器支持。这个提案对实例属性和静态属性,都规定了新的写法:

  1. 类的实例属性:类的实例属性可以用等式,写入类的定义之中。
class MyClass {
  myProp = 42;

  constructor() {
    console.log(this.myProp); // 42
  }
}

为了可读性的目的,对于那些在constructor里面已经定义的实例属性,新写法允许直接列出。

class ReactCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
  state;
}
  1. 类的静态属性:类的静态属性只要在上面的实例属性写法前面,加上static关键字就可以了。
class MyClass {
  static myStaticProp = 42;

  constructor() {
    console.log(MyClass.myProp); // 42
  }
}

11.new.target属性

new是从构造函数生成实例的命令。ES6为new命令引入了一个new.target属性,(在构造函数中)返回new命令作用于的那个构造函数。如果构造函数不是通过new命令调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。

Class内部调用new.target,返回当前Class。子类继承父类时,new.target会返回子类。

12.Mixin模式的实现

Mixin模式指的是,将多个类的接口“混入”(mix in)另一个类。

二、ES6 修饰器

1.类的修饰

修饰器(Decorator)是一个函数,用来修改类的行为。这是ES7的一个提案,目前Babel转码器已经支持。
修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。

基本上,修饰器的行为就是下面这样:

@decorator
class A {}

// 等同于
class A {}
A = decorator(A) || A;

修饰器本质就是编译时执行的函数。
修饰器函数的第一个参数,就是所要修饰的目标类。

2.方法的修饰

示例:

function readonly(target, name, descriptor){
  // descriptor对象原来的值如下
  // {
  //   value: specifiedFunction,
  //   enumerable: false,
  //   configurable: true,
  //   writable: true
  // };
  descriptor.writable = false;
  return descriptor;
}

class Person {
  @readonly
  name() { return `${this.first} ${this.last}` }
}

readonly(Person.prototype, 'name', descriptor);
// 类似于
Object.defineProperty(Person.prototype, 'name', descriptor);

此时,修饰器函数一共可以接受三个参数,第一个参数是所要修饰的目标对象,第二个参数是所要修饰的属性名,第三个参数是该属性的描述对象。

修饰器有注释的作用。从下面代码中,我们一眼就能看出,Person类是可测试的,而name方法是只读和不可枚举的:

@testable
class Person {
  @readonly
  @nonenumerable
  name() { return `${this.first} ${this.last}` }
}

如果同一个方法有多个修饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行。

除了注释,修饰器还能用来类型检查。

3.为什么修饰器不能用于函数?

修饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。

4.core-decorators.js

core-decorators.js是一个第三方模块,提供了几个常见的修饰器,通过它可以更好地理解修饰器。

(1)@autobind

autobind修饰器使得方法中的this对象,绑定原始对象。

(2)@readonly

readonly修饰器使得属性或方法不可写。

(3)@override

override修饰器检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错。

(4)@suppressWarnings

suppressWarnings修饰器抑制decorated修饰器导致的console.warn()调用。但是,异步代码发出的调用除外。

4.使用修饰器实现自动发布事件

我们可以使用修饰器,使得对象的方法被调用时,自动发出一个事件。

可使用Postal.js事件“发布/订阅”库。

5.Mixin

在修饰器的基础上,可以实现Mixin模式。所谓Mixin模式,就是对象继承的一种替代方案,中文译为“混入”(mix in),意为在一个对象之中混入另外一个对象的方法。

下面,我们部署一个通用脚本mixins.js,将mixin写成一个修饰器:

export function mixins(...list) {
  return function (target) {
    Object.assign(target.prototype, ...list);
  };
}

然后,就可以使用上面这个修饰器,为类“混入”各种方法。

import { mixins } from './mixins';

const Foo = {
  foo() { console.log('foo') }
};

@mixins(Foo)
class MyClass {}

let obj = new MyClass();
obj.foo() // "foo"

6.Trait

Trait也是一种修饰器,效果与Mixin类似,但是提供更多功能,比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等等。

traits-decorator第三方模块,这个模块提供的traits修饰器,不仅可以接受对象,还可以接受ES6类作为参数。

Trait不允许“混入”同名方法。

7.Babel转码器的支持

目前,Babel转码器已经支持Decorator。
首先,安装babel-core和babel-preset-stage-0。
然后,设置配置文件.babelrc。

{
  "plugins": ["transform-decorators"]
}

这时,Babel就可以对Decorator转码了。

三、Module

1.简介

在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代现有的 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

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

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

2.优点

  • 由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
  • 不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
  • 将来浏览器的新 API 就能用模块格式提供,不再必要做成全局变量或者navigator对象的属性。
  • 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。

3.严格模式

ES6 的模块自动采用严格模式。
严格模式主要有以下限制:

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

4.export和import

1)export 命令

模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
正确的写法:

// 写法一
export var m = 1;

// 写法二
var m = 1;
export {m};

// 写法三
var n = 1;
export {n as m};
2)import 命令

使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。

import命令具有提升效果,会提升到整个模块的头部,首先执行。本质是,import命令是编译阶段执行的,在代码运行之前。

如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。

3)export default 命令

为模块指定默认输出。

其他模块加载该模块时,import命令可以为该匿名函数指定任意名字,这时import命令后面,不使用大括号。

一个模块只能有一个默认输出,因此export default命令只能使用一次。

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

4)export 与 import 的复合写法

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

export { foo, bar } from 'my_module';

// 等同于
import { foo, bar } from 'my_module';
export { foo, boo};
5)模块的整体加载
import * as circle from './circle';

console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

5.模块的继承

例如,有一个circleplus模块,继承了circle模块:

// circleplus.js

export * from 'circle';//继承
export var e = 2.71828182846;
export default function(x) {
  return Math.exp(x);
}

6.ES6模块加载的实质

ES6模块输出的是值的引用。

ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import时,不会去执行模块,而是只生成一个动态的只读引用。等到真的需要用到时,再到模块里面去取值,换句话说,ES6的输入有点像Unix系统的“符号连接”,原始值变了,import输入的值也会跟着变。因此,ES6模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

7.浏览器的模块加载

浏览器使用 ES6 模块的语法如下。

<script type="module" src="foo.js"></script>

浏览器对于带有type="module"的

对于外部的模块脚本(上例是foo.js),有几点需要注意。

  • 该脚本自动采用严格模块。
  • 该脚本内部的顶层变量,都只在该脚本内部有效,外部不可见。
  • 该脚本内部的顶层的this关键字,返回undefined,而不是指向window。

8.循环加载

“循环加载”(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。

通常,“循环加载”表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。

但是实际上,这是很难避免的,尤其是依赖关系复杂的大项目,很容易出现a依赖b,b依赖c,c又依赖a这样的情况。这意味着,模块加载机制必须考虑“循环加载”的情况。

对于JavaScript语言来说,目前最常见的两种模块格式CommonJS和ES6,处理“循环加载”的方法是不一样的,返回的结果也不一样。

1)CommonJS模块的加载原理

CommonJS的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。

CommonJS模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。

2)CommonJS模块的循环加载

CommonJS模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。

CommonJS输入的是被输出值的拷贝,不是引用。

另外,由于CommonJS模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。所以,输入变量的时候,必须非常小心。

3)ES6模块的循环加载

ES6模块是动态引用,如果使用import从一个模块加载变量(即import foo from ‘foo’),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

9.import()动态加载

动态加载:require到底加载哪一个模块,只有运行时才知道。import语句做不到这一点。

因此,有一个提案,建议引入import()函数,完成动态加载。

import(specifier)

上面代码中,import函数的参数specifier,指定所要加载的模块的位置。import语句能够接受什么参数,import()函数就能接受什么参数,两者区别主要是后者为动态加载。

import()返回一个 Promise 对象。

import()类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载。

10.ES6模块的转码

除了Babel可以用来转码之外,还有以下两个方法,也可以用来转码。

  • ES6 module transpiler
  • SystemJS
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值