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

更新:謝謝你們的支持,最近折騰了一個博客官網出來,方便你們系統閱讀,後續會有更多內容和更多優化,猛戳這裏查看javascript

------ 如下是正文 ------html

引言

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

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

有什麼想法或者意見均可以在評論區留言。下圖是本文的思惟導圖,高清思惟導圖和更多文章請看個人 Githubwebpack

5-2

原型鏈

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

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

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

注意上面的說法,原型上的方法和屬性被 繼承 到新對象中,並非被複制到新對象,咱們看下面這個例子。web

// 木易楊
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__ 呢?

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
複製代碼

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

  • 一、多個實例對引用類型的操做會被篡改
  • 二、子類型的原型上的 constructor 屬性被重寫了
  • 三、給子類型原型添加屬性和方法必須在替換原型以後
  • 四、建立子類型實例時沒法向父類型的構造函數傳參

問題 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…

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

相關文章
相關標籤/搜索