前言 在我一開始學習java web的時候,對JS就一直抱著一種只是簡單用用的心態,於是並沒有一步一步地去學習,當時認為用法與java類似,但是在實際web項目中使用時卻比較麻煩,便直接粗略瞭解後開始使用jQuery。但現如今,前端發展迅速,js語法方便也有了相當大的改善,並且伴隨著node.js的 ...
前言
在我一開始學習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
所以,遍歷所有對象的__proto__最終都會來到Object的原型對象。