JS(五)原型與原型鏈

在這篇文章里我們將要了解以下幾個方面:

  • 關于內存的那點事兒
  • 關于垃圾回收那點事兒
  • 包裝對象
  • 什么是原型
  • 什么是原型鏈
  • __ proto __和prototype

一、關于內存那點事兒

我們經(jīng)常會想,當我們聲明并賦值一個變量時,存儲的狀態(tài)是如何的,我們再行調用時又是一套什么操作呢?

我們來假設一下,如果計算機內存有2G,開機后,操作系統(tǒng)分配到了512M,瀏覽器分配到了1G,假設除去各種頁面html等等內存分配外,JS分配到了100M,那這100M如何分配?

是這樣的,它劃分了兩個區(qū),我們簡單稱其為代碼區(qū)和數(shù)據(jù)區(qū),代碼區(qū)來存儲代碼,數(shù)據(jù)區(qū)來存儲賦值的數(shù)據(jù)。

比如下列代碼:

var a = 1
// 代碼區(qū) 存儲a
// 數(shù)據(jù)區(qū) 存儲1

代碼區(qū)和數(shù)據(jù)區(qū)又存在著引用關系,比如我們只要訪問a,就可以讀取到1.

在JS中,每一個數(shù)據(jù)都需要一個內存空間。內存空間又被分為兩種,棧內存(stack)與堆內存(heap)。

數(shù)據(jù)區(qū)內也分了兩個區(qū),棧(stack)內存和堆(heap)內存,如下圖所示:

image

我們接下來就看一下數(shù)據(jù)區(qū)。

繼續(xù)重溫7種數(shù)據(jù)類型:
Number,String,Boolean,null,undefined:簡單類型數(shù)據(jù)存在stack內存中
Object:復雜類型數(shù)據(jù)存儲在heap內存中

JS里所有的數(shù)字都是以64位浮點數(shù)儲存的,16位存儲一個字符,所以在棧內存內都是64位01

我們來看看代碼:

//代碼
var a = 1
var b = 2
var c = true
// 代碼區(qū)              //stack
a                      0000…1…000000(64位浮點數(shù))
b                      0000…10…000000(64位浮點數(shù))
c                      100000000000……(64位浮點數(shù))

假設b=a,那就把a存的東西復制然后覆蓋到b儲存的地方。

再復雜一點,我們存儲復雜類型呢?也就是heap內存里是怎樣呢?

// 代碼
var o = {
    name:'vava'
    age:18
}
o.gender = female
var o2 = {
    name:'yaya'
}
// 代碼區(qū)              //stack                  //heap
   o                   ADDR 88                 88:name:'vava'  
                                                  age:18
                                                  gender:'female'
   o2                  ADDR 26                 26:name:'yaya'

就像上述代碼,當需要存儲字符的時候,一行64位浮點數(shù),只能存儲4個字符,非常浪費且再添加屬性的時候就需要整體移動下位的代碼,很麻煩,所以我們就在棧內存里存儲一個地址,地址隨意,但是這個地址指向heap內存里相應地址的位置,而我們需要儲存的內容就寫在這里。

如果上述代碼內,令o2=o, 那么同理,復制o的存儲內容覆蓋到o2,也就是,o2的地址26被88覆蓋,o2指向88,將引用88的內容。

上述代碼即下圖:


image

ps:沒有什么是畫個圖解決不了的 ~ 遇到內存空間問題,放心大膽的畫圖看看吧~

二、關于垃圾回收(GC)的那點事兒

1、Garbage Collection

我們已經(jīng)知道程序的運行需要內存。但是如果我們不再用到的內存呢?如果不釋放,內存占用越來越高,輕則影響系統(tǒng)性能,重則導致進程崩潰。所以我們頁面用完就要釋放內存,讓其重新分配。

如果一個對象沒有被引用,它就是垃圾,就會被回收。

// 代碼
var a = {name:'vava'}
var b = {name:'haha'}
// 代碼區(qū)                    // stack                  //Heap
    a                        ADDR 67                   67:vava
    b                        ADDR 17                   17:haha
// 當a = b 
//代碼區(qū)                     // stack                  //Heap
    a                        ADDR 17                   (67:vava 被回收)
    b                        ADDR 17                   17:haha

可以看到,一開始a指向地址67,b指向地址17,當a=b,b的地址覆蓋過來,a和b指向地址17,地址67不再被引用,所以被回收。

2、內存泄漏:不再用到的內存,沒有及時釋放,就叫做內存泄漏(memory leak)

即由于瀏覽器的一些bug,使得該被標記為垃圾回收的東西沒有被當做垃圾回收,內存始終被占用著,沒有被釋放。

比如我們有一個document.body.onclik事件占用著內存,沒有被清除,我們可以如下操作:

window.onunload = function(){
       document.body.onclik = null
}

注意,還有其他事件的話,其他事件都要=null。

3、深拷貝V.S淺拷貝

var a = 1
var b = a
    b = 2
    a
    // 1

像這樣,b改變不會影響a,這就是深拷貝

對于所有的基本類型,賦值都是深拷貝,所以我們來研究對象。

image
// 代碼區(qū)         // stack              //heap
    o             ADDR 9              9:name:'yaya'
    b             ADDR 9              // 也指向地址9

如上述,b.name = 'maya' ,改變name的值,是在地址9中完成,這時o和b都指向地址9,所以,引用a,得到的也是改變后的。

像這樣,b的改變會導致a的改變,就是淺拷貝。

三、包裝對象

我們都知道對象是一種復合值:它是屬性或已命名值得集合。當屬性值為一個函數(shù)時稱之為方法。那么我們來看一下下圖:

image

我們可以看到字符串也同樣具有屬性和方法,但字符串既不是對象,怎么會有屬性呢?

其實只要我們引用上述字符串的屬性,JavaScript就會將字符串值通過調用new String(s)的方式轉換成對象,之歌對象繼承了字符串的方法,處理屬性的引用。屬性引用結束,這個新創(chuàng)建的對象就被銷毀。

我們可以這樣來想,我們既想用簡單類型,又想獲得對象的方法,然后Branden Eich就想了個辦法,我們可以建立臨時對象,獲取屬性后返回給調用,然后銷毀就可以。

var n = 1
n.toString()
// '1'
// 代碼區(qū)             // stack               // heap
   n                     1
   temp                  ADDR 77             77: 1
                                                 toSting()
                                                 valueOf()

temp就是臨時對象,當調用toString返回后,temp馬上被銷毀了。

其他的數(shù)據(jù)類型也是一樣,

image

調取屬性的背后都是這樣一套操作。

四、什么是原型

從上一節(jié)包裝對象我們可以看到,利用new創(chuàng)建并初始化一個新對象,運算符new后面跟著一個函數(shù)調用,叫做構造函數(shù)。

var s = new String('hello')

如上述代碼,s就是被創(chuàng)建的實例對象,new運算符后跟著的String(注意這里開頭必須大寫來和string區(qū)分)就是構造函數(shù)。

我們可以看到從實例對象中調用的屬性,但是他們都具有的屬性比如toString以及valueOf等,如果每個實例對象都在heap存儲處生成這些都有的屬性豈不是很費內存?所以我們可以共有屬性歸攏起來,然后通過 __ proto__ 來指向共有屬性。

var o = new Object({name:'vava'})
//代碼區(qū)             // stack             //heap
   o                    ADDR 89           89: name:vava             object:toString()
                                              __proto__:object              valueOf()
                                                                            ……

所以在公有屬性調用時就是上述操作

image
image

通過在console.log打印出來,我們可以看到一個哈希,__ proto__指向了object去調用屬性。

object公有屬性是所有對象的公有屬性,但是還有一些比如是只有number數(shù)據(jù)類型共同具有的,或者只有String數(shù)據(jù)類型共同具有的呢?我們來看一下:

image
image
image

明白了么?也是一樣的操作去調用,只不過實例對象通過__ proto__先調用number公有屬性.如果沒有要調用的屬性,就繼續(xù)通過__ proto__調用object公有屬性。

看到這你可能想問了,說了這么多跟原型有什么關系呢?當然有了,公有屬性就是原型,每一個對象都在原型繼承屬性,所有的函數(shù)對象都具有原型對象,并且通過JavaScript代碼Object.prototype來獲得對屬性的引用。

image

看上圖就可以了解到,構造函數(shù)通過new運算符創(chuàng)建了實例對象,實例對象內通過__ proto__指向了原型對象即Object.prototype.

image

當你聲明了一個對象,JS引擎除了在棧里搞了一個哈希,還干了一件事,那就是把__ proto__指向了公有屬性,即原型。

五、什么是原型鏈

原型鏈:每一個對象都有自己的原型對象,原型對象本身也是對象,原型對象也有自己的原型對象,這樣就形成了一個鏈式結構,叫做原型鏈。

比如下面這個原型鏈

實例對象s---(通過__ proto__)-->String.prototype---(通過__ proto__)---->Object.prototype----(通過__ proto__)----->null

對這個實例化對象而言,訪問對象的屬性,是首先在對象本身去找,如果沒有,就會去他的原型對象中找,一直找到原型鏈的終點null;根據(jù)定義,null沒有原型,并作為這個原型鏈中的最后一個環(huán)節(jié)。

六、__ proto__和prototype

var 對象 = new 函數(shù)() , 故而

對象.__ proto__ === 函數(shù).prototype

__ proto __是實例對象的公有屬性引用,prototype是函數(shù)對象公有屬性的引用。

1、函數(shù).prototype.__ proto __ ===Object.prototype

image

這里我們循著原型鏈就可以理解,當函數(shù)是Object,它的原型對象自然指向了原型鏈的終點null。

2、函數(shù).__ proto __ === Function.prototype

image

Function是Object的構造函數(shù),所以函數(shù)Object或者函數(shù)Number等等,他們的__ proto __ 都指向了Function.prototype

從上圖中有清晰的對比,實例對象o的__ proto __ 就指向了構造實例對象函數(shù)的原型對象,即Object.prototype.而函數(shù)Object的__ proto __ 就指向了它的構造函數(shù)Function.prototype.

所以接下來兩個推論我們也很容易理解了:

3、Function.__ proto __ === Function.prototype

4、Function.prototype.__ proto __ === Object.prototype

(這里Function.prototype是對象,原型對象)

tips:
1、每一個構造函數(shù)都擁有一個prototype屬性,這個屬性指向一個對象,也就是原型對象。當使用這個構造函數(shù)創(chuàng)建實例的時候,prototype屬性指向的原型對象就成為實例的原型對象。
2、原型對象默認擁有一個constructor屬性,指向指向它的那個構造函數(shù)(也就是說構造函數(shù)和原型對象是互相指向的關系)。
3、每個對象都擁有一個隱藏的屬性[[prototype]],指向它的原型對象,這個屬性可以通過 Object.getPrototypeOf(obj)obj.__proto__ 來訪問。
4、實際上,構造函數(shù)的prototype屬性與它創(chuàng)建的實例對象的[[prototype]]屬性指向的是同一個對象,即 對象.__proto__ === 函數(shù).prototype
5、如上文所述,原型對象就是用來存放實例中共有的那部分屬性。
6、在JavaScript中,所有的對象都是由它的原型對象繼承而來,反之,所有的對象都可以作為原型對象存在。
7、訪問對象的屬性時,JavaScript會首先在對象自身的屬性內查找,若沒有找到,則會跳轉到該對象的原型對象中查找。

就到這里啦~ 歡迎糾錯 ~

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容