- 看透JavaScript:原理、方法與實踐
- 韓路彪
- 11430字
- 2020-11-28 15:50:44
4.3 處理業務
在JS中,使用函數(function)對象用來處理業務是最常見的用法。JS中真正對對象的操作大部分都是通過函數對象來執行的。函數的創建方式,前面已經介紹過,其中用來處理業務的是函數體。函數體主要包括變量、操作符和語句三大部分內容。
本節首先介紹JS中的函數變量、操作符和語句,然后介紹函數中最容易出問題的變量作用域和閉包。因為本書不是針對零基礎讀者的,所以對函數的變量、操作符和語句這部分的介紹并不會非常詳細、面面俱到。如果大家需要詳細學習這部分內容,那么可以參考其他相關資料。
4.3.1 變量
在函數中,變量的主要作用是暫存業務處理過程中用到的一些值。JS中的變量是使用var關鍵字來定義的,無論什么類型的變量都使用它來定義,因此JS是一種弱類型語言。但是,它的變量是區分大小寫的,也就是說,red、Red、reD是三個不同的變量。JS中的變量早期要求必須是用 “_”、“$”、英文字母和數字組成,而且第一個字符不可以是數字。對于現在的引擎來說,只要在詞法解析的時候不引起誤會就可以了,即使使用中文也是可以的。
另外,變量也不可以使用JS的關鍵字和保留字,這在任何語言里都是一樣的,否則就亂套了。
ES5.1中規定了以下關鍵字。
break do instanceof typeof case else new var catch finally return void continue for switch while debugger function this with default if throw delete in try
ES2015中規定了以下關鍵字。
break do instanceof typeof case else in var catch export new void class extends return while const finally super continue wit for switch yield debugger function this default if throw delete import try
除了上面的關鍵字外,還有一些詞雖然不是關鍵字,但是在后續版本中可能會成為關鍵字,現在是保留字,最好也不要使用。保留字可分為普通保留字和嚴格模式的保留字兩種類型,嚴格模式的保留字只在“strict model”模式中會報錯。
ES5.1中的普通保留字如下。
class enum extends super const export import
ES2015中的普通保留字如下。
enum await
ES5.1中的嚴格模式保留字如下。
implements let private public yield interface package protected static
ES2015中的嚴格模式保留字如下。
implements package protected interface private public
多知道點
JS中的strict model
ES5中引入了strict model(嚴格模式)。嚴格模式下的JS程序需要比非嚴格模式下的程序更加規范。嚴格模式對語法做了比較嚴格的要求,例如,不可以使用with語句、不可以重復定義變量、不可以不定義變量直接使用(在非嚴格模式下會自動定義為全局變量)等。如果使用嚴格模式,那么只需要在代碼中加入“strict model”字符串就可以了。可以將它用到全局中,也可以用到指定的函數中。如果只用到指定的函數中,則只需要在函數內部添加“strict model”字符串就可以了。
非嚴格模式主要是為了向前兼容。在新寫的程序中應該盡量使用嚴格模式,不過最好局部使用而不是全局使用的,因為頁面中可能還會引用別人寫的代碼,例如一些庫文件,它們不一定是按嚴格模式寫的,所以最好使用局部的(function級)嚴格模式。
4.3.2 操作符
相信大家對操作符都不陌生,操作符是直接告訴編譯器怎么操作對象的工具。本節首先給大家列出了ES2015中的所有操作符,并對每個操作符列出其功能,列舉相應的示例,最后再對一些特殊的操作符或者特殊的用法進行單獨講解。
1. ES2015操作符列表
表4-1列出了ES2015中所用的操作符。
表4-1 ES2015所用的操作符

(續)

2.使用細節與技巧
(1)==和===
這兩個符號都是用來判斷是否相等的,但是具體用法存在一些區別。因為JS是一種弱類型語言,所以不同類型之間也可以比較是否相等,如果用==則會比較轉換后的值是否相等,如果用===比較則只要類型不同就返回false,例如下面的例子。
var a= 1, b="1"; console.log(a==b); //輸出true console.log(a===b); //輸出false
(2)>>和>>>
這兩個符號的作用都是右移位,下面先來介紹一下左移和右移的概念。在計算機中保存的數據(主要指整數)也像平時所寫的數一樣,除了數字本身外還有“位”的概念,例如4567這個數的千位為4,百位為5,十位為6,個位為7,把不同的數字放到不同的位,其權重就不一樣了。假如現在的數只有4位(就像某些需要填數字的單據上一格填一個數字,一共有4格,只能填4個數字),那么4567左移一位就是5670,左移兩位就是6700,而右移一位就成了0456,右移兩位就成了0045。在計算機中的左移右移跟這里類似,只是它不一定是4位,而且每一位都只能是0或1。在程序中經常使用左移右移來做2的整數倍的乘除法,這就像十進制中左移一位擴大10倍右移一位縮小到原來的1/10一樣,不過左移右移要比乘除法的計算簡單很多。對于處理器來說也一樣,移位要比計算乘除速度快。
不過計算機中的移位跟上述十進制的移位還是存在區別的。在計算機中因為只有0和1,所以為了區分正負數,現有的做法是將負數用補碼來表示,這樣只要看最高位是0還是1,就可以區分正負數了(如果讀者不明白補碼也沒關系,這里只要知道有符號數將最高位用作符號位,符號位0表示正數,符號位1表示負數即可)。但是這時候問題就來了,對于一個負數來說,右移一位后其符號位(最高位)正常應該變成0,這樣就變成正數了,此時就會出現問題了,因此在右移的時候對有符號數和無符號分別使用了不同的操作符。
當然,這只是表層的區別,在底層有符號數和無符號數使用的是兩套不同的進位/溢出標志,例如x86處理器中有符號數用OF,無符號數用CF標志。另外,數據本身只是一串由0和1組成的編碼,是無法區分有符號數還是無符號數的,只是人為將其看作有符號數或者無符號數而已(在程序的底層會通過類型標志進行區分)。對于一些強類型語言來說,在定義數據的時候就會指定數據的類型,這樣在使用時,就可以清楚地知道應該將其看作有符號數還是無符號數。因為JS是一種弱類型語言,變量的類型可以任意轉換,所以對于-1>>>0這樣的操作來說,操作的目標是表示-1的一串數(對于32位數來說就是0XFFFFFFFF,即32個1),操作的過程是將其當作無符號數左移0位,這時雖然不會對數字本身做任何修改,但是,因為在操作的過程中已經將其看作無符號數,所以其結果也就變成了4294967295,即無符號數的32個1,同樣這也是有符號數-1的32位編碼。
(3)&&和||
邏輯與操作符&&的判斷邏輯是依次判斷每個表達式,當遇到false的表達式時就會馬上返回而不再繼續執行后面的表達式,如果所有表達式都為true,則返回true,例如下面的語句。
(表達式1)&& (表達式2) && (表達式3) && (表達式4) && (表達式5)
當表達式1和表達式2都為true而表達式3為false時,表達式4和表達式5就不再判斷了,也就不會被執行。利用這個特性可以執行一些條件語句,例如下面的語句。
b=a>5&&a-3;
這條語句在a>5的時候就會執行a-3并將計算結果賦值給b,否則將false賦值給b,相當于下面的語句。
if(a>5){ b=a-3; }else{ b=false; }
通過多個表達式的邏輯與組合可以很便捷地完成一組相互具有依賴性的操作。
邏輯或操作符||使用好了也非常方便。它的判斷邏輯是依次判斷每個表達式,當遇到第一個true表達式的時候,馬上返回而不再繼續執行后面的表達式,如果所有表達式都為false則返回false,例如下面的語句。
typeof jQuery! ="undefined"||importjQuery();
這條語句首先通過判斷jQuery是否存在來判斷是否需要引入jQuery,如果不存在,則調用importjQuery函數導入jQuery,否則就不導入。
另外,點操作符也非常重要,后面會進行詳細介紹。按位操作在硬件中使用得比較多,在軟件編程中主要用于組合或提取某個標志量,在后面用到的時候還會介紹。“…”和“=>”是ES2015中新增的操作符,后面會進行專門講解。其他操作符比較簡單,通過表4-1中的例子可以很容易地理解,本書就不詳細介紹了。
4.3.3 語句
語句是用來執行具體功能的,可以分為單條語句和語句塊。單條語句以分號結束,語句塊用花括號包含,語句塊可以包含多條語句。
語句主要由變量(或屬性)、操作符和關鍵字組成。有的語句使用操作符完成,還有的語句需要使用相應的關鍵字。使用操作符的語句只需要按照4.3.1節介紹的各種操作符的使用方法來完成就可以了。下面將介紹使用關鍵字完成的語句。
1. var和let語句
var關鍵字用來定義變量,如果在函數內部定義,則定義的變量只能在函數內部使用,如果在函數外部定義,則會成為全局變量。在函數中使用變量時會優先使用內部變量,只有找不到內部變量時才會使用全局變量,下面來看個例子。
var color = "red"; function localColor(){ var color = "blue"; console.log(color); } function globalColor(){ console.log(color); } localColor(); //blue globalColor(); //red
localColor方法中由于定義了內部變量color,因此會輸出blue; globalColor方法中由于沒有定義內部變量color,因此會輸出全局變量red。
多知道點
在JS中函數是怎么執行的
函數無非兩部分:數據和對數據的操作。數據又分為外部數據和內部數據,對于外部數據,本書將在后邊的作用域鏈中進行介紹。內部數據又分為參數和變量兩部分。在函數每次執行的時候參數都會被賦予一個新值,而變量則每次都會被設置為一個相同的初始值。
函數的變量和參數是怎么保存的呢?對于多個數據來說,最常用也是最簡單的保存方式就是使用數組保存,這樣按序號查找起來就非常方便了。而且,一般來說,一個函數的參數和變量都會集中保存在一個數組或者跟數組類似的結構(例如棧)中。但是,數組本身存在一個非常致命的缺點,它要求每個元素的長度都相等,這對于參數(或變量)來說是很難符合要求的。但是,為了使用數組(或棧)的便捷性通常會在數組中保存一個包含地址的數據(除地址外,還可能包含數據類型等其他數據),而不是實際的數據,這樣既可以使用數組,又可以保存不同長度的數據。此時,在函數中使用參數(或變量)的時候只需要使用“第幾個參數(或變量)”就可以了,至于數組中具體一個元素使用多少位,則需要根據不同的硬件平臺(例如,是32位還是64位)和具體引擎的開發者來確定。但是,這里還存在一個小問題,對于復雜的數據來說,這樣保存無可厚非,而對于直接使用數組元素就可以保存的簡單數據(例如整數)來說,再使用這種方式就顯得復雜了,而且多一步通過地址查找數據的操作也會影響效率,因此這種情況一般會直接將值保存到數組中,而不是保存地址。
函數在每次執行之前都會新建一個參數數組和一個變量數組(當然也可以合并為一個數組,而且通常會使用棧來實現),然后將調用時所傳遞的參數設置到參數數組中,而變量數組在每次執行前都具有相同的內容,對數據進行操作時只需要使用“第幾個參數”或者“第幾個變量”即可。
簡單的數據(例如整數)會直接保存在數組中,而對于復雜的數據,數組中只保存地址,具體的數據保存在堆中。可以簡單地將堆理解為一堆草紙,其所保存的數據是所有函數所共享的,不過也并不是每個函數都可以調用堆中所有的數據。因為調用堆中數據的前提是能找到,如果找不到當然也就調用不了。例如,在函數中定義了一個字符串的對象變量s,這時就會將s的內容保存到堆中,然后將堆中所保存數據的地址保存到函數的變量數組中,這時對于函數外部來說,雖然可以訪問堆中的數據,但是因為沒有s的地址,所以也就無法訪問s這個字符串變量了。
下面來看一個例子,首先對下面的代碼設置斷點,然后使用FireBug進行調試。
function paramF(p1){ var msg="hello"; console.log(p1); //此行設置斷點 for(var i in arguments){ console.log(arguments[i]); } } paramF("a", "b", "c"); //a a b c
當執行到斷點處時,在FireBug中可以看到如圖4-6所示的結構。

圖4-6 FireBug的調試界面
從圖4-6的右側可以看出,函數在執行時會將參數p1和函數中所用到的變量msg、i放到相同的地位,即在函數內部執行的時候就不會區分是參數還是變量。另外,在JS的函數中,會自動創建一個名為arguments的內部變量,然后將所有參數的地址保存到其中。arguments類似數組對象,可以通過它來獲取函數調用時所傳遞的參數。接著來看下面的例子。
function paramF(p1){ console.log(p1); for(var i in arguments){ console.log(arguments[i]); } } paramF("a"); //a a paramF("a", "b", "c"); //a a b c
paramF方法首先打印了參數p1的值,然后遍歷打印arguments中所有參數的值。可以看出,參數p1的值和arguments[0]的值是一樣的,函數的參數按順序依次保存在arguments變量中。還可以看到,在調用函數時傳入參數的個數也可以和定義時不一樣。例如,雖然paramF函數定義時只有一個參數,但是在調用時卻可以傳遞三個參數,當然也可以傳遞任意個數的參數,甚至不傳遞參數,因此JS中不存在同名函數重載的用法。
函數定義時的參數(通常叫形參)和arguments對象的關系如下:在JS的函數調用前JS引擎會創建一個arguments對象,然后在其中保存調用時的參數(通常叫實參),而形參其實只是一個名字,在實際操作時會將其翻譯為arguments對象的一個元素。例如,對于“console.log(p1); ”這條語句,在操作時會被翻譯為“控制臺打印arguments的第一個元素”,即函數的形參只是一個名字,是給程序員看的,引擎在實際操作時會自動將其翻譯為arguments中的一個元素,可以使用下面的例子來驗證。
function paramF(p1){ console.log(arguments[0]===p1); } paramF("www.excelib.com"); //true
當然,這里給大家介紹的只是一種實現方案,還有其他方案。例如,可以直接把參數對象放入棧中,不同參數可以使用偏移量來表示,不過原理都是一樣的。
在JS中使用var定義的變量是函數級作用域而不是塊級作用域,即一個語句塊內部定義的變量在語句塊外部也可以使用,例如下面的例子。
(function (num){ if(num>36){ var result=true; } console.log(result); })(81); //true
這里的result是在if語句塊中定義的,但是在if語句塊外部依然可以調用。這是因為JS的方法在執行時會將其自身所有使用var定義的變量統一放到前面介紹的變量數組中,所以在一個函數中,所有使用var定義的變量都是同等地位的,即在JS中使用var定義的變量是function級作用域而不是塊級作用域。
多知道點
JS中自運行的匿名函數
在JS中可以使用匿名函數,其原理非常簡單。前面說過,在JS中函數其實也是一種對象,在底層只要用一塊內存將其保存下來即可。在調用時只需要找到這塊內存,然后創建好執行環境(包含前面所介紹的參數數組、變量數組等內容)就可以執行了。所以有兩個關鍵方面:①將函數對象保存到一塊內存中;②找到這塊內存。通常使用函數名來查找這塊內存的地址,不過函數名只是查找這塊內存的一個工具,最主要的目的其實是找到這塊內存,也就是說,即使沒有函數名也可以,只要能找到這塊內存就行。因此可以使用匿名函數,其用法如下。
首先使用function關鍵字定義一個函數,然后將其使用小括號括起來(這只是語法的要求,否則后面的執行語句無法被引擎正確識別),這樣就將函數定義好了,引擎會為其分配一塊內存來保存。然后直接在后面加個小括號,并將參數放入其中,這樣引擎就知道要使用這塊內存所保存的函數來執行了。因為對于JS來說,在函數后面加小括號是調用函數的意思,這是JS的語法規則(如果是我們來設計,當然也可以設計為見到“調用XXX”的字符串執行函數)。這時既有保存函數的內存也有內存的地址,這樣就可以執行了。例如,上面的例子中首先定義了一個函數,然后使用小括號將其擴起來,后面又加了一個表示執行的小括號,并將參數81放入其中,這樣就可以執行了。
下面再來看一個例子。
var log=(function (){ console.log("創建日志函數"); return function(param){ console.log(param); }; })(); log("www.excelib.com");
這里也創建了一個自運行的匿名函數,不過其返回值仍然是一個匿名函數,也就是說函數自運行后返回的結果仍然是一個函數。把返回的函數賦值給log變量,就可以使用log變量來調用返回的函數了(注意與前面所介紹的函數表達式創建函數的區別)。這里其實包含兩塊保存函數的內存,自運行的匿名函數本身有一塊內存來保存,當碰到后面表示執行的小括號后就會自動執行,另外還有一塊內存來保存所返回的函數,而返回的值其實是這塊內存的地址,這樣log變量指向了這塊保存函數的內存,因此也可以使用log來調用此函數。
雖然JS表面看起來有很多復雜的東西,但只要理解了其本質(特別是內存模型)后就很簡單了。
在ES2015中可以使用let來定義塊級變量,這樣定義的變量在塊的外部不可以使用。例如,將前面例子中的result改用let來定義,在if語句塊外面就無法使用result輸出結果了。
2. if-else語句
if-else語句的作用是進行條件判斷。當需要進行判斷時,就要使用if語句來完成,其結構如下。
if(條件){ 語句塊 }
當條件為true時,執行語句塊里的相應內容,否則不執行,例如下面的例子。
function sayHello(lang){ var hello = "你好"; if(lang=="en-us"){ hello = "hello"; } return hello; } sayHello(); //你好 sayHello("en-us"); //hello
在sayHello方法中,如果傳入值為“en-us”的lang參數,則會返回“hello”,否則會返回“你好”。
有些時候需要對多種情況進行判斷,可以組合使用if-else語句,在else后邊可以接著寫if語句,表示對另外一種情況的判斷,也可以不跟if語句,用來表示如果所有條件都不符合時執行的默認操作,例如,將前面的例子做如下修改。
function sayHello(lang){ var hello; if(lang=="en-us"){ hello = "hello"; }else if(lang=="zh-tw"){ hello = "妳好"; }else if(lang=="zh-hk"){ hello = "妳好"; }else{ hello = "你好"; }
return hello; }
這時sayHello方法就對“en-us”“zh-tw”和“zh-hk”三種情況做了判斷,如果都不是就會執行最后的默認語句塊,即將hello設置為“你好”。
if語句的條件還可以使用前面介紹過的邏輯操作符來對多個條件進行組合判斷,例如用||表示或,用&&表示與,用!表示非,上面例子中的sayHello方法可以寫成下面的形式。
function sayHello(lang){ var hello; if(lang=="en-us"){ hello = "hello"; }else if(lang=="zh-tw" || lang=="zh-hk"){ hello = "妳好"; }else{ hello = "你好"; } return hello; }
這個例子就把lang=="zh-tw"和lang=="zh-hk"兩個條件合并到一起組成一個條件,當lang為“zh-tw”或者“zh-hk”的時候都會給hello賦值 “妳好”。
3. while語句
while語句和if語句的結構相同,都是一個條件和一個語句塊,只是關鍵字不同,while語句的結構如下。
while(條件){ 語句塊 }
while語句和if語句的不同之處在于:if語句如果條件成立的話會且只會執行一次語句塊里的內容,而while語句會反復執行語句塊里的內容,直到條件不成立為止。我們來看下面的例子。
var step = 5; function widthTo(obj, width){ var owidth = obj.width; var isAdd = width - owidth>0; while(owidth! =width){ if(isAdd){ owidth+=step; }else{ owidth-=step; } if(width-owidth<step || owidth-width<step){ owidth = width; }
sleep(50); //休眠50ms, JS自身并無此函數,這里只是為了說明問題 obj.width = owidth; } }
在這個例子中,widthTo方法的作用是將相應對象的width屬性修改為指定大小,并且不是一次修改到位而是按指定步長逐次修改,具體修改時需要先判斷是變大還是變小,然后再修改,每次修改等待50ms,這么做就可以給人一種簡單動畫的感覺。這里使用的就是while循環,當對象的width屬性沒有達到目標大小時就會一直向目標方向變化,只有達到目標大小時才會停止并繼續向下執行。while語句也可以理解為執行多次if語句,例如,上面代碼中的while語句相當于下面的語句。
if(owidth! =width){ 具體操作; } if(owidth! =width){ 具體操作; } if(owidth! =width){ 具體操作; } ……
當if語句塊足夠多的時候也可以完成與while語句相同的功能。當然,實際使用時沒有這么用的,這里只是為了讓大家更加清晰地理解while語句的原理。
4. do-while語句
do-while語句和while語句類似,只是while語句的條件判斷在語句執行之前,而do-while語句的條件判斷在語句執行之后,do-while語句的結構如下。
do{ 語句塊 } while(條件)
在do-while語句中,每執行完一次語句之后進行一次條件判斷,如果條件成立,就會循環執行,直到條件不成立為止。由于do-while語句是在執行語句之后判斷,所以語句塊至少會執行一次。我們來看下面的例子。
function getTopLeader(person){ var leader; do{ leader = person; person = leader.getLeader(); }while(person! =null); return leader; }
這個例子中,getTopLeader方法要獲取TopLeader(最高領導)。要找最高領導很簡單,隨便找個人問他的領導是誰,如果他有領導,則繼續問他的領導的領導是誰,直到有人說我沒有領導,好,他就是最高領導,我們將他返回去就可以了。這里使用了do-while語句。do-while語句與while語句的區別是,do-while語句至少會執行一次語句塊的內容。在上面的例子中,首先查詢上級領導,然后判斷是否為空,查詢上級領導的操作至少會執行一次,因此我們使用了do-while語句。其實do-while語句也可以換成while語句來執行,只需要在執行前先執行一次語句塊中的內容。例如,上面例子中的getTopLeader方法也可以寫成下面的形式。
function getTopLeader(person){ var leader = person; person = leader.getLeader(); //在while前先執行了一次 while(person! =null){ leader = person; person = leader.getLeader(); } return leader; }
這兩種寫法的效果是完全相同的。
5. for語句
for語句也是一種循環語句,并且比while語句和do-while語句更加靈活。for語句的結構如下。
for(表達式1; 表達式2; 表達式3){ 語句塊 }
for語句的執行步驟如下。
1)執行表達式1,一般用來定義循環變量。
2)判斷表達式2是否為真,如果不為真則結束for語句塊。
3)執行語句塊。
4)執行表達式3,一般用于每次執行后修改循環變量的值。
5)跳轉到第2步重新判斷。
這里要特別注意表達式3的執行位置,可以通過下面的例子清晰地看到上述過程。
var k; for(var i=0, j=""; i<=1; i++, j=1, k=1){ console.log(i); console.log(typeof i); console.log(typeof j); console.log(typeof k);
console.log("----------------"); }
這里的表達式1和表達式3都通過使用逗號來并列使用多條語句。表達式1為var i=0, j="",定義了兩個變量i和j, i是number類型,值為0, j為string類型,值為空字符串;表達式2為i<=1,判斷i是否小于等于1;表達式3為i++, j=1, k=1,共三條語句,i加1并將j和k設置為1,這里的j由原來的string類型轉換為number類型,并且初始化了k變量。執行后輸出結果如下。
0 number string undefined ---------------- 1 number number number ----------------
從輸出結果可以看出,在第一次執行語句塊的時候(i為0)表達式1已經執行了,因此這時i為number類型,j為string類型,k還沒有初始化。第一次執行完語句塊之后會執行表達式3,這時將j變為number類型并將k初始化,所以第二次輸出的j和k都是number類型了。第二次執行完語句塊之后會再次執行表達式3(每次執行完語句塊之后都會執行表達式3),這時會將i變為2,然后判斷表達式2的時候就不符合條件了,也就不再循環。
我們再來看一個計算10的階乘的例子。
var n=10, result; for(var i=1, result=1; i<=n; i++){ result*=i; }
這個例子中,首先定義了兩個變量n和result, n用來指定要計算誰的階乘,result用于保存計算的結果,然后使用for語句執行具體階乘計算并將結果保存到result中。
for語句中包含表達式1、表達式2、表達式3,并且都可以為空,但是它們之間的分號不可以省略,如果不需要相應的表達式,可以不寫內容但是不可省略分號,例如下面的代碼。
var n=10, result= 1, i=1; for(; i<=n; ){ result*=i; i++; }
這個例子的執行結果和上一個例子的執行結果完全相同,只是將循環變量i的定義放到for語句前面并將i++放到語句塊中。需要注意的是,雖然這時for語句中的表達式1和表達式3都不需要了,但是分號是不可以省略的。只有表達式2的這種情況可以簡單地使用while循環來完成,例如上面的代碼還可以寫出如下形式。
var n=10, result= 1, i=1; while(i<=n){ result*=i; i++; }
從這里就可以看出for循環的功能是最強大的,而while和do-while循環在特定條件下的使用方式會比較簡單。
6. for-in語句
for-in語句可以遍歷對象的屬性,準確來說是遍歷對象中可以遍歷的屬性(對象的屬性是否可遍歷會在后面詳細講解)。for-in語句的結構如下。
for(屬性名in對象){ 語句塊 }
for-in語句在遍歷過程中直接獲取的是屬性的名稱,可以使用方括號來獲取屬性的值,例如下面的例子。
var obj = {n:1, b:true, s:"hi"}; for(var propName in obj){ console.log(propName + ":" + obj[propName]); }
執行后輸出結果如下。
n:1 b:true s:hi
ES2015中新增了for-of語句,它可以直接獲取屬性的值,后面再詳細介紹。
7. continue語句
continue用于循環語句塊中,作用是跳過本次循環,進行下一次循環語句塊的執行(即跳過循環體中未執行的語句),在進入下一次執行之前也會判斷條件是否成立,如果條件不成立就會結束循環。另外,如果是for語句,那么在執行判斷前還會先執行表達式3。請看下面的例子。
var array = ["a", "b"]; array[3] = "c"; for(var i=0; i<array.length; i++){ console.log("enter block") if(! array[i]){
continue; } console.log(i+"->"+array[i]); }
在此例中,首先定義了一個數組array,并對其進行初始化。需要注意的是,數組的第一個元素的編號是0,因此array數組的a元素和b元素分別對應編號0和1。然后又給編號為3的元素賦值c,這時array數組的編號為2的元素是沒有內容的。array數組初始化完之后,使用for循環遍歷其中的元素并輸出,在輸出之前判斷是否存在,如果不存在,則使用continue語句跳過本次執行(輸出),進入下一次循環,最后的執行結果如下。
enter block 0->a enter block 1->b enter block enter block 3->c
當i為2時進入循環語句塊,這時會輸出“enter block”,但是當判斷到array[2]不存在時,就會跳過而不執行輸出語句,然后接著執行i++,判斷條件并進入i=3時語句塊的執行,這時又會輸出“enter block”,因此輸出的結果中有兩次連續的“enter block”。
8. break語句
break語句可以用在循環中,也可以用在switch語句中。對于switch語句中break的用法,我們放到switch語句中講解。在循環語句中,break的作用是跳出循環,它跟continue的區別是break會直接結束循環,而continue只是跳過本次語句塊的執行而不會結束循環。將上一小節中的continue改為break,代碼如下。
var array = ["a", "b"]; array[3] = "c"; for(var i=0; i<array.length; i++){ console.log("enter into block") if(! array[i]){ break; } console.log(i+"->"+array[i]); }
這時控制臺輸出的結果如下。
enter into block 0->a enter into block 1->b enter into block
當i=2時會結束循環,所以編號為3的元素就不會被打印。
9. return語句
return語句在函數中的作用是結束函數并返回結果。返回的結果跟在return語句后面,使用空格分開,例如下面的add函數。
function add(a, b){ var c = a+b; return c; }
add函數將兩個參數a、b的值相加后賦值給內部變量c并返回。return語句除了可以返回變量外,還可以直接返回表達式,即上述代碼中的add函數可以不定義c變量而直接返回a+b,形式如下。
function add(a, b){ return a+b; }
return除了返回結果外,還經常用于結束函數的執行,例如下面的代碼。
function setColor(obj, color){ if(typeof obj ! = "object"){ return; } if(color! ="red" && color! ="green" && color! ="blue"){ return; } obj.color = color; }
這個例子中,setColor函數的作用是將obj的color屬性設置為指定的值。首先判斷obj是不是object類型,如果不是則不進行操作而直接返回,然后判斷color是不是red、green、blue中的一種,如果不是也不進行操作而直接返回,最后將obj的color屬性設置為傳入的color。這里的return語句的主要功能是結束函數執行。
10. with語句
JS是一種面向對象的語言,對象主要是通過其屬性來使用,如果需要對同一個對象的多個屬性多次進行操作,就需要多次寫對象的名稱,例如下面的操作。
$("#cur").css("color", "yellow"); $("#cur").css("backgroundColor", "red");
這個例子中重復使用了$("#cur"),這時就可以使用with語句。
with ($("#cur")){ css("color", "yellow"); css("backgroundColor", "red"); }
這兩種寫法的作用是相同的,只是將所要操作的對象$("#cur")統一放到with語句的小括號中。with語句的作用就是指定所操作的對象,但是由于它會影響運行速度且不利于優化,所以不建議使用,并且在strict model(嚴格模式)下已經禁用了with語句。
11. switch-case-default語句
switch語句用于對指定變量進行分類處理,不同的類型使用不同的case語句進行區分,例如下面的例子。
var grade = "優"; switch (grade){ case "不及格": console.log("低于60分"); break; case "及格": console.log("60到75分"); break; case "良": console.log("75到90分"); break; case "優": console.log("90分(含)以上"); break; }
上述代碼根據不同的等級輸出相應分數的范圍,等級寫在圓括號中,每種類型的值寫到case后面。需要注意的是,case和值之間由空格分隔,值后面有冒號,而且每一種情況結束后都要使用break語句跳出,否則會接著執行下一種類型的相應語句。
另外,在switch語句中經常會用到default語句,用于在所有case都不符合條件時執行。default語句應放在所有case語句之后,例如下面的例子。
var grade = "良好"; switch (grade){ case "不及格": console.log("低于60分"); break; case "及格": console.log("60到75分"); break; case "良": console.log("75到90分"); break; case "優": console.log("90分(含)以上"); break; default : console.log("沒有這種等級"); }
上面的代碼中,grade的值為“良好”,而“良好”在所有的case中都沒有,這時就會執行default并打印“沒有這種等級”。
switch和if-else語句的區別是,switch語句只是對單一的變量進行分類處理,而if-else可以在不同的判斷條件中對不同的變量進行判斷。因此,if-else語句更加靈活,switch語句更加簡單、清晰。
12. try-catch-finally語句
try語句用于處理異常,其結構如下。
try{ 正常語句塊 } catch (error) { 異常處理語句塊 } finally { 最后執行語句 }
當“正常語句塊”在執行過程中發生錯誤時,就會執行“異常處理語句塊”,而無論是否拋出異常都會執行“最后執行語句”。我們來看下面的例子。
function getMessage(person){ try{ var result = person.name+", "+person.isEngineer(); }catch (error){ console.log(error.name+":"+error.message); result = "處理異常"; }finally{ return result; } } var msg = getMessage({name:"張三"}); console.log(msg);
這個例子中,getMessage方法中將傳入person的name屬性和isEngineer()方法的返回值連接到一起并返回,連接過程在try正常語句塊中。如果在處理過程中遇到異常,就會執行catch的異常處理語句塊。這里的異常處理語句塊在控制臺打印出異常信息并將“處理異常”賦給返回值,最后在finally語句中將結果返回,無論拼接過程是否發生了異常,最終都會返回result。在最后的調用語句中,傳入的對象只有name屬性而沒有isEngineer方法,此時,執行就會拋出異常,進而會執行異常處理語句塊,打印異常信息并將“處理異常”賦值給返回值。上述代碼的執行結果如下。
TypeError:person.isEngineer is not a function 處理異常
第一行是在異常處理語句塊中打印的,第二行是getMessage調用完后的返回值,是最后一行的代碼打印的。在實際使用中,應該在捕獲到異常后,返回事先指定好的一個值,而不是直接返回一個字符串,例如可以為getMessage對象定義一個專門用來表示異常的返回值屬性。
getMessage.errorMsg="處理異常";
這樣在調用getMessage函數之后,就可以使用它來判斷是否正確執行。例如,可以按照如下形式使用。
var msg = getMessage({name:"張三"}); if(msg === getMessage.errorMsg){ //getMessage方法執行異常后的操作 }else{ console.log(msg); }
另外,如果沒有必須執行的語句,那么也可以省略finally語句塊。
13. throw語句
throw用于主動拋出異常。JS中throw拋出的異常可以是任何類型的,而且可以使用try-catch語句進行捕獲,catch捕獲到的就是throw所拋出的,例如下面的例子。
function add5(n){ if(typeof n ! = "number") throw "不是數字怎么加"; return n+5; } try{ add5("a"); }catch (error){ console.log(error); }
在這個例子中,add5方法的作用是將傳入的參數加5后返回。在運算之前先判斷傳入的參數是否為數字類型,如果不是則拋出異常,異常信息為“不是數字怎么加”,拋出異常后就不再往下執行了。在調用時因為傳入了非數字的“a”,所以會拋出異常,異常信息可以通過catch來捕獲并輸出到控制臺,代碼執行后控制臺會打印“不是數字怎么加”。當然,如果想和其他JS異常使用統一的異常處理結構,那么拋出的異常也可以封裝為包含name和message屬性的對象,例如上面的例子可以寫成如下形式。
function add5(n){ if(typeof n ! = "number") throw {name:"類型錯誤", message:"不是數字怎么加"}; return n+5; } try{ add5("a"); }catch (error){ console.log(error.name+":"+error.message); }
14. typeof語句
typeof語句的作用是獲取變量的類型,調用語法如下。
typeof變量
typeof在ES2015中的返回值一共有7種:undefined、function、object、boolean、number、string和symbol,請看下面的例子。
var a; console.log(typeof undefined); //undefined console.log(typeof a); //undefined console.log(typeof b); //undefined console.log(typeof null); //object console.log(typeof function(){}); //function console.log(typeof {}); //object console.log(typeof true); //boolean console.log(typeof 123.7); //number console.log(typeof "str"); //string console.log(typeof Symbol("abc")); //symbol console.log(typeof [1,2,3]); //object
需要特別注意的是,null和數組的類型都是object,因為null本身也是一個對象,而數組中可以包含其他任何類型的元素,它并不是底層對象,所以它們沒有自己獨有的類型。
undefined是一種特殊的類型,它所代表的是這么一種類型:它們有名字,但是不知道自己是什么類型。即只要有名字但是沒有賦值的變量都是undefined類型。對于上述例子中的a和b來說,雖然a定義了,b沒有定義,但是它們都屬于“有名字沒值”的變量,因此它們都是undefined類型。如果用之前學習過的內存模型來說,undefined類型的對象就是只占用一塊內存空間(用來保存變量名字)的對象。
另外,symbol類型是ES2015中新增的內容,本書會在后面給大家詳細介紹。
15. instanceof語句
instanceof語句比typeof語句更進了一步,可以判斷一個對象是不是某種類型的實例。就好像周圍的東西可以分為動物、植物、空氣、水、金屬等大的類型,而動物、植物以及金屬還可以再進行更細的分類。typeof的作用就是查看一樣東西屬于什么大的類型,例如是動物還是植物,而instanceof可以判斷東西的具體類型,例如,如果是動物的話,可以判斷是不是人,如果是人還可以判斷是黃種人還是白種人、黑種人等。instanceof語句的結構如下。
obj instanceof TypeObject
instanceof語句的返回值為布爾類型,表示判斷是否正確。例如,下面的例子使用instanceof來判斷arr變量是否為數組類型。
var arr = [1,2,3]; console.log(arr instanceof Array); //true
后面將會學到function類型的對象,可以使用new關鍵字來創建object類型的對象,那時就可以使用instanceof來判斷一個變量是否為指定類型的function對象創建的。
4.3.4 變量作用域
這里所說的變量作用域主要是指使用var定義的變量的作用域,這種變量的作用域是function級的,這一點我們在前面講var語句的時候已經給大家介紹過了。JS中的function是可以嵌套使用的,嵌套的function中的變量的作用域又是怎樣的呢?請先看個例子。
var v=0; function f1(){ var v=1; function f2(){ console.log(v); } f2(); } f1(); //1
這個例子中,定義了全局變量v,在函數f1中定義了局部變量v, f2定義在f1函數中,當調用函數f1時,會在其內部調用函數f2, f2中用到了變量v,這時v會使用f1函數中定義的v。
在調用嵌套函數時,引擎會根據嵌套的層次自動創建一個參數作用域鏈,然后將各層次函數所定義的變量從外到內依次存放到作用域鏈中。例如,在上述示例中,執行函數f2時,會首先將全局對象(瀏覽器中指頁面本身,也就是Window對象)放在最下層,然后放f1,最后放f2。可以在f2函數中加入斷點,然后使用FireBug清楚地看到這一點,加入斷點后,執行代碼時FireBug中的結果如圖4-7所示。

圖4-7 加入斷點后,執行代碼時FireBug中的顯示結果
用到變量的時候會在變量作用域鏈中從上往下查找變量的值。例如,在上述示例中會先在f2中查找,如果找不到就會去f1查找,如果還查找不到就會到全局變量中查找。從圖4-1中可以看到f2中沒有v變量,因此會向下查找f1,在f1可以找到變量v,找到后就會使用f1中定義的變量v,最后會將“1”打印到控制臺。
再來看下面的例子。
var x= 0, y= 0, z= 0, w=0; function f1(){ var y=1, z=1, w=1; function f2(){ var z= 2, w=2; function f3(){ var w=3; console.log(x); console.log(y); console.log(z); console.log(w); } f3(); } f2(); } f1();
這個例子最終會輸出什么呢?
這個例子中,每個變量查找的順序都是f3→f2→f1→全局變量。對于x來說,會一直到全局變量才能找到,y會在f1中找到,z會在f2中找到,w將直接使用f3自己定義的局部變量,所以最后的輸出結果如下。
0 1 2 3
多知道點
調用子函數與嵌套函數中變量查找的區別
在變量作用域中很容易混淆子函數和嵌套函數中變量的查找過程。對于嵌套函數中的變量,會按照函數嵌套的層次在作用域鏈中從上往下查找,而調用子函數時并不會使用父函數中的變量,例如下面的例子。
var v=0; function logV(){
console.log(v); } function f(){ var v=1; logV(); } f();
在上述示例中,在函數f中調用了子函數logV,在logV中打印了變量v,這時v會使用全局變量而不會使用f中的局部變量,最后會打印出0,這是因為在調用logV函數時又會創建新的作用域鏈,新建的作用域鏈只包含logV函數嵌套定義的層級而不會包含調用時的f函數。也就是說,這個例子中有兩套獨立的作用域鏈,調用f函數時有一套,在f中調用logV函數時有另外一套,它們都只有兩層,第一層為全局變量,第二層為函數自身,在logV函數中會使用全局變量v而不會使用f函數中的v(因為f函數根本不在logV函數調用的作用域鏈中)。
4.3.5 閉包
閉包是JS中非常重要且對于新手來說又不容易理解的一個概念。4.3.4節介紹了JS中變量是function級作用域,也就是說,在function中定義的變量可以在function內部(包括內部定義的嵌套function中)使用,而在function外部是無法使用的。但是,本書之前介紹過,函數就是一塊保存了現有數據的內存,只要找到這塊內存就可以對其進行調用。因此,如果想辦法獲取到內部定義的嵌套函數,那樣不就可以在外部使用嵌套函數來調用內部定義的局部變量了嗎?這種用法就是閉包,例如下面這個例子。
function f1(){ var v=1; function f2(){ console.log(v); } return f2; } var f = f1(); f(); //1
這個例子中,函數f1中定義了變量v,正常情況下在f1外面是無法訪問v的,但是f1中嵌套定義的函數f2是可以訪問v的,而且在調用f1時會返回函數f2,這樣就可以在f1外部訪問f1的局部變量v,這就是閉包。當然,如果需要還可以在f2中直接返回v的值,這樣就可以在f1外部獲取v的值。
需要注意的是,在使用閉包時,在保存返回函數的變量失效之前定義閉包的function會一直保存在內存中,例如下面的例子。
function f1(){ var v=1; function f2(){ console.log(v++); } return f2; } var f = f1(); f(); //1 f(); //2 f(); //3
這個例子中,f2函數在打印出v的值后又將v的值加了1,連續調用f函數時會在控制臺依次打印出1 2 3,這說明f1函數一直在內存中保存著。這是因為保存f1返回嵌套函數f2的變量f是全局變量,它會一直保存在內存中,而f所指向的f2函數在執行時需要依賴f1,所以f1就會一直保存在內存中。這里需要注意f2本身因為沒有被依賴,所以f2并不會一直保存在內存中,通過下面的例子可以清楚地看到這一點。
function f1(){ var v=1; function f2(){ var v1 = 1; console.log(v+", "+v1); v++; v1++; } return f2; } var f = f1(); f(); //1,1 f(); //2,1 f(); //3,1
從上面的例子可以看出,f1中定義的變量v在每次調用時會累加,這說明每次調用時使用的都是原來的數據,而f2中定義的變量v1則在每次調用時都會創建新的數據。其原理其實非常簡單,在函數f1執行時會創建一套f1的變量數組,在函數f2執行時會創建另外一套f2的變量數組。按照JS中變量作用域鏈的規則,在f2中可以調用執行f1時所創建的變量數組,為了f2可以正確執行,只要在f2還可能被調用的時候執行f1時所創建的執行環境(包括變量數組)就不會被釋放,因此,f1中定義的變量v會使用同一個,而f2每次執行完之后所創建的執行環境就沒用了,會被釋放,而在下次執行時又會創建新的執行環境。
- ASP.NET Web API:Build RESTful web applications and services on the .NET framework
- AngularJS Testing Cookbook
- Mastering Adobe Captivate 2017(Fourth Edition)
- MySQL 8從入門到精通(視頻教學版)
- Visual C++實例精通
- Java加密與解密的藝術(第2版)
- Learning Hunk
- C# 8.0核心技術指南(原書第8版)
- Modernizing Legacy Applications in PHP
- Learning Grunt
- Mastering Apache Camel
- Python 3快速入門與實戰
- Qt 5.12實戰
- Clojure Data Structures and Algorithms Cookbook
- Java與Android移動應用開發:技術、方法與實踐