ES6的class探索

  1. constructor
  2. super
  3. 静态方法
  4. 私有方法和私有属性
  5. 静态块
  6. new.target属性
  7. ES6与ES5的对比
  8. class关键字的一些限制

ES6 中的关键字class本质上还是通过原型链和函数实现的,提供了一种更加方便、逻辑更加清晰的方法去实现类。

所以ES6的class属于一种语法糖,使写法更优雅,更加面向对象的编程,其思想和ES5没有本质区别。

ES6的 class 内部是基于寄生组合式继承,它是目前最理想的继承方式。

//ES6
class Point{
  constructor(x,y){
    this.x = x;
    this.y = y;
  }
  toString(){
    return `${this.x},${this.y}`
  }
}

等同于

// ES5
function Point(x,y){
  this.x = x;
  this.y = y;
}
Point.prototype.toString = function(){
  return '(' + this.x + ',' + this.y + ')';
};

constructor

constructor方法是类的构造函数,是一个默认方法,通过new命令创建对象实例时,自动调用该方法。

一个类必须有constructor方法,如果没有显示定义,一个默认的constructor方法会被默认添加。一般constructor方法返回实例对象this,但是也可以指定constructor方法返回一个全新的对象,让返回的实例对象不是该类的实例。

ES6中的constructor构造方法对应ES5中的构造函数。

super

super关键字既可以当函数使用,也可以当对象使用。这两种情况下,它的使用方法完全不同。
对象函数中的this指向的是当前函数所在的对象,而super指向的是当前函数所在对象的原型,比this更深了一层。在子类构造函数中调用super(),相当于调用父类的constructor

  1. super当函数使用

    class A{}
    class B extends A{
     constructor(){
       super(); // ES6要求,子类的构造函数必须执行一次super函数,否则会报错
    }
    

    注:在 constructor 中必须调用 super 方法,因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工,而 super 就代表了父类的构造函数。super 虽然代表了父类 A 的构造函数,但是返回的是子类 B 的实例,即 super 内部的 this 指的是 B,因此 super() 在这里相当于 A.prototype.constructor.call(this, props)

class A {
  constructor() {
    console.log(new.target.name); // new.target 指向当前正在执行的函数
  }
}

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

new A(); // A
new B(); // B

可以看到,在 super() 执行时,它指向的是 子类 B 的构造函数,而不是父类 A 的构造函数。也就是说,super() 内部的 this 指向的是 B。

  1. super当对象使用

    • 在普通方法中,指向父类的原型对象;
    • 在静态方法中,指向父类。
    class A{
     c(){
       return 2
     }
    }
    
    class B extends A{
     constructor(){
       super();
       console.log(super.c()); // 2
     }
    }
    let b = new B();
    

    上面代码中,子类B当中的super.c()就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.c()就相当于A.prototype.c()

    通过super调用父类的方法时,super会绑定子类的this

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

    上面代码中,super.s() 虽然调用的是 A.prototytpe.s(),但是 A.prototytpe.s()会绑定子类 B 的 this,导致输出的是 2,而不是 1。也就是说,实际上执行的是 super.s.call(this)

由于绑定子类的 this,所以如果通过 super 对某个属性赋值,这时 super 就是 this,赋值的属性会变成子类实例的属性。

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,但并没有 x 方法,所以返回 undefined

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

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

上面代码中,console.log(super); 中的 super,无法看出是作为函数使用,还是作为对象使用,所以 JavaScript 引擎解析代码的时候就会报错。如果能清晰的表明 super 的数据类型,就不会报错。
由于对象总是继承其他对象的,所以可以在任意一个对象中,使用 super 关键字。

静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上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 bar() {
    this.baz();
  }
  static baz() {
    console.log('hello');
  }
  baz() {
    console.log('world');
  }
}
 
Foo.bar() // hello

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

  • 父类的静态方法,可以被子类继承。
  • 静态方法也是可以从super对象上调用的。

私有方法和私有属性

私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问。这是常见需求,有利于代码的封装,但 ES6 不提供,只能通过变通方法模拟实现。

  • 一种做法是在命名上加以区别

    class Widget {
      // 公有方法
      foo (baz) {
        this._bar(baz);
      }
      // 私有方法
      _bar(baz) {
        return this.snaf = baz;
      }
      // ...
    }
    
  • 另一种方法就是索性将私有方法移出类,因为类内部的所有方法都是对外可见的。

    class Widget {
      foo (baz) {
        bar.call(this, baz); // 内部调用了bar.call(this, baz)。这使得bar()实际上成为了当前类的私有方法
      }
      // ...
    }
     
    function bar(baz) {
      return this.snaf = baz;
    }
    
  • 还有一种方法是利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。

静态块

ES2022引入的新概念。允许在类的内部设置一个代码块,在类生成时运行一次,主要作用是对静态属性进行初始化。

class C {
  static x = ...;
  static y;
  static z;
 
  static {
    try {
      const obj = doSomethingWith(this.x);
      this.y = obj.y;
      this.z = obj.z;
    }
    catch {
      this.y = ...;
      this.z = ...;
    }
  }
}

上面代码中,类的内部有一个 static 代码块,这就是静态块。它的好处是将静态属性y和z的初始化逻辑,写入了类的内部,而且只运行一次。
每个类只能有一个静态块,在静态属性声明后运行。静态块的内部不能有return语句。
静态块内部可以使用类名或this,指代当前类。

new.target属性

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

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 内部调用new.target,返回当前 class
需要注意的是,子类继承父类时,new.target会返回子类。
利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。

ES6与ES5的对比

  • 静态方法对比

    • ES6实现方式
    class Employee{
      constructor(name, dept){
        this.name = name;
        this.dept = dept;
      }
      static func(){
        console.log('static')
      }
      getName(){
        console.log(this.name)
      }
    }
    
    • ES5实现方式
    function Employee(name, dept){
      this.name = name;
      this.dept = dept;
    }
    Employee.fun = function(){
      console.log("static")
    }
    Employee.prototype.getName=function(){
      console.log(this.name)
    }
    
  • 继承方式对比

    • ES6实现方式
    class Manager extends Employee{
      constructor(name, dept, reports){
        super(name, dept);
        this.reports = reports;
      }
    }
    
    • ES5实现方式
    function Manager(reports, name ,dept){
      Employee.call(this, name, dept);
      this.reports = reports;
    }
    Manager.fun = Employee.fun;
    

class关键字的一些限制

  1. 灵活性
  2. 类声明不可提升
  3. 无法重写类

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 chaoyumail@126.com

×

喜欢就点赞,疼爱就打赏