前言
寫這篇筆記的初衷秘狞,是想進(jìn)一步了解ES6的特性extends
是如何實現(xiàn)了繼承角撞,查看源碼后發(fā)現(xiàn)核心的一段不能理解
// 賦值原型
subClass.prototype = Object.create(superClass && superClass.prototype)
// 這一步是作甚锦秒?根據(jù)組合繼承的邏輯听盖,完全沒有必要這一步杨拐?
if (superClass)
Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
由于不太清楚__proto__
與原型prototype
的含義棕所,以及Object
糊肠,Function
的關(guān)系辨宠,便做進(jìn)一步深究。
題外話:我并不覺得深究一件事有何不值得货裹,就像是“走火入魔”般嗤形,何況這件事是很多人都曾做過的。我相信我會喜歡上深究一個問題這樣一個過程而不是結(jié)果泪酱。
概述
這里我想要理清楚的問題是:
1派殷、Object
,Function
墓阀,prototype
毡惜,__proto__
究竟是怎樣的關(guān)系,怎么得來斯撮?
2经伙、ES5合理繼承的方式(即前篇文章所提到的組合繼承)與ES6extends
有何異同?ES6源碼剖析
一、Object帕膜,F(xiàn)unction枣氧,prototype,__proto__
之間的關(guān)系
1.理解對象
我們常說垮刹,“js是面向?qū)ο蟮拇锿蹋驗樗部梢該碛凶约旱膶傩院头椒?..”,其實說了跟沒說沒啥區(qū)別荒典,反正我還是沒理解酪劫。
我覺得最能解釋它的是:Object.prototype
是一切對象和函數(shù)的根源,一張圖來證明我的觀點:
為什么這么說寺董?我們會發(fā)現(xiàn)覆糟,每當(dāng)我們在控制臺打印一個對象的時候,都能順著原型鏈找到圖中所有的內(nèi)容遮咖。并且:
// 說明Object.prototype不是任何一個構(gòu)造函數(shù)的實例
Object.prototype.__proto__ === null
2.誰構(gòu)造了誰
js原生內(nèi)置了部分構(gòu)造函數(shù)滩字,其中就包含了Object和Function
Object、Function御吞、Boolean麦箍、Number、String陶珠、Array内列、Date、RegExp背率、Error
我們知道话瞧,當(dāng)我們想要一個子類擁有父類的屬性和方法時,會先創(chuàng)建一個父類Parent
寝姿,其實這個構(gòu)造函數(shù)Parent
形式等價于內(nèi)置的構(gòu)造函數(shù)
那Parent
和這些內(nèi)置構(gòu)造函數(shù)繼承的誰呢交排?答案是Function
本身,換句話說所有的構(gòu)造函數(shù)都是Function
的實例饵筑,如圖所示:
![HWC41$DF0P)]X66P7Y{M%2X.png](http://upload-images.jianshu.io/upload_images/3637499-448654f044fe6fc6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
依照我們前面的說法埃篓,實例的__proto__
始終指向構(gòu)造函數(shù)的prototype
沒錯。
那Function
又是誰構(gòu)造而來根资?我發(fā)現(xiàn)我真是蠢到了極點架专,圖中不是答案么,Function
是自己的實例玄帕,也就是Function
由自己構(gòu)造的部脚,聽起來像是科幻小說。
既然這樣裤纹,那構(gòu)造Function
的時候委刘,它的原型從哪繼承而來?這里我不得不引用別人的原文:
Function.prototype是Object的實例對象
雖然我們從控制臺印證了這個觀點
![G]HSRKI{}M8I~8E5JK0JF.png](http://upload-images.jianshu.io/upload_images/3637499-fc8dfd11d19fd018.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
但是這里我還是得先弄清楚prototype
和__proto__
。最終锡移,在別人的文章中找到了似乎可以印證的觀點
prototype是函數(shù)的一個屬性(每個函數(shù)都有一個prototype屬性)呕童,指向一個對象
我們先接受這個觀點,那么這個對象指向的誰呢淆珊?創(chuàng)建函數(shù)的方式有3種
- 通過Function構(gòu)造函數(shù)
- 字面量創(chuàng)建
- 直接聲明
![{A3}PS6C3HTGRJ_0YJ5]4W.png](http://upload-images.jianshu.io/upload_images/3637499-8d44d6ab3065fb68.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 從圖中我們可以看到夺饲,所有function實例的
prototype都是指向
Object.prototype`
// 這里的a其實形式上等價于Function
a.prototype.__proto__ === Function.prototype.__proto__ === Object.prototype
__proto__
是一個對象擁有的內(nèi)置屬性,指向于它所對應(yīng)的原型對象施符,原型鏈正是基于__proto__
才得以形成(note:不是基于函數(shù)對象的屬性prototype)
3.小總結(jié)
我們知道了__proto__
和prototype
的區(qū)別钞支,那我們自然而然的得出:
// 所有構(gòu)造函數(shù)都是Function的實例
Object.__proto__ === Function.prototype
Function.__proto__ === Function.prototype
// 所有Function都有一個prototype指向Object.prototype
// 也就是說,F(xiàn)unction.prototype都是Object.prototype的實例
// 這一點我不是很確定操刀,但從控制臺看到確實如此
Function.prototype.__proto__ === Object.prototype
// Object.prototype到達(dá)了根源,不指向任何誰婴洼,原型鏈到此就結(jié)束了
Object.prototype.__proto__ === null
因此骨坑,關(guān)于誰構(gòu)造了誰這個問題,答案是:
所以柬采,是先有的Object.prototype,再有的Function.prototype欢唾,再有的Function和Object函數(shù)對象
最后再帶上一張圖來加深理解,此圖來源Javascript中Function,Object,Prototypes,proto等概念詳解
此小節(jié)參考
js 原型的問題 Object 和 Function 到底是什么關(guān)系粉捻?
Js中Prototype礁遣、proto、Constructor肩刃、Object祟霍、Function關(guān)系介紹
4.疑問
當(dāng)我們通過3種方式創(chuàng)建函數(shù)的時候,它們的構(gòu)造函數(shù)是一樣的嗎盈包?
![LR3J6$(]NL7@T~M7@5A8Q@C.png](http://upload-images.jianshu.io/upload_images/3637499-4b45ba659b03c618.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
由上圖可以發(fā)現(xiàn)沸呐,通過3種方式創(chuàng)建函數(shù),它們實質(zhì)是一樣的呢燥,都是調(diào)用Function構(gòu)造函數(shù)來創(chuàng)建崭添,只不過創(chuàng)建的時候匿名不匿名的問題
a.constructor === b.constructor === c.constructor === Function
包括通過字面量創(chuàng)建的對象和通過構(gòu)造函數(shù)創(chuàng)建對象他們的區(qū)別,只不過是多了一層中間構(gòu)造函數(shù)而已
// 1.通過構(gòu)造函數(shù)創(chuàng)建對象
function Extra(name) {
this.name = name
}
var instance = new Extra('hehe')
// 2.通過字面量創(chuàng)建
var instance2 = {
name: 'hehe2'
}
// instance與instance2的區(qū)別
instance.__proto__ === Extra.prototype
Extra.prototype.__proto__ === Object.prototype
// 而
instance2.__proto__ === Object.prototype
// 可以發(fā)現(xiàn)instance就多了一層構(gòu)造函數(shù)Extra的原型叛氨,我們還可以知道呼渣,原型鏈就是基于__proto__才得以依次往上查找
區(qū)別我想肯定不止這些,因為我還不知道創(chuàng)建函數(shù)是怎樣的一個過程寞埠,包括
new
的時候屁置,具體發(fā)生了哪些細(xì)節(jié),大致我只知道仁连,new
一個構(gòu)造函數(shù)的時候缰犁,在其內(nèi)部完成了原型鏈的連接(即繼承至Object.prototype
,可能是通過this = {}
實現(xiàn)的),并且賦值了this的指向帅容。而通過字面量創(chuàng)建的時候是沒有這些過程的颇象。
二、ES5合理繼承的方式(即組合繼承)與ES6 extends的異同
阮一峰老師的教程里有說到
ES5 的繼承并徘,實質(zhì)是先創(chuàng)造子類的實例對象this遣钳,然后再將父類的方法添加到this上面(Parent.apply(this))。
ES6 的繼承機(jī)制完全不同麦乞,實質(zhì)是先創(chuàng)造父類的實例對象this(所以必須先調(diào)用super方法)蕴茴,然后再用子類的構(gòu)造函數(shù)修改this。
也就是說ES6中子類是沒有this
的姐直,必須通過super得到父類的實例對象this倦淀,這是結(jié)果,那么實現(xiàn)呢声畏?
// 簡單的extends繼承
class Parent{
// static屬性和方法
static sex = 'man'
static getSex() {
console.log(Parent.sex)
}
// 構(gòu)造函數(shù)
constructor(name) {
this.name = name
}
// 非靜態(tài)方法
say() {
console.log(this.name)
}
}
class Child extends Parent {
static sex = 'women'
constructor(name, age) {
super(name)
this.age = age
}
say() {
console.log(this.name, this.age)
}
}
編譯后查看源碼
會發(fā)現(xiàn)es6 class 繼承實現(xiàn)通過這4個方法:_createClass
撞叽,_possibleConstructorReturn
, _inherits
, _classCallCheck
插龄,那么extends是如何繼承愿棋,包括super是如何獲取this的,我們來一步步解析均牢。
1.編譯后的Parent
var Parent = function () {
// 函數(shù)內(nèi)部聲明一個同名的構(gòu)造函數(shù)
function Parent(name) {
// 檢查當(dāng)前的this是否是構(gòu)造函數(shù)的實例糠雨,也就是說必須通過new的方式調(diào)用構(gòu)造函數(shù)
// 而不能是像調(diào)用函數(shù)一樣直接調(diào)用,因為這樣是不會生成實例的this
_classCallCheck(this, Parent);
// 將構(gòu)造函數(shù)的屬性賦值給當(dāng)前實例的this
this.name = name;
}
// 將Parent中的靜態(tài)方法直接賦值Parent構(gòu)造函數(shù)
_createClass(Parent, null, [{
key: 'getSex',
value: function getSex() {
console.log(Parent.sex);
}
}]);
// 將Parent中的非靜態(tài)方法賦值Parent的原型徘跪,也就是Parent.prototype
_createClass(Parent, [{
key: 'say',
value: function say() {
console.log(this.name);
}
}]);
return Parent;
}();
Parent
做了2件事情甘邀,也可以說所有的通過Class
關(guān)鍵字聲明的函數(shù)做了2件事
- 在函數(shù)里面聲明創(chuàng)建了一個同名的構(gòu)造函數(shù),將
constructor
以外的static
聲明的方法和非static聲明的方法分別掛載到構(gòu)造函數(shù)本身和構(gòu)造函數(shù)的原型上 - 該函數(shù)會返回這個同名的構(gòu)造函數(shù)垮庐,這里是采用寄生模式創(chuàng)建的構(gòu)造函數(shù)鹃答。這里還做了校驗,必須通過new來調(diào)用突硝,否則拋出異常
2.constructor
以外的方法是如何掛載的
// 用該方法實現(xiàn)
var _createClass = function () {
// 重寫了es5的defineProperties方法测摔,至于為什么會重寫,后面解釋
// 該方法就是遍歷props解恰,然后利用defineProperty锋八,依次給target定義屬性
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
// 是否可枚舉,默認(rèn)為false 护盈?也就是說把所有的方法都置為不可枚舉
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor)
descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
// 返回一個函數(shù)挟纱,如果是static方法就定義在Constructor上,否則就定義在Constructor.prototype上
// 我們可以對應(yīng)到Parent里調(diào)用_createClass 時腐宋,對于靜態(tài)和非靜態(tài)方法的傳參
return function (Constructor, protoProps, staticProps) {
if (protoProps)
defineProperties(Constructor.prototype, protoProps);
if (staticProps)
defineProperties(Constructor, staticProps);
return Constructor;
};
}();
我們可以看到紊服,ES6把所有定義在構(gòu)造函數(shù)或原型中的方法都定義為不可枚舉檀轨,而屬性是通過默認(rèn)賦值可枚舉的。為什么欺嗤?誰能解釋下参萄。。煎饼。讹挎。
3.插播一條小廣告,弄清楚數(shù)據(jù)屬性
數(shù)據(jù)屬性有4個(這里只是簡要的帶過)
-
value
屬性的值 -
enumerable
是否可枚舉 -
configurable
是否可修改 -
writable
是否可寫
我們在定義對象或者賦值對象屬性的時候吆玖,通常是不知道這些數(shù)據(jù)屬性的筒溃,因為默認(rèn)情況下,都是true
但是在有些時候沾乘,我們是不希望屬性是可枚舉的怜奖,就像前一章說過的組合繼承,手動賦值constructor
// 此時constructor也是可枚舉的
Child.prototype.constructor = Child
另外翅阵,通過Object.defineProperty
定義屬性時歪玲,如果不指定數(shù)據(jù)屬性,默認(rèn)情況下都為false
根據(jù)這些解釋怎顾,就可以理解前文中_createClass
為什么要重寫defineProperties
方法了。
4.編譯后的Child
同理漱贱,Child與Parent一樣槐雾,都會有相同的2個步驟(前面說的2件事情),不同的是
var Child = function (_Parent) {
function Child(name, age) {
// ...
// 2.這里我想就是阮一峰老師文章里有說的
// 拿到Parent實例的this幅狮,封裝成Child子類的this
var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name));
// ...
}
// 1.這一步是關(guān)鍵的繼承
_inherits(Child, _Parent);
// ...
}(Parent);
我們看到Child多做了2件事情募强,先說第1件事:
_inherits
繼承
_inherits
也做了2件事
- 就是我們組合繼承中提到的,利用
Object.create
將子類的原型指向父類的原型 - 將子類的
__proto__
指向父類構(gòu)造函數(shù)崇摄,也就是說擎值,認(rèn)為子類是父類構(gòu)造而來。
這里有點繞逐抑,其實就是這么個意思:你還記得組合繼承中鸠儿,在Child
的構(gòu)造函數(shù)里call
了Parent
一下么,將Parent
上的屬性都復(fù)制一份到Child
的this中厕氨。那么在這里进每,ES6并不知道要call誰啊,所以只好將父類的構(gòu)造函數(shù)指給子類的__proto__
命斧,這樣后面就只需要Child.__proto__.call(this)
了田晚。
function _inherits(subClass, superClass) {
// 校驗代碼...
// 利用 Object.create創(chuàng)建實例對象,并將實例賦值給subClass.prototype
// 并手動賦值constructor
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
// 這一步就是上面說的第2件事国葬,將superClass構(gòu)造函數(shù)賦值給subClass.__proto__贤徒,方便后面調(diào)用
// 因為我們知道芹壕,所有的函數(shù)都是由Function構(gòu)造而來
// 也就是說如果這里不賦值,subClass.__proto__ === Function.prototype
if (superClass)
Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
Child做的第2件事情
_possibleConstructorReturn
調(diào)用父類構(gòu)造函數(shù)
其實這里的表述有誤接奈,調(diào)用父類構(gòu)造函數(shù)并不是_possibleConstructorReturn
做的踢涌,_possibleConstructorReturn
只是做了一個簡單的校驗。
// 調(diào)用父類構(gòu)造函數(shù)
(Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name)
看到這一句就證明我前面的理解是對的鲫趁,這一句在es5組合繼承中就是
Parent.call(this, name)
只不過它不知道Parent
是誰斯嚎,所以就先把Parent
賦值給了Child.__proto__
。由于ES6中都是通過寄生模式來創(chuàng)建構(gòu)造函數(shù)挨厚,這里call之后堡僻,返回的是Parent
的實例,與組合繼承的call并不一致疫剃。
5.總結(jié)
我們再回過頭來看ES5與ES6繼承的區(qū)別钉疫,其實ES5與ES6的繼承沒有太大的區(qū)別,其原理都是采用了組合繼承巢价,核心唯一不同的就是這個this
值的問題牲阁,另外就是對定義靜態(tài)方法做了封裝(staitc)。
es5的繼承壤躲,是我們手動寫父類城菊,子類手動call父類碉克。
但是es6中的繼承是抽象出來的語法糖凌唬,并不知道你這里哪個是父類哪個是子類,所以它得通過一個巧妙的方法來知道這個是父類這個是子類漏麦。Child.__proto__ === Parent
或者調(diào)用Object.setPrototypeOf()
客税。
之所以this
不一樣,是因為它們創(chuàng)建構(gòu)造函數(shù)的機(jī)制不一樣
- es5是直接聲明撕贞,那樣this就直接被初始化了更耻,所以只能在子類里通過call來“豐富”它的this
- es6則是通過寄生模式(非工廠模式),返回的一個新的構(gòu)造函數(shù)捏膨,當(dāng)你call的時候秧均,相當(dāng)于new了這個新的構(gòu)造函數(shù),此時父類的構(gòu)造函數(shù)看起來就是個閉包号涯,因為他還要返回new后的實例對象熬北。所以在子類中是直接就拿到了父類的實例對象,那么就將this指向了他诚隙,再賦值自己的屬性讶隐。
后話
這是自己第一次系統(tǒng)的去理清其中的關(guān)系,并不是很熟練的掌握了個中的原理久又,有誤之處還望指出巫延!
相關(guān)