小邵教你玩轉JS面向對象

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

後續我會陸陸續續更新javascript方面,儘可能把javascript這個學習路徑體系都寫一下
包括前端所經常使用的es六、angular、react、vue、nodejs、koa、express、公衆號等等
都會從淺到深,從入門開始逐步寫,但願能讓你們有所收穫,也但願你們關注我~前端

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

Author: 邵威儒
Email: 166661688@qq.com
Wechat: 166661688
github: github.com/iamswr/java


es6的class能夠看 juejin.im/post/5b7b95…node

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

或許光看文字不太好理解,也能夠對應着代碼敲一下,來感覺一下繼承是怎樣的~git

接下來我給你們講下我對javascript面向對象的理解。es6


面向對象的好處、特性

好處:github

  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方法,行爲是一致的,可是他們卻不等,是由於他們不一樣在一個堆區,若是隻有一、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數據這樣的鍵值對有效
// 對於函數等等反而無效,很差用,接着繼續看方法2、三。
複製代碼
// 方法二:
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)
複製代碼
相關文章
相關標籤/搜索