JavaScript 核心 - 物件導向與原型鍊


Posted by ai86109 on 2020-09-30

前一篇的最後,有示範了一個簡單的錢包程式碼

function createWallet(initMoney){
  var money = initMoney
  return {
    add: function(num){
      money += num
    },
    deduct: function(num){
      if(num >= 10){
        money -= 10
      } else {
        money -= num
      }
    },
    getMoney(){
      return money
    }
  }
}

var myWallet = createWallet(99)
myWallet.add(1)
myWallet.deduct(100)
console.log(myWallet.getMoney())

在這段程式碼中利用 var myWallet = createWallet(99),然後再 call myWallet 裡的 add 這個 method,他不像我們一般直接 call 一個 function,myWallet 在這邊就像一個物件一樣,這就是物件導向。


ES6 中的物件導向

JavaScript 在 ES6 之後有了 class 這個語法

class 名稱一定是大寫開頭,例如 class Dog{ },可以在裡面寫很多功能

看以下程式碼應該就可以知道物件導向最基本的樣子了

class Dog{
  setName(name) {
    this.name = name
  }
  getName(name) {
    return this.name
  }
  sayHello() {
    console.log(this.name + ': hello')
  }
}

var yoyo = new Dog()
yoyo.setName('yoyo')
console.log(yoyo.getName())
yoyo.sayHello()

var bigMac = new Dog()
bigMac.setName('bigMac')
bigMac.sayHello()

setName 這種功能的我們通常稱為 setter
getName 這種功能的我們通常稱為 getter

// this 在這邊指的是要動作的對象
例如 yoyo.setName('yoyo'),表示要對 yoyo 去做 setName 的動作,所以 this 指的是 yoyo。

-

那當我們在建立一個新狗的時候,var yoyo = new Dog()

這邊看起來很像是在呼叫一個 function 的樣子

那如果在這裡傳進參數的話會傳到哪呢?

答案是會到 constructor(建構子)裡

class Dog{
  constructor(name) {
    this.name = name
  }
}

當你在 call Dog 的時候,其實就是在 call 這個 function

此時 constructor 就會被建立,因此 constructor 很適合作為初始化之用,這裡也可以對他做操作,如設定名字等等。


ES5 以前的物件導向

ES6 可以利用 class 的方式實現物件導向,那在 ES5 的時候要怎麼做呢?

你可能會想到用之前我們在講閉包時候的方法

function Dog(name){
  var myName = name
  return {
    getName: function(){
      return myName
    },
    sayHello: function(){
      console.log(myName)
    }
  }
}

var yoyo = Dog('yoyo')
yoyo.sayHello()

沒錯的確可以這樣做,但這樣做會有一個問題

你每創造一隻新狗的時候,他就會 return 你一個 getName & sayHello 的 function

所以如果有一萬隻狗,就會回傳一萬個

這樣是很浪費記憶體空間的。

-
所以在 ES5 做物件導向時,其實是用以下的程式碼去實作

function Dog(name) {
  this.name = name
}

Dog.prototype.getName = function() {
  return this.name
}

Dog.prototype.sayHello = function() {
  console.log(this.name)
}

var yoyo = new Dog('yoyo')
yoyo.sayHello()

跟 ES6 很像,這個 function Dog 可以把它看成 ES6 class 裡的 constructor,那我要怎麼知道 Dog 這個函式是一般的函式還是當作constructor 用。

⇒ 只要呼叫時有加 new,就是當 constructor 用,反之則是一般 function
var yoyo = new Dog('yoyo') -> 物件導向
var yoyo = Dog('yoyo') -> 一般呼叫 function

⇒ 在新增 method 時,要用 prototype

以上這種方法就不會每次創一個新的狗時,回傳新的 function 佔記憶體空間了。


原型鍊(prototype)

那你可能會很好奇,yoyo.sayHello() 是怎麼 call 到 Dog.prototype.sayHello 的,他們之間應該有某種連結才對

的確,在 JavaScript 中有個隱藏的屬性叫做 __proto__

我們將他 log 出來 console.log(yoyo.__proto__),他會印出 Dog { getName: [Function], sayHello: [Function] },跟上面的 method 是吻合的

所以我們可以知道 yoyo.__proto__ === Dog.prototype

這是 new 幫你設定好的(稍後會更詳細說明)

當你在 call yoyo.sayHello() 的時候,會按照以下步驟去找:

  1. yoyo 本身有沒有 sayHello
  2. yoyo.__proto__ 有沒有 sayHello(我們剛知道 yoyo.__proto__ 等於 Dog.prototype,所以會去這找)
  3. yoyo.__proto__.__proto__ 有沒有 sayHello(其實就等於 Object.prototype)
  4. yoyo.__proto__.__proto__.__proto__ 這個 log 出來就會是 null,也就是已經找到頂了

以上經由 __proto__ 構成的 chain,就叫做 prototype chain 原型鍊。

-
知道了這個就可以做很多有趣的事

String.prototype.first = function() {
  return this[0]
}

var a = '123'
console.log(a.first())

答案就會是 1,也就是會回傳他的第一個字

之前說過的 Object.prototype.toString.call(),就是利用這個原理

而 ES6 的 class 底層也是用 prototype 去實作的。


new 做了什麼

OK,了解原型鍊之後,要來說說 new 做了什麼

但說這個之前要先講一個重要的東西

當我們有一個 function 時,我們用 .call 的方式去 call 這個 function,傳進去的參數就會是 this 的值

function test(){
  console.log(this)
}

test.call('123')

這邊只要先記得,用 .call 的時候,this 就是我傳進去的值。

-
繼續來講 new,先附上剛剛 ES5 的寫法

function Dog(name) {
  this.name = name
}

Dog.prototype.getName = function() {
  return this.name
}

Dog.prototype.sayHello = function() {
  console.log(this.name)
}

var yoyo = new Dog('yoyo')
yoyo.sayHello()

現在我們想做一個新的狗 bobo

var bobo = newDog('bobo')
bobo.sayHello()

並且自己實作 new 的功能,讓 bobo.sayHello() 可以 work

-
第一步我們要先讓 newDog('bobo’) 被 call 的時候,能夠將資訊放入 constructor

function Dog(name){
  this.name = name
}

做法就是用我們剛剛講的 .call 並且傳入參數的方式

function newDog(name) {
  var obj = {}
  Dog.call(obj, name)
}

因為 var bobo = newDog('bobo’),所以這裡的 name 是 bobo

Dog.call 傳入的第一個參數會是 this 的值,第二個參數會是 function Dog 的參數

所以帶入後會是這樣

function Dog(‘bobo’){
  {}.name = ‘bobo’
}

也就是會等於 {name: ‘bobo’}

-
第二步要設定 proto,讓他連到 Dog.prototype

最後再讓他 return 回來就好了

function newDog(name){
  var obj = {}
  Dog.call(obj, name)
  obj.__proto__ = Dog.prototype
  return obj
}

上面這樣寫其實就跟 var bobo = new Dog('bobo’) 一樣

以上就是 new 所做的事。


繼承(Inheritance)

在物件導向的最後要講一個十分重要的概念:繼承

我們現在已經有狗的各種 method,假如我們現在有一種狗叫黑狗

他有狗的特性,但還有一些黑狗特殊的 method,我們就可以用繼承來實作

class Dog{
  constructor(name) {
    this.name = name
  }
  sayHello(){
    console.log(this.name + ': hello')
  }
}

class BlackDog extends Dog{
  test(){
    console.log('test', this.name)
  }
}

const d = new BlackDog('fofo')
d.test()

因為在 BlackDog 這裡我們沒有 constructor,所以 fofo 傳入後會再往 parents 那邊找,this.name 就是 fofo。

因為繼承的關係,這隻黑狗 d 兩邊 function 的 method 都可以使用。

-
那如果我現在想在黑狗初始化時,就順便 sayHello

應該只要直接在 BlackDog 這裡加一個 constructor 就好了吧!

constructor(name){
  this.sayHello()
}

不行哦,這邊需要先將傳進來的資訊先往上傳到 Dog 的 constructor 初始化,才能使用 this。

所以必須寫成

constructor(name) {
  super(name)
  this.sayHello()
}

用 super 將帶入的資訊再往上傳去初始化。

有關繼承可以參考這篇:Javascript继承机制的设计思想


原型鍊可以參考:
該來理解 JavaScript 的原型鍊了
从设计初衷解释 JavaScript 原型链


#class #prototype







Related Posts

1141. User Activity for the Past 30 Days I

1141. User Activity for the Past 30 Days I

Day01 聊聊對話機器人與LINE Developers

Day01 聊聊對話機器人與LINE Developers

JAVA筆記_網路程式設計

JAVA筆記_網路程式設計


Comments