所有JS原型相關(guān)知識(shí)點(diǎn)

前言
The last time, I have learned
【THE LAST TIME】一直是我想寫(xiě)的一個(gè)系列,旨在厚積薄發(fā),重溫前端。
也是給自己的查缺補(bǔ)漏和技術(shù)分享。
歡迎大家多多評(píng)論指點(diǎn)吐槽。

系列文章均首發(fā)于公眾號(hào)【全棧前端精選】,筆者文章集合詳見(jiàn)GitHub 地址:Nealyang/personalBlog。目錄和發(fā)文順序皆為暫定
首先我想說(shuō),【THE LAST TIME】系列的的內(nèi)容,向來(lái)都是包括但不限于標(biāo)題的范圍。
再回來(lái)說(shuō)原型,老生常談的問(wèn)題了。但是著實(shí) 現(xiàn)在不少熟練工也貌似沒(méi)有梳理清楚 Function 和 Object、prototype 和__proto__的關(guān)系,本文將從原型到繼承到 es6 語(yǔ)法糖的實(shí)現(xiàn)來(lái)介紹系統(tǒng)性的介紹 JavaScript 繼承。如果你能夠回答上來(lái)以下問(wèn)題,那么這位看官,基本這篇不用再花時(shí)間閱讀了~
為什么 typeof判斷null是Object類(lèi)型?Function和Object是什么關(guān)系?new關(guān)鍵字具體做了什么?手寫(xiě)實(shí)現(xiàn)。prototype和__proto__是什么關(guān)系?什么情況下相等?ES5 實(shí)現(xiàn)繼承有幾種方式,優(yōu)缺點(diǎn)是啥 ES6 如何實(shí)現(xiàn)一個(gè)類(lèi) ES6 extends關(guān)鍵字實(shí)現(xiàn)原理是什么
如果對(duì)以上問(wèn)題有那么一些疑惑~那么。。。

THE LAST TIME 系列回顧
目錄
雖文章較長(zhǎng),但較為基礎(chǔ)。大家酌情閱讀所需章節(jié)。
注意文末有思考題哦~~
原型一把梭 函數(shù)對(duì)象和普通對(duì)象 __proto__prototypeconstructortypeof&&instanceof原理淺析typeof基本用法typeof原理淺析instanceof基本用法instanceof原理淺析ES5 中的繼承實(shí)現(xiàn)方式 new手寫(xiě)版本一new手寫(xiě)版本二new關(guān)鍵字類(lèi)式繼承 構(gòu)造函數(shù)繼承 組合式繼承 原型式繼承 寄生式繼承 寄生組合式繼承 ES6 類(lèi)的實(shí)現(xiàn)原理 _inherits_possibleConstructorReturn基礎(chǔ)類(lèi) 添加屬性 添加方法 extend關(guān)鍵字
原型一把梭
這。。。說(shuō)是最基礎(chǔ)沒(méi)人反駁吧,說(shuō)沒(méi)有用有人反駁吧,說(shuō)很多人到現(xiàn)在沒(méi)梳理清楚沒(méi)人反駁吧!OK~ 為什么文章那么多,你卻還沒(méi)有弄明白?

在概念梳理之前,我們還是放一張老掉牙所謂的經(jīng)典神圖:

function Foo就是一個(gè)方法,比如JavaScript 中內(nèi)置的Array、String等function Object就是一個(gè)Objectfunction Function就是Function以上都是 function,所以__proto__都是Function.prototype再次強(qiáng)調(diào), String、Array、Number、Function、Object都是function
老鐵,如果對(duì)這張圖已非常清晰,那么可直接跳過(guò)此章節(jié)
老規(guī)矩,我們直接來(lái)梳理概念。
函數(shù)對(duì)象和普通對(duì)象
老話(huà)說(shuō),萬(wàn)物皆對(duì)象。而我們都知道在 JavaScript 中,創(chuàng)建對(duì)象有好幾種方式,比如對(duì)象字面量,或者直接通過(guò)構(gòu)造函數(shù) new 一個(gè)對(duì)象出來(lái):

暫且我們先不管上面的代碼有什么意義。至少,我們能看出,都是對(duì)象,卻存在著差異性
其實(shí)在 JavaScript 中,我們將對(duì)象分為函數(shù)對(duì)象和普通對(duì)象。所謂的函數(shù)對(duì)象,其實(shí)就是 JavaScript 的用函數(shù)來(lái)模擬的類(lèi)實(shí)現(xiàn)。JavaScript 中的 Object 和 Function 就是典型的函數(shù)對(duì)象。
關(guān)于函數(shù)對(duì)象和普通對(duì)象,最直觀(guān)的感受就是。。。咱直接看代碼:
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
不知道大家看到上述代碼有沒(méi)有一些疑惑的地方~別著急,我們一點(diǎn)一點(diǎn)梳理。
上述代碼中,obj1,obj2,obj3,obj4都是普通對(duì)象,fun1,fun2,fun3 都是 Function 的實(shí)例,也就是函數(shù)對(duì)象。
所以可以看出,所有 Function 的實(shí)例都是函數(shù)對(duì)象,其他的均為普通對(duì)象,其中包括 Function 實(shí)例的實(shí)例。

JavaScript 中萬(wàn)物皆對(duì)象,而對(duì)象皆出自構(gòu)造(構(gòu)造函數(shù))。
上圖中,你疑惑的點(diǎn)是不是 Function 和 new Function 的關(guān)系。其實(shí)是這樣子的:
Function.__proto__ === Function.prototype//true
__proto__
首先我們需要明確兩點(diǎn):1??
__proto__和constructor是對(duì)象獨(dú)有的。2??prototype屬性是函數(shù)獨(dú)有的;
但是在 JavaScript 中,函數(shù)也是對(duì)象,所以函數(shù)也擁有__proto__和 constructor屬性。

結(jié)合上面我們介紹的 Object 和 Function 的關(guān)系,看一下代碼和關(guān)系圖
function Person(){…};
let nealyang = new Person();

再梳理上圖關(guān)系之前,我們?cè)賮?lái)講解下__proto__。

__proto__ 的例子,說(shuō)起來(lái)比較復(fù)雜,可以說(shuō)是一個(gè)歷史問(wèn)題。
ECMAScript 規(guī)范描述 prototype 是一個(gè)隱式引用,但之前的一些瀏覽器,已經(jīng)私自實(shí)現(xiàn)了 __proto__這個(gè)屬性,使得可以通過(guò) obj.__proto__ 這個(gè)顯式的屬性訪(fǎng)問(wèn),訪(fǎng)問(wèn)到被定義為隱式屬性的 prototype。
因此,情況是這樣的,ECMAScript 規(guī)范說(shuō) prototype 應(yīng)當(dāng)是一個(gè)隱式引用:
通過(guò) Object.getPrototypeOf(obj)間接訪(fǎng)問(wèn)指定對(duì)象的prototype對(duì)象通過(guò) Object.setPrototypeOf(obj, anotherObj)間接設(shè)置指定對(duì)象的prototype對(duì)象部分瀏覽器提前開(kāi)了 __proto__的口子,使得可以通過(guò)obj.__proto__直接訪(fǎng)問(wèn)原型,通過(guò)obj.__proto__ = anotherObj直接設(shè)置原型ECMAScript 2015 規(guī)范只好向事實(shí)低頭,將 __proto__屬性納入了規(guī)范的一部分
從瀏覽器的打印結(jié)果我們可以看出,上圖對(duì)象 a 存在一個(gè)__proto__屬性。而事實(shí)上,他只是開(kāi)發(fā)者工具方便開(kāi)發(fā)者查看原型的故意渲染出來(lái)的一個(gè)虛擬節(jié)點(diǎn)。雖然我們可以查看,但實(shí)則并不存在該對(duì)象上。
__proto__屬性既不能被 for in 遍歷出來(lái),也不能被 Object.keys(obj) 查找出來(lái)。
訪(fǎng)問(wèn)對(duì)象的 obj.__proto__ 屬性,默認(rèn)走的是 Object.prototype 對(duì)象上 __proto__ 屬性的 get/set 方法。
Object.defineProperty(Object.prototype,'__proto__',{
get(){
console.log('get')
}
});
({}).__proto__;
console.log((new Object()).__proto__);

關(guān)于更多__proto__更深入的介紹,可以參看工業(yè)聚大佬的《深入理解 JavaScript 原型》一文。
這里我們需要知道的是,__proto__是對(duì)象所獨(dú)有的,并且__proto__是一個(gè)對(duì)象指向另一個(gè)對(duì)象,也就是他的原型對(duì)象。我們也可以理解為父類(lèi)對(duì)象。它的作用就是當(dāng)你在訪(fǎng)問(wèn)一個(gè)對(duì)象屬性的時(shí)候,如果該對(duì)象內(nèi)部不存在這個(gè)屬性,那么就回去它的__proto__屬性所指向的對(duì)象(父類(lèi)對(duì)象)上查找,如果父類(lèi)對(duì)象依舊不存在這個(gè)屬性,那么就回去其父類(lèi)的__proto__屬性所指向的父類(lèi)的父類(lèi)上去查找。以此類(lèi)推,知道找到 null。而這個(gè)查找的過(guò)程,也就構(gòu)成了我們常說(shuō)的原型鏈。
prototype
object that provides shared properties for other objects
在規(guī)范里,prototype 被定義為:給其它對(duì)象提供共享屬性的對(duì)象。prototype 自己也是對(duì)象,只是被用以承擔(dān)某個(gè)職能罷了.
所有對(duì)象,都可以作為另一個(gè)對(duì)象的 prototype 來(lái)用。

修改__proto__的關(guān)系圖,我們添加了 prototype,prototype是函數(shù)所獨(dú)有的。**它的作用就是包含可以給特定類(lèi)型的所有實(shí)例提供共享的屬性和方法。它的含義就是函數(shù)的遠(yuǎn)行對(duì)象,**也就是這個(gè)函數(shù)所創(chuàng)建的實(shí)例的遠(yuǎn)行對(duì)象,正如上圖:nealyang.__proto__ === Person.prototype。任何函數(shù)在創(chuàng)建的時(shí)候,都會(huì)默認(rèn)給該函數(shù)添加 prototype 屬性.
constructor
constructor屬性也是對(duì)象所獨(dú)有的,它是一個(gè)對(duì)象指向一個(gè)函數(shù),這個(gè)函數(shù)就是該對(duì)象的構(gòu)造函數(shù)。
注意,每一個(gè)對(duì)象都有其對(duì)應(yīng)的構(gòu)造函數(shù),本身或者繼承而來(lái)。單從constructor這個(gè)屬性來(lái)講,只有prototype對(duì)象才有。每個(gè)函數(shù)在創(chuàng)建的時(shí)候,JavaScript 會(huì)同時(shí)創(chuàng)建一個(gè)該函數(shù)對(duì)應(yīng)的prototype對(duì)象,而函數(shù)創(chuàng)建的對(duì)象.__proto__ === 該函數(shù).prototype,該函數(shù).prototype.constructor===該函數(shù)本身,故通過(guò)函數(shù)創(chuàng)建的對(duì)象即使自己沒(méi)有constructor屬性,它也能通過(guò)__proto__找到對(duì)應(yīng)的constructor,所以任何對(duì)象最終都可以找到其對(duì)應(yīng)的構(gòu)造函數(shù)。
唯一特殊的可能就是我開(kāi)篇拋出來(lái)的一個(gè)問(wèn)題。JavaScript 原型的老祖宗:Function。它是它自己的構(gòu)造函數(shù)。所以Function.prototype === Function.__proto。
為了直觀(guān)了解,我們?cè)谏厦娴膱D中,繼續(xù)添加上constructor:

其中 constructor 屬性,虛線(xiàn)表示繼承而來(lái)的 constructor 屬性。
__proto__介紹的原型鏈,我們?cè)趫D中直觀(guān)的標(biāo)出來(lái)的話(huà)就是如下這個(gè)樣子

typeof && instanceof 原理
問(wèn)什么好端端的說(shuō)原型、說(shuō)繼承會(huì)扯到類(lèi)型判斷的原理上來(lái)呢。畢竟原理上有一絲的聯(lián)系,往往面試也是由淺入深、順藤摸瓜的擰出整個(gè)知識(shí)面。所以這里我們也簡(jiǎn)單說(shuō)一下吧。
typeof
MDN 文檔點(diǎn)擊這里:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/typeof
基本用法
typeof 的用法相比大家都比較熟悉,一般被用于來(lái)判斷一個(gè)變量的類(lèi)型。我們可以使用 typeof 來(lái)判斷number、undefined、symbol、string、function、boolean、object 這七種數(shù)據(jù)類(lèi)型。但是遺憾的是,typeof 在判斷 object 類(lèi)型時(shí)候,有些許的尷尬。它并不能明確的告訴你,該 object 屬于哪一種 object。
let s = new String('abc');
typeof s === 'object'// true
typeof null;//"object"
原理淺析
要想弄明白為什么 typeof 判斷 null 為 object,其實(shí)需要從js 底層如何存儲(chǔ)變量類(lèi)型來(lái)說(shuō)起。雖然說(shuō),這是 JavaScript 設(shè)計(jì)的一個(gè) bug。
在 JavaScript 最初的實(shí)現(xiàn)中,JavaScript 中的值是由一個(gè)表示類(lèi)型的標(biāo)簽和實(shí)際數(shù)據(jù)值表示的。對(duì)象的類(lèi)型標(biāo)簽是 0。由于 null 代表的是空指針(大多數(shù)平臺(tái)下值為 0x00),因此,null 的類(lèi)型標(biāo)簽是 0,typeof null 也因此返回 "object"。曾有一個(gè) ECMAScript 的修復(fù)提案(通過(guò)選擇性加入的方式),但被拒絕了。該提案會(huì)導(dǎo)致 typeof null === 'null'。
js 在底層存儲(chǔ)變量的時(shí)候,會(huì)在變量的機(jī)器碼的低位1-3位存儲(chǔ)其類(lèi)型信息:
1:整數(shù) 110:布爾 100:字符串 010:浮點(diǎn)數(shù) 000:對(duì)象
但是,對(duì)于 undefined 和 null 來(lái)說(shuō),這兩個(gè)值的信息存儲(chǔ)是有點(diǎn)特殊的:
null:所有機(jī)器碼均為0undefined:用 ?2^30 整數(shù)來(lái)表示
所以在用 typeof 來(lái)判斷變量類(lèi)型的時(shí)候,我們需要注意,最好是用 typeof 來(lái)判斷基本數(shù)據(jù)類(lèi)型(包括symbol),避免對(duì) null 的判斷。
typeof只是咱在討論原型帶出的instanceof的附加討論區(qū)
instanceof
object instanceof constructor
instanceof 和 typeof 非常的類(lèi)似。instanceof 運(yùn)算符用來(lái)檢測(cè) constructor.prototype 是否存在于參數(shù) object 的原型鏈上。與 typeof 方法不同的是,instanceof 方法要求開(kāi)發(fā)者明確地確認(rèn)對(duì)象為某特定類(lèi)型。
基本用法
// 定義構(gòu)造函數(shù)
function C(){}
function D(){}
var o = new C();
o instanceof C; // true,因?yàn)?Object.getPrototypeOf(o) === C.prototype
o instanceof D; // false,因?yàn)?D.prototype 不在 o 的原型鏈上
o instanceof Object; // true,因?yàn)?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 指向了一個(gè)空對(duì)象,這個(gè)空對(duì)象不在 o 的原型鏈上.
D.prototype = new C(); // 繼承
var o3 = new D();
o3 instanceof D; // true
o3 instanceof C; // true 因?yàn)?C.prototype 現(xiàn)在在 o3 的原型鏈上
如上,是 instanceof 的基本用法,它可以判斷一個(gè)實(shí)例是否是其父類(lèi)型或者祖先類(lèi)型的實(shí)例。
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,而其他類(lèi) instanceof 自己卻又不等于 true 呢?如何解釋?zhuān)?/p>
要想從根本上了解 instanceof 的奧秘,需要從兩個(gè)方面著手:1,語(yǔ)言規(guī)范中是如何定義這個(gè)運(yùn)算符的。2,JavaScript 原型繼承機(jī)制。
原理淺析
經(jīng)過(guò)上述的分析,想必大家對(duì)這種經(jīng)典神圖已經(jīng)不那么陌生了吧,那咱就對(duì)著這張圖來(lái)聊聊 instanceof

這里,我直接將規(guī)范定義翻譯為 JavaScript 代碼如下:
function instance_of(L, R) {//L 表示左表達(dá)式,R 表示右表達(dá)式
var O = R.prototype;// 取 R 的顯示原型
L = L.__proto__;// 取 L 的隱式原型
while (true) {
if (L === null)
return false;
if (O === L)// 這里重點(diǎn):當(dāng) O 嚴(yán)格等于 L 時(shí),返回 true
return true;
L = L.__proto__;
}
}
所以如上原理,加上上文解釋的原型相關(guān)知識(shí),我們?cè)賮?lái)解析下為什么Object 和 Function instanceof 自己等于 true。
Object instanceof Object
// 為了方便表述,首先區(qū)分左側(cè)表達(dá)式和右側(cè)表達(dá)式
ObjectL = Object, ObjectR = Object;
// 下面根據(jù)規(guī)范逐步推演
O = ObjectR.prototype = Object.prototype
L = ObjectL.__proto__ = Function.prototype
// 第一次判斷
O != L
// 循環(huán)查找 L 是否還有 __proto__
L = Function.prototype.__proto__ = Object.prototype
// 第二次判斷
O == L
// 返回 true
Function instanceof Function
// 為了方便表述,首先區(qū)分左側(cè)表達(dá)式和右側(cè)表達(dá)式
FunctionL = Function, FunctionR = Function;
// 下面根據(jù)規(guī)范逐步推演
O = FunctionR.prototype = Function.prototype
L = FunctionL.__proto__ = Function.prototype
// 第一次判斷
O == L
// 返回 true
Foo instanceof Foo
// 為了方便表述,首先區(qū)分左側(cè)表達(dá)式和右側(cè)表達(dá)式
FooL = Foo, FooR = Foo;
// 下面根據(jù)規(guī)范逐步推演
O = FooR.prototype = Foo.prototype
L = FooL.__proto__ = Function.prototype
// 第一次判斷
O != L
// 循環(huán)再次查找 L 是否還有 __proto__
L = Function.prototype.__proto__ = Object.prototype
// 第二次判斷
O != L
// 再次循環(huán)查找 L 是否還有 __proto__
L = Object.prototype.__proto__ = null
// 第三次判斷
L == null
// 返回 false
ES5 中的繼承實(shí)現(xiàn)方式
在繼承實(shí)現(xiàn)上,工業(yè)聚大大在他的原型文章中,將原型繼承分為兩大類(lèi),顯式繼承和隱式繼承。感興趣的可以點(diǎn)擊文末參考鏈接查看。

但是本文還是希望能夠基于“通俗”的方式來(lái)講解幾種常見(jiàn)的繼承方式和優(yōu)缺點(diǎn)。大家可多多對(duì)比查看,其實(shí)原理都是一樣,名詞也只是所謂的代稱(chēng)而已。
關(guān)于繼承的文章,很多書(shū)本和博客中都有很詳細(xì)的講解。以下幾種繼承方式,均總結(jié)與《JavaScript 設(shè)計(jì)模式》一書(shū)。也是筆者三年前寫(xiě)的一篇文章了。
new 關(guān)鍵字
在講解繼承之前呢,我覺(jué)得 new 這個(gè)東西很有必要介紹下~
一個(gè)例子看下new 關(guān)鍵字都干了啥
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
從上面的例子我們可以看到:
訪(fǎng)問(wèn)到 Person構(gòu)造函數(shù)里的屬性訪(fǎng)問(wèn)到 Person.prototype中的屬性
new 手寫(xiě)版本一
function objectFactory() {
const obj = new Object(),//從Object.prototype上克隆一個(gè)對(duì)象
Constructor = [].shift.call(arguments);//取得外部傳入的構(gòu)造器
const F=function(){};
F.prototype= Constructor.prototype;
obj=new F();//指向正確的原型
Constructor.apply(obj, arguments);//借用外部傳入的構(gòu)造器給obj設(shè)置屬性
return obj;//返回 obj
};
用 new Object()的方式新建了一個(gè)對(duì)象 obj取出第一個(gè)參數(shù),就是我們要傳入的構(gòu)造函數(shù)。此外因?yàn)?shift 會(huì)修改原數(shù)組,所以 arguments會(huì)被去除第一個(gè)參數(shù)將 obj 的原型指向構(gòu)造函數(shù),這樣 obj 就可以訪(fǎng)問(wèn)到構(gòu)造函數(shù)原型中的屬性 使用 apply,改變構(gòu)造函數(shù)this的指向到新建的對(duì)象,這樣 obj 就可以訪(fǎng)問(wèn)到構(gòu)造函數(shù)中的屬性返回 obj
下面我們來(lái)測(cè)試一下:
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上克隆一個(gè)對(duì)象
Constructor = [].shift.call(arguments);//取得外部傳入的構(gòu)造器
console.log({Constructor})
const F=function(){};
F.prototype= Constructor.prototype;
obj=new F();//指向正確的原型
Constructor.apply(obj, arguments);//借用外部傳入的構(gòu)造器給obj設(shè)置屬性
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
注意上面我們沒(méi)有直接修改 obj 的__proto__隱式掛載。

new 手寫(xiě)版本二
考慮構(gòu)造函數(shù)又返回值的情況:
如果構(gòu)造函數(shù)返回一個(gè)對(duì)象,那么我們也返回這個(gè)對(duì)象 如上否則,就返回默認(rèn)值
function objectFactory() {
var obj = new Object(),//從Object.prototype上克隆一個(gè)對(duì)象
Constructor = [].shift.call(arguments);//取得外部傳入的構(gòu)造器
var F=function(){};
F.prototype= Constructor.prototype;
obj=new F();//指向正確的原型
var ret = Constructor.apply(obj, arguments);//借用外部傳入的構(gòu)造器給obj設(shè)置屬性
return typeof ret === 'object' ? ret : obj;//確保構(gòu)造器總是返回一個(gè)對(duì)象
};
關(guān)于 call、apply、bind、this 等用法和原理講解:【THE LAST TIME】this:call、apply、bind
類(lèi)式繼承
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的原理我們知道,第三個(gè)console如果這么寫(xiě)就返回true了console.log(SubClass.prototype instanceof SuperClass)
雖然實(shí)現(xiàn)起來(lái)清晰簡(jiǎn)潔,但是這種繼承方式有兩個(gè)缺點(diǎn):
由于子類(lèi)通過(guò)其原型prototype對(duì)父類(lèi)實(shí)例化,繼承了父類(lèi),所以說(shuō)父類(lèi)中如果共有屬性是引用類(lèi)型,就會(huì)在子類(lèi)中被所有的實(shí)例所共享,因此一個(gè)子類(lèi)的實(shí)例更改子類(lèi)原型從父類(lèi)構(gòu)造函數(shù)中繼承的共有屬性就會(huì)直接影響到其他的子類(lèi) 由于子類(lèi)實(shí)現(xiàn)的繼承是靠其原型prototype對(duì)父類(lèi)進(jìn)行實(shí)例化實(shí)現(xiàn)的,因此在創(chuàng)建父類(lèi)的時(shí)候,是無(wú)法向父類(lèi)傳遞參數(shù)的。因而在實(shí)例化父類(lèi)的時(shí)候也無(wú)法對(duì)父類(lèi)構(gòu)造函數(shù)內(nèi)的屬性進(jìn)行初始化
構(gòu)造函數(shù)繼承
function SuperClass(id) {
this.books = ['js','css'];
this.id = id;
}
SuperClass.prototype.showBooks = function() {
console.log(this.books);
}
function SubClass(id) {
//繼承父類(lèi)
SuperClass.call(this,id);
}
//創(chuàng)建第一個(gè)子類(lèi)實(shí)例
var instance1 = new SubClass(10);
//創(chuàng)建第二個(gè)子類(lèi)實(shí)例
var instance2 = new SubClass(11);
instance1.books.push('html');
console.log(instance1)
console.log(instance2)
instance1.showBooks();//TypeError
SuperClass.call(this,id)當(dāng)然就是構(gòu)造函數(shù)繼承的核心語(yǔ)句了.由于父類(lèi)中給this綁定屬性,因此子類(lèi)自然也就繼承父類(lèi)的共有屬性。由于這種類(lèi)型的繼承沒(méi)有涉及到原型prototype,所以父類(lèi)的原型方法自然不會(huì)被子類(lèi)繼承,而如果想被子類(lèi)繼承,就必須放到構(gòu)造函數(shù)中,這樣創(chuàng)建出來(lái)的每一個(gè)實(shí)例都會(huì)單獨(dú)的擁有一份而不能共用,這樣就違背了代碼復(fù)用的原則,所以綜合上述兩種,我們提出了組合式繼承方法
組合式繼承
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);
}
如上,我們就解決了之前說(shuō)到的一些問(wèn)題,但是是不是從代碼看,還是有些不爽呢?至少這個(gè)SuperClass的構(gòu)造函數(shù)執(zhí)行了兩遍就感覺(jué)非常的不妥.
原型式繼承
function inheritObject(o) {
//聲明一個(gè)過(guò)渡對(duì)象
function F() { }
//過(guò)渡對(duì)象的原型繼承父對(duì)象
F.prototype = o;
//返回過(guò)渡對(duì)象的實(shí)例,該對(duì)象的原型繼承了父對(duì)象
return new F();
}
原型式繼承大致的實(shí)現(xiàn)方式如上,是不是想到了我們new關(guān)鍵字模擬的實(shí)現(xiàn)?
其實(shí)這種方式和類(lèi)式繼承非常的相似,他只是對(duì)類(lèi)式繼承的一個(gè)封裝,其中的過(guò)渡對(duì)象就相當(dāng)于類(lèi)式繼承的子類(lèi),只不過(guò)在原型繼承中作為一個(gè)普通的過(guò)渡對(duì)象存在,目的是為了創(chuàng)建要返回的新的實(shí)例對(duì)象。
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);
如上代碼我們可以看出,原型式繼承和類(lèi)式繼承一個(gè)樣子,對(duì)于引用類(lèi)型的變量,還是存在子類(lèi)實(shí)例共享的情況。
所以,我們還有下面的寄生式繼
寄生式繼承
var book = {
name:'js book',
likeBook:['html book','css book']
}
function createBook(obj) {
//通過(guò)原型方式創(chuàng)建新的對(duì)象
var o = new inheritObject(obj);
// 拓展新對(duì)象
o.getName = function(name) {
console.log(name)
}
// 返回拓展后的新對(duì)象
return o;
}
其實(shí)寄生式繼承就是對(duì)原型繼承的拓展,一個(gè)二次封裝的過(guò)程,這樣新創(chuàng)建的對(duì)象不僅僅有父類(lèi)的屬性和方法,還新增了別的屬性和方法。
寄生組合式繼承
回到之前的組合式繼承,那時(shí)候我們將類(lèi)式繼承和構(gòu)造函數(shù)繼承組合使用,但是存在的問(wèn)題就是子類(lèi)不是父類(lèi)的實(shí)例,而子類(lèi)的原型是父類(lèi)的實(shí)例,所以才有了寄生組合式繼承
而寄生組合式繼承是寄生式繼承和構(gòu)造函數(shù)繼承的組合。但是這里寄生式繼承有些特殊,這里他處理不是對(duì)象,而是類(lèi)的原型。
function inheritObject(o) {
//聲明一個(gè)過(guò)渡對(duì)象
function F() { }
//過(guò)渡對(duì)象的原型繼承父對(duì)象
F.prototype = o;
//返回過(guò)渡對(duì)象的實(shí)例,該對(duì)象的原型繼承了父對(duì)象
return new F();
}
function inheritPrototype(subClass,superClass) {
// 復(fù)制一份父類(lèi)的原型副本到變量中
var p = inheritObject(superClass.prototype);
// 修正因?yàn)橹貙?xiě)子類(lèi)的原型導(dǎo)致子類(lèi)的constructor屬性被修改
p.constructor = subClass;
// 設(shè)置子類(lèi)原型
subClass.prototype = p;
}
組合式繼承中,通過(guò)構(gòu)造函數(shù)繼承的屬性和方法都是沒(méi)有問(wèn)題的,所以這里我們主要探究通過(guò)寄生式繼承重新繼承父類(lèi)的原型。
我們需要繼承的僅僅是父類(lèi)的原型,不用去調(diào)用父類(lèi)的構(gòu)造函數(shù)。換句話(huà)說(shuō),在構(gòu)造函數(shù)繼承中,我們已經(jīng)調(diào)用了父類(lèi)的構(gòu)造函數(shù)。因此我們需要的就是父類(lèi)的原型對(duì)象的一個(gè)副本,而這個(gè)副本我們可以通過(guò)原型繼承拿到,但是這么直接賦值給子類(lèi)會(huì)有問(wèn)題,因?yàn)閷?duì)父類(lèi)原型對(duì)象復(fù)制得到的復(fù)制對(duì)象p中的constructor屬性指向的不是subClass子類(lèi)對(duì)象,因此在寄生式繼承中要對(duì)復(fù)制對(duì)象p做一次增強(qiáng),修復(fù)起constructor屬性指向性不正確的問(wèn)題,最后將得到的復(fù)制對(duì)象p賦值給子類(lèi)原型,這樣子類(lèi)的原型就繼承了父類(lèi)的原型并且沒(méi)有執(zhí)行父類(lèi)的構(gòu)造函數(shù)。
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();

這種方式繼承其實(shí)如上圖所示,其中最大的改變就是子類(lèi)原型中的處理,被賦予父類(lèi)原型中的一個(gè)引用,這是一個(gè)對(duì)象,因此有一點(diǎn)你需要注意,就是子類(lèi)在想添加原型方法必須通過(guò)prototype.來(lái)添加,否則直接賦予對(duì)象就會(huì)覆蓋從父類(lèi)原型繼承的對(duì)象了.
ES6 類(lèi)的實(shí)現(xiàn)原理
關(guān)于 ES6 中的 class 的一些基本用法和介紹,限于篇幅,本文就不做介紹了。該章節(jié),我們主要通過(guò) babel的 REPL來(lái)查看分析 es6 中各個(gè)語(yǔ)法糖包括繼承的一些實(shí)現(xiàn)方式。
基礎(chǔ)類(lèi)

我們就會(huì)按照這個(gè)類(lèi),來(lái)回摩擦。然后再來(lái)分析編譯后的代碼。
"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就是來(lái)判斷實(shí)例關(guān)系的的。上述代碼就比較簡(jiǎn)單了,_classCallCheck的作用就是檢查 Person 這個(gè)類(lèi),是否是通過(guò)new 關(guān)鍵字調(diào)用的。畢竟被編譯成 ES5 以后,function 可以直接調(diào)用,但是如果直接調(diào)用的話(huà),this 就指向 window 對(duì)象,就會(huì)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", '實(shí)例屬性');
this.name = name;
};
_defineProperty(Person, "jingtai", ' 靜態(tài)屬性');
其實(shí)就是講屬性賦值給誰(shuí)的問(wèn)題。如果是實(shí)例屬性,直接賦值到 this 上,如果是靜態(tài)屬性,則賦值類(lèi)上。_defineProperty也就是來(lái)判斷下是否屬性名重復(fù)而已。
添加方法

"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 =
/*#__PURE__*/
function () {
function Person(name) {
_classCallCheck(this, Person);
_defineProperty(this, "shili", '實(shí)例屬性');
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", ' 靜態(tài)屬性');
看起來(lái)代碼量還不少,其實(shí)就是一個(gè)_createClass函數(shù)和_defineProperties函數(shù)而已。
首先看_createClass這個(gè)函數(shù)的三個(gè)參數(shù),第一個(gè)是構(gòu)造函數(shù),第二個(gè)是需要添加到原型上的函數(shù)數(shù)組,第三個(gè)是添加到類(lèi)本身的函數(shù)數(shù)組。其實(shí)這個(gè)函數(shù)的作用非常的簡(jiǎn)單。就是加強(qiáng)一下構(gòu)造函數(shù),所謂的加強(qiáng)構(gòu)造函數(shù)就是給構(gòu)造函數(shù)或者其原型上添加一些函數(shù)。
而_defineProperties就是多個(gè)_defineProperty(感覺(jué)是廢話(huà),不過(guò)的確如此)。默認(rèn) enumerable 為 false,configurable 為 true。
其實(shí)如上就是 es6 class 的實(shí)現(xiàn)原理。
extend 關(guān)鍵字

"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 =
/*#__PURE__*/
function (_Parent) {
_inherits(Child, _Parent);
function Child(name, age) {
var _this;
_classCallCheck(this, Child);
_this = _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this, name)); // 調(diào)用父類(lèi)的 constructor(name)
_this.age = age;
return _this;
}
return Child;
}(Parent);
var child1 = new Child('全棧前端精選', '0.3');
console.log(child1);
刪去類(lèi)相關(guān)的代碼生成,剩下的就是繼承的語(yǔ)法糖剖析了。其中super 關(guān)鍵字表示父類(lèi)的構(gòu)造函數(shù),相當(dāng)于 ES5 的 Parent.call(this),然后再根據(jù)我們上文說(shuō)到的繼承方式,有沒(méi)有感覺(jué)該集成的實(shí)現(xiàn)跟我們說(shuō)的寄生組合式繼承非常的相似呢?
在 ES6 class 中,子類(lèi)必須在 constructor 方法中調(diào)用 super 方法,否則新建實(shí)例時(shí)會(huì)報(bào)錯(cuò)。這是因?yàn)?strong>子類(lèi)沒(méi)有自己的 this 對(duì)象,而是繼承父類(lèi)的 this 對(duì)象,然后對(duì)其進(jìn)行加工。如果不調(diào)用 super 方法,子類(lèi)就得不到 this 對(duì)象。
也正是因?yàn)檫@個(gè)原因,在子類(lèi)的構(gòu)造函數(shù)中,只有調(diào)用 super 之后,才可以使用 this 關(guān)鍵字,否則會(huì)報(bào)錯(cuò)。
關(guān)于 ES6 中原型鏈?zhǔn)疽鈭D可以參照如下示意圖:

關(guān)于ES6 中的 extend 關(guān)鍵字,上述代碼我們完全可以根據(jù)執(zhí)行來(lái)看。其實(shí)重點(diǎn)代碼無(wú)非就兩行:
_inherits(Child, _Parent);
_this = _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this, name));
我們分別來(lái)分析下具體的實(shí)現(xiàn):
_inherits
代碼比較簡(jiǎn)單,都是上文提到的內(nèi)容,就是建立 Child 和 Parent 的原型鏈關(guān)系。代碼解釋已備注在代碼內(nèi)
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {//subClass 類(lèi)型判斷
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {//Object.create 第二個(gè)參數(shù)是給subClass.prototype添加了 constructor 屬性
value: subClass,
writable: true,
configurable: true//注意這里enumerable沒(méi)有指名,默認(rèn)是 false,也就是說(shuō)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));
根據(jù)上圖我們整理的 es6 原型圖可知:
Child.prototype === Parent
所以上面的代碼我們可以翻譯為:
_this = _possibleConstructorReturn(this, Parent.call(this, name));
然后我們?cè)僖粚右粚訐茉创a的實(shí)現(xiàn)
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其實(shí)就是 Child 的 IIFE返回的 function new 調(diào)用的 this,打印出來(lái)結(jié)果如下:

這里可能對(duì)Parent.call(this,name)有些疑惑,沒(méi)關(guān)系,我們可以在 Chrome 下調(diào)試下。


可以看到,當(dāng)我們 Parent 的構(gòu)造函數(shù)這么寫(xiě)
class Parent {
constructor(name) {
this.name = name;
}
}
那么最終,傳遞給_possibleConstructorReturn函數(shù)的第二參數(shù) call就是一個(gè) undefined。所以在_possibleConstructorReturn函數(shù)里面會(huì)對(duì) call進(jìn)行判斷,返回正確的 this 指向:Child。
所以整體代碼的目的就是根據(jù) Parent 構(gòu)造函數(shù)的返回值類(lèi)型確定子類(lèi)構(gòu)造函數(shù) this 的初始值 _this。
最后
【THE LAST TIME】系列關(guān)于 JavaScript 基礎(chǔ)的文章目前更新三篇,我們最后再來(lái)一道經(jīng)典的面試題吧!
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);
}
//請(qǐng)寫(xiě)出以下輸出結(jié)果:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
老鐵,評(píng)論區(qū)留下你的思考吧~
參考文獻(xiàn)
深入理解 JavaScript 原型 幫你徹底搞懂JS中的prototype、__proto__與constructor JavaScript instanceof 運(yùn)算符深入剖析 JavaScript深入之創(chuàng)建對(duì)象的多種方式以及優(yōu)缺點(diǎn) ES6 系列之 Babel 是如何編譯 Class 的(上) ES6—類(lèi)的實(shí)現(xiàn)原理 es6類(lèi)和繼承的實(shí)現(xiàn)原理 JavaScript深入之new的模擬實(shí)現(xiàn)
完
?? 看完三件事
如果你覺(jué)得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:
點(diǎn)個(gè)【在看】,或者分享轉(zhuǎn)發(fā),讓更多的人也能看到這篇內(nèi)容
關(guān)注公眾號(hào)【全棧前端精選】,不定期分享原創(chuàng)&精品技術(shù)文章。
公眾號(hào)內(nèi)回復(fù):【 1 】。加入全棧前端精選公眾號(hào)交流群。
覺(jué)得文章不錯(cuò)可以分享到朋友圈讓更多的小伙伴看到哦~
