快速讀懂 JS 原型鏈

最近參加了公司內部技術分享,分享同窗提到了 Js 原型鏈的問題,並從 V8 的視角展開發散,刷新了我以前對原型鏈的認識,聽完後決定重學一下原型鏈,鞏固一下基礎。前端

  • 理解原型鏈
  • 深刻原型鏈
  • 總結與思考

理解原型鏈

Js 中的原型鏈是一個比較有意思的話題,它採用了一套巧妙的方法,解決了 Js 中的繼承問題。web

按個人理解,原型鏈能夠拆分紅:微信

  • 原型(prototype)
  • 鏈( __proto__

原型(prototype)

原型(prototype)是一個普通的對象,它爲構造函數的實例共享了屬性和方法。在全部的實例中,引用到的原型都是同一個對象。編輯器

例如:函數

function Student(name{
  this.name = name;
  this.study = function ({
    console.log("study js");
  };
}
// 建立 2 個實例
const student1 = new Student("xiaoming");
const student2 = new Student("xiaohong");
student1.study();
student2.study();

上面的代碼中,咱們建立了 2 個 Student 實例,每一個實例都有一個 study 方法,用來打印 "study js"。flex

這樣寫會有個問題:2 個實例中的 study 方法都是獨立的,雖然功能相同,但在系統中佔用的是 2 分內存,若是我建立 100 個 Student 實例,就得佔用 100 分內存,這樣算下去,將會形成大量的內存浪費。this

因此 Js 創造了 prototype。spa

function Student(name{
  this.name = name;
}
Student.prototype.study = function ({
  console.log("study js");
};
// 建立 2 個實例
const student1 = new Student("xiaoming");
const student2 = new Student("xiaohong");
student1.study();
student2.study();

使用 prototype 以後, study 方法存放在 Student 的原型中,內存中只會存放一份,全部 Student 實例都會共享它,內存問題就迎刃而解了。.net

但這裏還存在一個問題。prototype

爲何 student1 可以訪問到 Student 原型上的屬性和方法?

答案在 __proto__ 中,咱們接着往下看。

鏈(__proto__

鏈(__proto__)能夠理解爲一個指針,它是實例對象中的一個屬性,指向了構造函數的原型(prototype)。

咱們來看一個案例:

function Student(name{
  this.name = name;
}
Student.prototype.study = function ({
  console.log("study js");
};

const student = new Student("xiaoming");
student.study(); // study js
console.log(student.__proto__ === Student.prototype); // true

從打印結果能夠得出:函數實例的 __proto__ 指向了構造函數的 prototype,上文中遺留的問題也就解決了。

但不少同窗可能有這個疑問。

爲何調用 student.study 時,訪問到的倒是 Student.prototype.study 呢?

答案在原型鏈中,咱們接着往下看。

原型鏈

原型鏈指的是:一個實例對象,在調用屬性或方法時,會依次從實例自己、構造函數原型、構造函數原型的原型... 上去尋找,查看是否有對應的屬性或方法。這樣的尋找方式就好像一個鏈條同樣,從實例對象,一直找到 Object.prototype ,專業上稱之爲原型鏈。

仍是來看一個案例:

function Student(name{
  this.name = name;
}
Student.prototype.study = function ({
  console.log("study js");
};

const student = new Student("xiaoming");
student.study(); // study js。
// 在實例中沒找到,在構造函數的原型上找到了。
// 實際調用的是:student.__proto__.say 也就是 Student.prototype.say。

student.toString(); // "[object Object]"
// 在實例中沒找到。
// 在構造函數的原型上也沒找到。
// 在構造函數的原型的原型上找到了。
// 實際調用的是 student.__proto__.__proto__.toString 也就是 Object.prototype.toString。

能夠看到, __proto__ 就像一個鏈同樣,串聯起了實例對象和原型。

一樣,上面代碼中還會存在如下疑問。

爲何 Student.prototype.__proto__ 是 Object.prototype?

這裏提供一個推導步驟:

  1. 先找 __proto__ 前面的對象,也就是 Student.prototype 的構造函數。

    1. 判斷 Student.prototype 類型, typeof Student.prototypeobject
    2. object 的構造函數是 Object。
    3. 得出 Student.prototype 的構造函數是 Object。
  2. 因此 Student.prototype.__proto__ 是 Object.prototype。

這個推導方法很實用,除了自定義構造函數對象以外,其餘對象均可以推導出正確答案。

原型鏈常見問題

原型鏈中的問題不少,這裏再列舉幾個常見的問題。

Function.__proto__ 是什麼?

  1. 找 Function 的構造函數。

    1. 判斷 Function 類型, typeof Functionfunction
    2. 函數類型的構造函數就是 Function。
    3. 得出 Function 的構造函數是 Function。
  2. 因此 Function.__proto__ = Function.prototype。


Number.__proto__ 是什麼?

這裏只是稍微變了一下,不少同窗就不知道了,其實和上面的問題是同樣的。

  1. 找 Number 的構造函數。

    1. 判斷 Number 類型, typeof Numberfunction
    2. 函數類型的構造函數就是 Function。
    3. 得出 Number 的構造函數是 Function。
  2. 因此 Number.__proto__ = Function.prototype。


Object.prototype.__proto__ 是什麼?

這是個特例,若是按照常理去推導,Object.prototype.__proto__ 是 Object.prototype,但這是不對的,這樣下去原型鏈就在 Object 處無限循環了。

爲了解決這個問題,Js 的造物主就直接在規定了 Object.prototype.__proto__ 爲 null,打破了原型鏈的無線循環。

明白了這些問題以後,看一下這張經典的圖,咱們應該都能理解了。

深刻原型鏈

介紹完傳統的原型鏈判斷,咱們再從 V8 的層面理解一下。

V8 是怎麼建立對象的

Js 代碼在執行時,會被 V8 引擎解析,這時 V8 會用不一樣的模板來處理 Js 中的對象和函數。

例如:

  • ObjectTemplate 用來建立對象
  • FunctionTemplate 用來建立函數
  • PrototypeTemplate 用來建立函數原型

細品一下 V8 中的定義,咱們能夠獲得如下結論。

  • Js 中的函數都是 FunctionTemplate 建立出來的,返回值的是 FunctionTemplate 實例
  • Js 中的對象都是 ObjectTemplate 建立出來的,返回值的是 ObjectTemplate 實例
  • Js 中函數的原型(prototype)都是經過 PrototypeTemplate 建立出來的,返回值是 ObjectTemplate 實例

因此 Js 中的對象的原型能夠這樣判斷:

  • 全部的對象的原型都是 Object.prototype,自定義構造函數的實例除外。
  • 自定義構造函數的實例,它的原型是對應的構造函數原型。

在 Js 中的函數原型判斷就更加簡單了。

  • 全部的函數原型,都是 Function.prototype。

下圖展現了全部的內置構造函數,他們的原型都是 Function.prototype。

看到這裏,你是否也能夠一看就看出任何對象的原型呢?

附:V8 中的函數解析案例

瞭解完原型鏈以後,咱們看一下 V8 中的函數解析。

function Student(name{
  this.name = name;
}
Student.prototype.study = function ({
  console.log("study js");
};
const student = new Student('xiaoming')

這段代碼在 V8 中會這樣執行:

// 建立一個函數
v8::Local<v8::FunctionTemplate> Student = v8::FunctionTemplate::New();
// 獲取函數原型
v8::Local<v8::Template> proto_Student = Student->PrototypeTemplate();
// 設置原型上的方法
proto_Student->Set("study", v8::FunctionTemplate::New(InvokeCallback));
// 獲取函數實例
v8::Local<v8::ObjectTemplate> instance_Student = Student->InstanceTemplate();
// 設置實例的屬性
instance_Student->Set("name", String::New('xiaoming'));
// 返回構造函數
v8::Local<v8::Function> function = Student->GetFunction();
// 返回構造函數實例
v8::Local<v8::Object> instance = function->NewInstance();

以上代碼能夠分爲 4 個步驟:

  • 建立函數模板。
  • 在函數模板中,拿到函數原型,並賦值。
  • 在函數模板中,拿到函數實例,並賦值。
  • 返回構造函數。
  • 返回構造函數實例。

V8 中的總體執行流程是符合正常預期的,這裏瞭解一下便可。

總結與思考

本文分別從傳統 Js 方面、V8 層面組件剖析了原型鏈的本質,但願你們都能有所收穫。

最後,若是你對此有任何想法,歡迎留言評論!


本文分享自微信公衆號 - 前端日誌(gh_12dcc43e6039)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索