小邵教你玩轉JS面向對象

NO IMAGE

前言:大家好,我叫邵威儒,大家都喜歡喊我小邵,學的金融專業卻憑藉興趣愛好入了程序猿的坑,從大學買的第一本vb和自學vb,我就與編程結下不解之緣,隨後自學易語言寫遊戲輔助、交易軟件,至今進入了前端領域,看到不少朋友都寫文章分享,自己也弄一個玩玩,以下文章純屬個人理解,便於記錄學習,肯定有理解錯誤或理解不到位的地方,意在站在前輩的肩膀,分享個人對技術的通俗理解,共同成長!

後續我會陸陸續續更新javascript方面,儘量把javascript這個學習路徑體系都寫一下
包括前端所常用的es6、angular、react、vue、nodejs、koa、express、公眾號等等
都會從淺到深,從入門開始逐步寫,希望能讓大家有所收穫,也希望大家關注我~

文章列表:juejin.im/user/5a84f8…

Author: 邵威儒
Email: [email protected]
Wechat: 166661688
github: github.com/iamswr/


es6的class可以看 juejin.im/post/5b7b95…

面向對象在面試中會經常問起,特別是對於繼承的理解,關於面向對象的定義我就不說了,我主要從繼承方面來講面向對象的好處,更重要的是收穫一種編程思維。

或許光看文字不太好理解,也可以對應著代碼敲一下,來感受一下繼承是怎樣的~

接下來我給大家講下我對javascript面向對象的理解。


目錄

面向對象的好處、特性

好處:

  1. 更方便
  2. 複用性好
  3. 高內聚和低耦合
  4. 代碼冗餘度低

特性:

// 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('睡覺')
}
}

小邵教你玩轉JS面向對象

如何找到原型對象

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

構造函數、原型對象、實例之間的關係

小邵教你玩轉JS面向對象

繼承

面向對象的繼承方式有很多種,原型鏈繼承、借用構造函數繼承、組合繼承、原型式繼承、寄生式繼承、寄生式組合繼承、深拷貝繼承等等。

原型鏈繼承

利用原型鏈的特性,當在自身找不到時,會沿著原型鏈往上找。

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,此時它們之間的聯繫是這樣的。

小邵教你玩轉JS面向對象

既然要讓實例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() // * '吃飯'

此時關係圖為

小邵教你玩轉JS面向對象

現在修改了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的原型對象,此時的關係圖如下

小邵教你玩轉JS面向對象

這種稱為原型鏈繼承,到此為止原型鏈繼承就結束了

藉助構造函數繼承

通過這樣的方式,會有一個問題,原型對象類似一個共享庫,所有實例共享原型對象同一個屬性方法,如果原型對象上有引用類型,那麼會被所有實例共享,也就是某個實例更改了,則會影響其他實例,我們可以看一下

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)的方式,叫做藉助構造函數繼承

結合上面所看,使用了原型鏈繼承和藉助構造函數繼承,兩者結合起來使用叫組合繼承,關係圖如下:

小邵教你玩轉JS面向對象

那麼還有個問題,當父構造函數需要接收參數時,怎麼處理?

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) // * [ '小紅' ]

小邵教你玩轉JS面向對象

這樣我們就可以在子構造函數中給父構造函數傳參了,而且我們也發現上圖中,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會沿著這個原型鏈,去找到父構造函數的原型對象

小邵教你玩轉JS面向對象

原型式繼承

// 原型式繼承
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)

相關文章

webpack4.x最詳細入門講解

小邵教你玩轉Typescript、ts版React全家桶腳手架

小邵教你玩轉nodejs之nodejs概念、事件環機制(1)

小邵教你玩轉ES6