自己对class的总结和理解

238 阅读9分钟

一.由来

在ES6之前,生成实例对象都是通过构造函数的形式,如下所示

  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 都可以做的到,上面的代码用 ES6 的class改写,就是下面这样。

class Point {
    constructor(x, y) {
      this.x = x;
      this.y = y;
    }
    toString() {
      return '(' + this.x + ', ' + this.y + ')';
    }
  }

上面代码定义了一个“类”,可以看到里面有一个constructor方法,这就是构造函数,这里的this关键字则代表实例对象,因为在用new去创建一个类实例的时候,会把this绑定为类实例。

Point类除了构造方法,还定义了一个toString方法。类的所有方法都定义在类的prototype属性上面

class Point {
  constructor() {
    // ...
  }
  toString() {
    // ...
  }
  toValue() {
    // ...
  }
}
// 等同于
Point.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};

注意:在类的实例上面调用方法,其实就是调用原型上的方法。间接证明类point就是一个函数,因为对象是没有prototype属性的。

类的实例

与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class point函数上)。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}
var point = new Point(2, 3);
point.toString() // (2, 3)
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true

上面代码中,x和y都是实例对象point自身的属性(因为定义在this变量上),所以hasOwnProperty方法返回true,而toString是原型对象的属性(因为定义在Point类上,类的所有方法都定义在类的prototype属性上面),所以hasOwnProperty方法返回false。这些都与 ES5 的行为保持一致。

与 ES5 一样,类的所有实例共享一个原型对象。如果存在继承现象类的__proto__属性并不指向父类实例

var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__//true

取值函数(getter)和存值函数(setter)

与 ES5 一样,在“类”的内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

 复制代码
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'

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

上面代码中,printName方法中的this,默认指向Logger类的实例。但是,如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境(由于 class 内部是严格模式,所以 this 实际指向的是undefined),从而导致找不到print方法而报错。 举一个例子


import React, { Component } from 'react';
 
export  default class Input extends Component {
    h=3
    handleChange = (e) => {
        this.d();
    }
        
    d(){
    // 在这里相当于内部使用,this的指向就是类实例
        console.log(this.h)
    }

    render() {
        return (
                    <input 
                        type="text" 
                        value={radioValue}
                        checked={radioValue === 'male'}
                        onChange={this.handleChange}
                    />
        );
    }
}


image.png 如果是这样写:

import React, { Component } from 'react';
 
export  default class Input extends Component {
    h=3
    
    handleChange  (e) {
        this.d();
    }
        
    d(){
        console.log(this.h,this)
    }

    render() {
        return (
                    <input 
                        type="text" 
                        onChange={this.handleChange}
                    />
        );
    }
}

直接报错,你可以认为handleChange函数,相当于在类的外部被使用(总结在内部的方法其实不需要绑定this,只有看起来写在内部其实在外部调用的才需要绑定this) image.png 一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。

class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }
  // ...
}

另一种解决方法是使用箭头函数。

class Obj {
  constructor() {
    this.getThis = () => this;
  }
}
const myObj = new Obj();
myObj.getThis() === myObj // true

箭头函数内部的this总是指向定义时所在的对象。上面代码中,箭头函数位于构造函数内部,它的定义生效的时候,是在构造函数执行的时候。这时,箭头函数所在的运行环境,肯定是实例对象,所以this会总是指向实例对象。

二.静态方法和静态属性

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

class Foo {
  static classMethod() {
    return 'hello';
  }
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

注意,如果静态方法包含this关键字,这个this指的是类(即这个函数),而不是实例。

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

class Foo {
  static classMethod() {
    return 'hello';
  }
}
class Bar extends Foo {
}
Bar.classMethod() // 'hello'

上面代码中,父类Foo有一个静态方法,子类Bar可以调用这个方法。

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

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

静态属性的写法

class Foo {
  // ...
}
Foo.prop = 1;
// 新写法
class Foo {
  static prop = 1;
}

三.私有方法和私有属性(ts已经有private这个属性了)

私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问。这是常见需求,有利于代码的封装,但 ES6 不提供,只能通过变通方法模拟实现。 私有属性和私有方法前面,也可以加上static关键字,表示这是一个静态的私有属性或私有方法。

class FakeMath {
  static PI = 22 / 7;
  static #totallyRandomNumber = 4;
  static #computeRandomNumber() {
    return FakeMath.#totallyRandomNumber;
  }
  static random() {
    console.log('I heard you like random numbers…')
    return FakeMath.#computeRandomNumber();
  }
}
FakeMath.PI // 3.142857142857143
FakeMath.random()
// I heard you like random numbers…
// 4
FakeMath.#totallyRandomNumber // 报错
FakeMath.#computeRandomNumber() // 报错

上面代码中,#totallyRandomNumber是私有属性,#computeRandomNumber()是私有方法,只能在FakeMath这个类的内部调用,外部调用就会报错。

四.实例属性的另一种写法

实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层。

class IncreasingCounter {
  constructor() {
    this._count = 0;
  }
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}
上面代码中,实例属性this._count定义在constructor()方法里面。另一种写法是,这个属性也可以定义在类的最顶层,其他都不变。

class IncreasingCounter {
  count = 0;
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this.count++;
  }
}

上面代码中,实例属性count与取值函数value()和increment()方法,处于同一个层级。这时,不需要在实例属性前面加上this。

这种新写法的好处是,所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性。

class foo {
  bar = 'hello';
  baz = 'world';
  constructor() {
    // ...
  }
}

上面的代码,一眼就能看出,foo类有两个实例属性,一目了然。另外,写起来也比较简洁。

五.继承

class Human{
    constructor(name){
        this.name = name
    }
    run(){
        console.log('走你')
    }
}
class Man extends Human{
    constructor(name){
        super(name)
        this.sex='男'
    }
    habit(){
        console.log('爱打篮球')
    }
}

代码详解:
es6中类的写法:自身属性写在constructor里面,共有属性(原型链上的)run直接和他并列着写

继承的写法:class后面是子类然后extends你的父类
也就是

Man extends Human
//等价于
Man.prototype.__proto__ = Human.prototype

如下图:

image.png 然后super(name)就等价于Human.call(this,name),super就是调用Human中的constructor函数,把父类的this绑定为子类的this. call则是立即调用。

super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。

第一种情况,super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。

第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

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指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。

class A {
    constructor() {
      this.p = 2;
    }
  }
  class B extends A {
    get m() {
      return super.p;
    }
  }
  let b = new B();
  b.m // undefined

上面代码中,p是父类A实例的属性,super.p就引用不到它。

如果属性定义在父类的原型对象上,super就可以取到。

class A {}
A.prototype.x = 2;
class B extends A {
  constructor() {
    super();
    console.log(super.x) // 2
  }
}
let b = new B();

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()虽然调用的是A.prototype.print(),但是A.prototype.print()内部的this指向子类B的实例,导致输出的是2,而不是1。也就是说,实际上执行的是super.print.call(this),继承的实际就是将父类的this绑定为子类的this对象

如果super作为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象。

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在静态方法之中指向父类,在普通方法之中指向父类的原型对象。

另外,在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。

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

上面代码中,静态方法B.m里面,super.print指向父类的静态方法。这个方法里面的this指向的是B,而不是B的实例。

注意,使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。

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

上面代码中,console.log(super)当中的super,无法看出是作为函数使用,还是作为对象使用,所以 JavaScript 引擎解析代码的时候就会报错。这时,如果能清晰地表明super的数据类型,就不会报错。

class A {}
class B extends A {
  constructor() {
    super();
    console.log(super.valueOf() instanceof B); // true
  }
}
let b = new B();

上面代码中,super.valueOf()表明super是一个对象,因此就不会报错。同时,由于super使得this指向B的实例,所以super.valueOf()返回的是一个B的实例。

最后,由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super关键字。

var obj = {
  toString() {
    return "MyObject: " + super.toString();
  }
};
obj.toString(); // MyObject: [object Object]

六、原型对象与原型链

见另一篇文章 juejin.cn/post/708937…