1) 引子
前不久我建立的技術(shù)群里一位MM問(wèn)了一個(gè)這樣的問(wèn)題,她貼出的代碼如下所示:
執(zhí)行結(jié)果如下所示:
第一個(gè)alert:
第二個(gè)alert:
這是一個(gè)令人詫異的結(jié)果,為什么第一個(gè)彈出框顯示的是undefined,而不是1呢?這種疑惑的原理我描述如下:
一個(gè)頁(yè)面里直接定義在script標(biāo)簽下的變量是全局變量即屬于window對(duì)象的變量,按照javascript作用域鏈的原理,當(dāng)一個(gè)變量在當(dāng)前作用域下找不到該變量的定義,那么javascript引擎就會(huì)沿著作用域鏈往上找直到在全局作用域里查找,按上面的代碼所示,雖然函數(shù)內(nèi)部重新定義了變量的值,但是內(nèi)部定義之前函數(shù)使用了該變量,那么按照作用域鏈的原理在函數(shù)內(nèi)部變量定義之前使用該變量,javascript引擎應(yīng)該會(huì)在全局作用域里找到變量定義,而實(shí)際情況卻是變量未定義,這到底是怎么回事呢?
當(dāng)時(shí)群里很多人都給出了問(wèn)題的解答,我也給出了我自己的解答,其實(shí)這個(gè)問(wèn)題很久之前我的確研究過(guò),但是剛被問(wèn)起了我居然還是有個(gè)卡殼期,在加上最近研究javascriptMVC的寫(xiě)法,發(fā)現(xiàn)自己讀代碼時(shí)候?qū)ew 、prototype、apply以及call的用法任然要體味半天,所以我覺(jué)得有必要對(duì)javascript基礎(chǔ)語(yǔ)法里比較難理解的問(wèn)題做個(gè)梳理,其實(shí)寫(xiě)博客的一個(gè)很大的好處就是寫(xiě)出來(lái)的知識(shí)邏輯會(huì)比你在腦子里反復(fù)梳理的邏輯映像更加的深刻。
下面開(kāi)始本文的主要內(nèi)容,我會(huì)從基礎(chǔ)知識(shí)一步步講起。
2) Javascript的變量
Java語(yǔ)言里有一句很經(jīng)典的話:在java的世界里,一切皆是對(duì)象。
Javascript雖然跟java沒(méi)有半點(diǎn)毛關(guān)系,但是很多會(huì)使用javascript的朋友同樣認(rèn)為:在javascript的世界里,一切也皆是對(duì)象。
其實(shí)javascript語(yǔ)言和java語(yǔ)言一樣變量是分為兩種類型:基本數(shù)據(jù)類型和引用類型。
基本類型是指:Undefined、Null、Boolean、Number和String;而引用類型是指多個(gè)指構(gòu)成的對(duì)象,所以javascript的對(duì)象指的是引用類型。在java里能說(shuō)一切是對(duì)象,是因?yàn)閖ava語(yǔ)言里對(duì)所有基本類型都做了對(duì)象封裝,而這點(diǎn)在javascript語(yǔ)言里也是一樣的,所以提在javascript世界里一切皆為對(duì)象也不為過(guò)。
但是實(shí)際開(kāi)發(fā)里如果我們對(duì)基本類型和引用類型的區(qū)別不是很清晰,就會(huì)碰到我們很多不能理解的問(wèn)題,下面我們來(lái)看看下面的代碼:
var str = "sharpxiajun"; str.attr01 = "hello world"; console.log(str);// 運(yùn)行結(jié)果:sharpxiajun console.log(str.attr01);// 運(yùn)行結(jié)果:undefined
運(yùn)行之,我們發(fā)現(xiàn)作為基本數(shù)據(jù)類型,我們沒(méi)法為這個(gè)變量添加屬性,當(dāng)然方法也同樣不可以,例如下面的代碼:
運(yùn)行之,結(jié)果如下圖所示:
當(dāng)我們使用引用類型時(shí)候,結(jié)果就和上面完全不同了,大家請(qǐng)看下面的代碼:
var obj1 = new Object(); obj1.name = "obj1 name"; console.log(obj1.name);// 運(yùn)行結(jié)果:obj1 name
javascript里的基本類型和引用類型的區(qū)別和其他語(yǔ)言類似,這是一個(gè)老調(diào)長(zhǎng)談的問(wèn)題,但是在現(xiàn)實(shí)中很多人都理解它,但是卻很難應(yīng)用它去理解問(wèn)題。
Javascript里的基本變量是存放在棧區(qū)的(棧區(qū)指內(nèi)存里的棧內(nèi)存),它的存儲(chǔ)結(jié)構(gòu)如下圖所示:
javascript里引用變量的存儲(chǔ)就比基本類型存儲(chǔ)要復(fù)雜多,引用類型的存儲(chǔ)需要內(nèi)存的棧區(qū)和堆區(qū)(堆區(qū)是指內(nèi)存里的堆內(nèi)存)共同完成,如下圖所示:
在javascript里變量的存儲(chǔ)包含三個(gè)部分:
部分一:棧區(qū)的變量標(biāo)示符;
部分二:棧區(qū)變量的值;
部分三:堆區(qū)存儲(chǔ)的對(duì)象。
變量不同的定義,這三個(gè)部分也會(huì)隨之發(fā)生變化,下面我來(lái)列舉一些典型的場(chǎng)景:
場(chǎng)景一:如下代碼所示:
var qqq; console.log(qqq);// 運(yùn)行結(jié)果:undefined
運(yùn)行結(jié)果是undefined,上面的代碼的標(biāo)準(zhǔn)解釋就是變量被命名了,但是還未初始化,此時(shí)在變量存儲(chǔ)的內(nèi)存里只擁有棧區(qū)的變量標(biāo)示符而沒(méi)有棧區(qū)的變量值,當(dāng)然更沒(méi)有堆區(qū)存儲(chǔ)的對(duì)象。
場(chǎng)景二:如下代碼所示:
var qqq; console.log(qqq);// 運(yùn)行結(jié)果:undefined console.log(xxx);
運(yùn)行之,結(jié)果如下圖所示:
會(huì)提示變量未定義。在任何語(yǔ)言里變量未定義就使用都是違法的,我們看到j(luò)avascript里也是如此,但是我們做javascript開(kāi)發(fā)時(shí)候,經(jīng)常有人會(huì)說(shuō)變量未定義也是可以使用,怎么我的例子里卻不能使用了?那么我們看看下面的代碼:
xxx = "outer xxx"; console.log(xxx);// 運(yùn)行結(jié)果:outer xxx function testFtn(){ sss = "inner sss"; console.log(sss);// 運(yùn)行結(jié)果:outer sss } testFtn(); console.log(sss);//運(yùn)行結(jié)果:outer sss console.log(window.sss);//運(yùn)行結(jié)果:outer sss
在javascript定義變量需要使用var關(guān)鍵字,但是javascript可以不使用var預(yù)先定義好變量,在javascript我們可以直接賦值給沒(méi)有被var定義的變量,不過(guò)此時(shí)你這么操作變量,不管這個(gè)操作是在全局作用域里還是在局部作用域里,變量最終都是屬于window對(duì)象,我們看看window對(duì)象的結(jié)構(gòu),如下圖所示:
由這兩個(gè)場(chǎng)景我們可以知道在javascript里的變量不能正常使用即報(bào)出“xxx is not defined”錯(cuò)誤(這個(gè)錯(cuò)誤下,后續(xù)的javascript代碼將不能正常運(yùn)行)只有當(dāng)這個(gè)變量既沒(méi)有被var定義同時(shí)也沒(méi)有進(jìn)行賦值操作才會(huì)發(fā)生,而只有賦值操作的變量不管這個(gè)變量在那個(gè)作用域里進(jìn)行的賦值,這個(gè)變量最終都是屬于全局變量即window對(duì)象。
由上面我列舉的兩個(gè)場(chǎng)景我們來(lái)理解下引子里網(wǎng)友提出的問(wèn)題,下面我修改一下代碼,如下所示:
結(jié)果如下圖所示:
我再改下代碼:
運(yùn)行之,結(jié)果如下所示:
對(duì)比二者代碼以及引子里的代碼,我們發(fā)現(xiàn)問(wèn)題的關(guān)鍵是var a=2所引起的。在代碼一里我注釋了全局變量的定義,結(jié)果和引子里代碼的結(jié)果一致,這說(shuō)明函數(shù)內(nèi)部a變量的使用和全局環(huán)境是無(wú)關(guān)的,代碼二里我注釋了關(guān)鍵代碼var a = 2,代碼運(yùn)行結(jié)果發(fā)生了變化,程序報(bào)錯(cuò)了,的確很讓人困惑,困惑之處在于局部作用域里變量定義的位置在變量第一次使用之后,但是程序沒(méi)有報(bào)錯(cuò),這不符合javascript變量未定義既要報(bào)錯(cuò)的原理。
其實(shí)這個(gè)變量任然被定義即內(nèi)存存儲(chǔ)里有了標(biāo)示符,只不過(guò)沒(méi)有被賦值,代碼一則說(shuō)明,內(nèi)部變量a已經(jīng)和外部環(huán)境無(wú)關(guān),怎么回事?如果我們按照代碼運(yùn)行是按照順序執(zhí)行的邏輯來(lái)理解,這個(gè)代碼也就沒(méi)法理解。
其實(shí)javascript里的變量和其他語(yǔ)言有很大的不同,javascript的變量是一個(gè)松散的類型,松散類型變量的特點(diǎn)是變量定義時(shí)候不需要指定變量的類型,變量在運(yùn)行時(shí)候可以隨便改變數(shù)據(jù)的類型,但是這種特性并不代表javascript變量沒(méi)有類型,當(dāng)變量類型被確定后javascript的變量也是有類型的。但是在現(xiàn)實(shí)中,很多程序員把javascript松散類型理解為了javascript變量是可以隨意定義即你可以不用var定義,也可以使用var定義,其實(shí)在javascript語(yǔ)言里變量定義沒(méi)有使用var,變量必須有賦值操作,只有賦值操作的變量是賦予給window,這其實(shí)是javascript語(yǔ)言設(shè)計(jì)者提升javascript安全性的一個(gè)做法。
此外javascript語(yǔ)言的松散類型的特點(diǎn)以及運(yùn)行時(shí)候隨時(shí)更改變量類型的特點(diǎn),很多程序員會(huì)認(rèn)為javascript變量的定義是在運(yùn)行期進(jìn)行的,更有甚者有些人認(rèn)為javascript代碼只有運(yùn)行期,其實(shí)這種理解是錯(cuò)誤的,javascript代碼在運(yùn)行前還有一個(gè)過(guò)程就是:預(yù)加載,預(yù)加載的目的是要事先構(gòu)造運(yùn)行環(huán)境例如全局環(huán)境,函數(shù)運(yùn)行環(huán)境,還要構(gòu)造作用域鏈(關(guān)于作用域鏈和環(huán)境,本文后續(xù)會(huì)做詳細(xì)的講解),而環(huán)境和作用域的構(gòu)造的核心內(nèi)容就是指定好變量屬于哪個(gè)范疇,因此在javascript語(yǔ)言里變量的定義是在預(yù)加載完成而非在運(yùn)行時(shí)期。
所以,引子里的代碼在函數(shù)的局部作用域下變量a被重新定義了,在預(yù)加載時(shí)候a的作用域范圍也就被框定了,a變量不再屬于全局變量,而是屬于函數(shù)作用域,只不過(guò)賦值操作是在運(yùn)行期執(zhí)行(這就是為什么javascript語(yǔ)言在運(yùn)行時(shí)候會(huì)改變變量的類型,因?yàn)橘x值操作是在運(yùn)行期進(jìn)行的),所以第一次使用a變量時(shí)候,a變量在局部作用域里沒(méi)有被賦值,只有棧區(qū)的標(biāo)示名稱,因此結(jié)果就是undefined了。
不過(guò)賦值操作也不是完全不對(duì)預(yù)加載產(chǎn)生影響,預(yù)加載時(shí)候javascript引擎會(huì)掃描所有代碼,但不會(huì)運(yùn)行它,當(dāng)預(yù)加載掃描到了賦值操作,但是賦值操作的變量有沒(méi)有被var定義,那么該變量就會(huì)被賦予全局變量即window對(duì)象。
根據(jù)上面的內(nèi)容我們還可以理解下javascript兩個(gè)特別的類型:undefined和null,從javascript變量存儲(chǔ)的三部分角度思考,當(dāng)變量的值為undefined時(shí)候,那么該變量只有棧區(qū)的標(biāo)示符,如果我們對(duì)undefined的變量進(jìn)行賦值操作,如果值是基本類型,那么棧區(qū)的值就有值了,如果棧區(qū)是對(duì)象那么堆區(qū)會(huì)有一個(gè)對(duì)象,而棧區(qū)的值則是堆區(qū)對(duì)象的地址,如果變量值是null的話,我們很自然認(rèn)為這個(gè)變量是對(duì)象,而且是個(gè)空對(duì)象,按照我前面講到的變量存儲(chǔ)的三部分考慮:當(dāng)變量為null時(shí)候,棧區(qū)的標(biāo)示符和值都會(huì)有值,堆區(qū)應(yīng)該也有,只不過(guò)堆區(qū)是個(gè)空對(duì)象,這么說(shuō)來(lái)null其實(shí)比undefined更耗內(nèi)存了,那么我們看看下面的代碼:
var ooo = null; console.log(ooo);// 運(yùn)行結(jié)果:null console.log(ooo == undefined);// 運(yùn)行結(jié)果:true console.log(ooo == null);// 運(yùn)行結(jié)果:true console.log(ooo === undefined);// 運(yùn)行結(jié)果:false console.log(ooo === null);// 運(yùn)行結(jié)果:true
運(yùn)行之,結(jié)果很震驚啊,null居然可以和undefined相等,但是使用更加精確的三等號(hào)“===”,發(fā)現(xiàn)二者還是有點(diǎn)不同,其實(shí)javascript里undefined類型源自于null即null是undefined的父類,本質(zhì)上null和undefined除了名字這個(gè)馬甲不同,其他都是一樣的,不過(guò)要讓一個(gè)變量是null時(shí)候必須使用等號(hào)“=”進(jìn)行賦值了。
當(dāng)變量為undefined和null時(shí)候我們?nèi)绻麨E用它javascript語(yǔ)言可能就會(huì)報(bào)錯(cuò),后續(xù)代碼會(huì)無(wú)法正常運(yùn)行,所以javascript開(kāi)發(fā)規(guī)范里要求變量定義時(shí)候最好馬上賦值,賦值好處就是我們后面不管怎么使用該變量,程序都很難因?yàn)樽兞课炊x而報(bào)錯(cuò)從而終止程序的運(yùn)行,例如上文里就算變量是string基本類型,在變量定義屬性程序還是不會(huì)報(bào)錯(cuò),這是提升程序健壯性的一個(gè)重要手段,由引子的例子我們還知道,變量定義最好放在變量所述作用域的最前端,這么做也是保證代碼健壯性的一個(gè)重要手段。
下面我們?cè)倏匆欢未a:
var str; if (undefined != str && null != str && "" != str){ console.log("true"); }else{ console.log("false"); } if (undefined != str && "" != str){ console.log("true"); }else{ console.log("false"); } if (null != str && "" != str){ console.log("true"); }else{ console.log("false"); } if (!!str){ console.log("true"); }else{ console.log("false"); } str = ""; if (!!str){ console.log("true"); }else{ console.log("false"); }
運(yùn)行之,結(jié)果都是打印出false。
使用雙等號(hào)“==”,undefined和null是一回事,所以第一個(gè)if語(yǔ)句的寫(xiě)法完全多余,增加了不少代碼量,而第二種和第三種寫(xiě)法是等價(jià),究其本質(zhì)前三種寫(xiě)法本質(zhì)都是一致的,但是現(xiàn)實(shí)中很多程序員會(huì)選用寫(xiě)法一,原因就是他們還沒(méi)理解undefined和null的不同,第四種寫(xiě)法是更加完美的寫(xiě)法,在javascript里如果if語(yǔ)句的條件是undefined和null,那么if判斷的結(jié)果就是false,使用!運(yùn)算符if計(jì)算結(jié)果就是true了,再加一個(gè)就是false,所以這里我建議在書(shū)寫(xiě)javascript代碼時(shí)候判斷代碼是否為未定義和null時(shí)候最好使用!運(yùn)算符。
代碼四里我們看到當(dāng)字符串被賦值了,但是賦值是個(gè)空字符串時(shí)候,if的條件判斷也是false,javascript里有五種基本類型,undefined、null、boolean、Number和string,現(xiàn)在我們發(fā)現(xiàn)除了Number都可以使用!來(lái)判斷if的ture和false,那么基本類型Number呢?
運(yùn)行之,結(jié)果是false。
如果我們把num改為負(fù)數(shù)或正數(shù),那么運(yùn)行之的結(jié)果就是true了。
這說(shuō)明了一個(gè)道理:我們定義變量初始化值的時(shí)候,如果基本類型是string,我們賦值空字符串,如果基本類型是number我們賦值為0,這樣使用if語(yǔ)句我們就可以判斷該變量是否是被使用過(guò)了。
但是當(dāng)變量是對(duì)象時(shí)候,結(jié)果卻不一樣了,如下代碼:
運(yùn)行之,代碼是true。
所以在定義對(duì)象變量時(shí)候,初始化時(shí)候我們要給變量賦予null,這樣if語(yǔ)句就可以判斷變量是否初始化過(guò)。
其實(shí)if加上!運(yùn)算判斷對(duì)象的現(xiàn)象還有玄機(jī),這個(gè)玄機(jī)要等我把場(chǎng)景三講完才能說(shuō)清楚哦。
場(chǎng)景三:復(fù)制變量的值和函數(shù)傳遞參數(shù)
首先看看這個(gè)場(chǎng)景的代碼:
var s1 = "sharpxiajun"; var s2 = s1; console.log(s1);//// 運(yùn)行結(jié)果:sharpxiajun console.log(s2);//// 運(yùn)行結(jié)果:sharpxiajun s2 = "xtq"; console.log(s1);//// 運(yùn)行結(jié)果:sharpxiajun console.log(s2);//// 運(yùn)行結(jié)果:xtq
上面是基本類型變量的賦值,我們?cè)倏纯聪旅娴拇a:
var obj1 = new Object(); obj1.name = "obj1 name"; console.log(obj1.name);// 運(yùn)行結(jié)果:obj1 name var obj2 = obj1; console.log(obj2.name);// 運(yùn)行結(jié)果:obj1 name obj1.name = "sharpxiajun"; console.log(obj2.name);// 運(yùn)行結(jié)果:sharpxiajun
我們發(fā)現(xiàn)當(dāng)復(fù)制的是對(duì)象,那么obj1和obj2兩個(gè)對(duì)象被串聯(lián)起來(lái)了,obj1變量里的屬性被改變時(shí)候,obj2的屬性也被修改。
函數(shù)傳遞參數(shù)的本質(zhì)就是外部的變量復(fù)制到函數(shù)參數(shù)的變量里,我們看看下面的代碼:
function testFtn(sNm,pObj){ console.log(sNm);// 運(yùn)行結(jié)果:new Name console.log(pObj.oName);// 運(yùn)行結(jié)果:new obj sNm = "change name"; pObj.oName = "change obj"; } var sNm = "new Name"; var pObj = {oName:"new obj"}; testFtn(sNm,pObj); console.log(sNm);// 運(yùn)行結(jié)果:new Name console.log(pObj.oName);// 運(yùn)行結(jié)果:change obj
這個(gè)結(jié)果和變量賦值的結(jié)果是一致的。
在javascript里傳遞參數(shù)是按值傳遞的。
上面函數(shù)傳參的問(wèn)題是很多公司都愛(ài)面試的問(wèn)題,其實(shí)很多人都不知道javascript傳參的本質(zhì)是怎樣的,如果把上面?zhèn)鲄⒌睦痈牡膹?fù)雜點(diǎn),很多朋友都會(huì)栽倒到這個(gè)面試題下。
為了說(shuō)明這個(gè)問(wèn)題的原理,就得把上面講到的變量存儲(chǔ)原理綜合運(yùn)用了,這里我把前文的內(nèi)容再?gòu)?fù)述一遍,兩張圖,如下所示:
這是基本類型存儲(chǔ)的內(nèi)存結(jié)構(gòu)。
這是引用類型存儲(chǔ)的內(nèi)存結(jié)構(gòu)。
還有個(gè)知識(shí),如下:
在javascript里變量的存儲(chǔ)包含三個(gè)部分:
部分一:棧區(qū)的變量標(biāo)示符;
部分二:棧區(qū)變量的值;
部分三:堆區(qū)存儲(chǔ)的對(duì)象。
在javascript里變量的復(fù)制(函數(shù)傳參也是變量賦值)本質(zhì)是傳值,這個(gè)值就是棧區(qū)的值,而基本類型的內(nèi)容是存放在棧區(qū)的值里,所以復(fù)制基本變量后,兩個(gè)變量是獨(dú)立的互不影響,但是當(dāng)復(fù)制的是引用類型時(shí)候,復(fù)制操作還是復(fù)制棧區(qū)的值,但是這個(gè)時(shí)候值是堆區(qū)對(duì)象的地址,因?yàn)閖avascript語(yǔ)言是不允許操作堆內(nèi)存,因此堆內(nèi)存的變量并沒(méi)有被復(fù)制,所以復(fù)制引用對(duì)象復(fù)制的值就是堆內(nèi)存的地址,而復(fù)制雙方的兩個(gè)變量使用的對(duì)象是相同的,因此復(fù)制的變量其中一個(gè)修改了對(duì)象,另一個(gè)變量也會(huì)受到影響。
原理講完了,下面我列舉一個(gè)拔高的例子,代碼如下:
var ftn1 = function(){ console.log("test:ftn1"); }; var ftn2 = function(){ console.log("test:ftn2"); }; function ftn(f){ f(); f = ftn2; } ftn(ftn1);// 運(yùn)行結(jié)果:test:ftn1 console.log("====================華麗的分割線======================"); ftn1();// 運(yùn)行結(jié)果:test:ftn1
這個(gè)代碼是很早之前有位朋友考我的,我當(dāng)時(shí)答對(duì)了,但是我是蒙的,問(wèn)我的朋友答錯(cuò)了,其實(shí)當(dāng)時(shí)我們兩個(gè)都沒(méi)搞懂其中緣由,我朋友是這么分析的他認(rèn)為f是函數(shù)的參數(shù),屬于函數(shù)的局部作用域,因此更改f的值,是沒(méi)法改變ftn1的值,因?yàn)榈搅送獠孔饔糜騠就失效了,但是這種解釋很難說(shuō)明我上文里給出的函數(shù)傳參的實(shí)例,其實(shí)這個(gè)問(wèn)題答案就是函數(shù)傳參的原理,只不過(guò)這里加入了個(gè)混淆因素函數(shù),在javascript函數(shù)也是對(duì)象,局部作用域里f = ftn2操作是將f在棧區(qū)的地址改為了ftn2的地址,對(duì)外部的ftn1和ftn2沒(méi)有任何改變。
記?。簀avascript里變量復(fù)制和函數(shù)傳參都是在傳遞棧區(qū)的值。
棧區(qū)的值除了變量復(fù)制起作用,它在if語(yǔ)句里也會(huì)起到作用,當(dāng)棧區(qū)的值為undefined、null、“”(空字符串)、0、false時(shí)候,if的條件判斷則是為false,我們可以通過(guò)!運(yùn)算符計(jì)算,因此當(dāng)我們的代碼如下:
結(jié)果則是true,因?yàn)関ar obj = {}相當(dāng)于var obj = new Object(),雖然對(duì)象里沒(méi)什么內(nèi)容,但是在堆區(qū)里,對(duì)象的內(nèi)存已經(jīng)分配了,而變量棧區(qū)的值已經(jīng)是內(nèi)存地址了,所以if語(yǔ)句判斷就是true了。
看來(lái)本主題又沒(méi)法寫(xiě)完,其實(shí)本來(lái)我寫(xiě)本文是想講new,prototype,call(apply)以及this,沒(méi)想講變量定義就講了這么多,算了,先發(fā)表出來(lái)吧,吃了晚飯接著寫(xiě),希望今天寫(xiě)完。
聯(lián)系客服