1. 程式人生 > >一文讓你對js的原型與原型鏈不再害怕、迷惑

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

[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