詳解ES6中的class

文章首發(fā)于 個(gè)人博客

目錄

  • class
  • 靜態(tài)方法
  • 靜態(tài)屬性
  • 繼承
  • super

class

class是一個(gè)語(yǔ)法糖,其底層還是通過(guò) 構(gòu)造函數(shù) 去創(chuàng)建的。所以它的絕大部分功能,ES5 都可以做到。新的class寫(xiě)法只是讓對(duì)象原型的寫(xiě)法更加清晰、更像面向?qū)ο缶幊痰恼Z(yǔ)法而已。

function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.sayName = function() {
    return this.name;
}

const xiaoming = new Person('小明', 18);
console.log(xiaoming);

上面代碼用ES6class實(shí)現(xiàn),就是下面這樣

class Person {
    constructor(name, age) {
      this.name = name;
      this.age = age;
    }
  
    sayName() {
      return this.name;
    }
}
const xiaoming = new Person('小明', 18)
console.log(xiaoming);
// { name: '小明', age: 18 }

console.log((typeof Person));
// function
console.log(Person === Person.prototype.constructor);
// true

constructor方法,這就是構(gòu)造方法,this關(guān)鍵字代表實(shí)例對(duì)象。
類(lèi)的數(shù)據(jù)類(lèi)型就是函數(shù),類(lèi)本身就指向構(gòu)造函數(shù)。

定義類(lèi)的時(shí)候,前面不需要加 function, 而且方法之間不需要逗號(hào)分隔,加了會(huì)報(bào)錯(cuò)。

類(lèi)的所有方法都定義在類(lèi)的prototype屬性上面。

class A {
    constructor() {}
    toString() {}
    toValue() {}
}
// 等同于

A.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};

在類(lèi)的實(shí)例上面調(diào)用方法,其實(shí)就是調(diào)用原型上的方法。

let a = new A();
a.constructor === A.prototype.constructor // true

constructor 方法

constructor方法是類(lèi)的默認(rèn)方法,通過(guò)new命令生成對(duì)象實(shí)例時(shí),自動(dòng)調(diào)用該方法。一個(gè)類(lèi)必須有constructor方法,如果沒(méi)有顯式定義,一個(gè)空的constructor方法會(huì)被默認(rèn)添加。

class A {
}

// 等同于
class A {
  constructor() {}
}

constructor方法默認(rèn)返回實(shí)例對(duì)象(即this),完全可以指定返回另外一個(gè)對(duì)象。

class A {
  constructor() {
      return Object.create(null);
  }
}

console.log((new A()) instanceof A);
// false

類(lèi)的實(shí)例

實(shí)例的屬性除非顯式定義在其本身(即定義在this對(duì)象上),否則都是定義在原型上(即定義在class上)。

注意:

  1. class不存在變量提升
new A(); // ReferenceError
class A {}

因?yàn)?ES6 不會(huì)把類(lèi)的聲明提升到代碼頭部。這種規(guī)定的原因與繼承有關(guān),必須保證子類(lèi)在父類(lèi)之后定義。

{
  let A = class {};
  class B extends A {}
}

上面的代碼不會(huì)報(bào)錯(cuò),因?yàn)?B繼承 A的時(shí)候,A已經(jīng)有了定義。但是,如果存在 class提升,上面代碼就會(huì)報(bào)錯(cuò),因?yàn)?class 會(huì)被提升到代碼頭部,而let命令是不提升的,所以導(dǎo)致 B 繼承 A 的時(shí)候,F(xiàn)oo還沒(méi)有定義。

  1. this的指向
    類(lèi)的方法內(nèi)部如果含有this,它默認(rèn)指向類(lèi)的實(shí)例。但是,必須非常小心,一旦單獨(dú)使用該方法,很可能報(bào)錯(cuò)。

靜態(tài)方法

類(lèi)相當(dāng)于實(shí)例的原型,所有在類(lèi)中定義的方法,都會(huì)被實(shí)例繼承。
如果在一個(gè)方法前,加上 static 關(guān)鍵字,就表示該方法不會(huì)被實(shí)例繼承,而是直接通過(guò)類(lèi)來(lái)調(diào)用,這就稱(chēng)為"靜態(tài)方法"。

class A {
    static classMethod() {
        return 'hello';
    }
}
A.classMethod();
console.log(A.classMethod());
// 'hello'

const a = new A();
a.classMethod();
// TypeError: a.classMethod is not a function

A 類(lèi)的classMethod 方法前有 static關(guān)鍵字,表明這是一個(gè)靜態(tài)方法,可以在 A 類(lèi)上直接調(diào)用,而不是在實(shí)例上調(diào)用
在實(shí)例a上調(diào)用靜態(tài)方法,會(huì)拋出一個(gè)錯(cuò)誤,表示不存在改方法。

如果靜態(tài)方法包含this關(guān)鍵字,這個(gè)this指的是類(lèi),而不是實(shí)例。


class A {
    static classMethod() {
      this.baz();
    }
    static baz() {
      console.log('hello');
    }
    baz() {
      console.log('world');
    }
}
A.classMethod();
// hello

靜態(tài)方法classMethod調(diào)用了this.baz,這里的this指的是A類(lèi),而不是A的實(shí)例,等同于調(diào)用A.baz。另外,從這個(gè)例子還可以看出,靜態(tài)方法可以與非靜態(tài)方法重名。

父類(lèi)的靜態(tài)方法,可以被子類(lèi)繼承。


class A {
    static classMethod() {
        console.log('hello');
    }
}

class B extends A {}

B.classMethod() // 'hello'

靜態(tài)屬性

靜態(tài)屬性指的是 Class 本身的屬性,即Class.propName,而不是定義在實(shí)例對(duì)象(this)上的屬性。
寫(xiě)法是在實(shí)例屬性的前面,加上static關(guān)鍵字。

class MyClass {
  static myStaticProp = 42;

  constructor() {
    console.log(MyClass.myStaticProp); // 42
  }
}

繼承

Class 可以通過(guò)extends關(guān)鍵字實(shí)現(xiàn)繼承

class Animal {}
class Cat extends Animal { };

上面代碼中 定義了一個(gè) Cat 類(lèi),該類(lèi)通過(guò) extends關(guān)鍵字,繼承了 Animal 類(lèi)中所有的屬性和方法。
但是由于沒(méi)有部署任何代碼,所以這兩個(gè)類(lèi)完全一樣,等于復(fù)制了一個(gè)Animal類(lèi)。
下面,我們?cè)贑at內(nèi)部加上代碼。

class Cat extends Animal {
    constructor(name, age, color) {
        // 調(diào)用父類(lèi)的constructor(name, age)
        super(name, age);
        this.color = color;
    }
    toString() {
        return this.color + ' ' + super.toString(); // 調(diào)用父類(lèi)的toString()
    }
}

constructor方法和toString方法之中,都出現(xiàn)了super關(guān)鍵字,它在這里表示父類(lèi)的構(gòu)造函數(shù),用來(lái)新建父類(lèi)的this對(duì)象。

子類(lèi)必須在 constructor 方法中調(diào)用 super 方法,否則新建實(shí)例就會(huì)報(bào)錯(cuò)。
這是因?yàn)樽宇?lèi)自己的this對(duì)象,必須先通過(guò) 父類(lèi)的構(gòu)造函數(shù)完成塑造,得到與父類(lèi)同樣的實(shí)例屬性和方法,然后再對(duì)其進(jìn)行加工,加上子類(lèi)自己的實(shí)例屬性和方法。如果不調(diào)用super方法,子類(lèi)就得不到this對(duì)象。

class Animal { /* ... */ }

class Cat extends Animal {
  constructor() {
  }
}

let cp = new Cat();
// ReferenceError

Cat 繼承了父類(lèi) Animal,但是它的構(gòu)造函數(shù)沒(méi)有調(diào)用super方法,導(dǎo)致新建實(shí)例報(bào)錯(cuò)。

ES5的繼承,實(shí)質(zhì)是先創(chuàng)建了子類(lèi)的實(shí)例對(duì)象 this, 然后再將 父類(lèi)的方法添加到 this上面。

ES6的繼承機(jī)制完全不同,實(shí)質(zhì)是先將 父類(lèi)實(shí)例對(duì)象的屬性和方法,加到 this 上面(所以必須先調(diào)用 super 方法),然后再用子類(lèi)的構(gòu)造函數(shù)修改 this。
如果子類(lèi)沒(méi)有定義constructor方法,這個(gè)方法會(huì)被默認(rèn)添加,代碼如下。也就是說(shuō),不管有沒(méi)有顯式定義,任何一個(gè)子類(lèi)都有constructor方法。

class Cat extends Animal {

}
// 等同于

class Cat extends Animal {
    constructor(...args) {
        super(...args);
    }
}

另一個(gè)需要注意的地方是,在子類(lèi)的構(gòu)造函數(shù)中,只有調(diào)用super之后,才可以使用this關(guān)鍵字,否則會(huì)報(bào)錯(cuò)。這是因?yàn)樽宇?lèi)實(shí)例的構(gòu)建,基于父類(lèi)實(shí)例,只有super方法才能調(diào)用父類(lèi)實(shí)例。

class A {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class B extends A {
  constructor(x, y, name) {
    this.name = name; // ReferenceError
    super(x, y);
    this.name = name; // 正確
  }
}

上面代碼中,子類(lèi)的constructor方法沒(méi)有調(diào)用super之前,就使用this關(guān)鍵字,結(jié)果報(bào)錯(cuò),而放在super方法之后就是正確的。

父類(lèi)的靜態(tài)方法,也會(huì)被子類(lèi)繼承。

class A {
  static hello() {
    console.log('hello world');
  }
}

class B extends A {
}

B.hello()  // hello world

super

super這個(gè)關(guān)鍵字,既可以當(dāng)作函數(shù)使用,也可以當(dāng)作對(duì)象使用

super作為函數(shù)調(diào)用

super作為函數(shù)調(diào)用時(shí),代表父類(lèi)的構(gòu)造函數(shù)。ES6 要求,子類(lèi)的構(gòu)造函數(shù)必須執(zhí)行一次super函數(shù)。

class A {}

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

子類(lèi)B的構(gòu)造函數(shù)之中的super(),代表調(diào)用父類(lèi)的構(gòu)造函數(shù)。這是必須的,否則 JavaScript 引擎會(huì)報(bào)錯(cuò)。

注意,super雖然代表了父類(lèi)A的構(gòu)造函數(shù),但是返回的是子類(lèi)B的實(shí)例,即super內(nèi)部的this指的是B的實(shí)例,因此super()在這里相當(dāng)于A.prototype.constructor.call(this)。

class A {
  constructor() {
    // new.target 指向正在執(zhí)行的函數(shù)
    console.log(new.target.name);
  }
}
class B extends A {
  constructor() {
    super();
  }
}
new A() // A
new B() // B

super()執(zhí)行時(shí),它指向的是子類(lèi)B的構(gòu)造函數(shù),而不是父類(lèi)A的構(gòu)造函數(shù)。也就是說(shuō),super()內(nèi)部的this指向的是B。

super作為對(duì)象調(diào)用

在普通方法中,指向父類(lèi)的原型對(duì)象;
在靜態(tài)方法中,指向父類(lèi)。

super對(duì)象在普通函數(shù)中調(diào)用

class A {
  p() {
    return 2;
  }
}

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

let b = new B();

上面代碼中,子類(lèi)B當(dāng)中的super.p(),就是將super當(dāng)作一個(gè)對(duì)象使用。這時(shí),super在普通方法之中,指向A.prototype,所以super.p()就相當(dāng)于A.prototype.p()。

這里需要注意,由于super指向父類(lèi)的原型對(duì)象,所以定義在父類(lèi)實(shí)例上的方法或?qū)傩裕菬o(wú)法通過(guò)super調(diào)用的。

class A {
  constructor() {
    this.p = 2;
  }
}

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

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

上面代碼中,p是父類(lèi)A實(shí)例的屬性,super.p就引用不到它。

如果屬性定義在父類(lèi)的原型對(duì)象上,super就可以取到。

class A {}
A.prototype.x = 2;

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

let b = new B();

上面代碼中,屬性x是定義在A.prototype上面的,所以super.x可以取到它的值。

super對(duì)象在靜態(tài)方法中調(diào)用

用在靜態(tài)方法之中,這時(shí)super將指向父類(lèi),而不是父類(lèi)的原型對(duì)象。

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

const child = new Child();
child.myMethod(2); // instance 2

上面代碼中,super在靜態(tài)方法之中指向父類(lèi),在普通方法之中指向父類(lèi)的原型對(duì)象。

另外,在子類(lèi)的靜態(tài)方法中通過(guò)super調(diào)用父類(lèi)的方法時(shí),方法內(nèi)部的this指向當(dāng)前的子類(lèi),而不是子類(lèi)的實(shí)例。

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

上面代碼中,靜態(tài)方法B.m里面,super.print指向父類(lèi)的靜態(tài)方法。這個(gè)方法里面的this指向的是B,而不是B的實(shí)例。

總結(jié)

  • class是一個(gè)語(yǔ)法糖,其底層還是通過(guò) 構(gòu)造函數(shù) 去創(chuàng)建的。
  • 類(lèi)的所有方法都定義在類(lèi)的prototype屬性上面。
  • 靜態(tài)方法:在方法前加static,表示該方法不會(huì)被實(shí)例繼承,而是直接通過(guò)類(lèi)來(lái)調(diào)用。
  • 靜態(tài)屬性:在屬性前加static,指的是 Class 本身的屬性,而不是定義在實(shí)例對(duì)象(this)上的屬性。
  • ES6的繼承和ES5的繼承區(qū)別在于:
    • ES5的繼承,實(shí)質(zhì)是先創(chuàng)建了子類(lèi)的實(shí)例對(duì)象 this, 然后再將 父類(lèi)的方法添加到 this上面
    • ES6的繼承是先將父類(lèi)實(shí)例對(duì)象的屬性和方法,加到 this 上面(所以必須先調(diào)用 super 方法),然后再用子類(lèi)的構(gòu)造函數(shù)修改 this。
  • super
    • 作為函數(shù)調(diào)用,代表父類(lèi)的構(gòu)造函數(shù)
    • 作為對(duì)象調(diào)用,在普通方法中,指向父類(lèi)的原型對(duì)象;在靜態(tài)方法中,指向父類(lèi)。

再來(lái)幾道題檢查一下

1. 下面代碼輸出什么

class Person {
  constructor(name) {
    this.name = name
  }
}

const member = new Person("John")
console.log(typeof member)

答案:object
解析:
類(lèi)是構(gòu)造函數(shù)的語(yǔ)法糖,如果用構(gòu)造函數(shù)的方式來(lái)重寫(xiě)Person類(lèi)則將是:

function Person() {
  this.name = name
}

通過(guò)new來(lái)調(diào)用構(gòu)造函數(shù),將會(huì)生成構(gòu)造函數(shù)Person的實(shí)例,對(duì)實(shí)例執(zhí)行typeof關(guān)鍵字將返回"object",上述情況打印出"object"。

2. 下面代碼輸出什么

class Chameleon {
  static colorChange(newColor) {
    this.newColor = newColor
    return this.newColor
  }

  constructor({ newColor = 'green' } = {}) {
    this.newColor = newColor
  }
}

const freddie = new Chameleon({ newColor: 'purple' })
freddie.colorChange('orange')

答案:TypeError
解析:
colorChange 是一個(gè)靜態(tài)方法。靜態(tài)方法被設(shè)計(jì)為只能被創(chuàng)建它們的構(gòu)造器使用(也就是 Chameleon),并且不能傳遞給實(shí)例。因?yàn)?freddie 是一個(gè)實(shí)例,靜態(tài)方法不能被實(shí)例使用,因此拋出了 TypeError 錯(cuò)誤。

3.下面代碼輸出什么

class Person {
  constructor() {
    this.name = "Lydia"
  }
}

Person = class AnotherPerson {
  constructor() {
    this.name = "Sarah"
  }
}

const member = new Person()
console.log(member.name)

答案:"Sarah"
我們可以將類(lèi)設(shè)置為等于其他類(lèi)/函數(shù)構(gòu)造函數(shù)。 在這種情況下,我們將Person設(shè)置為AnotherPerson。 這個(gè)構(gòu)造函數(shù)的名字是Sarah,所以新的Person實(shí)例member上的name屬性是Sarah。

其他

最近發(fā)起了一個(gè)100天前端進(jìn)階計(jì)劃,主要是深挖每個(gè)知識(shí)點(diǎn)背后的原理,歡迎關(guān)注 微信公眾號(hào)「牧碼的星星」,我們一起學(xué)習(xí),打卡100天。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容