The prototype chain in JavaScript


Posted by Christy on 2021-10-07

本篇為 [JS201] 進階 JavaScript:那些你一直搞不懂的地方筆記之五。

一樣先提心得:一開始完全看不懂 js 的原型鏈,經過不斷地調整讀書計畫,努力的看懂影片跟文章,慢慢的進入狀況了。影片跟文章內容應該是大同小異,但不知道為什麼,最近都是先理解文章後才看懂影片的。

我把我的思考過程留下來,希望往後學理論相關的東西,可以有一個借鏡。這可能是在 Lidemy 學程式的過程中,第一次學理論課,希望我能夠領悟出實作跟理論分別該怎麼學。

重點整理:

a. __proto__ 是常見用法,但用正式用法比較好:Object.getPrototypeOf()

參考資料:Object.prototype.__proto__

b. 在 js 裡面,幾乎每個東西都是物件

c. 每個物件裡面都有 __proto__ 屬性,來標示自己所繼承的原型

d. 寫好一個物件以後,通常透過外部來操控內部的方法們,而不會在外面寫一個函式,直接改變物件裡面的值。

e. 當 new 一個 instance 時,instance 放的參數,可以在 constructor 裡面收到。

f. 利用 ES5 語法實作 prototype,會更容易理解 ES6 的 class 原理

g. __proto__ 其實就是「指向上一層的 prototype」

h. 了解 new 背後做的事

i. 了解繼承的概念

j. hasOwnProperty 可以知道屬性存在哪裡

k. A instanceof B 判斷 A 是不是 B 的 instance

物件導向基礎與 prototype

一、什麼是物件導向?

把物件包在函式裡面,可以隱藏、保護資訊,並且更容易模組化,更有規範;更容易對物件作操作

二、物件導向的基礎範例

1. ES6 才有的 class 語法可以用,來看一下最基本的類別雛形:

a. 名稱一定要大寫開頭

b. 就像一個設計圖,去定義裡面的內容

c. 裡面的內容就代表 class 的 method:

sayHello() 前面不用加 function,加了會出現錯誤 SyntaxError: Unexpected identifier,所以每個 method 的名稱都是一個 identifier(?)

d. 要用 new 從設計圖把 d 實體化(做一個 instance)

e. 再用 d 用操作物件

// 裡面放函式
class Dog{
  sayHello() {
    console.log('hello')
  }
}

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

2. this 在物件導向裡面是誰?

a. 誰呼叫 this,誰就是 this;在這個例子裡,this 就是 d

b. 常見模式會有一個 setter、一個 getter: 目的是為了在「不要直接改變物件內容的情況下做操作」

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

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

3. 如何接收 instance 裡面的參數?

class 裡面的 constructor 可以接收 instance 裡面的參數;個人理解,constructor 很像是一個存東西的倉庫,下面可以接執行其他行為的 method,接著用 instance + method 去操控物件

class Dog {
  constructor (name) {
    this.name = name
  }
  printName() {
    console.log(this.name)
  }
}

var d = new Dog('rookie')
d.printName() // rookie

var b = new Dog('lucky')
b.printName() // lucky

4. 物件導向最基本的概念:

有一個設計圖 -> new 一個 instance -> 用 d.methood() 的方法操作物件

三、ES5 的 class

在 ES6 以前,是沒有 class 這個語法糖的,那我們該怎麼實作呢?

下面的缺點是,無法共用同樣的資源

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

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

var e = Dog('123')
e.sayHello()
console.log(d.sayHello === e.sayHello) // false

在 ES5 中,可以把一個函式當作 constructor 用,來看看用 prototype 改寫後:

// 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()

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

console.log(d.sayHello === b.sayHello) // true

見識到了 prototype 的威力了,接下來要來解析它

四、從 prototype 來看「原型鍊」

到底 __proto__ 是什麼?

其實就是「指向上一層的 prototype」

下面範例層級由頂到底,概念是這樣:

Object -> Dog -> d

所以 d.__proto__ === Dog.prototype 會是 true

Dog.prototype.__proto__ === Object.prototype 也是 true

function Dog(name) {
  this.name = 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()
console.log(d.__proto__ === Dog.prototype) // true
console.log(Dog.prototype.__proto__ === Object.prototype) // true

我覺得這裡難的是 __proto__prototype 之間的關係

後記:我隔了兩天又重看了一遍,其實難的不是上面那個關係,而是我在心裡沒有一個具象化的圖來表示。

因此,我把各自的 .__proto__.prototype 印出來看,並畫了一個圖,好像又更懂了一點點。

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

1. 預備知識:當用 .call() 呼叫函式時,第一個引數傳什麼,this 就會是什麼

function test() {
  console.log(this)
}
test.call('123') // [String: '123']

2. this 的原理:this 其實就是 new 完以後產生的那一大個物件啦

模仿 new 後面的行為

a. 產生一個新的 obj

b. 去呼叫 construtor,初始化物件

c. 利用 __proto__ 方式,把 obj 連接到上層的 prototype

d. 回傳設定好的 obj

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

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

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

var d = newDog('abc')
d.sayHello()

// 模仿 new 背後做的事
function newDog(name) {
  var obj = {}
  Dog.call(obj, name)
  obj.__proto__ = Dog.prototype
  return obj
}

六、物件導向的繼承:Inheritance

以下為 ES6 以後的語法:

a. extends 可以繼承一個 class 的東西

b. BlackDog 會繼承 Dog,因此可以用 test() 也可以用 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('hello')
d.test() // test! hello
d.sayHello() // hello

c. 當黑狗被建立時,就跟我們 sayHello,寫一個 constructor 吧

d. 當黑狗裡面有 constructor 時,要先 call super() 並把參數傳進去,初始化以後,再呼叫想要的 method 才會有正確的值

呼叫 super() 其實就是在呼叫上一層的 constructor

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

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

class BlackDog extends Dog {
  constructor(name) {
    super(name) // 等於在呼叫 Dog.constructor
    this.sayHello()
  }
  test() {
    console.log('test!', this.name)
  }
}
const d = new BlackDog('hello')

小結:

繼承某個東西以後,記得用 super() 初始化設定

用繼承的好處就是類似把大綱先寫好,其他有小變化再自己改


本篇為 該來理解 JavaScript 的原型鍊了 閱讀筆記

prototype 先到這裡為止,下面兩篇更深的文章留到以後慢慢懂

JavaScript深入之从原型到原型链

JS原型链图解教程










Related Posts

OOP - 7 關於封裝

OOP - 7 關於封裝

Day06 抽象層 (abstraction)

Day06 抽象層 (abstraction)

起始點....

起始點....


Comments