js如何實現繼承


js繼承有5種實現方式:
1、繼承第一種方式:對象冒充
  function Parent(username){
    this.username = username;
    this.hello = function(){
      alert(this.username);
    }
  }
  function Child(username,password){
    //通過以下3行實現將Parent的屬性和方法追加到Child中,從而實現繼承
    //第一步:this.method是作為一個臨時的屬性,並且指向Parent所指向的對象,
    //第二步:執行this.method方法,即執行Parent所指向的對象函數
    //第三步:銷毀this.method屬性,即此時Child就已經擁有了Parent的所有屬性和方法
    this.method = Parent;
    this.method(username);//最關鍵的一行
    delete this.method;

    this.password = password;
    this.world = function(){
      alert(this.password);
    }
  }
  var parent = new Parent("zhangsan");
  var child = new Child("lisi","123456");
  parent.hello();
  child.hello();
  child.world();

2、繼承第二種方式:call()方法方式
  call方法是Function類中的方法
  call方法的第一個參數的值賦值給類(即方法)中出現的this
  call方法的第二個參數開始依次賦值給類(即方法)所接受的參數

  function test(str){
    alert(this.name + " " + str);
  }
  var object = new Object();
  object.name = "zhangsan";
  test.call(object,"langsin");//此時,第一個參數值object傳遞給了test類(即方法)中出現的this,而第二個參數"langsin"則賦值給了test類(即方法)的str

  function Parent(username){
    this.username = username;
    this.hello = function(){
      alert(this.username);
    }
  }
  function Child(username,password){
    Parent.call(this,username);
    
    this.password = password;
    this.world = function(){
      alert(this.password);
    }
  }
  var parent = new Parent("zhangsan");
  var child = new Child("lisi","123456");
  parent.hello();
  child.hello();
  child.world();

3、繼承的第三種方式:apply()方法方式
  apply方法接受2個參數,
    A、第一個參數與call方法的第一個參數一樣,即賦值給類(即方法)中出現的this
    B、第二個參數為數組類型,這個數組中的每個元素依次賦值給類(即方法)所接受的參數

  function Parent(username){
    this.username = username;
    this.hello = function(){
      alert(this.username);
    }
  }
  function Child(username,password){
    Parent.apply(this,new Array(username));
    
    this.password = password;
    this.world = function(){
      alert(this.password);
    }
  }
  var parent = new Parent("zhangsan");
  var child = new Child("lisi","123456");
  parent.hello();
  child.hello();
  child.world();

4、繼承的第四種方式:原型鏈方式,即子類通過prototype將所有在父類中通過prototype追加的屬性和方法都追加到Child,從而實現了繼承
  function Person(){
  }
  Person.prototype.hello = "hello";
  Person.prototype.sayHello = function(){
    alert(this.hello);
  }
  
  function Child(){
  }
  Child.prototype = new Person();//這行的作用是:將Parent中將所有通過prototype追加的屬性和方法都追加到Child,從而實現了繼承
  Child.prototype.world = "world";
  Child.prototype.sayWorld = function(){
    alert(this.world);
  }
  
  var c = new Child();
  c.sayHello();
  c.sayWorld();

5、繼承的第五種方式:混合方式
  混合了call方式、原型鏈方式

  function Parent(hello){
    this.hello = hello;
  }
  Parent.prototype.sayHello = function(){
    alert(this.hello);
  }

  function Child(hello,world){
    Parent.call(this,hello);//將父類的屬性繼承過來
    this.world = world;//新增一些屬性
  }

  Child.prototype = new Parent();//將父類的方法繼承過來

  Child.prototype.sayWorld = function(){//新增一些方法
    alert(this.world);
  }

  var c = new Child("zhangsan","lisi");
  c.sayHello();
  c.sayWorld();

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

JS繼承之借用構造函數繼承和組合繼承

 

根據少一點套路,多一點真誠這個原則,繼續學習。

 

借用構造函數繼承

在解決原型中包含引用類型值所帶來問題的過程中,開發人員開始使用一種叫做借用構造函數
(constructor stealing)的技術(有時候也叫做偽造對象或經典繼承)。這種技術的基本思想相當簡單,即
在子類型構造函數的內部調用超類型構造函數。

基本模式

1
2
3
4
5
6
7
8
9
10
11
12
function  SuperType(){
   this .colors = [ "red" "blue" "green" ];
}
function  SubType(){
    //繼承了SuperType
   SuperType.call( this );
}
var  instance1 =  new  SubType();
instance1.colors.push( "black" );
alert(instance1.colors);  //"red,blue,green,black"
var  instance2 =  new  SubType();
alert(instance2.colors);  //"red,blue,green"

基本思想

借用構造函數的基本思想就是利用call或者apply把父類中通過this指定的屬性和方法復制(借用)到子類創建的實例中。因為this對象是在運行時基於函數的執行環境綁定的。也就是說,在全局中,this等於window,而當函數被作為某個對象的方法調用時,this等於那個對象。call 、apply方法可以用來代替另一個對象調用一個方法。call、apply 方法可將一個函數的對象上下文從初始的上下文改變為由 thisObj 指定的新對象。   

所以,這個借用構造函數就是,new對象的時候(注意,new操作符與直接調用是不同的,以函數的方式直接調用的時候,this指向window,new創建的時候,this指向創建的這個實例),創建了一個新的實例對象,並且執行SubType里面的代碼,而SubType里面用call調用了SuperTyep,也就是說把this指向改成了指向新的實例,所以就會把SuperType里面的this相關屬性和方法賦值到新的實例上,而不是賦值到SupType上面。所有實例中就擁有了父類定義的這些this的屬性和方法。

優勢

相對於原型鏈而言,借用構造函數有一個很大的優勢,即可以在子類型構造函數中向超類型構造函數傳遞參數。因為屬性是綁定到this上面的,所以調用的時候才賦到相應的實例中,各個實例的值就不會互相影響了。

例如:

復制代碼
function SuperType(name){
this.name = name;
}
function SubType(){
//繼承了SuperType,同時還傳遞了參數
SuperType.call(this, "Nicholas");
//實例屬性
this.age = 29;
}
var instance = new SubType();
alert(instance.name); //"Nicholas";
alert(instance.age); //29
復制代碼

劣勢

如果僅僅是借用構造函數,那么也將無法避免構造函數模式存在的問題——方法都在構造函數中定義,因此函數復用就無從談起了。而且,在超類型的原型中定義的方法,對子類型而言也是不可見的,結果所有類型都只能使用構造函數模式。考慮到這些問題,借用構造函數的技術也是很少單獨使用的。

組合繼承

組合繼承(combination inheritance),有時候也叫做偽經典繼承。是將原型鏈和借用構造函數的技術組合到一塊,從而發揮二者之長的一種繼承模式。

基本思想

思路是使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。這樣,既通過在原型上定義方法實現了函數復用,又能夠保證每個實例都有它自己的屬性。

基本模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function  SuperType(name){
   this .name = name;
   this .colors = [ "red" "blue" "green" ];
}
SuperType.prototype.sayName =  function (){
    alert( this .name);
};
function  SubType(name, age){
//繼承屬性
   SuperType.call( this , name);
   this .age = age;
}
//繼承方法
SubType.prototype =  new  SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge =  function (){
alert( this .age);
};
var  instance1 =  new  SubType( "Nicholas" , 29);
instance1.colors.push( "black" );
alert(instance1.colors);  //"red,blue,green,black"
instance1.sayName();  //"Nicholas";
instance1.sayAge();  //29
var  instance2 =  new  SubType( "Greg" , 27);
alert(instance2.colors);  //"red,blue,green"
instance2.sayName();  //"Greg";
instance2.sayAge();  //27

優勢

組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優點,成為JavaScript 中最常用的繼承模式。

劣勢

組合繼承最大的問題就是無論什么情況下,都會調用兩次超類型構造函數:一次是在創建子類型原型的時候,另一次是在子類型構造函數內部。雖然子類型最終會包含超類型對象的全部實例屬性,但我們不得不在調用子類型構造函數時重寫這些屬性。

  

 To be continued...

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

JS繼承之原型繼承

 許多OO語言都支持兩種繼承方式:接口繼承和實現繼承。接口繼承只繼承方法簽名,而實現繼承則繼承實際的方法。如前所述,由於函數沒有簽名,在ECMAScript中無法實現接口繼承。ECMAScript只支持實現繼承,而且其實現繼承主要是依靠原型鏈來實現的。  

                                             --摘自《JavaScript高級程序設計》

 

原型繼承
原型鏈是實現原型繼承的主要方法,基本思想就是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。
 
實現原型鏈的基本模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
function  SuperType(){
  this .property= true ;
}
 
SuperType.prototype.getSuperValue= function (){
   return  this .property;
}
 
function  SubType(){
   this .subproperty= false ;
}
 
SubType.prototype= new  SuperType();
1
SubType.prototype.getSubValue= function (){<br>  return  this .property;<br> }
1
var  instance= new  SubType();
1
instance.getSuperValue();  //true;

例子中的實例及構造函數和原型之間的關系圖:

 

在例子代碼中,定義了兩個對象,subType和superType。

兩個對象之間實現了繼承,而這種繼承方式是通過創建SuperType的實例並將該實例賦給subType.prototype實現的。實現的本質就是重寫了原型對象

這樣subType.prototype中就會存在一個指針指向superType的原型對象。也就是說,存在superType的實例中的屬性和方法現在都存在於subType.prototype中了。這樣繼承了之后,又可以為subType添加新的方法和屬性。

要注意,這個指針([[prototype]])默認情況下是不可以再被外部訪問的,估計是會被一些內部方法使用的,例如用for...in來遍歷原型鏈上可以被枚舉的屬性的時候,就需要通過這個指針找到當前對象所繼承的對象。不過,Firefox、Safari和Chrome在每個對象上都支持一個屬性__proto__。

原型繼承需要注意的一些問題

1.別忘記默認的類型

我們知道,所有的引用類型都繼承了Object,而這個繼承也是通過原型鏈實現的。所以所有的對象都擁有Object具有的一些默認的方法。如

:hasOwnProperty()、propertyIsEnumerable()、toLocaleString()、toString()和valueOf()。

2. 確定原型和實例的關系
可以通過兩種方式來確定原型和實例之間的關系。

①使用instanceof 操作符,只要用這個操作符來測試實例與原型鏈中出現過的構造函數,結果就會返回true。

②第二種方式是使用isPrototypeOf()方法。同樣,只要是原型鏈中出現過的原型,都可以說是該原型鏈所派生的實例的原型,因此isPrototypeOf()方法也會返回true。

例子:

復制代碼
alert(instance instanceof Object); //true
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true

alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true
復制代碼

③子類要在繼承后定義新方法

因為,原型繼承是實質上是重寫原型對象。所以,如果在繼承前就在子類的prototype上定義一些方法和屬性。那么繼承的時候,子類的這些屬性和方法將會被覆蓋。

如圖:

④不能使用對象字面量創建原型方法

這個的原理跟第三點的實際上是一樣的。當你使用對象字面量創建原型方法重寫原型的時候,實質上相當於重寫了原型鏈,所以原來的原型鏈就被切斷了。

 

⑤注意父類包含引用類型的情況

如圖:

這個例子中的SuperType 構造函數定義了一個colors 屬性,該屬性包含一個數組(引用類型值)。SuperType 的每個實例都會有各自包含自己數組的colors 屬性。當SubType 通過原型鏈繼承了SuperType 之后,SubType.prototype 就變成了SuperType 的一個實例,因此它也擁有了一個它自己的colors 屬性——就跟專門創建了一個SubType.prototype.colors 屬性一樣。但結果是什么呢?結果是SubType 的所有實例都會共享這一個colors 屬性。而我們對instance1.colors 的修改能夠通過instance2.colors 反映出來。也就是說,這樣的修改會影響各個實例。

原型繼承的缺點(問題)

①最明顯的就是上述第⑤點,有引用類型的時候,各個實例對該引用的操作會影響其他實例。

②沒有辦法在不影響所有對象實例的情況下,給超類型的構造函數傳遞參數。

 

有鑒於此,實踐中很少會單獨使用原型繼承。

 

最近要回顧一下原生js的一些重要的基礎知識點,秋招秋招。。。

 

 

 

 

 

 

 

JavaScript面向對象與原型

工廠模式:無法識別對象

function createObject(name, age) { //集中實例化的函數
  var obj = new Object();
  obj.name = name;
  obj.age = age;
  obj.run = function () {
    return this.name + this.age + '運行中...';
  };
  return obj;
}
var box1 = createObject('Lee', 100); //第一個實例
var box2 = createObject('Jack', 200); //第二個實例
alert(box1.run());
alert(box2.run()); //保持獨立

alert(typeof box1); //Object
alert(box1 instanceof Object); //true

構造函數(構造方法):可以識別對象

function Box(name, age) { //構造函數模式
  this.name = name;
  this.age = age;
  this.run = function () {
    return this.name + this.age + '運行中...';
  };
}
var box1 = new Box('Lee', 100); //new Box()即可
var box2 = new Box('Jack', 200);

alert(box1.run());
alert(box1 instanceof Box); //很清晰的識別他從屬於 Box

構造函數的方法有一些規范:

1.函數名和實例化構造名相同且大寫,(PS:非強制,但這么寫有助於區分構造函數和普通函數);

2.通過構造函數創建對象,必須使用 new 運算符。

var o = new Object();
Box.call(o, 'Jack', 200) //對象冒充調用
alert(o.run());

原型(共享):創建的每個函數都有一個 prototype(原型)屬性,這個屬性是一個對象,它的用途是包含可以由特定類型的所有實例共享的屬性和方法。邏輯上可以這么理解:prototype 通過調用構造函數而創建的那個對象的原型對象。使用原型的好處可以讓所有對象實例共享它所包含的屬性和方法。也就是說,不必在構造函數中定義對象信息,而是可以直接將這些信息添加到原型中。

unction Box() {} //聲明一個構造函數
Box.prototype.name = 'Lee'; //在原型里添加屬性
Box.prototype.age = 100;
Box.prototype.run = function () { //在原型里添加方法
  return this.name + this.age + '運行中...';
};

在原型模式聲明中,多了兩個屬性,這兩個屬性都是創建對象時自動生成的。

__proto__屬性:是實例指向原型對象的一個指針,它的作用就是指向構造函數的原型屬性 constructor 。通過這兩個屬性,就可以訪問到原型里的屬性和方法了。

PS:IE 瀏覽器在腳本訪問__proto__會不能識別,火狐和谷歌瀏覽器及其他某些瀏覽器均能識別。雖然可以輸出,但無法獲取內部信息。

判斷一個對象是否指向了該構造函數的原型對象,可以使用 isPrototypeOf()方法來測試。

exp:alert(Box.prototype.isPrototypeOf(box)); //只要實例化對象,即都會指向

原型模式的執行流程:
1.先查找構造函數實例里的屬性或方法,如果有,立刻返回;
2.如果構造函數實例里沒有,則去它的原型對象里找,如果有,就返回;

雖然我們可以通過對象實例訪問保存在原型中的值,但卻不能訪問通過對象實例重寫原型中的值。

var box1 = new Box();
alert(box1.name); //Lee,原型里的值
box1.name = 'Jack';
alert(box.1name); //Jack,就近原則,
var box2 = new Box();
alert(box2.name); //Lee,原型里的值,沒有被 box1 修改
如果想要 box1 也能在后面繼續訪問到原型里的值,可以把構造函數里的屬性刪除即可 ,具體如下:
delete box1.name; //刪除屬性
alert(box1.name);
如何判斷屬性是在構造函數的實例里,還是在原型里?可以使用 hasOwnProperty()函數來驗證:

alert(box.hasOwnProperty('name')); //實例里有返回 true,否則返回 false

 

in 操作符會在通過對象能夠訪問給定屬性時返回 true,無論該屬性存在於實例中還是原型中。
alert('name' in box); //true,存在實例中或原型中
我們可以通過 hasOwnProperty()方法檢測屬性是否存在實例中,也可以通過 in 來判斷實例或原型中是否存在屬性。那么結合這兩種方法,可以判斷原型中是否存在屬性。

function isProperty(object, property) { //判斷原型中是否存在屬性
  return !object.hasOwnProperty(property) && (property in object);
}

var box = new Box();
alert(isProperty(box, 'name')) //true,如果原型有

為了讓屬性和方法更好的體現封裝的效果,並且減少不必要的輸入,原型的創建可以使用字面量的方式:

function Box() {};
Box.prototype = { //使用字面量的方式
  name : 'Lee',
  age : 100,
  run : function () {
    return this.name + this.age + '運行中...';
  }
};

使用構造函數創建原型對象和使用字面量創建對象在使用上基本相同,但還是有一些區別,字面量創建的方式使用 constructor 屬性不會指向實例,而會指向 Object,構造函數創建的方式則相反。

如果想讓字面量方式的 constructor 指向實例對象,那么可以這么做:
Box.prototype = {
  constructor : Box, //直接強制指向即可
};

原型對象不僅僅可以在自定義對象的情況下使用,而 ECMAScript 內置的引用類型都可以使用這種方式,並且內置的引用類型本身也使用了原型。

alert(Array.prototype.sort); //sort 就是 Array 類型的原型方法
alert(String.prototype.substring); //substring 就是 String 類型的原型方法
String.prototype.addstring = function () { //給 String 類型添加一個方法
  return this + ',被添加了!'; //this 代表調用的字符串
};
alert('Lee'.addstring()); //使用這個方法

PS:盡管給原生的內置引用類型添加方法使用起來特別方便,但我們不推薦使用這種方法。因為它可能會導致命名沖突,不利於代碼維護。

動態原型模式

function Box(name ,age) { //將所有信息封裝到函數體內
  this.name = name;
  this.age = age;
  if (typeof this.run != 'function') { //僅在第一次調用的初始化
    Box.prototype.run = function () {
      return this.name + this.age + '運行中...';
    };
  }
}
var box = new Box('Lee', 100);
alert(box.run());

PS:使用動態原型模式,要注意一點,不可以再使用字面量的方式重寫原型,因為會切斷實例和新原型之間的聯系。

寄生構造函數

function Box(name, age) {
  var obj = new Object();
  obj.name = name;
  obj.age = age;
  obj.run = function () {
    return this.name + this.age + '運行中...';
  };
  return obj;
}

穩妥構造函數

function Box(name , age) {
  var obj = new Object();
  obj.run = function () {
    return name + age + '運行中...'; //直接打印參數即可
  };
  return obj;
}
var box = Box('Lee', 100); //直接調用函數
alert(box.run());

繼承:依靠原型鏈完成

function Box() { //Box 構造
  this.name = 'Lee';
}
function Desk() { //Desk 構造
  this.age = 100;
}
Desk.prototype = new Box(); //Desc 繼承了 Box,通過原型,形成鏈條
var desk = new Desk();
alert(desk.age);
alert(desk.name); //得到被繼承的屬性
function Table() { //Table 構造
  this.level = 'AAAAA';
}
Table.prototype = new Desk(); //繼續原型鏈繼承
var table = new Table();
alert(table.name); //繼承了 Box 和 Desk

在 JavaScript 里,被繼承的函數稱為超類型(父類,基類也行,其他語言叫法),繼承的函數稱為子類型(子類,派生類)。繼承也有之前問題,比如字面量重寫原型會中斷關系,使用引用類型的原型,並且子類型還無法給超類型傳遞參數。

對象冒充(偽造對象、經典繼承、借用構造函數):解決引用共享和超類型無法傳參的問題

function Box(age) {
  this.name = ['Lee', 'Jack', 'Hello']
  this.age = age;
}
function Desk(age) {
  Box.call(this, age); //對象冒充,給超類型傳參

}
var desk = new Desk(200);
alert(desk.age);
alert(desk.name);
desk.name.push('AAA'); //添加的新數據,只給 desk
alert(desk.name);

組合繼承:原型鏈+ 借用構造函數

function Box(age) {
  this.name = ['Lee', 'Jack', 'Hello']
  this.age = age;
}
Box.prototype.run = function () {
  return this.name + this.age;
};
function Desk(age) {
  Box.call(this, age); //對象冒充
}
Desk.prototype = new Box(); //原型鏈繼承
var desk = new Desk(100);
alert(desk.run());

寄生組合繼承

function obj(o) {     //傳遞一個字面量函數
  function F() {}    //臨時新建一個構造函數,用來存儲傳遞進來的對象
  F.prototype = o;    //將o對象實例賦值給F構造的原型對象

  return new F();    //返回實例化后的構造函數
}
function create(box, desk) {
  var f = obj(box.prototype);
  f.constructor = desk;
  desk.prototype = f;
}
function Box(name) {
  this.name = name;
  this.arr = ['哥哥','妹妹','父母'];
}
Box.prototype.run = function () {
  return this.name;
};
function Desk(name, age) {
  Box.call(this, name);
  this.age = age;
}
inPrototype(Box, Desk); //通過這里實現繼承
var desk = new Desk('Lee',100);
desk.arr.push('姐姐');
alert(desk.arr);
alert(desk.run()); //只共享了方法
var desk2 = new Desk('Jack', 200);
alert(desk2.arr); //引用問題解決


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM