說到JavaScript的原型和原型鏈,相關文章已有很多,可是大都晦澀難懂。本文將換一個角度出發,先理解原型和原型鏈是什麼,有什麼做用,再去分析那些使人頭疼的關係。數組
原型和原型鏈都是來源於對象而服務於對象的概念,因此咱們要先明確一點:bash
JavaScript中一切引用類型都是對象,對象就是屬性的集合。app
Array類型
、Function類型
、Object類型
、Date類型
、RegExp類型
等都是引用類型。函數
也就是說 數組是對象、函數是對象、正則是對象、對象仍是對象。測試
上面咱們說到對象就是屬性(property)的集合,有人可能要問不是還有方法嗎?其實方法也是一種屬性,由於它也是鍵值對
的表現形式,具體見下圖。ui
能夠看到obj
上確實多了一個sayHello
的屬性,值爲一個函數,可是問題來了,obj
上面並無hasOwnProperty
這個方法,爲何咱們能夠調用呢?這就引出了 原型。this
每個對象從被建立開始就和另外一個對象關聯,從另外一個對象上繼承其屬性,這個另外一個對象
就是 原型。spa
當訪問一個對象的屬性時,先在對象的自己找,找不到就去對象的原型上找,若是仍是找不到,就去對象的原型(原型也是對象,也有它本身的原型)的原型上找,如此繼續,直到找到爲止,或者查找到最頂層的原型對象中也沒有找到,就結束查找,返回undefined
。prototype
這條由對象及其原型組成的鏈就叫作原型鏈。code
如今咱們已經初步理解了原型和原型鏈,到如今你們明白爲何數組均可以使用push
、slice
等方法,函數可使用call
、bind
等方法了吧,由於在它們的原型鏈上找到了對應的方法。
OK,總結一下:
對象的建立方式主要有兩種,一種是new
操做符後跟函數調用,另外一種是字面量表示法。
目前咱們如今能夠理解爲:全部對象都是由new
操做符後跟函數調用來建立的,字面量表示法只是語法糖(即本質也是new
,功能不變,使用更簡潔)。
// new操做符後跟函數調用
let obj = new Object()
let arr = new Array()
// 字面量表示法
let obj = { a: 1}
// 等同於
let obj = new Object()
obj.a = 1
let arr = [1,2]
// 等同於
let arr = new Array()
arr[0] = 1
arr[1] = 2
複製代碼複製代碼
Object
、Array
等稱爲構造函數,不要怕這個概念,構造函數和普通函數並無什麼不一樣,只是因爲這些函數常被用來跟在new
後面建立對象。new
後面調用一個空函數也會返回一個對象,任何一個函數均可以當作構造函數。
因此構造函數更合理的理解應該是函數的構造調用
。
Number
、String
、Boolean
、Array
、Object
、Function
、Date
、RegExp
、Error
這些都是函數,並且是原生構造函數,在運行時會自動出如今執行環境中。
構造函數是爲了建立特定類型的對象,這些經過同一構造函數建立的對象有相同原型,共享某些方法。舉個例子,全部的數組均可以調用push
方法,由於它們有相同原型。
咱們來本身實現一個構造函數:
// 慣例,構造函數應以大寫字母開頭
function Person(name) {
// 函數內this指向構造的對象
// 構造一個name屬性
this.name = name
// 構造一個sayName方法
this.sayName = function() {
console.log(this.name)
}
}
// 使用自定義構造函數Person建立對象
let person = new Person('logan')
person.sayName() // 輸出:logan
複製代碼複製代碼
總結一下:構造函數用來建立對象,同一構造函數建立的對象,其原型相同。
萬物逃不開真香定律,初步瞭解了相關知識,咱們也要試着來理解一下這些頭疼的單詞,而且看一下指來指去的箭頭了。
上面總結過,每一個對象都有原型,那麼咱們怎麼獲取到一個對象的原型呢?那就是對象的__proto__
屬性,指向對象的原型。
上面也總結過,引用類型皆對象,因此引用類型都有__proto__
屬性,對象有__proto__
屬性,函數有__proto__
屬性,數組也有__proto__
屬性,只要是引用類型,就有__proto__
屬性,都指向它們各自的原型對象。
__proto__
屬性雖然在ECMAScript 6語言規範中標準化,可是不推薦被使用,如今更推薦使用Object.getPrototypeOf
,Object.getPrototypeOf(obj)
也能夠獲取到obj
對象的原型。本文中使用__proto__
只是爲了便於理解。
Object.getPrototypeOf(person) === person.__proto__ // true
複製代碼複製代碼
上面說過,構造函數是爲了建立特定類型的對象,那若是我想讓Person
這個構造函數建立的對象都共享一個方法,總不能像下面這樣吧:
錯誤示範
// 調用構造函數Person建立一個新對象personA
let personA = new Person('張三')
// 在personA的原型上添加一個方法,以供以後Person建立的對象所共享
personA.__proto__.eat = function() {
console.log('吃東西')
}
let personB = new Person('李四')
personB.eat() // 輸出:吃東西
複製代碼複製代碼
可是每次要修改一類對象的原型對象,都去建立一個新的對象實例,而後訪問其原型對象並添加or修改屬性總以爲畫蛇添足。既然構造函數建立的對象實例的原型對象都是同一個,那麼構造函數和其構造出的對象實例的原型對象之間有聯繫就完美了。
這個聯繫就是prototype
。每一個函數擁有prototype
屬性,指向使用new
操做符和該函數建立的對象實例的原型對象。
Person.prototype === person.__proto__ // true
複製代碼複製代碼
看到這裏咱們就明白了,若是想讓Person
建立出的對象實例共享屬性,應該這樣寫:
正確示範
Person.prototype.drink = function() {
console.log('喝東西')
}
let personA = new Person('張三')
personB.drink() // 輸出:喝東西
複製代碼複製代碼
OK,慣例,總結一下:
__proto__
屬性,函數有__proto__
屬性,數組也有__proto__
屬性,只要是引用類型,就有__proto__
屬性,指向其原型。prototype
屬性,只有函數有prototype
屬性,只有函數有prototype
屬性,指向new
操做符加調用該函數建立的對象實例的原型對象。原型鏈之因此叫原型鏈,而不叫原型環,說明它是善始善終的,那麼原型鏈的頂層是什麼呢?
拿咱們的person
對象來看,它的原型對象,很簡單
// 1. person的原型對象
person.__proto__ === Person.prototype
複製代碼複製代碼
接着往上找,Person.prototype
也是一個普通對象,能夠理解爲Object
構造函數建立的,因此得出下面結論,
// 2. Person.prototype的原型對象
Person.prototype.__proto__ === Object.prototype
複製代碼複製代碼
Object.prototype
也是一個對象,那麼它的原型呢?這裏比較特殊,切記!!!
Object.prototype.__proto__ === null
複製代碼複製代碼
咱們就能夠換個方式描述下 原型鏈 :由對象的__proto__
屬性串連起來的直到Object.prototype.__proto__
(爲null
)的鏈就是原型鏈。
在上面內容的基礎之上,咱們來模擬一下js引擎讀取對象屬性:
function getProperty(obj, propName) {
// 在對象自己查找
if (obj.hasOwnProperty(propName)) {
return obj[propName]
} else if (obj.__proto__ !== null) {
// 若是對象有原型,則在原型上遞歸查找
return getProperty(obj.__proto__, propName)
} else {
// 直到找到Object.prototype,Object.prototype.__proto__爲null,返回undefined
return undefined
}
}
複製代碼複製代碼
回憶一下以前的描述,構造函數都有一個prototype
屬性,指向使用這個構造函數建立的對象實例的原型對象。
這個原型對象中默認有一個constructor
屬性,指回該構造函數。
Person.prototype.constructor === Person // true
複製代碼複製代碼
之因此開頭不說,是由於這個屬性對咱們理解原型及原型鏈並沒有太大幫助,反而容易混淆。
以前提到過引用類型皆對象,函數也是對象,那麼函數對象的原型鏈是怎麼樣的呢?
對象都是被構造函數建立的,函數對象的構造函數就是Function
,注意這裏F
是大寫。
let fn = function() {}
// 函數(包括原生構造函數)的原型對象爲Function.prototype
fn.__proto__ === Function.prototype // true
Array.__proto__ === Function.prototype // true
Object.__proto__ === Function.prototype // true
複製代碼複製代碼
Function.prototype
也是一個普通對象,因此Function.prototype.__proto__ === Object.prototype
這裏有一個特例,Function
的__proto__
屬性指向Function.prototype
。
總結一下:函數都是由Function
原生構造函數建立的,因此函數的__proto__
屬性指向Function
的prototype
屬性
真香警告!
有點亂?沒事,咱們先將以前的知識都總結一下,而後慢慢分析此圖:
知識點
__proto__
屬性指向其原型對象,構造函數的prototype
屬性指向其建立的對象實例的原型對象,因此對象的__proto__
屬性等於建立它的構造函數的prototype
屬性。Object
Object
Function
Object.prototype
沒有原型對象OK,咱們根據以上六點總結來分析上圖,先從左上角的f1
、f2
入手:
// f一、f2都是經過new Foo()建立的對象,構造函數爲Foo,因此有
f1.__proto__ === Foo.prototype
// Foo.prototype爲普通對象,構造函數爲Object,因此有
Foo.prototype.__proto === Object.prototype
// Object.prototype沒有原型對象
Object.prototype.__proto__ === null
複製代碼複製代碼
而後對構造函數Foo
下手:
// Foo是個函數對象,構造函數爲Function
Foo.__proto__ === Function.prototype
// Function.prototype爲普通對象,構造函數爲Object,因此有
Function.prototype.__proto__ === Object.prototype
複製代碼複製代碼
接着對原生構造函數Object
建立的o1
、o2
下手:
// o一、o2構造函數爲Object
o1.__proto__ === Object.prototype
複製代碼複製代碼
最後對原生構造函數Object
和Function
下手:
// 原生構造函數也是函數對象,其構造函數爲Function
Object.__proto__ === Function.prototype
// 特例
Function.__proto__ === Function.prototype
複製代碼複製代碼
分析完畢,也沒有想象中那麼複雜是吧。
若是有內容引發不適,建議從頭看一遍,或者去看看參考文章內的文章。
日常咱們判斷一個變量的類型會使用typeof
運算符,可是引用類型並不適用,除了函數對象會返回function
外,其餘都返回object
。咱們想要知道一個對象的具體類型,就須要使用到instanceof
。
let fn = function() {}
let arr = []
fn instanceof Function // true
arr instanceof Array // true
fn instanceof Object // true
arr instanceof Object // true
複製代碼複製代碼
爲何fn instanceof Object
和arr instanceof Object
都返回true
呢?咱們來看一下MDN上對於instanceof
運算符的描述:
instanceof運算符用於測試構造函數的prototype屬性是否出如今對象的原型鏈中的任何位置
也就是說instanceof
操做符左邊是一個對象,右邊是一個構造函數,在左邊對象的原型鏈上查找,知道找到右邊構造函數的prototype屬性就返回true
,或者查找到頂層null
(也就是Object.prototype.__proto__
),就返回false
。 咱們模擬實現一下:
function instanceOf(obj, Constructor) { // obj 表示左邊的對象,Constructor表示右邊的構造函數
let rightP = Constructor.prototype // 取構造函數顯示原型
let leftP = obj.__proto__ // 取對象隱式原型
// 到達原型鏈頂層還未找到則返回false
if (leftP === null) {
return false
}
// 對象實例的隱式原型等於構造函數顯示原型則返回true
if (leftP === rightP) {
return true
}
// 查找原型鏈上一層
return instanceOf(obj.__proto__, Constructor)
}
複製代碼複製代碼
如今就能夠解釋一些比較使人費解的結果了:
fn instanceof Object //true
// 1. fn.__proto__ === Function.prototype
// 2. fn.__proto__.__proto__ === Function.prototype.__proto__ === Object.prototype
arr instanceof Object //true
// 1. arr.__proto__ === Array.prototype
// 2. arr.__proto__.__proto__ === Array.prototype.__proto__ === Object.prototype
Object instanceof Object // true
// 1. Object.__proto__ === Function.prototype
// 2. Object.__proto__.__proto__ === Function.prototype.__proto__ === Object.prototype
Function instanceof Function // true
// Function.__proto__ === Function.prototype
複製代碼複製代碼
總結一下:instanceof
運算符用於檢查右邊構造函數的prototype
屬性是否出如今左邊對象的原型鏈中的任何位置。其實它表示的是一種原型鏈繼承的關係。
以前說對象的建立方式主要有兩種,一種是new
操做符後跟函數調用,另外一種是字面量表示法。
其實還有第三種就是ES5提供的Object.create()
方法,會建立一個新對象,第一個參數接收一個對象,將會做爲新建立對象的原型對象,第二個可選參數是屬性描述符(不經常使用,默認是undefined
)。具體請查看Object.create()。
咱們來模擬一個簡易版的Object.create
:
function createObj(proto) {
function F() {}
F.prototype = proto
return new F()
}
複製代碼複製代碼
咱們日常所說的空對象,其實並非嚴格意義上的空對象,它的原型對象指向Object.prototype
,還能夠繼承hasOwnProperty
、toString
、valueOf
等方法。
若是想要生成一個不繼承任何屬性的對象,可使用Object.create(null)
。
若是想要生成一個日常字面量方法生成的對象,須要將其原型對象指向Object.prototype
:
let obj = Object.create(Object.prototype)
// 等價於
let obj = {}
複製代碼複製代碼
當咱們使用new
時,作了些什麼?
__proto__
屬性指向構造函數的prototype
屬性。依然來模擬實現一下:
function newOperator(func, ...args) {
if (typeof func !== 'function') {
console.error('第一個參數必須爲函數,您傳入的參數爲', func)
return
}
// 建立一個全新對象,並將其`__proto__`屬性指向構造函數的`prototype`屬性
let newObj = Object.create(func.prototype)
// 將構造函數調用的this指向這個新對象,並執行構造函數
let result = func.apply(newObj, args)
// 若是構造函數返回對象類型Object,則正常返回,不然返回這個新的對象
return (result instanceof Object) ? result : newObj
}
複製代碼複製代碼
其實這裏徹底不必去糾結雞生蛋仍是蛋生雞的問題,我本身的理解是:Function
是原生構造函數,自動出如今運行環境中,因此不存在本身生成本身。之因此Function.__proto__ === Function.prototype
,是爲了代表Function
做爲一個原生構造函數,自己也是一個函數對象,僅此而已。
前面咱們講到每個對象都會從原型「繼承」屬性,實際上,繼承是一個十分具備迷惑性的說法,引用《你不知道的JavaScript》中的話,就是:
繼承意味着複製操做,然而 JavaScript 默認並不會複製對象的屬性,相反,JavaScript 只是在兩個對象之間建立一個關聯,這樣,一個對象就能夠經過委託訪問另外一個對象的屬性,因此與其叫繼承,委託的說法反而更準確些。