[JS基礎] 帶你深入瞭解JS原型

CodeSpirit發表於2022-03-22

?簡介

下面這張圖大家應該很很熟悉了,各位大佬講原型及原型鏈的時候是大部分都會用到下面這張圖片的
我想以自己的方式來講述一遍,一是幫助我自己更好的複習,二是希望能夠幫助到想要複習或者學習原型的同學

  • 在講解之前,我先講點基礎概念
  • JS物件中他有一個內建原型[[prototype]],這個原型就是隱式原型__proto__,而建構函式因為其本身就是 一個物件,所以它有一個隱式原型,但是建構函式又是一個函式,所以他有一個特殊的屬性顯式原型prototype,大家也可以看到constructor這個屬性,預設情況下,函式物件上都會有一個constructor屬性,裡面存放著是函式的原型物件
  • 好,接下來我會分步講解這張圖的內容,同時會輔以圖片來講解

?建構函式與例項化的關係

  • 我們先看這個地方function Foo() 他是一個建構函式
  • 那麼它的顯式原型一定是指向Foo的原型物件上
  • 同時我們看f1這個例項化Foo的物件,因為是物件,所以它只有一個__proto__隱式原型的
  • ❓我們可以看到圖片中,隱式原型是指向了Foo的原型物件上的,這是為什麼呢?
  • ✅其實這裡我要講下 new這個操作 到底幹了些什麼,這應該是一道經典的面試題吧
    • 1️⃣ 首先會建立一個空物件 {}
    • 2️⃣ 將this指向該空象,然後將建構函式的顯式原型賦予給這個物件的隱式原型上去 __proto__=prototype
    • 3️⃣ 開始執行建構函式內的程式碼,將建構函式內部屬性和方法賦值給該物件
  • ?所以大家看,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);

?‍?️ 大家可以想下這道題的輸出結果是什麼,這道題也是很考察大家對原型鏈的理解的,接下來我就會開始講解了

  1. 首先 A 是一個建構函式,它有顯式原型和隱式原型
  2. A在顯式原型上新增了一個屬性n,其值為1
  3. 通過例項化A得到了b,有隱式原型,這時候就考察大家對new操作熟不熟悉,這時候有b.__proto__===A.prototype
  4. 這時候A的顯式原型被整個替換了,所以之前的例項化的b就取不到這個更換原型之後的值了
  5. 例項化A得到了c,這時候有c.__proto__===A.prototype 只不過這個顯式原型不再是之前的了
  6. 開始輸出
  7. b.n是輸出1的,因為是加在了最開始A的顯式原型上面,所以是能取到值的
  8. b.m是取不到值的,因為這個m是後面更換了A的顯式原型所加上的值,此時的b是取不到的
  9. c.n輸出為2,因為更換後的顯式原型上面是有n 是2
  10. c.m輸出為3,同理
  11. 大家聽懂了嗎,這道題主要就是考你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();

?這道題是我認為比較難的一道題,大家可以好好想下這道題的輸出到底是什麼,接下來我會開始講解這道題

  1. F是一個建構函式,它擁有顯式原型和隱式原型
    1. 此時F.prototype.__proto__===Object.prototype
    2. F.__proto__===Function.prototype
    3. 這個時候,他們是有這種關係的
  2. Object.prototype上加上了一個函式a
  3. Function.prototype加上了一個函式b
  4. 例項化F得到了f,這個時候他有隱式原型f.__proto__===F.prototype
  5. 接下來我們看輸出
    1. f.a()首先它的隱式原型是等於建構函式的顯式原型的,而F.prototype.__proto__是等於Object.prototype,也就是說f是可以沿著這條原型鏈一路往上找到,最後是可以找到這個a函式的,所以可以輸出a
    2. f.b()我們可以想下,沿著原型鏈我們可以找到這個函式b嘛,並不可以,對不對?,所以這裡會報錯,說不存在這麼一個函式
    3. F.a()我們來看下這個建構函式是有顯式原型和隱式原型的,所以我們看回剛開始的那個解釋,它是能找到Object.prototypeFunction.prototype的,所以能夠輸出a
    4. F.b()也是同理,所以輸出b
  6. 這道題大家看懂了嘛,主要是考察了函式和普通物件的區別,也是很好的考察了原型鏈的關係的,下面是帶註解的程式碼
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

相關文章