前言
面向?qū)ο蟮娜筇匦裕悍庋b、繼承和多態(tài)。上一篇我們簡(jiǎn)單的了解了封裝的過程,也就是把對(duì)象的屬性和方法封裝到一個(gè)函數(shù)中,這一篇講一下JAVAScript中繼承的實(shí)現(xiàn),繼承是面向?qū)ο笾蟹浅V匾奶匦裕梢詭椭覀兲岣叽a的復(fù)用性。繼承主要的思想就是將重復(fù)的代碼邏輯抽取到分類中,子類只需要通過繼承分類,就可以使用分類中的方法,但是在實(shí)現(xiàn)JavaScript繼承之前,需要先了解一個(gè)重要的知識(shí)點(diǎn)“原型鏈”。
1.JavaScript中的原型鏈
在上一篇JavaScript面向?qū)ο?mdash;對(duì)象的創(chuàng)建和操作中已經(jīng)簡(jiǎn)單的了解過了JavaScript中對(duì)象的原型和函數(shù)的原型,當(dāng)我們從一個(gè)對(duì)象上獲取屬性時(shí),如果在當(dāng)前對(duì)象自身沒有找到該屬性的話,就會(huì)去它原型上面獲取,如果原型中也沒有找到就會(huì)去它原型的原型上找,沿著這么一條線進(jìn)行查找,那么這條線就是我們所說的原型鏈了。
示例代碼:
const obj = {
name: 'curry',
age: 30
}
obj.__proto__ = {}
obj.__proto__.__proto__ = {}
obj.__proto__.__proto__.__proto__ = { height: 1.83 }
console.log(obj.height) // 1.83
對(duì)應(yīng)的內(nèi)存中的查找過程:
當(dāng)通過原型鏈查找某個(gè)屬性時(shí),一直找不到的話會(huì)一直查找下去么?肯定是不會(huì)的,JavaScript的原型鏈也是有盡頭的,這個(gè)盡頭就是Object的原型。
2.Object的原型
事實(shí)上,不管是對(duì)象還是函數(shù),它們?cè)玩湹谋M頭都是Object的原型,也稱之為頂層原型,我們可以打印看看這個(gè)頂層原型長什么樣。
(1)打印Object的原型
console.log(Object.prototype)
- 在node環(huán)境中:
- 在瀏覽器中:
(2)Object原型的特殊之處
- 如果我們?cè)俅未蛴bject.prototype的原型,這個(gè)原型屬性已經(jīng)指向了null;
- console.log(Object.prototype.__proto__) // null
- 并且在Object.prototype上有很多默認(rèn)的屬性和方法,像toString、hasOwnProperty等;
(3)上一篇中講到當(dāng)使用new操作符調(diào)用構(gòu)造函數(shù)時(shí),其對(duì)象的[[prototype]]會(huì)指向該構(gòu)造函數(shù)的原型prototype,其實(shí)Object也是一個(gè)構(gòu)造函數(shù),因?yàn)槲覀兛梢允褂胣ew操作符來調(diào)用它,創(chuàng)建一個(gè)空對(duì)象。
- 示例代碼:
- const obj = new Object() obj.name = 'curry' obj.age = 30 console.log(obj.__proto__ === Object.prototype) // true console.log(obj.__proto__) // [Object: null prototype] {} console.log(obj.__proto__.__proto__) // null
- 內(nèi)存表現(xiàn):
(4)總結(jié)
- 從Object的原型可以得出一個(gè)結(jié)論“原型鏈最頂層的原型對(duì)象就是Object的原型對(duì)象”,這也就是為什么所有的對(duì)象都可以調(diào)用toString方法了;
- 從繼承的角度來講就是“Object是所有類的父類”;
3.JavaScript繼承的實(shí)現(xiàn)方案
3.1.方案一:通過原型鏈實(shí)現(xiàn)繼承
如果需要實(shí)現(xiàn)繼承,那么就可以利用原型鏈來實(shí)現(xiàn)了。
- 定義一個(gè)父類Person和子類Student;
- 父類中存放公共的屬性和方法供子類使用;
- 核心:將父類的實(shí)例化對(duì)象賦值給子類的原型;
// 定義Person父類公共的屬性
function Person() {
this.name = 'curry'
this.age = 30
}
// 定義Person父類的公共方法
Person.prototype.say = function() {
console.log('I am ' + this.name)
}
// 定義Student子類特有的屬性
function Student() {
this.sno = 101111
}
// 實(shí)現(xiàn)繼承的核心:將父類的實(shí)例化對(duì)象賦值給子類的原型
Student.prototype = new Person()
// 定義Student子類特有的方法
Student.prototype.studying = function() {
console.log(this.name + ' studying')
}
// 實(shí)例化Student
const stu = new Student()
console.log(stu.name) // curry
console.log(stu.age) // 30
console.log(stu.sno) // 101111
stu.say() // I am curry
stu.studying() // curry studying
內(nèi)存表現(xiàn):
缺點(diǎn):
- 從內(nèi)存表現(xiàn)圖中就可以看出,當(dāng)打印stu對(duì)象時(shí),name和age屬性是看不到的,因?yàn)椴粫?huì)打印原型上的東西;
- 當(dāng)父類中的屬性為引用類型時(shí),子類的多個(gè)實(shí)例對(duì)象會(huì)共用這個(gè)引用類型,如果進(jìn)行修改,會(huì)相互影響;
- 在使用該方案實(shí)現(xiàn)繼承時(shí),屬性都是寫死的,不支持動(dòng)態(tài)傳入?yún)?shù)來定制化屬性值;
3.2.方案二:借用構(gòu)造函數(shù)實(shí)現(xiàn)繼承
針對(duì)方案一的缺點(diǎn),可以借用構(gòu)造函數(shù)來進(jìn)行優(yōu)化。
- 在子類中通過call調(diào)用父類,這樣在實(shí)例化子類時(shí),每個(gè)實(shí)例就可以創(chuàng)建自己?jiǎn)为?dú)屬性了;
// 定義Person父類公共的屬性
function Person(name, age) {
this.name = name
this.age = age
}
// 定義Person父類的公共方法
Person.prototype.say = function() {
console.log('I am ' + this.name)
}
// 定義Student子類特有的屬性
function Student(name, age, sno) {
// 通過call調(diào)用Person父類,創(chuàng)建自己的name和age屬性
Person.call(this, name, age)
this.sno = sno
}
// 實(shí)現(xiàn)繼承的核心:將父類的實(shí)例化對(duì)象賦值給子類的原型
Student.prototype = new Person()
// 定義Student子類特有的方法
Student.prototype.studying = function() {
console.log(this.name + ' studying')
}
// 實(shí)例化Student
const stu1 = new Student('curry', 30, 101111)
const stu2 = new Student('kobe', 24, 101112)
console.log(stu1) // Person { name: 'curry', age: 30, sno: 101111 }
console.log(stu2) // Person { name: 'kobe', age: 24, sno: 101112 }
內(nèi)存表現(xiàn):
缺點(diǎn):
- 在實(shí)現(xiàn)繼承的過程中,Person構(gòu)造函數(shù)被調(diào)用了兩次,一次在new Person(),一次在Person.call();
- 在Person的實(shí)例化對(duì)象上,也就是stu1和stu2的原型上,多出來了沒有使用的屬性name和age;
3.3.方案三:寄生組合式繼承
通過上面兩種方案,我們想實(shí)現(xiàn)繼承的目的是重復(fù)利用另外一個(gè)對(duì)象的屬性和方法,如果想解決方案二中的缺點(diǎn),那么就要減少Person的調(diào)用次數(shù),避免去執(zhí)行new Person(),而解決的辦法就是可以新增一個(gè)對(duì)象,讓該對(duì)象的原型指向Person的原型即可。
(1)對(duì)象的原型式繼承
將對(duì)象的原型指向構(gòu)造函數(shù)的原型的過程就叫做對(duì)象的原型式繼承,主要可以通過以下三種方式實(shí)現(xiàn):
- 封裝一個(gè)函數(shù),將傳入的對(duì)象賦值給構(gòu)造函數(shù)的原型,最后將構(gòu)造函數(shù)的實(shí)例化對(duì)象返回;
- function createObj(o) { // 定義一個(gè)Fn構(gòu)造函數(shù) function Fn() {} // 將傳入的對(duì)象賦值給Fn的原型 Fn.prototype = o // 返回Fn的實(shí)例化對(duì)象 return new Fn() } const protoObj = { name: 'curry', age: 30 } const obj = createObj(protoObj) // 得到的obj對(duì)象的原型已經(jīng)指向了protoObj console.log(obj.name) // curry console.log(obj.age) // 30 console.log(obj.__proto__ === protoObj) // true
- 改變上面方法中的函數(shù)體實(shí)現(xiàn),使用Object.setPrototypeOf()方法來實(shí)現(xiàn),該方法設(shè)置一個(gè)指定的對(duì)象的原型到另一個(gè)對(duì)象或null;
- function createObj(o) { // 定義一個(gè)空對(duì)象 const newObj = {} // 將傳入的對(duì)象賦值給該空對(duì)象的原型 Object.setPrototypeOf(newObj, o) // 返回該空對(duì)象 return newObj }
- 直接使用Object.create()方法,該方法可以創(chuàng)建一個(gè)新對(duì)象,使用現(xiàn)有的對(duì)象來提供新創(chuàng)建的對(duì)象的__proto__;
- const protoObj = { name: 'curry', age: 30 } const obj = Object.create(protoObj) console.log(obj.name) // curry console.log(obj.age) // 30 console.log(obj.__proto__ === protoObj) // true
(2)寄生組合式繼承的實(shí)現(xiàn)
寄生式繼承就是將對(duì)象的原型式繼承和工廠模式進(jìn)行結(jié)合,即封裝一個(gè)函數(shù)來實(shí)現(xiàn)繼承的過程。而這樣結(jié)合起來實(shí)現(xiàn)的繼承,又可以稱之為寄生組合式繼承。下面就看看具體的實(shí)現(xiàn)過程吧。
- 創(chuàng)建一個(gè)原型指向Person父類的對(duì)象,將其賦值給Student子類的原型;
- 在上面的實(shí)現(xiàn)方案中,Student子類的實(shí)例對(duì)象的類型都是Person,可以通過重新定義constructor來優(yōu)化;
// 定義Person父類公共的屬性
function Person(name, age) {
this.name = name
this.age = age
}
// 定義Person父類的公共方法
Person.prototype.say = function() {
console.log('I am ' + this.name)
}
// 定義Student子類特有的屬性
function Student(name, age, sno) {
// 通過call調(diào)用Person父類,創(chuàng)建自己的name和age屬性
Person.call(this, name, age)
this.sno = sno
}
// 調(diào)用Object.create方法生成一個(gè)原型指向Person原型的對(duì)象,并將這個(gè)對(duì)象賦值給Student的原型
Student.prototype = Object.create(Person.prototype)
// 定義Student原型上constructor的值為Student
Object.defineProperty(Student.prototype, 'constructor', {
configurable: true,
enumerable: false,
writable: true,
value: Student
})
// 定義Student子類特有的方法
Student.prototype.studying = function() {
console.log(this.name + ' studying')
}
// 實(shí)例化Student
const stu1 = new Student('curry', 30, 101111)
const stu2 = new Student('kobe', 24, 101112)
console.log(stu1) // Student { name: 'curry', age: 30, sno: 101111 }
console.log(stu2) // Student { name: 'kobe', age: 24, sno: 101112 }
內(nèi)存表現(xiàn):
總結(jié):
- 多個(gè)地方用到了繼承,可以將上面的核心代碼賦值在一個(gè)函數(shù)里面,如果不想用Object.create(),也可以使用上面封裝的createObj函數(shù);
- function createObj(o) { function Fn() {} Fn.prototype = o return new Fn() } /** * @param {function} SubClass * @param {function} SuperClass */ function inherit(SubClass, SuperClass) { SubClass.prototype = createObj(SuperClass.prototype) Object.defineProperty(SubClass.prototype, 'constructor', { configurable: true, enumerable: false, writable: true, value: SubClass }) }
- 寄生組合式實(shí)現(xiàn)繼承的原理其實(shí)就是創(chuàng)建一個(gè)空對(duì)象用于存放子類原型上的方法,并且這個(gè)對(duì)象的原型指向父類的原型,在ES6中推出的class的實(shí)現(xiàn)原理就在這;
原文地址:
https://www.cnblogs.com/MomentYY/p/15999285.html