小邵教你玩轉JS物件導向

邵威儒發表於2018-09-02

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

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

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

Author: 邵威儒
Email: 166661688@qq.com
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)
複製程式碼

相關文章