javascript原型與原型鏈

說到JavaScript的原型和原型鏈,相關文章已有很多,可是大都晦澀難懂。本文將換一個角度出發,先理解原型和原型鏈是什麼,有什麼做用,再去分析那些使人頭疼的關係。數組

1、引用類型皆爲對象

原型和原型鏈都是來源於對象而服務於對象的概念,因此咱們要先明確一點:bash

JavaScript中一切引用類型都是對象,對象就是屬性的集合。app

Array類型Function類型Object類型Date類型RegExp類型等都是引用類型。函數

也就是說 數組是對象、函數是對象、正則是對象、對象仍是對象。測試

2、原型和原型鏈是什麼

上面咱們說到對象就是屬性(property)的集合,有人可能要問不是還有方法嗎?其實方法也是一種屬性,由於它也是鍵值對的表現形式,具體見下圖。ui

能夠看到obj上確實多了一個sayHello的屬性,值爲一個函數,可是問題來了,obj上面並無hasOwnProperty這個方法,爲何咱們能夠調用呢?這就引出了 原型this

每個對象從被建立開始就和另外一個對象關聯,從另外一個對象上繼承其屬性,這個另外一個對象就是 原型spa

當訪問一個對象的屬性時,先在對象的自己找,找不到就去對象的原型上找,若是仍是找不到,就去對象的原型(原型也是對象,也有它本身的原型)的原型上找,如此繼續,直到找到爲止,或者查找到最頂層的原型對象中也沒有找到,就結束查找,返回undefinedprototype

這條由對象及其原型組成的鏈就叫作原型鏈。code

如今咱們已經初步理解了原型和原型鏈,到如今你們明白爲何數組均可以使用pushslice等方法,函數可使用callbind等方法了吧,由於在它們的原型鏈上找到了對應的方法。

OK,總結一下

  1. 原型存在的意義就是組成原型鏈:引用類型皆對象,每一個對象都有原型,原型也是對象,也有它本身的原型,一層一層,組成原型鏈。
  2. 原型鏈存在的意義就是繼承:訪問對象屬性時,在對象自己找不到,就在原型鏈上一層一層找。說白了就是一個對象能夠訪問其餘對象的屬性。
  3. 繼承存在的意義就是屬性共享:好處有二:一是代碼重用,字面意思;二是可擴展,不一樣對象可能繼承相同的屬性,也能夠定義只屬於本身的屬性。

3、建立對象

對象的建立方式主要有兩種,一種是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
複製代碼複製代碼

ObjectArray等稱爲構造函數,不要怕這個概念,構造函數和普通函數並無什麼不一樣,只是因爲這些函數常被用來跟在new後面建立對象。new後面調用一個空函數也會返回一個對象,任何一個函數均可以當作構造函數

因此構造函數更合理的理解應該是函數的構造調用

NumberStringBooleanArrayObjectFunctionDateRegExpError這些都是函數,並且是原生構造函數,在運行時會自動出如今執行環境中。

構造函數是爲了建立特定類型的對象,這些經過同一構造函數建立的對象有相同原型,共享某些方法。舉個例子,全部的數組均可以調用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
複製代碼複製代碼

總結一下構造函數用來建立對象,同一構造函數建立的對象,其原型相同。

4、__proto__與prototype

萬物逃不開真香定律,初步瞭解了相關知識,咱們也要試着來理解一下這些頭疼的單詞,而且看一下指來指去的箭頭了。

上面總結過,每一個對象都有原型,那麼咱們怎麼獲取到一個對象的原型呢?那就是對象的__proto__屬性,指向對象的原型。

上面也總結過,引用類型皆對象,因此引用類型都有__proto__屬性,對象有__proto__屬性,函數有__proto__屬性,數組也有__proto__屬性,只要是引用類型,就有__proto__屬性,都指向它們各自的原型對象。

__proto__屬性雖然在ECMAScript 6語言規範中標準化,可是不推薦被使用,如今更推薦使用Object.getPrototypeOfObject.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,慣例,總結一下

  1. 對象有__proto__屬性,函數有__proto__屬性,數組也有__proto__屬性,只要是引用類型,就有__proto__屬性,指向其原型。
  2. 只有函數有prototype屬性,只有函數有prototype屬性,只有函數有prototype屬性,指向new操做符加調用該函數建立的對象實例的原型對象。

5、原型鏈頂層

原型鏈之因此叫原型鏈,而不叫原型環,說明它是善始善終的,那麼原型鏈的頂層是什麼呢?

拿咱們的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
    }
}
複製代碼複製代碼

6、constructor

回憶一下以前的描述,構造函數都有一個prototype屬性,指向使用這個構造函數建立的對象實例的原型對象

這個原型對象中默認有一個constructor屬性,指回該構造函數。

Person.prototype.constructor === Person // true
複製代碼複製代碼

之因此開頭不說,是由於這個屬性對咱們理解原型及原型鏈並沒有太大幫助,反而容易混淆。

7、函數對象的原型鏈

以前提到過引用類型皆對象,函數也是對象,那麼函數對象的原型鏈是怎麼樣的呢?

對象都是被構造函數建立的,函數對象的構造函數就是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__屬性指向Functionprototype屬性

8、小試牛刀

真香警告!

有點亂?沒事,咱們先將以前的知識都總結一下,而後慢慢分析此圖:

知識點

  1. 引用類型都是對象,每一個對象都有原型對象。
  2. 對象都是由構造函數建立,對象的__proto__屬性指向其原型對象,構造函數的prototype屬性指向其建立的對象實例的原型對象,因此對象的__proto__屬性等於建立它的構造函數的prototype屬性。
  3. 全部經過字面量表示法建立的普通對象的構造函數爲Object
  4. 全部原型對象都是普通對象,構造函數爲Object
  5. 全部函數的構造函數是Function
  6. Object.prototype沒有原型對象

OK,咱們根據以上六點總結來分析上圖,先從左上角的f1f2入手:

// 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建立的o1o2下手:

// o一、o2構造函數爲Object
o1.__proto__ === Object.prototype
複製代碼複製代碼

最後對原生構造函數ObjectFunction下手:

// 原生構造函數也是函數對象,其構造函數爲Function
Object.__proto__ === Function.prototype
// 特例
Function.__proto__ === Function.prototype
複製代碼複製代碼

分析完畢,也沒有想象中那麼複雜是吧。

若是有內容引發不適,建議從頭看一遍,或者去看看參考文章內的文章。

9、觸類旁通

1. instanceof操做符

日常咱們判斷一個變量的類型會使用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 Objectarr 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屬性是否出如今左邊對象的原型鏈中的任何位置。其實它表示的是一種原型鏈繼承的關係。

2. Object.create

以前說對象的建立方式主要有兩種,一種是new操做符後跟函數調用,另外一種是字面量表示法。

其實還有第三種就是ES5提供的Object.create()方法,會建立一個新對象,第一個參數接收一個對象,將會做爲新建立對象的原型對象,第二個可選參數是屬性描述符(不經常使用,默認是undefined)。具體請查看Object.create()

咱們來模擬一個簡易版的Object.create

function createObj(proto) {
    function F() {}
    F.prototype = proto
    return new F()
}
複製代碼複製代碼

咱們日常所說的空對象,其實並非嚴格意義上的空對象,它的原型對象指向Object.prototype,還能夠繼承hasOwnPropertytoStringvalueOf等方法。

若是想要生成一個不繼承任何屬性的對象,可使用Object.create(null)

若是想要生成一個日常字面量方法生成的對象,須要將其原型對象指向Object.prototype

let obj = Object.create(Object.prototype)
// 等價於
let obj = {}
複製代碼複製代碼

3. new操做符

當咱們使用new時,作了些什麼?

  1. 建立一個全新對象,並將其__proto__屬性指向構造函數的prototype屬性。
  2. 將構造函數調用的this指向這個新對象,並執行構造函數。
  3. 若是構造函數返回對象類型Object(包含Functoin, Array, Date, RegExg, Error等),則正常返回,不然返回這個新的對象。

依然來模擬實現一下:

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
}
複製代碼複製代碼

4. Function.__proto__ === Function.prototype

其實這裏徹底不必去糾結雞生蛋仍是蛋生雞的問題,我本身的理解是:Function是原生構造函數,自動出如今運行環境中,因此不存在本身生成本身。之因此Function.__proto__ === Function.prototype,是爲了代表Function做爲一個原生構造函數,自己也是一個函數對象,僅此而已。

5. 真的是繼承嗎?

前面咱們講到每個對象都會從原型「繼承」屬性,實際上,繼承是一個十分具備迷惑性的說法,引用《你不知道的JavaScript》中的話,就是:

繼承意味着複製操做,然而 JavaScript 默認並不會複製對象的屬性,相反,JavaScript 只是在兩個對象之間建立一個關聯,這樣,一個對象就能夠經過委託訪問另外一個對象的屬性,因此與其叫繼承,委託的說法反而更準確些。

相關文章
相關標籤/搜索