玩轉JavaScript OOP[3]——徹底理解繼承和原型鏈

来源:http://www.cnblogs.com/keepfool/archive/2016/06/10/5573121.html
-Advertisement-
Play Games

概述 上一篇我們介紹了通過構造函數和原型可以實現JavaScript中的“類”,由於構造函數和函數的原型都是對象,所以JavaScript的“類”本質上也是對象。這一篇我們將介紹JavaScript中的一個重要概念原型鏈,以及如何經原型鏈實現JavaScript中的繼承。 C#的繼承 首先,我們簡單... ...


概述

上一篇我們介紹了通過構造函數和原型可以實現JavaScript中的“類”,由於構造函數和函數的原型都是對象,所以JavaScript的“類”本質上也是對象。這一篇我們將介紹JavaScript中的一個重要概念原型鏈,以及如何經原型鏈實現JavaScript中的繼承。

C#的繼承

首先,我們簡單描述一下繼承的概念:當一個類和另一個類構成"is a kind of"關係時,這兩個類就構成了繼承關係。繼承關係的雙方分別是子類和基類,子類可以重用基類中的屬性和方法。

C#可以顯式地定義class,也可以讓一個class直接繼承另外一個class,下麵這段代碼就是一個簡單的繼承。

public class Person
{
    public string Name { get { return "keepfool"; } }

    public string SayHello()
    {
        return "Hello, I am " + this.Name;
    }
}

public class Employee : Person
{
    public string Email { get; set; }
}

由於Employee類是繼承Person類的,所以Employee類的實例能夠使用Person類的屬性和方法。

Employee emp = new Employee();
Console.WriteLine(emp.Name);
Console.WriteLine(emp.SayHello());

Console.WriteLine("emp{0}是Person類的實例", emp is Person ? "" : "不");

emp是Employee類的一個實例,同時也是Person類的實例,它可以訪問定義在Person類的Name屬性和SayHello()方法。

image

這是C#的繼承語法,JavaScript則沒有提供這樣的語法,現在我們來探討如何在JavaScript中實現繼承。

JavaScript原型繼承

繼承的目的

在JavaScript中定義兩個構造函數Person()和Employee(),為了方便理解和講解,我們可以將它們理解為Person類和Employee類。
以下內容提到的Person類、Employee類,和Person()構造函數、Employee()構造函數是一個意思。

function Person() {
	this.name = 'keefool';
	this.sayHello = function() {
		return 'Hello, I am ' + this.name;
	}
}

function Employee(email) {
	this.email = email;
}

var person = new Person();
var emp = new Employee('[email protected]');

目前Person()和Employee()構造函數是兩個彼此獨立的存在,它們沒有任何關係。
所以由Employee()構造函數創建的實例emp,肯定是訪問不到Person的name屬性和sayHello()方法的。

image

使用instanceof操作符同樣可以確定emp是Employee類的實例,而不是Person類的實例。

image

實現繼承的目的是什麼?當然是讓子類能夠使用基類的屬性和方法。
在這個示例中,我們的目的是實現Employee繼承Person,然後讓Employee的實例能夠訪問Person的name和sayHello()了。

JavaScript是如何實現繼承的呢?
這個答案有很多種,這裡我先只介紹比較常見的一種——通過原型實現繼承。

實現繼承

當我們定義函數時,JavaScript會自動的為函數分配一個prototype屬性。
Person()也是一個函數,那麼Person()函數也會有prototype屬性,即Person.prototype。

function Person() {
	this.name = 'keefool';
	this.sayHello = function() {
		return 'Hello, I am ' + this.name;
	}
}	

// 定義了函數後,JavaScript自動地為Person()函數分配了一個prototype屬性
// Person.prototype = {};

我們可以在Person.prototype上定義一些屬性和方法,這些屬性和方法是可以被Person的實例使用的。

	
function Person() {
	this.name = 'keefool';
	this.sayHello = function() {
		return 'Hello, I am ' + this.name;
	}
}

Person.prototype.height = 176;

var person = new Person();
// 訪問Person.prototype上定義的屬性
person.height;	// 輸出176

同理在Employee.prototype上定義的屬性和方法,也可以被Employee類的實例使用。
咱們的目的是讓Employee的實例能夠訪問name屬性和sayHello()方法,如果沒有Person()構造函數,咱們是這麼做的:

	
function Employee(email) {
	this.email = email;
}
Employee.prototype = {
	name : 'keefool',
	sayHello = function() {
		return 'Hello, I am ' + this.name;
	}
}

既然Person()構造函數已經定義了name和sayHello(),我們就不必這麼做了。
怎麼做呢?讓Employee.prototype指向一個Person類的實例。

function Person() {
	this.name = 'keefool';
	this.sayHello = function() {
		return 'Hello, I am ' + this.name;
	}
}
function Employee(email) {
	this.email = email;
}

var person = new Person(); 
Employee.prototype = person;

var emp = new Employee('[email protected]');

現在我們就可以訪問emp.name和emp.sayHello()方法了。

image

在Chrome控制台,使用instanceof操作符,可以看到emp對象現在已經是Person類的實例了。

image

這是如何實現的?

  • Employee.prototype是一個引用類型,它指向一個Person類的一個實例person。
  • person對象恰恰是有name屬性和sayHello()方法的,訪問Employee.prototype就像訪問person對象一樣。
  • 訪問emp.name和emp.sayHello()時,實際訪問的是Employee.prototype.name和Employee.prototype.sayHello(),最終訪問的是person.name和person.sayHello()。

如果你對這段代碼還是有所疑惑,你可以這麼理解:

var person = new Person();
Employee.prototype.name = person.name;
Employee.prototype.sayHello = person.sayHello;

由於person對象在後面完全沒有用到,以上這兩行代碼可以合併為一行。

function Person() {
	this.name = 'keefool';
	this.sayHello = function() {
		return 'Hello, I am ' + this.name;
	}
}
function Employee(email) {
	this.email = email;
}

Employee.prototype = new Person();
var emp = new Employee('[email protected]');

下麵這幅圖概括了實現Employee繼承Person的過程:

image

name和sayHello()不是Employee類的自有屬性和方法,它來源於Employee.prototype。
而Employee.prototype指向一個Person的實例,這個實例是能夠訪問name和sayHello()的。

image

原型繼承的本質

JavaScript的原型繼承的本質:將構造函數的原型對象指向由另外一個構造函數創建的實例。

這行代碼Employee.prototype = new Person()描述的就是這個意思。
現在我們可以說Employee()構造函數繼承了Person()構造函數。

用一句話概括這個繼承實現的過程:

Employee()構造函數的原型引用了一個由Person()構造函數創建的實例,從而建立了Employee()和Person()的繼承關係。

再談constructor

對象的constructor屬性

上一篇文章有提到過,每個對象都有constructor屬性,constructor屬性應該指向對象的構造函。
例如:Person實例的constructor屬性是指向Person()構造函數的。

var person = new Person();

image

在未設置Employee.prototype時,emp對象的構造函數原本也是指向Employee()構造函數的。
image

當設置了Employee.prototype = new Person();時,emp對象的構造函數卻指向了Person()構造函數。

image

無形之中,emp.constructor被改寫了。
emp對象看起來不像是Employee()構造函數創建的,而是Person()構造函數創建的。

這不是我們期望的,我們希望emp對象看起來也是由Employee()構造函數創建的,即emp.constructor應該是指向Employee()構造函數的。
要解決這個問題,我們先弄清楚對象的constructor屬性是從哪兒來的,知道它是從哪兒來的就知道為什麼emp.constructor被改寫了。

constructor屬性的來源

當我們沒有改寫構造函數的原型對象時,constructor屬性是構造函數原型對象的自有屬性。
例如:Person()構造函數的原型沒有改寫,constructor是Person.prototype的自有屬性。

image

當我們改寫了構造函數的原型對象後,constructor屬性就不是構造函數原型對象的自有屬性了。
例如:Employee()構造函數的原型被改寫後,constructor就不是Person.prototype的自有屬性了。

image

Employee.prototype的constructor屬性是指向Person()構造函數的。

image

這說明:當對象被創建時,對象本身沒有constructor屬性,而是來源於創建對象的構造函數的原型對象。

即當我們訪問emp.constructor時,實際訪問的是Employee.prototype.constructor,Employee.prototype.constructor實際引用的是Person()構造函數,person.constructor引用是Person()構造函數,Person()構造函數實際上是Person.prototype.constructor。

這個關係有點亂,我們可以用以下式子來表示這個關係:

emp.constructor = person.constructor = Employee.prototype.constructor = Person = Person.prototype.constructor

它們最終都指向Person.prototype.constructor!

改寫原型對象的constructor

弄清楚了對象的constructor屬性的來弄去脈,上述問題就好解決了。
解決辦法就是讓Employee.prototype.constructor指向Employee()構造函數。

var o = {};

function Person() {
	this.name = 'keefool';
	this.sayHello = function() {
		return 'Hello, I am ' + this.name;
	}
}
function Employee(email) {
	this.email = email;
}

Employee.prototype = new Person();
Employee.prototype.constructor = Employee;

var emp = new Employee('[email protected]');

image

如果你還是不能理解關鍵的這行代碼:

Employee.prototype.constructor = Employee;

你可以嘗試從C#的角度去理解,在C#中Employee類的實例肯定是由Employee類的構造函數創建出來的。

原型鏈

原型鏈是JavaScript中非常重要的概念,理解它有助於理解JavaScript面向對象編程的本質。

__proto__屬性

定義函數時,函數就有了prototype屬性,該屬性指向一個對象。
prototype屬性指向的對象是共用的,這有點像C#中的靜態屬性。
站在C#的角度講,由new創建的對象是不能直接訪問類的靜態屬性的。
那麼在JavaScript中,為什麼對象能夠訪問到prototype中的屬性和方法的呢?

因為:當對象由new構造函數創建時,對象會自帶一個__proto__屬性,這個屬性是由JavaScript分配的。
這個屬性是一個引用類型,它指向的正是構造函數的原型。

例如:當emp對象被創建時,JavaScript自動地為emp對象分配了一個__proto__屬性,這個屬性是指向Employee.prototype的。

image

在Chrome的控制台查看emp.__proto__的內容

image

首先,▼Person {name: "keepfool"}表示emp.__proto__是一個Person對象,因為Employee.prototype確實指向一個Person對象。
其次,我們把emp.__proto__的屬性分為3個部分來看。

  1. 第1部分:name屬性和sayHello()方法,它們兩個來源於Person對象。
  2. 第2部分:constructor屬性,因為我們重寫了Employee()構造函數的原型對象的constructor屬性,即Employee.prototype.constructor = Employee,所以constructor是指向Employee()構造函數的。
  3. 第3部分:__proto__它指向一個Object,Person類是Employee類的父類,那麼誰是Person類的父類呢?——Object類。

對象的__proto__屬性就像一個秘密鏈接,它指向了創建該對象的構造函數的原型對象。

什麼是原型鏈

我們註意到第3部分的內容仍然是一個__proto__屬性,我們展開它看個究竟吧。

再往下看,還有兩層__proto__。

image

emp.__proto__.__proto__:從▶constructor:function Person()可以看出它是Person()構造函數的原型。

image

Person.prototype包含兩部分內容:

  • Person()構造函數
  • 一個__proto__屬性,即emp__proto__.__proto__.__proto__,這個屬性指向內置的Object對象。

image

我們將這一系列的__proto__稱之為原型鏈。

理解原型鏈

下麵兩幅圖展示了本文示例的原型鏈,這兩幅圖表示的同一個意思。原型鏈的最頂端是null,因為Object.prototype是沒有__proto__屬性。

image

image

下表清晰地描述了每一層__proto__表示的內容:

編號 原型鏈 原型鏈指向的對象 描述
1 emp.__proto__ Employee.prototype Employee()構造函數的原型對象
2 emp.__proto__.__proto__ Person.prototype Person()構造函數的原型對象
3 emp.__proto__.__proto__.__proto__ Object.prototype Object()構造函數的原型對象
4 emp.__proto__.__proto__.__proto__.__proto__ null 原型鏈的頂端

image

原型鏈查找

現在可以解釋emp對象能夠訪問到name屬性和sayHello()方法了。
以訪問emp.sayHello()為例,我們用幾個慢鏡頭來闡述:

  1. emp是由Employee()構造函數創建的,JavaScript先去Employee()構造函數查找sayHello()方法
  2. 在Employee()中沒找到sayHello()方法,但emp有一個__proto__屬性,於是JavaScript就去emp.__proto__中查找
  3. emp.__proto__和Employee.prototype是相等的,而Employee.prototype指向的是一個Person對象
  4. 於是JavaScript就在這個Person對象中查找,結果發現了sayHello()方法
  5. 最終JavaScript調用的是emp.__proto__.sayHello(),也就是Employee.prototype.sayHello()。

JavaScript在背後做的事情

另外,在實現Employee()繼承Person(),以及emp對象訪問name和sayHello()時,JavaScript是幫我們做了一些事情的,見下圖:

image

將方法提升到原型對象

上一篇有提到過,Person類的sayHello()方法放到它的原型對象中更合適,這樣所有的Person實例共用一個sayHelo()方法副本,如果我們把這個方法提到原型對象會發生什麼?

var o = {};

function Person() {
	this.name = 'keefool';
}

Person.prototype.sayHello = function(){
	return 'Hello, I am ' + this.name;
}

function Employee(email) {
	this.email = email;
}

Employee.prototype = new Person();
Employee.prototype.constructor = Employee;

var emp = new Employee('[email protected]');

可以看到sayHello()方法的路徑是:emp.__proto__.__proto__.sayHello(),比直接定義在Person()構造函數中多了一層。

image

這樣看來將方法定義在原型對象中並不是絕對的好,會使得JavaScript遍歷較多層數的原型鏈,這也會有一些性能上的損失。

原型鏈示例

為了加強對原型鏈的理解,我們來做個簡單的示例吧。

上圖已經說明瞭toString()方法是屬於內置的Object對象的,我們以toString()方法來講解這個示例。

在Chrome控制台輸入emp.toString(),我們得到的結果是"[object Object]"
toString()方法是在emp的第3層原型鏈找到的,即emp.__proto__.__proto__.__proto__,它就是Object對象。

image

emp.toString()輸出"[object Object]"沒有什麼意義,現在我們在Person.prototype上定義一個toString()方法。

var o = {};

function Person() {
	this.name = 'keefool';
}

Person.prototype.sayHello = function(){
	return 'Hello, I am ' + this.name;
}

Person.prototype.toString = function() {
	return '[' + this.name + ']';
}

function Employee(email) {
	this.email = email;
}

Employee.prototype = new Person();
Employee.prototype.constructor = Employee;

var emp = new Employee('[email protected]');

這時toString()方法是在emp對象的第2層原型鏈找到的,即emp.__proto__.__proto__
emp.__proto__.__proto__是Person()構造函數的原型對象,即Person.prototype。

image

這個也是一個簡單的重寫示例,Person.protoype重寫了toString()方法,emp最終調用的是Person.prototype.toString()方法。

總結

  • JavaScript實現原型繼承有兩個關鍵:1.子類構造函數原型指向父類的一個實例 2.重寫子類構造函數原型的constructor屬性,讓其指向子類構造函數本身。
  • 在定義函數時,JavaScript自動地給函數分配了一個prototype屬性;在創建對象時,JavaScript自動的為對象分配了一個__proto__屬性。
  • __proto__是JavaScript的原型鏈,每個__proto__都是一個對象,它是子類能夠訪問基類屬性和方法的橋梁。
  • 當訪問一個對象的屬性時,首先查找自有屬性,其次逐層地遍歷__proto__原型鏈。
  • JavaScript是基於對象和原型的語言,“類”、“繼承”這些概念都是通過對象和原型實現的。

隱藏文章目錄 顯示右邊欄 關註keepfool
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前段時間寫過一篇 "CSS基礎知識之position" ,當時對float的理解不太準確,被慕課網多名讀者指出(原文已修正,如有誤導實在抱歉)。現對float進行更深入的學習,在此把學習心得分享給大家。 浮動的基礎知識 浮動有4個屬性:left(左浮動)、right(右浮動)、none(不浮動)、i ...
  • ...
  • ...
  • 一、JSONP 常用的Jquery框架支持jsonp方式請求,該方式只支持GET方法,傳參大小有限,而且需要後臺根據jsonp的請求方式進行封裝結果返回。 其中參數jsonp預設為callback,jsonpCallback為隨機生成的回調函數名,若指定handleRequest,則後臺參數返回時為 ...
  • 一、原生JavaScript編寫tab切換 二、jQuery編寫tab切換 在用jQuery編寫選項卡過程中,重要的事搞清楚 .eq() 和 .index() 的使用方法。 .eq()是jQuery遍歷的一種方法,參數是元素的索引 .index() 。要註意index是基於0開始的;如果index為 ...
  • 部署普通站點 1、首先下載apache24版本,下載地址為http://pan.baidu.com/s/1pLmvDgB; 2、解壓到你的電腦本地目錄,如D:\Apache24(下文配置都會以當前目錄作為參考說明) 3、修改D:\Apache24\conf\目錄下的httpd.conf文件 部署普通 ...
  • JavaScript數組簡介 JavaScript中的數組與其他語言中的數組是不同的,主要體現在: 數組中存儲的各項可以是不同類型的數據 數組的大小是動態變化的,當新增項時或移除項時可以動態的改變大小來容納當前數據項 在JavaScript中創建數組 在JavaScript中創建數組有兩種方式: 其... ...
  • webpack集成了模塊載入和打包等功能 ,這兩年在前端圈越來越手歡迎。平時一般是用requirejs、seajs作為模塊載入用,用grunt/gulp作為前端構建。webpack作為模塊化載入相容了amd/cmd模式,並且作為模塊化的資源可以是js/css/image coffeescript/s ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...