2020年了,別再重複學習原型了

前置

原型是 JavaScript 巧妙的設計,它非常容易理解。都 2020 年了,看完這篇希望你以後不需要再重複學習 JavaScript 原型了。如有不當之處,懇請指點一二!

單詞

下面是相關單詞及其翻譯,牢牢記住它們就成功一半了。

  • constructor 構造器
  • proto & prototype 原型;雛形;最初形態

constructor

function Drink() {}

const a = new Drink()
console.log(a.constructor) // ƒ Drink() {}

a 是由函數 Drink 構造而來的。

prototype

簡單寫點程式碼,後面全是 console.log

function f() {
  this.a = 1
  this.b = 2
}

const o = new f()
f.prototype.b = 3
f.prototype.c = 4

o 是 new f() 返回的結果,不妨回顧一下當執行 new f()時, new 操作符對函數 f 做了些什麼事。

  1. 創建一個空對象(即{}): var obj = Object.create(null)
  2. 鏈接該對象(即設置該對象的構造函數)到另一個對象(本函數), 這個空對象繼承其原型: obj.__proto__ = f.prototype
  3. 使用指定的參數調用函數 f,new Foo 等同於 new Foo(),也就是 f 不帶任何參數調用的情況; 將步驟 1 創建的對象作為 this 的上下文(將 this 綁定到新創建的對象 | f 函數中的 this 的指針替換成 obj) ,f.call(obj)
  4. 如果該函數沒有顯式地在函數中寫 return,則返回 this。

對於一個函數,如果不使用 new 操作它,它只是一個正常的函數;使用 new 操作符僅僅改變了它的 this 指向且在函數內部隱式地創建了一個對象,然後再稱之為 「構造函數」。僅此而已。

如果你對第三步中的操作有困惑,看幾個簡單的例子:

設置構造函數
function f() {
  this.a = 1
  this.b = 2
}
f()
console.log(f.constructor) //ƒ Function() { [native code] }
function f() {
  this.a = 1
  this.b = 2
}

const o = new f()
console.log(o.constructor)
// ƒ f() {
//   this.a = 1
//   this.b = 2
// }
this指針替換
function f() {
  console.log(this) // window
  this.a = 1
  this.b = 2
}

f()
function f() {
  console.log(this) // f {}
  this.a = 1
  this.b = 2
  console.log(this) // f {a: 1, b: 2}
}

new f()
什麼是call?
const drink = {
  name: 'Coca Cola',
  color: 'black',
  price: '3.5',
  intro: function () {
    console.log(`名稱:${this.name},顏色:${this.color},價格:${this.price}`)
  },
}

const computer = {
  name: 'Orange Juice',
  color: 'orange',
  price: '4',
}

drink.intro.call(computer) //名稱:Orange Juice,顏色:orange,價格:4

確保上面的內容你能十分清晰,否咋不要進行下面的內容。

console.log(o.b) // 2

o 的值是通過 new f() 得到的對象,this 指向這個對象,函數中給 this 添加了屬性 b 為其賦值為 2,並將他返回。所以 這裡列印出 2。f.prototype 是無法被訪問到的,這種情況還被稱之為 property shadowing —屬性遮蔽

console.log(o.c) // 4
console.log(o.__proto__.c) // 4
console.log(o.__proto__ === f.prototype) // true

函數中並沒有給 this 添加 c 屬性並為其賦值 4,但是列印 o.c 返回 4。通過上文你已經知道 constructor 是幹什麼的了:

console.log(o.constructor.prototype.b) // 3

o 是由函數 f 構造的,o.constructor 返回函數 f,所以o.constructor.prototype === f.prototype, f.prototype 返回什麼呢?上面初始程式碼中直接寫好的,現在可以翻上去看看 f.prototype,所以 o.constructor.prototype.b 返回 3。查找對象上的屬性就是先找自身再通過 __proto__ 一層一層往上找的:

  • 如果自身有該屬性直接獲取它的值;
  • 如果自身有且其構造器的 prototype 上也有,屬性遮蔽不會忘了吧;
  • 如果一直沿著 __proto__ 找但沒找到,會返回 undefined;為什麼呢?
console.log({}.constructor) // ƒ Object() { [native code] }
console.log({}.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__) // null

看到這裡,應該十分清晰了。這就是最終為什麼會返回 undefind 的原因:Object.prototype.__proto__ 指向 null。

小練習

做一個簡單又不給你解釋的小練習吧!

console.log(o.b)
console.log(o.__proto__.b)
console.log(o.d)
答案
// 2
// 3
// undefined

重要提示

對於 Object.prototype.__proto__

測試程式碼

如果看完還是不太明白,動手試一試吧!我把本文用到的程式碼片段放到此處供你快速拷貝。

function f() {
  this.a = 1
  this.b = 2
}

const o = new f()
f.prototype.b = 3
f.prototype.c = 4

console.log(o.b) // 2

console.log(o.c) // 4
console.log(o.__proto__.c) // 4
console.log(o.__proto__ === f.prototype) // true

console.log(o.constructor.prototype.b) // 3

console.log(o.b) // 2
console.log(o.__proto__.b) // 3
console.log(o.d) // undefined

console.log({}.constructor) // ƒ Object() { [native code] }
console.log({}.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__) // null

// --------- other --------
console.log(o.__proto__) // {b: 3, c: 4, constructor: ƒ}
console.log(o.__proto__.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__) // null
console.log(o.__proto__.__proto__.__proto__) // null
console.log(f.prototype.__proto__ === Object.prototype) // true