JavaScript原型鏈深入理解
在JS中,原型鏈是一個重要的概念,不管是繼承還是屬性值的查找中,都用到了原型鏈的基本知識,有些朋友經常問我一些關于原型鏈的問題,今天整理一下自己對原型鏈的理解,下次我就不用在去解釋了,直接看文章。
首先,大家都知道在JS中有六種 基本數據 類型和一種 復雜類型 。
六種基本數據類型:
- String
- Number
- Boolean
- null
- undefined
- Symbol(ES2015新加入,解決屬性名的沖突問題。
另外一種復雜類型自然而然就是 Object ,有的人也說JS中 一切皆是對象 。上面的六種基本數據類型中,除了null和undefined沒有構造函數外,其他4種都對應有其構造函數對象,有時利用這些構造函數可以強制轉換數據類型。
要想講清楚原型鏈的一些問題,還有一個特殊的Object對象必須事先說清楚,那就是 Function 對象。它也是對象,只不過比其他普通對象復雜點罷了。
組合繼承
首先我們先寫一個大家熟悉的組合繼承(原型鏈+構造函數),然后根據這個組合繼承去了解原型鏈到底是怎樣連接起來的。
//父類
function Person(name,age){
if(this instanceof Person){
this.name = name;
this.age = age;
}else {
Person.call(this,name,age);
}
}
Person.prototype.say = function(){
console.log('我叫' + this.name +',今年' + this.age + '歲了!')
}
//子類
function Student(name,age,gride){
if(this instanceof Student){
Person.call(this,name,age);
this.gride = gride;
}else {
Student.call(this,name,age,gride);
}
}
Student.prototype = new Person();
Student.prototype.constructor = Student;
Student.prototype.say = function(){
console.log('我叫' + this.name +',今年' + this.age + '歲了,考試考了' + this.gride + '分!');
}
var stu = new Student('chping',23,100);
stu.say();
此時會輸出 我叫chping,今年23歲了,考試考了100分! ,這說明我們的組合繼承就實現了,下面可以根據這個例子逐句解釋一下原型鏈的相關問題,順便講解這個組合繼承了。
第一個問題:安全使用構造函數
首先可能你對 Person 和 Student 里面的那個判斷有些疑問,你可能是下面這樣寫構造函數:
function Person(name,age){
this.name = name;
this.age = age;
}
不這樣寫,主要是 防止構造函數被執行 ,因為一旦構造函數執行,其內的屬性值會被掛載到window上面去了(當構造函數執行的時候,里面的 this 是指向window的)。
好的,這個問題解決了,在看第二個問題。
第二個問題:prototype對象
接著我們來體會一下下面這句話:
每一個函數對象都有一個prototype屬性,該屬性指向其prototype對象。
這句話相信你已經聽了很多遍了,可能并不是這樣說的,但是就是這么個意思。那么這句話應該怎么理解呢?
其實也不難,先看前半部分 每一個函數對象都有一個prototype屬性 。這半句就是說 函數對象 默認有一個屬性,這個屬性叫 prototype 。另外,函數對象就是指用 function 聲明的對象(補充一下:在ES2015之前,有兩種聲明變量的方式, function 用來聲明函數變量, var 用來聲明普通變量。但是在ES2015中新加了 let 和 const )。
再看一下后半部分 該屬性指向其prototype對象 。后半句的意思就是說 prototype 屬性指向的是一個也叫 prototype 的對象,該對象是隨著函數對象而產生的。也就是說,只要通過 function 定義一個函數對象,就會生成一個 prototype對象 ,并在函數上生成一個 prototype屬性 來指向該 prototype屬性 。用圖來表示一下就是下面的樣子:
構造函數與其prototype對象
從圖中(此圖很丑,歡迎投稿:))你可以看到還有一個知識點,就是 prototype對象 中還有一個 constructor 對象,該對象又指向了構造函數,這也是一個需要注意的知識點,后面我們在展開來說,這里先記一下。
通過上面的這句話,我們可以聯想到,Object好像也是一個構造函數,Function好像也是一個函數,他們是不是也是這樣的呢?回答是肯定的,他們也是這樣的。如下圖:
Object的構造函數與其prototype對象
;
Function的構造函數與其prototype對象
第三個問題:__proto__ 屬性
先說明一下, __proto__ 的寫法是前后各兩個英文輸入法下的下劃線,不是一個。
然后我們再來看這樣一句話:
每一個對象都有一個__protto__屬性,該屬性指向創建這個對象的構造函數的prototype對象。
這句話稍微有點繞,我再來解釋一下這句話。這句話的前半部分比較好理解,就是說JS中的每一個對象都有一個屬性,這個屬性的名字叫做 __proto__ ,還要再說的話,就是注意 JS中一切皆是對象 這句話。
這句話后半部分有點繞,我們把它分成兩句話去理解:
- 創建這個對象的構造函數
- 的prototype對象
這樣就明白了,但是創建這個對象的構造函數怎么確定呢?這是個問題,不好解釋,我也解釋不好。就總結一下:
- function定義的函數對象的 __proto__ 屬性指向Function對象的prototype對象 。
- 非function定義的對象的 __proto__ 屬性指向創建它的構造函數的prototype對象 。(就是都指向Object的prototype對象)
- Object的prototype對象的 __proto__ 指向null 。
還是看圖吧:
對象的 proto 屬性
相信通過圖你已經看懂了 __proto__ 屬性的指向問題了。
思考組合繼承
//父類
function Person(name,age){
if(this instanceof Person){
this.name = name;
this.age = age;
}else {
Person.call(this,name,age);
}
}
//上面通過function聲明了一個函數對象,那么該對象的肯定有一個prototype屬性,
//并且指向其prototype對象。我們可以打印驗證一下
console.log(Person.prototype);
/*打印結果如下
{
constructor: function Person(name,age)
arguments:null
caller:null
length:2
name:"Person"
prototype:Object
__proto__:function()
,
__proto__: Object
}
*/
這說明 prototype對象 就是一個空對象添加了一個 constructor屬性 。
另外,也看到了 prototype 對象有一個 __proto__ 屬性,指向Object,先記住。
此外,我們也看到了 constructor 指向的Person函數確實存在prototype屬性和 __proto__ 屬性,以及其指向問題,我們同時也可以打印驗證一下:
console.log(Person.prototype.constructor === Person);//true
console.log(Person.prototype.__proto__=== Object.prototype);//true
console.log(Person.__proto__=== Function.prototype);//true
我們再來驗證一下 Object 和 Function 的prototype屬性,constructor屬性還有 __proto__ 屬性
console.log(Object.prototype);
console.log(Object.prototype.constructor === Object);//true
console.log(Object.prototype.__proto__ === null);//true
console.log(Object.__proto__ === Function.prototype);//true
console.log(Function.prototype);
console.log(Function.prototype.constructor === Function);//true
console.log(Function.prototype.__proto__ === Object.prototype);//true
console.log(Function.__proto__ === Function.prototype);//true
OK,相信大家對原型鏈有了一定了解了。我們接著往下看。
Person.prototype.say = function(){
console.log('我叫' + this.name +',今年' + this.age + '歲了!')
}
`
在Person的 prototype對象 上添加了一個say方法,和給普通對象添加方法并沒有區別,只不過在稍后使用的時候才會展現出它的與眾不同。
接下來是Student類:
function Student(name,age,gride){
if(this instanceof Student){
Person.call(this,name,age);
this.gride = gride;
}else {
Student.call(this,name,age,gride);
}
}
在此時,Student和Person以 Person.call(this,name,age); 這一句代碼產生了聯系,此時Student僅僅是通過構造函數繼承的方式調用了Person,這并不是本文重點,我們此時可以認為原型鏈上Student和Person并沒有任何聯系,讓它們在原型鏈上產生聯系的是下面這條語句。
Student.prototype = new Person();
Student.prototype.constructor = Student;
我們來想想這兩條語句都干了啥?
首先我們知道,Student函數對象在被 function 聲明的時候已經生成了其 prototype 對象,并且通過 prototype屬性 建立了聯系。
這里的第一條語句居然是, 改變了Student函數對象的prototype屬性指向,不再指向function聲明時自動生成的prototype對象,而是指向Person函數對象的一個實例對象。 讓我們用圖展示一下,就成了下面這樣:
原型鏈繼承關鍵步驟圖解
OK,原型鏈繼承就這樣實現了。但是由于我們讓 Student 函數對象的 prototype 屬性重新指向了一個 Person 函數對象的實例,而這個實例對象里面是不可能有 constructor 屬性的,自然也不會指向 Student (為什么沒有呢?前面已經說了,因為只有用 function 聲明函數對象的時候,自動生成的 prototype對象 中才默認有 constructor屬性 ,其他對象不會有)。
接下來就是在Student函數對象新指向的prototype對象上添加say方法:
Student.prototype.say = function(){
console.log('我叫' + this.name +',今年' + this.age + '歲了,考試考了' + this.gride + '分!');
}
這個就沒有說的必要了,就是在對象上加了一個方法,只不過這個對象有些特別罷了。然后就是通過Student構造函數來生成實例:
var stu = new Student('chping',23,100);
stu.say();
此時,我們還是要看看這兩句干了啥?
先看第一句, new Student('chping',23,100); ,其中我們必須先得知道關鍵字 new 做了什么:
var obj = {}
Student.call(this,'chping',23,100);
obj.__proto__ = Student.prototype;
return obj;
這樣相信你就明白,第一句干的活了:
- 首先創建一個空對象。
- 將屬性掛載到該空對象上。
- 將空對象的 __proto__ 屬性連接到Student函數對象的prototype對象上,來生成原型鏈。
- 返回該對象給stu變量。
接下來就是第二句 stu.say() ,這句話的意思就是 stu實例 對象調用 say方法 ,但是在查找的時候發現, stu實例 對象上并沒有這個方法,于是 原型鏈 就來了。此時他會根據其 __proto__ 屬性來查找 Student 函數對象的 prototype對象 上有沒有 say方法 ,然后它發現正好有一個 say方法 ,于是就可以執行該方法了。
此時又產生一個問題,在執行Student函數對象的prototype上的say方法時,里面的 this 指向誰呢?
可以想一下,此時 say 方法是被誰調用的,很明顯是 stu 實例對象,所以 this 指向 stu ,所以, this.name 、 this.age 、 this.gride 就是實例對象 stu 上面的 chping 、 23 、 100 了。
下面我們再來看一下完整的原型鏈繼承的圖解,如果你能完全看懂這張圖,那么你對原型鏈的理解也就差不多了。
原型鏈繼承圖解
結語
原型鏈的基礎知識差不多通過上面這個例子就介紹完了,我們來總結一下:
- 每一個函數對象都有一個prototype屬性,該屬性指向其prototype對象 。
- 每一個對象都有一個 __protto__ 屬性,該屬性指向創建這個對象的構造函數的prototype對象 。
- function定義的函數對象的 proto 屬性指向Function對象的prototype對象 。
- 非function定義的對象的 proto 屬性指向創建它的構造函數的prototype對 。(就是都指向Object的prototype對象)
- Object的prototype對象的 proto 指向null 。
然后在看看下面幾個常見的原型鏈的小題目,相信你對原型鏈會有一個新的認識了。
第一題
function Person(){
this.name = 1;
}
var person1 = new Person();
Person.prototype.name =2;
console.log(person1.name);
console.log(person1.__proto__.name);
Person.prototype = {
name:3
}
console.log(person1.name);
console.log(person1.__proto__.constructor);
console.log(person1.__proto__.name);
var person2 = new Person();
console.log(person2.__proto__.name);
console.log(person2.__proto__.constructor == Object);
console.log(person2.name);
上面的 console.log 會打印什么?
這個題考察的是對prototype對象的理解。
第二題
function Outer() {
this.a = 1;
}
function Inner() {}
var outer = new Outer();
Inner.prototype = outer;
var inner = new Inner();
inner.a = inner.a + 1;
console.log(inner);
console.log(outer);
猜猜上面會是什么結果?
這個題考察的是對實例對象上屬性的理解。
第三題
var animal = function(){}
var dog =function(){}
animal.price = 2000;
dog.prototype = animal;
var dd = new dog();
console.log(dog.price);
console.log(dd.price);
在分析一下這個題目的輸出結果?
這個題目考察的是 __proto__ 屬性的理解。
第四題
下面放大招了,這個題目可能不完全是原型鏈的問題,對JS基礎知識的一個綜合考察,可以試一試:
var a = new Object();
a.param = 123;
function foo(){
get = function(){
console.log(1);
};
return this;
}
foo.get = function(){
console.log(2);
};
foo.prototype.get = function(){
console.log(3);
};
var get = function(){
console.log(4);
};
function get(){
console.log(5);
}
foo.get();
get();
foo().get();
get();
new foo.get();
new foo().get();
new new foo().get();
來自:http://www.jianshu.com/p/b745c5481fab