JavaScript 原型 與 原型鏈

YXi發表於2019-07-27

原型

什麼是JS原型?

如果對JS原型解釋的話,會涉及到兩個概念:建構函式,原型物件

  • 建構函式

通俗的說,就是你在script標籤裡面宣告的那個函式

<script>


function Test(){
	// 我就是建構函式
}


</script>
複製程式碼
  • 原型物件

在宣告瞭一個函式之後,瀏覽器會自動按照一定的規則建立一個物件,這個物件就叫做原型物件。這個原型物件其實是儲存在了記憶體當中

在宣告瞭一個函式後,這個建構函式(宣告瞭的函式)中會有一個屬性prototype,這個屬性指向的就是這個建構函式(宣告瞭的函式)對應的原型物件;原型物件中有一個屬性constructor,這個屬性指向的是這個建構函式(宣告瞭的函式)

如下圖:

圖片載入失敗!

使用建構函式建立物件

我們的建構函式使用new來建立物件的時候,就像下面這樣:

<script>

 	function students(){
 		// 我就是建構函式
 	}

 	let stu = new students();

</script>
複製程式碼

此時,stu就是那個建構函式students建立出來的物件,這個stu物件中是沒有prototype屬性的,prototype屬性只有在建構函式students中有,看圖!

圖片載入失敗!

圖片載入失敗!

可以看出,建構函式students中有prototype屬性,指向的是students對應的原型物件;而stu是建構函式students建立出來的物件,他不存在prototype屬性,所以在呼叫prototype的時候的結構是undefined,但stu有一個__proto__屬性,stu呼叫這個屬性可以直接訪問到建構函式students的原型物件(也就是說,stu的__proto__屬性指向的是建構函式的原型物件),看圖:

圖片載入失敗!

說明:

  • 從上面的程式碼中可以看到,建立stu物件雖然使用的是students建構函式,但是物件建立出來之後,這個stu物件其實已經與students建構函式沒有任何關係了,stu物件的__proto__屬性指向的是students建構函式的原型物件
  • 如果使用new students()建立多個物件stu1、stu2、stu3,則多個物件都會同時指向students建構函式的原型物件
  • 我們可以手動給這個原型物件新增屬性和方法,那麼stu1,stu2,stu3…這些物件就會共享這些在原型中新增的屬性和方法
  • 如果我們訪問stu中的一個屬性name,如果在stu物件中找到,則直接返回。如果stu物件中沒有找到,則直接去stu物件的__proto__屬性指向的原型物件中查詢,如果查詢到則返回。(如果原型中也沒有找到,則繼續向上找原型的原型—原型鏈)
  • 如果通過stu物件新增了一個屬性name,則stu物件來說就遮蔽了原型中的屬性name。 換句話說:在stu中就沒有辦法訪問到原型的屬性name了
  • 通過stu物件只能讀取原型中的屬性name的值,而不能修改原型中的屬性name的值。 stu.name = “李四”; 並不是修改了原型中的值,而是在stu物件中給新增了一個屬性name

開始擼程式碼:

<script type="text/javascript">
	function students () {        
	}
	// 可以使用students.prototype 直接訪問到原型物件
	//給students函式的原型物件中新增一個屬性 name並且值是 "張三"
	students.prototype.name = "張三";
	students.prototype.age = 20;

	var stu = new students();
	/*
		訪問stu物件的屬性name,雖然在stu物件中我們並沒有明確的新增屬性name,但是
		stu的__proto__屬性指向的原型中有name屬性,所以這個地方可以訪問到屬性name
		就值。
		注意:這個時候不能通過stu物件刪除name屬性,因為只能刪除在stu中刪除的物件。
	*/
	alert(stu.name);  // 張三

	var stu1 = new students();
	alert(stu1.name);  // 張三  都是從原型中找到的,所以一樣。

	alert(stu.name === stu1.name);  // true

	// 由於不能修改原型中的值,則這種方法就直接在stu中新增了一個新的屬性name,然後在stu中無法再訪問到
	//原型中的屬性。
	stu.name = "李四";
	alert(stu.name); //李四
	// 由於stu1中沒有name屬性,則對stu1來說仍然是訪問的原型中的屬性。    
	alert(stu1.name);  // 張三  
</script>
複製程式碼

與原型有關的幾個方法:

  • prototype屬性

prototype 存在於建構函式中 (其實任意函式中都有,只是不是建構函式的時候prototype我們不關注而已) ,他指向了這個建構函式的原型物件

  • constructor屬性

constructor屬性存在於原型物件中,他指向了建構函式

如下程式碼:

<script type="text/javascript">
    function students () {
    }
    alert(students.prototype.constructor === students); // true
</script>
複製程式碼
  • _ _ proto_ _ 屬性(注意:左右各是2個下劃線)

用構造方法建立一個新的物件之後,這個物件中預設會有一個屬性__proto__, 這個屬性就指向了構造方法的原型物件


原型鏈

說完原型後,趁熱打鐵,說一下原型鏈

在js中,萬物皆物件,物件可以說是重中之重了。每一個物件都擁有自己的屬性。但是在這個世界中有很多東西都是相似的,可以歸為一類,他們有共同的方法和屬性。不可能讓每一個物件都定義一個屬性吧。那樣太消耗記憶體了。所以,在js中怎麼才能讓多個物件共享一個或多個方法呢?原型的出現就是為了解決這個問題。

  • prototype和__proto__的區別

一切皆物件,函式是特殊的物件。
所有的引用型別(函式、陣列和物件)都擁有__proto__屬性(隱式原型)
所有函式擁有prototype屬性(顯示原型)
原型物件:擁有prototype屬性的物件,在定義函式時就被建立

  • 建構函式

大多數情況下,__proto__可以理解為“構造器的原型”(__proto__ === constructor.prototype)

function Word(words){
		this.words = words
	}
	Word.prototype = {
			alert(){
				alert(this.words)
			}
	}
 
	var w = new Word("Hello Word");
	w.alert();
	console.log(w.__proto__ === Word.prototype) // true
複製程式碼

例項w的隱式原型指向它建構函式的顯示原型。(指向即恆等於)

w.__proto__ === Word.prototype // true
複製程式碼

當呼叫某種方法或查詢某種屬性時,首先會在自身呼叫和查詢,如果自身沒有該屬性或方法,則會去它的__proto__屬性中呼叫查詢,也就是構造方法的prototype屬性中查詢。例項通過這樣的方法繼承建構函式的方法和屬性。

例項通過__proto__屬性指向構造方法的prototype的屬性實現方法和屬性的繼承。


  • 原型鏈

由於__proto__屬性是任何物件都有的屬性,在js中一切皆物件,所以會形成一條__proto__連線起來的鏈條,遞迴訪問__proto__必須最終到頭,並且值為null。

圖片載入失敗!

Function.prototype.a = 'a';
	Object.prototype.b = 'b';
	function Person(){}
	console.log(Person);
	let p = new Person();
	console.log(p); // Person {} 物件
	console.log(p.a); // null
	console.log(p.b);  // b

複製程式碼

說明: p是Person()的例項,是一個Person物件,它擁有一個屬性值__proto__,並且__proto__是一個物件,包含兩個屬性值constructor和__proto__,

圖片載入失敗!

會發現p.__proto__===Person.prototype,   p.__proto__.constructor ===Person // true,即p.__proto__.constructor指向了建構函式本身。  

Person.prototype的__proto__屬性,指向了Object 的prototype屬性。即p.b的列印結果為b。則例項w通過__proto__屬性繼承了Object 的屬性b。通過__proto__屬性一直向上查詢,一直到null為止。

那麼建構函式Person的__proto__指向了Function.prototype。

圖片載入失敗!

  1. 查詢屬性,如果本身沒有,則會去__proto__中查詢,也就是建構函式的顯式原型中查詢,如果建構函式中也沒有該屬性,因為建構函式也是物件,也有__proto__,那麼會去它的顯式原型中查詢,一直到null,如果沒有則返回undefined
  2. p.__proto__.constructor == function Person(){}
  3. p.__proto__proto__== Object.prototype
  4. p.__proto____proto__.__proto__== Object.prototype.__proto__== null
  5. 通過__proto__形成原型鏈而非protrotype

圖片載入失敗!

事實上,js裡完全依靠"原型鏈"模式來實現繼承。

再簡單說一下__proto__、prototype、constructor

  • _ _proto _ _:事實上就是原型鏈指標
  • prototype:這個是指向原型物件的
  • constructor:每一個原型物件都包含一個指向建構函式的指標,就是constructor

繼承的實現方式:

為了實現繼承,_ _proto _ _會指向上一層的原型物件,而上一層的結構依然類似,那麼就利用proto一直指向Object的原型物件上!Object.prototype. _ _ proto _ _ = null;表示到達最頂端。如此形成了原型鏈繼承。

下面有個圖非常經典:

圖片載入失敗!

大家可以自己動手畫一下,加深自己的理解

大致總結一下:

1.Object是作為眾多new出來的例項的基類 function Object(){ [ native code ] }

2.Function是作為眾多function出來的函式的基類 function Function(){ [ native code ] }

3.建構函式的__proto__(包括Function.prototype和Object.prototype)都指向Function.prototype

4.原型物件的__proto__都指向Object.prototype

5.Object.prototype._ _ proto _ _指向null


^_<

相關文章