js原型、原型鏈、繼承的理解


一、原型、原型鏈

原型是Javascript中的繼承的基礎,JavaScript的繼承主要依靠原型鏈來實現的。

原型

​ 在JavaScript中,我們創建一個函數A(就是聲明一個函數), 就會為該函數創建一個prototype屬性。而且也會在內存中創建一個對象B,A函數的屬性 prototype 指向這個對象B( 即:prototype的屬性的值是這個對象 )。這個對象B就是函數A的原型對象,簡稱函數的原型。這個原型對象B 默認會有一個屬性 constructor, constructor屬性指向函數A ( 意思就是說:constructor屬性的值是函數A )。

​看下面的代碼:

<body>
    <script type="text/javascript">
    	/*
    		聲明一個函數,則這個函數默認會有一個屬性叫 prototype 。而且瀏覽器會自動按照一定的規則
    		創建一個對象,這個對象就是這個函數的原型對象,prototype屬性指向這個原型對象。這個原型對象
    		有一個屬性叫constructor 執行了這個函數
			
			注意:原型對象默認只有屬性:constructor。其他都是從Object繼承而來,暫且不用考慮。
		*/
	    function Person () {
	    	
	    }	    
    </script>
</body>

下面的圖描述了聲明一個函數之后發生的事情:
image.png

當把一個函數作為構造函數 (理論上任何函數都可以作為構造函數) 使用new創建對象的時候,那么這個對象就會存在一個默認的不可見的屬性,來指向了構造函數的原型對象。 這個不可見的屬性我們一般用 [[prototype]] 來表示,只是這個屬性沒有辦法直接訪問到。

看下面的代碼:

<body>
    <script type="text/javascript">
	    function Person () {
	    	
	    }	
        /*
        	利用構造函數創建一個對象,則這個對象會自動添加一個不可見的屬性 [[prototype]], 而且這個屬性
        	指向了構造函數的原型對象。
        */
      	var p1 = new Person();
    </script>
</body>

觀察下面的示意圖:
image.png

原型鏈

每個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。假如讓一個原型對象等於另一個類型的實例,那么此時的原型對象將包含一個指向另一個原型的指針,相應的,另一個原型中也包含一個指向另一個構造函數的指針。假如另一個原型又是另一個類型的實例,那么上述關系依然成立,如此層層遞進,就構成了實例與原型的鏈條,這就是我們說的原型鏈。

實現原型鏈有一種基本模式,代碼如下:

function SuperType() {
  this.prototype = true;
}
SuperType.prototype.getSuperValue = function () {
  return this.prototype
}

function SubType () {
  this.subprototype = false;
}
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
  return this.subprototype 
}

var instance = new SubType();
alert(instance.getSuperValue()); // true

以上代碼定義了兩個類型:SuperType和SubType。每個類型分別有一個屬性和方法。它們的主要區別是SubType繼承了SuperType,而繼承是通過創建SuperType實例。換句話說,原來存在於SuperType的實例中的所有屬性和方法,現在也存在於SubType.prototype中了。在確立了繼承關系之后,我們給SubType.prototype添加了一個方法,這樣就在繼承了SuperType的屬性和方法的基礎上又添加了一個新方法。這個例子中的實例及構造函數和原型之間的關系如圖:
image.png

image.png

二、繼承

繼承的方法:1.原型鏈繼承;2.構造函數繼承;3.組合繼承; 4.原型式繼承; 5.寄生式繼承 ;6.寄生組合式繼承;

我們先定義一個父類

原型鏈繼承

function SuperType() {
  this.property = true;
}
SuperType.prototype.getSuperValue = function() {
  return this.property;
}
function SubType() {
  this.Subproperty = false;
}
SubType.prototype = new SuperType(); // 繼承了SuperType
SubType.prototype.getSubValue = function() {
  return this.Subproperty ;
}
var instance = new SubType();
alert(instance.getSuperValue()) // true

繼承原理:通過讓子類的原型等於父類的實例,來實現繼承。
優點:繼承了超類型的構造函數的所有屬性和方法。
缺點:1、在創建子類實例時,無法向超類型的構造函數傳參,繼承單一。
   2、所有新實例都會共享父類實例的屬性。(原型上的屬性是共享的,一個實例修改了原型引用類型的屬性,另一個實例的原型屬性也會被修改!)

function SuperType() {
  this.colors = ['red','blue','green'];
}
function SubType() {
}
SubType.prototype = new SuperType(); // 繼承方法

var instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); // ['red','blue','green','black']

var instance2 = new SubType();
console.log(instance2.colors); // ['red','blue','green','black']

構造函數繼承

function SuperType() {
  this.colors = ['red', 'blue', 'green'];
}
function SubType() {
  SuperType.call(this); //  繼承了SuperType
}
var instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'blue', 'green', 'black']

var instance2 = new SubType();
console.log(instance2.colors); // ['red', 'blue', 'green']

在子類的內部調用父類,通過call改變父類中this的指向。等於是復制父類的實例屬性給子類。
優點:可以在子類構造函數中,向超類型構造函數傳遞參數。
缺點:只繼承了父類構造函數的屬性,沒有繼承父類原型的屬性。所有的方法都在構造函數中定義,無法實現復用,影響性能。

組合繼承 (原型鏈+構造函數)

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('xxx', 15);
instance1.colors.push('black');
console.log(instance1.colors); // ['red','blue','green','black']
instance1.sayNAme(); // xxx
instance1.sayAge(); // 15

var instance2 = new SubType('yyy', 12);
console.log(instance2.colors); // ['red','blue','green']
instance2.sayNAme(); // yyy
instance2.sayAge(); // 12

使用原型鏈實現對原型屬性和方法的繼承,通過構造函數來實現對實例屬性的繼承。這樣既通過在原型上定義方法實現了函數的復用,又能夠保證每個實例都有它自己的屬性。
缺點:調用兩次父類構造函數。

三、與原型有關的幾個屬性和方法

2.1 prototype屬性

​ prototype 存在於構造函數中 (其實任意函數中都有,只是不是構造函數的時候prototype我們不關注而已) ,他指向了這個構造函數的原型對象。

​ 參考前面的示意圖。

2.2 constructor屬性

​ constructor屬性存在於原型對象中,他指向了構造函數

看下面的代碼:

<script type="text/javascript">
	function Person () {
	}
	alert(Person.prototype.constructor === Person);	// true
	var p1 = new Person();
  	//使用instanceof 操作符可以判斷一個對象的類型。  
  	//typeof一般用來獲取簡單類型和函數。而引用類型一般使用instanceof,因為引用類型用typeof 總是返回object。
	alert(p1 instanceof Person);	// true
</script>

我們根據需要,可以Person.prototype 屬性指定新的對象,來作為Person的原型對象。

但是這個時候有個問題,新的對象的constructor屬性則不再指向Person構造函數了。

看下面的代碼:

<script type="text/javascript">
	function Person () {
		
	}
	//直接給Person的原型指定對象字面量。則這個對象的constructor屬性不再指向Person函數
	Person.prototype = {
		name:"志玲",
		age:20
	};
	var p1 = new Person();
	alert(p1.name);  // 志玲

	alert(p1 instanceof Person); // true
	alert(Person.prototype.constructor === Person); //false
  	//如果constructor對你很重要,你應該在Person.prototype中添加一行這樣的代碼:
  	/*
  	Person.prototype.constructor = Person;	//讓constructor重新指向Person函數

  	*/
</script>

2.3 _ _ proto _ _屬性(注意:左右各是2個下划線)

​ 用構造方法創建一個新的對象之后,這個對象中默認會有一個不可訪問的屬性 [[prototype]] , 這個屬性就指向了構造方法的原型對象。

​ 但是在個別瀏覽器中,也提供了對這個屬性[[prototype]]的訪問(chrome瀏覽器和火狐瀏覽器。ie瀏覽器不支持)。訪問方式:p1.proto

​ 但是開發者盡量不要用這種方式去訪問,因為操作不慎會改變這個對象的繼承原型鏈。

<script type="text/javascript">
	function Person () {
		
	}
	//直接給Person的原型指定對象字面量。則這個對象的constructor屬性不再指向Person函數
	Person.prototype = {
		constructor : Person,
		name:"志玲",
		age:20
	};
	var p1 = new Person();

	alert(p1.__proto__ === Person.prototype);	//true
	
</script>

2.4 hasOwnProperty() 方法
​ 大家知道,我們用去訪問一個對象的屬性的時候,這個屬性既有可能來自對象本身,也有可能來自這個對象的[[prototype]]屬性指向的原型。

​ 那么如何判斷這個對象的來源呢?

​ hasOwnProperty方法,可以判斷一個屬性是否來自對象本身。

<script type="text/javascript">
	function Person () {
		
	}
	Person.prototype.name = "志玲";
	var p1 = new Person();
	p1.sex = "女";
  	//sex屬性是直接在p1屬性中添加,所以是true
	alert("sex屬性是對象本身的:" + p1.hasOwnProperty("sex"));
  	// name屬性是在原型中添加的,所以是false
	alert("name屬性是對象本身的:" + p1.hasOwnProperty("name"));
  	//  age 屬性不存在,所以也是false
	alert("age屬性是存在於對象本身:" + p1.hasOwnProperty("age"));
	
</script>

所以,通過hasOwnProperty這個方法可以判斷一個對象是否在對象本身添加的,但是不能判斷是否存在於原型中,因為有可能這個屬性不存在。

也即是說,在原型中的屬性和不存在的屬性都會返回fasle。

如何判斷一個屬性是否存在於原型中呢?

2.5 in 操作符

​ in操作符用來判斷一個屬性是否存在於這個對象中。但是在查找這個屬性時候,現在對象本身中找,如果對象找不到再去原型中找。換句話說,只要對象和原型中有一個地方存在這個屬性,就返回true

<script type="text/javascript">
	function Person () {
		
	}
	Person.prototype.name = "志玲";
	var p1 = new Person();
	p1.sex = "女";
	alert("sex" in p1);		// 對象本身添加的,所以true
	alert("name" in p1);	//原型中存在,所以true
	alert("age" in p1); 	//對象和原型中都不存在,所以false
	
</script>

回到前面的問題,如果判斷一個屬性是否存在於原型中:

如果一個屬性存在,但是沒有在對象本身中,則一定存在於原型中。

<script type="text/javascript">
	function Person () {
	}
	Person.prototype.name = "志玲";
	var p1 = new Person();
	p1.sex = "女";
	
	//定義一個函數去判斷原型所在的位置
	function propertyLocation(obj, prop){
		if(!(prop in obj)){
			alert(prop + "屬性不存在");
		}else if(obj.hasOwnProperty(prop)){
			alert(prop + "屬性存在於對象中");
		}else {
			alert(prop + "對象存在於原型中");
		}
	}
	propertyLocation(p1, "age");
	propertyLocation(p1, "name");
	propertyLocation(p1, "sex");
</script>


免責聲明!

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



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