前言
在前端這塊領域,原型與原型鏈是每一個前端人員必須掌握的概念。我們多次在面試或者一些技術博客里面看見這個概念。由此可見,這個玩意對于前端來說有多重要。其實它本身理解起來不難,但是很多剛入行前端的同學,看到prototype、proto理解起來還是有點吃力,然后腦子里面就亂成一鍋粥,就像我一樣。但是這是很正常的事情,沒什么大不了的,就像我們想要學會跑步,那么我們就必須先學會走路。任何事情都是有個過程的。所以現(xiàn)在就跟我一起來攻克這個難點吧。通過這篇文章你將掌握以下知識點:
- 理解 _proto;
- 理解 prototype;
- 理解javascript中對象的概念;
- 理解原型和原型鏈;
- 理解javascript中類的概念;
- 理解new的實現(xiàn);
- 理解instanceof的實現(xiàn);
- 理解javascript的繼承;
- 加深對javascript這門語言的理解。
這也是本篇文章的寫作思路。
對象
那么我們就從對象這一概念開始說起,其實對象這一概念相信大家并不陌生。有一種說法是“javasrcript中萬物皆是對象”,其實這個說法是錯誤的,一個很簡單的例子,javasript中簡單基本類型(string、boolean、number、null、undefined、symbol)本身就不是對象。其實javasript中對象主要分為函數(shù)對象和普通對象。其中:
- String
- Number
- Boolean
- Object
- Function
- Array
- Date
- RegExp
- Error
這些都是函數(shù)對象,他們同時也被稱為內置對象。函數(shù)對象本身其實就是一個純函數(shù),javascript用他們來模擬類。普通對象就很簡單了,就是我們常見的對象:
const obj = {
name: 'juefei',
desc: 'cool'
}
可能說到這,你還是無法理解到底啥是函數(shù)對象,啥是普通對象,那我們就一起來看看下面的代碼:
const obj1 = {};
const obj2 = new Object();
function func1() {
}
const obj3 = new func1();
const func2 = new function() {
}
const func3 = new Function()
接著我們來分別打印一下他們:
console.log(obj1); // object
console.log(obj2); // object
console.log(obj3); // object
console.log(func1); // function
console.log(func2); // function
console.log(func3); // function
所以可以看見,obj1、obj2、,obj3是普通對象,他們都是Object的實例,而func1、func2、func3則都是Function的實例,稱為函數(shù)對象。我們再看看:
console.log(typeof Object); //f unction
console.log(typeof Function); // function
你是不是驚呆了,原來Object和Function都是 Function的實例。所以我們得出一個結論就是:
只要是Function的實例,那就是函數(shù)對象,其余則為普通對象。
同樣我們也可以看出,不僅 Object 是函數(shù)對象,就連 Function 本身也是函數(shù)對象,因為我們通過 console.log(typeof Function); 得知 Function 是 Function 的實例。是不是又開始有點繞了?沒事,到這一步你就記住我們剛剛的結論就算完成目標:
只要是Function的實例,那就是函數(shù)對象,其余則為普通對象。
那么說到對象,我們從上面可以看出,一個對象是通過構造函數(shù) new 出來的,這其實跟原型和原型鏈有很大的關系,那么原型和原型鏈到底是用來干嘛的呢?
原型
涉及到這兩個概念,我們就必須先來介紹兩個東西: proto 和 prototype ,這兩個變量可以說,在 javascript 這門語言里面隨處可見,我們不管他三七二十一,我們先來看一張表:
對象類型 proto prototype
普通對象 ? ?
函數(shù)對象 ? ?
所以,請你先記住以下結論:
只有函數(shù)對象有 prototype 屬性,普通對象 沒有這個屬性。
函數(shù)對象 和 普通對象 都有 proto這個屬性。
prototype 和 proto都是在創(chuàng)建一個函數(shù)或者對象會自動生成的屬性。
接著我們來驗證一下:
function func (){ //func稱為構造函數(shù)
}
console.log( typeof func.prototype); // object
console.log(typeof func.__proto__); // function
const obj = {}
console.log(typeof obj.__proto__) //object
console.log(typeof obj.prototype) //undefined
(看見了吧,普通對象真的沒有 prototype 屬性)
所以就驗證了我們剛剛的結論:
只有函數(shù)對象有 prototype 屬性,普通對象 沒有這個屬性
函數(shù)對象 和 普通對象 都有 proto這個屬性。
prototype 和 proto都是在創(chuàng)建一個函數(shù)或者對象會自動生成的屬性。
你看我又重復寫了一遍,我不是為了湊字數(shù),是為了你加深記憶,這對于我們接下來的篇幅很重要。接著我們來看看下面的代碼:
console.log(obj.__proto__ === Object.prototype); // true
console.log(func.__proto__ === Function.prototype); // true
所以我們又得出如下結論:
實例的 proto屬性主動指向構造的 prototype;
prototype 屬性被 proto 屬性 所指向。
這就是prototype 屬性和 proto 屬性的區(qū)別與聯(lián)系。這可能又有點繞了,來多看幾遍這一節(jié),多背一下我們的結論。我們繼續(xù)。
那么問題來了,既然func是一個函數(shù)對象,函數(shù)對象是有 prototype 屬性的,那么func.prototype.proto等于啥呢?
為了解決這個問題,我們來思考一下:
首先,我們看看func.prototype 是啥:
console.log(typeof func.prototype); //object
好,我們知道了,func.prototype 是一個對象,那既然是對象,那 func.prototype 那不就是 Object的實例嗎?那也就是說,func.prototype.proto屬性肯定是指向 Object.prototype 咯!好,我們來驗證一下:
console.log(func.prototype.proto === Object.prototype); //true
看見沒有,就是這樣的。那看到這里,我們應該也知道當我們這創(chuàng)建一個構造函數(shù)的時候,javascript是如何幫我們自動生成proto和prototype屬性的。哈哈沒錯就是這樣:
//我們手動創(chuàng)建func函數(shù)
function func() {}
//javascript悄悄咪咪執(zhí)行以下代碼:
func._proto = Function.prototype; //實例的 proto 屬性主動指向構造的 prototype
func.prototype = {
constructor: func,
__proto: Object.prototype //我們剛剛才在上面驗證的,你別又忘記了
}
所以prototype又被稱為顯式原型對象,而proto又被稱為隱式原型對象。
hi,看到這里,你是不是有種腦子開了光的感覺。哈哈,所以到現(xiàn)在你應該已經理解原型的概念了,如果你還不理解,那就把上述章節(jié)再看一遍。最好拿個紙筆出來跟著畫一畫,順便拿出電腦把示例代碼敲一敲。好,整理一下頭腦,接下來我們來看看什么又是原型鏈。
原型鏈
再介紹這個概念之前,我們先來看如下代碼:
function Person = function(name,desc){
this.name = name;
this.desc = desc;
} //***1****//
Person.prototype.getName = function(){
return this.name;
}//***2****//
Person.prototype.getDesc = function(){
return this.desc;
}//***3****//
const obj = new Person('juefei','cool');//***4****//
console.log(obj);//***5****//
console.log(obj.getName);//***6****//
接下來我們來逐步解析一下:
創(chuàng)建了一個構造函數(shù) Person,此時,Person.portotype自動創(chuàng)建,其中包含了 constructor 和 proto兩個屬性;
給對象 Person.prototype 新增了一個方法 getName;
給對象 Person.prototype 新增了一個方法 getDesc;
根據(jù)構造函數(shù) Person 新建一個實例: obj(在創(chuàng)建實例的時候,構造函數(shù)會自動執(zhí)行);
打印實例 obj :
{
name: 'juefei',
desc: 'cool'
}
根據(jù)上面一節(jié)的結論,我們得出:
obj.proto = Person.prototype;
執(zhí)行到第6步時,由于在實例 obj 上面找不到 getName()這個方法,所以它就會自動去通過自身的 proto 繼續(xù)向上查找,結果找到了 Person.prototype ,接著它發(fā)現(xiàn),剛好 Person.prototype 上面有getName()方法,于是找到了這個方法,它就停止了尋找。怎么樣,是不是有一種環(huán)環(huán)相扣的感覺?他們形成一個鏈了,沒錯,這就是原型鏈。
我們得出如下結論:
在訪問一個對象(假設這個對象叫obj)的屬性/方法時,若在當前的對象上面找不到,則會嘗試通過obj.proto去尋找,而 obj.proto 又指向其構造函數(shù)(假設叫objCreated)的 prototype,所以它又自動去 objCreated.prototype 的屬性/方法上面去找,結果還是沒找到,那么就訪問 objCreated.prototype.proto繼續(xù)往上面尋找,直到找到,則停止對原型鏈對尋找,若最終還是沒能找到,則返回 undefined 。一直沿著原型鏈尋找下去,直到找到 Object.prototype.proto,指向 null,于是返回 undefined了。
是不是自然而然就理解了。我又給你畫了個圖(請對照著上面??那個圖看):
接下來我們再來增加一些概念:
任何內置函數(shù)對象本身的 proto屬性都指向 Function的原型對象,即:Function.prototype;
除了 Object.prototype.proto指向 null ,所有的內置函數(shù)對象的原型對象的 proto屬性 ( 內置函數(shù)對象.prototype.proto),都指向Object。
我們得出如下終極原型鏈的圖:
針對這個圖,我最終給出我們經??匆娔莻€原型鏈的圖:
好好對比一下,拿出紙和筆畫一畫,根據(jù)上面章節(jié)的講解,相信你很容易就能明白。
javascript中的類
剛剛我們終于明白什么是 原型 和 原型鏈。下面我們根據(jù)上面的概念來講解一下javascript中的類。我們知道,在面向對象的語言中,類可以被實例化多次,這個實例化是指我們可以根據(jù)構造函數(shù)去獨立復制多個獨立的實例,這些實例之間是獨立的。但是實際上在 javascript 卻不是這樣的,因為它不是這種復制機制。我們不能創(chuàng)建一個類的多個實例,我們只能創(chuàng)建這個類的多個對象,因為他們都是通過原型和原型鏈關聯(lián)到同一個對象。所以在 javascript 中 ,類都是通過原型和原型鏈來實現(xiàn)的,它其實是一種委托方式。
new的實現(xiàn)
了解了上面javascript中的類的概念,那我們應該很容易就理解new的過程,其核心無非就是執(zhí)行原型鏈的鏈接:
function myNew(Cons,...args){
let obj = {};
obj.__proto__ = Cons.prototype; //執(zhí)行原型鏈接
let res = Cons.call(obj,args);
return typeof res === 'object' ? res : obj;
}
instanceof的實現(xiàn)
那么學習了原型和原型鏈,instanceof的實現(xiàn)肯定也很簡單了,它也是通過原型和原型鏈來實現(xiàn)的:
function myInstanceof(left,right){
let rightProto =right.prototype;
let leftValue = left.__proto__;
while(true){
if(leftValue === null){
return false;
}
if(leftValue === rightProto){
return true;
}
leftValue = leftValue.__proto__;
}
}
我就不講解過程了,因為我知道你肯定能看懂,哈哈。
javascript的繼承
我們都知道繼承也是通過原型和原型鏈來實現(xiàn)的,那我在這里介紹兩種常見的繼承方式:
組合繼承:
// 組合式繼承
//通過call繼承Parent的屬性,并傳入?yún)?shù)
//將Child的原型對象指向Parent的實例,從而繼承Parent的函數(shù)
function Parent(value){
this.val = value;
}
Parent.prototype.getValue = function(){
console.log(this.val);
}
function Child(value){
Parent.call(this,value);//繼承Parentd的屬性
}
Child.prototype = new Parent();
寄生組合式繼承:
//寄生組合式繼承
//通過call繼承Parent的屬性,并傳入?yún)?shù)
//通過Object.create()繼承Parent的函數(shù)
function Parent(value){
this.val = value;
}
Parent.prototype.getValue = function(){
console.log(this.val);
}
function Child(value){
//繼承Parentd的屬性
Parent.call(this,value);
}
Child.prototype = Object.create(Parent.prototype,{
constructor:{
value:Child,
writable:true,
configurable:true,
enumerable:false
}
})
總結
若 A 通過 new 創(chuàng)建了 B,則 B.proto = A.prototype;
執(zhí)行B.a,若在B中找不到a,則會在B.proto中,也就是A.prototype中查找,若A.prototype中仍然沒有,則會繼續(xù)向上查找,最終,一定會找到Object.prototype,倘若還找不到,因為Object.prototype.proto指向null,因此會返回undefined;
原型鏈的頂端,一定有 Object.prototype.proto ——> null。