JavaScript中的函數繼承

面向對象和基於對象

幾乎每一個開發人員都有面向對象語言(好比C++、C#、Java)的開發經驗。在傳統面向對象的語言中,有兩個很是重要的概念——類和實例。定義了一些事物公共的行爲和方法;而實例則是類的一個具體實現。咱們還知道,面向對象編程有三個重要的概念——封裝、繼承和多態。 可是在Javascript的世界中,全部的這一切特性彷佛都不存在。由於Javascript自己不是面向對象的語言,而是基於對象的語言。Javascript中全部事物都是對象,包括字符串、數組、日期,甚至是函數,請看一個有趣的實例:編程

//定義一個函數 
function add(a,b){
	add.invokeTimes++;
	return a+b;
}
//由於函數自己也是對象,在這裏咱們爲add定義一個屬性,用來記錄次函數被調用的次數
add.invokeTimes = 0;
add(1,1);
add(2,2);
console.log(add.invokeTimes);//2
複製代碼

模擬Javascript中類和繼承

在面向對象的語言中,咱們使用類來建立一個自定義對象。然而Javascript中全部事物都是對象,那麼用什麼方法來建立自定義對象呢? 在這裏咱們引入一個新概念——原型(prototype),咱們能夠簡單的把prototype看作是一個模板,新建立的自定義對象都是這個模板(prototye)的一個拷貝(實際上不是拷貝而是連接,只不過這種連接是不可見,給人的感受好像是拷貝)。 使用prototype建立自定義對象的一個例子:數組

//構造函數
function Person(name,gender){
	this.name = name;
	this.gender = gender;
}
//定義Person的原型,原型中的屬性能夠被自定義對象引用
Person.prototype = {
	getName:function(){
		return this.name;
	},
	getGender:function() {
		return this.gender;
	}
}
複製代碼

這裏咱們把函數Person稱爲構造函數,也就是建立自定義對象的函數。能夠看出,Javascript經過結構函數和原型的方式模擬實現了類的功能。 建立自定義對象(實例化類):bash

var Person1 = new Person("張三","男");
console.log(Person1.getName());//張三
var Person2 = new Person("娜娜","女");
console.log(Person2.getName());//娜娜
複製代碼

當代碼var Person1 = new Person("張三","男")執行時,其實內部作了以下幾件事情:app

建立一個空白對象(new Object())。 拷貝Person.prototype中的屬性(鍵值對)到這個空對象中(咱們前面提到,內部實現時不是拷貝而是一個隱藏的連接)。 將這個對象經過this關鍵字傳遞到構造函數中並執行構造函數。 將這個對象賦值給變量Person1。函數

爲了證實prototype模板並非被拷貝到實例化的對象中,而是一種連接的方式,請看以下實例:性能

function Person(name,gender){
	this.name = name;
	this.gender= gender;
}
Person.prototype.age = 20;
var Person1 = new Person('娜娜','女');
console.log(Person1.age);

//覆蓋prototype中的age屬性
Person1.age = 25;
console.log(Person1.age);//25
delete Person1.age;
//在刪除實例屬性age後,此屬性值又從prototype中獲取
console.log(Person1.age);//20
複製代碼

Javascript繼承的幾種方式

爲了闡述Javascript繼承的幾種方式,首先咱們提早約定共同語言:ui

//約定
function Fun(){
	//私有屬性
	var val = 1;        //私有基本屬性
	var arr = [1];      //私有引用屬性
	function fun() {}   //私有函數(引用屬性)
	
	//實例屬性
	this.val = 1;              //公有基本屬性
	this.arr = [1];            //公有引用屬性
	this.fun = function(){};   //公有函數(引用屬性)
}
//原型屬性
Fun.prototype.val = 1;             //原型基本屬性
Fun.prototype.arr = [1];           //原型引用屬性
Fun.prototype.fun = function(){};  //原型函數(引用屬性)
複製代碼

1、簡單原型鏈實現繼承

這是實現繼承最簡單的方式了。 若是「貓」的prototype對象,指向一個Animal的示例,那麼全部「貓」的實例,就能繼承Animal了。this

具體實現

function Animal(){
	this.species = "動物";
	this.classes = ['脊椎動物','爬行動物'];
}
function Cat(name,color){
	this.name = name;
	this.color = color;
}
//將Cat的prototype對象指向一個Animal的實例
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

var cat1 = new Cat("大毛","黃色");
var cat2 = new Cat("二毛","白色");
cat1.classes.push('哺乳動物');
cat1.species = '哺乳動物';
console.log(cat1.species);//哺乳動物
console.log(cat2.species);//動物
console.log(cat1.classes);//["脊椎動物", "爬行動物", "哺乳動物"]
console.log(cat2.classes);//["脊椎動物", "爬行動物", "哺乳動物"]
複製代碼

咱們將Cat的prototype對象指向一個Animal的示例。spa

Cat.prototype = new Animal();
複製代碼

它至關於徹底刪除了prototype對象原先的值,而後賦予一個新值。prototype

Cat.prototype.constructor = Cat;
複製代碼

任何一個prototype對象都有一個constructor屬性,指向它的構造函數。若是沒有「Cat.prototype = new Animal(); 」這一行,Cat.prototype.constructor是指向Cat的;加了這一行之後,Cat.prototype.constructor指向Animal。

console.log(Cat.prototype.constructor == Animal);//true
複製代碼

更重要的是,每個實例也有一個constructor屬性,默認調用prototype對象的constructor屬性。

console.log(cat1.constructor = Cat.prototype.constructor);//true
複製代碼

所以,在運行「Cat.prototype = new Animal();」這一行以後,cat1.constructor也指向了Animal!

console.log(cat1.constructor == Animal);//true
複製代碼

這顯然會致使繼承鏈的紊亂(cat1明明是構造函數Cat生成的),所以咱們必須手動糾正,將Cat.prototype對象的constructor值改成Cat。 這一點很重要,編程時務必遵照。若是替換裏prototype對象

o.prototype = {};
複製代碼

那麼,下一步必然是爲新的prototype對象加上contructor屬性,並將這個屬性指回原來的構造函數。

o.prototype.constructor = o;
複製代碼

存在的問題

1.修改cat1.classes後cat2.classes也發生了變化,由於來自原型對象的引用屬性是全部實例共享的。 能夠這樣理解:執行cat1.classes.push('哺乳動物');先對cat1進行屬性查找,找遍了實例屬性(在本例中沒有實例屬性),沒找到,就開始順着原型鏈向上找,拿到了cat1的原型對象,一查找,發現有classes屬性。因而給classes末尾插入了‘哺乳動物’,所喲cat2.classes也發生了變化。

2.建立子類實例時,沒法向父類構造函數傳遞參數。

2、借用構造函數和call或者apply方法

簡單原型鏈真夠簡單,但是存在兩個致命的缺點簡直沒法使用,因而上世紀末的Jsers就想辦法修復了這兩個缺陷,而後就出現了借用構造函數這種方式。

具體實現

function Animal(species){
	this.species = species;
	this.classes = ['脊椎動物','爬行動物'];
}
function Cat(name,color,species){
	Animal.call(this,species);//核心
	this.name = name;
	this.color = color;
}

var cat1 = new Cat("大毛","黃色",'動物');
var cat2 = new Cat("二毛","白色",'哺乳動物');

cat1.classes.push('哺乳動物');
console.log(cat1.species);//動物
console.log(cat2.species);//哺乳動物

console.log(cat1.classes);//["脊椎動物", "爬行動物", "哺乳動物"]
console.log(cat2.classes);//["脊椎動物", "爬行動物"]
複製代碼

核心

借父類的構造函數來加強子類實例,等因而把父類的實例屬性複製了一份給子類實例裝上了(徹底沒有用到原型)。

優缺點

優勢: 1.解決了子類實例共享父類引用屬性的問題; 2.建立子類實例時,能夠向父類構造函數傳參。 缺點: 沒法實現函數複用,每一個子類實例都持有一個新的fun函數,太多了就會影響性能,內存爆炸。

3、組合繼承(最經常使用)

目前咱們借用構造函數方式仍是有問題(沒法實現函數複用),不要緊,接着修復,因而出現了組合繼承。

具體實現

function Animal(species){
	//只在此處聲明基本屬性和引用屬性
	this.species = species;
	this.classes = ['脊椎動物','爬行動物'];
}
//在此處聲明函數
Animal.prototype.eat = function(){
	console.log('動物必須吃東西獲取能量');
}
Animal.prototype.run = function(){
	console.log('動物正在跑動');
}
function Cat(name,color,species){
	Animal.call(this,species);//核心
	this.name = name;
	this.color = color;
}
Cat.prototype = new Animal();

var cat1 = new Cat("大毛","黃色",'動物');
var cat2 = new Cat("二毛","白色",'哺乳動物');

cat1.classes.push('哺乳動物');
console.log(cat1.species);//動物
console.log(cat2.species);//哺乳動物

console.log(cat1.classes);//["脊椎動物", "爬行動物", "哺乳動物"]
console.log(cat2.classes);//["脊椎動物", "爬行動物"]
console.log(cat1.eat === cat2.eat);//true
複製代碼

具體實現

把實例函數都放在原型對象上,以實現函數複用。同時還要保留借用構造函數方式的優勢,經過Animal.call(this,species)繼承父類的基本屬性和引用屬性並保留能傳參的優勢;經過Cat.prototype = new Animal(),繼承父類函數,實現函數複用。

優缺點

優勢: 1.不存在引用屬性共享的問題 2.可傳參 3.函數能夠複用 缺點: (一點小瑕疵)子類原型上有一份多餘的父類實例屬性,由於父類構造函數被調用了兩次,生成了兩份,而子類實例上的那一份屏蔽了子類原型上的。又是內存浪費,不過已經改進了不少。

4、直接繼承prototype(改進簡單原型鏈繼承)

第四種方法是對第二種方法的改進。因爲Animal對象中,不變的屬性均可以直接寫入Animal.prototype。因此,咱們也可讓Cat()跳過Animal(),直接繼承Animal.prototype。

具體實現

function Animal(){}
Animal.prototype.species = '動物';
function Cat(name,color){
	this.name = name;
	this.color = color;
}
//將Cat的prototype對象指向Animal的prototype對象,這樣就實現了繼承
Cat.prototype = Animal.prototype;
Cat.prototype.constructor = Cat;

var cat1 = new Cat('大毛','黃色');
console.log(cat1.species);//動物
複製代碼

優缺點

優勢: 與第一種方法相比,這樣作的優勢是效率比較高(不用執行和創建Animal的示例了),比較省內存。 缺點: Cat.prototype和Animal.prototype如今指向了同一個對象,那麼任何對Cat.prototype的修改,都會反映到Animal.prototype。 Cat.prototype.constructor = Cat,把Animal.prototype對象的constructor屬性也改掉了

console.log(Animal.prototype.constructor);//Cat
複製代碼

5、利用空對象做爲中介(寄生組合繼承)

因爲「直接繼承prototype」存在上述的缺點,因此就有了如下方法,利用一個空對象做爲中介。

function Animal(){}
Animal.prototype.species = '動物';
function Cat(name,color){
	Animal.call(this);
	this.name = name;
	this.color = color;
}

//利用空對象做爲中介,核心
var F = function(){};
F.prototype = Animal.prototype;
Cat.prototype = new F();
Cat.prototype.constructor = Cat;
複製代碼

F是空對象,因此幾乎不佔內存。這時,修改Cat的prototype對象,就不會影響到Animal的prototype對象。

console.log(Animal.prototype.constructor);//Animal
複製代碼

將上述方法封裝成一個函數,便於使用

function extend(Child,Parent){
	var F = function(){};
	F.prototype = Parent.prototype;
	Child.prototype = new F();
	Child.prototype.constructor = Child;
	Child.uber = Parent.prototype;
}
複製代碼

使用方法以下:

function Animal(){}
Animal.prototype.species = '動物';
function Cat(name,color){
	this.name = name;
	this.color = color;
}
extend(Cat,Animal);
var cat1 = new Cat('大毛','黃色');
console.log(cat1.species);//動物
複製代碼

函數的最後一行

Child.uber = Parent.prototype;
複製代碼

爲子對象設一個uber屬性,這個屬性直接指向父對象的prototype屬性。(uber是一個德語詞,意思是"向上"、"上一層"。)這等於在子對象上打開一條通道,能夠直接調用父對象的方法。這一行放在這裏,只是爲了實現繼承的完備性,純屬備用性質。

6、拷貝繼承

上面是採用prototype對象,實現繼承。咱們也能夠換一種思路,純粹採用「拷貝」方法實現繼承。簡單說,就是把父對象的全部屬性和方法,拷貝進子對象。 定義一個函數,實現屬性拷貝的目的:

function extend(Child,Parent){
	var p = Parent.prototype;
	var c = Child.prototype;
	for(var i in p){
		c[i] = p[i];
	}
	c.uber = p;
}
複製代碼

這個函數的做用就是將父對象的prototype對象中的屬性,一一拷貝給Child對象的prototype對象。 繼承的具體實現以下:

function Animal(){}
Animal.prototype.species = '動物';
function Cat(name,color){
	this.name = name;
	this.color = color;
}
extend(Cat,Animall);
var cat = new Cat('大毛','黃色');
console.log(cat.species);//動物
複製代碼
相關文章
相關標籤/搜索