淺析JavaScript的繼承和原型鏈

2022-02-15 19:00:27
本篇文章給大家帶來了中繼承和原型鏈的相關知識,其中包括建構函式、原型以及class語法糖的相關問題,希望對大家有幫助。

淺析JavaScript的繼承和原型鏈

一、前言

JavaScript的繼承和原型鏈是我在學習前端過程中遇到的較為少有的難以理解的部分,這裡便將我所有知道和了解到的東西記錄了下來,希望能夠給還在這裡面苦苦掙扎的兄弟萌一點點小的幫助,也歡迎各位大佬批評指正。

二、建構函式

2.1 建構函式的範例成員和靜態成員

建構函式由範例成員和靜態成員二者組成,其中範例成員是在函數內部通過this關鍵字新增的成員;只能通過範例化物件以後通過範例化物件進行存取;而靜態成員是函數本身上新增的成員,只能通過建構函式來存取。

//創造一個建構函式let Father = function(name,age){
    //範例成員
    this.name = name;
    this.age = age;
    this.method = "我是一個範例成員";}
    //靜態成員Father.like = "mother";
    //檢驗範例物件是否能夠被建構函式直接存取console.log(Father.method);
    //undefinedconsole.log(Father.like);
    //mother
    //範例化一個物件let father = new Father("小王",27);
    //檢驗靜態物件是否能夠被範例化物件存取console.log(father.name);
    //小王console.log(father.age);
    //27console.log(father.like);
    //undefined

2.2 範例化物件的過程

通過new關鍵字可以通過建構函式實現一個範例化物件,那麼在具體範例化的過程中發生了什麼呢?大致可以劃分為以下幾個步驟:

(1) 建立一個空物件 son {}

(2) 為 son 準備原型鏈連線 son.__proto__ = Father.prototype

(3) 重新系結this,使建構函式的this指向新物件 Father.call(this)

(4) 為新物件屬性賦值 son.name

(5) 返回this return this,此時的新物件就擁有了建構函式的方法和屬性了

一個小問題:所有範例化物件的方法都是共用的嗎?

建構函式的方法分為兩種,第一種為在函數內部直接定義的方法,第二種為通過原型新增的方法;

//函數內部直接定義的方法let Father = function(){
    this.read = function(){
        console.log("我是內部定義的read方法!");
    }}//通過原型鏈新增的方法Father.prototype.look = function(){
    console.log("我是通過原型鏈定義的look方法!");}
    //範例化物件進行檢驗let father1 = new Father();let father2 = new Father();
    father1.read();
    //我是內部定義的read方法!father2.read();
    //我是內部定義的read方法!console.log(father1.read === father2.read);
    //falsefather1.look();
    //我是通過原型鏈定義的look方法!father2.look();
    //我是通過原型鏈定義的look方法!console.log(father1.look === father2.look);
    /true

可以發現,函數內部直接定義的方法在每範例化一個新的物件以後,都會給這個方法分配一個新的記憶體空間,而通過原型新增的方法便會共用一個空間。

一個小問題:所有範例化物件的屬性都是共用的嗎?

不存在記憶體空間的問題,判斷時看其值是否相同;

let Father = function(name){
    this.name = name;}let father1 = new Father("小王");
    let father2 = new Father("小紅");
    console.log(father1.name === father2.name);
    //falselet father1 = new Father("小王");
    let father2 = new Father("小王");
    console.log(father1.name === father2.name);
    //true

因此我們可以總結一下定義建構函式的基本規則,即公共屬性定義到建構函式裡面,公共方法我們放到原型物件身上。

三、原型

3.1 什麼是原型

Father.prototype 就是原型,它是一個物件,也可以稱為原型物件。

3.2 原型的作用是什麼

原型的作用,就是共用方法。

我們通過 Father.prototype.method 可以共用方法,不會反應開闢空間儲存方法。

3.3 原型中的this指向哪兒

原型中this的指向是範例。

四、原型鏈

原型鏈本人感覺是一個對於初學者或者說是部分前端菜雞(例如本人)來說特別難以理解的東西,為了讓下面的部分更容易理解,這裡強行先記住以下幾點:

  1. __proto__是每個物件都有的屬性,prototype是每個函數特有的方法;
  2. 每個物件的__proto__屬性都會指向自身建構函式的prototype;
  3. constructor屬性始終指向建立當前物件的建構函式;
  4. Function.__proto__ === Function.prototype;
  5. Object.prototype.__proto__ === null 也就是原型鏈的終點;

4.1 什麼是原型鏈

原型與原型層層相連結的過程即為原型鏈。

4.2 原型鏈的應用

物件可以使用建構函式prototype原型物件的屬性和方法,就是因為物件有__proto__原型的存在每個物件都有__proto__原型的存在

let Father = function(name){
    this.name = name;}let father = new Father("老王");console.log(father.__proto__ === Father.prototype);
    //true
    //驗證上述說法中的第二條

4.3 原型鏈圖

原型鏈

結合寫在最前面的幾點,理解上圖應該問題不大了,圖中圈起來的部分就是駭人聽聞的原型鏈。

4.4 原型鏈的查詢方式

function Star(name) {
	this.name = name;
	//(1)首先看obj物件身上是否有dance方法,如果有,則執行物件身上的方法
	this.dance = function () {
		console.log(this.name + '1');
	}}//(2)如果沒有dance方法,就去建構函式原型物件prototype身上去查詢dance這個方法。Star.prototype.dance = function () {
	console.log(this.name + '2');};
	//(3)如果再沒有dance方法,就去Object原型物件prototype身上去查詢dance這個方法。Object.prototype.dance = function () {
	console.log(this.name + '3');};
	//(4)如果再沒有,則會報錯。let obj = new Star('小紅');obj.dance();

(1)首先看obj物件身上是否有dance方法,如果有,則執行物件身上的方法。

(2)如果沒有dance方法,就去建構函式原型物件prototype身上去查詢dance這個方法。

(3)如果再沒有dance方法,就去Object原型物件prototype身上去查詢dance這個方法。

(4)如果再沒有,則會報錯。

一個小問題:在原型上新增方法需要注意的地方

有兩種新增方法,第一種為上面的寫法,直接通過 建構函式.prototype.方法名 進行新增;第二種為重定義建構函式的prototype,但是此種情況會丟失掉原有的constructor構造器,所以一定要再連線回去,例子如下:

function Star(name) {
	this.name = name;}Star.prototype = {
    dance:function(){
    	console.log("重定義prototype");
	}}Star.prototype.constructor = Star;

另外,類似於Array、String這些內建的類是不能這麼處理的。

五、繼承

這裡就長話短說,首先我們要明確繼承需要繼承哪些東西,在前文中我們提到了定義建構函式的基本規則,即**公共屬性定義到建構函式裡面,公共方法我們放到原型物件身上。**我們所需要繼承的東西也不外乎就這二者,公共屬性的繼承可以通過call()或者apply()進行this的指向定義,而公共方法可以通過原型物件的賦值進行處理,因此我們很容易想到如下的方法:

//定義一個父類別function Father(name) {
	this.name = name;}Father.prototype.dance = function () {
	console.log('I am dancing');};//定義一個子類function Son(name, age) {
	Father.call(this, name);
	this.age = age;}//通過賦值的方法連線Son.prototype = Father.prototype;//為子類新增方法Son.prototype.sing = function () {
	console.log('I am singing');};
	let son = new Son('小紅', 100);
	//此時父類別也被影響了console.log(Father.prototype) 
	//{dance: ƒ, sing: ƒ, constructor: ƒ}

很顯然,當我們只想修改子類裡面的方法時,顯然上述方法不太合適;因此 我們可以嘗試new一個新的父類別出來,程式碼如下:

function Father(name) {
	this.name = name;}Father.prototype.dance = function () {
	console.log('I am dancing');};function Son(name, age) {
	Father.call(this, name);
	this.age = age;}Son.prototype = new Father();Son.prototype.sing = function () {
	console.log('I am singing');};let son = new Son('小紅', 100);console.log(Father.prototype) 
	//{dance: ƒ, constructor: ƒ}

六、class語法糖

對於以前瞭解過物件導向程式設計的程式設計師來講,上述關於繼承的寫法屬實讓人有些難以接受,因此在es6裡面新增了一個語法糖來更方便更便捷地書寫繼承,這裡就直接上程式碼了;

class Father {
	constructor(name) {
		this.name = name;
	}
	dance() {
		console.log("我是" + this.name + ",我今年" + this.age + "歲," + "我在跳舞");
	}}class Son extends Father {
	constructor(name, age) {
		super(name);
		this.age = age;
	}
	sing() {
		console.log("我是" + this.name + ",我今年" + this.age + "歲," + "我在唱歌");
	}}let obj = new Son('小紅', 19);
	obj.sing();obj.dance();

分析一下上面程式碼,首先一個類(建構函式)裡面依舊為兩部分,即公共屬性和公共方法,constructor() 裡面存放了該建構函式的公共屬性,後面接著的便是公共方法,extends 關鍵字表示繼承的是哪個類,super() 便是將裡面父類別裡面相應的公共屬性拿出來,這樣看下來便可以將程式碼規整許多。

相關推薦:

以上就是淺析JavaScript的繼承和原型鏈的詳細內容,更多請關注TW511.COM其它相關文章!