原型模式——語言之魂
原型模式:用原型實(shí)例指向創(chuàng)建對象的類,使用于創(chuàng)建新的對象的類共享原型對象的屬性以及方法。(當(dāng)然JavaScript是基于原型鏈實(shí)現(xiàn)對象之間的繼承,這種繼承時(shí)基于一種對屬性或者方法的共享,而不是對屬性和方法的復(fù)制)
假設(shè)需求:頁面中有很多焦點(diǎn)圖(網(wǎng)頁中很常見的一種圖片輪播,切換效果)
最好的實(shí)現(xiàn)方式是通過創(chuàng)建對象來一一實(shí)現(xiàn),于是
// 圖片輪播類
var LoopImages = function(imgArr, container){
this.imagesArray = imgArr // 輪播圖片數(shù)組
this.container = container // 輪播圖片容器
this.createImage = function(){} // 創(chuàng)建輪播圖片
this.changeImage = function(){} // 切換下一張圖片
}
- 如果頁面中有多個(gè)這類焦點(diǎn)圖,動畫也是多樣的,可能是上下切換,可能是左右切換 等等。
- 因此創(chuàng)建的輪播圖片結(jié)構(gòu)應(yīng)該是多樣化的,切換效果也應(yīng)該是多樣化的,因此我們應(yīng)該抽象出一個(gè)基類,然后讓不同特效類去繼承這個(gè)基類,然后對于差異化的需求通過誠謝這些繼承下來的屬性或者方法來解決。當(dāng)然不同的子類之間可能存在不同的結(jié)構(gòu)樣式,比如有的包含一個(gè)左右切換箭頭
實(shí)例代碼
// 上下滑動切換類
var SlideLoopImg = function(imgArr, container){
// 構(gòu)造函數(shù)繼承圖片輪播類
LoopImages.call(this, imgArr, container);
// 重寫繼承的切換下一張圖片方法
this.changeImage = function() {
console.log('SlideLoopImg changeImage function');
}
}
// 漸隱切換類
var FadeLoopImg = function(imgArr, container, arrow){
LoopImages.call(this, imgArr, container)
// 切換箭頭私有變量
this.arrow = arrow;
this.changeImage = function(){
console.log('FadeLoopImg changeImage function');
}
}
// 實(shí)例化一個(gè)漸隱切換圖片類
var fadeImg = new FadeLoopImg([
'01.jpg',
'02.jpg',
'03.jpg',
'04.jpg'
], 'slide', [
'left.jpg',
'right.jpg'
]);
fadeImg.changeImage(); // FadeLoopImg changeImage function
但是,上面的代碼不是最優(yōu)解決方案
- 將屬性和方法都寫在基類的構(gòu)造函數(shù)里會有一些問題,比如每次子類繼承都要創(chuàng)建一次父類,如果父類的構(gòu)造函數(shù)中創(chuàng)建時(shí)存在很多耗時(shí)較長的邏輯,或者每次初始化都做一些重復(fù)性的東西,性能消耗太大。
- 所以需要一種共享機(jī)制,這樣每次創(chuàng)建基類時(shí),對于每次創(chuàng)建的一些簡單而又差異的屬性我們可以放在構(gòu)造函數(shù)中。消耗資源比較大的方法放在基類的原型中,可以避免很多不必要的消耗,這也就是原型模式中的一個(gè)雛形。
- 所以原型模式就是將可復(fù)用的、可共享的、耗時(shí)大的從基類中提出來,放在原型中,子類通過組合被繼承或者寄生組合式繼承將方法和屬性繼承襲來,對于子類中需要重寫的方法進(jìn)行重寫,這樣子類創(chuàng)建的對象既有子類的屬性和方法也共享了基類的原型方法
上面的理論請結(jié)合代碼來看
// 原型模式
// 圖片輪播類
var LoopImages = function(imgArr, container){
this.imagesArray = imgArr; // 輪播圖片數(shù)組
this.container = container; // 輪播圖片容器
}
LoopImages.prototype = {
// 創(chuàng)建輪播圖片
createImage: function(){
console.log('LoopImages createImage function');
},
// 切換下一張圖片
changeImage: function(){
console.log('LoopImages changeImage function');
}
}
// 上下滑動切換類
var SlideLoopImg = function(imgArr, container){
// 構(gòu)造函數(shù)繼承圖片輪播類
LoopImages.call(this, imgArr, container);
}
SlideLoopImg.prototype = new LoopImages();
// 重寫繼承的切換下一張圖片方法
SlideLoopImg.prototype.changeImage = function(){
console.log('SlideLoopImg changeImage function');
}
// 漸隱切換類
var FadeLoopImg = function(imgArr, container, arrow){
LoopImages.call(this, imgArr, container);
// 切換箭頭私有變量
this.arrow = arrow;
}
FadeLoopImg.prototype = new LoopImages();
FadeLoopImg.prototype.changeImage = function(){
console.log('FadeLoopImg changeImage function');
}
// 測試用例
// 實(shí)例化一個(gè)漸隱切換圖片類
var fadeImg = new FadeLoopImg([
'01.jpg',
'02.jpg',
'03.jpg',
'04.jpg'
], 'slide', [
'left.jpg',
'right.jpg'
]);
console.log(fadeImg.container); // slide
fadeImg.changeImage() // FadeLoopImg changeImage function
原型的拓展
- 原型對象是一個(gè)共享的對象,那么不論是父類的實(shí)例對象或者是子類的繼承,都是對它的一個(gè)指向引用,所以原型對象才會被共享,
- 既然被共享,那么對原型對象的拓展,不論是子類或者父類的實(shí)例對象都會繼承下來
LoopImages.prototype.getImageLength = function(){
return this.imagesArray.length;
}
FadeLoopImg.prototype.getContainer = function(){
return this.container;
}
console.log(fadeImg.getImageLength()) // 4
console.log(fadeImg.getContainer()) // slide
原型繼承
- 原型模式更多的是用在堆對象的創(chuàng)建上。
- 比如創(chuàng)建一個(gè)實(shí)例對象的構(gòu)造函數(shù)比較復(fù)雜,或者耗時(shí)比較長,或者通過創(chuàng)建多個(gè)對象來實(shí)現(xiàn)。
- 此時(shí)我們最好不要用new關(guān)鍵字去復(fù)制這些基類,但可以通過對這些對象屬性或者方法進(jìn)行復(fù)制來實(shí)現(xiàn)創(chuàng)建,這是原型模式的最初思想。
- 如果涉及多個(gè)對象,我們可以通過原型模式來實(shí)現(xiàn)對新對象的創(chuàng)建,需要一個(gè)原型模式的對象復(fù)制方法
/********
* 基于已經(jīng)存在的模板對象克隆出新對象的模式
* arguments[0],arguments[1],arguments[2]: 參數(shù)1,參數(shù)2,參數(shù)3表示模板對象
* 注意,這里對模板引用類型的屬性實(shí)質(zhì)上進(jìn)行了淺復(fù)制(引用類型屬性共享),當(dāng)然根據(jù)需求
* 可以自行進(jìn)行深復(fù)制(引用類型復(fù)制)(深拷貝參考不常見的面試題)
*******/
function prototypeExtend(){
let F = function(){}; // 緩存類,為實(shí)例化返回對象臨時(shí)創(chuàng)建
let args = argumengts; // 模板對象參數(shù)序列
let len = args.length;
let i = 0
for(;i< len; i++) {
// 遍歷每個(gè)模板對象中的屬性
for(let property in args[i]){
// 將這些屬性復(fù)制到緩存類原型中
F.prototype[property] = args[i][property]
}
}
return new F()
}
var penguin = prototypeExtend({
speed: 20,
swim: function(){
console.log('游泳速度'+ this.speed)
}
},{
run: function(speed){
console.log('奔跑速度'+ speed)
}
},
{
jump: function(){
console.log('跳躍動作')
}
}
)
因?yàn)槭峭ㄟ^prototypeExtend 創(chuàng)建的是一個(gè)對象,所以無需再用new去創(chuàng)建新的實(shí)例對象,直接使用
penguin.swim(); // 游泳速度 20
penguin.run(10); // 奔跑速度 10
penguin.jump(); // 跳躍動作
原型模式實(shí)質(zhì)上是一種繼承,也是一種創(chuàng)建型模式。
- 原型模式可以讓多個(gè)對象分享同一個(gè)原型對象的屬性與方法,這也是一種繼承方式
- 不過這種繼承的實(shí)現(xiàn)是不需要創(chuàng)建的,而是將原型對象分享給那些繼承的對象
- 有時(shí)候需要讓每個(gè)繼承對象獨(dú)立擁有一份原型對象,此時(shí)我們就需要對原型對象進(jìn)行復(fù)制(prototypeExtend)
- 所以,原型對象更適合在創(chuàng)建復(fù)雜的對象時(shí),對于那些需求一直在變化而導(dǎo)致對象結(jié)構(gòu)不停地改變時(shí),將那些比較穩(wěn)定的屬性與方法共用而提取的繼承的實(shí)現(xiàn)(初始化對象時(shí)將固定的屬性和方法放在prototype原型上)