寫在前面
之所以想寫這個帐姻,是想為以后學習 react
做個鋪墊稠集,每一個看似理所當然的結(jié)果,實際上推敲過程很耐人尋味饥瓷,就從 ES6
類的實例化和繼承開始剥纷,但是 ES6
的實現(xiàn)是在 ES5
的基礎上,所以需要對 ES5
中關于 構(gòu)造函數(shù)呢铆、繼承這些梳理清晰晦鞋。
關于 new 關鍵字
- 無論
ES6
還是ES5
中對于自定義對象的實現(xiàn)都不開new
這個關鍵詞,所以需要搞清楚它在構(gòu)造函數(shù)實例化過程中的作用棺克,因為透過它你能看清楚構(gòu)造函數(shù)內(nèi)部this
指向的問題. - 有時候文字解釋會讓讀者對一個概念產(chǎn)生各種想象悠垛,所以我比較偏好嘗試用代碼來解釋自己難以理解的地方,代碼如下:
// ES5 中構(gòu)造函數(shù)創(chuàng)建實例過程
function Person (name, age) {
this.name = name
this.age = age
}
// 返回一個具有 name = ww, age = 18 的 person 對象
var person = new Person('ww', 19)
// 那么 new 關鍵字做了哪些工作
// 下面是對使用 new 關鍵字配合構(gòu)造函數(shù)創(chuàng)建對象的過程模擬
var person = (function() {
function Person (name, age) {
this.name = name
this.age = age
}
// 創(chuàng)建一個隱式空對象
var object = {}
// 修正隱式對象的原型
setPrototypeOf(object, Person.prototype)
// 執(zhí)行構(gòu)造函數(shù)娜谊,為空對象賦值
Person.apply(object, arguments)
// 返回隱式對象
return object
})('www', 90)
function setPrototypeOf(object, proto) {
Object.setPrototypeOf ?
Object.setPrototypeOf(object, proto)
:
object.__proto__ = proto
}
// 返回的 person 我們通常會稱之為 Person 的一個實例鼎文。
- 上述就是對
new
在構(gòu)造函數(shù)實例化中的作用的描述,這里需要注意的一點是因俐,在ES5
中拇惋,如果沒有使用new
關鍵字,那么Person
只是作為window
的一個普通方法調(diào)用抹剩,所以也不會報錯撑帖,但是在ES6
的 類 的實例化中必須使用new
,否則會報錯澳眷,在解讀ES6
類的實例化會解釋報錯的原因胡嘿。
ES5中繼承的實現(xiàn)
- 在
ES5
中實現(xiàn)繼承的方式比較多,W3C
標準推薦的做法是有三種ECMAScript 繼承機制實現(xiàn)钳踊,下面直接上代碼來闡述ES5
實現(xiàn)繼承的過程衷敌,以及優(yōu)缺點
- 第一種是通過傳統(tǒng)的原型鏈繼承方式,比較好理解:自身若有被讀取的屬性或者方法的時候拓瞪,自取所需缴罗,如果沒有,就沿著原型鏈一層一層往上找祭埂,直到找到或者找不到返回
undefined
或者報錯
// 01-定義父類
function Human(home) {
this.home = home
}
// 02-為父類實例添加方法
Human.prototype.say = function () {
return `I'm a ${this.sex}, I come from ${this.home}`
}
// 03-定義一個子類
function Person(sex) {
this.sex = sex
}
// 04-子類的原型對象指向父類實例面氓,實現(xiàn)繼承
Person.prototype = new Human('Earth')
var man = new Person('man')
// 調(diào)用實例身上的方法,由于沒有,沿著原型鏈往上找舌界,
// 找到的是父類原型對象上面的 say() 方法
man.say() // 返回的是 I'm a man, I come from Earth
// 05-子類再實例化一個對象
var female = new Person('female')
// 06-修改原型鏈上的 home 屬性
Object.getPrototypeOf(female).home = 'Mars'
// 07-調(diào)用子類兩個實例的 say() 方法
man.say() // 返回結(jié)果:I'm a man, I come from Mars
female.say() // 返回結(jié)果:I'm a female, I come from Mars
// 別人本來是來自地球掘譬,你隨便動動手指,讓人家誕生在火星了呻拌,多少有點說不過去
// 基于這樣的特點葱轩,只要有任何一個子類實例修改了原型對象上的屬性或者父類實例自身修改了屬性
// 將會影響所有繼承它的子類,這個不是我們愿意看到的
// 所以就有了第二種方式:call 或者 apply 實現(xiàn)繼承
- 通過
call
或者apply
實現(xiàn)繼承和 對象冒充 繼承很相似藐握,但是有所不同靴拱,根本原因是this 總是指向函數(shù)運行時所在的那個對象
,下面是call
方法實現(xiàn)繼承過程
// 01-定義父類
function Human(home) {
this.home = home
}
// 02-為父類實例添加方法
Human.prototype.say = function () {
return `I'm a ${this.sex}, I come from ${this.home}`
}
// 03-定義一個子類
function Person(sex, home) {
// 04-實現(xiàn)借用父類創(chuàng)建子類自身的屬性趾娃,這是最重要的一點
// 如果你理解了 new 的作用缭嫡,就知道此刻 this 指向是誰了
Human.call(this, home)
this.sex = sex
}
// 05-實例化一個具有 sex=man 和 home=earth 特征的 子類實例
var man = new Person('man', 'Earth')
// 06-再實例化一個具有 sex=female 和 home=Mars 特征的 子類實例
var female = new Person('female', 'Mars')
// 07-無論任意子類實例修改 sex 和 home 屬性,或者 父類實例修改 home 屬性
// 都不會影響到其他的子類實例抬闷,因為屬性此刻私有化了
// 08-但是會發(fā)現(xiàn)調(diào)用子類兩個實例的 say() 方法妇蛀,會報錯,因為原型鏈上沒有這個方法
// 扔出錯誤:man.say is not a function
man.say()
female.say()
// call 或者 apply 方法實現(xiàn)繼承的最大優(yōu)勢就是能夠?qū)崿F(xiàn)屬性私有化笤成,但是劣勢就是沒有辦法繼承
// 父類原型對象上面的方法评架,所以為了解決原型鏈繼承和call方法繼承的缺點,將兩者的優(yōu)點糅合在一起
// 即混合繼承炕泳,能夠?qū)崿F(xiàn)完美的繼承
- 混合繼承纵诞,即通過
call
或者apply
實現(xiàn)屬性繼承,原型鏈實現(xiàn)方法繼承
// 01-定義父類
function Human(home) {
this.home = home
}
// 02-為父類實例添加方法
Human.prototype.say = function () {
return `I'm a ${this.sex}, I come from ${this.home}`
}
// 03-定義一個子類
function Person(sex, home) {
// 04-使用 call 繼承父類的屬性
Human.call(this, home)
this.sex = sex
}
// 05-使用原型鏈培遵,繼承父類的方法
Person.prototype = new Human('sun')
// 06-子類實例化一個對象
var man = new Person('man', 'Earth')
// 07-子類再實例化一個對象
var female = new Person('female', 'Mars')
// 08-修改原型鏈上的 home 屬性
Object.getPrototypeOf(female).home = 'Heaven'
// 09-調(diào)用子類兩個實例的 say() 方法浙芙,返回的 home 都是當前實例自身的 home 屬性
// 而不是原型鏈上的 home ,因為當前實例自身有該屬性籽腕,就不再往原型鏈上找了
// 所以通過 call 和 原型鏈能夠?qū)崿F(xiàn)完美繼承
man.say() // 返回結(jié)果:I'm a man, I come from Earth
female.say() // 返回結(jié)果:I'm a female, I come from Mars
ES6類實例化中的new
- 之所以會說這么多
ES5
的繼承嗡呼,是因為ES6
的繼承實現(xiàn)都是基于前者,可以看做是前者的語法糖皇耗,只有在理解基礎的前提下南窗,才能談進階 - 關于
ES6
中class
的一些基礎知識就不提了,現(xiàn)在來說說class
聲明的變量在實例化過程中為什么必須使用new
關鍵詞
// 01-聲明一個類
class Human {
constructor(home) {
this.home = home
}
}
// 02-雖然 Human 的本質(zhì)是一個 function郎楼,但是在沒有 new 的情況下調(diào)用類 js 引擎會拋出錯誤
// Class constructor Human cannot be invoked without 'new'万伤,這點跟 ES5 是不同的
const man = Human('Earth')
// 原因很簡單,在 class 聲明的變量內(nèi)部呜袁,默認開啟的是 嚴格模式敌买,所以如果沒有使用 new
// this 的指向是 undefined,對 undefined 任何屬性或者方法的讀寫都是沒有意義的傅寡,所以直接丟出錯誤
// 03-對 類 進行實例化是否使用 new 的判斷以及實例化過程模擬
// 開啟嚴格模式
'use strict'
function setPrototypeOf(object, proto) {
Object.setPrototypeOf ?
Object.setPrototypeOf(object, proto)
:
object.__proto__ = proto
}
var person = (function () {
function _classCallCheck(instance, constructor) {
if (!(instance instanceof constructor)) {
throw new TypeError(`Class constructor ${constructor.name} cannot be invoked without 'new'`)
}
}
function Human(home) {
// 在這一步來判斷當前 this 的指向放妈,如果你理解了 new 的作用北救,
// 你就會清楚荐操,當前 this 的指向
_classCallCheck(this, Human)
this.home = home
}
var object = {}
setPrototypeOf(object, Human.prototype)
Human.apply(object, arguments)
return object
})('Earth')
// 以上就解釋了為什么 ES6 必須使用 new芜抒,以及如果做判斷的過程
ES6類實例化過程中如何添加靜態(tài)方法和原型方法
- 和
ES5
一樣,ES6
有靜態(tài)方法和原型方法托启,兩者都可以通過傳統(tǒng)的點語法
來添加靜態(tài)屬性和方法宅倒,以及原型方法霸旗,但是在ES6
中將這層添加方式做了一層封裝逮栅,直接寫在類的內(nèi)部,方法就會添加到原型或者類本身上面了
class Human {
// constructor 和 say 方法添加到原型上
constructor(home) {
this.home = home
}
say() {
return `I come from ${this.home}`
}
// 靜態(tài)方法添加到當前類本身上
static drink() {
return `human had better drink everyday`
}
}
const person = new Human('Mars')
// 上述的實現(xiàn)過程可以通過下面代碼模擬
// 開啟嚴格模式
'use strict'
function setPrototypeOf(object, proto) {
Object.setPrototypeOf ?
Object.setPrototypeOf(object, proto)
:
object.__proto__ = proto
}
function _classCallCheck(instance, constructor) {
if (!(instance instanceof constructor)) {
throw new TypeError(`Class constructor ${constructor.name} cannot be invoked without 'new'`)
}
}
// 01-拓展構(gòu)造函數(shù)的方法
var _createClass = (function () {
// 02-定義一個將方法添加原型或者構(gòu)造函數(shù)本身的方法缔俄;
function defineProperties(target, props) {
for (var i = 0, length = props.length; i < length; i++) {
// 獲取屬性描述符疗绣,即 Object.defineProperty(object, key, descriptor) 的第三個參數(shù)
var descriptor = props[i]
// 指定當前方法默認不可枚舉线召,是為了避免 for...in 循環(huán)拿到原型身上屬性
descriptor.enumerable = descriptor.enumerable || false
// 指定當前方法默認是可配置的,因為添加到原型上的方法均是可以修改和刪除的
descriptor.configurable = true
// 指定當前方法默認是可重寫多矮,因為自定義的方法可以修改
if (descriptor.hasOwnProperty('value')) descriptor.writable = true
// 添加方法到原型或者構(gòu)造函數(shù)本身
Object.defineProperty(target, descriptor.key, descriptor)
}
}
return function (constructor, protoProps, staticProps) {
// 原型上添加方法
if (protoProps) defineProperties(constructor.prototype, protoProps)
// 構(gòu)造函數(shù)自身添加靜態(tài)方法
if (staticProps) defineProperties(constructor, staticProps)
return constructor
}
})()
// 03-對類往原型以及本身上面添加方法和實例化過程的模擬
var person = (function () {
// 04-構(gòu)造函數(shù)
function Human(home) {
_classCallCheck(this, Human)
this.home = home
}
// 執(zhí)行添加方法的函數(shù)缓淹;并且第二個參數(shù)默認會有一個 key = constructor 的配置對象;
_createClass(
Human,
// 原型方法
[{
key: 'constructor',
value: Human
}, {
key: 'say',
value: function say() {
return 'I come from ' + this.home
}
}],
// 靜態(tài)方法
[{
key: 'play',
value: function drink() {
return `human had better drink everyday`
}
}]
)
var object = {}
setPrototypeOf(object, Human.prototype)
Human.apply(object, arguments)
return object
})('Mars')
- 可以看出
ES6
中對于原型方法和靜態(tài)方法的處理更加完善了塔逃,因為無論是原型還是靜態(tài)方法讯壶,都將是不可枚舉的,這在你使用for...in
運算符的時候不需要考慮如何避免查找出原型上面的方法湾盗,但在ES5
中你需要顯示的調(diào)用Object.defineProperty()
方法來設置屬性或者方法是不可枚舉的伏蚊,因為通過點語法
添加的屬性和方法都是可枚舉的
ES6的繼承實現(xiàn)
-
ES6
的繼承和ES5
繼承并沒有區(qū)別,只是做了一層封裝格粪,讓整個繼承看起來更加清晰躏吊,需要注意的是,作為子類帐萎,其實有兩條原型鏈比伏,分別是subClass.__proto__
和subClass.prototype
,原因也很好理解
子類原型鏈.png
- 當子類作為對象的時候吓肋,子類原型是父類:
subClass.__proto__ = superClass
- 當子類作為構(gòu)造函數(shù)的時候凳怨,子類的原型是父類原型的實例:
subClass.prototype.__proto__ = superClass.prototype
- 這點很重要,因為在
ES6
繼承中有個extends
關鍵字是鬼,就是用來確定子類的兩條原型鏈肤舞,這個過程也可以來模擬;
// ES6 的繼承
class Human {
constructor(home) {
this.home = home
}
say() {
return `I come from ${this.home}`
}
}
class Person extends Human {}
// 下面是模擬繼承過程
'use strict'
// 是否使用 new 操作符
function _classCallCheck(instance, constructor) {
if (!(instance instanceof constructor)) {
throw new TypeError(`Class constructor ${constructor.name} cannot be invoked without 'new'`)
}
}
// 類型檢測
function _typeCheck(placeholder, dataType) {
var _toString = Object.prototype.toString
if (placeholder) {
if (_toString.call(dataType) !== '[object Function]' && _toString.call(dataType) !== '[object Null]')
throw new TypeError(`Class extends value ${dataType} is not a constructor or null`)
} else {
if (_toString.call(dataType) === '[object Function]' || _toString.call(dataType) === '[object Object]')
return true
}
}
// 拓展構(gòu)造函數(shù)
var __createClass = (function () {
function defineProperties(target, props) {
for (var i = 0, length = props.length; i < props; i++) {
var descriptor = props[i]
descriptor.enumerable = descriptor.enumerable || false
descriptor.configurable = true
if (descriptor.hasOwnProperty('value')) descriptor.writable = true
Object.defineProperty(target, descriptor.key, descriptor)
}
}
return function (constructor, protoProps, staticProps) {
if (protoProps) defineProperties(constructor.prototype, protoProps)
if (staticProps) defineProperties(constructor, staticProps)
return constructor
}
})()
// 子類繼承父類方法
function _inheriteMethods(subClass, superClass) {
// 檢測父類類型
_typeCheck(subClass, superClass)
// 排除父類為 null均蜜,并修正 constructor 指向李剖,繼承原型方法
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
configurable: true,
writable: true
}
})
// 排除父類為 null, 繼承父類靜態(tài)方法
if (superClass) {
Object.setPrototypeOf ?
Object.setPrototypeOf(subClass, superClass)
:
(subClass.__proto__ = superClass)
}
}
// 繼承父類屬性
function _inheriteProps(instance, constructorToInstance) {
// 確保父類創(chuàng)建出 this 之后囤耳,子類才能使用 this
if (!instance) {
throw new ReferenceError(`this hasn't been initialised - super() hasn't been called`)
}
// 在確定父類不是 null 的時候返回繼承父類屬性的子類實例篙顺,否則返回一個由子類創(chuàng)建的一個空實例
return constructorToInstance && _typeCheck(null, constructorToInstance) ?
constructorToInstance
:
instance
}
// 創(chuàng)建父類
var Human = (function () {
function Human(home) {
_classCallCheck(this, Human)
this.home = home
}
__createClass(Human, [{
key: 'say',
value: function say() {
return "I come from" + this.home
}
}])
return Human
})()
// 創(chuàng)建子類
var Person = (function () {
// 原型鏈繼承
_inheriteMethods.call(null, Person, arguments[0])
// 構(gòu)造函數(shù)
function Person() {
_classCallCheck(this, Person)
// 這步是對通過父類還是子類創(chuàng)建實例的判斷偶芍,取決于 Person 的父類是否為 null,
// 如果不為 null德玫,Person.__proto__ = Human
// 如果為 null匪蟀,Person.__proto__ = Function.prototype,
// 調(diào)用 apply 返回值的 undefined,最終返回由子類創(chuàng)建的空對象
return _inheriteProps(this, (Person.__proto__ || Object.getPrototypeOf(Person)).apply(this,
arguments))
}
return Person
})(Human)
// 以上就是對子類如何繼承父類屬性和方法的完整實現(xiàn)過程
寫在最后
- 上述對于
ES6
實例化宰僧、繼承過程的實現(xiàn)是基于 babel官網(wǎng)轉(zhuǎn)換之后材彪,修改了一些代碼得來的,如果你覺得意猶未盡琴儿,自己可以嘗試一下段化。 - 本文為原創(chuàng)文章,如果需要轉(zhuǎn)載造成,請注明出處显熏,方便溯源,如有錯誤地方晒屎,可以在下方留言喘蟆,歡迎校勘夷磕。