[進階 js 10] 物件導向 & Prototype


Posted by tzutzu858 on 2021-03-27

[進階 js 09] Closure & Scope Chain最後有一個改 money 的程式碼,這跟物件導向的概念很類似

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(10)
console.log(myWallet.getMoney())

在這段程式碼 call myWallet.add(1) ,就好像 myWallet 是一個物件,他不像我們一般直接 call 一個 function,而是對 myWallet 這個物件做一些操作,感覺更模組化。


ES6 談物件導向

ES6 以後有 class 這個語法可以用
class 名稱一定是大寫開頭,這點跟 Java class 命名一樣
實際運用一定要用 new 去 instantiated (實例、實體化)
可以使用 getName()setName() 去 get 和 set 裡面資料
以下就是物件導向最基本的樣子

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

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

通常不建議用 d.name = 123 去做存取,還是建議用 getName()setName()
因為你可能會這樣寫

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

你不會想另外寫東西去操作這些 return this.firstName + this.lastName 東西


constructor(建構子)

以上例子 new Dog() 這邊蠻像 function code
所以是可以傳參數進去 new Dog('abc')
那要怎麼接收呢?

constructor(參數) { }

class Dog{
  constructor(name) { 
      this.name = name
  }
  getName(name) {
    return this.name
  }
  sayHello() {
    console.log(this.name)
  }
}

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

var a = new Dog('def')
a.sayHello()

便可以根據這個設計圖產生非常多隻不同名字的狗狗


ES5 沒有 class 的替代方法

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

var d = Dog('abc')
var a = Dog('def')

差別只在於不用 new
但這邊會有個小地方是個問題
如果去比較 a.sayHello === d.sayHello
答案會是 false
他們兩個是不同的 function
但他們只要共用同一個 function,都是要做一樣的事情
不然如果你要有一千隻狗,就會一千個 getName 這個 function
這樣是很浪費記憶體空間的


ES5 類似機制

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

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

在這邊可以把一個 function 當作 constructor 用
那這樣要怎麼知道這個 function 是 constructor 還是普通 function

只有加 new 的時候
才會把這個 function 當作 constructor

上述例子如果沒有加 new,那log 出來就是 undefined


設定屬性的問題解決,接下來要怎麼解決 getName 這個 function

prototype

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')
var a = new Dog('def')

此時去比較 a.sayHello === d.sayHello
答案就會是 true


從 prototype 來看「原型鍊」

從上述例子 d.sayHello()Dog.prototype 之間一定要透過某種方式來連結,不然 js 引擎要怎麼知道d.sayHello()要去 Dog.prototype.sayHello
JavaScript 有個內部屬性 __proto__
這個屬性就是暗示說如果在 d 身上找不到 sayHello 的話,就去 __proto__
可以比較 d.__proto__ === Dog.prototype,答案會是 true


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

  1. d 身上有沒有 sayHello
  2. d.__proto__ 有沒有 sayHello
  3. d.__proto__.__proto__ 有沒有 sayHello
  4. d.__proto__.__proto__.__proto__ 有沒有 sayHello
  5. log 出來是 null 代表找到頂了

d.__proto__ = Dog.prototype
d.__proto__.__proto__ = Object.prototype
Dog.prototype.__proto__ = Object.prototype

以上經由 proto 構成一連串的 chain,就叫做 prototype chain 原型鍊
透過 prototype 的方式,讓底下的東西連起來,共享相同 function


console.log(Dog.__proto__) 印出來會是 [Function]
Dog.__proto__ === Function.prototype 答案會是 true


透過 __proto__ 這個底線機制,就可以做很多有趣事情

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

var a = '123'
console.log(a.first()) //印出 1

new

在理解 new 在背後做了什麼事之前,先來看個預備知識

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


發現 this 是一個超級大的值,圖片擷取不完
function 還有另外一個呼叫方式,叫 .call()

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

test.call('123');  // 發現 this 的值就變成 [String: '123']
test.call({});     // this 的值就變成 {}

代表 .call() 傳進去的第一個參數就是裡面 function 的 this

理解 new 背後在做的事情

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

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

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

// 下面 function 就等同於 new 在做的事情
function newDog(name) {
  var obj = {};
  Dog.call(obj, name);
  obj.__proto__ = Dog.prototype; 
  return obj;
}

var d = newDog('abc')
d.sayHello();   // 印出 abc
// 所以上面兩行就等於下面兩行
var a = new Dog('hi')
a.sayHello();

所以 new 背後在做的事情就是:

  1. 先建立一個空物件 obj
  2. 呼叫 constructor,然後利用 .call() 帶入空物件,並設定參數,Dog.call(obj, name) : 第一個參數代表 this 指向 obj,第二個參數代表要帶入的值
  3. 建立 prototype 關聯 : obj.__proto__ = Dog.prototype
  4. return 完成的 obj

Inheritance 物件導向的繼承

當其他 class 需要用到共同屬性時,可以利用 Inheritance 繼承的方法,來直接存取父層的屬性。

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('hello');
d.test(); // test hello
d.sayHello()  // hello

解析程式碼 :
const d = new BlackDog('hello');
因為在 class BlackDog extends Dog {...} 沒有寫 constructor 的關係
所以他會往 parent(父類別)的 constructor 找並執行
d.test();class BlackDog extends Dog {...} 有找到並直接執行


那如果想要黑狗被建立時就呼叫一個 function 直接 say hello
那可以在繼承裡面直接加 constructor

class BlackDog extends Dog {
    constructor() {
        this.sayHello();
    }
}

結果會出現錯誤

Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

這句話是在說 : call this 之前一定要 call super
super 就是上一層的 constructor

class BlackDog extends Dog {
    constructor(name) {
        super(name);
        this.sayHello();
    }
}

為什麼要這樣做呢?
假設沒有 call super
那在 this.sayHello(); 的時候,發現父類別的 sayHello() {} 裡又有 console.log(this.name);,但此時又還沒初始化(父類別還沒 constructor),那用到需要初始化的 this,不就造成一個 bug 了,所以才會強制一定要 call super
但單 call super 是沒有用的,父類別的 constructor 要傳參數 name 進去,不然就會變 undefined。所以既然已經覆寫了 constructor ,那就要接收一個 name constructor(name),然後在把 name 傳上去 super(name);,讓父類別的 constructor 成功初始化,此時再 call this.sayHello(); 才會有正確的值


文章參考 :

  1. Javascript面面觀:核心篇《物件導向》
  2. 該來理解 JavaScript 的原型鍊了









Related Posts

reverse engineer 1.1

reverse engineer 1.1

JS 的資料型態與賦值

JS 的資料型態與賦值

WeakMap

WeakMap


Comments