1. 程式人生 > 程式設計 >JavaScript中繼承原理與用法例項入門

JavaScript中繼承原理與用法例項入門

本文例項講述了JavaScript中繼承原理與用法。分享給大家供大家參考,具體如下:

正統的面相物件的語言都會提供extend之類的方法用於出來類的繼承,但Javascript並不提供extend方法,在Javascript中使用繼承需要用點技巧。

Javascript中的例項的屬性和行為是由建構函式和原型兩部分組成的,我們定義兩個類:Person和zhangsan,它們在記憶體中的表現如下圖1:
JavaScript中繼承原理與用法例項入門
如果想讓Zhangsan繼承Person,那麼我們需要把Person建構函式和原型中的屬性和行為全部傳給Zhangsan的建構函式和原型,如下圖2所示:
JavaScript中繼承原理與用法例項入門

Are you Ok?瞭解了繼承的思路後,那麼我們一步步完成Person和Zhangsan的繼承功能。首先,我們需要定義Person類,如下程式碼:

[程式碼1]

// 定義Person類
function Person (name){
  this.name = name;
  this.type = "人";
}
Person.prototype={
  say : function(){
    console.info("我是一個"+ this.type +",我的名字叫" + this.name);
  }
}  
//定義Zhangsan類
function Zhangsan (name){
}
Zhangsan.prototype={
  
}

Zhangsan雖然有自己特有的屬性和行為,但它大部分屬性和行為和Person相同,需要繼承自Person類。如前所述,JavaScript中繼承是要分別繼承建構函式和原型中的屬性和行為的。我們先讓Zhangsan繼承Person的建構函式中的行為和屬性,如下程式碼:

[程式碼2]

// 定義Person類
function Person (name){
  this.name = name;
  this.type = "黃";
}
Person.prototype={
  say : function(){
    console.info("我是一個"+ this.type +"種人,我的名字叫" + this.name);
  }
}  
//定義Zhangsan類
function Zhangsan (name){
  this.name = name;
  this.type = "黃";
}
Zhangsan.prototype={
}
//例項化Zhangsan物件
var zs = new Zhangsan("張三");
console.info(zs.type);  // 黃

執行正常,但我們怎麼沒看到繼承的“味道”呢?我們在Zhangsan的建構函式中將Person的屬性和行為複製了一份,與其說是繼承不如說是“真巧,這兩個類的建構函式除了函式名不同,其他地方都長得一樣”。她的缺點很明顯:如果Person類的建構函式有任何變動,我們也需要手動的同步修改Zhangsan類的建構函式,同樣一份程式碼,我們複製了一份寫在了程式中 的不同地方,這違法了DRY原則,降低了程式碼的可維護性。

好了,讓我們來改進它:
[程式碼3]

// 定義Person類
function Person (name){
  this.name = name;
  this.type = "黃";
}
Person.prototype={
  say : function(){
    console.info("我是一個"+ this.type +"種人,我的名字叫" + this.name);
  }
}  
// 定義Zhangsan類
function Zhangsan (name){
  Person(name);
}
Zhangsan.prototype={
}
// 例項化Zhangsan物件
var zs = new Zhangsan("張三");
console.info(zs.type);    // undefined

我們在Zhangsan的建構函式裡呼叫Person()函式,希望它內部的ths.xxx可以在Zhangsan類的建構函式裡執行一遍,但奇怪的是,出現“console.info(zs.type);”時,輸出的是undefined,這是怎麼回事呢?

這和Person的呼叫方式有關。在JavaScript中,function有兩種不同的呼叫方法:

  1. 作為函式存在,直接用“()”呼叫,例如“function test(){}; test();”test被用作函式,直接被“()”符號呼叫。

  2. 作為類的建構函式存在,使用new呼叫,例如“function test(){}; new test();”test作為類的建構函式,通過new進行test類的例項化。這兩種方法的呼叫,function內部的this指向會有所不同---作為函式的function,其this指向的是window,而作為建構函式的function,其this指向的例項物件。

上面程式碼中,Zhangsan類建構函式中的Person是通過函式方式呼叫的,它內部的this指向的是window物件,起效果等同於如下程式碼:
[程式碼4]

// 定義Person類
function Person (name){
  this.name = name;
  this.type = "黃";
}
Person.prototype={
  say : function(){
    console.info("我是一個"+ this.type +"種人,我的名字叫" + this.name);
  }
}  
// 定義Zhangsan類
function Zhangsan (name){
  window.name = name;
  window.type = "黃";
}
Zhangsan.prototype={
}
// 例項化Zhangsan物件
var zs = new Zhangsan("張三");
console.info(zs.type);  // undefined
console.info(type);    // 黃 (window.type可以省略寫成type)

如果想達到[程式碼3]的效果,讓Person內部this指向Zhangsan類的例項,可以通過call或apply方法實現,如下:
[程式碼5]

// 定義Person類
function Person (name){
  this.name = name;
  this.type = "黃";
}
Person.prototype={
  say : function(){
    console.info("我是一個"+ this.type +"種人,我的名字叫" + this.name);
  }
}  
// 定義Zhangsan類
function Zhangsan (name){
  Person.call(this,name);
}
Zhangsan.prototype={
}
// 例項化Zhangsan物件
var zs = new Zhangsan("張三");
console.info(zs.type);    // 黃

建構函式的屬性和行為已經成功實現了繼承,接下來我們要實現原型中的屬性和行為的繼承。既然Zhangsan類需要和Person類原型中同樣的屬性和行為,那麼能否將Person類的原型直接傳給Zhangsan類的原型,如下程式碼:
[程式碼6]

// 定義Person類
function Person (name){
  this.name = name;
  this.type = "黃";
}
Person.prototype={
  say : function(){
    console.info("我是一個"+ this.type +"種人,我的名字叫" + this.name);
  }
}  
// 定義Zhangsan類
function Zhangsan (name){
  Person.call(this,name);
}
Zhangsan.prototype = Person.prototype;
// 例項化Zhangsan物件
var zs = new Zhangsan("張三");
// 我是一個黃種人,我的名字叫張三
zs.say();

通過Person類的原型傳給Zhangsan類的原型,Zhangsan類成功獲得了say行為,但事情並不像想象中的那麼簡單,如果我們要給Zhangsan類新增run行為呢?如下程式碼:
[程式碼7:新增run行為]

// 定義Person類
function Person (name){
  this.name = name;
  this.type = "黃";
}
Person.prototype={
  say : function(){
    console.info("我是一個"+ this.type +"種人,我的名字叫" + this.name);
  }
}  
// 定義Zhangsan類
function Zhangsan (name){
  Person.call(this,name);
}
Zhangsan.prototype = Person.prototype;
Zhangsan.prototype.run = function(){
  console.info("我100米短跑只要10秒!");
}
// 例項化Zhangsan物件
var zs = new Zhangsan("張三");
zs.say();  // 我是一個黃種人,我的名字叫張三
zs.run();  //我100米短跑只要10秒!
var zs2 = new Person("張三2");
zs2.run();  //我100米短跑只要10秒!

我們只想給Zhangsan類新增run行為,為什麼Person類也獲得了run行為了呢?這涉及傳值和傳址的兩個問題----在JavaScript中,賦值語句會用傳值和傳地址兩種不同的方式進行賦值,如果是數值型、不爾型、字元型等基本資料型別,在進行賦值時會將資料直接賦值一份,將賦值的那一份資料進行賦值,也就是通常所說的傳值;如果是陣列、hash物件等複雜資料型別,在進行賦值時會直接用記憶體地址賦值,而不是將資料賦值一份,這就是傳址賦值,就是傳資料的對映地址。
[程式碼8:傳值與傳址]

var a=10;    // 基本資料型別
var b=a;    // 將變數a儲存的值賦值一份,傳給變數b,b和a各儲存一份資料
var c=[1,2,3];  // 複雜資料型別
var d=c;    // 將變數c指向的資料記憶體地址傳給變數d,c和d指向同一份資料
b++;
d.push(4);
console.info(a);  // 10
console.info(b);  // 11    變數b儲存的資料更改不會影響到變數a
console.info(c);  // 1,3,4  變數c和d指向同一份資料,資料更改會相互影響
console.info(d);  // 1,4

在原生JavaScript中,選擇傳值還是傳地址是根據資料型別來自動判斷的,但傳地址有時候會給我們帶來意想不到的麻煩,所以我們需要對複雜資料型別的賦值進行控制,讓複雜資料型別也可以進行傳值。

最簡單的做法是遍歷陣列或者Hash物件,將陣列或者Hash物件這種複雜的資料拆分成一個個簡單資料,然後分別賦值,如下面程式碼:
[程式碼9:對複雜資料型別進行傳值]

var a = [1,3],b = {name:'張三',sex:'男',tel:'1383838438'};
var c = [],d = {};
for(var p in a){
  c[p] = a[p]; 
}
for(var p in b){
  d[p] = b[p];
}
c.push('4');
d.email = '[email protected]';
console.info(a);      // [1,3]
console.info(c);      // [1,"4"]
console.info(b.email);    // undefined
console.info(d.email);    // [email protected]

值得一提的是,對於陣列的傳值還可以使用陣列類的slice或者concat方法實現,如下面程式碼:
[程式碼10:陣列傳值的簡單方法]

var a = [1,3];
var b = a.slice(),c = a.concat();
b.pop();
c.push(4);
console.info(a);    // [1,3]
console.info(b);    // [1,2]
console.info(c);    // [1,4]

prototype本質上也是一個hash物件,所以直接用它賦值時會進行傳址,這也是為什麼[程式碼7:新增潤行為]中,zs2居然會run的原因。我們可以用for in來遍歷prototype,從而實現prototype的傳值。但因為prototype和function(用做類的function)的關係,我們還有另外一種方法實現prototype的傳值----new SomeFunction(),如下面程式碼:
[程式碼11]

// 定義Person類
function Person (name){
  this.name = name;
  this.type = "黃";
}
Person.prototype={
  say : function(){
    console.info("我是一個"+ this.type +"種人,我的名字叫" + this.name);
  }
}  
// 定義Zhangsan類
function Zhangsan (name){
  Person.call(this,name);
}
Zhangsan.prototype = new Person();
Zhangsan.prototype.constructor = Person;
Zhangsan.prototype.run = function(){
  console.info("我100米短跑只要10秒!");
}
  
// 例項化Zhangsan物件
var zs = new Zhangsan("張三");
zs.say();  // 我是一個黃種人,我的名字叫張三
zs.run();  // 我100米短跑只要10秒!
var zs2 = new Person("張三2");
zs2.run();  // TypeError: zs2.run is not a function

您是否注意到上面這句Zhangsan.prototype.constructor = Person;,這是因為Zhangsan.prototype = new Person();時,Zhangsan.prototype.constructor指向了Person,我們需要將它糾正,重新指向Zhangsan。

感興趣的朋友可以使用線上HTML/CSS/JavaScript程式碼執行工具:http://tools.jb51.net/code/HtmlJsRun測試上述程式碼執行效果。

更多關於JavaScript相關內容感興趣的讀者可檢視本站專題:《javascript面向物件入門教程》、《JavaScript錯誤與除錯技巧總結》、《JavaScript資料結構與演算法技巧總結》、《JavaScript遍歷演算法與技巧總結》及《JavaScript數學運算用法總結》

希望本文所述對大家JavaScript程式設計有所幫助。