最近在總體地複習一遍現代前端必備的核心知識點,將會整理成一個前端分析總結文章系列。這篇是其中的第三篇,主要是總結下JS中原型與繼承等核心知識點。(另外,此係列文章也能夠在語雀專欄——硬核前端系列查看)。javascript
本文首發自 迪諾筆記,轉載請註明出處😁
「每一個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,實例都包含一個指向原型對象的內部指針。」
——《JavaScript高級程序設計》
實例對象是經過 new
操做符來操做構造函數 constructor
生成的。實例對象具備 __proto__
屬性,構造函數具備 prototype
屬性。html
原型(prototype
)自己也是一個對象,稱爲原型對象。構造函數上的屬性 prototype
指向原型對象,實例上的屬性 __proto__
指向原型對象。前端
prototype
與 ___proto___
的指向關係圖java
藉助原型實現繼承的核心思想是:對象在查找屬性和方法時首先看對象自身是否存在,不存在則去原型對象上查找,若未找到則繼續到原型對象的原型對象上查找,依次進行下去直到查找到內置對象Object
。即,實例屬性訪問是沿着原型鏈向上遞歸查找。git
實例屬性訪問時沿原型鏈遞歸查找es6
原型對象也是對象也存在本身的原型對象,這裏的原型對象造成的鏈條就是原型鏈。內置對象 Object
的原型對象是 null
,Object
對象是全部對象最頂層的原型對象。github
完整的構造函數、原型、原型鏈組成及之間的關係以下圖所示:typescript
如下面這個基礎的繼承實現爲例:數組
function constructorFn (state, data) { this.state = state; this.data = data; this.isPlay = function () { return this.state + ' is ' + this.data; } } var instance1 = new constructorFn ('1', 'doing'); var instance2 = new constructorFn ('2', 'done'); console.log(instance1.isPlay()); // 1 is doing console.log(instance2.isPlay()); // 2 is done
這裏分別生成了兩個實例:instance1
和 instance2
,其構造函數經過對 this
進行賦值使得各自實例有各自的獨立屬性和方法。app
這種狀況下的實例、原型對象、構造函數之間的關係圖以下:
實例、原型對象、構造函數之間的關係圖
上面例子中的 isPlay
方法在各自的實例對象上進行了重複定義,方法的邏輯是同樣的能夠進行復用,下面採用 prototype
來優化。
function constructorFn (state, data) { this.state = state; this.data = data; } constructorFn.prototype.isPlay = function () { return this.state + ' is ' + this.data; } var instance1 = new constructorFn ('1', 'doing'); var instance2 = new constructorFn ('2', 'done'); instance1.isDoing = 'nonono!'; instance2.isDoing = 'nonono!'; console.log(instance1.isPlay()); // 1 is doing console.log(instance2.isPlay()); // 2 is done console.log(instance1.isDoing); // nonono! console.log(instance2.isDoing); // nonono!
這裏將 isPlay
方法存到構造函數的原型對象上面,而後分別給兩個實例對象添加 isDoing
屬性。此時實例對象的 isPlay
方法是經過其 __proto__
指向的原型對象而訪問到構造函數原型 prototype
上的 isPlay
方法,而實例對象的 isDoing
屬性是其實例對象自己的屬性。
這種狀況下的實例、原型對象、構造函數之間的關係圖以下:
實例、原型對象、構造函數之間的關係圖
這時候經過構造函數的 prototype
修改原型對象屬性,全部繼承自原型的屬性都被修改,而實例對象自身的屬性不會改變。
constructorFn.prototype.isDoing = 'yesyesyes!'; console.log(instance1.isDoing); // yesyesyes! console.log(instance2.isDoing); // yesyesyes!
一樣,經過實例的 __proto__
修改原型對象屬性,全部繼承自原型的屬性都被修改,而實例對象自身的屬性不會改變。
- constructorFn.prototype.isDoing = 'yesyesyes!'; + instance1.__proto__.isDoing = 'yesyesyes!'; console.log(instance1.isDoing); // yesyesyes! console.log(instance2.isDoing); // yesyesyes!
上述狀況下的內存模型以下:
上述實例部份內存模型
多個實例對象的 ___proto___
屬性經過指針指向同一個原型對象——構造函數的原型對象;而實例自己的屬性則是指向存儲在對象自己。
若是直接修改實例自身的屬性 isDoing
,則另外一個實例的屬性不會跟着修改。
- constructorFn.prototype.isDoing = 'yesyesyes!'; + instance1.isDoing = 'yesyesyes!'; console.log(instance1.isDoing); // yesyesyes! console.log(instance2.isDoing); // nonono!
從對象自己開始,沿着原型組成的原型鏈逐級往上查找所訪問的屬性,找到相應的屬性就返回,若直到 Object.prototype
還未找到則返回 undefined。這裏屬性方法沿原型鏈的查找過程就是所謂的屬性繼承、方法重寫以及繼承方案的本質。
屬性沿着原型鏈查找示意圖
var obj = {}; obj.__proto__ = constructorFn.prototype; constructorFn.call(obj);
new
主要作了如下四件事情
__proto__
指向構造函數的原型 prototype
this
指向建立的對象而後執行仍是使用原型機制中用過的例子進行分析
function constructorFn (state, data) { this.state = state; this.data = data; } constructorFn.prototype.isPlay = function () { return this.state + ' is ' + this.data; } constructorFn.prototype.isDoing = 'nonono!'; var instance1 = new constructorFn ('1', 'doing'); var instance2 = new constructorFn ('2', 'done'); console.log(instance1.isPlay()); // 1 is doing console.log(instance2.isPlay()); // 2 is done console.log(instance1.isDoing); // nonono! console.log(instance2.isDoing); // nonono!
對於 var instance1 = new constructorFn ('1', 'doing');
執行過程以下圖所示。
new 的執行過程圖示
function constructorFn (state, data) { this.data = data; this.state = state; console.log(this); } var obj = { constructorFn }; constructorFn('a', 'b'); // Window obj.constructorFn('a', 'b'); // obj var instance1 = new constructorFn('a', 'b'); // instance1
對於 constructorFn('a', 'b');
函數體內部 this 指向 Window 對象;對於 obj.constructorFn('a', 'b');
函數體內部 this 綁定到了 obj;對於 var instance1 = new constructorFn('a', 'b');
函數體內部 this 綁定到了 new 出來的那個對象 instance1。
function a() { return () => { return () => { console.log(this) } } } console.log(a()()())
首先箭頭函數實際上是沒有 this 的,箭頭函數中的 this 只取決包裹箭頭函數的第一個普通函數的 this。在這個例子中,由於包裹箭頭函數的第一個普通函數是 a,因此此時的 this 是 window。另外對箭頭函數使用 bind這類函數是無效的。
最後種狀況也就是 bind 這些改變上下文的 API 了,對於這些函數來講,this 取決於第一個參數,若是第一個參數爲空,那麼就是 window。
三者都是將第一個參數做爲上下文綁定到其調用者的上下文 this 上,若是第一個參數不存在則默認綁定 Window 到調用者的上下文 this。
call 以散列值的形式將函數參數傳入前面調用函數並執行;apply 以數組的形式將函數參數傳入前面調用函數並執行;bind 僅綁定前面函數的上下分 this 不執行前面函數。
let a = {} let fn = function () { console.log(this) } fn.bind().bind(a)() // Window
上述代碼等價於:
let fn2 = function fn1() { return function() { return fn.apply() }.apply(a) } fn2()
就是說:屢次使用 bind 綁定 this 只有第一次綁定 this 生效
若是存在屢次綁定函數的上下文 this,則按照優先級進行判斷
首先,new 的方式優先級最高,接下來是 bind 這些函數,而後是 obj.foo() 這種調用方式,最後是 foo 這種調用方式,同時,箭頭函數的 this 一旦被綁定,就不會再被任何方式所改變。
主要用法以下:
function constructorFn (state, data) { this.data = data; this.state = state; } var instance1 = new constructorFn('a', 'b') console.log(instance1 instanceof constructorFn) // true || false
內部原理以下:
instanceof 主要的實現原理就是隻要右邊變量的 prototype 在左邊變量的原型鏈上便可。所以,instanceof 在查找的過程當中會遍歷左邊變量的原型鏈,直到找到右邊變量的 prototype,若是查找失敗,則會返回 false。
主要用法以下:
function constructorFn (state, data) { this.data = data; this.state = state; } var instance1 = new constructorFn('a', 'b'); var array1 = [1, 2, 3]; var boolean1 = true; var string1 = 'a'; var number1 = 123; var set1 = new Set(); var map1 = new Map(); class Class1 {}; var symbol1 = new Symbol(); console.log(Object.prototype.toString.call(instance1)); // [object Object] console.log(Object.prototype.toString.call(array1)); // [object Array] console.log(Object.prototype.toString.call(boolean1)); // [object Boolean] console.log(Object.prototype.toString.call(string1)); // [object String] console.log(Object.prototype.toString.call(number1)); // [object Number] console.log(Object.prototype.toString.call(constructorFn));// [object Function] console.log(Object.prototype.toString.call(null)); // [object Null] console.log(Object.prototype.toString.call(set1)); // [object Set] console.log(Object.prototype.toString.call(map1)); // [object Map] console.log(Object.prototype.toString.call(Class1)); // [object Function] console.log(Object.prototype.toString.call(symbol1)); // [object Symbol]
用法原理以下:
對象能夠有本身的 toString 方法,也能夠由從父類繼承過來的 toString 方法,這些方法的執行邏輯可能被更改過,估計直接經過 instance1.toString() 方
法不能準確獲取對象類型信息。
這種用法的思路是將 Object.prototype.toString
方法內部 this 綁定到當前的對象上調用,這樣無論當前的對象有沒有提供或繼承別的 toString 方法只會執行 Object.prototype
上的 toString 方法,確保了能夠準確打印對象的類型信息。
將子類的原型 prototype 指向父類的實例對象來實現父類屬性和方法的繼承;由於父類實例對象的構造函數 constructor 指向了父類原型,因此須要將子類原型構造函數 constructor 指向子類構造函數。
function Animal (name) { this.name = name; } Animal.prototype = { canRun: function () { console.log('it can run!'); } } function Cat () { this.speak = '喵!'; } Cat.prototype = new Animal('miao'); Cat.prototype.constructor = Cat;
經過 call、apply 改變函數的 this 指向,來將子類的 this 指向父類,在父類構造函數用當前子類 this 執行完成後,當前子類 this 即有了父類定義的屬性和方法。
function Animal (name) { this.name = name; } Animal.prototype = { canRun: function () { console.log('it can run!'); } } function Cat (name) { Animal.call(this, name); this.speak = '喵!'; }
原型鏈繼承與 call、apply 實現繼承的結合應用
核心是在子類的構造函數中經過 Animal.call(this)
繼承父類的屬性,而後改變子類的原型爲父類實例對象來繼承父類的方法。
function Animal(name) { this.name = name } Animal.prototype.getName = function() { console.log(this.name) } function Cat(name) { Animal.call(this, name) } Cat.prototype = new Animal() const cat1 = new Cat(1) cat1.getName() // 1 cat1 instanceof Animal // true
這種繼承方式優勢在於構造函數能夠傳參,不會與父類引用屬性共享,能夠複用父類的函數,可是也存在一個缺點就是在繼承父類函數的時候調用了父類構造函數,致使子類的原型上多了不須要的父類屬性,存在內存上的浪費。
說明下: es6 中的 class 類其實只是語法糖,上面打印 class 的類型信息能夠發現其本質仍是函數,只不過經過 extends、super等關鍵字對原型和構造函數的操做進行了簡化。
class Animal { constructor(name) { this.name = name } getValue() { console.log(this.name) } } class Cat extends Animal { constructor(name) { super(name) this.name = name } } let cat1 = new Cat(1) cat1.getName() // 1 cat1 instanceof Animal // true
class 實現繼承的核心在於使用 extends 代表繼承自哪一個父類,而且在子類構造函數中必須調用 super,由於這段代碼能夠當作 Animal.call(this, value)
。
TS 中的 class 繼承實際上是向 ECMAScript 規範靠近的,二者用法並沒有二致。
class Animal { public name: string | null = null constructor(name: string) { this.name = name } } class Cat extends Animal { constructor(name: string) { super(name) } getName () { console.log(this.name) } } let cat1 = new Cat('1') cat1.getName() // 1 cat1 instanceof Animal // true
既然看到這裏了不妨點個贊鼓勵下做者唄 :)
做者博客: https://blog.lessing.online
做者github: https://github.com/johniexu
【全面分析總結前端系列】