寫在前面
之所以想寫這個(gè),是想為以后學(xué)習(xí) react 做個(gè)鋪墊,每一個(gè)看似理所當(dāng)然的結(jié)果,實(shí)際上推敲過程很耐人尋味,就從 ES6 類的實(shí)例化和繼承開始,但是 ES6 的實(shí)現(xiàn)是在 ES5 的基礎(chǔ)上,所以需要對 ES5 中關(guān)于 構(gòu)造函數(shù)、繼承這些梳理清晰。
關(guān)于 new 關(guān)鍵字
- 無論
ES6還是ES5中對于自定義對象的實(shí)現(xiàn)都不開new這個(gè)關(guān)鍵詞,所以需要搞清楚它在構(gòu)造函數(shù)實(shí)例化過程中的作用,因?yàn)橥高^它你能看清楚構(gòu)造函數(shù)內(nèi)部this指向的問題. - 有時(shí)候文字解釋會(huì)讓讀者對一個(gè)概念產(chǎn)生各種想象,所以我比較偏好嘗試用代碼來解釋自己難以理解的地方,代碼如下:
// ES5 中構(gòu)造函數(shù)創(chuàng)建實(shí)例過程
function Person (name, age) {
this.name = name
this.age = age
}
// 返回一個(gè)具有 name = ww, age = 18 的 person 對象
var person = new Person('ww', 19)
// 那么 new 關(guān)鍵字做了哪些工作
// 下面是對使用 new 關(guān)鍵字配合構(gòu)造函數(shù)創(chuàng)建對象的過程模擬
var person = (function() {
function Person (name, age) {
this.name = name
this.age = age
}
// 創(chuàng)建一個(gè)隱式空對象
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 我們通常會(huì)稱之為 Person 的一個(gè)實(shí)例。
- 上述就是對
new在構(gòu)造函數(shù)實(shí)例化中的作用的描述,這里需要注意的一點(diǎn)是,在ES5中,如果沒有使用new關(guān)鍵字,那么Person只是作為window的一個(gè)普通方法調(diào)用,所以也不會(huì)報(bào)錯(cuò),但是在ES6的 類 的實(shí)例化中必須使用new,否則會(huì)報(bào)錯(cuò),在解讀ES6類的實(shí)例化會(huì)解釋報(bào)錯(cuò)的原因。
ES5中繼承的實(shí)現(xiàn)
- 在
ES5中實(shí)現(xiàn)繼承的方式比較多,W3C標(biāo)準(zhǔn)推薦的做法是有三種ECMAScript 繼承機(jī)制實(shí)現(xiàn),下面直接上代碼來闡述ES5實(shí)現(xiàn)繼承的過程,以及優(yōu)缺點(diǎn)
- 第一種是通過傳統(tǒng)的原型鏈繼承方式,比較好理解:自身若有被讀取的屬性或者方法的時(shí)候,自取所需,如果沒有,就沿著原型鏈一層一層往上找,直到找到或者找不到返回
undefined或者報(bào)錯(cuò)
// 01-定義父類
function Human(home) {
this.home = home
}
// 02-為父類實(shí)例添加方法
Human.prototype.say = function () {
return `I'm a ${this.sex}, I come from ${this.home}`
}
// 03-定義一個(gè)子類
function Person(sex) {
this.sex = sex
}
// 04-子類的原型對象指向父類實(shí)例,實(shí)現(xiàn)繼承
Person.prototype = new Human('Earth')
var man = new Person('man')
// 調(diào)用實(shí)例身上的方法,由于沒有,沿著原型鏈往上找,
// 找到的是父類原型對象上面的 say() 方法
man.say() // 返回的是 I'm a man, I come from Earth
// 05-子類再實(shí)例化一個(gè)對象
var female = new Person('female')
// 06-修改原型鏈上的 home 屬性
Object.getPrototypeOf(female).home = 'Mars'
// 07-調(diào)用子類兩個(gè)實(shí)例的 say() 方法
man.say() // 返回結(jié)果:I'm a man, I come from Mars
female.say() // 返回結(jié)果:I'm a female, I come from Mars
// 別人本來是來自地球,你隨便動(dòng)動(dòng)手指,讓人家誕生在火星了,多少有點(diǎn)說不過去
// 基于這樣的特點(diǎn),只要有任何一個(gè)子類實(shí)例修改了原型對象上的屬性或者父類實(shí)例自身修改了屬性
// 將會(huì)影響所有繼承它的子類,這個(gè)不是我們愿意看到的
// 所以就有了第二種方式:call 或者 apply 實(shí)現(xiàn)繼承
- 通過
call或者apply實(shí)現(xiàn)繼承和 對象冒充 繼承很相似,但是有所不同,根本原因是this 總是指向函數(shù)運(yùn)行時(shí)所在的那個(gè)對象,下面是call方法實(shí)現(xiàn)繼承過程
// 01-定義父類
function Human(home) {
this.home = home
}
// 02-為父類實(shí)例添加方法
Human.prototype.say = function () {
return `I'm a ${this.sex}, I come from ${this.home}`
}
// 03-定義一個(gè)子類
function Person(sex, home) {
// 04-實(shí)現(xiàn)借用父類創(chuàng)建子類自身的屬性,這是最重要的一點(diǎn)
// 如果你理解了 new 的作用,就知道此刻 this 指向是誰了
Human.call(this, home)
this.sex = sex
}
// 05-實(shí)例化一個(gè)具有 sex=man 和 home=earth 特征的 子類實(shí)例
var man = new Person('man', 'Earth')
// 06-再實(shí)例化一個(gè)具有 sex=female 和 home=Mars 特征的 子類實(shí)例
var female = new Person('female', 'Mars')
// 07-無論任意子類實(shí)例修改 sex 和 home 屬性,或者 父類實(shí)例修改 home 屬性
// 都不會(huì)影響到其他的子類實(shí)例,因?yàn)閷傩源丝趟接谢?
// 08-但是會(huì)發(fā)現(xiàn)調(diào)用子類兩個(gè)實(shí)例的 say() 方法,會(huì)報(bào)錯(cuò),因?yàn)樵玩溕蠜]有這個(gè)方法
// 扔出錯(cuò)誤:man.say is not a function
man.say()
female.say()
// call 或者 apply 方法實(shí)現(xiàn)繼承的最大優(yōu)勢就是能夠?qū)崿F(xiàn)屬性私有化,但是劣勢就是沒有辦法繼承
// 父類原型對象上面的方法,所以為了解決原型鏈繼承和call方法繼承的缺點(diǎn),將兩者的優(yōu)點(diǎn)糅合在一起
// 即混合繼承,能夠?qū)崿F(xiàn)完美的繼承
- 混合繼承,即通過
call或者apply實(shí)現(xiàn)屬性繼承,原型鏈實(shí)現(xiàn)方法繼承
// 01-定義父類
function Human(home) {
this.home = home
}
// 02-為父類實(shí)例添加方法
Human.prototype.say = function () {
return `I'm a ${this.sex}, I come from ${this.home}`
}
// 03-定義一個(gè)子類
function Person(sex, home) {
// 04-使用 call 繼承父類的屬性
Human.call(this, home)
this.sex = sex
}
// 05-使用原型鏈,繼承父類的方法
Person.prototype = new Human('sun')
// 06-子類實(shí)例化一個(gè)對象
var man = new Person('man', 'Earth')
// 07-子類再實(shí)例化一個(gè)對象
var female = new Person('female', 'Mars')
// 08-修改原型鏈上的 home 屬性
Object.getPrototypeOf(female).home = 'Heaven'
// 09-調(diào)用子類兩個(gè)實(shí)例的 say() 方法,返回的 home 都是當(dāng)前實(shí)例自身的 home 屬性
// 而不是原型鏈上的 home ,因?yàn)楫?dāng)前實(shí)例自身有該屬性,就不再往原型鏈上找了
// 所以通過 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類實(shí)例化中的new
- 之所以會(huì)說這么多
ES5的繼承,是因?yàn)?ES6的繼承實(shí)現(xiàn)都是基于前者,可以看做是前者的語法糖,只有在理解基礎(chǔ)的前提下,才能談進(jìn)階 - 關(guān)于
ES6中class的一些基礎(chǔ)知識就不提了,現(xiàn)在來說說class聲明的變量在實(shí)例化過程中為什么必須使用new關(guān)鍵詞
// 01-聲明一個(gè)類
class Human {
constructor(home) {
this.home = home
}
}
// 02-雖然 Human 的本質(zhì)是一個(gè) function,但是在沒有 new 的情況下調(diào)用類 js 引擎會(huì)拋出錯(cuò)誤
// Class constructor Human cannot be invoked without 'new',這點(diǎn)跟 ES5 是不同的
const man = Human('Earth')
// 原因很簡單,在 class 聲明的變量內(nèi)部,默認(rèn)開啟的是 嚴(yán)格模式,所以如果沒有使用 new
// this 的指向是 undefined,對 undefined 任何屬性或者方法的讀寫都是沒有意義的,所以直接丟出錯(cuò)誤
// 03-對 類 進(jìn)行實(shí)例化是否使用 new 的判斷以及實(shí)例化過程模擬
// 開啟嚴(yán)格模式
'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) {
// 在這一步來判斷當(dāng)前 this 的指向,如果你理解了 new 的作用,
// 你就會(huì)清楚,當(dāng)前 this 的指向
_classCallCheck(this, Human)
this.home = home
}
var object = {}
setPrototypeOf(object, Human.prototype)
Human.apply(object, arguments)
return object
})('Earth')
// 以上就解釋了為什么 ES6 必須使用 new,以及如果做判斷的過程
ES6類實(shí)例化過程中如何添加靜態(tài)方法和原型方法
- 和
ES5一樣,ES6有靜態(tài)方法和原型方法,兩者都可以通過傳統(tǒng)的點(diǎn)語法來添加靜態(tài)屬性和方法,以及原型方法,但是在ES6中將這層添加方式做了一層封裝,直接寫在類的內(nèi)部,方法就會(huì)添加到原型或者類本身上面了
class Human {
// constructor 和 say 方法添加到原型上
constructor(home) {
this.home = home
}
say() {
return `I come from ${this.home}`
}
// 靜態(tài)方法添加到當(dāng)前類本身上
static drink() {
return `human had better drink everyday`
}
}
const person = new Human('Mars')
// 上述的實(shí)現(xiàn)過程可以通過下面代碼模擬
// 開啟嚴(yá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è)將方法添加原型或者構(gòu)造函數(shù)本身的方法;
function defineProperties(target, props) {
for (var i = 0, length = props.length; i < length; i++) {
// 獲取屬性描述符,即 Object.defineProperty(object, key, descriptor) 的第三個(gè)參數(shù)
var descriptor = props[i]
// 指定當(dāng)前方法默認(rèn)不可枚舉,是為了避免 for...in 循環(huán)拿到原型身上屬性
descriptor.enumerable = descriptor.enumerable || false
// 指定當(dāng)前方法默認(rèn)是可配置的,因?yàn)樘砑拥皆蜕系姆椒ň强梢孕薷暮蛣h除的
descriptor.configurable = true
// 指定當(dāng)前方法默認(rèn)是可重寫,因?yàn)樽远x的方法可以修改
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-對類往原型以及本身上面添加方法和實(shí)例化過程的模擬
var person = (function () {
// 04-構(gòu)造函數(shù)
function Human(home) {
_classCallCheck(this, Human)
this.home = home
}
// 執(zhí)行添加方法的函數(shù);并且第二個(gè)參數(shù)默認(rèn)會(huì)有一個(gè) 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)方法的處理更加完善了,因?yàn)闊o論是原型還是靜態(tài)方法,都將是不可枚舉的,這在你使用for...in運(yùn)算符的時(shí)候不需要考慮如何避免查找出原型上面的方法,但在ES5中你需要顯示的調(diào)用Object.defineProperty()方法來設(shè)置屬性或者方法是不可枚舉的,因?yàn)橥ㄟ^點(diǎn)語法添加的屬性和方法都是可枚舉的
ES6的繼承實(shí)現(xiàn)
-
ES6的繼承和ES5繼承并沒有區(qū)別,只是做了一層封裝,讓整個(gè)繼承看起來更加清晰,需要注意的是,作為子類,其實(shí)有兩條原型鏈,分別是subClass.__proto__和subClass.prototype,原因也很好理解
子類原型鏈.png
- 當(dāng)子類作為對象的時(shí)候,子類原型是父類:
subClass.__proto__ = superClass - 當(dāng)子類作為構(gòu)造函數(shù)的時(shí)候,子類的原型是父類原型的實(shí)例:
subClass.prototype.__proto__ = superClass.prototype - 這點(diǎn)很重要,因?yàn)樵?
ES6繼承中有個(gè)extends關(guān)鍵字,就是用來確定子類的兩條原型鏈,這個(gè)過程也可以來模擬;
// 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 的時(shí)候返回繼承父類屬性的子類實(shí)例,否則返回一個(gè)由子類創(chuàng)建的一個(gè)空實(shí)例
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)建實(shí)例的判斷,取決于 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)
// 以上就是對子類如何繼承父類屬性和方法的完整實(shí)現(xiàn)過程
寫在最后
- 上述對于
ES6實(shí)例化、繼承過程的實(shí)現(xiàn)是基于 babel官網(wǎng)轉(zhuǎn)換之后,修改了一些代碼得來的,如果你覺得意猶未盡,自己可以嘗試一下。 - 本文為原創(chuàng)文章,如果需要轉(zhuǎn)載,請注明出處,方便溯源,如有錯(cuò)誤地方,可以在下方留言,歡迎??薄?/li>
