JavaScript由文檔對(duì)象模型DOM、瀏覽器對(duì)象模型BOM以及它的核心ECMAScript這三部分組成,本篇文章帶來了JavaScript中的底層原理知識(shí),希望對(duì)大家有幫助。
JavaScript是一門直譯式的解釋型腳本語言,它具有動(dòng)態(tài)性、弱類型、基于原型等特點(diǎn)。JavaScript植根于我們使用的Web瀏覽器中,它的解釋器為瀏覽器中的JavaScript引擎。這一門廣泛用于客戶端的腳本語言,最早是為了處理以前由服務(wù)器端語言負(fù)責(zé)的一些輸入驗(yàn)證操作,隨著Web時(shí)代的發(fā)展,JavaScript不斷發(fā)展壯大,成為一門功能全面的編程語言。它的用途早已不再局限于當(dāng)初簡(jiǎn)單的數(shù)據(jù)驗(yàn)證,而是具備了與瀏覽器窗口及其內(nèi)容等幾乎所有方面交互的能力。它既是一門非常簡(jiǎn)單的語言,又是一門及其復(fù)雜的語言,要想真正精通JavaScript,我們就必須深入的去了解它的一些底層設(shè)計(jì)原理。本文將參考《JavaScript高級(jí)程序設(shè)計(jì)》和《你不知道的JS》系列叢書,為大家講解一些關(guān)于JavaScript的底層知識(shí)。
數(shù)據(jù)類型
按照存儲(chǔ)方式,JavaScript的數(shù)據(jù)類型可以分為兩種,原始數(shù)據(jù)類型(原始值)和引用數(shù)據(jù)類型(引用值)。
原始數(shù)據(jù)類型目前有六種,包括Number、String、Boolean、Null、Undefined、Symbol(ES6),這些類型是可以直接操作的保存在變量中的實(shí)際值。原始數(shù)據(jù)類型存放在棧中,數(shù)據(jù)大小確定,它們是直接按值存放的,所以可以直接按值訪問。
引用數(shù)據(jù)類型則為Object,在JavaScript中除了原始數(shù)據(jù)類型以外的都是Object類型,包括數(shù)組、函數(shù)、正則表達(dá)式等都是對(duì)象。引用類型是存放在堆內(nèi)存中的對(duì)象,變量是保存在棧內(nèi)存中的一個(gè)指向堆內(nèi)存中對(duì)象的引用地址。當(dāng)定義了一個(gè)變量并初始化為引用值,若將它賦給另一個(gè)變量,則這兩個(gè)變量保存的是同一個(gè)地址,指向堆內(nèi)存中的同一個(gè)內(nèi)存空間。如果通過其中一個(gè)變量去修改引用數(shù)據(jù)類型的值,另一個(gè)變量也會(huì)跟著改變。
對(duì)于原始數(shù)據(jù)類型,除了null比較特殊(null會(huì)被認(rèn)為是一個(gè)空的對(duì)象引用),其它的我們可以用typeof進(jìn)行準(zhǔn)確判斷:
表達(dá)式 |
返回值 |
typeof 123 |
'number' |
typeof "abc" |
'string' |
typeof true |
'boolean' |
typeof null |
'object' |
typeof undefined |
'undefined' |
typeof unknownVariable(未定義的變量) |
'undefined' |
typeof Symbol() |
‘symbol’ |
typeof function() {} |
'function' |
typeof {} |
'object' |
typeof [] |
'object' |
typeof(/[0-9,a-z]/) |
‘object’ |
對(duì)于null類型,可以使用全等操作符進(jìn)行判斷。一個(gè)已經(jīng)聲明但未初始化的變量值會(huì)默認(rèn)賦予undefined (也可以手動(dòng)賦予undefined),在JavaScript中,使用相等操作符==無法區(qū)分null和undefined,ECMA-262規(guī)定對(duì)它們的相等性測(cè)試要返回true。要準(zhǔn)確區(qū)分兩個(gè)值,需要使用全等操作符===。
對(duì)于引用數(shù)據(jù)類型,除了function在方法設(shè)計(jì)上比較特殊,可以用typeof進(jìn)行準(zhǔn)確判斷,其它的都返回object類型。我們可以用instanceof 對(duì)引用類型值進(jìn)行判斷。instanceof 會(huì)檢測(cè)一個(gè)對(duì)象A是不是另一個(gè)對(duì)象B的實(shí)例,它在底層會(huì)查看對(duì)象B是否在對(duì)象A的原型鏈上存在著(實(shí)例和原型鏈文章后面會(huì)講)。如果存在,則返回true,如果不在則返回false。
表達(dá)式 |
返回值 |
[1,2,3] instanceof Array |
‘true’ |
function foo(){ } instanceof Function |
‘true’ |
/[0-9,a-z]/ instanceof RegExp |
‘true’ |
new Date() instanceof Date |
‘true’ |
{name:”Alan”,age:”22”} instanceof Object |
‘true’ |
由于所有引用類型值都是Object的實(shí)例,所以用instance操作符對(duì)它們進(jìn)行Object的判斷,結(jié)果也會(huì)返回true。
表達(dá)式 |
返回值 |
[1,2,3] instanceof Object |
‘true’ |
function foo(){ } instanceof Object |
‘true’ |
/[0-9,a-z]/ instanceof Object |
‘true’ |
new Date() instanceof Object |
‘true’ |
當(dāng)然,還有一種更為強(qiáng)大的方法,可以精準(zhǔn)的判斷任何JavaScript中的任何數(shù)據(jù)類型,那就是Object.prototype.toString.call() 方法。在ES5中,所有對(duì)象(原生對(duì)象和宿主對(duì)象)都有一個(gè)內(nèi)部屬性[[Class]],它的值是一個(gè)字符串,記錄了該對(duì)象的類型。目前包括"Array", "Boolean", "Date", "Error", "Function", "Math", "Number", "Object", "RegExp", "String",“Arguments”, "JSON","Symbol”。通過Object.prototype.toString() 方法可以用來查看該內(nèi)部屬性,除此自外沒有其它方法。
在Object.prototype.toString()方法被調(diào)用時(shí),會(huì)執(zhí)行以下步驟:1.獲取this對(duì)象的[[Class]]屬性值(關(guān)于this對(duì)象文章后面會(huì)講)。2.將該值放在兩個(gè)字符串”[object ” 與 “]” 中間并拼接起來。3.返回拼接完的字符串。
當(dāng)遇到this的值為null時(shí),Object.prototype.toString()方法直接返回”[object Null]”。當(dāng)遇到this的值為undefined時(shí),直接返回”[object Undefined]”。
表達(dá)式 |
返回值 |
Object.prototype.toString.call(123) |
[object Number] |
Object.prototype.toString.call(“abc”) |
[object String] |
Object.prototype.toString.call(true) |
[object Boolean] |
Object.prototype.toString.call(null) |
[object Null] |
Object.prototype.toString.call(undefined) |
[object Undefined] |
Object.prototype.toString.call(Symbol()) |
[object Symbol] |
Object.prototype.toString.call(function foo(){}) |
[object Function] |
Object.prototype.toString.call([1,2,3]) |
[object Array] |
Object.prototype.toString.call({name:”Alan” }) |
[object Object] |
Object.prototype.toString.call(new Date()) |
[object Date] |
Object.prototype.toString.call(RegExp()) |
[object RegExp] |
Object.prototype.toString.call(window.JSON) |
[object JSON] |
Object.prototype.toString.call(Math) |
[object Math] |
call()方法可以改變調(diào)用Object.prototype.toString()方法時(shí)this的指向,使它指向我們傳入的對(duì)象,因此能獲取到我們傳入對(duì)象的[[Class]]屬性(使用Object.prototype.toString.apply()也能達(dá)到同樣的效果)。
JavaScript的數(shù)據(jù)類型也是可以轉(zhuǎn)換的,數(shù)據(jù)類型轉(zhuǎn)換分為兩種方式:顯示類型轉(zhuǎn)換和隱式類型轉(zhuǎn)換。
顯示類型轉(zhuǎn)換可以調(diào)用方法有Boolean()、String()、Number()、parseInt()、parseFloat()和toString() (null和undefined值沒有這個(gè)方法),它們各自的用途一目了然,這里就不一一介紹了。
由于JavaScript是弱類型語言,在使用算術(shù)運(yùn)算符時(shí),運(yùn)算符兩邊的數(shù)據(jù)類型可以是任意的,不用像Java或C語言那樣指定相同的類型,引擎會(huì)自動(dòng)為它們進(jìn)行隱式類型轉(zhuǎn)換。隱式類型轉(zhuǎn)換不像顯示類型轉(zhuǎn)換那么直觀,主要是三種轉(zhuǎn)換方式:
1. 將值轉(zhuǎn)換為原始值:toPrimitive()
2. 將值轉(zhuǎn)換為數(shù)字:toNumber()
3. 將值轉(zhuǎn)換為字符串:toString()
一般來說,當(dāng)對(duì)數(shù)字和字符串進(jìn)行相加操作時(shí),數(shù)字會(huì)被轉(zhuǎn)換成字符串;當(dāng)進(jìn)行真值判斷時(shí)(如if、||、&&),參數(shù)會(huì)被轉(zhuǎn)換成布爾值;當(dāng)進(jìn)行比較運(yùn)算、算術(shù)運(yùn)算或自增減運(yùn)算時(shí),參數(shù)會(huì)被轉(zhuǎn)換成Number值;當(dāng)對(duì)象需要進(jìn)行隱式類型轉(zhuǎn)換時(shí),會(huì)取得對(duì)象的toString()方法或valueOf()方法的返回值。
關(guān)于NaN:
NaN是一個(gè)特殊的數(shù)值,表示非數(shù)值。首先,任何涉及NaN的運(yùn)算操作都會(huì)返回NaN。其次,NaN與任何值都不相等,包括NaN本身。ECMAScript定義了一個(gè)isNaN()函數(shù),可以用來測(cè)試某個(gè)參數(shù)是否為“非數(shù)值”。它首先會(huì)嘗試將參數(shù)隱式轉(zhuǎn)換為數(shù)值,如果無法轉(zhuǎn)換為數(shù)值則返回true。
我們可以先通過typeof判斷是否為Number類型,再通過isNaN來判斷當(dāng)前數(shù)據(jù)是否為NaN。
關(guān)于字符串:
JavaScript中的字符串是不可變的,字符串一旦被創(chuàng)建,它們的值就不能改變。要改變某個(gè)變量保存的字符串,首先要銷毀原來的字符串,然后再用另一個(gè)包含新值的字符串填充該變量。這個(gè)過程在后臺(tái)發(fā)生,而這也是某些老版本瀏覽器在拼接字符串時(shí)速度很慢的原因所在。
其實(shí)為了便于操作基本類型值,ECMAScript還提供了3個(gè)特殊的引用類型:Boolean、Number和String。原始數(shù)據(jù)類型是沒有屬性和方法的,當(dāng)我們?cè)谠碱愋椭瞪险{(diào)用方法讀取它們時(shí),訪問過程會(huì)處于一種讀取模式,后臺(tái)會(huì)創(chuàng)建一個(gè)相應(yīng)的原始包裝類型的對(duì)象,從而讓我們能夠調(diào)用一些方法來操作這些數(shù)據(jù)。這個(gè)過程分為三個(gè)步驟:1.創(chuàng)建原始包裝類型的實(shí)例 2.在實(shí)例上調(diào)用指定的方法 3.銷毀這個(gè)實(shí)例。
引用類型與原始包裝類型的主要區(qū)別是對(duì)象的生存周期,自動(dòng)創(chuàng)建的原始包裝類型對(duì)象,只存在于一行代碼的執(zhí)行瞬間,然后立即被銷毀,因此我們不能在運(yùn)行時(shí)為原始類型值添加屬性和方法。
預(yù)編譯
在《你不知道的JavaScript》一書中作者表示過,盡管將JavaScript歸類為“動(dòng)態(tài)語言”或“解釋執(zhí)行語言”,但事實(shí)上它是一門編譯語言。JavaScript運(yùn)行分為三個(gè)步驟:1.語法分析 2.預(yù)編譯 3.解釋執(zhí)行。語法分析和解釋執(zhí)行都不難理解,一個(gè)是檢查代碼是否有語法錯(cuò)誤,一個(gè)則負(fù)責(zé)將程序一行一行的執(zhí)行,但JavaScript中的預(yù)編譯階段卻稍微比較復(fù)雜。
任何JavaScript代碼在執(zhí)行前都要進(jìn)行編譯,編譯過程大部分情況下發(fā)生在代碼執(zhí)行前的幾微秒內(nèi)。編譯階段JavaScript引擎會(huì)從當(dāng)前代碼執(zhí)行作用域開始,對(duì)代碼進(jìn)行RHS查詢,以獲取變量的值。接著在執(zhí)行階段引擎會(huì)執(zhí)行LHS查詢,對(duì)變量進(jìn)行賦值。
在編譯階段,JavaScript引擎的一部分工作就是找到所有的聲明,并用合適的作用域?qū)⑺鼈冴P(guān)聯(lián)起來。在預(yù)編譯過程,如果是在全局作用域下,JavaScript引擎首先會(huì)在全局作用域上創(chuàng)建一個(gè)全局對(duì)象(GO對(duì)象,Global Object),并將變量聲明和函數(shù)聲明進(jìn)行提升。提升后的變量先默認(rèn)初始化為undefined,而函數(shù)則將整個(gè)函數(shù)體進(jìn)行提升(如果是以函數(shù)表達(dá)式的形式定義函數(shù),則應(yīng)用變量提升的規(guī)則),然后將它們存放到全局變量中。函數(shù)聲明的提升會(huì)優(yōu)先于變量聲明的提升,對(duì)于變量聲明來說,重復(fù)出現(xiàn)的var聲明會(huì)被引擎忽略,而后面出現(xiàn)的函數(shù)聲明可以覆蓋前面的函數(shù)聲明(ES6新的變量聲明語法let情況稍稍有點(diǎn)不一樣,這里暫時(shí)先不討論)。
函數(shù)體內(nèi)部是一塊獨(dú)立的作用域,在函數(shù)體內(nèi)部也會(huì)進(jìn)行預(yù)編譯階段。在函數(shù)體內(nèi)部,首先會(huì)創(chuàng)建一個(gè)活動(dòng)對(duì)象(AO對(duì)象,Active Object),并將形參和變量聲明以及函數(shù)體內(nèi)部的函數(shù)聲明進(jìn)行提升,形參和變量初始化為undefined,內(nèi)部函數(shù)依然為內(nèi)部函數(shù)體本身,然后將它們存放到活動(dòng)對(duì)象中。
編譯階段結(jié)束后,就會(huì)執(zhí)行JavaScript代碼。執(zhí)行過程根據(jù)先后順序依次對(duì)變量或形參進(jìn)行賦值操作。引擎會(huì)在作用域上查找是否有對(duì)應(yīng)的變量聲明或形參聲明,如果找到了就會(huì)對(duì)它們進(jìn)行賦值操作。對(duì)于非嚴(yán)格模式來說,若變量未經(jīng)聲明就進(jìn)行賦值,引擎會(huì)在全局環(huán)境自動(dòng)隱式地為該變量創(chuàng)建一個(gè)聲明,但對(duì)于嚴(yán)格模式來說對(duì)未經(jīng)聲明的變量進(jìn)行賦值操作則會(huì)報(bào)錯(cuò)。因?yàn)镴avaScript執(zhí)行是單線程的,所以如果在賦值操作(LHS查詢)執(zhí)行前就先對(duì)變量進(jìn)行獲取(RHS查詢)并輸出,會(huì)得到undefined的結(jié)果,因?yàn)榇藭r(shí)變量還未賦值。
執(zhí)行環(huán)境與作用域
每個(gè)函數(shù)都是Function對(duì)象的一個(gè)實(shí)例,在JavaScript中,每個(gè)對(duì)象都有一個(gè)僅供JavaScript引擎存取的內(nèi)部屬性—— [[Scope]]。對(duì)于函數(shù)來說,[[Scope]]屬性包含了函數(shù)被創(chuàng)建的作用域中對(duì)象的集合——作用域鏈。當(dāng)在全局環(huán)境中創(chuàng)建一個(gè)函數(shù)時(shí),該函數(shù)的作用域鏈便會(huì)插入一個(gè)全局對(duì)象,包含所有在全局范圍內(nèi)定義的變量。
內(nèi)部作用域可以訪問外部作用域,但外部作用域無法訪問內(nèi)部作用域。由于JavaScript沒有塊級(jí)作用域,因此在if語句或者for循環(huán)語句中定義的變量是可以在語句外部訪問到的。在ES6之前,javascript只有全局作用域和函數(shù)作用域,ES6新增了塊級(jí)作用域的機(jī)制。
而當(dāng)該函數(shù)被執(zhí)行時(shí),會(huì)為執(zhí)行函數(shù)創(chuàng)建一個(gè)稱為執(zhí)行環(huán)境(execution context,也稱為執(zhí)行上下文)的內(nèi)部對(duì)象。每個(gè)執(zhí)行環(huán)境都有自己的作用域鏈,當(dāng)執(zhí)行環(huán)境被創(chuàng)建時(shí),它的作用域鏈頂端先初始化為當(dāng)前運(yùn)行函數(shù)的[[Scope]]屬性中的對(duì)象。緊接著,函數(shù)運(yùn)行時(shí)的活動(dòng)對(duì)象(包括所有局部變量、命名參數(shù)、arguments參數(shù)集合和this)也會(huì)被創(chuàng)建并推入作用域鏈的最頂端。
函數(shù)每次執(zhí)行時(shí)對(duì)應(yīng)的執(zhí)行環(huán)境都是獨(dú)一無二的,多次調(diào)用同一個(gè)函數(shù)就會(huì)導(dǎo)致創(chuàng)建多個(gè)執(zhí)行環(huán)境。當(dāng)函數(shù)執(zhí)行完畢,執(zhí)行環(huán)境就會(huì)被銷毀。當(dāng)執(zhí)行環(huán)境被銷毀,活動(dòng)對(duì)象也隨之銷毀(全局執(zhí)行環(huán)境會(huì)等到應(yīng)用程序退出時(shí)才會(huì)被銷毀,如關(guān)閉網(wǎng)頁或?yàn)g覽器)。
函數(shù)執(zhí)行過程中,每遇到一個(gè)變量,都會(huì)經(jīng)歷一次標(biāo)識(shí)符解析過程,以決定從哪里獲取或存儲(chǔ)數(shù)據(jù)。標(biāo)識(shí)符解析是沿著作用域鏈一級(jí)一級(jí)地搜索標(biāo)識(shí)符的過程,全局變量始終都是作用域鏈的最后一個(gè)對(duì)象(即window對(duì)象)。
在JavaScript中,有兩個(gè)語句可以在執(zhí)行時(shí)臨時(shí)改變作用域鏈。第一個(gè)是with語句。with語句會(huì)創(chuàng)建一個(gè)可變對(duì)象,包含參數(shù)指定對(duì)象的所有屬性,并將該對(duì)象推入作用域鏈的首位,這意味著函數(shù)的活動(dòng)對(duì)象被擠到作用域鏈的第二位。這樣雖然使得訪問可變對(duì)象的屬性非常快,但訪問局部變量等的速度就變慢了。第二條能改變執(zhí)行環(huán)境作用域鏈的語句是try-catch語句中的catch子句。當(dāng)try代碼塊中發(fā)生錯(cuò)誤,執(zhí)行過程會(huì)自動(dòng)跳轉(zhuǎn)到catch子句,然后把異常對(duì)象推入一個(gè)變量對(duì)象并置于作用域的首位。在catch代碼塊內(nèi)部,函數(shù)所有局部變量將會(huì)放在第二個(gè)作用域鏈對(duì)象中。一旦catch子句執(zhí)行完畢,作用域鏈就會(huì)返回到之前的狀態(tài)。
構(gòu)造函數(shù)
JavaScript中的構(gòu)造函數(shù)可以用來創(chuàng)建特定類型的對(duì)象。為了區(qū)別于其它函數(shù),構(gòu)造函數(shù)一般使用大寫字母開頭。不過在JavaScript中這并不是必須的,因?yàn)镴avaScript不存在定義構(gòu)造函數(shù)的特殊語法。在JavaScript中,構(gòu)造函數(shù)與其它函數(shù)的唯一區(qū)別,就在于調(diào)用它們的方式不同。任何函數(shù),只要通過new操作符來調(diào)用,就可以作為構(gòu)造函數(shù)。
JavaScript函數(shù)有四種調(diào)用模式:1.獨(dú)立函數(shù)調(diào)用模式,如foo(arg)。2.方法調(diào)用模式,如obj.foo(arg)。3.構(gòu)造器調(diào)用模式,如new foo(arg)。4.call/apply調(diào)用模式,如foo.call(this,arg1,arg2)或foo.apply(this,args) (此處的args是一個(gè)數(shù)組)。
要?jiǎng)?chuàng)建構(gòu)造函數(shù)的實(shí)例,發(fā)揮構(gòu)造函數(shù)的作用,就必須使用new操作符。當(dāng)我們使用new操作符實(shí)例化構(gòu)造函數(shù)時(shí),構(gòu)造函數(shù)內(nèi)部會(huì)執(zhí)行以下步驟:
1.隱式創(chuàng)建一個(gè)this空對(duì)象
2.執(zhí)行構(gòu)造函數(shù)中的代碼(為當(dāng)前this對(duì)象添加屬性)
3.隱式返回當(dāng)前this對(duì)象
如果構(gòu)造函數(shù)顯示的返回一個(gè)對(duì)象,那么實(shí)例為這個(gè)返回的對(duì)象,否則則為隱式返回的this對(duì)象。
當(dāng)我們調(diào)用構(gòu)造函數(shù)創(chuàng)建實(shí)例后,實(shí)例便具備構(gòu)造函數(shù)所有的實(shí)例屬性和方法。對(duì)于通過構(gòu)造函數(shù)創(chuàng)建的不同實(shí)例,它們之間的實(shí)例屬性和方法都是各自獨(dú)立的。那怕是同名的引用類型值,不同實(shí)例之間也不會(huì)相互影響。
原型與原型鏈
原型和原型鏈既是JavaScript這門語言的精髓之一,也是這門語言的難點(diǎn)之一。原型prototype(顯式原型)是函數(shù)特有的屬性,任何時(shí)候,只要?jiǎng)?chuàng)建了一個(gè)函數(shù),這個(gè)函數(shù)就會(huì)自動(dòng)創(chuàng)建一個(gè)prototype屬性,并指向該函數(shù)的原型對(duì)象。所有原型對(duì)象都會(huì)自動(dòng)獲得一個(gè)constructor(構(gòu)造者,也可翻譯為構(gòu)造函數(shù))屬性,這個(gè)屬性包含一個(gè)指向prototype屬性所在函數(shù)(即構(gòu)造函數(shù)本身)的指針。而當(dāng)我們通過構(gòu)造函數(shù)創(chuàng)建一個(gè)實(shí)例后,該實(shí)例內(nèi)部將包含一個(gè)[[Prototype]]的內(nèi)部屬性(隱式原型),同樣也指向構(gòu)造函數(shù)的原型對(duì)象。在Firefox、Safari和Chrome瀏覽器中,每個(gè)對(duì)象都可以通過__proto__屬性訪問它們的[[Prototype]]屬性。而對(duì)其它瀏覽器而言,這個(gè)屬性對(duì)腳本則是完全不可見的。
構(gòu)造函數(shù)的prototype屬性和實(shí)例的[[Prototype]]都是指向構(gòu)造函數(shù)的原型對(duì)象,實(shí)例的 [[Prototype]] 屬性與構(gòu)造函數(shù)之間并沒有直接的關(guān)系。要想知道實(shí)例的 [[Prototype]] 屬性是否指向某個(gè)構(gòu)造函數(shù)的原型對(duì)象,我們可以使用isPrototypeOf()或者Object.getPrototypeOf() 方法。
每當(dāng)讀取某個(gè)對(duì)象實(shí)例的某個(gè)屬性時(shí),都會(huì)執(zhí)行一次搜索,目標(biāo)是具有給定名字的屬性。搜索首先從對(duì)象實(shí)例本身開始,如果在實(shí)例中找到了具有給定名稱的屬性,就返回該屬性的值;如果沒有找到,則繼續(xù)搜索該對(duì)象[[Prototype]]屬性指向的原型對(duì)象,在原型對(duì)象中查找給定名稱的屬性,如果找到再返回該屬性的值。
判斷對(duì)象是哪個(gè)構(gòu)造函數(shù)的直接實(shí)例,可以直接在實(shí)例上訪問constructor屬性,實(shí)例會(huì)通過[[Prototype]]讀取原型對(duì)象上的constructor屬性返回構(gòu)造函數(shù)本身。
原型對(duì)象中的值可以通過對(duì)象實(shí)例訪問,但卻不能通過對(duì)象實(shí)例修改。如果在實(shí)例中添加一個(gè)與實(shí)例原型對(duì)象同名的屬性,那我們就在實(shí)例中創(chuàng)建該屬性,這個(gè)實(shí)例屬性會(huì)阻止我們?cè)L問原型對(duì)象中的那個(gè)屬性,但不會(huì)修改那個(gè)屬性。簡(jiǎn)單將該實(shí)例屬性設(shè)為null并不能恢復(fù)訪問原型對(duì)象中該屬性的連接,若要恢復(fù)訪問原型對(duì)象中的該屬性,可以用delete操作符完全刪除對(duì)象實(shí)例的該屬性。
使用hasOwnProperty()方法可以檢測(cè)一個(gè)屬性是存在于實(shí)例中,還是存在于原型中。這個(gè)方法只有在給定屬性存在于對(duì)象實(shí)例中時(shí),才會(huì)返回true。若要取得對(duì)象自身所有可枚舉的實(shí)例屬性,可以使用ES5的Object.keys() 方法。若要獲取所有實(shí)例屬性,無論是否可枚舉,可以使用Object.getOwnPropertyNames() 方法。
原型具有動(dòng)態(tài)性,對(duì)原型對(duì)象所做的任何修改都能立即從實(shí)例上反應(yīng)出來,但如果是重寫整個(gè)原型對(duì)象,情況就不一樣了。調(diào)用構(gòu)造函數(shù)會(huì)為對(duì)象實(shí)例添加一個(gè)指向最初原型對(duì)象的 [[Prototype]] 指針,而重寫整個(gè)原型對(duì)象后,構(gòu)造函數(shù)指向新的原型對(duì)象,所有的原型對(duì)象屬性和方法都存在與新的原型對(duì)象上;而對(duì)象實(shí)例還指向最初的原型對(duì)象,這樣一來構(gòu)造函數(shù)與最初原型對(duì)象之間指向同一個(gè)原型對(duì)象產(chǎn)生的聯(lián)系就被切斷了,因?yàn)樗鼈兎謩e指向了不同的原型對(duì)象。
若要恢復(fù)這種聯(lián)系,可以在構(gòu)造函數(shù)prototype重寫后再實(shí)例化對(duì)象實(shí)例,或者修改對(duì)象實(shí)例的__proto__屬性重新指向構(gòu)造函數(shù)新的原型對(duì)象。
JavaScript將原型鏈作為實(shí)現(xiàn)繼承的主要方式,它利用原型讓一個(gè)引用類型繼承另一個(gè)引用類型的屬性和方法。構(gòu)造函數(shù)的實(shí)例有一個(gè)指向原型對(duì)象的 [[Prototype]] 屬性,當(dāng)我們讓構(gòu)造函數(shù)的原型對(duì)象等于另一個(gè)類型的實(shí)例,原型對(duì)象也將包含一個(gè)指向另一個(gè)原型的 [[Prototype]] 指針,假如另一個(gè)原型又是另一個(gè)類型的實(shí)例…如此層層遞進(jìn),就構(gòu)成了實(shí)例與原型的鏈條。這就是所謂原型鏈的基本概念。
原型鏈擴(kuò)展了原型搜索機(jī)制,當(dāng)讀取一個(gè)實(shí)例屬性時(shí),首先會(huì)在實(shí)例中搜索該屬性。如果沒有找到該屬性,則會(huì)繼續(xù)搜索實(shí)例[[Prototype]] 指向的原型對(duì)象,原型對(duì)象此時(shí)也變成了另一個(gè)構(gòu)造函數(shù)的實(shí)例,如果該原型對(duì)象上也找不到,就會(huì)繼續(xù)搜索該原型對(duì)象[[Prototype]] 指向的另一個(gè)原型對(duì)象…搜索過程沿著原型鏈不斷向上搜索,在找不到指定屬性或者方法的情況下,搜索過程就會(huì)一環(huán)一環(huán)地執(zhí)行到原型鏈末端才會(huì)停下來。
如果不對(duì)函數(shù)的原型對(duì)象進(jìn)行修改,所有引用類型都有一個(gè)[[Prototype]] 屬性默認(rèn)指向Object的原型對(duì)象。因此,所有函數(shù)的默認(rèn)原型都是Object的實(shí)例,這也正是所有自定義類型都會(huì)繼承toString()、valueOf() 等默認(rèn)方法的根本原因??梢允褂胕nstanceof操作符或isPrototypeOf() 方法來判斷實(shí)例的原型鏈中是否存在某個(gè)構(gòu)造函數(shù)的原型。
原型鏈雖然很強(qiáng)大,但是它也存在一些問題。第一個(gè)問題是原型對(duì)象上的引用類型值是所有實(shí)例共享的,這意味著不同實(shí)例的引用類型屬性或方法都指向同一個(gè)堆內(nèi)存,一個(gè)實(shí)例在原型上修改引用值會(huì)同時(shí)影響到所有其它實(shí)例在原型對(duì)象上的該引用值,這便是為何要在構(gòu)造函數(shù)中定義私有屬性或方法,而不是在原型上定義的原因。原型鏈的第二個(gè)問題,在于當(dāng)我們將一個(gè)構(gòu)造函數(shù)的原型prototype等于另一個(gè)構(gòu)造函數(shù)的實(shí)例時(shí),如果我們?cè)谶@時(shí)候給另一個(gè)構(gòu)造函數(shù)傳遞參數(shù)設(shè)置屬性值,那么基于原來的構(gòu)造函數(shù)所有實(shí)例的該屬性都會(huì)因?yàn)樵玩湹年P(guān)系跟著被賦予相同的值,而這有時(shí)候并不是我們想要的結(jié)果。
閉包
閉包是JavaScript最強(qiáng)大的特性之一,在JavaScript中,閉包,是指有權(quán)訪問另一個(gè)函數(shù)作用域中的變量的函數(shù),它意味著函數(shù)可以訪問局部作用域之外的數(shù)據(jù)。創(chuàng)建閉包的常見方式,就是在一個(gè)函數(shù)內(nèi)部創(chuàng)建另一個(gè)函數(shù)并返回這個(gè)函數(shù)。
一般來講,當(dāng)函數(shù)執(zhí)行完畢后,局部活動(dòng)對(duì)象就會(huì)被銷毀,內(nèi)存中僅保存全局作用域。但是,閉包的情況有所不同。
閉包函數(shù)的[[Scope]]屬性會(huì)初始化為包裹它的函數(shù)的作用域鏈,所以閉包包含了與執(zhí)行環(huán)境作用域鏈相同的對(duì)象的引用。一般來講,函數(shù)的活動(dòng)對(duì)象會(huì)隨著執(zhí)行環(huán)境一同銷毀。但引入閉包時(shí),由于引用仍然存在于閉包的[[Scope]]屬性中,因此原函數(shù)的活動(dòng)對(duì)象無法被銷毀。這意味著閉包函數(shù)與非閉包函數(shù)相比,需要