Skip to content

Latest commit

 

History

History
554 lines (442 loc) · 12.5 KB

9.class.md

File metadata and controls

554 lines (442 loc) · 12.5 KB

9.class

背景

ES 里没有像其他语言一样的 Class 和继承, 在 ES5 里开发者通常需要利用构造函数和原型模拟出这一特性, 但比较繁琐。而ES6 通过 classextends 关键字简化了这一操作,可以快速便捷地创建出类。虽然它是个语法糖,但大大提高了开发的体验和效率。

class 声明

ES5, 这样声明类:

function PersonType(name) {
  this.name = name;
}
PersonType.prototype.sayName = function () {
  console.log(this.name);
};
var person = new PersonType("Nicholas");
person.sayName();   // outputs "Nicholas"
console.log(person instanceof PersonType);  // true
console.log(person instanceof Object);      // true

ES6 可以这样:

class PersonClass {
  // equivalent of the PersonType constructor
  constructor(name) {
    this.name = name;
  }
  // equivalent of PersonType.prototype.sayName
  sayName() {
    console.log(this.name);
  }
}
let person = new PersonClass("Nicholas");
person.sayName();   // outputs "Nicholas"
console.log(person instanceof PersonClass); // true
console.log(person instanceof Object);      // true
console.log(typeof PersonClass);            // "function"
console.log(typeof PersonClass.prototype.sayName);  // "function"

用 关键字 class 声明一个类,里面的语法类似对象字面量,但是各属性之间不需要逗号,,方法声明也不用funtion(用了反而会报错)。看起来简洁多了。

另外,类的实例属性,只能诞生在 constructor 或其他方法里。

还有一点,类的原型,如上面的 PersonClass.prototype 是只读的,不能像 ES5 那样被修改。

class 声明的特点

  1. class 声明不存在变量提升
  2. class 声明里的代码默认是严格模式
  3. class 的方法都是不可枚举的,不能被 new 操作
  4. 直接调用而不是 new class 会报错
  5. class 方法里修改 class 名字会报错

ES5 来描述这些特定就是:

// direct equivalent of PersonClass
let PersonType2 = (function () {
  "use strict";
  const PersonType2 = function (name) {
    // make sure the function was called with new
    if (typeof new.target === "undefined") {
      throw new Error("Constructor must be called with new.");
    }
    this.name = name;
  }
  Object.defineProperty(PersonType2.prototype, "sayName", {
    value: function () {
      // make sure the method wasn't called with new
      if (typeof new.target !== "undefined") {
        throw new Error("Method cannot be called with new.");
      }
      console.log(this.name);
    },
    enumerable: false,
    writable: true,
    configurable: true
  });
  return PersonType2;
}());

class 表达式

函数有函数声明和函数表达式两种创建方式,class 也一样,除了上面的 class 声明,也有 class 表达式。

需要注意的是用表达式声明类,类名的变化。

这个比较简单,这里就不浪费大家的时间了。

作为一等公民的 class

编程语言里的一等公民,指的是一个值既能当参数传给函数,也能作为函数的返回值,并且可以赋值给其他变量。ES里函数是一等公民,而 class 也是。

存取器属性

一般存储器属性是针对某个属性而设置的, 而 class 也可以直接设置存储器属性:

class CustomHTMLElement {
  constructor(element) {
    this.element = element;
  }
  get html() {
    return this.element.innerHTML;
  }
  set html(value) {
    this.element.innerHTML = value;
  }
}


var descriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype, "html");
console.log("get" in descriptor); // true
console.log("set" in descriptor); // true
console.log(descriptor.enumerable); // false

计算属性名

和对象一样,类也是支持计算属性的:

let methodName = "sayName";
class PersonClass {
  constructor(name) {
    this.name = name;
  }
  [methodName]() {
    console.log(this.name);
  }
};
let me = new PersonClass("Nicholas");
me.sayName();           // "Nicholas"

这点,对存取器属性也不例外:

let propertyName = "html";
class CustomHTMLElement {
  constructor(element) {
    this.element = element;
  }
  get [propertyName]() {
    return this.element.innerHTML;
  }
  set [propertyName](value) {
    this.element.innerHTML = value;
  }
}

generator 方法

generator 章节对象的 generator 方法类似,class 也可以设置:

class MyClass {
  *createIterator() {
    yield 1;
    yield 2;
    yield 3;
  }
}
let instance = new MyClass();
let iterator = instance.createIterator();

既然有 iterator, 那最好个默认的,方便在 for-of 之类的场景下使用:

class Collection {
  constructor() {
    this.items = [];
  }
  *[Symbol.iterator]() {
    yield* this.items;
  }
}

var collection = new Collection();
collection.items.push(1);
collection.items.push(2);
collection.items.push(3);

for (let x of collection) {
  console.log(x);
}
// Output:
// 1
// 2
// 3

静态成员

ES5 可以直接在构造函数上添加属性来创建静态成员:

function PersonType(name) {
  this.name = name;
}
// static method
PersonType.create = function (name) {
  return new PersonType(name);
};
// instance method
PersonType.prototype.sayName = function () {
  console.log(this.name);
};

var person = PersonType.create("Nicholas");

之所以叫静态成员,是因为它不依赖实例的数据

ES6 通过在方法或存取器属性名前加 static 关键字,就可以实现相同的功能:

class PersonClass {
  // equivalent of the PersonType constructor
  constructor(name) {
    this.name = name;
  }
  // equivalent of PersonType.prototype.sayName
  sayName() {
    console.log(this.name);
  }
  // equivalent of PersonType.create
  static create(name) {
    return new PersonClass(name);
  }

  static get hobbies () {
    return  ['coding', 'reading']
  }
}

let person = PersonClass.create("Nicholas");

console.log(PersonClass.hobbies)

// ['coding', 'reading']

static 不能用在 constructor

继承

ES5 里的继承是这样的:

function Rectangle(length, width) {
  this.length = length;
  this.width = width;
}
Rectangle.prototype.getArea = function () {
  return this.length * this.width;
};
function Square(length) {
  Rectangle.call(this, length, length);
}
Square.prototype = Object.create(Rectangle.prototype, {
  constructor: {
    value: Square,
    enumerable: true,
    writable: true,
    configurable: true
  }
});
var square = new Square(3);
console.log(square.getArea());  // 9
console.log(square instanceof Square);  // true
console.log(square instanceof Rectangle);  // true

ES6extends 简化了这一做法:

class Rectangle {
  constructor(length, width) {
    this.length = length;
    this.width = width;
  }
  getArea() {
    return this.length * this.width;
  }
}
class Square extends Rectangle {
  constructor(length) {
    // equivalent of Rectangle.call(this, length, length)
    super(length, length);
  }
}
var square = new Square(3);
console.log(square.getArea()); // 9
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true

constructor 里必须要调用 super,并且是在使用 this之前。 唯一避免使用 super 的方法,就是在 constructor 里返回一个对象。

当派生类没写 constructor 时, ES 会自动调用它, 并传入实例化时的所有参数:

class Square extends Rectangle {
  // no constructor
}

// is equivalent to
class Square extends Rectangle {
  constructor(...args) {
    super(...args);
  }
}

子类要覆盖父类的方法,在子类里重写一个就可以了,跟 ES5 一样。

静态成员

需要留意的是,父类的静态成员也会被继承下来:

class Rectangle {
  constructor(length, width) {
    this.length = length;
    this.width = width;
  }
  getArea() {
    return this.length * this.width;
  }
  static create(length, width) {
    return new Rectangle(length, width);
  }
}
class Square extends Rectangle {
  constructor(length) {
    // equivalent of Rectangle.call(this, length, length)
    super(length, length);
  }
}
var rect = Square.create(3, 4);
console.log(rect instanceof Rectangle);
console.log(rect.getArea());
console.log(rect instanceof Square);
// true
// 12
// false

从表达式里继承

extends 后面可以跟任何表达式,只要这个表示最后的结果是个函数,函数有 [[Constuctor]] 和原型就可以。

如下,clsss 可以继承 ES5 里的类

function Rectangle(length, width) {
  this.length = length;
  this.width = width;
}
Rectangle.prototype.getArea = function () {
  return this.length * this.width;
};
class Square extends Rectangle {
  constructor(length) {
    super(length, length);
  }
}
var x = new Square(3);
console.log(x.getArea());
console.log(x instanceof Rectangle);
// 9
// true

凭借这个特性,class 还可以 extends mixin:

let SerializableMixin = {
  serialize() {
    return JSON.stringify(this);
  }
};
let AreaMixin = {
  getArea() {
    return this.length * this.width;
  }
};
function mixin(...mixins) {
  var base = function () { };
  Object.assign(base.prototype, ...mixins);
  return base;
}
class Square extends mixin(AreaMixin, SerializableMixin) {
  constructor(length) {
    super();
    this.length = length;
    this.width = length;
  }
}
var x = new Square(3);
console.log(x.getArea());
console.log(x.serialize());
// 9
// "{"length":3,"width":3}"

如果多个 mixin 有同样的属性的话,后者会覆盖前者。

继承内置对象

ES5 里,继承内置对象是存在问题的,如下面自定义数组的操作:

// built-in array behavior
var colors = [];
colors[0] = "red";
console.log(colors.length);
colors.length = 0;
console.log(colors[0]);
// 1
// undefined

// trying to inherit from array in ES5
function MyArray() {
  Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
  constructor: {
    value: MyArray,
    writable: true,
    configurable: true,
    enumerable: true
  }
});

var colors = new MyArray();
colors[0] = "red";
console.log(colors.length);
colors.length = 0;
console.log(colors[0]);
// 0
// "red"

可以看出,MyArray 并没有真正继承 Array, 因为 MyArray 对下标的长度的操作,与原生数组比是无效的。

这是因为在 ES5 的继承里,派生类 MyArray 先创建出 this, 然后 Array 才实例化。这意味着,是用内置的Array 去覆盖 MyArray 实例。而正确的做法应该是用 MyArray 实例去修改 Array 实例。

ES6 正是这么做的:

class MyArray extends Array {
  // empty
}
var colors = new MyArray();
colors[0] = "red";
console.log(colors.length);
colors.length = 0;
console.log(colors[0]);
// 1
// undefined

如上面用 ES6 方式继承类的实例,上述操作与原生数组一样,是有效的继承。

至于为什么 ES6 可以实现这个功能,其实是与 Symbol.species 密切相关,感兴趣的同学可以继续深究下

new.target

函数章节里,我们提到过 new.target 可以用来判断函数是被用作普通函数,还是构造函数。如果是用作构造函数,那 new.target 就等于调用它的构造函数。凭借它,我们可以对 class 更多控制

如我们限定一个 class 是抽象类,只能用来被继承的话,可以这么写:

// abstract base class
class Shape {
  constructor() {
    if (new.target === Shape) {
      throw new Error("This class cannot be instantiated directly.")
    }
  }
}
class Rectangle extends Shape {
  constructor(length, width) {
    super();
    this.length = length;
    this.width = width;
  }
}
var x = new Shape();
var y = new Rectangle(3, 4);
console.log(y instanceof Shape);
// throws an error
// no error
// true

其实在继承中,父类的 new.target 指向子类:

class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
    this.length = length;
    this.width = width;
  }
}
class Square extends Rectangle {
  constructor(length) {
    super(length, length)
  }
}
// new.target is Square
var obj = new Square(3);
// outputs false

总结

ES6 提供了 classextends 是创建类和实现继承更方便了。而且还可以继承原生对象,可以继承 mixin , 提高了开发效率。

但记住 class 只是 ES5 的语法糖,理解 ES5 的继承方法,对理解 class 是有一定帮助的。