看了就懂系列之 Prototype

寫這篇文章的目的

1.夯實JS基礎

2.分享

想學好 js , 不懂原型,是不行的,因為 js 是一門基於原型的語言

要弄懂原型,我推薦從定義入手,本文參考來自當前 web 文檔最權威的 MDN,英文好的同學可以直接看文檔簡單粗暴,地址在文章尾部。

不過基本都不夠好吧,可能也沒耐心和時間,所以我寫了這篇文章,耐心看完月薪2萬,指日可待哦。


正文

本文內容涉及到 prototype,__proto__ , 原型鏈。

一個合格的程序員,要有耐心,請留下至少一個小時的完整時間來看這篇文章。看完月薪兩萬,想想是不是就很開心了。

開始

prototype

prototype 中文意思為 原型

看下 MDN 中對 prototype 的定義

Each object has a private property which holds a link to another object called its prototype

翻譯:每個對象都有一個私有屬性,指向另一個對象,這個對象被稱為它的原型

那麼現在舉個例子,有一個對象A , 他有一個私有屬性,指向另一個對象B , 那麼這個對象B稱為對象A的原型 。

所以可以變成這樣 每一對象都有一個私有屬性,指向該對象的原型

對象.私有屬性 ----> 對象的原型

那麼現在來看這個私有屬性,它是如何表示的呢

  1. 早期用 [[prototype]] 來表示這個私有屬性

2. 後來 ES5 出了 Object.getPrototypeOf 這個函數來表示這個屬性

3. 還有一個非標準的表示法,就是 __proto__,不過事實上已被大部分瀏覽器實現了,連IE 的Edge都實現了。

替換掉這個私有屬性就是這樣子

對象.__proto__ ----> 對象的原型 或者Object.getPrototypeOf(對象) ----> 對象的原型

所以可以變成這樣,每一對象都有一個屬性 __proto__ ,指向該對象的原型

如果跟上了思路,那可以繼續下面,沒有的話仔細再看一遍上面的。

接下來記住三點

1 . 對象都有一個屬性 __proto__

2 . 函數都有一個屬性 prototype

3 . 對象的 _proto__ 屬性指向該對象的構造函數的prototype屬性

function Dog(){} 首先有一個函數Doglet dog = new Dog() 以Dog為構造函數new了一個對象dog,所以Dog是dog的構造函數console.log(dog.__proto__ === Dog.prototype)//true 對象dog的原型為Dog.prototype

好,現在思考一個很簡單的問題

什麼是原型?思考一下,最好能用自己的語言描述出來 。

我的回答是,原型是一個對象,通過對象的 __proto__ 屬性可以訪問到這個對象,或者對象的構造函數的 prototype 屬性也可以訪問的到,而構造函數的 prototype 屬性只是剛好叫這個名字,真正的原型是該屬性對應的對象。

原型鏈

接下來要知道一個概念了 。 原型鏈

MDN 中對它的描述

Each object has a private property which holds a link to another object called its prototype. That prototype object has a prototype of its own, and so on until an object is reached with null as its prototype. By definition, null has no prototype, and acts as the final link in this prototype chain.

我的理解是

每個對象都有自己的原型,原型對象也有自己的原型,直到原型對象為 null 為止,由這些原型對象連接而成的類似於鏈式的結構就是原型鏈

接下來我們看一些例子,來加深對原型和原型鏈的理解

let o = {a: 1} o的原型鏈是這樣子的 o是由Object構造函數構造出來的o ---> Object.prototype ---> nulllet b = [yo] b的原型鏈是這樣子的 b是數組,由Array構造函數構造出來的,Array的原型是由Object構造函數構造出來的b ---> Array.prototype ---> Object.prototype ---> nullfunction f(){} f的原型鏈是這樣子的 f是函數,由Function構造函數構造出來的,Function的原型是由Object構造函數構造出來的f ---> Function.prototype ---> Object.prototype ---> null

繼承

原型的作用是實現繼承,對了,有篇文章介紹了 js 的歷史,和為什麼引入原型,看了月薪上萬哦。地址: ruanyifeng.com/blog/201

繼承的原理是這樣的

MDN 中對它的描述

When trying to access a property of an object, the property will not only be sought on the object but on the prototype of the object, the prototype of the prototype, and so on until either a property with a matching name is found or the end of the prototype chain is reached.

翻譯過來就是,當試圖訪問一個對象的屬性時,不僅要在對象上尋找屬性,還要在對象的原型,原型的原型等等上尋找屬性,直到找到具有匹配名稱的屬性或者到達原型鏈的尾部

來看一些例子

o = {a: 1, b: 2} 假設現在有個對象o , o有兩個自己的屬性a和bo.__proto__ = {b: 3, c: 4} o.__proto__是個對象,這個對象有屬性b和co.__proto__.__proto__ == null 整個原型鏈看起來就是下面這樣的 {a: 1, b: 2} ---> {b: 3, c: 4} ---> null console.log(o.a); // 1 o有個自己的屬性a,值為1 console.log(o.b); // 2 o有個自己的屬性b,值為2,這裡要注意以下,在o的原型鏈上也有一個b屬性,值為3,由於屬性先是在o中被找到,所以值為2,這個現象叫做 "property shadowing." 屬性陰影 console.log(o.c); // 4 o自身沒有屬性c,查找o的原型鏈,找到c,值為4 console.log(o.d); // undefined o自身沒有屬性d,查找o的原型鏈,直到最後都沒有找到,停止查詢,返回undefined

如果上面例子看懂了,那麼下面這個例子你也就知道為什麼了

var o = {}; console.log(o.a) // undefined console.log(o.toString()) // [object Object]

o . a 為 undefined,是因為 o 本身沒有 a 這個屬性,o 的原型鏈上也沒有,所以返回undefined

o . toString() 為 [object Object] 是因為 o 本身雖然沒有 toString 這個方法,o 的原型鏈上卻有,Object.prototype 上默認就有這個方法。

接下來,先回憶下這句話,對象的 __proto__ 屬性指向該對象的構造函數的prototype屬性

繼承方案一

然後看一下繼承的一個簡單場景

function Animal(){} //首先聲明了一個Animal函數 Animal.prototype.say = function(){ console.log(hi) } //然後在函數Animal的prototype屬性上添加了一個方法say let dog = new Animal() //以Animal為構造函數實例化出一個對象賦值給dog dog.say() //hi 思考 : 這裡輸出hi是因為dog擁有say方法嗎

思考一下,有助於理解原型和原型鏈

敲黑板,注意啦,這裡的 dog 仍然是個空對象,因為 dog 的原型上有 say 方法,所以才可以輸出 hi,記住了。

繼承方案二

這裡做個對比,如果不使用 prototype,如何實現繼承

function Animal(){ this.say = function(){ console.log(hi) }} let dog = new Animal() dog.say() // hi 思考 : 這裡輸出hi是因為dog擁有say方法嗎

這裡的 dog 現在不是一個空對象啦,dog 真的擁有 say 方法了,可是我沒有看到

dog.say =function(){ console.log(hi) }

這句代碼啊,實際上,是因為 let dog = new Animal ( ) 這句代碼,js 實際上做了三件事 。

1 var dog = new Object() //創建一個空對象,叫做dog 2 dog.__proto__ = Animal.prototype //讓對象dog的__proto__屬性指向函數Animal的prototype屬性 3 Animal.call(dog) //call這裡就不細說了,這句話的意思是執行一次Animal函數,並且指定Animal函數中的this的值為dog,所以過程是這樣的,Animal函數執行,裡面的this改為dog,所以執行了dog.say=function{console.log(hi)}這句話,所以,dog就有了say方法。

現在這兩種方法都實現了繼承,一種是在函數的 prototype 屬性上添加屬性或者方法,一種是在函數中直接添加 。

那這兩種的差別是什麼,很簡單,敲黑板 。

在 prototype 上添加的一般是讓實例共享的屬性和方法,如果這些屬性和方法在函數中添加,那麼就會使得每個實例本身上就擁有這些屬性和方法,非常的佔用內存,在 prototype 上添加的話,只要讓實例在它們的原型中訪問就可以得到,這樣就實現了繼承,這也是當初 Brendan Eich 設計 js 時繼承這塊不引入類,而引入 prototype 的原因。


constructor

最後還有一點點小細節,快結束啦。堅持住,這是你拿 2萬 薪水和拿 5k 區別 。哈哈 。

大家有沒有見過類似這樣的情況,一個函數繼承另一個函數

1 function vehicle(){} // 函數vehicle 2 function car(){} // 函數car 3 car.prototype = Object.create(vehicle.prototype) // Object.create的用法我簡單說一下,使用指定的原型對象創建一個新的對象。第一個參數為指定的原型對象。這句代碼讓函數car的prototype屬性等於一個對象,這個對象的內容為函數vehicle的prototype屬性,這樣後以函數car為構造函數的實例的原型就是函數vehicle的prototype了,實現了繼承。 4 car.prototype.constructor = car // 為什麼要寫這句話 ?

重點是這最後一句話,我以前一直沒有懂,為什麼要寫這句話,我試過,沒有這句話,因為實例化對象的時候有個 new , 所以繼承的功能一樣都能實現 。

後來我查閱了資料,和自己動手實踐了一下,終於弄懂了。

加這句話後,輸出構造函數 car 的實例

圖1 構造函數 car 的實例

不加這句話的話,輸出構造函數 car 的實例

圖2 構造函數 car 的實例

對比兩個情況,發現黑線地方是不一樣的

現在我們先不寫 3 、4 句代碼,只寫 1 、2 句,輸出函數 car 的 prototype 屬性,結果

1 function vehicle() {} // 函數 vehicle2 function car() {} // 函數 car console.log(car.prototype) // 輸出函數 car 的 prototype 屬性

發現這個函數 car 的 prototype 屬性是一個對象, 有兩個屬性

1 constructor 函數默認有的屬性,指向函數本身

2 __proto__ 對象默認有的屬性,指向該對象的構造函數的 prototype 屬性

3 car.prototype = Object.create(vehicle.prototype)

當我執行這句代碼後,函數 car 的 prototype 屬性被重寫,輸出函數 car 的 prototype 屬性

發現這個函數 car 的 prototype 屬性指向的對象,只剩下一個__proto__ 屬性了 。

因為這種賦值方式是字面量的形式,原來的對象直接被覆蓋了,所以添加了第 4 句代碼後,

4 car.prototype.constructor = car

函數 car 的 prototype 屬性指向的對象又擁有了 constructor 屬性 。

可是就像我之前說的,這句話不加,也是可以實現繼承的,為什麼要多此一舉加上呢 ?

這時候對比一下上面的圖 1 , 和圖 2

圖 1 構造函數 car 的實例 .constructor // car圖 2 構造函數 car 的實例 .constructor // vehicle

是不是發現問題了 。

constructor 屬性默認指向該對象的構造函數

這裡引用@賀師俊 的回答

constructor其實沒有什麼用處,只是JavaScript語言設計的歷史遺留物。由於constructor屬性是可以變更的,所以未必真的指向對象的構造函數,只是一個提示。不過,從編程習慣上,我們應該盡量讓對象的constructor指向其構造函數,以維持這個慣例。

到這裡我才明確的理解了,加第 4 句代碼的作用

4 car.prototype.constructor = car

最後為了檢驗閱讀效果,思考兩個問題

1 . 什麼是原型

2 . 原型的作用

文中都有涉及,一定要有自己的理解哦 。

本文參考 MDN

地址

Inheritance and the prototype chain?

developer.mozilla.org圖標

哦,對了,月更。


推薦閱讀:

前端日刊-2017.11.14
前端日刊-2018.02.08
前端指紋,尋找蛛絲馬跡(技術周刊 2018-02-23)
前端日刊-2017.12.7
奇舞周刊第 246 期: 無障礙設計 & 前端數據流哲學

TAG:前端工程師 | 前端入門 |