一文讓你對js的原型與原型鏈不再害怕、迷惑
阿新 • • 發佈:2021-03-01
[toc]
# 原型與原型鏈的詳細剖析
寫在最前: 希望各位看完這篇文章後,再也不用害怕JS原型鏈部分的知識! -- by Fitz
一起努力,加油吧!
## 原型
原型分為兩種`顯式原型prototype`和`隱式原型__proto__`
### 顯式原型prototype
`顯式原型prototype`存在於函式中,是函式的一個屬性,**它預設指向一個Object空物件(原型物件)**
==注意: Object空物件(原型物件)只是內容為空, 並不是真正意義上的空物件Object.create(null), 還是能夠通過原型鏈看到Object.prototype上的各種屬性、方法,例如: toString()==
``` js
console.log(Object.prototype)
console.log(typeof Object.prototype) // object
```
![](https://img2020.cnblogs.com/blog/2117688/202102/2117688-20210228231631879-1354539463.png)
函式原型物件上的constructor屬性對應的函式物件自身
``` js
console.log(Object.prototype.constructor === Object) // true
/*
例如:
我定義了一個叫Test的函式
Test這個函式擁有它自己的prototype原型物件
原型物件上的constructor屬性對應的就是Test函式自己
*/
console.log(test.prototype.constructor === test) // true
```
![](https://img2020.cnblogs.com/blog/2117688/202102/2117688-20210228231650284-1382685396.png)
例項都會自動擁有其函式(建構函式)原型物件上的方法、屬性
``` js
function Person () {}
Person.prototype.sayHello = function () {
console.log('Hello')
}
var fitz = new Person() // fitz是Person建構函式的一個例項
fitz.sayHello() // 'Hello'
```
### 隱式原型__proto__
每個例項物件都擁有隱式原型屬性`__proto__`
``` js
function Student () {
// 建構函式Student
}
let fitz = new Student() // fitz是Student的例項物件
console.log(fitz.__proto__) // {constructor: ƒ}
```
### 顯式原型prototype與隱式原型__proto__的關係
1. 建構函式的顯式原型`prototype`預設指向一個空的(沒有我們自己定義的屬性、方法)Object物件
2. 建構函式的每個例項上都有的隱式原型`__proto__`, 都指向著建構函式的顯式原型`prototype`
![](https://img2020.cnblogs.com/blog/2117688/202102/2117688-20210228231718308-1257235043.png)
例項物件的隱式原型屬性就是其建構函式的顯式原型屬性
``` js
function Student () {
// 建構函式Student
}
let fitz = new Student() // fitz是Student的例項物件
console.log(fitz.__proto__ === Student.prototype) // true
```
關於原型物件建立整體的流程
``` js
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
*/
```
![](https://img2020.cnblogs.com/blog/2117688/202102/2117688-20210228231733580-1432675172.png)
## 原型鏈(隱式原型鏈)
原型鏈指的是: 在訪問一個物件中的屬性時,會先在自身中尋找,如果沒有找到就會沿著`__proto__`向上尋找,如果找到就返回屬性,沒有就返回undefined
``` js
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]
```
![](https://img2020.cnblogs.com/blog/2117688/202102/2117688-20210228231748928-509335279.png)
![](https://img2020.cnblogs.com/blog/2117688/202102/2117688-20210228231755868-189684551.png)
### 探尋原型鏈的盡頭
首先,理清從`自定義例項物件`到`Object建構函式的prototype`的關係
``` js
// 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
*/
```
![](https://img2020.cnblogs.com/blog/2117688/202102/2117688-20210228231813458-259070917.png)
==然後就是最關鍵的部分:== 著重理清`Object建構函式的prototype`往後部分的所有內容
``` js
// Object建構函式是JS引擎定義、生成的
console.log(Object)
// 檢視Object的顯示原型物件
console.log(Object.prototype) // 能夠看到toString()等方法
// 最為關鍵的一步, 這一步直接揭示了原型鏈的盡頭在哪
console.log(Object.prototype.__proto__) // null
```
![](https://img2020.cnblogs.com/blog/2117688/202102/2117688-20210228231825930-755958738.png)
**由此我們就能知道,原型鏈的盡頭就是: Object.prototype**
``` js
// 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
*/
```
![](https://img2020.cnblogs.com/blog/2117688/202102/2117688-20210228231839393-1271599859.png)
## 完整詳盡的分析原型鏈
基於這一張圖,我們就能夠比較全面的掌握JavaScript中原型鏈的概念,在分析前,小夥伴們可以看這張圖先自己思考一遍
![](https://img2020.cnblogs.com/blog/2117688/202102/2117688-20210228231858948-1188743816.jpg)
接下來是全面總結、分析原型鏈知識的部分
``` js
/*
根據上面這張圖由易入難,完整分析原型鏈
*/
// ===============第一部分: 自定義的建構函式及其例項============
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建構函式================
```
這張圖配合上面的程式碼
![](https://img2020.cnblogs.com/blog/2117688/202102/2117688-20210228231943845-1503638268.png)
### 關於原型鏈的補充總結
#### 所有函式都是 Function建構函式 的例項物件,包括Function建構函式自己
標題換句話表達, 所有函式的`__proto__`都指向`Function.prototype`
`Function.__proto__`指向`Function.prototype`
``` js
// 所有函式都是 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是一個函式物件
``` js
console.log(typeof Function.prototype) // function
console.dir(Function.prototype)
```
![](https://img2020.cnblogs.com/blog/2117688/202102/2117688-20210228232010048-986219566.png)
#### 所有函式的顯式原型prototype,都指向Object.prototype,Object建構函式的顯式原型除外
``` js
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
```
## 原型鏈的應用
讀取例項物件的屬性值時,會先在自身中尋找,如果自身沒有會到原型鏈中找
``` js
function Student () {}
Student.prototype.person = 'Fitz'
var f = new Student()
// person屬性是原型物件上的,而不是例項本身的
console.log(f.person) // 'Fitz'
```
![](https://img2020.cnblogs.com/blog/2117688/202102/2117688-20210228232024818-1388184815.png)
對例項的屬性進行操作時,不會影響(查詢)原型鏈,==如果例項中沒有當前屬性,會自動新增==
``` js
function Student () {}
Student.prototype.person = 'Fitz'
var f = new Student()
// 如果例項中沒有當前屬性,會自動新增
f.person = 'Lx'
f.age = 21
/*
屬性只會在例項上,與原型鏈無關
可以運用前面的 引用資料型別的知識理解
*/
```
![](https://img2020.cnblogs.com/blog/2117688/202102/2117688-20210228232039481-1156643809.png)
### 利用原型鏈,將例項的方法新增在原型物件上,例項的屬性新增在例項自身
好處: 避免了每次例項化物件時,都創建出一模一樣的方法,節省記憶體
``` js
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的例項物件**
``` js
// 模擬父類
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)
```
![](https://img2020.cnblogs.com/blog/2117688/202102/2117688-20210228232057858-4284101