
前言
The last time, I have learned
【THE LAST TIME】一直是我想寫的一個系列,旨在厚積薄發,重溫前端。
也是給自己的查缺補漏和技術分享。
歡迎大家多多評論指點吐槽。
系列文章均首發于公衆号【全棧前端精選】,筆者文章集合詳見GitHub 位址:Nealyang/personalBlog。目錄和發文順序皆為暫定
首先我想說,【THE LAST TIME】系列的的内容,向來都是包括但不限于标題的範圍。
再回來說原型,老生常談的問題了。但是着實 現在不少熟練工也貌似沒有梳理清楚
Function
和
Object
、
prototype
和
__proto__
的關系,本文将從原型到繼承到 es6 文法糖的實作來介紹系統性的介紹 JavaScript 繼承。如果你能夠回答上來以下問題,那麼這位看官,基本這篇不用再花時間閱讀了~
- 為什麼
判斷typeof
是null
類型?Object
-
和Function
是什麼關系?Object
-
關鍵字具體做了什麼?手寫實作。new
-
和prototype
是什麼關系?什麼情況下相等?__proto__
- ES5 實作繼承有幾種方式,優缺點是啥
- ES6 如何實作一個類
- ES6
關鍵字實作原理是什麼extends
如果對以上問題有那麼一些疑惑~那麼。。。
THE LAST TIME 系列回顧
- 【THE LAST TIME】徹底吃透 JavaScript 執行機制
- 【THE LAST TIME】this:call、apply、bind
目錄
雖文章較長,但較為基礎。大家酌情閱讀所需章節。
注意文末有思考題哦~~
- 原型一把梭
-
- 函數對象和普通對象
-
__proto__
-
prototype
-
constructor
-
&&typeof
原理淺析instanceof
-
-
基本用法typeof
-
原理淺析typeof
-
基本用法instanceof
-
原理淺析instanceof
-
- ES5 中的繼承實作方式
-
-
手寫版本一new
-
手寫版本二new
-
關鍵字new
- 類式繼承
- 構造函數繼承
- 組合式繼承
- 原型式繼承
- 寄生式繼承
- 寄生組合式繼承
-
- ES6 類的實作原理
-
-
_inherits
-
_possibleConstructorReturn
- 基礎類
- 添加屬性
- 添加方法
-
關鍵字extend
-
原型一把梭
這。。。說是最基礎沒人反駁吧,說沒有用有人反駁吧,說很多人到現在沒梳理清楚沒人反駁吧!OK~ 為什麼文章那麼多,你卻還沒有弄明白?
在概念梳理之前,我們還是放一張老掉牙所謂的經典神圖:
-
就是一個方法,比如JavaScript 中内置的function Foo
、Array
等String
-
就是一個function Object
Object
-
就是function Function
Function
- 以上都是
,是以function
都是__proto__
Function.prototype
- 再次強調,
都是String、Array、Number、Function、Object
function
老鐵,如果對這張圖已非常清晰,那麼可直接跳過此章節
老規矩,我們直接來梳理概念。
函數對象和普通對象
老話說,萬物皆對象。而我們都知道在 JavaScript 中,建立對象有好幾種方式,比如對象字面量,或者直接通過構造函數
new
一個對象出來:
暫且我們先不管上面的代碼有什麼意義。至少,我們能看出,都是對象,卻存在着差異性
其實在 JavaScript 中,我們将對象分為函數對象和普通對象。所謂的函數對象,其實就是 JavaScript 的用函數來模拟的類實作。JavaScript 中的 Object 和 Function 就是典型的函數對象。
關于函數對象和普通對象,最直覺的感受就是。。。咱直接看代碼:
function fun1(){};
const fun2 = function(){};
const fun3 = new Function('name','console.log(name)');
const obj1 = {};
const obj2 = new Object();
const obj3 = new fun1();
const obj4 = new new Function();
console.log(typeof Object);//function
console.log(typeof Function);//function
console.log(typeof fun1);//function
console.log(typeof fun2);//function
console.log(typeof fun3);//function
console.log(typeof obj1);//object
console.log(typeof obj2);//object
console.log(typeof obj3);//object
console.log(typeof obj4);//object
不知道大家看到上述代碼有沒有一些疑惑的地方~别着急,我們一點一點梳理。
上述代碼中,
obj1
,
obj2
,
obj3
,
obj4
都是普通對象,
fun1
,
fun2
,
fun3
都是
Function
的執行個體,也就是函數對象。
是以可以看出,所有 Function 的執行個體都是函數對象,其他的均為普通對象,其中包括 Function 執行個體的執行個體。
JavaScript 中萬物皆對象,而對象皆出自構造(構造函數)。
上圖中,你疑惑的點是不是
Function
和
new Function
的關系。其實是這樣子的:
Function.__proto__ === Function.prototype//true
__proto__
__proto__
首先我們需要明确兩點:1️⃣和
__proto__
是對象獨有的。2️⃣
constructor
屬性是函數獨有的;
prototype
但是在 JavaScript 中,函數也是對象,是以函數也擁有
__proto__
和
constructor
屬性。
結合上面我們介紹的
Object
和
Function
的關系,看一下代碼和關系圖
function Person(){…};
let nealyang = new Person();
proto
再梳理上圖關系之前,我們再來講解下
__proto__
。
__proto__
的例子,說起來比較複雜,可以說是一個曆史問題。
ECMAScript 規範描述
prototype
是一個隐式引用,但之前的一些浏覽器,已經私自實作了
__proto__
這個屬性,使得可以通過
obj.__proto__
這個顯式的屬性通路,通路到被定義為隐式屬性的
prototype
。
是以,情況是這樣的,ECMAScript 規範說
prototype
應當是一個隐式引用:
- 通過
間接通路指定對象的Object.getPrototypeOf(obj)
對象prototype
- 通過
間接設定指定對象的Object.setPrototypeOf(obj, anotherObj)
對象prototype
- 部分浏覽器提前開了
的口子,使得可以通過__proto__
直接通路原型,通過obj.__proto__
直接設定原型obj.__proto__ = anotherObj
- ECMAScript 2015 規範隻好向事實低頭,将
屬性納入了規範的一部分__proto__
從浏覽器的列印結果我們可以看出,上圖對象
a
存在一個
__proto__
屬性。而事實上,他隻是開發者工具友善開發者檢視原型的故意渲染出來的一個虛拟節點。雖然我們可以檢視,但實則并不存在該對象上。
__proto__
屬性既不能被
for in
周遊出來,也不能被
Object.keys(obj)
查找出來。
通路對象的
obj.__proto__
屬性,預設走的是
Object.prototype
對象上
__proto__
屬性的 get/set 方法。
Object.defineProperty(Object.prototype,'__proto__',{
get(){
console.log('get')
}
});
({}).__proto__;
console.log((new Object()).__proto__);
關于更多
__proto__
更深入的介紹,可以參看工業聚大佬的《深入了解 JavaScript 原型》一文。
這裡我們需要知道的是,
__proto__
是對象所獨有的,并且
__proto__
是一個對象指向另一個對象,也就是他的原型對象。我們也可以了解為父類對象。它的作用就是當你在通路一個對象屬性的時候,如果該對象内部不存在這個屬性,那麼就回去它的
__proto__
屬性所指向的對象(父類對象)上查找,如果父類對象依舊不存在這個屬性,那麼就回去其父類的
__proto__
屬性所指向的父類的父類上去查找。以此類推,知道找到
null
。而這個查找的過程,也就構成了我們常說的原型鍊。
prototype
object that provides shared properties for other objects
在規範裡,prototype 被定義為:給其它對象提供共享屬性的對象。
prototype
自己也是對象,隻是被用以承擔某個職能罷了.
所有對象,都可以作為另一個對象的
prototype
來用。
修改
__proto__
的關系圖,我們添加了
prototype
,
prototype
是函數所獨有的。**它的作用就是包含可以給特定類型的所有執行個體提供共享的屬性和方法。它的含義就是函數的遠行對象,**也就是這個函數所建立的執行個體的遠行對象,正如上圖:
nealyang.__proto__ === Person.prototype
。任何函數在建立的時候,都會預設給該函數添加
prototype
屬性.
constructor
constructor
屬性也是對象所獨有的,它是一個對象指向一個函數,這個函數就是該對象的構造函數。
注意,每一個對象都有其對應的構造函數,本身或者繼承而來。單從
constructor
這個屬性來講,隻有
prototype
對象才有。每個函數在建立的時候,JavaScript 會同時建立一個該函數對應的
prototype
對象,而
函數建立的對象.__proto__ === 該函數.prototype
,該
函數.prototype.constructor===該函數本身
,故通過函數建立的對象即使自己沒有
constructor
屬性,它也能通過
__proto__
找到對應的
constructor
,是以任何對象最終都可以找到其對應的構造函數。
唯一特殊的可能就是我開篇抛出來的一個問題。JavaScript 原型的老祖宗:
Function
。它是它自己的構造函數。是以
Function.prototype === Function.__proto
。
為了直覺了解,我們在上面的圖中,繼續添加上
constructor
:
其中
constructor
屬性,虛線表示繼承而來的 constructor 屬性。
__proto__
介紹的原型鍊,我們在圖中直覺的标出來的話就是如下這個樣子
typeof && instanceof 原理
問什麼好端端的說原型、說繼承會扯到類型判斷的原理上來呢。畢竟原理上有一絲的聯系,往往面試也是由淺入深、順藤摸瓜的擰出整個知識面。是以這裡我們也簡單說一下吧。
typeof
MDN 文檔點選這裡:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/typeof
基本用法
typeof
的用法相比大家都比較熟悉,一般被用于來判斷一個變量的類型。我們可以使用
typeof
來判斷
number
、
undefined
、
symbol
、
string
、
function
、
boolean
、
object
這七種資料類型。但是遺憾的是,
typeof
在判斷
object
類型時候,有些許的尴尬。它并不能明确的告訴你,該
object
屬于哪一種
object
。
let s = new String('abc');
typeof s === 'object'// true
typeof null;//"object"
原理淺析
要想弄明白為什麼
typeof
判斷
null
為
object
,其實需要從js 底層如何存儲變量類型來說起。雖然說,這是 JavaScript 設計的一個 bug。
在 JavaScript 最初的實作中,JavaScript 中的值是由一個表示類型的标簽和實際資料值表示的。對象的類型标簽是 0。由于
null
代表的是空指針(大多數平台下值為 0x00),是以,
null
的類型标簽是 0,
typeof null
也是以傳回
"object"
。曾有一個 ECMAScript 的修複提案(通過選擇性加入的方式),但被拒絕了。該提案會導緻
typeof null === 'null'
。
js 在底層存儲變量的時候,會在變量的機器碼的低位1-3位存儲其類型資訊:
- 1:整數
- 110:布爾
- 100:字元串
- 010:浮點數
- 000:對象
但是,對于
undefined
和
null
來說,這兩個值的資訊存儲是有點特殊的:
-
:所有機器碼均為0null
-
:用 −2^30 整數來表示undefined
是以在用
typeof
來判斷變量類型的時候,我們需要注意,最好是用
typeof
來判斷基本資料類型(包括
symbol
),避免對
null
的判斷。
隻是咱在讨論原型帶出的
typeof
的附加讨論區
instanceof
instanceof
object instanceof constructor
instanceof
和
typeof
非常的類似。
instanceof
運算符用來檢測
constructor.prototype
是否存在于參數
object
的原型鍊上。與
typeof
方法不同的是,
instanceof
方法要求開發者明确地确認對象為某特定類型。
基本用法
// 定義構造函數
function C(){}
function D(){}
var o = new C();
o instanceof C; // true,因為 Object.getPrototypeOf(o) === C.prototype
o instanceof D; // false,因為 D.prototype 不在 o 的原型鍊上
o instanceof Object; // true,因為 Object.prototype.isPrototypeOf(o) 傳回 true
C.prototype instanceof Object // true,同上
C.prototype = {};
var o2 = new C();
o2 instanceof C; // true
o instanceof C; // false,C.prototype 指向了一個空對象,這個空對象不在 o 的原型鍊上.
D.prototype = new C(); // 繼承
var o3 = new D();
o3 instanceof D; // true
o3 instanceof C; // true 因為 C.prototype 現在在 o3 的原型鍊上
如上,是
instanceof
的基本用法,它可以判斷一個執行個體是否是其父類型或者祖先類型的執行個體。
console.log(Object instanceof Object);//true
console.log(Function instanceof Function);//true
console.log(Number instanceof Number);//false
console.log(String instanceof String);//false
console.log(Function instanceof Object);//true
console.log(Foo instanceof Function);//true
console.log(Foo instanceof Foo);//false
為什麼
Object
和
Function
instanceof
自己等于
true
,而其他類
instanceof
自己卻又不等于
true
呢?如何解釋?
要想從根本上了解
instanceof
的奧秘,需要從兩個方面着手:1,語言規範中是如何定義這個運算符的。2,JavaScript 原型繼承機制。
原理淺析
經過上述的分析,想必大家對這種經典神圖已經不那麼陌生了吧,那咱就對着這張圖來聊聊
instanceof
這裡,我直接将規範定義翻譯為 JavaScript 代碼如下:
function instance_of(L, R) {//L 表示左表達式,R 表示右表達式
var O = R.prototype;// 取 R 的顯示原型
L = L.__proto__;// 取 L 的隐式原型
while (true) {
if (L === null)
return false;
if (O === L)// 這裡重點:當 O 嚴格等于 L 時,傳回 true
return true;
L = L.__proto__;
}
}
是以如上原理,加上上文解釋的原型相關知識,我們再來解析下為什麼
Object
和
Function
instanceof
自己等于
true
。
-
Object instanceof Object
// 為了友善表述,首先區分左側表達式和右側表達式
ObjectL = Object, ObjectR = Object;
// 下面根據規範逐漸推演
O = ObjectR.prototype = Object.prototype
L = ObjectL.__proto__ = Function.prototype
// 第一次判斷
O != L
// 循環查找 L 是否還有 __proto__
L = Function.prototype.__proto__ = Object.prototype
// 第二次判斷
O == L
// 傳回 true
- Function instanceof Function
// 為了友善表述,首先區分左側表達式和右側表達式
FunctionL = Function, FunctionR = Function;
// 下面根據規範逐漸推演
O = FunctionR.prototype = Function.prototype
L = FunctionL.__proto__ = Function.prototype
// 第一次判斷
O == L
// 傳回 true
-
Foo instanceof Foo
// 為了友善表述,首先區分左側表達式和右側表達式
FooL = Foo, FooR = Foo;
// 下面根據規範逐漸推演
O = FooR.prototype = Foo.prototype
L = FooL.__proto__ = Function.prototype
// 第一次判斷
O != L
// 循環再次查找 L 是否還有 __proto__
L = Function.prototype.__proto__ = Object.prototype
// 第二次判斷
O != L
// 再次循環查找 L 是否還有 __proto__
L = Object.prototype.__proto__ = null
// 第三次判斷
L == null
// 傳回 false
ES5 中的繼承實作方式
在繼承實作上,工業聚大大在他的原型文章中,将原型繼承分為兩大類,顯式繼承和隐式繼承。感興趣的可以點選文末參考連結檢視。
但是本文還是希望能夠基于“通俗”的方式來講解幾種常見的繼承方式和優缺點。大家可多多對比檢視,其實原理都是一樣,名詞也隻是所謂的代稱而已。
關于繼承的文章,很多書本和部落格中都有很詳細的講解。以下幾種繼承方式,均總結與《JavaScript 設計模式》一書。也是筆者三年前寫的一篇文章了。
new 關鍵字
在講解繼承之前呢,我覺得
new
這個東西很有必要介紹下~
一個例子看下
new
關鍵字都幹了啥
function Person(name,age){
this.name = name;
this.age = age;
this.sex = 'male';
}
Person.prototype.isHandsome = true;
Person.prototype.sayName = function(){
console.log(`Hello , my name is ${this.name}`);
}
let handsomeBoy = new Person('Nealyang',25);
console.log(handsomeBoy.name) // Nealyang
console.log(handsomeBoy.sex) // male
console.log(handsomeBoy.isHandsome) // true
handsomeBoy.sayName(); // Hello , my name is Nealyang
從上面的例子我們可以看到:
- 通路到
構造函數裡的屬性Person
- 通路到
中的屬性Person.prototype
new 手寫版本一
function objectFactory() {
const obj = new Object(),//從Object.prototype上克隆一個對象
Constructor = [].shift.call(arguments);//取得外部傳入的構造器
const F=function(){};
F.prototype= Constructor.prototype;
obj=new F();//指向正确的原型
Constructor.apply(obj, arguments);//借用外部傳入的構造器給obj設定屬性
return obj;//傳回 obj
};
- 用
的方式建立了一個對象 objnew Object()
- 取出第一個參數,就是我們要傳入的構造函數。此外因為 shift 會修改原數組,是以
會被去除第一個參數arguments
- 将 obj 的原型指向構造函數,這樣 obj 就可以通路到構造函數原型中的屬性
- 使用
,改變構造函數apply
的指向到建立的對象,這樣 obj 就可以通路到構造函數中的屬性this
- 傳回 obj
下面我們來測試一下:
function Person(name,age){
this.name = name;
this.age = age;
this.sex = 'male';
}
Person.prototype.isHandsome = true;
Person.prototype.sayName = function(){
console.log(`Hello , my name is ${this.name}`);
}
function objectFactory() {
let obj = new Object(),//從Object.prototype上克隆一個對象
Constructor = [].shift.call(arguments);//取得外部傳入的構造器
console.log({Constructor})
const F=function(){};
F.prototype= Constructor.prototype;
obj=new F();//指向正确的原型
Constructor.apply(obj, arguments);//借用外部傳入的構造器給obj設定屬性
return obj;//傳回 obj
};
let handsomeBoy = objectFactory(Person,'Nealyang',25);
console.log(handsomeBoy.name) // Nealyang
console.log(handsomeBoy.sex) // male
console.log(handsomeBoy.isHandsome) // true
handsomeBoy.sayName(); // Hello , my name is Nealyang
注意上面我們沒有直接修改 obj 的
__proto__
隐式挂載。
new 手寫版本二
考慮構造函數又傳回值的情況:
- 如果構造函數傳回一個對象,那麼我們也傳回這個對象
- 如上否則,就傳回預設值
function objectFactory() {
var obj = new Object(),//從Object.prototype上克隆一個對象
Constructor = [].shift.call(arguments);//取得外部傳入的構造器
var F=function(){};
F.prototype= Constructor.prototype;
obj=new F();//指向正确的原型
var ret = Constructor.apply(obj, arguments);//借用外部傳入的構造器給obj設定屬性
return typeof ret === 'object' ? ret : obj;//確定構造器總是傳回一個對象
};
關于 call、apply、bind、this 等用法和原理講解:【THE LAST TIME】this:call、apply、bind
類式繼承
function SuperClass() {
this.superValue = true;
}
SuperClass.prototype.getSuperValue = function() {
return this.superValue;
}
function SubClass() {
this.subValue = false;
}
SubClass.prototype = new SuperClass();
SubClass.prototype.getSubValue = function() {
return this.subValue;
}
var instance = new SubClass();
console.log(instance instanceof SuperClass)//true
console.log(instance instanceof SubClass)//true
console.log(SubClass instanceof SuperClass)//false
從我們之前介紹的的原理我們知道,第三個
instanceof
如果這麼寫就傳回
console
了
true
console.log(SubClass.prototype instanceof SuperClass)
雖然實作起來清晰簡潔,但是這種繼承方式有兩個缺點:
- 由于子類通過其原型prototype對父類執行個體化,繼承了父類,是以說父類中如果共有屬性是引用類型,就會在子類中被所有的執行個體所共享,是以一個子類的執行個體更改子類原型從父類構造函數中繼承的共有屬性就會直接影響到其他的子類
- 由于子類實作的繼承是靠其原型prototype對父類進行執行個體化實作的,是以在建立父類的時候,是無法向父類傳遞參數的。因而在執行個體化父類的時候也無法對父類構造函數内的屬性進行初始化
構造函數繼承
function SuperClass(id) {
this.books = ['js','css'];
this.id = id;
}
SuperClass.prototype.showBooks = function() {
console.log(this.books);
}
function SubClass(id) {
//繼承父類
SuperClass.call(this,id);
}
//建立第一個子類執行個體
var instance1 = new SubClass(10);
//建立第二個子類執行個體
var instance2 = new SubClass(11);
instance1.books.push('html');
console.log(instance1)
console.log(instance2)
instance1.showBooks();//TypeError
SuperClass.call(this,id)
當然就是構造函數繼承的核心語句了.由于父類中給this綁定屬性,是以子類自然也就繼承父類的共有屬性。由于這種類型的繼承沒有涉及到原型
prototype
,是以父類的原型方法自然不會被子類繼承,而如果想被子類繼承,就必須放到構造函數中,這樣建立出來的每一個執行個體都會單獨的擁有一份而不能共用,這樣就違背了代碼複用的原則,是以綜合上述兩種,我們提出了組合式繼承方法
組合式繼承
function SuperClass(name) {
this.name = name;
this.books = ['Js','CSS'];
}
SuperClass.prototype.getBooks = function() {
console.log(this.books);
}
function SubClass(name,time) {
SuperClass.call(this,name);
this.time = time;
}
SubClass.prototype = new SuperClass();
SubClass.prototype.getTime = function() {
console.log(this.time);
}
如上,我們就解決了之前說到的一些問題,但是是不是從代碼看,還是有些不爽呢?至少這個
SuperClass
的構造函數執行了兩遍就感覺非常的不妥.
原型式繼承
function inheritObject(o) {
//聲明一個過渡對象
function F() { }
//過渡對象的原型繼承父對象
F.prototype = o;
//傳回過渡對象的執行個體,該對象的原型繼承了父對象
return new F();
}
原型式繼承大緻的實作方式如上,是不是想到了我們
new
關鍵字模拟的實作?
其實這種方式和類式繼承非常的相似,他隻是對類式繼承的一個封裝,其中的過渡對象就相當于類式繼承的子類,隻不過在原型繼承中作為一個普通的過渡對象存在,目的是為了建立要傳回的新的執行個體對象。
var book = {
name:'js book',
likeBook:['css Book','html book']
}
var newBook = inheritObject(book);
newBook.name = 'ajax book';
newBook.likeBook.push('react book');
var otherBook = inheritObject(book);
otherBook.name = 'canvas book';
otherBook.likeBook.push('node book');
console.log(newBook,otherBook);
如上代碼我們可以看出,原型式繼承和類式繼承一個樣子,對于引用類型的變量,還是存在子類執行個體共享的情況。
是以,我們還有下面的寄生式繼
寄生式繼承
var book = {
name:'js book',
likeBook:['html book','css book']
}
function createBook(obj) {
//通過原型方式建立新的對象
var o = new inheritObject(obj);
// 拓展新對象
o.getName = function(name) {
console.log(name)
}
// 傳回拓展後的新對象
return o;
}
其實寄生式繼承就是對原型繼承的拓展,一個二次封裝的過程,這樣新建立的對象不僅僅有父類的屬性和方法,還新增了别的屬性和方法。
寄生組合式繼承
回到之前的組合式繼承,那時候我們将類式繼承和構造函數繼承組合使用,但是存在的問題就是子類不是父類的執行個體,而子類的原型是父類的執行個體,是以才有了寄生組合式繼承
而寄生組合式繼承是寄生式繼承和構造函數繼承的組合。但是這裡寄生式繼承有些特殊,這裡他處理不是對象,而是類的原型。
function inheritObject(o) {
//聲明一個過渡對象
function F() { }
//過渡對象的原型繼承父對象
F.prototype = o;
//傳回過渡對象的執行個體,該對象的原型繼承了父對象
return new F();
}
function inheritPrototype(subClass,superClass) {
// 複制一份父類的原型副本到變量中
var p = inheritObject(superClass.prototype);
// 修正因為重寫子類的原型導緻子類的constructor屬性被修改
p.constructor = subClass;
// 設定子類原型
subClass.prototype = p;
}
組合式繼承中,通過構造函數繼承的屬性和方法都是沒有問題的,是以這裡我們主要探究通過寄生式繼承重新繼承父類的原型。
我們需要繼承的僅僅是父類的原型,不用去調用父類的構造函數。換句話說,在構造函數繼承中,我們已經調用了父類的構造函數。是以我們需要的就是父類的原型對象的一個副本,而這個副本我們可以通過原型繼承拿到,但是這麼直接指派給子類會有問題,因為對父類原型對象複制得到的複制對象p中的
constructor
屬性指向的不是
subClass
子類對象,是以在寄生式繼承中要對複制對象p做一次增強,修複起
constructor
屬性指向性不正确的問題,最後将得到的複制對象p指派給子類原型,這樣子類的原型就繼承了父類的原型并且沒有執行父類的構造函數。
function SuperClass(name) {
this.name = name;
this.books=['js book','css book'];
}
SuperClass.prototype.getName = function() {
console.log(this.name);
}
function SubClass(name,time) {
SuperClass.call(this,name);
this.time = time;
}
inheritPrototype(SubClass,SuperClass);
SubClass.prototype.getTime = function() {
console.log(this.time);
}
var instance1 = new SubClass('React','2017/11/11')
var instance2 = new SubClass('Js','2018/22/33');
instance1.books.push('test book');
console.log(instance1.books,instance2.books);
instance2.getName();
instance2.getTime();
這種方式繼承其實如上圖所示,其中最大的改變就是子類原型中的處理,被賦予父類原型中的一個引用,這是一個對象,是以有一點你需要注意,就是子類在想添加原型方法必須通過prototype.來添加,否則直接賦予對象就會覆寫從父類原型繼承的對象了.
ES6 類的實作原理
關于 ES6 中的 class 的一些基本用法和介紹,限于篇幅,本文就不做介紹了。該章節,我們主要通過 babel的 REPL來檢視分析 es6 中各個文法糖包括繼承的一些實作方式。
基礎類
我們就會按照這個類,來回摩擦。然後再來分析編譯後的代碼。
"use strict";
function _instanceof(left, right) {
if (
right != null &&
typeof Symbol !== "undefined" &&
right[Symbol.hasInstance]
) {
return !!right[Symbol.hasInstance](left);
} else {
return left instanceof right;
}
}
function _classCallCheck(instance, Constructor) {
if (!_instanceof(instance, Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Person = function Person(name) {
_classCallCheck(this, Person);
this.name = name;
};
_instanceof
就是來判斷執行個體關系的的。上述代碼就比較簡單了,
_classCallCheck
的作用就是檢查
Person
這個類,是否是通過
new
關鍵字調用的。畢竟被編譯成 ES5 以後,
function
可以直接調用,但是如果直接調用的話,
this
就指向
window
對象,就會
Throw Error
了.
添加屬性
"use strict";
function _instanceof(left, right) {...}
function _classCallCheck(instance, Constructor) {...}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
var Person = function Person(name) {
_classCallCheck(this, Person);
_defineProperty(this, "shili", '執行個體屬性');
this.name = name;
};
_defineProperty(Person, "jingtai", ' 靜态屬性');
其實就是講屬性指派給誰的問題。如果是執行個體屬性,直接指派到
this
上,如果是靜态屬性,則指派類上。
_defineProperty
也就是來判斷下是否屬性名重複而已。
添加方法
"use strict";
function _instanceof(left, right) {...}
function _classCallCheck(instance, Constructor) {...}
function _defineProperty(obj, key, value) {...}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
var Person =
function () {
function Person(name) {
_classCallCheck(this, Person);
_defineProperty(this, "shili", '執行個體屬性');
this.name = name;
}
_createClass(Person, [{
key: "sayName",
value: function sayName() {
return this.name;
}
}, {
key: "name",
get: function get() {
return 'Nealyang';
},
set: function set(newName) {
console.log('new name is :' + newName);
}
}], [{
key: "eat",
value: function eat() {
return 'eat food';
}
}]);
return Person;
}();
_defineProperty(Person, "jingtai", ' 靜态屬性');
看起來代碼量還不少,其實就是一個
_createClass
函數和
_defineProperties
函數而已。
首先看
_createClass
這個函數的三個參數,第一個是構造函數,第二個是需要添加到原型上的函數數組,第三個是添加到類本身的函數數組。其實這個函數的作用非常的簡單。就是加強一下構造函數,所謂的加強構造函數就是給構造函數或者其原型上添加一些函數。
而
_defineProperties
就是多個
_defineProperty
(感覺是廢話,不過的确如此)。預設
enumerable
為
false
,
configurable
為
true
。
其實如上就是 es6 class 的實作原理。
extend 關鍵字
"use strict";
function _instanceof(left, right) {...}
function _classCallCheck(instance, Constructor) {...}
var Parent = function Parent(name) {...};
function _typeof(obj) {
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
_typeof = function _typeof(obj) {
return typeof obj;
};
} else {
_typeof = function _typeof(obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
}
return _typeof(obj);
}
function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === "object" || typeof call === "function")) {
return call;
}
return _assertThisInitialized(self);
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
});
if (superClass) _setPrototypeOf(subClass, superClass);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
var Child =
function (_Parent) {
_inherits(Child, _Parent);
function Child(name, age) {
var _this;
_classCallCheck(this, Child);
_this = _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this, name)); // 調用父類的 constructor(name)
_this.age = age;
return _this;
}
return Child;
}(Parent);
var child1 = new Child('全棧前端精選', '0.3');
console.log(child1);
删去類相關的代碼生成,剩下的就是繼承的文法糖剖析了。其中
super
關鍵字表示父類的構造函數,相當于 ES5 的
Parent.call(this)
,然後再根據我們上文說到的繼承方式,有沒有感覺該內建的實作跟我們說的寄生組合式繼承非常的相似呢?
在 ES6 class 中,子類必須在
constructor
方法中調用
super
方法,否則建立執行個體時會報錯。這是因為子類沒有自己的
this
對象,而是繼承父類的
this
對象,然後對其進行加工。如果不調用
super
方法,子類就得不到
this
對象。
也正是因為這個原因,在子類的構造函數中,隻有調用
super
之後,才可以使用
this
關鍵字,否則會報錯。
關于 ES6 中原型鍊示意圖可以參照如下示意圖:
圖檔來自冴羽的部落格
關于ES6 中的
extend
關鍵字,上述代碼我們完全可以根據執行來看。其實重點代碼無非就兩行:
_inherits(Child, _Parent);
_this = _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this, name));
我們分别來分析下具體的實作:
_inherits
代碼比較簡單,都是上文提到的内容,就是建立 Child 和 Parent 的原型鍊關系。代碼解釋已備注在代碼内
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {//subClass 類型判斷
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {//Object.create 第二個參數是給subClass.prototype添加了 constructor 屬性
value: subClass,
writable: true,
configurable: true//注意這裡enumerable沒有指名,預設是 false,也就是說constructor為不可枚舉的。
}
});
if (superClass) _setPrototypeOf(subClass, superClass);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
_possibleConstructorReturn
_this = _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this, name));
根據上圖我們整理的 es6 原型圖可知:
Child.prototype === Parent
是以上面的代碼我們可以翻譯為:
_this = _possibleConstructorReturn(this, Parent.call(this, name));
然後我們再一層一層撥源碼的實作
function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === "object" || typeof call === "function")) {
return call;
}
return _assertThisInitialized(self);
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}
上述代碼,
self
其實就是 Child 的 IIFE傳回的
function
new
調用的
this
,列印出來結果如下:
這裡可能對
Parent.call(this,name)
有些疑惑,沒關系,我們可以在 Chrome 下調試下。
可以看到,當我們
Parent
的構造函數這麼寫
class Parent {
constructor(name) {
this.name = name;
}
}
那麼最終,傳遞給
_possibleConstructorReturn
函數的第二參數
call
就是一個
undefined
。是以在
_possibleConstructorReturn
函數裡面會對
call
進行判斷,傳回正确的
this
指向:
Child
。
是以整體代碼的目的就是根據 Parent 構造函數的傳回值類型确定子類構造函數 this 的初始值 _this。
最後
【THE LAST TIME】系列關于 JavaScript 基礎的文章目前更新三篇,我們最後再來一道經典的面試題吧!
function Foo() {
getName = function() {
alert(1);
};
return this;
}
Foo.getName = function() {
alert(2);
};
Foo.prototype.getName = function() {
alert(3);
};
var getName = function() {
alert(4);
};
function getName() {
alert(5);
}
//請寫出以下輸出結果:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
老鐵,評論區留下你的思考吧~
參考文獻
- 深入了解 JavaScript 原型
- 幫你徹底搞懂JS中的prototype、__proto__與constructor
- JavaScript instanceof 運算符深入剖析
- JavaScript深入之建立對象的多種方式以及優缺點
- ES6 系列之 Babel 是如何編譯 Class 的(上)
- ES6—類的實作原理
- es6類和繼承的實作原理
- JavaScript深入之new的模拟實作
完
❤️ 看完三件事
如果你覺得這篇内容對你挺有啟發,我想邀請你幫我三個小忙:
- 點個【在看】,或者分享轉發,讓更多的人也能看到這篇内容
- 關注公衆号【全棧前端精選】,不定期分享原創&精品技術文章。
- 公衆号内回複:【 1 】。加入全棧前端精選公衆号交流群。
歡迎評論區留下你的精彩評論~
覺得文章不錯可以分享到朋友圈讓更多的小夥伴看到哦~
客官!在看一下呗