?簡介
下面這張圖大家應該很很熟悉了,各位大佬講原型及原型鏈的時候是大部分都會用到下面這張圖片的
我想以自己的方式來講述一遍,一是幫助我自己更好的複習,二是希望能夠幫助到想要複習或者學習原型的同學
- 在講解之前,我先講點基礎概念
- JS物件中他有一個內建原型
[[prototype]]
,這個原型就是隱式原型__proto__,而建構函式因為其本身就是 一個物件,所以它有一個隱式原型,但是建構函式又是一個函式,所以他有一個特殊的屬性顯式原型prototype
,大家也可以看到constructor
這個屬性,預設情況下,函式物件上都會有一個constructor
屬性,裡面存放著是函式的原型物件 - 好,接下來我會分步講解這張圖的內容,同時會輔以圖片來講解
?建構函式與例項化的關係
- 我們先看這個地方
function Foo()
他是一個建構函式 - 那麼它的顯式原型一定是指向
Foo
的原型物件上 - 同時我們看
f1
這個例項化Foo的物件,因為是物件,所以它只有一個__proto__
隱式原型的 - ❓我們可以看到圖片中,隱式原型是指向了
Foo
的原型物件上的,這是為什麼呢? - ✅其實這裡我要講下
new
這個操作 到底幹了些什麼,這應該是一道經典的面試題吧- 1️⃣ 首先會建立一個空物件
{}
- 2️⃣ 將
this
指向該空象,然後將建構函式的顯式原型賦予給這個物件的隱式原型上去__proto__=prototype
- 3️⃣ 開始執行建構函式內的程式碼,將建構函式內部屬性和方法賦值給該物件
- 1️⃣ 首先會建立一個空物件
- ?所以大家看,
f1
的隱式原型是指向了Foo
的顯式原型的吧,是不是很容易理解呢
- 接下來我們接著往下看,Foo的原型物件也就是一個普通的物件,所以他也就一個隱式原型,他的隱式原型就是指向
Object
的原型物件的,小夥伴看到這裡會不會很懵? 覺得為啥就是指向Object
的原型物件呢? - 我們重點看向o1這個普通物件,大家可能對
new Object
會有點陌生,因為大家建立物件的時候,往往都是通過建立一個字面量來申明一個物件的const obj={}
,實際上這個操作就是等於new Object()
.所以看到了new Object()
大家有沒有明白什麼呢?? - 哦 原來
Object
是一個建構函式,那麼他的顯式原型就是指向Object.Prototype
了,即原型物件,那麼在例項化Object
的時候,就會做我上面講的new操作符操作了,所以那個Foo.prototype
的隱式原型指向那個Object.prototype
是不是理解了呢??
- 接下來,我們要把重點放在建構函式
Foo
的隱式原型上,函式的隱式原型是誰給的呢? 上文講到,隱式原型是在例項化的時候,建構函式所賦予的,那麼誰會是Foo
的建構函式呢? - 看圖可知,它便是
Function
,例項化了Function
所得到的Foo
,所以Foo
的隱式原型是指向了Function
的顯式原型Function.prototype
的
〽站在高層的兩個建構函式
它便是 Function和Object建構函式,他們兩個有點特殊,所以單獨拿出來講講
- 大家看上面的圖,首先關注
Object
這個建構函式 Object
的顯式原型是指向了Object.prototype
,這一點大家肯定都沒有問題- 我們看,
Object.prototype
的隱式原型是指向了null
,這說明了一個什麼問題,隱式原型是在例項化的時候才會被賦予的,但是他是為空,所以我們可以得到一個結論,就是在JS中,頂層原型是Object.prototype
,??Object.prototype
是站在最頂峰的辣個男人 - 好,思緒收回來,我們看下
Object
的隱式原型是指向哪的,會發現它的隱式原型是指向函式的顯式原型的,說明Object
這個建構函式是通過Function
這個建構函式所例項化得到的 - 我們接著看下
Function
這個建構函式的顯式原型和隱式原型- 首先 它的顯式原型是
Function.prototype
,這點沒問題 - 然後 它的隱式原型也是
Function.prototype
嗯? 嗯? 好怪,不行,我再看一眼? - 然後你會發覺不對勁,很不對勁❗❗❗ 這不就是說明了
Function
是通過例項化自己得到的嗎? - 有點類似於 是先有雞還是先有蛋這一說法.不知道他們哪個是先出來的,應該是JS內部自己做了特殊處理,這一部分需要小夥伴記好了
- 首先 它的顯式原型是
- 然後我們在看下
Function.prototype
的隱式原型指向了Object.prototype
說明了函式的原型物件是通過例項化Object
所得到的 - 講到這 這副圖的內容我已經講的差不多了,最後我自己畫了一幅圖來幫助大家更好的理解
?習題練習
我出幾道練習題,大家自己練習一下,看看自己掌握的怎麼樣
/** 1.普通物件
* 2.建構函式
* 3.Function
* 4.Object
**/
function Foo(name) {
this.name = name;
}
const obj = {};
const obj2 = {
name: "給他一個新的原型",
}
const foo = new Foo("czx");
console.log(foo.__proto__ === Foo.prototype);
console.log(obj.__proto__ === Object.prototype)
console.log(Foo.prototype === Foo.__proto__);
console.log(Foo.__proto__ === Function.prototype);
console.log(Function.prototype === Function.__proto__);
console.log(Function.prototype.__proto__ === Object.prototype);
console.log(Object.__proto__ === Function.prototype);
console.log(Object.prototype.__proto__);
Foo.prototype = obj2;
console.log(Foo.prototype.__proto__ === Object.prototype)
console.log(foo.__proto__ === Foo.prototype);
答案
大家一定要自己寫完後再來看答案啊,這樣印象才深刻
console.log(foo.__proto__ === Foo.prototype);//true
console.log(obj.__proto__ === Object.prototype)//true
console.log(Foo.prototype === Foo.__proto__);//false
console.log(Foo.__proto__ === Function.prototype);//true
console.log(Function.prototype === Function.__proto__); //true
console.log(Function.prototype.__proto__ === Object.prototype);//true
console.log(Object.__proto__ === Function.prototype);//true;
console.log(Object.prototype.__proto__);//null
Foo.prototype = obj2;
console.log(Foo.prototype.__proto__ === Object.prototype)//true;
console.log(foo.__proto__ === Foo.prototype);//false
?原型鏈
只要理解了上面我講的原型,原型鏈自然而然就會了
- 大家可以將原型鏈理解為,如果我在這一層沒找到的東西,我可以去對應的上一層找,直到頂層為止,我來給大家出幾道題,就能很快知道了解了
題目一
function Foo(name, age) {
this.name = name;
this.age = age;
}
const bar = {
say: () => {
console.log("執行說話");
},
age2: 32,
}
Foo.prototype = bar;
const foo = new Foo("Spirit", 18)
foo.say();
console.log(foo.age2)
- 大家可以自己想下這段程式碼執行出來會是什麼結果
- 可以知道,我將Foo的顯式原型進行了替換,那麼我foo去呼叫原型物件上的方法是可以呼叫到的,而我建構函式Foo上是沒有這兩個屬性的,但因為他的顯式原型物件即bar,它上面是有的
- 所以foo是可以呼叫到的
題目二
var A = function () { };
A.prototype.n = 1;
var b = new A();
A.prototype = {
n: 2,
m: 3
}
var c = new A();
console.log(b.n);
console.log(b.m);
console.log(c.n);
console.log(c.m);
??️ 大家可以想下這道題的輸出結果是什麼,這道題也是很考察大家對原型鏈的理解的,接下來我就會開始講解了
- 首先
A
是一個建構函式,它有顯式原型和隱式原型 A
在顯式原型上新增了一個屬性n,其值為1- 通過例項化A得到了b,有隱式原型,這時候就考察大家對
new
操作熟不熟悉,這時候有b.__proto__===A.prototype
- 這時候A的顯式原型被整個替換了,所以之前的例項化的b就取不到這個更換原型之後的值了
- 例項化A得到了c,這時候有
c.__proto__===A.prototype
只不過這個顯式原型不再是之前的了 - 開始輸出
b.n
是輸出1的,因為是加在了最開始A的顯式原型上面,所以是能取到值的b.m
是取不到值的,因為這個m
是後面更換了A的顯式原型所加上的值,此時的b是取不到的c.n
輸出為2,因為更換後的顯式原型上面是有n 是2c.m
輸出為3,同理- 大家聽懂了嗎,這道題主要就是考你
new
操作做了什麼,讓你來判斷到底例項化之後是賦予了哪個顯式原型,下面的程式碼是帶註解版的
var A = function () { };
A.prototype.n = 1; //顯示原型上加了一個 n屬性 值為1
var b = new A(); //例項化了的b __proto__===A.prototype
A.prototype = {
n: 2,
m: 3
} //A的顯式原型被替換掉了
var c = new A(); //例項化的c 的__proto__===A更換後的顯式原型
console.log(b.n); //1
console.log(b.m); //undefined
console.log(c.n);//2
console.log(c.m);//3
題目三
var F = function () { };
Object.prototype.a = function () {
console.log('a');
};
Function.prototype.b = function () {
console.log('b');
}
var f = new F();
f.a();
f.b();
F.a();
F.b();
?這道題是我認為比較難的一道題,大家可以好好想下這道題的輸出到底是什麼,接下來我會開始講解這道題
F
是一個建構函式,它擁有顯式原型和隱式原型- 此時
F.prototype.__proto__===Object.prototype
F.__proto__===Function.prototype
- 這個時候,他們是有這種關係的
- 此時
- 在
Object.prototype
上加上了一個函式a
- 在
Function.prototype
加上了一個函式b
- 例項化F得到了
f
,這個時候他有隱式原型f.__proto__===F.prototype
- 接下來我們看輸出
f.a()
首先它的隱式原型是等於建構函式的顯式原型的,而F.prototype.__proto__
是等於Object.prototype
,也就是說f
是可以沿著這條原型鏈一路往上找到,最後是可以找到這個a
函式的,所以可以輸出a
f.b()
我們可以想下,沿著原型鏈我們可以找到這個函式b嘛,並不可以,對不對?,所以這裡會報錯,說不存在這麼一個函式F.a()
我們來看下這個建構函式是有顯式原型和隱式原型的,所以我們看回剛開始的那個解釋,它是能找到Object.prototype
和Function.prototype
的,所以能夠輸出aF.b()
也是同理,所以輸出b
- 這道題大家看懂了嘛,主要是考察了函式和普通物件的區別,也是很好的考察了原型鏈的關係的,下面是帶註解的程式碼
var F = function () { }; //建構函式
//他有隱式原型和顯式原型 此時他的隱式原型__proto__===Function.prototype.__proto__===Object.prototype
Object.prototype.a = function () { //Object.prototype是頂層原型
console.log('a');
};
Function.prototype.b = function () {
console.log('b');
}
var f = new F(); //例項化了的f 此時是一個物件 只有隱式原型 __proto__ 所以此時有 __proto__===F.prototype
//??️ F.prototype是一個物件,是Object所例項化得到的 所以F.prototype.__proto__===Object.prototype
f.a(); //a
f.b(); //報錯 沒有這個函式
F.a(); //a
F.b();//b