JavaScript 複習之 物件的繼承

DreamTruth發表於2019-02-27

一、原型物件的概述

通過建構函式為例項物件定義屬性,雖然很方便,但是有一個缺點。同一個建構函式的多個例項之間,無法共享屬性,從而造成對系統資源的浪費。

function Cat(name, color) {
  this.name = name;
  this.color = color;
  this.meow = function () {
    console.log('喵喵');
  };
}

var cat1 = new Cat('大毛', '白色');
var cat2 = new Cat('二毛', '黑色');

cat1.meow === cat2.meow
// false
複製程式碼

上面程式碼中,每新建一個例項,就會新建一個meow方法。這既沒有必要,又浪費系統資源。

這個問題的解決方法,就是js 中的原型物件(prototype)。

繼承機制的設計思想就是,原型物件的所有屬性和方法,都能被例項物件共享。

js 中規定,每個函式都有一個prototype屬性,指向一個物件。

原型物件的屬性不是例項物件自身的屬性。只要修改原型物件,變動就立刻體現在所有例項物件上。

如果例項物件自身就有某個屬性或方法,他就不會再去原型物件尋找這個屬性或方法。

原型鏈

js中規定,所有的物件都有自己的原型物件。一方面,每個物件都可以充當其他物件的原型;另一方面,由於原型物件也是物件,所以他也有自己的原型。因此,就會形成一個“原型鏈”:物件到原型,再到原型的原型...

原型鏈的盡頭就是null

讀取物件的某個屬性時,JavaScript 引擎先尋找物件本身的屬性,如果找不到,就到它的原型去找,如果還是找不到,就到原型的原型去找。如果直到最頂層的Object.prototype還是找不到,則返回undefined。如果物件自身和它的原型,都定義了一個同名屬性,那麼優先讀取物件自身的屬性,這叫做“覆蓋”(overriding)。

constructor 屬性

prototype物件有一個constructor屬性,預設指向prototype物件所在的建構函式。

二、instanceof 運算子

返回一個布林值,表示物件是否為某個建構函式的例項。

instanceof運算子左邊是例項物件,右邊是建構函式。他會檢查右邊構建函式的原型物件,是否在左邊物件的原型臉上。所以,下面寫法是等價的

var v = new Vehicle();
v instanceof Vehicle // true
//等同於
Vehicle.prototype.isPrototypeOf(v)

複製程式碼

由於instanceof檢查整個原型鏈,因此一個例項物件,可能會對多個建構函式都返回true

var d = new Date();
d instanceof Date // true
d instanceof Object // true
複製程式碼

instanceof的原理是檢查右邊建構函式的prototype屬性,是否在左邊物件的原型鏈上。

注意:有一種特殊情況,就是左邊物件的原型鏈上,只有null物件,這時instanceof判斷會失真。

注意instanceof只能用於物件,不適用原始型別的值。

undefined instanceof Object // false
null instanceof Object // false
複製程式碼

三、建構函式的繼承

讓一個建構函式繼承另一個建構函式。分兩步實現:

第一步在子類的建構函式中,呼叫父類的建構函式。

function Sub(){
    Super.call(this);
    this.prop = value;
}
複製程式碼

上面程式碼中,Sub是子類的建構函式,this是子類的例項。在例項上呼叫父類的建構函式Super,就會讓子類例項具有父類例項的屬性。

第二步,讓子類的原型指向父類的原型,這樣字類就可以繼承父類原型。

Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.prototype.method = '...';
複製程式碼

上面程式碼,Sub.prototype是子類原型,將它賦值為Object.create(Super.prototype),而不是直接等於Super.prototype。否則後面兩行對Sub.prototype的操作,會連父類的原型Super.prototype一起修改掉。

下面寫一個完整的建構函式繼承:

function Shape(){
    this.x = 0;
    this.y = 0;
}

Shape.prototype.move = function(x,y){
    this.x += x;
    this.y += y;
    console,log('Shape moved');
}

//第一步,讓子類繼承父類例項
function Rectangle(){
    Super.call(this);//呼叫父類建構函式
}
//第二步,子類繼承父類原型
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
複製程式碼

採用這樣的寫法以後,instanceof運算子會對子類和父類的建構函式,都返回true

var rect = new Rectangle();

rect instanceof Rectangle  // true
rect instanceof Shape  // true
複製程式碼

四、多重繼承

JavaScript 不提供多重繼承功能,即不允許一個物件同時繼承多個物件。但是,可以通過變通方法,實現這個功能。

function M1() {
  this.hello = 'hello';
}

function M2() {
  this.world = 'world';
}

function S() {
  M1.call(this);
  M2.call(this);
}

// 繼承 M1
S.prototype = Object.create(M1.prototype);
// 繼承鏈上加入 M2
Object.assign(S.prototype, M2.prototype);

// 指定建構函式
S.prototype.constructor = S;

var s = new S();
s.hello // 'hello'
s.world // 'world'
複製程式碼

上面程式碼中,子類S同時繼承了父類M1M2。這種模式又稱為 Mixin(混入)。

五、模組

傳統的做法,利用物件實現模組效果。

基本實現方法

簡單的做法是把模組寫成一個物件,所有的模組成員都放到這個物件裡面。

var module1 = new Object({
 _count : 0,
 m1 : function (){
  //...
 },
 m2 : function (){
   //...
 }
});
複製程式碼

上面的函式m1和m2,都封裝在module1物件裡。使用的時候,就是呼叫這個物件的屬性。

但是,這樣的寫法會暴露所有模組成員,內部狀態可以被外部改寫。比如,外部程式碼可以直接改變內部計數器的值。

封裝私有變數:建構函式的寫法

我們可以利用建構函式,封裝私有變數。

function StringBuilder() {
  var buffer = [];

  this.add = function (str) {
     buffer.push(str);
  };

  this.toString = function () {
    return buffer.join('');
  };

}
複製程式碼

上面程式碼中,buffer是模組的私有變數。一旦生成例項物件,外部是無法直接訪問buffer的。但是,這種方法將私有變數封裝在建構函式中,導致建構函式與例項物件是一體的,總是存在於記憶體之中,無法在使用完成後清除。這意味著,建構函式有雙重作用,既用來塑造例項物件,又用來儲存例項物件的資料,違背了建構函式與例項物件在資料上相分離的原則(即例項物件的資料,不應該儲存在例項物件以外)。同時,非常耗費記憶體。

封裝私有變數:立即執行函式的寫法

var module1 = (function () {
 var _count = 0;
 var m1 = function () {
   //...
 };
 var m2 = function () {
  //...
 };
 return {
  m1 : m1,
  m2 : m2
 };
})();
複製程式碼

使用上面的寫法,外部程式碼無法讀取內部的_count變數。

模組的放大模式

如果一個模組很大,必須分成幾個部分,或者一個模組需要繼承另一個模組,這時就有必要採用“放大模式”(augmentation)。

var module1 = (function (mod){
 mod.m3 = function () {
  //...
 };
 return mod;
})(module1);
複製程式碼

上面的程式碼為module1模組新增了一個新方法m3(),然後返回新的module1模組。

在瀏覽器環境中,模組的各個部分通常都是從網上獲取的,有時無法知道哪個部分會先載入。如果採用上面的寫法,第一個執行的部分有可能載入一個不存在空物件,這時就要採用"寬放大模式"(Loose augmentation)。

var module1 = (function (mod) {
 //...
 return mod;
})(window.module1 || {});
複製程式碼

與"放大模式"相比,“寬放大模式”就是“立即執行函式”的引數可以是空物件。

輸入全域性變數

獨立性是模組的重要特點,模組內部最好不與程式的其他部分直接互動。

為了在模組內部呼叫全域性變數,必須顯式地將其他變數輸入模組。

var module1 = (function ($, YAHOO) {
 //...
})(jQuery, YAHOO);
複製程式碼

上面的module1模組需要使用 jQuery 庫和 YUI 庫,就把這兩個庫(其實是兩個模組)當作引數輸入module1。這樣做除了保證模組的獨立性,還使得模組之間的依賴關係變得明顯。

立即執行函式還可以起到名稱空間的作用。

(function($, window, document) {

  function go(num) {
  }

  function handleEvents() {
  }

  function initialize() {
  }

  function dieCarouselDie() {
  }

  //attach to the global scope
  window.finalCarousel = {
    init : initialize,
    destroy : dieCarouselDie
  }

})( jQuery, window, document );
複製程式碼

上面程式碼中,finalCarousel物件輸出到全域性,對外暴露initdestroy介面,內部方法gohandleEventsinitializedieCarouselDie都是外部無法呼叫的。

相關文章