一、原型物件的概述
通過建構函式為例項物件定義屬性,雖然很方便,但是有一個缺點。同一個建構函式的多個例項之間,無法共享屬性,從而造成對系統資源的浪費。
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
同時繼承了父類M1
和M2
。這種模式又稱為 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
物件輸出到全域性,對外暴露init
和destroy
介面,內部方法go
、handleEvents
、initialize
、dieCarouselDie
都是外部無法呼叫的。