面向對象編程很重要的一個方面冠骄,就是對象的繼承。A 對象通過繼承 B 對象加袋,就能直接擁有 B 對象的所有屬性和方法凛辣。這對于代碼的復用是非常有用的。
大部分面向對象的編程語言职烧,都是通過“類”(class)來實現(xiàn)對象的繼承扁誓。JavaScript 語言的繼承則是通過“原型對象”(prototype)
1.原型對象概述
1.1 構造函數(shù)的缺點
JavaScript 通過構造函數(shù)生成新對象,因此構造函數(shù)可以視為對象的模板蚀之。實例對象的屬性和方法蝗敢,可以定義在構造函數(shù)內部。
function Cat (name, color) {
this.name = name;
this.color = color;
}
var cat1 = new Cat('大毛', '白色');
cat1.name // '大毛'
cat1.color // '白色'
上面代碼中足删,Cat
函數(shù)是一個構造函數(shù)寿谴,函數(shù)內部定義了name
屬性和color
屬性,所有實例對象(上例是cat1
)都會生成這兩個屬性失受,即這兩個屬性會定義在實例對象上面讶泰。
通過構造函數(shù)為實例對象定義屬性,雖然很方便拂到,但是有一個缺點痪署。同一個構造函數(shù)的多個實例之間,無法共享屬性兄旬,從而造成對系統(tǒng)資源的浪費狼犯。
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
上面代碼中,cat1
和cat2
是同一個構造函數(shù)的兩個實例,它們都具有meow
方法辜王。由于meow
方法是生成在每個實例對象上面劈狐,所以兩個實例就生成了兩次。也就是說呐馆,每新建一個實例肥缔,就會新建一個meow
方法。這既沒有必要汹来,又浪費系統(tǒng)資源续膳,因為所有meow
方法都是同樣的行為,完全應該共享收班。
這個問題的解決方法坟岔,就是 JavaScript 的原型對象(prototype)。
1.2 prototype 屬性的作用
JavaScript 繼承機制的設計思想就是摔桦,原型對象的所有屬性和方法社付,都能被實例對象共享。也就是說邻耕,如果屬性和方法定義在原型上鸥咖,那么所有實例對象就能共享,不僅節(jié)省了內存兄世,還體現(xiàn)了實例對象之間的聯(lián)系啼辣。
下面,先看怎么為對象指定原型御滩。JavaScript 規(guī)定鸥拧,每個函數(shù)都有一個prototype
屬性,指向一個對象削解。
function f() {}
typeof f.prototype // "object"
上面代碼中富弦,函數(shù)f
默認具有prototype
屬性,指向一個對象钠绍。
對于普通函數(shù)來說舆声,該屬性基本無用。但是柳爽,對于構造函數(shù)來說,生成實例的時候碱屁,該屬性會自動成為實例對象的原型磷脯。
function Animal(name) {
this.name = name;
}
Animal.prototype.color = 'white';
var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');
cat1.color // 'white'
cat2.color // 'white'
上面代碼中,構造函數(shù)Animal
的prototype
屬性娩脾,就是實例對象cat1
和cat2
的原型對象赵誓。原型對象上添加一個color
屬性,結果,實例對象都共享了該屬性俩功。
原型對象的屬性不是實例對象自身的屬性幻枉。只要修改原型對象,變動就立刻會體現(xiàn)在所有實例對象上诡蜓。
Animal.prototype.color = 'yellow';
cat1.color // "yellow"
cat2.color // "yellow"
上面代碼中熬甫,原型對象的color
屬性的值變?yōu)?code>yellow,兩個實例對象的color
屬性立刻跟著變了蔓罚。這是因為實例對象其實沒有color
屬性椿肩,都是讀取原型對象的color
屬性。也就是說豺谈,當實例對象本身沒有某個屬性或方法的時候郑象,它會到原型對象去尋找該屬性或方法。這就是原型對象的特殊之處茬末。
如果實例對象自身就有某個屬性或方法厂榛,它就不會再去原型對象尋找這個屬性或方法。
cat1.color = 'black';
cat1.color // 'black'
cat2.color // 'yellow'
Animal.prototype.color // 'yellow';
上面代碼中丽惭,實例對象cat1
的color
屬性改為black
击奶,就使得它不再去原型對象讀取color
屬性,后者的值依然為yellow
吐根。
總結一下正歼,原型對象的作用,就是定義所有實例對象共享的屬性和方法拷橘。這也是它被稱為原型對象的原因局义,而實例對象可以視作從原型對象衍生出來的子對象。
Animal.prototype.walk = function () {
console.log(this.name + ' is walking');
};
上面代碼中冗疮,Animal.prototype
對象上面定義了一個walk
方法萄唇,這個方法將可以在所有Animal
實例對象上面調用。
(4)綁定回調函數(shù)的對象
前面的按鈕點擊事件的例子术幔,可以改寫如下另萤。
var o = new Object();
o.f = function () {
console.log(this === o);
}
var f = function (){
o.f.apply(o);
// 或者 o.f.call(o);
};
// jQuery 的寫法
$('#button').on('click', f);
上面代碼中,點擊按鈕以后诅挑,控制臺將會顯示true
四敞。由于apply
方法(或者call
方法)不僅綁定函數(shù)執(zhí)行時所在的對象,還會立即執(zhí)行函數(shù)拔妥,因此不得不把綁定語句寫在一個函數(shù)體內忿危。更簡潔的寫法是采用下面介紹的bind
方法。
4.3 Function.prototype.bind()
bind方法用于將函數(shù)體內的this綁定到某個對象没龙,然后返回一個新函數(shù)铺厨。
var d = new Date();
d.getTime() // 1481869925657
var print = d.getTime;
print() // Uncaught TypeError: this is not a Date object.
面代碼中缎玫,我們將d.getTime
方法賦給變量print
,然后調用print
就報錯了解滓。這是因為getTime
方法內部的this
赃磨,綁定Date
對象的實例,賦給變量print
以后洼裤,內部的this
已經不指向Date
對象的實例了邻辉。
bind
方法可以解決這個問題。
var print = d.getTime.bind(d);
print() // 1481869925657
bind
方法的參數(shù)就是所要綁定this
的對象逸邦,下面是一個更清晰的例子恩沛。
var counter = {
count: 0,
inc: function () {
this.count++;
}
};
var func = counter.inc.bind(counter);
func();
counter.count // 1
上面代碼中,counter.inc
方法被賦值給變量func
缕减。這時必須用bind
方法將inc
內部的this
雷客,綁定到counter
,否則就會出錯桥狡。
this
綁定到其他對象也是可以的搅裙。
var counter = {
count: 0,
inc: function () {
this.count++;
}
};
var obj = {
count: 100
};
var func = counter.inc.bind(obj);
func();
obj.count // 101
上面代碼中,bind
方法將inc
方法內部的this
裹芝,綁定到obj
對象部逮。結果調用func
函數(shù)以后,遞增的就是obj
內部的count
屬性嫂易。
bind
還可以接受更多的參數(shù)兄朋,將這些參數(shù)綁定原函數(shù)的參數(shù)。
var add = function (x, y) {
return x * this.m + y * this.n;
}
var obj = {
m: 2,
n: 2
};
var newAdd = add.bind(obj, 5);
newAdd(5) // 20
上面代碼中怜械,bind
方法除了綁定this
對象颅和,還將add
函數(shù)的第一個參數(shù)x
綁定成5
,然后返回一個新函數(shù)newAdd
缕允,這個函數(shù)只要再接受一個參數(shù)y
就能運行了峡扩。
如果bind
方法的第一個參數(shù)是null
或undefined
,等于將this
綁定到全局對象障本,函數(shù)運行時this
指向頂層對象(瀏覽器為window
)教届。
function add(x, y) {
return x + y;
}
var plus5 = add.bind(null, 5);
plus5(10) // 15
注意:
上面代碼中,函數(shù)add
內部并沒有this
驾霜,使用bind
方法的主要目的是綁定參數(shù)x
案训,以后每次運行新函數(shù)plus5
,就只需要提供另一個參數(shù)y
就夠了粪糙。而且因為add
內部沒有this
萤衰,所以bind
的第一個參數(shù)是null
,不過這里如果是其他對象猜旬,也沒有影響脆栋。
bind
方法有一些使用注意點。
(1)每一次返回一個新函數(shù)
bind
方法每運行一次洒擦,就返回一個新函數(shù)椿争,這會產生一些問題。比如熟嫩,監(jiān)聽事件的時候秦踪,不能寫成下面這樣。
element.addEventListener('click', o.m.bind(o));
上面代碼中掸茅,click
事件綁定bind
方法生成的一個匿名函數(shù)椅邓。這樣會導致無法取消綁定,所以昧狮,下面的代碼是無效的景馁。
element.removeEventListener('click', o.m.bind(o));
正確的方法是寫成下面這樣:
var listener = o.m.bind(o);
element.addEventListener('click', listener);
// ...
element.removeEventListener('click', listener);
(2)結合回調函數(shù)使用
回調函數(shù)是 JavaScript 最常用的模式之一,但是一個常見的錯誤是逗鸣,將包含this
的方法直接當作回調函數(shù)合住。解決方法就是使用bind
方法,將counter.inc
綁定counter
撒璧。
var counter = {
count: 0,
inc: function () {
'use strict';
this.count++;
}
};
function callIt(callback) {
callback();
}
callIt(counter.inc.bind(counter));
counter.count // 1
上面代碼中透葛,callIt
方法會調用回調函數(shù)。這時如果直接把counter.inc
傳入卿樱,調用時counter.inc
內部的this
就會指向全局對象僚害。使用bind
方法將counter.inc
綁定counter
以后,就不會有這個問題繁调,this
總是指向counter
萨蚕。
還有一種情況比較隱蔽,就是某些數(shù)組方法可以接受一個函數(shù)當作參數(shù)涉馁。這些函數(shù)內部的this
指向门岔,很可能也會出錯。
var obj = {
name: '張三',
times: [1, 2, 3],
print: function () {
this.times.forEach(function (n) {
console.log(this.name);
});
}
};
obj.print()
// 沒有任何輸出
上面代碼中烤送,obj.print
內部this.times
的this
是指向obj
的寒随,這個沒有問題。但是帮坚,forEach
方法的回調函數(shù)內部的this.name
卻是指向全局對象妻往,導致沒有辦法取到值。稍微改動一下,就可以看得更清楚冰评。
obj.print = function () {
this.times.forEach(function (n) {
console.log(this === window);
});
};
obj.print()
// true
// true
// true
解決這個問題嗦枢,也是通過bind
方法綁定this
。
obj.print = function () {
this.times.forEach(function (n) {
console.log(this.name);
}.bind(this));
};
obj.print()
// 張三
// 張三
// 張三
(3)結合call方法使用
利用bind
方法好渠,可以改寫一些 JavaScript 原生方法的使用形式昨稼,以數(shù)組的slice
方法為例。
[1, 2, 3].slice(0, 1) // [1]
// 等同于
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]
上面的代碼中拳锚,數(shù)組的slice
方法從[1, 2, 3]
里面假栓,按照指定位置和長度切分出另一個數(shù)組。這樣做的本質是在[1, 2, 3]
上面調用Array.prototype.slice
方法霍掺,因此可以用call
方法表達這個過程匾荆,得到同樣的結果。
call
方法實質上是調用Function.prototype.call
方法杆烁,因此上面的表達式可以用bind
方法改寫牙丽。
var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]
上面代碼的含義就是,將Array.prototype.slice
變成Function.prototype.call
方法所在的對象兔魂,調用時就變成了Array.prototype.slice.call
烤芦。類似的寫法還可以用于其他數(shù)組方法。
var push = Function.prototype.call.bind(Array.prototype.push);
var pop = Function.prototype.call.bind(Array.prototype.pop);
var a = [1 ,2 ,3];
push(a, 4)
a // [1, 2, 3, 4]
pop(a)
a // [1, 2, 3]
如果再進一步入热,將Function.prototype.call
方法綁定到Function.prototype.bind
對象拍棕,就意味著bind
的調用形式也可以被改寫。
function f() {
console.log(this.v);
}
var o = { v: 123 };
var bind = Function.prototype.call.bind(Function.prototype.bind);
bind(f, o)() // 123
上面代碼的含義就是勺良,將Function.prototype.bind
方法綁定在Function.prototype.call
上面绰播,所以bind
方法就可以直接使用,不需要在函數(shù)實例上使用尚困。