一文讓你對js的原型與原型鏈不再害怕、迷惑

原型與原型鏈的詳細剖析

寫在最前: 希望各位看完這篇文章後,再也不用害怕JS原型鏈部分的知識! — by Fitz

一起努力,加油吧!

原型

原型分為兩種顯式原型prototype隱式原型__proto__

顯式原型prototype

顯式原型prototype存在於函數中,是函數的一個屬性,它默認指向一個Object空對象(原型對象)
注意: Object空對象(原型對象)只是內容為空, 並不是真正意義上的空對象Object.create(null), 還是能夠通過原型鏈看到Object.prototype上的各種屬性、方法,例如: toString()

console.log(Object.prototype)
console.log(typeof Object.prototype)    // object

函數原型對象上的constructor屬性對應的函數對象自身

console.log(Object.prototype.constructor === Object)    // true
/* 
    例如:
        我定義了一個叫Test的函數
        Test這個函數擁有它自己的prototype原型對象
        原型對象上的constructor屬性對應的就是Test函數自己
*/
console.log(test.prototype.constructor === test)    // true

實例都會自動擁有其函數(構造函數)原型對象上的方法、屬性

function Person () {}
Person.prototype.sayHello = function () {
    console.log('Hello')
}

var fitz = new Person() // fitz是Person構造函數的一個實例
fitz.sayHello() // 'Hello'

隱式原型__proto__

每個實例對象都擁有隱式原型屬性__proto__

function Student () {
    // 構造函數Student
}
let fitz = new Student()    // fitz是Student的實例對象
console.log(fitz.__proto__) // {constructor: ƒ}

顯式原型prototype與隱式原型__proto__的關係

  1. 構造函數的顯式原型prototype默認指向一個空的(沒有我們自己定義的屬性、方法)Object對象
  2. 構造函數的每個實例上都有的隱式原型__proto__, 都指向著構造函數的顯式原型prototype

實例對象的隱式原型屬性就是其構造函數的顯式原型屬性

function Student () {
    // 構造函數Student
}
let fitz = new Student()    // fitz是Student的實例對象
console.log(fitz.__proto__ === Student.prototype) // true

關於原型對象創建整體的流程

function Person () {}   // 函數創建的時候,JS引擎為Person函數自動添加prototype對象屬性, 屬性指向一個空的Object對象

let fitz = new Person() // 實例對象創建的時候, JS引擎自動添加__proto__對象屬性, 同時將這個__proto__指向該實例對象的構造函數的prototype

/* 
    JS引擎自動做了的事:
        1.   Person.prototype = new Object()
        2.   per1.__proto__ = Person.prototype
*/

原型鏈(隱式原型鏈)

原型鏈指的是: 在訪問一個對象中的屬性時,會先在自身中尋找,如果沒有找到就會沿著__proto__向上尋找,如果找到就返回屬性,沒有就返回undefined

function Student () {
    this.sayName = function () {
        console.log('Fitz')
    }
}
// 向Student的顯示原型對象上添加sayAge()方法
Student.prototype.sayAge = function () {
    console.log(21)
}

var a = new Student()
a.sayName()     // 'Fitz'
a.sayAge()      // 21
console.log(a.toString())   // [Object object]

探尋原型鏈的盡頭

首先,理清從自定義實例對象Object構造函數的prototype的關係

// Object構造函數是JS引擎定義、生成的
console.log(Object)
// 查看Object的顯示原型對象
console.log(Object.prototype) // 能夠看到toString()等方法

// 自定義一個Student構造函數
function Student () {}
const stu = new Student()   // 創建一個Student實例對象

/* 
    因為原型鏈就是隱式原型鏈,本質上是沿著隱式原型屬性__proto__
    向上尋找屬性、方法的一個過程
*/

// 所以我們通過stu實例對象探尋原型鏈的盡頭
console.log(stu.__proto__) // 實例stu的隱式原型
// 實例對象的__proto__ 指向  它構造函數的prototype
console.log(stu.__proto__ === Student.prototype) // true
// 構造函數的prototype默認是一個空的Object實例對象
console.log(Student.prototype)
// 空的Object實例對象的構造函數一定是Object構造函數
console.log(Student.prototype.__proto__ === Object.prototype) //true
/* 
    到這裡,暫時總結一下此時的原型鏈狀態:
    stu.__proto__ => Student.prototype => object.__proto__ => Object.prototype
*/

然後就是最關鍵的部分: 著重理清Object構造函數的prototype往後部分的所有內容

// Object構造函數是JS引擎定義、生成的
console.log(Object)
// 查看Object的顯示原型對象
console.log(Object.prototype) // 能夠看到toString()等方法

// 最為關鍵的一步, 這一步直接揭示了原型鏈的盡頭在哪
console.log(Object.prototype.__proto__) // null

由此我們就能知道,原型鏈的盡頭就是: Object.prototype

// Object構造函數是JS引擎定義、生成的
console.log(Object)
// 查看Object的顯示原型對象
console.log(Object.prototype) // 能夠看到toString()等方法

// 自定義一個Student構造函數
function Student() { }
const stu = new Student()   // 創建一個Student實例對象

/* 
    因為原型鏈就是隱式原型鏈,本質上是沿著隱式原型屬性__proto__
    向上尋找屬性、方法的一個過程
*/

// 所以我們通過stu實例對象探尋原型鏈的盡頭
console.log(stu.__proto__) // 實例stu的隱式原型
// 實例對象的__proto__ 指向  它構造函數的prototype
console.log(stu.__proto__ === Student.prototype) // true
// 構造函數的prototype默認是一個空的Object實例對象
console.log(Student.prototype)
// 空的Object實例對象的構造函數一定是Object構造函數
console.log(Student.prototype.__proto__ === Object.prototype) //true
/* 
    到這裡,暫時總結一下此時的原型鏈狀態:
    stu.__proto__ => Student.prototype => object.__proto__ => Object.prototype
    */

// 最為關鍵的一步, 這一步直接揭示了原型鏈的盡頭在哪
console.log(Object.prototype.__proto__) // null

/* 
    到這裡,我們就能總結出原型鏈的盡頭就是Object.prototype的結論:
    stu.__proto__ => Student.prototype => object.__proto__ => Object.prototype => null
*/

完整詳盡的分析原型鏈

基於這一張圖,我們就能夠比較全面的掌握JavaScript中原型鏈的概念,在分析前,小夥伴們可以看這張圖先自己思考一遍

接下來是全面總結、分析原型鏈知識的部分

/* 
    根據上面這張圖由易入難,完整分析原型鏈
*/

// ===============第一部分: 自定義的構造函數及其實例============
function Foo () {}  // 1. 構造函數Foo
var f1 = new Foo()  // 2. 實例對象f1
// 3. 實例對象的隱式原型 指向 其構造函數的顯示原型
console.log(f1.__proto__ === Foo.prototype) // true
// 4. 構造函數的顯式原型是一個空的object對象
// 5. 這個空的object對象是Object構造函數的實例
console.log(Foo.prototype.__proto__ === Object.prototype) // true
// 6. 自定義構造函數是 Function構造函數的實例
//    換句話說: Foo這個構造函數,是new Function()出來的
console.log(Foo.__proto__ === Function.prototype) // true
// ===============第一部分: 自定義的構造函數及其實例============

// =============第二部分: Object構造函數及原型鏈的盡頭============
console.log(Object) // 1.  ƒ Object() { [native code] }
// 2. 實例對象o1、o2
var o1 = new Object()
var o2 = {}
// 3. Object構造函數也是Function構造函數的實例
//    換句話說: Object這個構造函數,也是new Function()出來的
console.log(Object.__proto__ === Function.prototype) // ture
// 4. Object構造函數的顯式原型(Object.prototype)就是原型鏈的盡頭
console.log(Object.prototype.__proto__) // 5. null
// =============第二部分: Object構造函數及原型鏈的盡頭============


// =================第三部分: 特殊Function構造函數================
console.log(Function) // 1.  ƒ Function() { [native code] }
// 2. Function構造函數的原型對象跟其他普通的構造函數一樣   隱式原型指向空object對象
console.log(Function.prototype.__proto__ === Object.prototype) // true
// 3. 重點特殊的地方: Function構造函數是自己的實例
//    換句話說: Function構造函數,是new Function()自己出來的, 即我生出我自己
console.log(Function.__proto__ === Function.prototype) // true
// 4. Function.prototype是一個函數,而不是像其他函數一樣是一個空的Object對象
console.log(typeof Function.prototype) // function
// =================第三部分: 特殊Function構造函數================

這張圖配合上面的程式碼

關於原型鏈的補充總結

所有函數都是 Function構造函數 的實例對象,包括Function構造函數自己

標題換句話表達, 所有函數的__proto__都指向Function.prototype
Function.__proto__指向Function.prototype

// 所有函數都是 Function構造函數 的實例對象
/* 換句話說: 無論是 
                普通函數、方法
                自定義構造函數
                Object等一些JS引擎內置的構造函數
                Function構造函數本身(我生我自己)
            都是Function構造函數的實例對象
*/
const sayHello = function () {console.log('hello')} // 自定義函數
const Student = function (name) {    // 自定義構造函數
    this.name = name
}
console.log(sayHello.__proto__=== Function.prototype)
console.log(Student.__proto__=== Function.prototype)
console.log(Object.__proto__=== Function.prototype)
console.log(Date.__proto__=== Function.prototype)
// 最為特殊的Function(我生我自己)
console.log(Function.__proto__=== Function.prototype)

Function.prototype是一個函數對象

console.log(typeof Function.prototype) // function
console.dir(Function.prototype)

所有函數的顯式原型prototype,都指向Object.prototype,Object構造函數的顯式原型除外

console.log(Function.prototype instanceof Object) // true
console.log(Function.prototype.__proto__ === Object.prototype) // true
console.log(Date.prototype instanceof Object) // true
console.log(Date.prototype.__proto__ === Object.prototype) // true
// object構造函數的原型對象除外的理由, Object.prototype是原型鏈的盡頭
console.log(Object.prototype instanceof Object) // false
console.log(Object.prototype.__proto__ === Object.prototype) // false
console.log(Object.prototype.__proto__) // null

原型鏈的應用

讀取實例對象的屬性值時,會先在自身中尋找,如果自身沒有會到原型鏈中找

function Student () {}
Student.prototype.person = 'Fitz'
var f = new Student()
// person屬性是原型對象上的,而不是實例本身的
console.log(f.person)   // 'Fitz'

對實例的屬性進行操作時,不會影響(查找)原型鏈,如果實例中沒有當前屬性,會自動添加

function Student () {}
Student.prototype.person = 'Fitz'
var f = new Student()
// 如果實例中沒有當前屬性,會自動添加
f.person = 'Lx'
f.age = 21
/* 
    屬性只會在實例上,與原型鏈無關
    可以運用前面的 引用數據類型的知識理解
*/

利用原型鏈,將實例的方法添加在原型對象上,實例的屬性添加在實例自身

好處: 避免了每次實例化對象時,都創建出一模一樣的方法,節省記憶體

function Person (name, age){
    this.name = name
    this.age = age
}
// 實例的方法統一放在構造函數的原型對象上
// 這樣實例在調用方法時,可以通過原型鏈順利找到該方法
Person.prototype.printInfo = function () {
    console.log(`name: ${this.name}`)
    console.log(`age: ${this.age}`)
}

var fitz = new Person('fitz', 21)
fitz.printInfo()

原型鏈繼承

嘗試使用原型鏈來模擬類的繼承

實現的關鍵是: 子類的原型是父類的實例

思路來源於: 既然所有的實例對象都能調用toString()方法那就看看為什麼,

  1. toString()方法在Object.prototype顯式原型對象上
  2. 實例對象的隱式原型__proto__ 指向其 構造函數的顯式原型prototype
  3. 而關鍵就是,構造函數的顯式原型是Object.prototype的實例對象
// 模擬父類
function Father() {
    _Fathername = '我是父類'
    this.name = 'Father'
}
Father.prototype.getFathername = function () {
    console.log(_Fathername)
}
Father.prototype.getName = function () {
    console.log(this.name)
}

// 模擬子類
function Son() {
    _SonName = '我是子類'
    this.name = 'Son'
}


// 實現子類繼承父類
Son.prototype = new Father()


Son.prototype.getSonName = function () {
    console.log(_SonName)
}

// 需要實現的目標
var son = new Son()
// 能在子類使用父類的方法
son.getFathername() // '我是父類'
son.getName() // 'Son'

console.log(son)