一文讓你對js的原型與原型鏈不再害怕、迷惑
- 2021 年 2 月 28 日
- 筆記
- javascript
原型與原型鏈的詳細剖析
寫在最前: 希望各位看完這篇文章後,再也不用害怕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__的關係
- 構造函數的顯式原型
prototype
默認指向一個空的(沒有我們自己定義的屬性、方法)Object對象 - 構造函數的每個實例上都有的隱式原型
__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()
方法那就看看為什麼,
- toString()方法在Object.prototype顯式原型對象上
- 實例對象的隱式原型__proto__ 指向其 構造函數的顯式原型prototype
- 而關鍵就是,構造函數的顯式原型是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)