Class的基本語法

文章編寫參考 阮一峯《ECMAScript 6 入門》


1. 簡介

在ES6之前要生成實例一般是通過構造函數

function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.toString = function () {
    return "I am " + this.name
}
var p = new Person('Blue', 23)

JS的繼承是基於原型的繼承,和傳統的面嚮對象語言有很大的差異。

ES6提供了更接近傳統語言的類的寫法,引入了Class這個概念,作爲對象的模板。通過class關鍵字可以定義類。

Class只是提供了定義類的語法糖,他的絕大部分功能ES6都可以做到,新的class語法只是讓對象原型的寫法更加清晰,更像面向對象編程的語法而已。上面的代碼用ES6的class改寫成下面這樣子

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    toString() {
        return "I am " + this.name
    }
}

上面代碼中【constructor】也就是構造方法,構造函數中的this是指實例對象。Person 類除了構造方法還定義了一個toString方法。

【注意】定義“類”的方法時,前面【不需要加關鍵詞function】,直接寫方法名稱和方法體就行。另外,【方法之間不需要用逗號分隔】,加了反而會報錯。

ES6的類在使用的時候,也是使用new命令,這個和ES5沒有任何的區別

var p = new Person('Blue', 23)

【構造函數的prototype屬性在類上面依然存在,事實上,類的所有方法都是定義在類的prototype屬性上的】

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    toString() {

    }
    toValue() {

    }
}
//等同於
Person.prototype = {
    constructor() { },
    toString() { },
    toValue() { },
};

【類的實例調用的方法其實就是調用的原型上的方法】

class Person {}
var p = new Person();
p.constructor===Person.prototype.constructor;
//true

上面代碼中p是Person類的實例,它的constructor方法就是Person 類原型的constructor方法。

由於類的方法是定義在類的prototype對象上,所以類的新方法可以定義在類的prototype對象上。在對象的擴展中有一個Object.assign( )方法用於合併對象屬性,那麼我們可以用這個方法向類添加多個方法。

class Person { }

Object.assign(Person.prototype, {
    toString() { },
    toValue() { }
})

【prototype對象的constructor屬性直接指向類本身】

class Person { }

Person.prototype.constructor === Person
//true

【類內部的所有方法都是不可枚舉的】

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    toString() { }
    toValue() { }
}
Object.keys(Person.prototype);
//[]

Object.getOwnPropertyNames(Person.prototype);
//[ 'constructor', 'toString', 'toValue' ]

在對象的擴展中,對象的屬性可以使用表達式,類其實也是一個對象,所以在類中屬性名稱也可以使用表達式

let mth = 'toValue';
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    toString() { }
    [mth]() { }
}

上面代碼中mth就是一個表達式,依次來獲取方法名稱。


2. 嚴格模式

類和模塊的內部,【默認就是嚴格模式】,所以不需要使用use strict指定運行模式。【只要你的代碼寫在類或模塊之中,就只有嚴格模式可用】。

考慮到未來所有的代碼,其實都是運行在模塊之中,所以 ES6 實際上把整個語言升級到了嚴格模式。


3. constructor方法

constructor方法是類的默認方法,在類被new生成實例的時候,會自動調用該方法。一個類中必須有constructor方法,如果沒有顯示的定義constructor方法,一個空的constructor方法會被默認添加。

class Person {
    toString() { }
}
//等同於
class Person {
    constructor() {
    }
    toString() { }
}

【constructor方法默認返回實例對象(即this)】所以我們完全可以自定義返回另外一個對象;

class Person {
    constructor() {
        return Object.create(null);
    }
}
new Person() instanceof Person
//false

上面代碼中,我在構造函數中定義了返回一個全新的對象,產生的結果就是new出來的實例不是Person類的實例。

【類必須使用new來生成實例,也就是說constructor方法只能通過new自動調用】然而普通的構造函數則可以單獨調用構造函數

class Person {
    constructor() {
        return Object.create(null);
    }
}
Person();
// TypeError: Class constructor Person cannot be invoked without 'new'

4. 類的實例對象

生成類的實例方式只有一種,那就是通過new來生成,ES5也是如此。

class Person {
    constructor() {
        return Object.create(null);
    }
}
var p = new Person();

【實例的屬性除非顯示的定義在實例的本身上(即定義在this對象上),否則都是定義在原型對象上(即定義在class上)】

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    toString() {

    }
}
let p = new Person('Blue', 23);

p.hasOwnProperty("name");   //true

p.hasOwnProperty("age");   //true

p.hasOwnProperty("toString");   //false

p.__proto__.hasOwnProperty("toString")  //true

上面代碼中,name和age屬性是顯示的定義在實例對象上的,toString方法則是定義在原型對象上的。

【所有的實例對象共享同一個原型對象】

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    toString() {

    }
}
let p1 = new Person('Blue', 23);
let p2 = new Person('Crazy', 25);
Object.getPrototypeOf(p1) === Object.getPrototypeOf(p2);

上面代碼通過Person類生成了兩個不同的實例,但是他們的原型對象是全等的。既然如此,那麼我們在原型上面動態添加屬性或者方法也同時被所有的實例共享。

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    toString() {

    }
}
let p1 = new Person('Blue', 23);
let p2 = new Person('Crazy', 25);

//往原型對象上添加方法
Object.assign(Object.getPrototypeOf(p1), {
    toAge() {
        console.log(this.age);
    }
});

p1.toAge(); //23
p2.toAge(); //25

上面代碼中,通過一個實例對象獲取到原型對象並且添加一個方法後,所有實例都可以獲得該方法。但是不建議這樣子幹,因爲這會改變“類”的原始定義,影響到所有實例。


5. Class表達式

與函數一樣,類也可以使用表達式的形式定義。

const MyClass = class Me {
    getClassName() {
        return Me.name
    }
}

上面代碼使用表達式定義了一個類。需要注意的是,這個【類的名字是MyClass而不是Me,Me只在 Class 的內部代碼可用】,指代當前類。

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

上面代碼表示,Me只在 Class 內部有定義。

如果類的內部沒有使用的話,可以省略Me

const MyClass = class {
    constructor(name) {
        this.name = name;
    }
    getClassName() {
        console.log(this.name);
    }
}
let mc = new MyClass('Blue');
mc.getClassName();  //blue

下面是一個立即執行的類的實例

const person = new class {
    constructor(name) {
        this.name = name;
    }
    sayHi() {
        console.log("I am ", this.name);
    }
}('Blue');

person.sayHi(); //I am  Blue

6. 類不存在變量提升

ES6中的類不存在變量提升,這個跟ES5還是有很大區別的

let p = new Person('Blue');     //// 報錯
class Person {
    constructor(name) {
        this.name = name;
    }
    sayHi() {
        console.log("I am ", this.name);
    }
}

上面代碼,由於class不存在變量提升,所以new的時候會報錯,當時Person還未定義

let People = class { };
class Person extends People{

}

上面的代碼不會報錯,構建Person類繼承People的時候People類已經存在了;如果class存在變量提升,let是不存在變量提升的,那麼以上代碼就會報錯,構造Person類的時候People還沒有構建。


7. 私有方法

在構建類的時候,我們一些方法或者屬性是在類的內部調用的,並不希望實例調用,所以我們需要創建私有方法。但是ES6並不支持類中方法的私有化,我們只能通過變通的方式來私有化方法或屬性。

【命名標識法】
以前我們約定,名稱前加“_”表示私有

class Person {
    //公有方法
    toString() { }
    //私有方法
    _toAge() { }
}

上面代碼就通過變量名稱來標識私有方法,但是這樣做其實只是一個約定,並不保險。因爲實例還是可以調用到這個所謂的私有方法。

【私有方法移出模塊】

另一種方法就是索性將私有方法移出模塊,因爲模塊內部的所有方法都是對外可見的。

class Person {
    constructor(name) {
        this.name = name;
    }
    sayHi(name) {
        _sayHi.call(this, this.name);
    }
}

function _sayHi(name) {
    console.log("I am", name);
}

let p = new Person('Blue');
p.sayHi();  ////I am Blue

上面代碼中,sayHi是公有的方法,方法內部調用模塊外部的方法實現方法的私有化,雖然這樣將方法移出模塊,暴露在全局中,但是針對類來說確實實現了方法的私有化。

【利用Symbol定義私有屬性】

因爲Symbol的值是唯一的,所以可以用Symbol值作爲屬性名稱實現私有,這時我個人比較推薦的一種方法。

let v = Symbol('v');
class Person {
    constructor(name) {
        this.name = name;
    }
    sayHi(name) {
        console.log("I am", this.name);
    }
    [v]() {
        console.log("私有方法");
    }
}

上面代碼中用Symbol值作爲屬性名,因爲Symbol值唯一,所以在實例中是沒法訪問到的。


8. this指向問題

類的方法內部如果含有this,那麼這個this指向該類的實例,但是如果將含this的方法拿出來單獨使用,那很可能報錯。

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    sayHi(name) {
        console.log("I am", this.name);
    }
    toAge() {
        console.log(this.age);
    }
}

let { sayHi } = Person;

sayHi();
//// TypeError: Cannot read property 'name' of undefined

上面代碼,sayHi中的this默認指向Person的實例,但是解構出來單獨使用時,this就指向了運行環境,但是該環境中並沒有name屬性,所以導致報錯。

一個比較簡單的解決方法是,在構造方法中綁定this,這樣就不會找不到name了。

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
        this.sayHi = this.sayHi.bind(this);
    }
    sayHi(name) {
        console.log("I am", this.name);
    }
    toAge() {
        console.log(this.age);
    }
}

9. name屬性

由於本質上,ES6 的類只是 ES5 的構造函數的一層包裝,所以函數的許多特性都被Class繼承,包括name屬性。

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

【name屬性總是返回緊跟在class關鍵字後面的類名】


10. Class的取值函數和存值函數

與 ES5 一樣,在“類”的內部可以使用get和set關鍵字,對某個屬性設置存值函數和取值函數,攔截該屬性的存取行爲。

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屬性有對應的存值函數和取值函數,因此賦值和讀取行爲都被自定義了。

存值函數和取值函數是設置在屬性的 Descriptor 對象上的

class CustomHTMLElement {
  constructor(element) {
    this.element = element;
  }

  get html() {
    return this.element.innerHTML;
  }

  set html(value) {
    this.element.innerHTML = value;
  }
}

var descriptor = Object.getOwnPropertyDescriptor(
  CustomHTMLElement.prototype, "html"
);

"get" in descriptor  // true
"set" in descriptor  // true

11. Class的靜態方法

類相當於實例的原型,所有在類中定義的方法,都會被實例繼承。如果在一個方法前,加上【static關鍵字】,就表示該方法不會被實例繼承,而是直接通過類來調用,這就稱爲“靜態方法”。

class Person {
    static sayHi() {
        console.log('Hello');
    }
}
Person.sayHi();
//Hello

上面代碼中sayHi方法被關鍵字static修飾,表示該方法爲靜態方法,只能在Person類上直接調用。如果在實例上調用靜態方法,會拋出一個錯誤表示該方法不存在。因爲【靜態方法不被實例所繼承】

【父類的靜態方法,可以被子類繼承】

class People {
    static sayHi() {
        console.log('Hello');
    }
}
class Person extends People {

}
Person.sayHi();
//Hello

上面代碼中,父類People有一個靜態方法,子類Person繼承了這個靜態方法

【靜態方法也是可以從super對象上調用的】

class People {
    static sayHi() {
        console.log('Hello');
    }
}
class Person extends People {
    static sayHello() {
        super.sayHi();
    }
}
Person.sayHello();

12. Class的靜態屬性和實例屬性

靜態屬性指的是 Class 本身的屬性,即Class.propName,而不是定義在實例對象(this)上的屬性。

class Foo {
}

Foo.prop = 1;
Foo.prop // 1

上面的寫法爲Foo類定義了一個靜態屬性prop。

目前,只有這種寫法可行,因爲 ES6 明確規定,【Class 內部只有靜態方法,沒有靜態屬性】

// 以下兩種寫法都無效
class Foo {
  // 寫法一
  prop: 2

  // 寫法二
  static prop: 2
}

Foo.prop // undefined

13. new.target屬性

new是從構造函數生成實例的命令。ES6 爲new命令引入了一個new.target屬性,該屬性一般用在在構造函數之中,返回new命令作用於的那個構造函數。【如果構造函數不是通過new命令調用的,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, '張三');  // 報錯

上面代碼確保構造函數只能通過new命令調用。

Class 內部調用new.target,返回當前 Class。

class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
    this.length = length;
    this.width = width;
  }
}

var obj = new Rectangle(3, 4); //// 輸出 true

【注意】子類繼承父類時,new.target會返回子類

class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
    // ...
  }
}

class Square extends Rectangle {
  constructor(length) {
    super(length, length);
  }
}

var obj = new Square(3); // 輸出 false

上面代碼中,new.target會返回子類。

利用這個特點,可以寫出【不能獨立使用、必須繼承後才能使用的類】。

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會報錯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章