一文徹底理解JavaScript原型與原型鏈
前言
JavaScript中有許多內(nèi)置對象,如:Object, Math, Date等。我們通常會這樣使用它們:
// 創(chuàng)建一個JavaScript Date實(shí)例 const date = new Date(); // 調(diào)用getFullYear方法,返回日期對象對應(yīng)的年份 date.getFullYear(); // 調(diào)用Date的now方法 // 返回自1970-1-1 00:00:00 UTC(世界標(biāo)準(zhǔn)時間)至今所經(jīng)過的毫秒數(shù) Date.now()
當(dāng)然,我們也可以自己創(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ū)別嗎?- 對象的實(shí)例為什么可以調(diào)用構(gòu)造函數(shù)的原型方法,它們之間有什么關(guān)系嗎?
接下來,讓我們帶著這些問題一步步深入學(xué)習(xí)。
new對函數(shù)做了什么?
當(dāng)我們使用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可以看到,當(dāng)我們使用new執(zhí)行函數(shù)的時候,new會幫我們在函數(shù)內(nèi)部加工this,最終將this作為實(shí)例返回給我們,可以方便我們調(diào)用其中的屬性和方法。
下面,我們嘗試實(shí)現(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;
}簡單用一下我們實(shí)現(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__這些屬性。你可能會疑惑這些屬性的具體用途,別急,我們馬上進(jìn)行介紹!
原型和原型鏈
在學(xué)習(xí)原型和原型鏈之前,我們需要首先掌握以下三個屬性:
prototype: 每一個函數(shù)都有一個特殊的屬性,叫做原型(prototype)constructor: 相比于普通對象的屬性,prototype屬性本身會有一個屬性constructor,該屬性的值為prototype所在的函數(shù)__proto__: 每一個對象都有一個__proto__屬性,該屬性指向?qū)ο?實(shí)例)所屬構(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()我們畫圖來描述一下上邊代碼中實(shí)例、構(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)用對象原型上的方法。
當(dāng)我們需要獲取實(shí)例上的某個屬性時:
上例中:
- 實(shí)例:
fn - 實(shí)例所屬類:
Fn
- 首先會從自身的私有屬性上進(jìn)行查找
- 如果沒有找到,會到自身的
__proto__上進(jìn)行查找,而實(shí)例的__proto__指向其所屬類的prototype,即會在類的prototype上進(jìn)行查找 - 如果還沒有找到,繼續(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)概念,下面讓我們通過一些實(shí)際例子來應(yīng)用一下吧!
借用原型方法
在JavaScript中,我們可以通過call/bind/apply來更改函數(shù)中this指向,原型上方法的this也可以通過這些api來進(jìn)行更改。比如我們要將一個偽數(shù)組轉(zhuǎn)換為真實(shí)數(shù)組,可以這樣做:
function fn() {
return Array.prototype.slice.call(arguments)
}
fn(1,2,3) // [ 1, 2, 3]這里我們使用arguments調(diào)用了數(shù)組原型上的slice,這是怎么做到的呢?我們先簡單模擬下slice方法的實(shí)現(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í)例(數(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ù)組原型上的方法。
當(dāng)我們將Array.prototype.slice方法的this指向arguments對象時,由于arguments擁有索引屬性以及length屬性,所以可以像數(shù)組一樣根據(jù)length和索引來進(jìn)行遍歷,從而相當(dāng)于用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í)例)改為了數(shù)組(Array的實(shí)例)和函數(shù)(Function的實(shí)例),相當(dāng)于為數(shù)組和函數(shù)調(diào)用了對象上的toString方法,而不是調(diào)用它們自身的toString方法。
通過借用原型方法,我們可以讓變量調(diào)用自身以及自己原型上沒有的方法,增加了代碼的靈活性,也避免了一些不必要的重復(fù)工作。
實(shí)現(xiàn)構(gòu)造函數(shù)之間的繼承
通過JavaScript中的原型和原型鏈,我們可以實(shí)現(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的實(shí)例,那么B.prototype中就擁有了私有方法a,以及原型對象上的方法B.prototype.__proto__即A.prototype上的方法getA。最后記得要修正B.prototype的constructor屬性,因?yàn)榇藭r它變成了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的實(shí)例,看下是否成功繼承了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ù)當(dāng)做普通函數(shù)來執(zhí)行,并通過call指定this,從而實(shí)現(xiàn)實(shí)例自身屬性的繼承,然后再通過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的實(shí)例,看下是否成功繼承了A中的屬性和方法。
const b = new B();
console.log('b', b.a);
b.getA();
console.log('b', b.b);
b.getB();
// b 100
// 100
// b 200
// 200class extends實(shí)現(xiàn)繼承
在es6中為開發(fā)者提供了extends關(guān)鍵字,可以很方便的實(shí)現(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的實(shí)例,看下是否成功繼承了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)鍵字是如何實(shí)現(xiàn)繼承的呢?下面我們用babel 編譯代碼,看下其源碼中比較重要的幾個點(diǎn):

看下這倆個方法的實(shí)現(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(); // sayextends的實(shí)現(xiàn)類似于方案二:
apply方法更改父類this指向,繼承私有屬性Object.create繼承原型屬性Object.setPrototypeOf繼承靜態(tài)屬性
結(jié)語
理解JavaScript的原型原型鏈可能并不會直接提升你的JavaScrit編程能力,但是它可以幫助我們更好的理解JavaScript中一些知識點(diǎn),想明白一些之前不太理解的東西。在各個流行庫或者框架中也有對于原型或原型鏈的相關(guān)應(yīng)用,學(xué)習(xí)這些知識也可以為我們閱讀框架源碼奠定一些基礎(chǔ)。
到此這篇關(guān)于一文徹底理解JavaScript原型與原型鏈的文章就介紹到這了,更多相關(guān)JS原型與原型鏈內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript關(guān)于提高網(wǎng)站性能的幾點(diǎn)建議(一)
這篇文章主要介紹了JavaScript關(guān)于提高網(wǎng)站性能的幾點(diǎn)建議(一)的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-07-07
利用NodeJS和PhantomJS抓取網(wǎng)站頁面信息以及網(wǎng)站截圖
這篇文章主要介紹了利用NodeJS和PhantomJS抓取網(wǎng)站頁面信息以及網(wǎng)站截圖的方法,提供實(shí)例代碼供大家參考2013-11-11
細(xì)數(shù)localStorage的用法及使用注意事項(xiàng)
這篇文章主要介紹了細(xì)數(shù)localStorage的用法及使用注意事項(xiàng),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-04-04
用js模仿word格式刷功能實(shí)現(xiàn)代碼 [推薦]
非常不錯的模仿word格式刷實(shí)現(xiàn)代碼。推薦大家參考下思路。2009-07-07
Javascript 字符串模板的簡單實(shí)現(xiàn)
這篇文章給大家描述的是如何一步步簡單的實(shí)現(xiàn)Javascript 字符串模板,對于初學(xué)javascript的菜鳥們來說應(yīng)該是篇不錯的文章,希望對大家能夠有所幫助。2016-02-02
微信公眾號JS-SDK獲取當(dāng)前經(jīng)緯度以及地址信息的方法
最近微信JS-SDK開發(fā)過程中,遇到了獲取坐標(biāo)位置的需求,所以下面這篇文章主要給大家介紹了關(guān)于微信公眾號JS-SDK獲取當(dāng)前經(jīng)緯度以及地址信息的相關(guān)資料,需要的朋友可以參考下2022-06-06
關(guān)于javascript模塊加載技術(shù)的一些思考
這篇文章主要介紹了關(guān)于javascript模塊加載技術(shù)的一些思考 ,需要的朋友可以參考下2014-11-11
JS動態(tài)添加元素及綁定事件造成程序重復(fù)執(zhí)行解決
這篇文章主要給大家介紹了關(guān)于JS動態(tài)添加元素及綁定事件造成程序重復(fù)執(zhí)行的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧。2017-12-12

