寫久了ES6的class、extends,你是否還記得在ES5中如何實現類與類的繼承

面向對象程序設計

面向對象程序設計(Object Orientend Programming, OOP) 是一種計算機編程範式,經過儘量的模擬人類的思惟方式,使得軟件的開發方法與過程儘量接近人類認識世界,解決現實問題的方法和過程。其主要目標是重用、靈活性和擴展型。 OOP = 對象 + 類 + 多態 + 消息,其核心是類與對象。javascript

類(class) 是對現實世界的抽象,包括表示靜態屬性的數據和對數據的操做,對象(Object 則是類的實例化。java

面向對象程序設計具備封裝性、繼承性、多態性三大特色。封裝性 是指將計算機系統中的數據以及對數據的操做組裝到一塊兒,一併封裝到一個有機的實體中去,也就是一個類中。繼承性 後者延續前者的特色,複用前者的數據和對數據的操做方法。多態性 即多個對象接收到同一個徹底相同的消息以後,所表現出來的動做各不相同,具備多種形態。編程

ES6中的類與繼承

看完上面枯燥無味的概念,咱們回到Javascript中,在E6裏,實現類只需用Class關鍵字便可,實現繼承也只須要用extends關鍵子字便可,看起來和其餘OOP語言(Java,C++)的寫法相似,但內部的實現機理卻徹底不同。markdown

class Person {
  constructor(name) { this.name = name }
  say() { console.log(this.name) }
}

class Student extends Person { 
  constructor(name, id) {
    super(name);
    this.id = id;
  }
  say1() { console.log(this.name, this.id) }
}
複製代碼

ES6中雖然提供了類的語法,但本質仍是基於函數模擬去實現類的,繼承也是基於原型鏈函數

console.log(typeof Person); // function
console.log(Person.prototype.say); // [Function: say]
console.log(Student.prototype.__proto__); // Student {}
複製代碼

ES5中類的實現

前面提到ES6雖然提供了類語法,但本質仍是基於函數模擬和原型鏈來實現類與類的繼承,那麼還記得ES5的JavaScript是如何去實現類嘛?優化

咱們都知道,經過類能夠建立任意多個具備相同屬性的方法和對象,但ES5中並無類的概念。那麼既然是建立對象,就必定須要類嘛?不必定,好比咱們常常寫的一類函數,傳入參數返回一個對象,這也被稱爲工廠模式ui

function createPerson(name) { 
  return {
     name: name,
     say: function() { console.log(this.name) } 
  }
}
複製代碼

固然爲了更符合OOP的編程習慣(new),有了下面的構造函數模式this

function Person(name) {
  this.name = name;				// 屬性
  this.say = function() { // 方法
  	console.log(this.name)
  }
}
複製代碼

經過new關鍵字調用產生一個Person實例。而在new的過程當中,會建立一個新對象,將構造函數的做用域賦給新對象,而且this指向了這個新對象。執行構造函數中的代碼,爲這個對象添加屬性,而後返回新對象。值得注意的是,若是直接調用這個函數的話,this會指向global,至關於給global添加屬性和方法。spa

然而細心的讀者可能發現了這種模式的問題,即每個方法都須要在每一個實例上從新建立一遍,體現不出來類實例共享數據操做的優點。prototype

因爲JavaScript是一門基於原型的語言,建立的每個對象都有一個prototype(原型屬性),這個屬性是一個指向某一個對象的指針,而這個對象的用途是包含能夠由特定類型的全部實例共享的屬性和方法。

function Person() {}
Person.prototype.name = "sundial-dreams";
Person.prototype.say = function() { console.log(this.name) }
複製代碼

當咱們建立一個函數時,會爲這個函數建立一個prototype屬性,這個屬性指向函數的原型對象,而且原型對象會自動得到一個constructor屬性(不可枚舉),該屬性指向建立的函數自己。

console.log(Person.prototype); // Person { name: 'sundial-dreams', say: [Function] }
console.log(Person.prototype.contructor); // [Function: Person]
console.log(new Person().__proto__); // Person { name: 'sundial-dreams', say: [Function] }
複製代碼

基於原型的特色,咱們能夠將類方法放到原型中去實現,類的屬性放到構造函數中去實現,所以有了下面這種組合模式(構造函數+原型)

function Person(name) {
  this.name = name;
}
Person.prototype.say = function () { console.log(this.name) }
複製代碼

這也是也是咱們在ES5中最經常使用的一種實現類的方式。

ES5中的繼承

說完ES5中類實現,結下來再來分析ES5中繼承的實現。繼承的本質是代碼複用,複用前者的屬性和方法,許多OOP語言都支持接口繼承(implements)和實現繼承(extends),而JavaScript只支持實現繼承,並且仍是依靠原型鏈去實現。

還記得對象是怎麼查找本身的屬性的嘛,先本身內部找,沒有在去原型對象上找,尚未就去原型對象的原型對象上找(一直套娃),直到原型對象爲null時還沒找到,則返回個undefined。另外咱們也知道函數的默認原型都是Object,其實找到最遠也就是Object了。下面是按照鏈表的方式遍歷每個原型對象,遍歷到最後面其實就是Object了。

var p = new Person(), t = p.__proto__;
while (t) {
    console.log(t.constructor)
    t =  t.__proto__;
}
// [Function: Person]
// [Function: Object]
複製代碼

下面這個例子也許更能解釋清楚原型鏈

Object.prototype._name = "sundial-dreams";
var p = new Person("dpf");
console.log(p._name); // sundial-dreams 等價於p.__proto__.__proto__._name
複製代碼

補充了點原型鏈的知識,先來看看簡單的繼承如何實現,既然繼承就是複用前者的屬性和方法,那麼能夠利用JavaScript函數的特色,經過call來給後者的this添加前者的屬性,也就是借用構造函數模式

function Person(name) {
    this.name = name;
}

Person.prototype.say = function () {
    console.log(this.name);
}

function Student(name, id) {
    Person.call(this, name); // 給this添加Person裏的屬性
    this.id = id;
}

Student.prototype.say1 = function () {
    console.log(this.name, this.id)
}
複製代碼

但讀者也已經發現了問題,這種繼承方式只把屬性給拿過來,方法還沒拿過來呢,這能配叫繼承嘛。

因此既然要繼承方法,那還不簡單,利用原型鏈的知識,只要把Student的原型對象指向Person實例化的對象就能夠了,也就是組合繼承方式

function Student(name, id) {
    Person.call(this, name); // 給this添加Person裏的屬性
    this.id = id;
}
Student.prototype = new Person(); // 原型指向Person的實例,所以能有Person的屬性和方法
Student.prototype.constructor = Student; // 別忘了修正構造函數
複製代碼

但咱們只想繼承父類的方法卻每次都要實例化出父類的對象,而且還向原型中添加了父類的屬性,感受有點冗餘,所以出現了下面這種原型式繼承。利用箇中間函數,剔除掉Person中的屬性,只保留它的方法。

function F() {} // 中間函數
F.prototype = Person.prototype; // 剔除掉Person中的屬性,保留函數定義的部分
Student.prototype = new F();    // 所以new F只包含Person的方法
Student.prototype.constructor = Student; // 修正構造函數
複製代碼

還記得Object.create函數嘛,他能夠接受兩個參數,第一個參數爲原型,第二個參數爲而外的新增屬性。所以上面的寫法能夠轉化爲下面這種寫法。

Student.prototype = Object.create(Person, { constructor: Student })
複製代碼

換個角度想,不就是想複用父類原型上的的方法嘛,那直接利用子類原型的__proto__屬性,讓他指向Person.prototype不就能夠了嘛。按照上面講的對象屬性搜索規則,方法能夠一直沿着原型鏈搜索到。暫且將這種方式稱爲優化版的原型式繼承

Student.prototype.__proto__ = Person.prototype; // 這塊沒改Student.prototype.constructor,因此不須要修正它
複製代碼

固然若是以爲這麼寫可讀性以爲差的話,還能夠利用Object.setPrototypeOf來替代,好比

Object.setPrototypeOf(Student.prototype, Person.prototype); // 這塊也沒改Student.prototype.constructor,因此不須要修正它
複製代碼

固然,若是咱們想繼承靜態屬性或方法時,好比Person有一個靜態方法

Person.hello = function () {
  console.log("hello world");
}
複製代碼

繼承時其實能夠繼續使用Object.setPrototypeOf

Object.setPrototypeOf(Student, Person);
複製代碼

不管是哪一種方式,本質上仍是對原型鏈進行操做,因此只要對原型鏈理解深入,對繼承的本質有必定的理解,利用原型鏈寫繼承也不是什麼難事。

總結

本文簡單的扯了扯ES5中類的實現以及繼承的實現,因爲本身學的第一門OOP語言是C++,因此最開始接觸到JavaScript的類與繼承時其實理解了很是久,才慢慢的轉化了過來,慢慢的接受了JavaScript的基於原型的設計思想。ES6的Class語法用起來確實舒服,但也別忘了類在JavaScript中最基礎的實現。

相關文章
相關標籤/搜索