JS 原型鏈

2022-01-11 20:00:16

JS 原型鏈

1. 原型和原型鏈的基礎結論

1.1 函數與物件的關係

  • 函數是物件,物件都是通過函數建立的。
  • 函數與物件並不是簡單的包含與被包含的關係。

1.2 原型的類別

  • 顯示原型:prototype,是每個函數function獨有的屬性。
  • 隱式原型: __proto__,是每個物件都具有的屬性。

1.3 原型和原型鏈

  • 原型:一個函數可以看成一個類,原型是所有類都有的一個屬性,原型的作用就是給這個類的一個物件都新增一個統一的方法。
  • 原型鏈:每個物件都有一個__proto__,它指向它的prototype原型物件;它的prototype原型物件又有一個__proto__指向它的prototype原型物件,就這樣層層向上直到最終找到頂級物件Objectprototype,這個查詢路徑就是原型鏈。

1.4 JavaScript 裡最頂層的兩個概念

  • Function 是最頂層的構造器

FunctionJavaScript 裡最頂層的構造器,它構造了系統中的所有物件,包括定義物件、系統內建物件、甚至包括它自己。

  • Object 是最頂層的物件
    • 所有物件都是繼承 Object 的原型
    • Object 也是被 Function 構造出來的

1.5 instanceof

obj instanceof F

  • **常見的不夠正確描述:**用來判斷一個物件是否是某個建構函式的範例,比如我們建立一個函數,並且將它範例化
    image-20220110143653657

  • 正確的描述:obj.__proto__.__proto__... => F.prototype。沿著物件obj的原型鏈查詢是否存在物件F.prototype,若存在則返回true,若查詢到原型鏈的終點Object.prototype仍未找到,則返回false

2. 經典的原型和原型鏈的分析

接下來我們將主要講解以下類別:

image-20220110144233519

2.1 函數.prototype

  • **前提結論:**函數都是物件,每個函數都自帶一個屬性叫做 prototype
  • 栗子:

image-20220110144624578

  • 詳細結構圖:

image-20220110144700156

  • **最終結論:**每個函數下其實有個prototype屬性,prototype的屬性值是一個物件,這個物件預設的只有一個叫做constructor的屬性,指向這個函數本身。

2.2 物件.__proto__

  • **前提結論:**每個物件都有一個隱藏的屬性叫做__proto__
  • 例子:

image-20220110145132234

  • 解釋:
    • [[Prototype]]:是物件的一個內部屬性, chrome的引擎通過__proto__向外暴露了這個屬性。實際上它可以看作就是物件的__proto__屬性
    • __proto__的值:等於構造該物件的函數的prototype屬性。testObj.__proto__ === testFn.prototype
  • **結論:**每個物件都有一個__proto__屬性,指向建立該物件的函數的prototype

image-20220110145631512

2.3 函數.__proto__

  • **前提結論:**在JavaScript中,函數都是物件,是物件就有隱藏的__proto__屬性
  • 解釋: Function是最頂級的構造器,函數物件都是通過它構造的
  • 結論:函數.__proto__ === Function.prototype

image-20220110150007141

2.4 函數.prototype.__proto__

  • 解釋:函數 .prototype,它本質上是和var obj = {}是一樣的,由new Object建立的
  • 結論:函數.protype.__proto__ === Object.prototype

image-20220110150541292

2.5 Object.__proto__

  • 解釋:Object是由頂層建構函式Function構造的
  • 結論:Object.__proto__ === Function.prototype

image-20220110150954478

2.6 Object.prototype.__proto__

  • 結論:Object.prototype較為特殊,它是頂級物件的原型,所以它的__proto__指向是null

image-20220110151326286

2.7 Function.__proto__

  • **解釋:**函數物件都是由被頂級建構函式Function建立。所以Function是被自身建立的
  • 結論:Function.__proto__ === Function.prototype

image-20220110151630972

2.8 Function.prototype.__proto__

  • **解釋:**函數 prototype,它本質上是和var obj = {}是一樣的,由new Object建立的
  • 結論:Function.prototype.__proto__ === Object.prototype

image-20220110151917982

最終我們將形成一個經典的原型圖:

image-20220110152037301

3. 基於原型鏈的繼承

JavaScript 物件有一個指向一個原型物件的鏈。當試圖存取一個物件的屬性時,它不僅僅在該物件上搜尋,還會搜尋該物件的原型,以及該物件的原型的原型,依次層層向上搜尋,直到找到一個名字匹配的屬性或到達原型鏈的末尾。

遵循ECMAScript標準,someObject.[[Prototype]] 符號是用於指向 someObject 的原型。從 ECMAScript 6 開始,[[Prototype]] 可以通過 Object.getPrototypeOf()Object.setPrototypeOf() 存取器來存取。這個等同於 JavaScript 的非標準但許多瀏覽器實現的屬性 __proto__

但它不應該與建構函式 funcprototype 屬性相混淆。被建構函式建立的範例物件的 [[Prototype]] 指向 funcprototype 屬性。Object.prototype 屬性表示 Object 的原型物件。

這裡演示當嘗試存取屬性時會發生什麼:

// 讓我們從一個函數裡建立一個物件o,它自身擁有屬性a和b的:
let f = function () {
   this.a = 1;
   this.b = 2;
}
/* 這麼寫也一樣
function f() {
  this.a = 1;
  this.b = 2;
}
*/
let o = new f(); // {a: 1, b: 2}

// 在f函數的原型上定義屬性
f.prototype.b = 3;
f.prototype.c = 4;

// 不要在 f 函數的原型上直接定義 f.prototype = {b:3,c:4};這樣會直接打破原型鏈
// o.[[Prototype]] 有屬性 b 和 c
//  (其實就是 o.__proto__ 或者 o.constructor.prototype)
// o.[[Prototype]].[[Prototype]] 是 Object.prototype.
// 最後o.[[Prototype]].[[Prototype]].[[Prototype]]是null
// 這就是原型鏈的末尾,即 null,
// 根據定義,null 就是沒有 [[Prototype]]。

// 綜上,整個原型鏈如下:

// {a:1, b:2} ---> {b:3, c:4} ---> Object.prototype---> null

console.log(o.a); // 1
// a是o的自身屬性嗎?是的,該屬性的值為 1

console.log(o.b); // 2
// b是o的自身屬性嗎?是的,該屬性的值為 2
// 原型上也有一個'b'屬性,但是它不會被存取到。
// 這種情況被稱為"屬性遮蔽 (property shadowing)"

console.log(o.c); // 4
// c是o的自身屬性嗎?不是,那看看它的原型上有沒有
// c是o.[[Prototype]]的屬性嗎?是的,該屬性的值為 4

console.log(o.d); // undefined
// d 是 o 的自身屬性嗎?不是,那看看它的原型上有沒有
// d 是 o.[[Prototype]] 的屬性嗎?不是,那看看它的原型上有沒有
// o.[[Prototype]].[[Prototype]] 為 null,停止搜尋
// 找不到 d 屬性,返回 undefined

4. 檢驗

最後,我們通過四個小問題來檢驗一下是否真的掌握了它:

  • 題目 1
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);

請寫出上面程式設計的輸出結果是什麼?

  • 題目 2
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();

請寫出上面程式設計的輸出結果是什麼?

  • 題目 3
function Person(name) {
    this.name = name
}
let p = new Person('Tom');

問題1: p.__proto__等於什麼?

問題2:Person.__proto__等於什麼?

  • 題目 4
var foo = {},
    F = function(){};
Object.prototype.a = 'value a';
Function.prototype.b = 'value b';

console.log(foo.a);
console.log(foo.b);

console.log(F.a);
console.log(F.b);

請寫出上面程式設計的輸出結果是什麼?

  • 題目 1 答案:
b.n -> 1
b.m -> undefined;

c.n -> 2;
c.m -> 3;
  • 題目 2 答案:
f.a() -> a
f.b() -> f.b is not a function

F.a() -> a
F.b() -> b
  • 題目 3 答案

答案1:Person.prototype

答案2:Function.prototype

  • 題目 4 答案
foo.a => value a
foo.b => undefined
F.a => value a
F.b => value b

大家都掌握了嗎?