物件導向基礎與 prototype


Posted by ericcch24 on 2020-10-16

什麼是物件導向

從閉包那邊的程式碼來看

function getWallet() {
  var my_balance = 999
  return {
    deduct: function(n) {
      my_balance -= (n > 10 ? 10 : n) // 超過 10 塊只扣 10 塊
    }
  }
}

var wallet = getWallet()
wallet.deduct(13) // 只被扣 10 塊
my_balance -= 999 // Uncaught ReferenceError: my_balance is not defined

其中 wallet.deduct(13) 表示對 wallet 這個物件呼叫裡面的 function,這差不多是物件導向的概念,==也就是要對某個物件作操作==。
而一般直接呼叫 function 的寫法是這樣 deduct(wallet)


class 範例實作物件導向(ES6)

class Dog {
  constructor(name) {
    this.name = name 
    // this 指到下面的 instance 所呼叫的不同變數
  }

  getName() {
    return this.name
  }

  sayHello() {
    console.log(this.name)
  }
}

var d = new Dog('abc') // 這叫 instance
// new -> 就是呼叫 class Dog 內的 constructor 建構子

// 因為 d 呼叫了 class Dog 內的 constructor
// 所以上面 constructor 的 this 就會指到 d 這個變數

d.sayHello()  // abc


var b = new Dog('blabla')
b.sayHello()  // blabla

ES5 的 class

// constructor
function Person(name, age) {
  this.name = name;
  this.age = age;
}

var nick = new Person('nick', 18); // instance
var peter = new Person('peter', 18); // instance

Person是一個構造函數,可以用new這個關鍵字 new 出一個 instance 來。

除此之外,也可以幫Person加入一些方法。

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.log = function () {
    console.log(this.name + ', age:' + this.age);
  }
}

var nick = new Person('nick', 18);
nick.log(); // nick, age:18

var peter = new Person('peter', 20);
peter.log(); // peter, age:20

可是這樣其實還有一個小問題, name 跟 age 這兩個屬性,很明顯是每一個 instance 都會不一樣的。可是 log 這個 method,其實是每一個 instance 彼此之間可以共享的,因為都在做同一件事情。

在現在這種情況下,雖然 nick 的 log 這個 function 跟 peter 的 log 這個 function 是在做同一件事,但其實還是佔用了兩份空間,意思就是他們其實是兩個不同的 function。

那怎麼辦呢?我們可以把這個 function 抽出來,變成所有 Person 都可以共享的方法。講到這邊,你應該有聽過一個東西叫做 prototype。只要把 log 這個 function 指定在 Person.prototype 上面,所有 Person 的 instance 都可以共享這個方法。

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.log = function () {
  console.log(this.name + ', age:' + this.age);
}

var nick = new Person('nick', 18);
var peter = new Person('peter', 20);

console.log(nick.log === peter.log) // true

// 功能依舊跟之前一樣
nick.log(); // nick, age:18
peter.log(); // peter, age:20

小總結

你有一個叫做Person的函數,就可以把Person當作 constructor,利用var obj = new Person()來 new 出一個Person的 instance,並且可以在Person.prototype上面加上你想讓所有 instance 共享的屬性或是方法。

參考資料:該來理解 JavaScript 的原型鍊了


教學影片範例

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')
// 使用 new 的 function(這裡指Dog()) 就會被
// 當成 constructor 使用

d.sayHello() 
// 可以呼叫上面的 Dog.prototype.sayHello

var b = new Dog('blabla')
b.sayHello()

探究原型鍊原理

以上面var nick = new Person('nick', 18);的例子來說,當我在呼叫nick.log()的時候,JavaScript 是怎麼找到這個 function 的?

因為 nick 這個 instance 本身並沒有 log 這個 function。==但根據 JavaScript 的機制,nick 是 Person 的 instance,所以如果在 nick 本身找不到 log,它會試著從Person.prototype去找。==

==nick 跟Person.prototype會透過__proto__的方式連接起來,才知道說要往哪邊去找 log 這個 function。==

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.log = function () {
  console.log(this.name + ', age:' + this.age);
}

var nick = new Person('nick', 18);

console.log(nick.__proto__ === Person.prototype) // true

nick 的__proto__會指向Person.prototype,所以在發現 nick 沒有 log 這個 method 的時候,==JavaScript 就會試著透過__proto__找到Person.prototype==,去看Person.prototype裡面有沒有 log 這個 method。

假如Person.prototype還是沒有,就繼續依照這個規則,去看Person.prototype.__proto__裡面有沒有 log 這個 method,就這樣一直不斷找下去。找到某個東西的__proto__是 null 為止。意思就是這邊是最上層了。

==而上面這一條透過__proto__不斷串起來的鍊,就叫做原型鍊==。透過這一條原型鍊,就可以達成類似繼承的功能,可以呼叫自己 parent 的 method。

其中因為Person其實就是個 Function 的 instance,所以Person.__proto__就是Function.prototype

底下是原型鍊的解釋 code

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.log = function () {
  console.log(this.name + ', age:' + this.age);
}

var nick = new Person('nick', 18);

// 這個剛講過了,nick.__proto__ 會指向 Person.prototype
console.log(nick.__proto__ === Person.prototype) // true

// 那 Person.prototype.__proto__ 會指向誰呢?會指向 Object.prototype
console.log(Person.prototype.__proto__ === Object.prototype) // true
console.log(Person.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype) // true

// 那 Object.prototype.__proto__ 又會指向誰呢?會指向 null,這就是原型鍊的頂端了
console.log(Object.prototype.__proto__) // null

如果想知道一個屬性是存在 instance 身上,還是存在於它屬於的原型鍊當中,可以用hasOwnProperty這個方法:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.log = function () {
  console.log(this.name + ', age:' + this.age);
}

var nick = new Person('nick', 18);
console.log(nick.hasOwnProperty('log')); // false
console.log(nick.__proto__.hasOwnProperty('log')); // true

new 做了什麼

有了原型鍊的概念之後,就不難理解new這個關鍵字背後會做的事情是什麼。

假設現在有一行程式碼是:var nick = new Person('nick');,那它有以下幾件事情要做:

  1. 創出一個新的 object,我們叫它 O
  2. 把 O 的 __proto__ 指向 Person 的 prototype,才能繼承原型鍊
  3. 拿 O 當作 context,呼叫 Person 這個建構函式
  4. 回傳 O
    我們可以寫一段程式碼來模擬這個情形:
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.log = function () {
  console.log(this.name + ', age:' + this.age);
}

function newObj(Constructor, arguments) {
  var o = new Object();

  // 讓 o 繼承原型鍊
  o.__proto__ = Constructor.prototype;

  // 執行建構函式
  Constructor.apply(o, arguments);

  // 回傳建立好的物件
  return o;
}

var nick = newObj(Person, ['nick', 18]);
nick.log(); // nick, age:18

new 的教學影片範例:

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('hi')

var b = newDog('hello')
b.sayHello()

// newDog() 要做 new 做的事
function newDog(name) {
  var obj = {} // 建立新的 obj

  Dog.call(obj, name)
  // 呼叫任意 constructor,這邊呼叫 Dog
  // 把 obj 當作 constructor 的 this 丟進去
  // 所以 Dog 內的 this 就是這邊傳入的 obj
  console.log(obj) // 此時的 obj => { name: 'hello' }

  obj.__proto__ = Dog.prototype
  // 設定要關聯的 prototype
  // 就可以使用上面的 Dog.prototype 相關的 function

  return obj
}

小補充:

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

test.call({}) // {}
// 使用 call 呼叫時,
// test 函式內的 this 就會對應到 call() 傳入的參數

還有 instanceof, constructor 等函式,詳見參考資料
參考資料:該來理解 JavaScript 的原型鍊了

tags: Week16

#week16







Related Posts

ASP.NET Core Web API 入門教學 - 參數化並過濾接受值

ASP.NET Core Web API 入門教學 - 參數化並過濾接受值

Web開發學習筆記05 — for Loop、while Loop & 遍歷陣列、物件

Web開發學習筆記05 — for Loop、while Loop & 遍歷陣列、物件

製作 Figma Plugin

製作 Figma Plugin


Comments