ES6中的Class

170 阅读8分钟

一、前言

面向对象的语言大致可以分成基于类的语言和基于原型的语言。基于类的语言诸如Java和C++,是在两个不同的实体的基础上构建的:类(class)和实例(instance)。基于原型的语言依靠原型对象(prototype object)来创建对象的层级结构、创建属性和实现继承。

1.基于类的语言

  • 一个类(class)用于定义某一个对象集合所具有的特征属性。类是抽象的,并不表示所描述的对象集合中的任一特定个体
  • 一个实例(instance)是一个类的实例化,代表一个类中一个特定的个体。一个类中的所有实例都具有父类中所包含的所有属性。
  • 定义类时需要专门的类定义(class definition)来定义一个类,并在定义时,可选使用构造器(constructor)的特殊方法来创建类的实例。最后可以通过使用new操作符来创建类的实例。
  • 基于类的语言通过对类的定义实现类的继承。在定义一个新类时,可以指定某一个已有的类为父类,则子类将继承父类所有的属性和方法,子类也可以添加新的属性和方法或者重写继承的属性。
  • 在基于类的语言中,通常在编译时创建类,然后在编译时或者运行时对类的实例进行实例化。一旦定义了类,无法对类的属性进行更改。

2.基于原型的语言

  • 基于原型的语言中一般不存在类的概念,但有一个原型对象(prototype object)的概念。原型对象用于作为一个创建新对象的模板,新对象可以从原型对象中获得原始的属性。当然,新对象也可以另外指定自身的属性。任意对象都可以指定另一个对象作为它的原型,从而允许前者共享后者的属性。
  • JavaScript并没有单独的类定义(class definition),通常使用定义构造函数(Constructor)的方式来创建一系列有特定属性和方法的对象。创建新对象时也可以使用new操作符。
  • JavaScript通过构造函数(Constructor)与原型对象(prototype object)相关联的方式实现继承。通过指定由某特定构造函数而创建的实例对象的原型对象来实现继承。使用原型对象的好处:可以让所有对象实例共享它所包含的属性和方法。即不必再构造函数中定义对象实例的所有信息,而是将这些信息直接添加到原型对象中。
  • 在 JavaScript 中,允许运行时添加或者移除任何对象的属性。如果您为一个对象中添加了一个属性,而这个对象又作为其它对象的原型,则以该对象作为原型的所有其它对象也将获得该属性。(换句话说,原型对象是动态的)

二、ES6中的Class

1.为什么要用Class?

传统的依靠原型操作对象的方法与传统的面向对象语言差异很大,ES6提供了更接近传统语言的写法,即模拟了依靠类来操作对象的方法。但是ES6中的class只是一个语法糖,它的几乎所有功能依靠原型也可以实现。即ES6中的类建立在原型上,但是也有某些新有的语法和语义。

2.定义类和创建实例

(1)定义类

类声明: 类的声明中有一个constructor()方法,即构造方法,其中的this关键字代表实例对象。类的其他方法直接加载constructor()方法之后即可,且不需要加上function关键字。方法与方法之间不需要逗号。

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
	}
}

要注意的地方:

  • 类的数据类型就是函数。类本身就指向构造函数。
  • 原先的构造函数的prototype属性在ES6的类中继续存在。事实上,类的所有方法都定义在类的prototype属性上面
  • 类声明不存在声明提升。
  • 类的内部的所有定义的方法都是不可枚举的(nonenumerable)(与ES5不一致)。

类表达式: 类表达式是定义类的另一种方法。类表达式可以命名或不命名。命名类表达式的名称是该类体的局部名称。

let Rectangle = class Rectangle2 {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};
console.log(Rectangle.name);
// 输出: "Rectangle2"

(2)类体和方法定义

类体: 即一对花括号{}中的部分,这是定义方法和构造函数的位置。类声明和类表达式的类体都执行在严格模式下。诸如构造函数、静态方法、原型方法、getter与setter都在严格模式下执行。

constructor()方法constructor()方法是类的默认方法,当通过new命令生成对象实例时会自动调用该方法。一个类必须有一个该方法,如果未被显式定义,则默认为一个空的constructor()方法。constructor()方法默认返回实例对象(即this),也可以指定返回另外一个对象(不推荐)。与ES5一样,类的所有实例都共享一个原型对象。

原型方法:直接定义在类中的方法均为原型方法,包括constructor()方法。 在各大浏览器中,可以通过实例的__proto__属性为“类”添加方法(但不推荐,__proto__ 并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,生产环境中,我们可以使用 Object.getPrototypeOf 方法来获取实例对象的原型,然后再来为原型添加方法/属性。)。

静态方法: static关键字用于定义一个类的静态方法。该静态方法直接作为Class自身对象的方法,所以不需要实例化该类。

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
	
    static displayName = "Point";

    static distance(a, b) {
        const dx = a.x - b.x;
        const dy = a.y - b.y;
        return Math.hypot(dx, dy);
    }
}
console.log(Point.displayName);
// "Point"
console.log(Point.distance(p1, p2));
// 7.0710678118654755

getter与setter: getter与setter关键字用于创建一个返回动态计算值的伪属性。get语法将对象属性绑定到查询该属性时将被调用的函数,setter语法将对象属性绑定到设置该属性时将被调用的函数。

//getter
const obj = {
  log: ['a', 'b', 'c'],
  get latest() {
    if (this.log.length === 0) {
      return undefined;
    }
    return this.log[this.log.length - 1];
  }
};

console.log(obj.latest);
// expected output: "c"
//setter
const language = {
  set current(name) {
    this.log.push(name);
  },
  log: []
};

language.current = 'EN';
language.current = 'FA';

console.log(language.log);
// expected output: Array ["EN", "FA"]

私有方法与私有属性: 私有方法和私有属性,是只能在类的内部访问的方法和属性,这种特性有利于代码的封装。注意,ES6没有提供直接的创建方法,但可以通过变通方法模拟实现。

  • 在要作为私有的属性和方法名前面加上下划线_
class Widget {

  // 公有方法
  foo (baz) {
    this._bar(baz);
  }

  // 私有方法
  _bar(baz) {
    return this.snaf = baz;
  }

  // ...
}
  • 利用函数提升的特性,直接将私有方法移出类。
class Widget {
  foo (baz) {
    bar.call(this, baz);
  }

  // ...
}
function bar(baz) {
  return this.snaf = baz;
}
  • 将私有方法的名字命名为一个Symbol
const bar = Symbol('bar');
const snaf = Symbol('snaf');
export default class myClass{

  // 公有方法
  foo(baz) {
    this[bar](baz);
  }

  // 私有方法
  [bar](baz) {
    return this[snaf] = baz;
  }

  // ...
};

3.继承与层级结构

(1)继承的基本实现

extends关键字在类声明或类表达式中用于创建一个类作为另一个类的子类。

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name); // 调用超类构造函数并传入name参数
  }

  speak() {
    console.log(`${this.name} barks.`);
  }
}
//Dog成为了Animal的子类

需要注意的点:

  • 类不能继承常规对象,如果要继承常规对象,可以改用Object.setPrototypeOf()
var Animal = {
  speak() {
    console.log(this.name + ' makes a noise.');
  }
};

 class Dog {
  constructor(name) {
    this.name = name;
  }
}

 Object.setPrototypeOf(Dog.prototype, Animal);

 var d = new Dog('Jack');
d.speak(); // Jack makes a noise.
  • Object.getPrototypeOf()方法可以用来从子类上获取父类。

(2)super关键字

super关键字既可以作函数使用,也可以作对象使用。

  • super作函数使用时,代表父类的构造函数。ES6中子类的构造函数constructor()中必须再执行一次super()函数,代表调用父类的构造函数。这是必须的操作。 super虽然代表了父类的构造函数方法,但是返回的是子类的实例,即super内部的this指向的是子类的实例,因此super()相当于A.prototype.constructor.call(this),这里A代表父类。
class A {
  constructor() {
    console.log(new.target.name);//target.name指向正在执行的函数
  }
}
class B extends A {
  constructor() {
    super();
  }
}
new A() // A
new B() // B
  • super作对象使用时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。 注意:由于super指向父类的原型对象,所以定义在对象实例上的方法和属性,是无法通过super调用的。
class A {
  constructor() {
    this.p = 2;
  }
}

class B extends A {
  get m() {
    return super.p;
  }
}

let b = new B();
b.m // undefined

ES6 规定,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。例子:

class A {
  constructor() {
    this.x = 1;
  }
  print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  m() {
    super.print();
  }
}

let b = new B();
b.m() // 2,这里super.print()方法内的this指向b。

(3)类的两条继承链

在大多数的浏览器中,每一个对象都有一个__proto__属性,指向对应的构造函数的prototype属性,即指向原型对象。由于Class集成了构造函数和原型对象方法,同时具有prototype属性和__proto__属性,所以存在以下两条继承链:

  • 子类的__proto__属性,表示构造函数的继承,总是指向父类。
  • 子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

(4)实例的__proto__属性

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