一文徹底理解JavaScript原型與原型鏈
前言
JavaScript
中有許多內(nèi)置對象,如:Object, Math, Date
等。我們通常會這樣使用它們:
// 創(chuàng)建一個JavaScript Date實例 const date = new Date(); // 調(diào)用getFullYear方法,返回日期對象對應的年份 date.getFullYear(); // 調(diào)用Date的now方法 // 返回自1970-1-1 00:00:00 UTC(世界標準時間)至今所經(jīng)過的毫秒數(shù) Date.now()
當然,我們也可以自己創(chuàng)建自定義對象:
function Person() { this.name = '張三'; this.age = 18; } Person.prototype.say = function() { console.log('say'); } const person = new Person(); person.name; // 張三 person.say(); // say
看到這些代碼,不知道你是否有這些疑問:
new
關(guān)鍵執(zhí)行函數(shù)和普通函數(shù)執(zhí)行有什么區(qū)別嗎?- 對象的實例為什么可以調(diào)用構(gòu)造函數(shù)的原型方法,它們之間有什么關(guān)系嗎?
接下來,讓我們帶著這些問題一步步深入學習。
new對函數(shù)做了什么?
當我們使用new
關(guān)鍵字執(zhí)行一個函數(shù)時,除了具有函數(shù)直接執(zhí)行的所有特性之外,new
還幫我們做了如下的事情:
- 創(chuàng)建一個空的簡單
JavaScript
對象(即{}
) - 將空對象的
__proto__
連接到(賦值為)該函數(shù)的prototype
- 將函數(shù)的
this
指向新創(chuàng)建的對象 - 函數(shù)中如果沒有返回對象的話,將
this
作為返回值
用代碼表示大概是這樣:
// 1. 創(chuàng)建空的簡單js對象 const plainObject = {}; // 2. 將空對象的__proto__連接到該函數(shù)的prototype plainObject.__proto__ = function.prototype; // 3. 將函數(shù)的this指向新創(chuàng)建的對象 this = plainObject; // 4. 返回this return this
可以看到,當我們使用new
執(zhí)行函數(shù)的時候,new
會幫我們在函數(shù)內(nèi)部加工this
,最終將this
作為實例返回給我們,可以方便我們調(diào)用其中的屬性和方法。
下面,我們嘗試實現(xiàn)一下new:
function _new (Constructor, ...args) { // const plainObject = {}; // plainObject.__proto__ = constructor.prototype; // __proto__在有些瀏覽器中不支持,而且JavaScript也不推薦直接使用該屬性 // Object.create: 創(chuàng)建一個新對象,使用現(xiàn)有的對象提供新創(chuàng)建的對象的__proto__ const plainObject = Object.create(Constructor.prototype); // 將this指向新創(chuàng)建的對象 const result = Constructor.call(plainObject, ...args); const isObject = result !== null && typeof result === 'object' || typeof result === 'function'; // 如果返回值不是對象的話,返回this(這里是plainObject) return isObject ? result : plainObject; }
簡單用一下我們實現(xiàn)的_new
方法:
function Animal (name) { this.name = name; this.age = 2; } Animal.prototype.say = function () { console.log('say'); }; const animal = new Animal('Panda'); console.log(animal.name); // Panda animal.say(); // say
在介紹new
的時候,我們提到了prototype
,__proto__
這些屬性。你可能會疑惑這些屬性的具體用途,別急,我們馬上進行介紹!
原型和原型鏈
在學習原型和原型鏈之前,我們需要首先掌握以下三個屬性:
prototype
: 每一個函數(shù)都有一個特殊的屬性,叫做原型(prototype
)constructor
: 相比于普通對象的屬性,prototype
屬性本身會有一個屬性constructor
,該屬性的值為prototype
所在的函數(shù)__proto__
: 每一個對象都有一個__proto__
屬性,該屬性指向?qū)ο?實例)所屬構(gòu)造函數(shù)(類)的原型prototype
以上的解釋只針對于JavaScript
語言
我們再來看下邊的一個例子:
function Fn () { this.x = 100; this.y = 200; this.getX = function () { console.log(this.x); }; } Fn.prototype.getX = function () { console.log(this.x); }; Fn.prototype.getY = function () { console.log(this.y); }; const fn = new Fn()
我們畫圖來描述一下上邊代碼中實例、構(gòu)造函數(shù)、以及prototype
和__proto__
之間的關(guān)系:
我們再來看一下Function
和Object
以及其原型之間的關(guān)系:
由于Function
和Object
都是函數(shù),因此它們的所屬類為Function
,它們的__proto__
都指向Function.prototype
。而Function.prototype.__proto__
又指向Object.prototype
,所以它們既可以調(diào)用函數(shù)原型上的方法,也可以調(diào)用對象原型上的方法。
當我們需要獲取實例上的某個屬性時:
上例中:
- 實例:
fn
- 實例所屬類:
Fn
- 首先會從自身的私有屬性上進行查找
- 如果沒有找到,會到自身的
__proto__
上進行查找,而實例的__proto__
指向其所屬類的prototype
,即會在類的prototype
上進行查找 - 如果還沒有找到,繼續(xù)到類的
prototype
的__proto__
中查找,即Object.prototype
- 如果在
Object.prototype
中依舊沒有找到,那么返回null
上述查找過程便形成了JavaScript
中的原型鏈。
在理解了原型鏈和原型的指向關(guān)系后,我們看看以下代碼會輸出什么:
const f1 = new Fn(); const f2 = new Fn(); console.log(f1.getX === f2.getX); console.log(f1.getY === f2.getY); console.log(f1.__proto__.getY === Fn.prototype.getY); console.log(f1.__proto__.getX === f2.getX); console.log(f1.getX === Fn.prototype.getX); console.log(f1.constructor); console.log(Fn.prototype.__proto__.constructor); f1.getX(); f1.__proto__.getX(); f2.getY(); Fn.prototype.getY(); // false // true // true // false // false // Fn // Object // 100 // undefined // 200 // undefined
到這里,我們已經(jīng)初步理解了原型和原型鏈的一些相關(guān)概念,下面讓我們通過一些實際例子來應用一下吧!
借用原型方法
在JavaScript
中,我們可以通過call/bind/apply
來更改函數(shù)中this
指向,原型上方法的this
也可以通過這些api
來進行更改。比如我們要將一個偽數(shù)組轉(zhuǎn)換為真實數(shù)組,可以這樣做:
function fn() { return Array.prototype.slice.call(arguments) } fn(1,2,3) // [ 1, 2, 3]
這里我們使用arguments
調(diào)用了數(shù)組原型上的slice
,這是怎么做到的呢?我們先簡單模擬下slice
方法的實現(xiàn):
arguments
是一個類似數(shù)組的對象,有length
屬性和從零開始的索引,它可以調(diào)用Object.prototype
上的方法,但是不能調(diào)用Array.prototype
上的方法。
Array.prototype.mySlice = function (start = 0, end = this.length) { const array = []; // 一般會通過Array的實例(數(shù)組)調(diào)用該方法,所以this指向調(diào)用該方法的數(shù)組 // 這里我們將this指向了arguments = {0: 1, 1: 2, 2: 3, length: 3} for (let i = 0; i < end; i++) { array[i] = this[i]; } return array; }; function fn () { return Array.prototype.mySlice.call(arguments); } console.log(fn(1, 2, 3)); // [1, 2, 3]
可能你想直接調(diào)用arguments.slice()
方法,但是遺憾的是arguments
是一個對象,不能調(diào)用數(shù)組原型上的方法。
當我們將Array.prototype.slice
方法的this
指向arguments
對象時,由于arguments
擁有索引屬性以及length
屬性,所以可以像數(shù)組一樣根據(jù)length
和索引來進行遍歷,從而相當于用arguments
調(diào)用了數(shù)組原型上的方法。
下面是另一個借用原型方法常見的例子:
Object.prototype.toString.call([1,2,3]) // [object Array] Object.prototype.toString.call(function() {}) // [object Number]
這里將Object.prototype.toString
的this
由對象(Object
的實例)改為了數(shù)組(Array
的實例)和函數(shù)(Function
的實例),相當于為數(shù)組和函數(shù)調(diào)用了對象上的toString
方法,而不是調(diào)用它們自身的toString
方法。
通過借用原型方法,我們可以讓變量調(diào)用自身以及自己原型上沒有的方法,增加了代碼的靈活性,也避免了一些不必要的重復工作。
實現(xiàn)構(gòu)造函數(shù)之間的繼承
通過JavaScript
中的原型和原型鏈,我們可以實現(xiàn)構(gòu)造函數(shù)的繼承關(guān)系。假設(shè)有如下A
,B
倆個構(gòu)造函數(shù):
function A () { this.a = 100; } A.prototype.getA = function () { console.log(this.a); }; function B () { this.b = 200; } B.prototype.getB = function () { console.log(this.b); };
方案一
這里我們可以讓B.prototype
成為A
的實例,那么B.prototype
中就擁有了私有方法a
,以及原型對象上的方法B.prototype.__proto__
即A.prototype
上的方法getA
。最后記得要修正B.prototype
的constructor
屬性,因為此時它變成了B.prototype.constructor
,也就是B
。
function A () { this.a = 100; } A.prototype.getA = function () { console.log(this.a); }; B.prototype = new A(); B.prototype.constructor = B; function B () { this.b = 200; } B.prototype.getB = function () { console.log(this.b); };
畫圖理解一下:
下面我們創(chuàng)建B
的實例,看下是否成功繼承了A
中的屬性和方法。
const b = new B(); console.log('b', b.a); b.getA(); console.log('b', b.b); b.getB(); // b 100 // 100 // b 200 // 200
方案二
我們也可以通過將父構(gòu)造函數(shù)當做普通函數(shù)來執(zhí)行,并通過call
指定this
,從而實現(xiàn)實例自身屬性的繼承,然后再通過Object.create
指定子構(gòu)造函數(shù)的原型對象。
function A () { this.a = 100; } A.prototype.getA = function () { console.log(this.a); }; // 繼承原型方法 // 創(chuàng)建一個新對象,使用一個已經(jīng)存在的對象作為新創(chuàng)建對象的原型 B.prototype = Object.create(A.prototype); B.prototype.constructor = B; function B () { // 繼承私有方法 A.call(this); // 如果有參數(shù)的話可以在這里傳入 this.b = 200; } B.prototype.getB = function () { console.log(this.b); };
這里我們再次通過畫圖的形式梳理一下邏輯:
下面我們創(chuàng)建B
的實例,看下是否成功繼承了A
中的屬性和方法。
const b = new B(); console.log('b', b.a); b.getA(); console.log('b', b.b); b.getB(); // b 100 // 100 // b 200 // 200
class extends實現(xiàn)繼承
在es6
中為開發(fā)者提供了extends
關(guān)鍵字,可以很方便的實現(xiàn)類之間的繼承:
function A () { this.a = 100; } A.prototype.getA = function () { console.log(this.a); }; // 繼承原型方法 // 創(chuàng)建一個新對象,使用一個已經(jīng)存在的對象作為新創(chuàng)建對象的原型 B.prototype = Object.create(A.prototype); B.prototype.constructor = B; function B () { // 繼承私有方法 A.call(this); // 如果有參數(shù)的話可以在這里傳入 this.b = 200; } B.prototype.getB = function () { console.log(this.b); };
下面我們創(chuàng)建B
的實例,看下是否成功繼承了A
中的屬性和方法。
const b = new B(); console.log('b', b.a); b.getA(); console.log('b', b.b); b.getB(); // b 100 // 100 // b 200 // 200
大家可能會好奇class
的extends
關(guān)鍵字是如何實現(xiàn)繼承的呢?下面我們用babel
編譯代碼,看下其源碼中比較重要的幾個點:
看下這倆個方法的實現(xiàn):
值得留意的一個地方是:extends
將父類的靜態(tài)方法也繼承到了子類中
class A { constructor () { this.a = 100; } getA () { console.log(this.a); } } A.say = function () { console.log('say'); }; class B extends A { constructor () { // 繼承私有方法 super(); this.b = 200; } getB () { console.log(this.b); } } B.say(); // say
extends的實現(xiàn)類似于方案二:
apply
方法更改父類this
指向,繼承私有屬性Object.create
繼承原型屬性Object.setPrototypeOf
繼承靜態(tài)屬性
結(jié)語
理解JavaScript
的原型原型鏈可能并不會直接提升你的JavaScrit
編程能力,但是它可以幫助我們更好的理解JavaScript
中一些知識點,想明白一些之前不太理解的東西。在各個流行庫或者框架中也有對于原型或原型鏈的相關(guān)應用,學習這些知識也可以為我們閱讀框架源碼奠定一些基礎(chǔ)。
到此這篇關(guān)于一文徹底理解JavaScript原型與原型鏈的文章就介紹到這了,更多相關(guān)JS原型與原型鏈內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript關(guān)于提高網(wǎng)站性能的幾點建議(一)
這篇文章主要介紹了JavaScript關(guān)于提高網(wǎng)站性能的幾點建議(一)的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-07-07利用NodeJS和PhantomJS抓取網(wǎng)站頁面信息以及網(wǎng)站截圖
這篇文章主要介紹了利用NodeJS和PhantomJS抓取網(wǎng)站頁面信息以及網(wǎng)站截圖的方法,提供實例代碼供大家參考2013-11-11微信公眾號JS-SDK獲取當前經(jīng)緯度以及地址信息的方法
最近微信JS-SDK開發(fā)過程中,遇到了獲取坐標位置的需求,所以下面這篇文章主要給大家介紹了關(guān)于微信公眾號JS-SDK獲取當前經(jīng)緯度以及地址信息的相關(guān)資料,需要的朋友可以參考下2022-06-06關(guān)于javascript模塊加載技術(shù)的一些思考
這篇文章主要介紹了關(guān)于javascript模塊加載技術(shù)的一些思考 ,需要的朋友可以參考下2014-11-11JS動態(tài)添加元素及綁定事件造成程序重復執(zhí)行解決
這篇文章主要給大家介紹了關(guān)于JS動態(tài)添加元素及綁定事件造成程序重復執(zhí)行的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面來一起看看吧。2017-12-12