個人對JS原型鏈的一些理解(prototype、__proto__)
前言
在我一開始學習java web的時候,對JS就一直抱著一種只是簡單用用的心態,於是並沒有一步一步地去學習,當時認為用法與java類似,但是在實際web專案中使用時卻比較麻煩,便直接粗略瞭解後開始使用jQuery。但現如今,前端發展迅速,js語法方便也有了相當大的改善,並且伴隨著node.js的登場,js的適用性也更加廣泛。其實也是自己瞭解到了electron的存在,再加上web開發中前端與後端開發也比較密切,於是這便又掉頭回來重新開始學習js。在學習的過程中,仔細學習了一下js的原型鏈,也在這裡做個記錄,如果有不對的地方,還請各位指出,本人感激不盡!
正文
在js的世界中,一切皆物件,那麼我們先將物件分為三類:例項物件、原型物件、函式物件。
例項物件簡單說就是通過建構函式所建立的物件。
函式物件好理解,js的函式本身也是個物件,這個物件有這方法名、引數、方法體等屬性。建構函式是一種特殊的函式,瞭解過其他OOP語言都知道,建構函式往往會在例項物件建立的時候呼叫,主要是用來完成例項物件的初始化操作。但是在js中,建構函式與普通函式並不太大區別,我們也可以像使用普通函式一樣使用建構函式,即不使用new關鍵字。所以從本質上講,普通函式也是建構函式,而建構函式只是從功能上區分的一個稱呼,體現在程式碼裡就是用不用new關鍵字。但為了接下來的說明,下面將都會使用建構函式物件。
原型物件比較特殊, 現在先暫時記住通過例項物件與函式物件都能找到對應的原型物件。
這三類物件之間其實都有著聯絡,而通過這些聯絡就形成了js的完整的原型鏈。我們接下來就按照這三類物件之間的關係來逐漸瞭解原型鏈。
例項物件與建構函式物件
首先來看例項物件與建構函式物件的聯絡。通過new關鍵字,我們可以通過建構函式得到一個例項物件。例如:
function Student(name){ this.name = name; } var stu = new Student('wang');
在上面的片段中,Student是一個建構函式,stu則是一個通過Student建立的例項物件。二者的聯絡很明顯,而在js裡則體現在例項物件stu的constructor屬性中:
stu.constructor === Student; // true
那麼反過來,我們雖然不能通過建構函式物件直接找到它所有的例項物件,但是可以通過 instanceof
關鍵字來判斷一個物件是不是這個建構函式的例項物件:
stu instanceof Student; // true
原型物件與其他兩類物件
上面我們也說了,建構函式與普通函式沒有什麼區別,那麼直接使用建構函式,那this自然是指內建全域性物件window。但如果用new,this就指的是新的例項物件,而且這個方法還會返回這個例項物件。到這裡大致就能猜到加了關鍵字new做了什麼操作了,它建立了一個新的空物件,並且把建構函式中的this替換為空物件,最後把這個物件返回。
那麼為例項方法增加一個普通函式也這樣做,從結果來說是沒有問題的:
function Student(name){ this.name = name; this.say = function(){ console.log`I'm ${name}`; }; } stu1 = new Student('wang'); stu2 = new Student('li'); stu1.say(); // I'm wang stu2.say(); // I'm li stu1.say === stu2.say; // false
但是我們會發現,stu1與stu2的say函式物件竟然不是一個,那就說明如果建立了1000個Student,就會有1000個say函式物件出現,而這1000個say實現的功能完全一致,這對記憶體而言顯然是極大的浪費。
如何解決這個問題呢?既然多個函式完全一致,那麼自然可以把這個函式物件放在一個地方,當訪問stu1和stu2的say函式時,統一去拿這個地方的函式物件即可。如果我們自己實現這個功能,當在例項物件中使用函式物件時,我們又得自己去手動去公共的地方尋找函式物件,這麼做顯然太費勁了。
好在這些js都已經幫我們做了,每個建構函式物件都擁有一個prototype屬性,這個屬性指向的是一個物件,這個物件我們就叫它原型物件。而這個原型物件又擁有一個與例項物件一樣的constructor屬性,同樣也是指向建構函式物件。
另外, 對於每個物件 ,又都有一個__proto__的屬性指向它的原型物件。當我們訪問一個物件的某個屬性時,實際上是先在當前物件尋找這個屬性,如果沒有找到,則會繼續到__proto__所指的物件(原型物件)中尋找。
function Student(name){ this.name = name; } Student.prototype.say = function(){ console.log`I'm ${name}`; }; new Student('liu').say === new Student('zhang').say; // true
為方便理解,這裡再放一張圖,對照著這張圖下面的程式碼就容易看明白了,之後如果遇到不明白的也可以回過頭來看圖,直觀明瞭。
var chen = new Student('chen') chen.__proto__ === Student.prototype; // true chen.constructor === Student; // true chen.constructor === Student.prototype.constructor; // true
深入
明白了上面這些概念,我們把視角放大,不再侷限於Student。前面我們說到所有物件都有一個__proto__的屬性,那麼對於函式物件和原型物件自然也不例外,我們接下來的關注點就是這兩類物件的__proto__屬性。
首先來看函式物件。在前面的程式碼中,Student函式物件的__proto__是誰呢?答案是Function的原型物件。
Student.__proto__ === Function.prototype; // true
一切皆物件,那麼Function的__proto__又是誰?還是Function的原型物件:
Function.__proto__ === Function.prototype; // true
為什麼?因為Function也是個函式物件。通常我們建立函式的方式為
function xxx(x){...} var yyy = function(y){...};
那麼其實還有一種寫法:
var zzz = new Function('z','...'); // 例如: var hello = new Function('msg','console.log(msg)'); hello('hi'); // hi
這樣的寫法顯然能直接看出來,Function是個函式物件。於是便有一些有趣的事情了:
Function.__proto__ === Function.prototype; // true Function.constructor === Function; // true Function.constructor === Function.prototype.constructor; // true
Function是自己的函式物件,也是自己的例項物件:
var Function = new Function(...);
至於為什麼會這樣,這就比較像先有雞還是先有蛋的問題了。我們只需要知道 所有函式物件(包括Function)的__proto__都指向Function的原型物件 。
與Function類似,Object也是一個函式物件。(舉一反三,Array,String,Number等都是)
我們可以這樣建立一個空的Object:
var obj = new Object();
那麼Object的原型物件的__proto__是誰呢?是null。
Object.prototype.__proto__; // null
之前說過,當我們用.操作符去拿一個屬性時,js會先在當前物件裡尋找,沒有的話去__proto__的物件(原型物件)裡尋找。那麼如果__proto__(原型物件)裡還沒有,就繼續去它的__proto__裡尋找,以此重複。那麼什麼時候是個頭呢?直到__proto__為null時。
我們知道所有物件都有toString方法,Student的例項物件stu也是個物件,但我們明顯沒有給它新增toString方法,為什麼它會有呢?因為stu的__proto__最終指向的是Object的原型物件。這也就是js繼承的本質了。
stu.__proto__; // {constructor: ƒ} stu.__proto__.__proto__; // {constructor: ƒ, …, toString: ƒ, …} stu.__proto__.__proto__ === Object.prototype; // true stu.toString === Object.prototype.toString; // true