ES6 的Class的基本語法,靜態方法, 實例屬性新寫法

Class的基本語法

1.0 簡介

ES6之前, JavaScript 語言中, 生成實例對象的方法是 通過構造函數,

下面是一個例子:

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 都可以做到,新的class寫法只是讓對象原型的寫法更加清晰、更像面向對象編程的語法而已。上面的代碼用 ES6 的class改寫,就是下面這樣。

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

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

上面代碼定義了一個“類”,可以看到裏面有一個constructor方法,這就是構造方法,而this關鍵字則代表實例對象。也就是說,ES5 的構造函數Point,對應 ES6 的Point類的構造方法。

ES6的類, 完全可以看做構造函數的另一種寫法.

class Point {
  // ...
}

typeof Point // "function"
Point === Point.prototype.constructor // true

1.1 constructor 方法

constructor方法是類的默認方法,通過new命令生成對象實例時,自動調用該方法。一個類必須有constructor方法,如果沒有顯式定義,一個空的constructor方法會被默認添加。

class Point {
}

// 等同於
class Point {
  constructor() {}
}

上面代碼中,定義了一個空的類Point,JavaScript 引擎會自動爲它添加一個空的constructor方法。

constructor方法默認返回實例對象(即this),完全可以指定返回另外一個對象

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

new Foo() instanceof Foo
// false

上面代碼中,constructor函數返回一個全新的對象,結果導致實例對象不是Foo類的實例。

類必須使用new調用,否則會報錯。這是它跟普通構造函數的一個主要區別,後者不用new也可以執行。

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

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

1.2 類的實例

生成類的實例的寫法, 與ES5完全一樣, 也就是使用 new 命令.前面說過, 如果忘記加上 new, 像函數那樣調用Class, 將會報錯.

class Point {
  // ...
}

// 報錯
var point = Point(2, 3);

// 正確
var point = new Point(2, 3);

與 ES5 一樣,實例的屬性除非顯式定義在其本身即定義在this對象上),否則都是定義在原型上(即定義在class上)。

//定義類
class Point {
    constructor(x, y) {
        this.x = x; // 實例的屬性顯示定義在其本身即: this 上
        this.y = y; // 實例的屬性顯示定義在其本身即: this 上
    }

    toString() { // 沒有顯示定義在實例本身即 this上, 定義在了類 class上, 那麼這個方法就定義在了原型上
        return '(' + this.x + ', ' + this.y + ')';
    }

}

var point = new Point(2, 3);

point.toString() // (2, 3)

point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
console.log(point.__proto__ === Point.prototype); // true

與 ES5 一樣,類的所有實例共享一個原型對象。

1.3 取值函數(getter)和存值函數(setter)

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

class MyClass {
    get a() { // 取值
        return 'getter';
    }

    set a(value) { // 存值
        console.log('setter: ' + value);
    }
}

let inst = new MyClass();

inst.a = 123; // 存值 setter: 123
console.log(inst.a); // 取值  getter

上面代碼中,prop屬性有對應的存值函數和取值函數,因此賦值和讀取行爲都被自定義了。

1.4 屬性表達式

類的屬性名, 可以採用表達式寫法表示

let methodName = 'getArea';

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

  [methodName]() {
    // ...
  }
}

上面代碼中,Square類的方法名getArea,是從表達式得到的。

1.5 Class 表達式

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

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

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

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

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

如果類內部沒有用到的話, 也可以省略 Me, 也就是可以寫成下面格式

const MyClass = class {}

採用 Class 表達式,可以寫出立即執行的 Class。

let person = new class {
  constructor(name) {
    this.name = name;
  }

  sayName() {
    console.log(this.name);
  }
}('張三');

person.sayName(); // "張三"

上面代碼中,person是一個立即執行的類的實例。

1.6 注意點

  1. 嚴格模式

    類和模塊的內部,默認就是嚴格模式,所以不需要使用use strict指定運行模式。只要你的代碼寫在類或模塊之中,就只有嚴格模式可用。考慮到未來所有的代碼,其實都是運行在模塊之中,所以 ES6 實際上把整個語言升級到了嚴格模式。

  2. 不存在提升

類不存在變量提升(hoist),這一點與 ES5 完全不同。

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

上面代碼中,Foo類使用在前,定義在後,這樣會報錯,因爲 ES6 不會把類的聲明提升到代碼頭部。這種規定的原因與下文要提到的繼承有關,必須保證子類在父類之後定義。

{
  let Foo = class {};
  class Bar extends Foo {
  }
}
  1. name屬性

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

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

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

  1. Generator 方法

如果某個方法之前加上星號(*),就表示該方法是一個 Generator 函數。

class Foo {
  constructor(...args) {
    this.args = args;
  }
  * [Symbol.iterator]() {
    for (let arg of this.args) {
      yield arg;
    }
  }
}

for (let x of new Foo('hello', 'world')) {
  console.log(x);
}
// hello
// world
  1. this的指向

類的方法內部如果含有 this, 它默認指向類的實例, 但是必須非常小心, 一旦單獨使用該方法, 很可能會報錯

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

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

const logger = new Logger();
const {print, printName} = logger;
print('Gene123123') // Gene123123
printName('123123'); // TypeError: Cannot read property 'print' of undefined 第三行 this調用報錯

上面代碼中,printName方法中的this,默認指向Logger類的實例。但是,如果將這個printName 方法提取出來單獨使用,this會指向該方法運行時所在的環境(由於 class 內部是嚴格模式,所以 this 實際指向的是undefined),從而導致找不到print方法而報錯。

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

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

  // ...
}

另一種解決方法是使用箭頭函數。

lass Obj {
  constructor() {
    this.getThis = () => this;
  }
}

const myObj = new Obj();
myObj.getThis() === myObj // true

箭頭函數內部的this總是指向定義時所在的對象。上面代碼中,箭頭函數位於構造函數內部,它的定義生效的時候,是在構造函數執行的時候。這時,箭頭函數所在的運行環境,肯定是實例對象,所以this會總是指向實例對象。

還有一種解決方法是使用Proxy,獲取方法的時候,自動綁定this

function selfish (target) {
  const cache = new WeakMap();
  const handler = {
    get (target, key) {
      const value = Reflect.get(target, key);
      if (typeof value !== 'function') {
        return value;
      }
      if (!cache.has(value)) {
        cache.set(value, value.bind(target));
      }
      return cache.get(value);
    }
  };
  const proxy = new Proxy(target, handler);
  return proxy;
}

const logger = selfish(new Logger());

2.0 靜態方法

類相當於實例的yuanxing,所有在類中定義的方法, 都會被實例繼承, 如果在一個方法前, 加上 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。另外,從這個例子還可以看出,靜態方法可以與非靜態方法重名

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

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"

3.0 實例屬性的最新寫法

實例屬性出來可以定義在 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類有兩個實例屬性,一目瞭然。另外,寫起來也比較簡潔。

4.0 靜態屬性

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

class Foo {
}

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

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

目前,只有這種寫法可行,因爲 ES6 明確規定,Class 內部只有靜態方法,沒有靜態屬性。現在有一個提案提供了類的靜態屬性,寫法是在實例屬性的前面,加上static關鍵字。

class MyClass {
  static myStaticProp = 42;

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

這個新寫法大大方便了靜態屬性的表達。

// 老寫法
class Foo {
  // ...
}
Foo.prop = 1;

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

5.0 私有方法和私有屬性

  1. 現有的解決方案

    私有方法和私有屬性, 是只能在類的內部訪問的方法和屬性, 外部不能訪問, 這是常見的需求, 有利於代碼的封裝, 但ES6不提供, 只能通過 變通方法模擬實現

    • 一種方法是在命名上加以區別

      class Widget {
      
        // 公有方法
        foo (baz) {
          this._bar(baz);
        }
      
        // 私有方法
        _bar(baz) {
          return this.snaf = baz;
        }
      
        // ...
      }
      

      上面代碼中,_bar方法前面的下劃線,表示這是一個只限於內部使用的私有方法。但是,這種命名是不保險的,在類的外部,還是可以調用到這個方法

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

    class Widget {
      foo (baz) {
        bar.call(this, baz);
      }
    
      // ...
    }
    
    function bar(baz) {
      return this.snaf = baz;
    }
    

    上面代碼中,foo是公開方法,內部調用了bar.call(this, baz)。這使得bar實際上成爲了當前模塊的私有方法。

  2. 私有屬性的提案

目前,有一個提案,爲class加了私有屬性。方法是在屬性名之前,使用#表示。

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

6.0 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, '張三');  // 報錯

上面代碼確保構造函數只能通過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

總結:

上一章: ES2017的 async函數介紹, await命令及使用解釋及注意問題

下一章: ES6的Class類繼承, super關鍵字使用及注意點

交流學習添加微信(備註技術交流學習): Gene199302
在這裏插入圖片描述

該博客爲學習阮一峯 ES6入門課所做的筆記記錄, 僅用來留作筆記記錄和學習理解

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