前言:大家好,我叫邵威儒,大家都喜歡喊我小邵,學的金融專業卻憑藉興趣愛好入了程式猿的坑,從大學買的第一本vb和自學vb,我就與程式設計結下不解之緣,隨後自學易語言寫遊戲輔助、交易軟體,至今進入了前端領域,看到不少朋友都寫文章分享,自己也弄一個玩玩,以下文章純屬個人理解,便於記錄學習,肯定有理解錯誤或理解不到位的地方,意在站在前輩的肩膀,分享個人對技術的通俗理解,共同成長!
後續我會陸陸續續更新javascript方面,儘量把javascript這個學習路徑體系都寫一下
包括前端所常用的es6、angular、react、vue、nodejs、koa、express、公眾號等等
都會從淺到深,從入門開始逐步寫,希望能讓大家有所收穫,也希望大家關注我~
Author: 邵威儒
Email: 166661688@qq.com
Wechat: 166661688
github: github.com/iamswr/
es6的class可以看 juejin.im/post/5b7b95…
物件導向在面試中會經常問起,特別是對於繼承的理解,關於物件導向的定義我就不說了,我主要從繼承方面來講物件導向的好處,更重要的是收穫一種程式設計思維。
或許光看文字不太好理解,也可以對應著程式碼敲一下,來感受一下繼承是怎樣的~
接下來我給大家講下我對javascript物件導向的理解。
物件導向的好處、特性
好處:
- 更方便
- 複用性好
- 高內聚和低耦合
- 程式碼冗餘度低
特性:
// 1.封裝
// 假設需要登記學籍,分別記錄小明和小紅的學籍號、姓名
let name1 = "小明"
let num1 = "030578001"
let name2 = "小紅"
let num2 = "030578002"
// 如果需要登記大量的資料,則弊端會非常明顯,而且不好維護,那麼我們會使用以下方法來登入,這也是物件導向的特性之一:封裝
let p1 = {
name:"小明",
num:"030578001"
}
let p2 = {
name:"小紅",
num:"030578002"
}
// 2.繼承
// 從已有的物件上,獲取屬性、方法
function Person(){
this.name = "邵威儒"
}
Person.prototype.eat = function(){
console.log("吃飯")
}
let p1 = new Person()
p1.eat() // 吃飯
let p2 = new Person()
p2.eat() // 吃飯
// 3.多型
// 同一操作,針對不同物件,會有不同的結果
let arr = [1,2,3]
arr.toString() // 1,2,3
let obj = new Object()
obj.toString() // [object Object]
複製程式碼
如何建立物件
// 1.字面量
// 該方式的劣勢比較明顯,就是無法複用,如果建立大量同型別的物件,則程式碼會非常冗餘
let person = {
name:"邵威儒",
age:28,
eat:function(){
console.log('吃飯')
}
}
// 2.利用內建物件的方式建立物件
// 該方式的劣勢也比較明顯,就是沒辦法判斷型別
function createObj(name,age){
let obj = new Object()
obj.name = name
obj.age = age
return obj
}
let p1 = createObj("邵威儒",28)
let p2 = createObj("swr",28)
console.log(p1 === p2) // false
console.log(p1.constructor) // Object 指向的建構函式是Object
console.log(p2.constructor) // Object 指向的建構函式是Object
// 那麼為什麼說沒辦法判斷型別呢?那麼我們建立一條狗的物件
// 可以看出,狗的constructor也是指向Object,那麼我們人和狗的型別就沒辦法去區分了
let dog = createObj('旺財',10)
console.log(dog.constructor) // Object 指向的建構函式是Object
// 3.利用建構函式的方式建立物件
// 其執行的過程:
// 3.1 使用new這個關鍵詞來建立物件
// 3.2 在建構函式內部把新建立出來的物件賦予給this
// 3.3 在建構函式內部把新建立(將來new的物件)的屬性方法綁到this上
// 3.4 預設是返回新建立的物件,特別需要注意的是
// 如果顯式return一個物件資料型別,那麼將來new的物件,就是顯式return的物件
function Person(name,age){
// 1.系統自動建立物件,並且把這個物件賦值到this上,此步不需要我們操作
// let this = new Object()
// 2.給這個物件賦屬性、方法,需要我們自己操作
this.name = name
this.age = age
this.eat = function(){
console.log(name + '吃飯')
}
// 3.系統自動返回建立的物件
// return this
}
let p1 = new Person("邵威儒",28)
console.log(p1.constructor) // Person 指向的建構函式是Person
function Dog(name,age){
this.name = name
this.age = age
}
let dog = new Dog("旺財",10)
console.log(dog.constructor) // Dog 指向的建構函式是Dog
複製程式碼
// 預設是返回新建立的物件,特別需要注意的是
// 如果顯式return一個物件資料型別,那麼將來new的物件,就是顯式return的物件
// 這個是之前一個小夥伴問的,我們看下面的例子
// 當我們顯式return一個原始資料型別
function Person(name,age){
this.name = name
this.age = age
return "1"
}
let p = new Person("邵威儒",28) // { name: '邵威儒', age: 28 }
// 當我們顯式return一個物件資料型別時
function Person(name,age){
this.name = name
this.age = age
return [1,2,3]
}
let p = new Person("邵威儒",28) // [ 1, 2, 3 ]
// 我們發現,當顯式return一個物件資料型別時,我們new出來的物件,得到的是return的值
複製程式碼
例項屬性方法、靜態屬性方法、原型屬性方法
例項屬性方法
都是繫結在將來通過建構函式建立的例項上,並且需要通過這個例項來訪問的屬性、方法
function Person(name,age){
// 例項屬性
this.name = name
this.age = age
// 例項方法
this.eat = function(){
console.log(this.name + '吃飯')
}
}
// 通過建構函式建立出例項p
let p = new Person("邵威儒",28)
// 通過例項p去訪問例項屬性
console.log(p.name) // 邵威儒
// 通過例項p去訪問例項方法
p.eat() // 邵威儒吃飯
複製程式碼
靜態屬性方法
繫結在建構函式上的屬性方法,需要通過建構函式訪問
// 比如我們想取出這個Person建構函式建立了多少個例項
function Person(name, age) {
this.name = name
this.age = age
if (!Person.total) {
Person.total = 0
}
Person.total++
}
let p1 = new Person('邵威儒',28)
console.log(Person.total) // 1
let p2 = new Person('swr',28)
console.log(Person.total) // 2
複製程式碼
原型屬性方法
建構函式new出來的例項,都共享這個建構函式的原型物件上的屬性方法,類似共享庫。
function Person(name,age){
this.name = name
this.age = age
}
Person.prototype.eat = function(){ // 使用prototype找到該Person的原型物件
console.log(this.name + '吃飯')
}
let p1 = new Person("邵威儒",28)
let p2 = new Person("swr",28)
console.log(p1.eat === p2.eat) // true
p1.eat() // 邵威儒吃飯
複製程式碼
我們為什麼需要原型物件(共享庫)?
因為通過new生成的例項,相當於是重新開闢了一個堆區,雖然是同型別,擁有類似的屬性和方法,但是這些屬性和方法,並不是相同的
function Person(name,age){
this.name = name
this.age = age
this.eat = function(){
console.log('吃飯')
}
}
let p1 = new Person("邵威儒",28)
let p2 = new Person("swr",28)
console.log(p1.eat === p2.eat) // fasle
複製程式碼
從上面可以得出,p1和p2的eat方法,行為是一致的,但是他們卻不等,是因為他們不同在一個堆區,如果只有1、2個例項還好,如果大量的例項,那麼會大量生成這種原本可以複用共用的屬性方法,非常耗費效能,不利於複用,此時我們就需要一個類似共享庫的物件,讓例項能夠沿著原型鏈,去找。
function Person(name){
this.name = name
}
Person.prototype.eat = functoin(){ // 通過建構函式Person的prototype屬性找到Person的原型物件
console.log('吃飯')
}
let p1 = new Person("邵威儒",28)
let p2 = new Person("swr",28)
console.log(p1.eat === p2.eat) // true
複製程式碼
這樣可以增加複用性,但是還存在一個問題,如果我們要給原型物件新增大量屬性方法時,我們不斷的Person.prototype.xxx = xxx、Person.prototype.xxxx = xxxx,這樣也是很繁瑣,那麼我們該怎麼解決這個問題?
function Person(name){
this.name = name
}
// 讓Person.prototype指標指向一個新的物件
Person.prototype = {
eat:function(){
console.log('吃飯')
},
sleep:function(){
console.log('睡覺')
}
}
複製程式碼
如何找到原型物件
function Person(name){
this.name = name
}
Person.prototype = {
eat:function(){
console.log('吃飯')
},
sleep:function(){
console.log('睡覺')
}
}
let p = new Person('邵威儒',28)
// 訪問原型物件
console.log(Peroson.prototype)
console.log(p.__proto__) // __proto__僅用於測試,不能寫在正式程式碼中
複製程式碼
和原型物件有關幾個常用方法
// 1.hasOwnProperty 在物件自身查詢屬性而不到原型上查詢
function Person(){
this.name = '邵威儒'
}
let p = new Person()
let key = 'name'
if((key in p) && p.hasOwnProperty(key)){
// name僅在p物件中
}
// 2.isPrototypeOf 判斷一個物件是否是某個例項的原型物件
function Person(){
this.name = '邵威儒'
}
let p = new Person()
let obj = Person.prototype
obj.isPrototypeOf(p) // true
複製程式碼
更改原型物件constructor指標
原型物件預設是有一個指標constructor指向其建構函式的,
如果我們把建構函式的原型物件,替換成另外一個原型物件,那麼這個新的原型
物件的constructor則不是指向該建構函式,會導致型別判斷的錯誤
function Person(){
this.name = '邵威儒'
}
Person.prototype = { // 把Person建構函式的原型物件替換成該物件
eat:function(){
console.log('吃飯')
}
}
console.log(Person.prototype.constructor) // Object
// 我們發現,該原型物件的constructor指向的是Object而不是Person
// 那麼我們現在解決一下這個問題,把原型物件的constructor指向到Person
Person.prototype.constructor = Person
console.log(Person.prototype.constructor) // Person
複製程式碼
建構函式、原型物件、例項之間的關係
繼承
物件導向的繼承方式有很多種,原型鏈繼承、借用建構函式繼承、組合繼承、原型式繼承、寄生式繼承、寄生式組合繼承、深拷貝繼承等等。
原型鏈繼承
利用原型鏈的特性,當在自身找不到時,會沿著原型鏈往上找。
function Person(){
this.name = '邵威儒'
this.pets = ['旺財','小黃']
}
Person.prototype.eat = function(){
console.log('吃飯')
}
function Student(){
this.num = "030578000"
}
let student = new Student()
console.log(student.num) // '030578000'
console.log(student.name) // undefined
console.log(student.pets) // undefined
student.eat() // 報錯
複製程式碼
從上面我們可以看到,Student沒有繼承Person,此時它們之間的聯絡是這樣的。
既然要讓例項student訪問到Person的原型物件屬性方法,
我們會想到,把Student.prototype改寫為Person.prototype
function Person(){
this.name = '邵威儒'
this.pets = ['旺財','小黃']
}
Person.prototype.eat = function(){
console.log('吃飯')
}
function Student(){
this.num = "030578000"
}
// * 改寫Student.prototype指標指向
Student.prototype = Person.prototype
let student = new Student()
console.log(student.num) // '030578000'
console.log(student.name) // undefined
console.log(student.pets) // undefined
student.eat() // * '吃飯'
複製程式碼
此時關係圖為
現在修改了Student.prototype指標指向為Person.prototype後,可以訪問Person.prototype上的eat方法,但是student還不能繼承Person.name和Person.pets,那我會想到,是Person的例項,才會同時擁有例項屬性方法和原型屬性方法。
function Person(){
this.name = '邵威儒'
this.pets = ['旺財','小黃']
}
Person.prototype.eat = function(){
console.log('吃飯')
}
function Student(){
this.num = "030578000"
}
// * new一個Person的例項,同時擁有其例項屬性方法和原型屬性方法
let p = new Person()
// * 把Student的原型物件指向例項p
Student.prototype = p
// * 把Student的原型物件的constructor指向Student,解決型別判斷問題
Student.prototype.constructor = Student
let student = new Student()
console.log(student.num) // '030578000'
console.log(student.name) // * '邵威儒'
console.log(student.pets) // * '[ '旺財', '小黃' ]'
student.eat() // '吃飯'
複製程式碼
因為例項p是由Person建構函式例項化出來的,所以同時擁有其例項屬性方法和原型屬性方法,並且把這個例項p作為Student的原型物件,此時的關係圖如下
這種稱為原型鏈繼承,到此為止原型鏈繼承就結束了
藉助建構函式繼承
通過這樣的方式,會有一個問題,原型物件類似一個共享庫,所有例項共享原型物件同一個屬性方法,如果原型物件上有引用型別,那麼會被所有例項共享,也就是某個例項更改了,則會影響其他例項,我們可以看一下
function Person(){
this.name = '邵威儒'
this.pets = ['旺財','小黃']
}
Person.prototype.eat = function(){
console.log('吃飯')
}
function Student(){
this.num = "030578000"
}
let p = new Person()
Student.prototype = p
Student.prototype.constructor = Student
let student = new Student()
let student2 = new Student() // * new多一個例項
console.log(student.num) // '030578000'
console.log(student.name) // '邵威儒'
console.log(student.pets) // '[ '旺財', '小黃' ]'
student.eat() // '吃飯'
// 此時我們修改某一個例項,pets是原型物件上的引用型別 陣列
student.pets.push('小紅')
console.log(student.pets) // * [ '旺財', '小黃', '小紅' ]
console.log(student2.pets) // * [ '旺財', '小黃', '小紅' ]
複製程式碼
從上面可以看出,student的pets(實際就是原型物件上的pets)被修改後,相關的例項student2也會受到影響。
那麼我們能不能把Person上的屬性方法,新增到Student上呢?以防都存在原型物件上,會被所有例項共享,特別是引用型別的修改,會影響所有相關例項。
可以利用call來實現。
function Person(){
this.name = '邵威儒'
this.pets = ['旺財','小黃']
}
Person.prototype.eat = function(){
console.log('吃飯')
}
function Student(){
Person.call(this) // * 利用call呼叫Person上的屬性方法拷貝一份到Student
this.num = "030578000"
}
let p = new Person()
Student.prototype = p
Student.prototype.constructor = Student
let student = new Student()
let student2 = new Student()
console.log(student.num) // '030578000'
console.log(student.name) // '邵威儒'
console.log(student.pets) // '[ '旺財', '小黃' ]'
student.eat() // '吃飯'
// * 此時我們修改某一個例項,pets是原型物件上的引用型別 陣列
student.pets.push('小紅')
console.log(student.pets) // * [ '旺財', '小黃', '小紅' ]
console.log(student2.pets) // * [ '旺財', '小黃' ]
複製程式碼
上面在子建構函式(Student)中利用call呼叫父建構函式(Person)的方式,叫做藉助建構函式繼承
結合上面所看,使用了原型鏈繼承和藉助建構函式繼承,兩者結合起來使用叫組合繼承,關係圖如下:
那麼還有個問題,當父建構函式需要接收引數時,怎麼處理?
function Person(name,pets){ // * 父建構函式接收name,pets引數
this.name = name // * 賦值到this上
this.pets = pets // * 賦值到this上
}
Person.prototype.eat = function(){
console.log('吃飯')
}
function Student(num,name,pets){ // * 在子建構函式中也接收引數
Person.call(this,name,pets) // * 在這裡把name和pets傳引數
this.num = num // * 賦值到this上
}
let p = new Person()
Student.prototype = p
Student.prototype.constructor = Student
let student = new Student("030578000","邵威儒",["旺財","小黃"])
let student2 = new Student("030578001","iamswr",["小紅"])
console.log(student.num) // '030578000'
console.log(student.name) // '邵威儒'
console.log(student.pets) // '[ '旺財', '小黃' ]'
student.eat() // '吃飯'
student.pets.push('小紅')
console.log(student.pets) // * [ '旺財', '小黃', '小紅' ]
console.log(student2.pets) // * [ '小紅' ]
複製程式碼
這樣我們就可以在子建構函式中給父建構函式傳參了,而且我們也發現上圖中,2個紅圈的地方,程式碼是重複了,那麼接下來我們怎麼解決呢?
能否在子建構函式設定原型物件的時候,只要父建構函式的原型物件屬性方法呢?
當然是可以的,接下來我們講寄生式組合繼承,也是目前程式猿認為解決繼承問題最好的方案
寄生式組合繼承
function Person(name,pets){
this.name = name
this.pets = pets
}
Person.prototype.eat = function(){
console.log('吃飯')
}
function Student(num,name,pets){
Person.call(this,name,pets)
this.num = num
}
// * 寄生式繼承
function Temp(){} // * 宣告一個空的建構函式,用於橋樑作用
Temp.prototype = Person.prototype // * 把Temp建構函式的原型物件指向Person的原型物件
let temp = new Temp() // * 用建構函式Temp例項化一個例項temp
Student.prototype = temp // * 把子建構函式的原型物件指向temp
temp.constructor = Student // * 把temp的constructor指向Student
let student1 = new Student('030578001','邵威儒',['旺財','小黃'])
console.log(student1) // Student { name: '邵威儒',
pets: [ '旺財', '小黃' ],
num: '030578001' }
let student2 = new Student('030578002','iamswr',['小紅'])
console.log(student2) // Student { name: 'iamswr',
pets: [ '小紅' ],
num: '030578002' }
複製程式碼
至此為止,我們就完成了寄生式組合繼承了,主要邏輯就是用一個空的建構函式,來當做橋樑,並且把其原型物件指向父建構函式的原型物件,並且例項化一個temp,temp會沿著這個原型鏈,去找到父建構函式的原型物件
原型式繼承
// 原型式繼承
function createObjWithObj(obj){ // * 傳入一個原型物件
function Temp(){}
Temp.prototype = obj
let o = new Temp()
return o
}
// * 把Person的原型物件當做temp的原型物件
let temp = createObjWithObj(Person.prototype)
// * 也可以使用Object.create實現
// * 把Person的原型物件當做temp2的原型物件
let temp2 = Object.create(Person.prototype)
複製程式碼
寄生式繼承
// 寄生式繼承
// 我們在原型式的基礎上,希望給這個物件新增一些屬性方法
// 那麼我們在原型式的基礎上擴充套件
function createNewObjWithObj(obj) {
let o = createObjWithObj(obj)
o.name = "邵威儒"
o.age = 28
return o
}
複製程式碼
深拷貝繼承
// 方法一:利用JSON.stringify和JSON.parse
let swr = {
name:"邵威儒",
age:28
}
let swrcopy = JSON.parse(JSON.stringify(swr))
console.log(swrcopy) // { name:"邵威儒",age:28 }
// 此時我們修改swr的屬性
swr.age = 29
console.log(swr) // { name:"邵威儒",age:29 }
// 但是swrcopy卻不會受swr影響
console.log(swrcopy) // { name:"邵威儒",age:28 }
// 這種方式進行深拷貝,只針對json資料這樣的鍵值對有效
// 對於函式等等反而無效,不好用,接著繼續看方法二、三。
複製程式碼
// 方法二:
function deepCopy(fromObj,toObj) { // 深拷貝函式
// 容錯
if(fromObj === null) return null // 當fromObj為null
if(fromObj instanceof RegExp) return new RegExp(fromObj) // 當fromObj為正則
if(fromObj instanceof Date) return new Date(fromObj) // 當fromObj為Date
toObj = toObj || {}
for(let key in fromObj){ // 遍歷
if(typeof fromObj[key] !== 'object'){ // 是否為物件
toObj[key] = fromObj[key] // 如果為原始資料型別,則直接賦值
}else{
toObj[key] = new fromObj[key].constructor // 如果為object,則new這個object指向的建構函式
deepCopy(fromObj[key],toObj[key]) // 遞迴
}
}
return toObj
}
let dog = {
name:"小白",
sex:"公",
firends:[
{
name:"小黃",
sex:"母"
}
]
}
let dogcopy = deepCopy(dog)
// 此時我們把dog的屬性進行修改
dog.firends[0].sex = '公'
console.log(dog) // { name: '小白',
sex: '公',
firends: [ { name: '小黃', sex: '公' }] }
// 當我們列印dogcopy,會發現dogcopy不會受dog的影響
console.log(dogcopy) // { name: '小白',
sex: '公',
firends: [ { name: '小黃', sex: '母' } ] }
複製程式碼
// 方法三:
let dog = {
name:"小白",
sex:"公",
firends:[
{
name:"小黃",
sex:"母"
}
]
}
function deepCopy(obj) {
if(obj === null) return null
if(typeof obj !== 'object') return obj
if(obj instanceof RegExp) return new RegExp(obj)
if(obj instanceof Date) return new Date(obj)
let newObj = new obj.constructor
for(let key in obj){
newObj[key] = deepCopy(obj[key])
}
return newObj
}
let dogcopy = deepCopy(dog)
dog.firends[0].sex = '公'
console.log(dogcopy)
複製程式碼