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

React Native AppState 狀態介紹

React Native AppState 狀態介紹

reverse engineer 1.1

reverse engineer 1.1

我用死薪水輕鬆理財賺千萬

我用死薪水輕鬆理財賺千萬


Comments