深刻理解 JavaScript 中的 class

在 ES6 規範中,引入了 class 的概念。使得 JS 開發者終於告別了,直接使用原型對象模仿面向對象中的類和類繼承時代。javascript

可是JS 中並無一個真正的 class 原始類型, class 僅僅只是對原型對象運用語法糖。因此,只有理解如何使用原型對象實現類和類繼承,才能真正地用好 classjava

ES6:class

經過類來建立對象,使得開發者沒必要寫重複的代碼,以達到代碼複用的目的。它基於的邏輯是,兩個或多個對象的結構功能相似,能夠抽象出一個模板,依照模板複製出多個類似的對象。就像自行車製造商一遍一遍地複用相同的藍圖來製造大量的自行車。git

使用 ES6 中的 class 聲明一個類,是很是簡單的事。它的語法以下:github

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

  hello(){
    console.log('Hello, my name is ' + this.name + '.');
  }
}

var xiaoMing = new Person('xiaoMing');
xiaoMing.hello() // Hello, my name is xiaoMing.

xiaoMing 是經過類 Person 實例化出來的對象。對象 xiaoMing 是按照類 Person 這個模板,實例化出來的對象。實例化出來的對象擁有類預先訂製好的結構和功能。segmentfault

ES6 的語法很簡單,可是在實例化的背後,到底是什麼在起做用呢?ide

class 實例化的背後原理

使用 class 的語法,讓開發者告別了使用 prototype 模仿面向對象的時代。可是,class 並非 ES6 引入的全新概念,它的原理依舊是原型繼承。函數

typeof class == "function"

經過類型判斷,咱們能夠得知,class 的並非什麼全新的數據類型,它實際只是 function (或者說 object)。this

class Person {
  // ...
}

typeof Person // function

爲了更加直觀地瞭解 Person 的實質,能夠將它在控制檯打印出來,以下。spa

圖片描述

Person 的屬性並很少,除去用 [[...]] 包起來的內置屬性外,大部分屬性根據名字就能明白它的做用。須要咱們重點關注的是 prototype__proto__ 兩個屬性。prototype

(關於 __proto__ 能夠在本文的姊妹篇 找到答案)

實例化的原理: prototype

先來說講 prototype 屬性,它指向一個特殊性對象:原型對象

原型對象因此特殊,是由於它擁有一個普通對象沒有的能力:將它的屬性共享給其餘對象。

在 ES6 規範 中,對 原型對象 是以下定義的:

object that provides shared properties for other objects

原型對象是如何將它的屬性分享給其餘對象的呢?

這裏使用 ES5 建立一個類,並將它實例化,來看看它的實質。

function Person() {
  this.name = name
}

// 1. 首先給 Person.prototype 原型對象添加了 describe 方法 。
Person.prototype.describe = function(){
  console.log('Hello, my name is ' + this.name + '.');
}

// 2. 實例化對象的 __proto__ 指向 Person.prototype
var jane = new Person('jane');
jane.__proto__ === Person.prototype;


// 3. 讀取 describe 方法時,實際會沿着原型鏈查找到 Person.prototype 原型對象上。
jane.describe() // Hello, my name is jane.

上述使用 JS 模仿面向對象實例化的背後,實際有三個步驟:

  1. 首先給 Person.prototype 屬性所指的原型對象上添加了一個方法 describe

  2. 在使用 new 關鍵字建立對象時,會默認給該對象添加一個原型屬性 __proto__,該屬性指向 Person.prototype 原型對象。

  3. 在讀取 describe 方法時,首先會在 jane 對象查找該方法,可是 jane 對象並不直接擁有 describe 方法。因此會沿着原型鏈查找到 Person.prototype 原型對象上,最後返回該原型對象的 describe 方法。

JS 中面向對象實例化的背後原理,實際上就是 原型對象

爲了方便你們理解,從網上扒了一張的圖片,放到這來便於你們理解。

圖片描述

prototype__proto__ 區別

理解上述原理後,還須要注意 prototype__proto__ 屬性的區別。

__proto__ 所指的對象,真正將它的屬性分享給它所屬的對象。全部的對象都有 __proto__ 屬性,它是一個內置屬性,被用於繼承。

prototype 是一個只屬於 function 的屬性。當使用 new 方法調用該構造函數的時候,它被用於構建新對象的 __proto__。另外它不可寫,不可枚舉,不可配置。

( new Foo() ).__proto__ === Foo.prototype
( new Foo() ).prototype === undefined

class 定義屬性

當咱們使用 class 定義屬性(方法)的時候,實際上等因而在 class 的原型對象上定義屬性。

class Foo {
  constructor(){ /* constructor */  }

  describe(){ /* describe */  }
}

// 等價於
function Foo (){
  /* constructor */
}

Foo.prototype.describe = function(){  /* describe */  }

constructor 是一個比較特殊的屬性,它指向構造函數(類)自己。能夠經過如下代碼驗證。

Foo.prototype.constructor === Foo // true

類繼承

在傳統面向對象中,類是能夠繼承類的。這樣子類就能夠複製父類的方法,達到代碼複用的目的。

ES6 也提供了類繼承的語法 extends,以下:

class Foo {
  constructor(who){
    this.me = who;
  }

  identify(){
    return "I am " + this.me;
  }
}


class Bar extends Foo {
  constructor(who){
    // super() 指的是調用父類
    // 調用的同時,會綁定 this 。
    // 如:Foo.call(this, who)
    super(who);
  }

  speak(){
    alert( "Hello, " + this.identify() + "." );
  }
}

var b1 = new Bar( "b1" );

b1.speak();

當實例 b1 調用 speak 方法時,b1 自己沒有 speak,因此會到 Bar.prototype 原型對象上查找,而且調用原型對象上的 speak 方法。調用 identify 方式時,因爲 this 指向的是 b1 對象。因此也會先在 b1 自己查找,而後沿着原型鏈,查找 Bar.prototype,最後在 Foo.prototype 原型對象上找到 identify 方法,而後調用。

實際上,在 JavaScript 中,類繼承的本質依舊是原型對象。

他們的關係以下圖所示:

圖片描述

參考文章

相關文章
相關標籤/搜索