【JS基礎】原型物件的那些事(一)

諾頓發表於2019-05-05

談起js的基礎,繞不過去的坎就是:原型鏈、作用域鏈、this(em...好吧,還有閉包),今天總結一下關於原型物件的一些知識,供自己和大家複習。

概念理解

什麼是原型物件呢?有以下幾點:

1.建構函式有一個prototype屬性,指向建構函式的原型物件。而例項有一個__proto__屬性,也指向原型物件。

PS: 準確的說,例項指向原型物件的是[[Prototype]]屬性,但這是一個隱式屬性,指令碼不可訪問。因此瀏覽器廠商提供了一個屬性__proto__,用來顯式指向原型物件,但它並不是ECMA規範。

注意函式的是prototype屬性,例項的是__proto__屬性,不要弄錯。

舉個例子,我們有一個建構函式Person:

function Person(name) {
    this.name = name
}
複製程式碼

這時,我們建立一個Person的例項person

var person = new Person("張三")
複製程式碼

按照上邊的理論,就可以表示為:

Person.prototype === person.__proto__

他們指向的都是原型物件。

2.通過同一個建構函式例項化的多個例項物件具有同一個原型物件。

var person1 = new Person("張三")
var person2 = new Person("李四")
複製程式碼

person1.__proto__person2.__proto__Person.prototype 他們是兩兩相等的。

3.原型物件有一個constructor屬性,指向該原型物件對應的建構函式。

    Person.prototype.constructor === Person
    person.__proto__.constructor === Person
複製程式碼

4.例項物件本身並沒有constructor屬性,但它可以繼承原型物件的constructor屬性。

    person1.constructor === Person
    person2.constructor === Person
複製程式碼

作用

OK,說清楚了什麼是原型,就要說一下這玩意是幹嘛用的,為啥要在建構函式里加這麼個東西。

還是以建構函式Person為例,稍微改一下:

function Person(name) {
    this.name = name
    this.sayName = function() {
        console.log(this.name)
    }
}

var person1 = new Person("張三")
var person2 = new Person("李四")
複製程式碼

我們在建構函式Person中增加了一個方法sayName,這樣Person的例項person1person2各自都有了一個sayName方法。

注意,我說的是各自,什麼意思呢?就是說每次建立一個例項,就要在記憶體中建立一個sayName方法,這些sayName並不是同一個sayName

    person1.sayName === person2.sayName 
    
    -> false
複製程式碼

多個例項重複建立相同的方法,這顯然是浪費資源的。這個時候,我們的原型物件登場了。假如建構函式中的方法我們這樣寫:

function Person(name) {
    this.name = name
}

Person.prototype.sayName = function() {
    console.log(this.name)
}

var person1 = new Person("張三")
var person2 = new Person("李四")
複製程式碼

和之前的區別是,我們將sayName方法寫到了建構函式的原型物件上,而不是寫在建構函式裡。

這裡要先提一個概念,就是當物件找屬性或者方法時,先在自己身上找,找到就呼叫。在自己身上找不到時,就會去他的原型物件上找。這就是原型鏈的概念,先點到這,大家知道這件事就可以了。

還記得之前說的嗎:

通過同一個建構函式例項化的多個例項物件具有同一個原型物件

person1person2上顯然是沒有sayName方法的,但是他們的原型物件有啊。

所以這裡的person1.sayNameperson2.sayName,實際上都是繼承自他原型物件上的sayName方法,既然原型物件是同一個,那sayName方法自然也是同一個了,所以此時:

    person1.sayName === person2.sayName   
    
    -> true
複製程式碼

將需要共享的方法和屬性放到原型物件上,例項在呼叫這些屬性和方法時,不用每次都建立,從而節約資源,這就是原型物件的作用。

共享帶來的“煩惱”

但是,既然是共享,就有一點問題了,還是Person建構函式,我們再改造一下。

    function Person(name) {
        this.name = name
    }
    
    Person.prototype.ageList = [12, 16, 18]
   
    var person1 = new Person("張三")
    var person2 = new Person("李四")
複製程式碼

這個時候,我們在person1上做一些操作:

    person1.ageList.push(30)
複製程式碼

看一下此時person2.ageList是什麼:

    person2.ageList 
    
    -> [12, 16, 18, 30]
複製程式碼

有點意思,person2上的ageList也多了30。

原因其實還是因為共享。

共享不好的地方就是:一個例項對引用型別(陣列、物件、函式)的屬性進行修改,會導致原型物件上的屬性修改(其實修改的就是原型物件上的屬性,例項是沒有這個屬性的),進而導致所有的例項中,這個屬性都改了!

很顯然,大部分時候,我們喜歡共享,可以節約資源。但是不喜歡每一個例項都受影響,要不還建立不同的例項幹嘛,用一個不就好了(攤手)。

所以,我們需要把那些需要共享的屬性和方法,寫在原型物件上,而每個例項單獨用的、不希望互相影響的屬性,就寫在建構函式裡邊。類似這樣:

function Person(name) {
    this.name = name
    this.ageList = [12, 16, 18]
}

var person1 = new Person("張三")
var person2 = new Person("李四")

person1.ageList.push(30)

person1.ageList 
-> [12, 16, 18, 30]

person2.ageList 
-> [12, 16, 18]
複製程式碼

此處有坑

關於原型物件,還有兩個坑,需要和大家說一下。

    function Person(name) {
        this.name = name
    }
    
    Person.prototype.ageList = [12, 16, 18]
   
    var person1 = new Person("張三")
    var person2 = new Person("李四")
    
    person1.ageList.push(30)
    
    person2.ageList 
    -> [12, 16, 18, 30]
複製程式碼

這個沒毛病,但是假如我把操作 person1.ageList.push(30) 改為 person1.ageList = [1, 2, 3],結果會怎樣呢?

    person2.ageList 
    
    -> [12, 16, 18]
複製程式碼

這裡就奇怪了,都是對person1.ageList進行操作,怎麼就不一樣呢?

其實原因在於,person1.ageList = [1, 2, 3]是一個賦值操作。

我們說過,person1本身是沒有ageList屬性的,而賦值操作,會給person1增加自己的ageList屬性。既然自己有了,也就不用去原型物件上找了。這個時候,原型物件的ageList其實是沒有變化的。而person2沒有自己的ageList屬性,所以person2.ageList還是繼承自原型,就是[12, 16, 18]

    function Person(name) {
        this.name = name
    }
    
    Person.prototype = {
        ageList : [12, 16, 18]
    }
   
    var person1 = new Person("張三")
    var person2 = new Person("李四")
    
    person1.ageList.push(30)
    
    person2.ageList -> [12, 16, 18, 30]
複製程式碼

這裡依然沒毛病,但是寫法上有一個變化:我們不再採用Person.prototype.ageList = [12, 16, 18]的形式賦值,而是給Person.prototype賦值了一個物件,物件中有ageList

這樣看貌似沒有問題,用起來也都一樣:改變person1.ageListperson2.ageList也變化了,說明person1.ageListperson2.ageList還是繼承自同一個原型物件。

但是,這裡有一個問題,之前我們說過:

例項物件本身並沒有constructor屬性,但它可以繼承原型物件的constructor屬性

但是此時

    person1.constructor === Person 
    -> false
    
    person2.constructor === Person 
    -> false
複製程式碼

為什麼呢?

因為通過給Person.prototype賦值一個物件,就修改了原型物件的指向,此時原型物件的constructor指向內建建構函式Object了,使用Person.prototype.ageList = [12, 16, 18]的形式賦值,就不會造成這樣的問題。

所以當給原型物件賦值一個新物件時,切記將原型物件的constructor指回原建構函式:

    Person.prototype.constructor = Person
複製程式碼

以上就是本次分享的內容,關於原型物件的其他知識,下一篇JS基礎—原型物件的那些事(二)會講到。

相關文章