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