javascript原型和原型鏈是js中的重點也是難點,理論上來說應該是屬於面向對象編程的基礎知識,那么我們今天為什么要來講這個呢?(因為我也忘了,最近看資料才揭開面紗…… 哈哈哈)
好了,直接進入正文。在js的編程世界中,萬物皆對象;不管你是數組還是函數還是對象,都是屬於對象類型;那么這么多對象,如何進行管理呢?js中把對象分為實例對象、函數對象、原型對象三大類;
實例對象:
通過構造函數(所謂構造函數我們可以簡單理解為進行new操作的函數就是構造函數)所創建的對象都是實例對象;
var people = new Student(); console.log(people)
上面代碼中的people就是一個實例對象,Student 就是一個構造函數;
函數對象:
函數對象我們可以簡單的理解為函數,因為在js中函數本身就屬於一個對象;而上述代碼中的Student是一個構造函數,構造函數是一種特殊的函數,構造函數往往都是在實例對象創建后才進行調用,作用是對實例對象進行初始化的操作;構造函數和普通函數的區分僅僅只是功能區分的一個稱呼,體現在JS代碼中的區別就是new和不new的區別;有new關鍵字就是新建一個構造函數,沒有new關鍵字就是新建一個普通函數;
原型對象:
原型對象我們可以簡單的理解為原型對象是實例對象和函數對象的父對象,俗稱:爸爸,並且通過實例對象和函數對象都能找到原型對象;
我們先來看看實例對象和函數對象的關系:
function people(name){ this.name=name; console.log(this.name); } var Student=new people("咸魚");
上面代碼中我們可以看到people是構造函數,Student是實例對象,二者之間的關系體現在constructor屬性中;
Student.constructor==people;//true
我們的實例對象對象中是可以通過constructor屬性來訪問到構造函數的,那么反過來可以實現嗎?
答案是:不行!!! 但是我們可以變通一下通過instanceof方法來進行檢查
Student instanceof people //true
通過這種方式,我們可以間接的檢查一個對象是否是構造函數的實例對象
new關鍵字做了什么?
上面講了構造函數和普通函數在JS中沒有什么太大的區別,所以我們可以想到那么直接使用函數的話,this的指向肯定是瀏覽器全局對象window,那么我們new一下之后,this的指向將會改變為新的實例對象,並且還會將這個新的實例對象返回回來;
所以我們可以總結new做了什么:
1.新建了一個空的函數對象;
2.改變this的指向,將this的指向改為接收新函數對象的實例對象
3.返回這個新的實例對象
大家來看一個小demo:
function people(name){ this.name=name; this.say=function(){ console.log(this.name); } } var Student=new people("咸魚"); var man=new people("張三") Student.say(); //咸魚 man.say(); //張三 console.log(Student.say === man.say); //false
從上面代碼中我們可以看到我們有兩個實例對象分別是Student、man;有兩個構造函數people;我們可以看到最后兩個結果進行比較是不相等的,這就說明兩個函數對象不是同一個,這就驗證了上面的new關鍵字,每new一下,就會新建一個空白的函數對象;如果我們要做的事情都一樣,每一次都需要去重新new一個對象,如果是大量重復的做100次同樣的事情呢?難道我們要去new100次嗎?這會給內存帶來極大的浪費;
那現在怎么解決呢?JS官方都已經替我們想好了辦法,我們可以把這個方法放到原型對象上,每次我們需要用的時候去調用就可以了;那么我們怎么放到原型對象上呢?
JS中每一個構造函數都有一個prototype屬性,這個屬性指向的是另一個對象,這個對象就是原型對象;通過這個屬性我們可以直接找到原型對象,當然原型對象也是一個對象,畢竟萬物皆對象嘛;同時原型對象中也有一個constructor屬性,這個屬性也是指向構造函數;
對了,JS中每一個對象都有一個__proto__屬性(注意是左右兩邊各兩個下划線),我們可以通過這個對象直接訪問到原型對象;構造函數可以直接通過prototype直接訪問到原型對象,實例對象可以通過__proto__直接訪問到原型對象,這之間就有了聯系;所以得到結論:實例對象.__proto__===函數對象.prototype,因為這兩者都是訪問到原型對象,共同的爸爸了;
我們可以將上面的代碼改造一下:
function people(name){ this.name=name; people.prototype.say=function(){ console.log(this.name); } } var Student=new people("咸魚"); var man=new people("張三") Student.say(); //咸魚 man.say(); //張三 console.log(Student.say === man.say); //true
這樣我們就可以直接將say方法直接掛到原型對象上面了;
這里有個圖例可以對照着看,更方便理解:
function Student(name){ this.name=name; } var xianyu = new Student('xianyu'); console.log(xianyu.__proto__ === Student.prototype); // true console.log(xianyu.constructor === Student); // true console.log(xianyu.constructor === Student.prototype.constructor); // true
得出的結果如下:
所以我們可以總結一下結論:
1.實例對象通過__proto__和函數對象通過prototype都是能夠訪問到原型對象的;
2.實例對象通過constructor屬性可以訪問到構造函數,因為實例對象的constructor屬性直接指向構造函數;
3.原型對象上面的constructor屬性跟實例對象一樣,都是指向其構造函數
原型鏈:
對象可以通過“.”操作獲取到一個屬性的值,首先會在對象自身開始查找,如果查不到會到原型對象(__proto__)中去查找,如果原型對象中還沒有就會把當前得到的原型對象當作實例對象,繼續通過(__proto__)去查找當前原型對象的原型對象中去找,也就是去爸爸的爸爸那里找,直到__proto__為null時停止;
上面可能有點拗口,我們換個生活中的例子來理解:就是你有個對象,你丈母娘問你要20W彩禮,你自己拿不出來你沒有(對象本身沒有屬性),丈母娘讓你問你爸爸(原型對象)要,如果你爸爸也沒有,你爸爸就得問你爸爸的爸爸也就是你爺爺要(原型對象的原型對象,這里的第一個原型對象實質上成了一個實例對象,因為你爸爸在你爺爺那里永遠是兒子),如果你爺爺也沒有就繼續往上要……如此反復,實在拿不出來(null),丈母娘大手一揮,拿不出來就不嫁了(直到為null時停止);
原型鏈實現繼承:
我們知道所有的對象都有個toString()方法,上述代碼中實例對象xianyu其實也是一個對象,這就是JS中的萬物皆對象的道理,我們沒有給xianyu加任何toString()方法,它是哪里來的?繼承來的!因為xianyu.__proto__最終指向的是Function原型對象(Function函數對象一直往上查找原型對象最終是Function原型對象,Object函數對象一直往上查找原型對象最終是null),因為Function原型對象中有,所以xianyu作為它的實例會繼承上面的方法,這就是JS繼承的本質;
也就是說你爹(原型對象)那有20W,沒花在銀行存着,你(實例對象)也可以理解為你有20W在銀行存着,因為你爹的錢遲早會給你,你爹的錢間接就是你的錢,你要是還存着,你的兒子(實例對象的實例對象)也就間接有了20W;
總結:
1.實例對象可以通過__proto__訪問原型對象,函數對象可以通過prototype訪問原型對象;
2.原型對象上面的方法一定會繼承給下屬的實例對象,反之如果要給原型對象上添加方法需要通過 函數對象.prototype.方法名 或 實例對象.__proto__.方法名 進行添加;
3.原型對象實質上也是一個對象,原型對象上面也會有__proto__、constructor等屬性,所以原型對象可以通過 原型對象.__proto__來訪問原型對象的原型對象,也可以通過constructor來知道自己是屬於哪個構造函數上面的實例對象;