简介

ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

ES5 传统构造函数方式

  1. function Point(x, y) {
  2. this.x = x;
  3. this.y = y;
  4. }
  5. Point.prototype.toString = function () {
  6. return '(' + this.x + ', ' + this.y + ')';
  7. };
  8. var p = new Point(1, 2);

ES6 Class方式

  1. class Point{
  2. constructor(x,y){
  3. this.x = x;
  4. this.y = y;
  5. }
  6. toString(){
  7. return '(' + this.x + ',' + this.y + ')';
  8. }
  9. }
  10. var p = new Point(1,2)

上面代码定义了一个“类”,可以看到里面有一个constructor()方法,这就是构造方法,而this关键字则代表实例对象。这种新的 Class 写法,本质上与本章开头的 ES5 的构造函数Point是一致的。
Point类除了构造方法,还定义了一个toString()方法。注意,定义toString()方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法与方法之间不需要逗号分隔,加了会报错

注意**
构造函数的prototype属性,在’类’上继续存在,事实上,类里没有加修饰符的方法都是定义在prototype上。**

  1. class Point {
  2. constructor() {
  3. // ...
  4. }
  5. toString() {
  6. // ...
  7. }
  8. toValue() {
  9. // ...
  10. }
  11. }
  12. // 等同于
  13. Point.prototype = {
  14. constructor() {},
  15. toString() {},
  16. toValue() {},
  17. };
  • 类的内部定义的所有方法都是不可以被枚举的。这与ES5中的不同。 ```javascript // ES6 class内部定义的方法是不可以被枚举的 class Point { constructor(x, y) { // … }

    toString() { // … } }

Object.keys(Point.prototype) // [] Object.getOwnPropertyNames(Point.prototype) // [“constructor”,”toString”]

// ES5 定义在构造函数的原型上的方法是可以被枚举的 var Point = function (x, y) { // … };

Point.prototype.toString = function () { // … };

Object.keys(Point.prototype) // [“toString”] Object.getOwnPropertyNames(Point.prototype) // [“constructor”,”toString”]

<a name="mQ0vw"></a>
# constructor

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

// 等同于
class Point {
  constructor() {}
}
  • constructor()方法默认返回实例对象,即this,完全可以指定constructor()返回其他对象。 ```javascript class Foo{ constructor(){
    return object.create(null)
    
    } }

new Foo() instanceof Foo // false

上面代码中,_**`constructor()`**_函数返回一个全新的对象,结果导致实例对象不是_**`Foo`**_类的实例。

- **类必须使用**_**`new`**_**调用,否则会报错。这是它和普通构造函数的主要区别,构造函数不使用**_**`new`**_**调用也可以执行。**
```javascript
class Foo {
  constructor() {
    return Object.create(null);
  }
}

Foo()
// TypeError: Class constructor Foo cannot be invoked without 'new'

类的实例

  • 与ES5一样,所有实例对象共享一个原型对象 ```javascript var p1 = new Point(2,3); var p2 = new Point(3,2);

p1.proto === p2.proto //true p1.proto.printName = function () { return ‘Oops’ };

p1.printName() // “Oops” p2.printName() // “Oops”

var p3 = new Point(4,2); p3.printName() // “Oops”

<a name="Bqncn"></a>
# 取值函数(getters)和存值函数(setters)

- **与 ES5 一样,在“类”的内部可以使用**_**`get`**_**和**_**`set`**_**关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。**
```javascript
class MyClass {
  constructor() {
    // ...
  }
  get prop() {
    return 'getter';
  }
  set prop(value) {
    console.log('setter: '+value);
  }
}

let inst = new MyClass();

inst.prop = 123;
// setter: 123

inst.prop
// 'getter'

上面代码中的prop属性虽然没有显式的定义在”类”中,但是prop属性已经有了取值函数和存值函数,也就相当于在”类”中被定义了,所以prop属性可以被访问。

属性表达式

  • 类的属性名,可以采用表达式。 ```javascript let methodName = ‘getArea’;

class Square { constructor(length) { // … }

methodName { // … } }

上面代码中,`Square`类的方法名`getArea`,是从表达式得到的。
<a name="BO8sX"></a>
# Class表达式

- **与函数一样,class也可以采用表达式的形式。**
```javascript
const MyClass = class Me {
  getClassName() {
    return Me.name;
  }
};

let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined

上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是Me,但是Me只在 Class 的内部可用,指代当前类。在 Class 外部,这个类只能用MyClass引用

注意点

(1)严格模式

类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。

(2)不存在提升

类不存在变量提升(hoist),这一点与 ES5 完全不同。

new Foo(); // ReferenceError
class Foo {}

(3)name 属性

由于本质上,ES6 的类只是 ES5 的构造函数的一层包装,所以函数的许多特性都被Class继承,包括name属性。

class Point {}
Point.name // "Point"

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

(4)Generator 方法

如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数。

(5)this 的指向

类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。

class Logger {
  printName(name = 'there') {
    this.print(`Hello ${name}`);
  }

  print(text) {
    console.log(text);
  }
}

const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined

静态方法和静态属性

  • 类相当于实例的原型,所有在类中定义的方法,都会被实例继承,如果在某个方法或属性前加上static关键字,那么该属性或方法将**不会被实例继承**,并且只能通过类调用,不可以通过实例对象来调用。 ```javascript class Foo { static name = ‘kobe’; static classMethod() { return ‘hello’; } }

Foo.classMethod() // ‘hello’

var foo = new Foo(); foo.classMethod() // TypeError: foo.classMethod is not a function


- **注意,如果静态方法包含**`this`**关键字,这个**`this`**指的是类,而不是实例。**
```javascript
class Foo {
  static bar() {
    this.baz();
  }
  static baz() {
    console.log('hello');
  }
  baz() {
    console.log('world');
  }
}

Foo.bar() // hello

上面代码中,静态方法bar调用了this.baz,这里的this指的是Foo类,而不是Foo的实例,等同于调用Foo.baz。另外,从这个例子还可以看出,静态方法**可以**与非静态方法重名

  • 父类的静态方法可以被子类继承 ```javascript class Foo { static classMethod() { return ‘hello’; } }

class Bar extends Foo { }

Bar.classMethod() // ‘hello’

**静态方法也是可以从****`super`****对象上调用的。**
```javascript
class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
  static classMethod() {
    return super.classMethod() + ', too';
  }
}

Bar.classMethod() // "hello, too"

私有属性和方法

  • 目前,有一个提案,为**class加了私有属性和私有方法。方法是在属性名或方法名之前,使用#**表示,子类不能继承父类的私有属性和方法。 ```javascript class IncreasingCounter {

    count = 0;

    get value() { console.log(‘Getting the current value!’); return this.#count; } increment() { this.#count++; } }

const counter = new IncreasingCounter(); counter.#count // 报错 counter.#count = 42 // 报错

上面代码中,`#count`就是私有属性,**只能在类的内部使用**(`this.#count`)。如果在类的外部使用,就会报错。

- **私有属性和私有方法前面,也可以加上****`static`****关键字,表示这是一个静态的私有属性或私有方法。**
<a name="XEutj"></a>
# new.target

- **命令引入了一个****`new.target`****属性,该属性一般用在构造函数之中,返回****`new`****命令作用于的那个构造函数。如果构造函数不是通过****`new`****命令或****`Reflect.construct()`****调用的,****`new.target`****会返回****`undefined`****,因此这个属性可以用来确定构造函数是怎么调用的。**
```javascript
function Person(name) {
  if (new.target !== undefined) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 命令生成实例');
  }
}

// 另一种写法
function Person(name) {
  if (new.target === Person) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 命令生成实例');
  }
}

var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三');  // 报错

利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。

class Shape {
  constructor() {
    if (new.target === Shape) {
      throw new Error('本类不能实例化');
    }
  }
}

class Rectangle extends Shape {
  constructor(length, width) {
    super();
    // ...
  }
}

var x = new Shape();  // 报错
var y = new Rectangle(3, 4);  // 正确

上面代码中,Shape类不能被实例化,只能用于继承。
注意,在函数外部,使用**new.target**会报错。
**

super关键字

super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
第一种情况,super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数,子类构造函数中使用了this关键字,那么super函数必须在第一行就执行。

这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工,如果不调用super方法,子类就得不到this对象。因此,只有调用super之后,才可以使用this关键字。
class A {}

class B extends A {
  constructor() {
    super();
  }
}

因此super()在这里相当于A.prototype.constructor.call(this)
  • 作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。 ```javascript class A {}

class B extends A { m() { super(); // 报错 } }


- **第二种情况,`super`作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。**
```javascript
class A {
  p() {
    return 2;
  }
}

class B extends A {
  constructor() {
    super();
    console.log(super.p()); // 2
  }
}

let b = new B();

上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()。
  • 这里需要注意,定义在**实例对象**上的属性或者方法是无法通过super来调用的。 ```javascript 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`指向当前的****子类实例**。
```javascript
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来获取某个属性或调用某个方法时,super指向的是父类的原型或父类,但如果通过super来给某个属性或方法来赋值,这时super指向的就是this(子类的实例),赋值的属性就会变成子类实例的属性。 ```javascript class A { constructor() { this.x = 1; } }

class B extends A { constructor() { super(); this.x = 2; super.x = 3; console.log(super.x); // undefined console.log(this.x); // 3 } }

let b = new B();

上面代码中,super.x赋值为3,这时等同于对this.x赋值为3。而当读取super.x的时候,读的是A.prototype.x,所以返回undefined。


   - **在****子类的静态方法****中通过**`super`**调用父类的方法或属性**,那么`super`指向的是父类,而不是父类的原型,也就是说在静态方法中通过`super`调用父类的方法或属性,那么**调用的将是父类的静态属性或方法**。**另外,在子类的静态方法中通过`super`调用父类的方法时**,**父类方法内部的**`this`**指向当前的子类,而不是子类的实例**。
```javascript
class Parent {
  static myMethod(msg) {
    console.log('static', msg);
  }

  myMethod(msg) {
    console.log('instance', msg);
  }
}

class Child extends Parent {
  static myMethod(msg) {
    super.myMethod(msg);
  }

  myMethod(msg) {
    super.myMethod(msg);
  }
}

Child.myMethod(1); // static 1

var child = new Child();
child.myMethod(2); // instance 2
  • 注意,使用**super**的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。 ```javascript class A {}

class B extends A { constructor() { super(); console.log(super); // 报错 } }


   - 最后,**由于对象总是继承其他对象的,所以可以在任意一个对象中,使用****`super`****关键字。**
```javascript
var obj = {
  toString() {
    return "MyObject: " + super.toString();
  }
};

obj.toString(); // MyObject: [object Object]