【進階5-2期】圖解原型鏈及其繼承

木易楊說發表於2019-04-08

引言

上篇文章介紹了建構函式、原型和原型鏈的關係,並且說明了 prototype[[Prototype]]__proto__ 之間的區別,今天這篇文章用圖解的方式向大家介紹原型鏈及其繼承方案,在介紹原型鏈繼承的過程中講解原型鏈運作機制以及屬性遮蔽等知識。

建議閱讀上篇文章後再來閱讀本文,連結:【進階5-1期】重新認識建構函式、原型和原型鏈

有什麼想法或者意見都可以在評論區留言。下圖是本文的思維導圖,高清思維導圖和更多文章請看我的 Github

5-2

原型鏈

48185513-25833c00-e370-11e8-9939-678da278704d

上篇文章中我們介紹了原型鏈的概念,即每個物件擁有一個原型物件,通過 __proto__ 指標指向上一個原型 ,並從中繼承方法和屬性,同時原型物件也可能擁有原型,這樣一層一層,最終指向 null,這種關係被稱為原型鏈(prototype chain)。

根據規範不建議直接使用 __proto__,推薦使用 Object.getPrototypeOf(),不過為了行文方便邏輯清晰,下面都以 __proto__ 代替。

注意上面的說法,原型上的方法和屬性被 繼承 到新物件中,並不是被複制到新物件,我們看下面這個例子。

// 木易楊
function Foo(name) {
	this.name = name;
}
Foo.prototype.getName = function() {
  	return this.name;
}
Foo.prototype.length = 3;
let foo = new Foo('muyiy'); // 相當於 foo.__proto__ = Foo.prototype
console.dir(foo);
複製程式碼

image-20190406105351100

原型上的屬性和方法定義在 prototype 物件上,而非物件例項本身。當訪問一個物件的屬性 / 方法時,它不僅僅在該物件上查詢,還會查詢該物件的原型,以及該物件的原型的原型,一層一層向上查詢,直到找到一個名字匹配的屬性 / 方法或到達原型鏈的末尾(null)。

比如呼叫 foo.valueOf() 會發生什麼?

  • 首先檢查 foo 物件是否具有可用的 valueOf() 方法。
  • 如果沒有,則檢查 foo 物件的原型物件(即 Foo.prototype)是否具有可用的 valueof() 方法。
  • 如果沒有,則檢查 Foo.prototype 所指向的物件的原型物件(即 Object.prototype)是否具有可用的 valueOf() 方法。這裡有這個方法,於是該方法被呼叫。

image-20190407165429484

prototype__proto__

上篇文章介紹了 prototype__proto__ 的區別,其中原型物件 prototype 是建構函式的屬性,__proto__ 是每個例項上都有的屬性,這兩個並不一樣,但 foo.__proto__Foo.prototype 指向同一個物件。

這次我們再深入一點,原型鏈的構建是依賴於 prototype 還是 __proto__ 呢?

【進階5-2期】圖解原型鏈及其繼承

kenneth-kin-lum.blogspot.com/2012/10/jav…

Foo.prototype 中的 prototype 並沒有構建成一條原型鏈,其只是指向原型鏈中的某一處。原型鏈的構建依賴於 __proto__,如上圖通過 foo.__proto__ 指向 Foo.prototypefoo.__proto__.__proto__ 指向 Bichon.prototype,如此一層一層最終連結到 null

可以這麼理解 Foo,我是一個 constructor,我也是一個 function,我身上有著 prototype 的 reference,只要隨時呼叫 foo = new Foo(),我就會將 foo.__proto__ 指向到我的 prototype 物件。

不要使用 Bar.prototype = Foo,因為這不會執行 Foo 的原型,而是指向函式 Foo。 因此原型鏈將會回溯到 Function.prototype 而不是 Foo.prototype,因此 method 方法將不會在 Bar 的原型鏈上。

// 木易楊
function Foo() {
  	return 'foo';
}
Foo.prototype.method = function() {
  	return 'method';
}
function Bar() {
  	return 'bar';
}
Bar.prototype = Foo; // Bar.prototype 指向到函式
let bar = new Bar();
console.dir(bar);

bar.method(); // Uncaught TypeError: bar.method is not a function
複製程式碼

image-20190404190228096

instanceof 原理及實現

instanceof 運算子用來檢測 constructor.prototype 是否存在於引數 object 的原型鏈上。

// 木易楊
function C(){} 
function D(){} 

var o = new C();

o instanceof C; // true,因為 Object.getPrototypeOf(o) === C.prototype
o instanceof D; // false,因為 D.prototype 不在 o 的原型鏈上
複製程式碼

instanceof 原理就是一層一層查詢 __proto__,如果和 constructor.prototype 相等則返回 true,如果一直沒有查詢成功則返回 false。

instance.[__proto__...] === instance.constructor.prototype
複製程式碼

知道了原理後我們來實現 instanceof,程式碼如下。

// 木易楊
function instance_of(L, R) {//L 表示左表示式,R 表示右表示式
   var O = R.prototype;// 取 R 的顯示原型
   L = L.__proto__;// 取 L 的隱式原型
   while (true) { 
       // Object.prototype.__proto__ === null
       if (L === null) 
         return false; 
       if (O === L)// 這裡重點:當 O 嚴格等於 L 時,返回 true 
         return true; 
       L = L.__proto__; 
   } 
}

// 測試
function C(){} 
function D(){} 

var o = new C();

instance_of(o, C); // true
instance_of(o, D); // false
複製程式碼

原型鏈繼承

原型鏈繼承的本質是重寫原型物件,代之以一個新型別的例項。如下程式碼,新原型 Cat 不僅有 new Animal() 例項上的全部屬性和方法,並且由於指向了 Animal 原型,所以還繼承了Animal 原型上的屬性和方法。

// 木易楊
function Animal() {
    this.value = 'animal';
}

Animal.prototype.run = function() {
    return this.value + ' is runing';
}

function Cat() {}

// 這裡是關鍵,建立 Animal 的例項,並將該例項賦值給 Cat.prototype
// 相當於 Cat.prototype.__proto__ = Animal.prototype
Cat.prototype = new Animal(); 

var instance = new Cat();
instance.value = 'cat'; // 建立 instance 的自身屬性 value
console.log(instance.run()); // cat is runing
複製程式碼

原型鏈繼承方案有以下缺點:

  • 1、多個例項對引用型別的操作會被篡改
  • 2、子型別的原型上的 constructor 屬性被重寫了
  • 3、給子型別原型新增屬性和方法必須在替換原型之後
  • 4、建立子型別例項時無法向父型別的建構函式傳參

問題 1

原型鏈繼承方案中,原型實際上會變成另一個型別的例項,如下程式碼,Cat.prototype 變成了 Animal 的一個例項,所以 Animal 的例項屬性 names 就變成了 Cat.prototype 的屬性。

而原型屬性上的引用型別值會被所有例項共享,所以多個例項對引用型別的操作會被篡改。如下程式碼,改變了 instance1.names 後影響了 instance2

// 木易楊
function Animal(){
  this.names = ["cat", "dog"];
}
function Cat(){}

Cat.prototype = new Animal();

var instance1 = new Cat();
instance1.names.push("tiger");
console.log(instance1.names); // ["cat", "dog", "tiger"]

var instance2 = new Cat(); 
console.log(instance2.names); // ["cat", "dog", "tiger"]
複製程式碼

問題 2

子型別原型上的 constructor 屬性被重寫了,執行 Cat.prototype = new Animal() 後原型被覆蓋,Cat.prototype 上丟失了 constructor 屬性, Cat.prototype 指向了 Animal.prototype,而 Animal.prototype.constructor 指向了 Animal,所以 Cat.prototype.constructor 指向了 Animal

Cat.prototype = new Animal(); 
Cat.prototype.constructor === Animal
// true
複製程式碼

image-20190407153437908

解決辦法就是重寫 Cat.prototype.constructor 屬性,指向自己的建構函式 Cat

// 木易楊
function Animal() {
    this.value = 'animal';
}

Animal.prototype.run = function() {
    return this.value + ' is runing';
}

function Cat() {}
Cat.prototype = new Animal(); 

// 新增,重寫 Cat.prototype 的 constructor 屬性,指向自己的建構函式 Cat
Cat.prototype.constructor = Cat; 
複製程式碼

image-20190407164839128

問題 3

給子型別原型新增屬性和方法必須在替換原型之後,原因在第二點已經解釋過了,因為子型別的原型會被覆蓋。

// 木易楊
function Animal() {
    this.value = 'animal';
}

Animal.prototype.run = function() {
    return this.value + ' is runing';
}

function Cat() {}
Cat.prototype = new Animal(); 
Cat.prototype.constructor = Cat; 

// 新增
Cat.prototype.getValue = function() {
  return this.value;
}

var instance = new Cat();
instance.value = 'cat'; 
console.log(instance.getValue()); // cat
複製程式碼

屬性遮蔽

改造上面的程式碼,在 Cat.prototype 上新增 run 方法,但是 Animal.prototype 上也有一個 run 方法,不過它不會被訪問到,這種情況稱為屬性遮蔽 (property shadowing)。

// 木易楊
function Animal() {
    this.value = 'animal';
}

Animal.prototype.run = function() {
    return this.value + ' is runing';
}

function Cat() {}
Cat.prototype = new Animal(); 
Cat.prototype.constructor = Cat; 

// 新增
Cat.prototype.run = function() {
  return 'cat cat cat';
}

var instance = new Cat();
instance.value = 'cat'; 
console.log(instance.run()); // cat cat cat
複製程式碼

那如何訪問被遮蔽的屬性呢?通過 __proto__ 呼叫原型鏈上的屬性即可。

// 接上
console.log(instance.__proto__.__proto__.run()); // undefined is runing
複製程式碼

image-20190407162620611

其他繼承方案

原型鏈繼承方案有很多問題,實踐中很少會單獨使用,日常工作中使用 ES6 Class extends(模擬原型)繼承方案即可,更多更詳細的繼承方案可以閱讀我之前寫的一篇文章,歡迎拍磚。

點選閱讀:JavaScript 常用八種繼承方案

擴充套件題

有以下 3 個判斷陣列的方法,請分別介紹它們之間的區別和優劣

Object.prototype.toString.call() 、 instanceof 以及 Array.isArray()

參考答案:點選檢視

小結

  • 每個物件擁有一個原型物件,通過 __proto__ 指標指向上一個原型 ,並從中繼承方法和屬性,同時原型物件也可能擁有原型,這樣一層一層,最終指向 null,這種關係被稱為原型鏈
  • 當訪問一個物件的屬性 / 方法時,它不僅僅在該物件上查詢,還會查詢該物件的原型,以及該物件的原型的原型,一層一層向上查詢,直到找到一個名字匹配的屬性 / 方法或到達原型鏈的末尾(null)。
  • 原型鏈的構建依賴於 __proto__,一層一層最終連結到 null
  • instanceof 原理就是一層一層查詢 __proto__,如果和 constructor.prototype 相等則返回 true,如果一直沒有查詢成功則返回 false。
  • 原型鏈繼承的本質是重寫原型物件,代之以一個新型別的例項

參考

進階系列目錄

  • 【進階1期】 呼叫堆疊
  • 【進階2期】 作用域閉包
  • 【進階3期】 this全面解析
  • 【進階4期】 深淺拷貝原理
  • 【進階5期】 原型Prototype
  • 【進階6期】 高階函式
  • 【進階7期】 事件機制
  • 【進階8期】 Event Loop原理
  • 【進階9期】 Promise原理
  • 【進階10期】Async/Await原理
  • 【進階11期】防抖/節流原理
  • 【進階12期】模組化詳解
  • 【進階13期】ES6重難點
  • 【進階14期】計算機網路概述
  • 【進階15期】瀏覽器渲染原理
  • 【進階16期】webpack配置
  • 【進階17期】webpack原理
  • 【進階18期】前端監控
  • 【進階19期】跨域和安全
  • 【進階20期】效能優化
  • 【進階21期】VirtualDom原理
  • 【進階22期】Diff演算法
  • 【進階23期】MVVM雙向繫結
  • 【進階24期】Vuex原理
  • 【進階25期】Redux原理
  • 【進階26期】路由原理
  • 【進階27期】VueRouter原始碼解析
  • 【進階28期】ReactRouter原始碼解析

交流

進階系列文章彙總如下,內有優質前端資料,覺得不錯點個star。

github.com/yygmind/blo…

我是木易楊,公眾號「高階前端進階」作者,跟著我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高階前端的世界,在進階的路上,共勉!

【進階5-2期】圖解原型鏈及其繼承

相關文章