前言
在我一開始學習java web的時候,對JS就一直抱着一種只是簡單用用的心態,因而並無一步一步地去學習,當時認爲用法與java相似,可是在實際web項目中使用時卻比較麻煩,便直接粗略瞭解後開始使用jQuery。但現現在,前端發展迅速,js語法方便也有了至關大的改善,而且伴隨着node.js的登場,js的適用性也更加普遍。其實也是本身瞭解到了electron的存在,再加上web開發中前端與後端開發也比較密切,因而這便又掉頭回來從新開始學習js。在學習的過程當中,仔細學習了一下js的原型鏈,也在這裏作個記錄,若是有不對的地方,還請各位指出,本人感激涕零!javascript
正文
在js的世界中,一切皆對象,那麼咱們先將對象分爲三類:實例對象、原型對象、函數對象。前端
實例對象簡單說就是經過構造函數所建立的對象。java
函數對象好理解,js的函數自己也是個對象,這個對象有這方法名、參數、方法體等屬性。構造函數是一種特殊的函數,瞭解過其餘OOP語言都知道,構造函數每每會在實例對象建立的時候調用,主要是用來完成實例對象的初始化操做。可是在js中,構造函數與普通函數並不太大區別,咱們也能夠像使用普通函數同樣使用構造函數,即不使用new關鍵字。因此從本質上講,普通函數也是構造函數,而構造函數只是從功能上區分的一個稱呼,體如今代碼裏就是用不用new關鍵字。但爲了接下來的說明,下面將都會使用構造函數對象。node
原型對象比較特殊, 如今先暫時記住經過實例對象與函數對象都能找到對應的原型對象。web
這三類對象之間其實都有着聯繫,而經過這些聯繫就造成了js的完整的原型鏈。咱們接下來就按照這三類對象之間的關係來逐漸瞭解原型鏈。後端
實例對象與構造函數對象
首先來看實例對象與構造函數對象的聯繫。經過new關鍵字,咱們能夠經過構造函數獲得一個實例對象。例如:electron
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替換爲空對象,最後把這個對象返回。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的原型對象。