JavaScript 程式執行原理:JS實現物件導向


Posted by backas36 on 2021-08-16

const User = function(username, greetWord){
  this.username = username
  this.greetWord = greetWord
}

// instance1
const yang = new User('Yang', 'Hello')

// instance2
const ashi = new User('Ashi', '哩賀')

再次複習一下 new 關鍵字做了些什麼事情:

首先會建立一個 object {} => 然後呼叫 User,此時的 this 就會是這個 {} => 執行 User 裡面的程式 this.username = username //省略.... => 偷偷的把 {} proto 與 User 的
prototype 連接起來 => 回傳這個 object。

要使用 function expression 還是 function declaration 都可以,但需要注意不能使用箭頭function,因為箭頭 function 的 this 運作不太一樣。

要記得如果 function 是要拿來當 class 用的,那麼變數名頭一個字必須是大寫 User

instanceof 檢查是否為某物件的 instance

instanceof 可以拿來檢查某個物件是否為某個物件(類別) 的 instance:

console.log(yang instanceof User) //true

let arr = []
console.log(arr instanceof Array) //true

當然,除了 property 外,我們也可以建立 method,如剛剛上面提到的,如果我們直接在 User 裡面新增 method 的話,這樣會當我們有一千個 instance 的時候就創造了一千個 method,但他們應該是共用一個的,
所以當我們需要建立共用的 method 的話,就要使用 prototype ,不只是 method,property 也是可以的。

const User = function(username, greetWord){
  this.username = username
  this.greetWord = greetWord

}

// prototypes
User.prototype.greeting = function() {
    console.log(this.greetWord + ',' + this.username)
  }

User.prototype.country = 'Taiwan';

// instance1
const yang = new User('Yang', 'Hello')
yang.greeting()


// instance2
const ashi = new User('Ashi', '哩賀')
ashi.greeting()

console.log(yang.country, ashi.country) // Tawian, Taiwan

這時候可以去驗證一下 instance 的 proto 與 類別 的 prototype:

console.log(yang.__proto__ === User.prototype) //true
console.log(ashi.__proto__ === User.prototype) //true

或是也可以使用 isPrototypeOf 來驗證 :

console.log(User.prototype.isPrototypeOf(ashi)) //true
console.log(User.prototype.isPrototypeOf(yang)) //true

let arr = []
console.log(Array.prototype.isPrototypeOf(arr)) //true

至於 property 的話,我們可以使用 hasOwnProperty 來驗證哪些 property 是共用哪些是該物件所有:

console.log(yang.hasOwnProperty('username')) //true
console.log(yang.hasOwnProperty('country')) //false

原型鍊

驗證一下剛剛有提到的原型鍊:

console.log(yang.__proto__) // Person.prototype
console.log(yang.__proto__.__proto__) //Object.prototype
console.log(yang.__proto__.__proto__.__proto__) //null

使用 ES6 class 的方式實現物件導向

如果以剛剛 contructor function 舉的例子用 ES6 的 Class 改寫:

// class expression
class UserClass {
  constructor(username, greetWord) {
     this.username = username
     this.greetWord = greetWord
  }

  greeting() {
    console.log(this.greetWord + ',' + this.username)
  }
}

// instance1
const yang = new UserClass('Yang', 'Hello')
yang.greeting()


// instance2
const ashi = new UserClass('Ashi', '哩賀')
ashi.greeting()

因為有了 Class,所以我們不必再以 UserClass.prototype 的方式去建立共用的屬性,而是在 class 裡面直接建立,但要注意別放到 constructor 裡面了。

Classes 沒有 hoisting 的特性,並且默認使用嚴格模式執行。

setter 與 getter

在使用 ES6 Class 時,如果我們要建立共用的屬性我們也可以使用 get

// class expression
class UserClass {
  constructor(username, greetWord) {
     this.username = username
     this.greetWord = greetWord
  }

  greeting() {
    console.log(this.greetWord + ',' + this.username)
  }

  get country() {
    return 'Taiwan'
  }
}

// instance1
const yang = new UserClass('Yang', 'Hello')
yang.greeting()

// instance2
const ashi = new UserClass('Ashi', '哩賀')
ashi.greeting()

console.log(yang.country, ashi.country)

好的,這是簡單的 getter 應用。

通常我們使用 setter 與 getter 是要實現驗證的功能,現在,我們假設我們要驗證 username 的長度是否為剛好 4 個字,不是的話 username 就會是 undefined:

// class expression
class UserClass {
  constructor(username, greetWord) {
     this.username = username
     this.greetWord = greetWord
  }

  greeting() {
    console.log(this.greetWord + ',' + this.username)
  }

  get country() {
    return 'Taiwan'
  }

  set username(name) {
    if(name.length === 4){
      this._username = name
    } else {
      console.log('Wrong length')
    }
  }

  get username() {
    return this._username
  }
}



// instance1
const yang = new UserClass('Yang', 'Hello')
yang.greeting()


// instance2
const ashi = new UserClass('Ashi', '哩賀')
ashi.greeting()

const jacky = new UserClass('Jacky', '你好')
jacky.greeting() // 你好, undefined

因為我們要建立 username 這個屬性,但是 username 與 constructor 裡面的 username 撞名了,所以在 set 裡面通常我們會在一樣的屬性名稱前面加上底線,並且記得要使用 get 來 return username,這樣我們才可以正確地得到 username 。

static

有時候我們在建立一個類別的屬性時候,並不希望該屬性被繼承,例如我們想為 User 這個 class 添加自己的屬性,並且不讓子類別使用,我們就稱這個屬性為 static property。

在 constructor function 下我們可以把下面程式碼加在 constructor function 的外面 :

const User = function(username, greetWord){
  this.username = username
  this.greetWord = greetWord
}

User.userInfo = function(){
  console.log('this is User class')
}

User.userInfo()

如果是 ES6 class 下,可以在 class function 裡面加上 :

class UserClass {
    //..省略

    static userInfo() {
        console.log('this is User class')
     }
}

UserClass.userInfo()

這樣想想也就知道為什麼 Array.from✔️ 為什麼不是 Array.prototype.from⛔️ 了,Array.from 是要建立一個陣列,所以是會讓 Array 物件來使用,而不是利用 prototype 的方式來呼叫。

第三種方式實現物件導向 Object.create

這個方式比較特別一點,不是利用剛剛的 constructor 和 new 的概念去實作,有點類似像原型鍊的方式,但又不太像,應該算是利用 Scope chain 的原理來建立物件與物件的連結的,有點難以解釋,看範例比較快:

const UserObj = {
  greeting() {
    console.log(this.greetWord + ',' + this.username)
  },
  cons(username, greetWord){
    this.username = username
    this.greetWord = greetWord
  }
}

// instance1
const yang = Object.create(UserObj)
yang.cons('Yang', 'hello')
yang.greeting()

這邊建立的 cons function ,你可以想像成就跟 constructor 做的事情一樣。

類別間的繼承

剛剛提到的都是利用原型鍊來繼承物件與類別的連結,但如果我們想建立一個類別繼承某類別呢,假設我現在要建立一個 admin 類別,admin 其實也是算 User 的其中一員不是嗎?

三種願望一次滿足,剛剛介紹的三種方式怎麼建立子類別,就讓我一一來介紹:

constructor function 中,類別與類別的繼承

const User = function(username, greetWord) {
  this.username = username
  this.greetWord = greetWord
}

User.prototype.greeting = function() {
    console.log(this.greetWord + ',' + this.username)
  }

User.prototype.country = 'Taiwan';

const Admin = function(username, greetWord, role) {
  User.call(this, username, greetWord)

  this.role = role
}



// 建立與 User 的連結
Admin.prototype = Object.create(User.prototype)

// 建立屬於 Admin 自己的屬性
Admin.prototype.delUser = function() {
  console.log(this.username, ' => yes, u can delete one of users')
}

// instance1
const yang = new User('Yang', 'Hello')
const huii = new Admin('Huii', '安安', 'admin')
huii.greeting()
huii.delUser()

yang.delUser() ; // error

記得 Admin 之所以可以使用 User 的 method 是靠這行來的 Admin.prototype = Object.create(User.prototype),如果沒加上這行的話,那麼就不會建立繼承的關係。

還有如果這時候我們 console.log(Admin.prototype.constructor) 的話,會發現竟然是 User,這是因為我們使用 Object.create 的關係,這時候只要加上這行就可以把邏輯改成正確的 Admin 了 Admin.prototype.constructor = Admin

ES6 Class 中的,類別與類別的繼承

class UserClass {
  constructor(username, greetWord) {
     this.username = username
     this.greetWord = greetWord
  }

  greeting() {
    console.log(this.greetWord + ',' + this.username)
  }

  get country() {
    return 'Taiwan'
  }

  set username(name) {
    if(name.length === 4){
      this._username = name
    } else {
      console.log('Wrong length')
    }
  }

  get username() {
    return this._username
  }

  static userInfo() {
    console.log('this is User class')
  }
}

UserClass.userInfo = function(){
  console.log('this is User class')
}


class AdminClass extends UserClass {
  constructor(username, greetWord, role) {
     super(username, greetWord)
     this.role = role
  }

  delUser() {
    console.log(this.username, ' => yes, u can delete one of users')
  }

}

const yang = new UserClass('Yang', 'Hello')
const huii = new AdminClass('Huii', '安安', 'admin')
huii.greeting()
huii.delUser()
console.log(huii.role) // admin

在 ES6 Class 中,如果子類別要繼承上層類別,在創建時我們必須使用關鍵字 extends
還有 super 的工作是為我們帶入 this 的值,假設我們的程式碼像這樣:

class AdminClass extends UserClass {
  constructor(username, greetWord, role) {

     this.role = role
  }

  delUser() {
    console.log(this.username, ' => yes, u can delete one of users')
  }

}

這樣會報錯,原因是因為沒加上 super 的話這個 this 會不知道我們在指誰,你可以想像成 super 就是幫我們告訴 UserClass,『 幫我新建一個物件,我要來為他新增專屬的屬性 』,如果 AdminClass 裡面並沒有要新增任何屬性的話那我們可以直接省略 constructor。例如:

class AdminClass extends UserClass {
  delUser() {
    console.log(this.username, ' => yes, u can delete one of users')
  }
}

這樣因為我們屬性全部都是繼承來的,就連新建的 method 也沒有需要新增 Admin 的屬性時,我們就可以直接把 constructor 與 super 省略,但是一般實務上應該是不太會這樣子用啦!

另外,如果我們在 AdminClass 中,新增一個跟上層類別 (UserClass) 一樣的 method ,因為原型鍊的關係,將會覆蓋過去 :


class AdminClass extends UserClass {
  constructor(username, greetWord, role) {
     super(username, greetWord)
     this.role = role
  }

  delUser() {
    console.log(this.username, ' => yes, u can delete one of users')
  }

  greeting() {
    console.log(this.greetWord + ',' + this.role + '!! ' + this.username)
  }

}

const huii = new AdminClass('Huii', '安安', 'admin')
huii.greeting()  // 安安,admin!! Huii

Object.create 中的,類別與類別的繼承

const UserObj = {
  greeting() {
    console.log(this.greetWord + ',' + this.username)
  },
  cons(username, greetWord){
    this.username = username
    this.greetWord = greetWord
  }
}

const AdminObj = Object.create(UserObj)
AdminObj.cons = function(username, greetWord, role){
    UserObj.cons.call(this, username, greetWord)
    this.role = role
  }
AdminObj.delUser = function() {
  console.log(this.username, ' => yes, u can delete one of users')
}



// instance1
const yang = Object.create(UserObj)
yang.cons('Yang', 'hello')
yang.greeting()


const huii = Object.create(AdminObj)
huii.cons('Huii', '安安', 'admin')
huii.greeting()

相較之下,Object.create 的方式建立子類別看起來好像沒那麼複雜,可能是因為沒有太多新的關鍵字要學習吧!
比較值得注意的是跟 constructor function 方式一樣,我們都必須了解 new 幫我們做了些什麼,還有必須理解 call 如何使用。

最後談談物件導向的封裝在 JS 中如何實現

因為我對這邊的了解還不夠深,但為了能夠完整整個物件導向的筆記,所以暫且就只列出我目前理解的範圍,如果日後更了解後再來 update 。

如果我們要將上述範例的 username 改成 private ,也就是讓外部不能夠隨意修改 username 的值,也就是讓 username 變成是唯讀的形式。

class UserClass {
  #username
  constructor(username, greetWord) {
     this.#username = username
     this.greetWord = greetWord
  }

  greeting() {
    console.log(this.greetWord + ',' + this.#username)
  }

  get username(){
    return this.#username
  }

}

const yang = new UserClass('Yang', 'Hello')

// 在外部改寫 username 值
yang.username = 'HACKER'
yang.greeting() //Hello,Yang

注意到我們多了一行 #username 並且是放在 constructor 外面,如果這個 username 的值是由 UserClass 來給的話,那我們連 constructor 裡面的 this.#username = username 這行都可以省略,因為這樣就代表 username 的值不是經由 new instance 與 UserClass 的 constructor 來生成,而是直接由 UserClass 內部來產生的。

再來必須要注意因為我們定義了 #username 為 private 之後還是不夠的,此時在外部 yang.username = 'HACKER' ,就等於幫 yang 新增一個叫做 username 屬性為 HACKER。為了解決這問題,我們必須使用 get 來設定當我們讀取 yang.username 實際上是 yang.#username
可以想像成利用 get 的方式來讀取 username = #username。

這部分真的寫得有點粗略,就當作是草稿好了,看看就好。


真是有夠長的筆記,但是我相信還是不夠完整,如果日後複習有時間再來補上,
接下來我要利用這個筆記來回答作業囉,也就當作是對這個筆記的進階補充!

本來想要再筆記 JS 中物件導向的封裝概念,但發現不是那麼好懂,好像也是因為 class private public 也是才剛雛形不久,所以資訊也不多!
等以後有空再來還債!


#js #物件導向







Related Posts

如何使用 Markdown 撰寫文章?

如何使用 Markdown 撰寫文章?

在瀏覽器與 node.js 運行 javascript

在瀏覽器與 node.js 運行 javascript

[Note] TypeScript: 常用型別與寫法

[Note] TypeScript: 常用型別與寫法


Comments