JS Advanced --物件導向基礎與 prototype


Posted by s103071049 on 2021-07-30

什麼是物件導向?

物件導向的世界 call function 不太一樣。

  • 物件導向:call function 的方式會是對這個物件做操作、
  • 優點:更容易模組化、隱藏資訊。

myWallet.add(1)
add(myWallet(1))

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 是一個物件,事實上他就是一個物件
myWallet.deduct(100)
console.log(myWallet.getMoney()) // 90

物件導向的基礎範例

ES6 以後有 class 語法可以使用,先從 ES6 如何實作物件導向談起。

class 名稱一定大寫開頭。class 是設計圖定義這個類別會有什麼樣的行為、有什麼樣的方法進行呼叫。

// 最基本 class 雛形
class Dog {
  // function,表 class 裡面的 method
  sayHello() {
    console.log('hello')
  }
}

實際使,須用 new 去實體化,從這個設計圖裡去實際產生 instance(實例)。

var d = new Dog() // 產生一個 instance
d.sayHello() // hello

目前代碼長相

class Dog {
  sayHello() {
    console.log('hello')
  }
}

var d = new Dog()
d.sayHello()

如何設定 d.setName('abc') 就是 abc ?

答:透過 this,this 一開始就存在物件導向裡面。先不管任何在物件導向外面用 this 的情形,當 this 在物件導向裡面用,他所指到的值就是誰呼叫他就指到他。

d.setName('abc') 我要對 d 做 setName,所以 this.name = name 這個 this 就是 d,所以狗的名稱就會是他傳進來的名稱。

class Dog {
  setName(name) {
    this.name = name
  }
  sayHello() {
    console.log(this.name)
  }
}

var d = new Dog()
d.setName('abc')
d.sayHello() // abc

透過 getter、setter 進行操作

  • setter:因為無法直接從外面存取裡面的值,所以要在 class 內部進行相對應的設定
  • getter:拿到相對應的值

想知道狗的名字就 console.log(d.getName())

class Dog {
  // setter
  setName(name) {
    this.name = name
  }
  // getter
  getName() {
    return this.name
  }
  sayHello() {
    console.log(this.name)
  }
}

var d = new Dog()
d.setName('abc')
console.log(d.getName())

也可以透過 console.log(d.name) 做到相同效果,d.name 進行存取。

d.name = 'aaa'
console.log(d.name) // aaa
console.log(d.getName()) // aaa

但不建議這麼寫。還是建議透過 setter、getter 進行存取因為有可能有時候需要做到其他事情。舉例來說:不會想自己操作這些東西。這是一個比較好的開發上習慣。

  getName() {
    return this.firstName + this.lastName
  }

再次觀察代碼

class Dog {
  // setter
  setName(name) {
    this.name = name
  }
  // getter
  getName() {
    return this.firstName + this.lastName
  }
  sayHello() {
    console.log(this.name)
  }
}

var d = new Dog()
d.setName('abc')
d.sayHello()

當我們在新建每一隻狗都會想給她名字,new instance 有點像 function call 的感覺,可以傳參數進去 instance。

可以透過 constructor 這個 function 去進行接收。當我 call new Dog() 其實就是在 call constructor() {略},所以我傳進來的參數就會出現在這邊 constructor(參數)

class Dog {
  // 建構子
  constructor(name) {
    this.name = name // this 指向對它操作的 instance
  }

  setName(name) {
    this.name = name
  }

  getName() {
    return this.firstName + this.lastName
  }
  sayHello() {
    console.log(this.name)
  }
}

var d = new Dog('abc')
d.setName('abc')
d.sayHello()

var e = new Dog('xxx')
e.sayHello()

小結

  1. 物件導向有設計圖 class
  2. 以 new instance 的方式將東西弄出來
  3. new ClassName(參數) call 這個參數進行初始化,就是 call class 的 constructor
  4. class 內使用 this 取指向對他操作的 instance,畢竟沒有 this 也不知道怎麼改值。

ES5 的 class

ES6 之前並沒有 class 這個關鍵字,所以我們可以怎麼做達到相同的效果呢 ?

作法一、建構 function

可以達到原本的效果。

只是每宣告一次 Dog 都會產生一個新的物件,新的物件會回傳這兩個 function : getName、sayHello,所以今天對兩隻狗 console.log(d.sayHello === b.sayHello),會發現 false,這兩個是不同的 function。

但他們做的功能相同,應該共用相同的 function,想想看如果是作法一的模式,若有一百萬隻狗,就會有一百萬個 getName function,每個 function 都做差不多的事情。

如果是 es6 的作法 console.log(d.sayHello() === b.sayHello() ),它 log 的就會是 true,因為它都指 console.log(this.name),會根據 this 的不同印出不同的內容。

作法一每創建一隻狗,就會有一個新的 getName function,這樣的做法非常耗費記憶體。

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

var b = Dog('xxx')
b.sayHello() // xxx

作法二、

希望保留這個語法:var d = new Dog('abc')

  • 將 new instance 的 instance 名稱變成 function 名稱
  • 以舊語法建構 es6 的 constructor,ES5 裡面可以把 function 當作 constructor 用。有加 new 才會把 function 當作 constructor 用。
  • 在這個 constructor 內使用 this
  • 透過 prototype 實作 class Dog 內部的 function
// 相當於 es6 constructor
function Dog(name) {
  this.name = name
}
var d = new Dog('abc') // Dog 的 instance
console.log(d) // Dog { name: 'abc' }

有加 new 才會把 function 當作 constructor 用。

// 相當於 es6 constructor
function Dog(name) {
  this.name = name
}
var d =  Dog('abc') // undefined
console.log(d) // Dog { name: 'abc' }

一樣成功呼叫兩隻狗,而且最下面呼叫的方式和 es6 一樣。
console.log(d.sayHello === e.sayHello) 是 true,因為他們都是 prototype 上面這個 function。

// 相當於 es6 constructor
function Dog(name) {
  this.name = name
}

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

var d = new Dog('abc')
d.sayHello() // abc

var e = new Dog('xxx')
e.sayHello() // xxx

小結

ES5 裡面,只要將 constructor 換成同名的 function,然後把 ES6 的 method 放在 prototype 上面,ClassName.prototype.methodName = function( ){略},就可以實作出 ES6 的 class。

從 prototype 來看「原型鍊」

d.sayHello() 會呼叫 Dog.prototype.sayHello,所以 d 跟 Dog.prototype 一定是透過某種方式把這兩個東西給連結在一起。

// 相當於 es6 constructor
function Dog(name) {
  this.name = name
}

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

var d = new Dog('abc')
d.sayHello() // abc

js 內部有一個屬性叫做 __proto__

以下面代碼為例:如果在 d 身上找不到 sayHello,應該就去這裡找,log 出 d.__proto__ => { getName: [Function (anonymous)], sayHello: [Function (anonymous)] },所以它其實就是 Dog.prototype

var d = new Dog('abc')
d.sayHello() // abc
console.log(d.__proto__) // { getName: [Function (anonymous)], sayHello: [Function (anonymous)] }

console.log(d.__proto__ === Dog.prototype) // true

ES6 class 底層是用這套機制進行實作。我們需要理解底層實作的原理 !

d.sayHello()

  1. d 身上有無 sayHello => 沒有

  2. d.__proto__ 身上有無 sayHello,透過 new 設定達到 d.__proto__ === Dog.prototype,所以會去 Dog.prototype 身上找有無 sayHello => 有,call 這個 sayHello,它的 this 就是 d

  3. 如果還是找不到,步驟 2 失敗 =>d.__proto__.__proto__ 身上有無 sayHello => 因為,d.__proto__.__proto__ === Object.prototype,所以會去 Dog.prototype 身上找有無 sayHello

  4. 如果還是找不到,步驟 2,3 失敗 =>d.__proto__.__proto__.__proto__ 身上有無 sayHello => 因為,d.__proto__.__proto__.__proto__ === null,所以 null 代表找到頂,如果還是沒有就代表沒有這個 function,就會拋出錯誤

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

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


var d = new Dog('abc')

d.sayHello() // TypeError: d.sayHello is not a function

幫 Dog、Object 都各加一個,會呼叫到 Dog 這個

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

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

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

Object.prototype.sayHello = function() {
  console.log('object', this.name)
}


var d = new Dog('abc')

d.sayHello() // dog abc

經由 __proto__ 構成的一連串 chain,就叫做 prototype chain (原型鍊)。透過 __proto__ 看能不能找到對應的 function。透過 prototype 進行連結,讓底下的東西可以共同享有同個 function。好處是只要寫一個 function,不用每個 instance 都寫一個 function。

js 很多 chain 相關的,像 scope chain。

console.log(Dog.__proto__) 會是一個 Function,{ }。

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

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

var d = new Dog('abc')

console.log(Dog.__proto__) //  { },Dog 是一個 function

console.log(Dog.__proto__ === Function.prototype) // true

為甚麼兩個 call 的結果不同 ?

Object.prototype.toString.call('123') // [object String]
var a = '123'
a.toString() // 123

a 本身沒有 toString,它一樣是放在 prototype 上面你才呼叫的到。所以當我們 call a.toString() 其實就是在 call String.prototype.toString。如果要再往上找一層,call object,就沒辦法直接 call a.toString(),要用其他方法去 call function 才能呼叫到它,因為它被原型鍊給擋住了。

字串一樣會有自己的原型鍊,

var a = '123'
a.toString() // 123

console.log(a.__proto__ === String.prototype) // true
console.log(a.toString === String.prototype.toString) // true

可以幫 String 加 prototype,這樣每一個 string 都可以呼叫到這個 function。

同理,也可以幫 array、object 加

String.prototype.first = function() {
  return this[0]
}
var a = '123'
console.log(a.first()) // 1
console.log(a.__proto__ === String.prototype) // true

小結

  1. __proto__ 將 instance 與 prototype 連接起來,透過這個屬性往上找

所以,new 到底做了什麼事?

this 的值會是一個非常大、非常奇怪的值

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

test()
/*
<ref *1> Object [global] {
  global: [Circular *1],
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Function (anonymous)]
  },
  queueMicrotask: [Function: queueMicrotask],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Function (anonymous)]
  }
}
*/

除了這種 test( ) 呼叫方式,也可以用 .call(參數) 呼叫 function

當用 .call 的方法,第一個參數就會是裡面的 this。我傳甚麼裡面的 this 就是甚麼

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

test.call('123') // [String: '123']
test.call({}) // { }

實作 new 機制

現在,newDog 就要做 new 所做的事情

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

// 希望達成目標
var b = newDog('hello')
b.sayHello()
function newDog(name) {

}
  1. 宣告一個新的 object
  2. 執行建構子(constructor)
  3. Dog 裡面的 this 就是 obj,Dog 的引數就是我傳進去的 name => console.log(obj) // { name: 'hello' } => 這個 this.name 就等於 obj.name 就等於傳進去的 name 'hello',現在有了一個執行完建構子把資料放到裡面的物件
  4. 串 prototype,obj.__proto__ = Dog.prototype,相互連結後就可以呼叫產生的 instance
  5. 回傳產生的物件
function Dog(name) {
  this.name = name
}

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

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

// 希望達成目標

var b = newDog('小狗汪汪')
b.sayHello()
console.log(b.getName())

function newDog(name) {
  var obj = {} // 建立物件
  Dog.call(obj, name) // Dog 裡面的 this 就是 obj,Dog 的引數就是我傳進去的 name
  obj.__proto__ = Dog.prototype
  return obj
}

new 可以接收任何 type,這邊只以 Dog 當範例,也可以改成更普遍的 function,只是換參數而已

小結

  1. .call(參數),物件導向裡的 this 就會是我傳進去的參數

  2. new 關鍵字所做的事情 :

  • 產生新的物件
  • 呼叫建構子
  • 將新產生的物件當作 this 丟入建構子 => 完成物件初始化
  • 設定 proto 讓他連到相對應的 prototype
  • 將弄好的物件回傳 => 他就是你想要的 instance 了

物件導向的繼承:Inheritance

需要用到一些共同屬性,不用所有東西都自己重做,比如:狗它有名字、它會握手、它會說話,今天有別的品種的狗,它也具備狗的行為是狗底下的分支,例如:黑狗、白狗,這個時候只要繼承狗就會繼承他的 method,只要狗有的 function 都可以用,因為已經繼承了。

解析黑狗繼承狗這段代碼

  • const d = new BlackDog('小黑'),因為 BlackDog 裡面沒寫建構子的關係,所以它就會去找他的 parent 也就是 Dog,他去找 Dog 裡面的建構子執行 => 黑狗現在有名字了
  • d.test() => 在 class BlackDog extends Dog {略} 這裡面找到,所以執行代碼 => this.name = 小黑 => 印出 test 小黑
  • 也可以 call d.sayHello()
class Dog {
  constructor(name) {
    this.name = name
  }
  sayHello() {
    console.log(this.name)
  }
}

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

const d = new BlackDog('小黑')
d.test() // test 小黑
d.sayHello() // 小黑

希望黑狗被建立,就 sayHello,和我們打招呼

ReferenceError: Must call super constructor in derived class before accessing 't

因為 call 建構子裡面的 this.sayHello(),class Dog 要 this.name,但尚未初始化,因此如果有用到需要初始化的東西就會出現錯誤。因此強制一定要 call super。

class Dog {
  constructor(name) {
    this.name = name
  }
  sayHello() {
    console.log(this.name)
  }
}
class BlackDog extends Dog {
  constructor() {
    this.sayHello()
  }
  test() {
    console.log('test', this.name)
  }
}
const d = new BlackDog('小黑')

super( ) 就是上一層的 constructor,所以當我 call super,就是再 call Dog 的 constructor

印出 undefined

顯然單 call super 是沒有用的,因為我 call super Class Dog 的 name 還是 undefined。既然已經覆寫了這個建構子,我就必須接收一個 name,將 name 一起傳上 parent 的建構子,讓他成功的初始化,初始化後再 call this.sayHello 才會有正確的值。

// 單 call super 是沒有用的,印出 undefined
class BlackDog extends Dog {
  constructor() {
    super() // Dog.constructor
    this.sayHello()
  }
  test() {
    console.log('test', this.name)
  }
}

const d = new BlackDog('小黑')

如果 super 不傳值,我的 parent 的建構子因為沒有進行初始化,所以 parent 的 sayHello function,就是 undefined,因為裡面的 this.name 沒有被初始化過

// 成功存取到值
class BlackDog extends Dog {
  constructor(name) {
    super(name) // Dog.constructor
    this.sayHello()
  }
  test() {
    console.log('test', this.name)
  }
}

const d = new BlackDog('小黑') // 小黑

小結

  1. 繼承某個 class 後,在建構子內用 super 呼叫 parent 的建構子,並設定初始化的東西。
  2. 完成設定後,可以使用繼承的 method

繼承的優點:
可以先將東西寫好,其餘變化較小的再用繼承去改行為


#js-advanced-物件導向基礎與-prototype #prototype







Related Posts

docker 是什麼:快速部署應用程式的實用工具

docker 是什麼:快速部署應用程式的實用工具

物件導向

物件導向

網路分層簡介

網路分層簡介


Comments