表達式和運算符
表達式(expression)JavaScript中的一個短語,JavaScript解釋器會將其計算(evaluate)出一個結果。程序中的常量是最簡單的一類表達式。變量名也是一種簡單的表達式,它的值就是賦值給變量的值。復雜表達式是由簡單表達式組成的。比如,數組訪問表達式是由一個表示數組的表達式、左方括號、一個整數表達式和右方括號構成。它們所組成的新的表達式的運算結果是該數組的特定位置的元素值。同樣的,函數調用表達式由一個表示函數對象的表達式和0個或多個參數表達式構成。
將簡單表達式組合成復雜表達式最常用的方法就是使用運算符(operator)。運算符按照特定的運算規則對操作數(通常是兩個)進行運算,並計算出新值。乘法運算符“
*
”是比較簡單的例子。表達式x*y
是對兩個變量表達式x和y進行運算並得出結果。有時我們更願意說運算符返回了一個值而不是“計算”出了一個值。
如果你熟悉C語法風格的其他編程語言,你會發現大多數JavaScript表達式和運算符都似曾相識。
原始表達式
最簡單的表達式是“原始表達式” (primary expression)。原始表達式是表達式的最小單位——它們不再包含其他表達式。JavaScript中的原始表達式包含常量或直接量、關鍵字和變量。
直接量是直接在程序中出現的常數值。它們看起來像:
1.23 //數字直接量
"hello" //字符串直接量
/pattern/ //正則表達式直接量
JavaScript中的一些保留字構成了原始表達式:
true // 返回一個布爾值:真
false // 返回一個布爾值:假
null // 返回一個值:空
this // 返回”當前"對象
和其他關鍵字不同,this並不是一 個常量,它在程序的不同地方返回的值也不相同。this關鍵字經常在面向對象編程中出現。在一個方法體內,this返回調用這個方法的對象。
最后,第三種原始表達式是變量:
i // 返回變量i的值
sum // 返回sum的值
undefined // undefined是全局變量,和null不同,它不是一個關鍵字
當JavaScript代碼碼中出現了標識符,JavaScript會將其當做變量而去査找它的值。如果變量名不存在,表達式運算結果為undefined。然而,在ECMAScript 5的嚴格模式中,對不存在的變量進行求值會拋出一個引用錯誤異常。
對象和數組的初始化表達式
對象和數組初始化表達式實際上是一個新創建的對象和數組。這些初始化表達式有時稱做“對象直接量”和“數組直接量”。然而和布爾直接量不同,它們不是原始表達式,因為它們所包含的成員或者元素都是子表達式。數組初始化表達式語法非常簡單,我們以此開始。
數組初始化表達式是通過一對方括號和其內由逗號隔開的列表構成的。初始化的結果是一個新創建的數組。數組的元素是逗號分隔的表達式的值:
[] // 一個空數組:[]內留空即表示該數組沒有任何元素
[1+2,3+4] // 擁有兩個元素的數組,第一個是3,第二個是7
數組初始化表達式中的元素初始化表達式也可以是數組初始化表達式。也就是說,這些表達式是可以嵌套的:
var matrix = [[1,2,3], [4,5,6], [7,8,9]];
JavaScript對數組初始化表達式進行求值的時候,數組初始化表達式中的元素表達式也都會各自計算一次。也就是說,數組初始化表達式每次計算的值有可能是不同的。
數組直接量中的列表逗號之間的元素可以省略,這時省略的空位會填充值undefined。例如,下面這個數組包含5個元素,其中三個元素是undefined:
var sparseArray = [1,,,,5];
數組直接量的元素列表結尾處可以留下單個逗號,這時並不會創建一個新的值為undefined的元素。
對象初始化表達式和數組初始化表達式非常類似,只是方括號被花括號代替,並且每個子表達式都包含一個屬性名和一個冒號作為前綴:
var p = { x:2.3, y:-1.2 }; // 一個擁有兩個屬性成員的對象
var q = {}; // 一個空對象
q.x=2.3; q.y = -1.2; // q的屬性成員和p的一樣
對象直接量也可以嵌套,比如:
var rectangle = { upperLeft: { x: 2, y: 2 },
lowerRight: { x: 4, y: 5 } };
JavaScript求對象初始化表達式的值的時候,對象表達式也都會各自計算一次,並且它們不必包含常數值:它們可以是任意JavaScript表達式。同樣,對象直接量中的屬性名稱可以是字符串而不是標識符(這在那些只能使用保留字或一些非法標識符作為屬性名的地方非常有用):
var side = 1;
var square = { "upperLeft": { x: p.x, y: p.y },
'lowerRight': { x: p.x + side, y: p.y + side}};
函數定義表達式
函數定義表達式定義一個JavaScript函數。表達式的值是這個新定義的函數。從某種意義上講,函數定義表達式可稱為“函數直接量”,畢竟對象初始化表達式也稱為“對象直接量” 。一個典型的函數定義表達式包含關鍵字function,跟隨其后的是一對圓括號,括號內是一個以逗號分割的列表,列表含有0個或多個標識符(參數名),然后再跟隨一個由花括號包裹的JavaScript代碼段(函數體),例如:
//這個函數返回傳入參數值的平方
var square = function(x) { return x * x; }
函數定義表達式同樣可以包含函數的名字。函數也可以通過函數語句來定義,而不是函數表達式。
屬性訪問表達式
屬性訪問表達式運算得到一個對象屬性或一個數組元素的值。JavaScript為屬性訪問定義了兩種語法:
expression . identifier
expression [ expression ]
第一種寫法是一個表達式后跟隨一個句點和標識符。表達式指定對象,標識符則指定需要訪問的屬性的名稱。第二種寫法是使用方括號,方括號內是另外一個表達式(這種方法適用於對象和數組)。第二個表達式指定要訪問的屬性的名稱或者代表要訪問數組元素的索引。這里有一些具體的例子:
var 0 = {x:1,y:{z:3}}; // 一個示例對象
var a = [o,4,[5,6]]; // 一個包含這個對象的示例數組
o.x // => 1:表達式o的x屬性
o.y.z // => 3:表達式o.y的z屬性
o["x"] // => 1: 對象o的x屬性
a[i] // => 4:表達式a中索引為1的元素
a[2]["l"] // => 6: 表達式a[2]中索引為1的元素
a[o].x // => 1: 表達式a[o]的x屬性
不管使用哪種形式的屬性訪問表達式,在“.”和“[”之前的表達式總是會首先計算。 如果計算結果是null或者undefined,表達式會拋出一個類型錯誤異常,因為這兩個值都不能包含任意屬性。如果運算結果不是對象(或者數組),JavaScript會將其轉換為對象。如果對象表達式后跟隨句點和標識符,則會査找由這個標識符所指定的 屬性的值,並將其作為整個表達式的值返回。如果對象表達式后跟隨一對方括號,則會計算方括號內的表達式的值並將它轉換為字符串。不論哪種情況,如果命名的屬性不存在,那么整個屬性訪問表達式的值就是undefined。
顯然.identifier的寫法更加簡單,但需要注意的是,這種方式只適用於要訪問的屬性名稱是合法的標識符,並且需要知道要訪問的屬性的名字。如果屬性名稱是一個保留字或者包含空格和標點符號,或是一個數字(對於數組來說),則必須使用方括號的寫法。 當屬性名是通過運算得出的值而不是固定的值的時候,這時必須使用方括號寫法。
調用表達式
JavaScript中的調用表達式(invocation expression)是一種調用(或者執行)函數或方法的語法表示。它以一個函數表達式開始,這個函數表達式指代了要調用的函數。函數表達式后跟隨一對圓括號,括號內是一個以逗號隔開的參數列表,參數可以有0個也可有多個,例如:
f(0) // f是一個函數表達式;0是一個參數表達式
Math.max(x,y,z) // Math.max是一個函數;x, y和z是參數
a.sort() // a.sort是一個函數,它沒有參數
當對調用表達式進行求值的時候,首先計算函數表達式,然后計算參數表達式,得到一組參數值。如果函數表達式的值不是一個可調用的對象,則拋出一個類型錯誤異常(所有的函數都是可調用的,即使宿主對象不是函數它也有可能被調用)。然后,實參的值被依次賦值給形參,這些形參是定義函數時指定的,接下來開始執行函數體。如果函數使用return語句給出一個返回值,那么這個返回值就是整個調用表達式的值。否則,調用表達式的值就是undefined。函數調用——包括當形參表達式的個數和函數定義中實參的個數不匹配的時候的運行情況。
任何一個調用表達式都包含一對圓括號和左圓括號之前的表達式。如果這個表達式是一個屬性訪問表達式,那么這個調用稱做“方法調用”(method invocation)。在方法調用中,執行函數體的時候,作為屬性訪問主題的對象和數組便是其調用方法內this的指 向。這種特性使得在面向對象編程范例中,函數(其OO名稱為“方法”)可以調用其宿主對象。
並不是方法調用的調用表達式通常使用全局對象作為this關鍵字的值。然而在ECMAScript 5中,那些通過嚴格模式定義的函數在調用時將使用undefined作為this的值,this不會指向全局對象。
對象創建表達式
對象創建表達式(object creation expression)創建一個對象並調用一個函數(這個函數稱做構造函數)初始化新對象的屬性。對象創建表達式和函數調用表達式非常類似,只是對象創建表達式之前多了一個關鍵字new:
> new Object()
> new Point(2,3)
如果一個對象創建表達式不需要傳入任何參數給構造函數的話,那么這對空圓括號是可以省略掉的:
new Object
new Date
當計算一個對象創建表達式的值時,和對象初始化表達式通過。創建對象的做法一樣, JavaScript首先創建一個新的空對象,然后,JavaScript通過傳入指定的參數並將這個新對象當做this的值來調用一個指定的函數。這個函數可以使用this來初始化這個新創建對象的屬性。那些被當成構造函數的函數不會返回一個值,並且這個新創建並被初始化后的對象就是整個對象創建表達式的值。如果一個構造函數確實返回了一個對象值,那么這個對象就作為整個對象創建表達式的值,而新創建的對象就廢棄了。
運算符概述
JavaScript中的運算符用於算術表達式、比較表達式、邏輯表達式、賦值表達式等。下表簡單列出了JavaScript中的運算符,作為一個方便的參照。
需要注意的是,大多數運算符都是由標點符號表示的,比如“+”和“=”。而另外一些運算符則是由關鍵字表示的,比如delete和instanceof。關鍵字運算符和標點符號所表示的運算符一樣都是正規的運算符,它們的語法都非常言簡意賅。
下表是按照運算符的優先級排序的,前面的運算符優先級要高於后面的運算符優先級。被水平分割線分隔開來的運算符具有不同的優先級。標題為A的列表示運算符的結合性,L (從左至右)或R (從右至左),標題為N的列表示操作數的個數。標題為“類型”的列表示期望的操作數類型,以及運算符的結果類型(在“→”符號之后)。下表之后的段落會解釋優先級、結合性和操作數類型的概念。下表只對運算符做單獨討論。
JavaScript運算符
運算符 | 操作 | A | N | 類型 |
---|---|---|---|---|
++ | 前/后增量 | R | 1 | lval→num |
-- | 前/后減量 | R | 1 | lval→ num |
- |
求反 | R | 1 | num→num |
+ |
轉換為數字 | R | 1 | num→num |
~ | 按位求反 | R | 1 | int→int |
! | 邏輯非 | R | 1 | bool→bool |
delete | 刪除屬性. | R | 1 | Ival→bool |
typeof | 檢測操作數類型 | R | 1 | any→str |
void | 返回undehned值 | R | 1 | any→undef |
* 、/、% |
乘,除,求余 | L | 2 | num,num→ num |
+、- | 加,減 | L | 2 | num,num→num |
+ |
字符串連接 | L | 2 | str,str→str |
<< | 左移位 | L | 2 | int,int→int |
>> |
有符號右移 | L | 2 | int,int→int |
>>> |
無符號右移 | L | 2 | int,int→int |
<、<=、>、>= | 比較數字順序 | L | 2 | num,num→bool |
<、<=、>、>= | 比較在字母表中的順序 | L | 2 | str,str→bool |
instanceof | 測試對象類 | L | 2 | obj,func→bool |
in | 測試屬性是否存在 | L | 2 | str,obj→bool |
== | 判斷相等 | L | 2 | any,any→ bool |
!= | 判斷不等 | L | 2 | any,any→bool |
=== | 判斷恆等 | L | 2 | any,any→bool |
!== | 判斷非恆等 | L | 2 | any,any→bool |
& | 按位與 | L | 2 | int,int→int |
^ | 按位異或 | L | 2 | int,int→int |
` | ` | 按位或 | L | 2 |
&& | 邏輯與 | L | 2 | any,any→any |
` | ` | 邏輯或 | L | |
?: | 條件運算符 | R | 3 | bool, any, any→any |
= | 變量賦值或對象屬性賦值 | R | 2 | lval,any→any |
`*=、/=、%=、+=、—=、&=、^=、 | =、<<=、>>=、>>>=` | 運算且賦值 | R | 2 |
, | 忽略第一個操作數, 返回第二個操作數 | L | 2 | any, any—any |
①Ival是left-value的簡寫,意思是“左值”。 |
操作數的個數
運算符可以根據其操作數的個數進行分類。JavaScript中的大多數運算符(比如“
*
”乘法運算符)是一個二元運算符(binary operator),將兩個表達式合並成一個稍復雜的表達式。換言之,它們的操作數均是兩個。JavaScript同樣支持一些一元運算符(unary operator),它們將一個表達式轉換為另一個稍復雜的表達式。表達式-x中的運 算符就是一個一元運算符,是將操作數x求負值。最后,JavaScript支持一個三元運算符(ternary operator),條件判斷運算符“?:”,它將三個表達式合並成一個表達式。
操作數類型和結果類型
一些運算符可以作用於任何數據類型,但仍然希望它們的操作數是指定類型的數據,並且大多數運算符返回(或計算出)一個特定類型的值。在表(JavaScript運算符)標題為“類型”的列中列 出了運算符操作數的類型(箭頭前)和運算結果的類型(箭頭后)。
JavaScript運算符通常會根據需要對操作數進行類型轉換。乘法運算符 “
*
”希望操作數為數字,但表達式卻是合法的,因為JavaScript會將操作數轉換為數宇。這個表達式的值是數字15,而不是字符串“15”。之前也提到過,JavaScript中的所有值不是真值就是假值,因此對於那些希望操作數是布爾類型的操作符來說,它們的操作數可以是任意類型。
有一些運算符對操作數類型有着不同程度的依賴。最明顯的例子是加法運算符,“+” 運算符可以對數字進行加法運算,也可以對字符串作連接。同樣,比如“<”比較運算符可以根據操作數類型的不同對數字進行大小值的比較,也可以比較字符在字母表中的次序先后。單個運算符的描述充分解釋了它們對類型有着怎樣的依賴以及對操作數進行怎樣的類型轉換。
左值
你可能會注意到,表(JavaScript運算符)中的賦值運算符和其他少數運算符期望它們的操作數是1 va 1類型。左值(lvalue)是一個古老的術語,它是指“表達式只能出現在賦值運算符的左側”。在JavaScript中,變量、對象屬性和數組元素均是左值。ECMAScript規范允許內置函數返回一個左值,但自定義的函數則不能返回左值。
運算符的副作用
計算一個簡單的表達式(比如2*3)不會對程序的運行狀態造成任何影響,程序后續執行的計算也不會受到該計算的影響。而有一些表達式則具有很多副作用,前后的表達式運算會相互影響。賦值運算符是最明顯的一個例子:如果給一個變量或屬性賦值,那么那些使用這個變量或屬性的表達式的值都會發生改變。"++”和遞增和遞減運算符與此類似,因為它們包含隱式的賦值。delete運算符同樣有副作用:刪除一個屬性就像(但不完全一樣)給這個屬性賦值undefined。
其他的JavaScript運算符都沒有副作用,但函數調用表達式和對象創建表達式有些特別, 在函數體或者構造函數內部運用了這些運算符並產生了副作用的時候,我們說函數調用表達式和對象創建表達式是有副作用的。
運算符優先級
表(JavaScript運算符)中所示的運算符是按照優先級從高到低排序的,每個水平分割線內的一組運算符具有相同的優先級。運算符優先級控制着運算符的執行順序。優先級高的運算符(表格的頂部)的執行總是先於優先級低(表格的底部)的運算符。
看一下下面這個表達式:
w = x + y*z;
乘法運算符“*”比加法運算符“+”具有更高的優先級,所以乘法先執行,加法后執行。然后,由於賦值運算符“=”具有最低的優先級,因此賦值操作是在右側的表達式計算出結果后進行的。
運算符的優先級可以通過顯式使用圓括號來重寫。為了讓加法先執行,乘法后執行,可以這樣寫:
w = (x + y)*z;
需要注意的是,屬性訪問表達式和調用表達式的優先級要比表(JavaScript)中列出的所有運算符都要高。看一下這個例子:
typeof my.functions[x](y)
盡管typeof是優先級最高的運算符之一,但typeof也是在兩次屬性訪問和函數調用之后執行的。
實際上,如果你真的不確定你所使用的運算符的優先級,最簡單的方法就是使用圓括號來強行指定運算次序。有些重要規則需要熟記:乘法和除法的優先級高於加法和減法, 賦值運算的優先級非常低,通常總是最后執行的。
運算符的結合性
在表(JavaScript)中標題為A的列說明了運算符的結合性。L指從左至右結合,R指從右至左結合。
結合性指定了在多個具有同樣優先級的運算符表達式中的運算順序。從左至右是指運算的執行是按照由左到右的順序進行。例如,減法運算符具有從左至右的結合性,因此:
w = x - y - z;
和這段代碼一模一樣:
W = ((X - y) - z);
反過來講,下面這個表達式:
x = ~-y;
w = x = y = z;
q = a?b:c?d:e?f:g;
和這段代碼一模一樣:
x = ~(-y); w = (x = (y = z)); q =
a?b:(c?d:(e?f:g));
因為一元操作符、賦值和三元條件運算符都具有從右至左的結合性。
運算順序
運算符的優先級和結合性規定了它們在復雜的表達式中的運算順序,但並沒有規定子表達式的計算過程中的運算順序。JavaScript總是嚴格按照從左至右的順序來計算表達式。 例如,在表達式w=x+y*z中,將首先計算子表達式w,然后計算x、y和z,然后,y的值和z的值相乘,再加上x的值,最后將其賦值給表達式w所指代的變量或屬性。給表達式添加圓括號將會改變乘法、加法和賦值運算的關系,但從左至右的順序是不會改變的。
只有在任何一個表達式具有副作用而影響到其他表達式的時候,其求值順序才會和看上去有所不同。如果表達式x中的一個變量自增1,這個變量在表達式z中使用,那么實際上是先計算出了x的值再計算z的值,這一點非常重要。
注
假設存在a=1,那么"b=(a++)+a;”將如何計算結果呢?按照正文所述,順序應該是,1)計算b,2)計算a++ (假設值為c), 3))計算a,4)計算c+a,,5)將c+a的結果賦值給b。按照"++”的定義,第2)步中a++的結果依然是1,即c為1,隨后a立即增1,因此在執行第3)步時,a的值已經是2。所以b的結果為3。很多初學者會誤認為a增1的操作是在表達式計算完畢后執行的。
算術表達式
加法同樣可以做字符串連接操作,並且其類型轉換有些特殊。
所有那些無法轉換為數字的操作數都轉換為NaN值。如果操作數(或者轉換結果)是NaN值,算術運算的結果也是NaN。
運算符“/”用第二個操作數來除第一個操作數,如果你使用過那些區分整型和浮點型數字的編程語言,那么當用一個整數除以另一個整數時,則希望得到的結果也是整數。但在JavaScript中,所有的數字都是浮點型的,除法運算的結果也是浮點型,比如,5/2的 結果是2.5,而不是2。除數為0的運算結果為正無窮大或負無窮大,而0/0的結果是NaN,所有這些運算均不會報錯。
運算符“%”計算的是第一個操作數對第二個操作數的模。換句話說,就是第一個操作數除以第二個操作數的余數。結果的符號和第一個操作數(被除數)的符號保持一致。例如,5%2結果是1,-5%2的結果是-1。
注
求余運算也叫模運算,模就是余數。
求余運算符的操作數通常都是整數,但也適用於浮點數,比如,6.5%2.1結果是0.2。
"+”運算符
二元加法運算符“+”可以對兩個數字做加法,也可以做字符串連接操作:
1 + 2 // => 3
"hello" + " " + "there" // => "hello there"
"1" + "2" // => "12"
當兩個操作數都是數字或都是字符串的時候,計算結果是顯而易見的。然而對於其他情況來說,則要進行一些必要的類型轉換,並且運算符的行為依賴於類型轉換的結果。加號的轉換規則優先考慮字符串連接,如果其中一個操作數是字符串或者轉換為字符串的對象,另外一個操作數將會轉換為字符串,加法將進行字符串的連接操作。如果兩個操作數都不是類字符串(string-like)的,那么都將進行算術加法運算。
從技術上講,加法操作符的行為表現為:
- 如果其中一個操作數是對象,則對象會遵循對象到原始值的轉換規則轉換為原始類值:日期對象通toString( )方法執行轉換,其他對象則通過 。valueOf( )方法執行轉換(如果valueOf( )方法返回一個原始值的話)。由於多數對象都不具備可用的valueOf( )方法,因此它們會通過toString( )方法來執行轉換。
- 在進行了對象到原始值的轉換后,如果其中一個操作數是字符串的話,另一個操作數也會轉換為字符串,然后進行字符串連接。
- 否則,兩個操作數都將轉換為數字(或者NaN),然后進行加法操作。
這里有一些例子:
1+2 // => 3:加法
"1" + "2" // => "12": 字符串連接
"1" + 2 // => "12": 數字轉換為字符串后進行字符串連接
1 + {} // => "1[object Object]": 對象轉換為字符串后進行字符串連接
true + true // => 2: 布爾值轉換為數字后做加法
2 + null // => 2:null轉換為0后做加法
2 + undefined // => NaN: undefined轉換為NaN后做加法
最后,需要特別注意的是,當加號運算符和字符串和數字一起使用時,需要考慮加法的結合性的對運算順序的影響。也就是說,運算結果是依賴於運算符的運算順序的,比如:
1 + 2 + " blind mice"; // => "3 blind mice"
1 + (2 + " blind mice"); // => "12 blind mice"
第一行沒有圓括號,"+”運算符具有從左至右的結合性,因此兩個數字首先進行加法計算,計算結果和字符串進行連接。在第二行中,圓括號改變了運算順序:數字2和字符串連接,生成一個新字符串,然后數字1和這個新字符串再次連接,生成了最終結果。
一元算術運算符
一元運算符作用於一個單獨的操作數,並產生一個新值。在JavaScript中,一元運算符具有很高的優先級,而且都是右結合(right-associative))。必要時,一元算術運算符(+、-、++和--)會將操作數轉換為數字。需要注意的是,“+”和 “-”是一元運算符,也是二元運算符。
下面介紹一元算術運算符:
一元加法(+)
一元加法運算符把操作數轉換為數字(或者NaN),並返回這個轉換后的數字。如果操作數本身就是數字,則直接返回這個數字。
—元減法(-)
當“-”用做一元運算符時,它會根據需要把操作數轉換為數字,然后改變運算結果的符號。
遞增(++)
遞增“++”運算符對其操作數進行增量(加一)操作,操作數是一個左值 (lvalue)(變量、數組元素或對象屬性)。運算符將操作數轉換為數字,然后給數字加1,並將加1后的數值重新賦值給變量、數組元素或者對象屬性。
遞增“++”運算符的返回值依賴於它相對於操作數的位置。當運算符在操作數之前,稱為“前增量” (pre-increment)運算符,它對操作數進行增量計算,並返回計算后的值。當運算符在操作數之后,稱為“后增量” (post-increment)運算符,它對操作數進行增量計算,但返回未做增量計算的(unincremented)值。思考 一下如下兩行代碼之間的區別:
var i = 1, j = ++i; // i和j的值都是2
var i = 1, j = i++; // i是2, j是1
需要注意的是,表達式++x並不總和x=x+1完全一樣,"++”運算符從不進行字符串連接操作,它總是會將操作數轉換為數字並增1。如果x是字符串"1”,++x的結果 就是數字2,而x+1是字符串“11”。
同樣需要注意的是,由於JavaScript會自動進行分號補全,因此不能在后增量運算符和操作數之間插入換行符。如果插入了換行符,JavaScript將會把操作數當做一條單獨的語句,並在其之前補上一個分號。
不管是前增量還是后增量,這個運算符通常用在for循環中,用於控制循環內的計數器。
遞減(--)
遞減“-”運算符的操作數也是一個左值。它把操作數轉換為數字,然后減1,並將計算后的值重新賦值給操作數。和“++”運算符一樣,遞減”運算符的返回值依賴於它相對操作數的位置,當遞減運算符在操作數之前,操作數減1並返回減1之后的值。當遞減運算符在操作數之后,操作數減1並返回減1之前的值。當遞減運算符在操作符的右側時,運算符和操作數之間不能有換行符。
位運算符
位運算符可以對由數字表示的二進制數據進行更低層級的按位運算。盡管它們並不是傳統的數學運算,但這里也將其歸類為算術運算符,因為它們作用於數值類型的操作數並返回數字。這里的4個運算符都是對操作數的每個位進行布爾運算,這里將操作數的每個位當做布爾值(1=true,0=false),其他三個位運算符用來進行左移位和右移位。
位運算符要求它的操作數是整數,這些整數表示為32位整型而不是64位浮點型。必要時,位運算符首先將操作數轉換為數宇,並將數字強制表示為32位整型,這會忽略原格式中的小數部分和任何超過32位的二進制位。移位運算符要求右操作數在0 ~ 31之間。在將其操作數轉換為無符號32位整數后,它們將舍棄第5位之后的二進制位,以便生成一 個位數正確的數字。需要注意的是,位運算符會將NaN,Infinity和-Infinity都轉換為0。
按位與(&)
位運算符“&”對它的整型操作數逐位執行布爾與(AND)操作。只有兩個操作數中相對應的位都是1,結果中的這一位才是1。例如,0x1234 & 0x00FF = 0x0034。
按位或(I)
位運算符“|”對它的整型操作數逐位執行布爾或(OR)操作。如果其中一個操作數相應的位為1,或者兩個操作數相應位都是1,那么結果中的這一位就為1。例 如:0x1234 | 0x00FF = 0X12FF。
按位異或(^)
位運算符“|”對它的整型操作數逐位執行布爾異或(XOR)操作。異或是指第一個操作數為true或第二個操作數為true,但兩者不能同時為true。如果兩個操作數中只有一個相應位為1(不能同時為1),那么結果中的這一位就是1。例如, 0xFF00 ^ 0xF0F0 = 0x0FF0。
按位非(~)
運算符“”是一元運算符,位於一個整型參數之前,它將操作數的所有位取反。根據JavaScript中帶符號的整數的表示方法,對一個值使用運算符相當於改變它的符號並減1。例如,0x0F = 0xFFFFFFF0或-16。
左移(<<)
將第一個操作數的所有二進制位進行左移操作,移動的位數由第二個操作數指定, 移動的位數是0 ~ 31之間的一個整數。例如,在表達式中,a的第一位變成了第 二位,a的第二位變成了它的第三位,以此類推。新的第一位用0來補充,舍棄第32位。將一個值左移1位相當於它乘以2,左移兩位相當於乘以4,以此類推。例如, 7<<2=28。
帶符號右移(>>)
運算符">>”將第一個操作數的所有位進行右移操作,移動的位數由第二個操作數指定,移動的位數是0 〜 31之間的一個整數。右邊溢出的位將忽略。填補在左邊的位由原操作數的符號決定,以便保持結果的符號與原操作數一致。如果第一個操作數是正數,移位后用0填補最高位,如果第一個操作數是負的,移位后就用1填補高位。將一個值右移1位,相當於用它除以2 (忽略余數),右移兩位,相當於它除以4,以此類推,例如,7>>1=3,-7>>1=-4。
無符號右移(>>>)
運算符">>>”和運算符">>” 一樣,只是左邊的高位總是填補0,與原來的操作數符號無關,例如,-1>>4=-1,但是-1>>>4=0X0FFFFFFF。
關系表達式
關系運算符用於測試兩個值之間的關系(比如“相等”,“小於”,或“是...的屬性”),根據關系是否存在而返回true或false。關系表達式總是返回一個布爾值,通常在if、while或者for語句中使用關系表達式,用以控制程序的執行流程。
相等和不等運算符
“==”
和“===”
運算符用於比較兩個值是否相等,當然它們對相等的定義不盡相同。兩個運算符允許任意類型的操作數,如果操作數相等則返回true,否則返回false。“==="
也稱為嚴格相等運算符(strict equality)(有時也稱做恆等運算符(identity operator)),它用來檢測兩個操作數是否嚴格相等。"==”
運算符稱做相等運算符
(equality operator),它用來檢測兩個操作數是否相等,這里“相等”的定義非常寬松,可以允許進行類型轉換。JavaScript支持
“=”
、"==”
和“===”
運算符。你應當理解這些(賦值、相等、恆等)運算符之間的區別,並在編碼過程中小心使用。盡管它們都可以稱做“相等”,但為了減少概念混淆,應該把“=”
稱做“得到或賦值”,把“==”
稱做“相等”,把"===”
稱做“嚴格相等” 。
“!=”
和“!==”
運算符的檢測規則是“==”
和“===”
運算符的求反。如果兩個值通過“==”
的比較結果為true,那么過“!=”的比較結果則為false。如果兩值通過“===”
的比較結果為true,那么通過“!==”
的比較結果則為false, “!”運算符是布爾非運算符。我們只要記住“!=”
稱做“不相等” 、"!==”
稱做“不嚴格相等”就可以了。JavaScript對象的比較是引用的比較,而不是值的比較。對象和其本身是相等的,但和其他任何對象都不相等。如果兩個不同的對象具有相同數量的屬性,相同的屬性名和值,它們依然是不相等的。相應位置的數組元素是相等的兩個數組也是不相等的。
嚴格相等運算符
"===”
首先計算其操作數的值,然后比較這兩個值,比較過程沒有任何類型轉換:
- 如果兩個值類型不相同,則它們不相等。
- 如果兩個值都是null或者都是undefined,則它們不相等。
- 如果兩個值都是布爾值true或都是布爾值false,則它們相等。
- 如果其中一個值是NaN,或者兩個值都是NaN,則它們不相等。NaN和其他任何值都是不相等的,包括它本身!通過x!==x來判斷x是否為NaN,只有在x為NaN的時候,這個表達式的值才為true。
- 如果兩個值為數字且數值相等,則它們相等。如果一個值為0,另一個值為-0,則它們同樣相等。
- 如果兩個值為字符串,且所含的對應位上的16位數完全相等,則它們相等。如果它們的長度或內容不同,則它們不等。兩個字符串可能含義完全一樣且所顯示出的字符也一樣,但具有不同編碼的16位值。JavaScript並不對Unicode進行標准化的轉換,因此像這樣的字符串通過
"===”
和“==”
運算符的比較結果也不相等。第三部分的String.localeCompare( )提供了另外一種比較字符串的方法。如果兩個引用值指向同一個對象、數組或函數,則它們是相等的。如果指向不同的對象,則它們是不等的,盡管兩個對象具有完全一樣的屬性。
相等運算符
“==”
和恆等運算符相似,但相等運算符的比較並不嚴格。如果兩個操作數不是同一類型,那么相等運算符會嘗試進行一些類型轉換,然后進行比較:
- 如果兩個操作數的類型相同,則和上文所述的嚴格相等的比較規則一樣。如果嚴格相等,那么比較結果為相等。如果它們不嚴格相等,則比較結果為不相等。
- 如果兩個操作數類型不同,
“==”
相等操作符也可能會認為它們相等。檢測相等將會遵守如下規則和類型轉換:- 如果一個值是null,另一個是undefined,則它們相等。
- 如果一個值是數字,另一個是字符串,先將字符串轉換為數字,然后使用轉換后的值進行比較。
- 如果其中一個值是true,則將其轉換為1再進行比較。如果其中一個值是false,則將其轉換為0再進行比較。
- 如果一個值是對象,另一個值是數字或字符串,則JavaScript類型轉換規則將對象轉換為原始值,然后再進行比較。對象通過toString( )方法或 者valueOf( )方法轉換為原始值。JavaScript語言核心的內置類首先嘗試使用valueOf( ),再嘗試使用toString( ),除了日期類,日期類只使用toString( )轉換。那些不是JavaScript語言核心中的對象則通過各自的實現中定義的方法轉換為原始值。
- 其他不同類型之間的比較均不相等。
這里有一個判斷相等的小例子:
"1"==true
這個表達式的結果是true,這表明完全不同類型的值比較結果為相等。布爾值true首先轉換為數字1,然后再執行比較。接下來,字符串“1”也轉換為了數字1,因為兩個數字的值相等,因此比較結果為true。
比較運算符
比較運算符用來檢測兩個操作數的大小關系(數值大小或者字母表的順序):
小於(<)
如果第一個操作數小於第二個操作數,則運算符的計算結果為true;否則為false。
大於(>)
如果第一個操作數大於第二個操作數,則“>”運算符的計算結果為true,否則為false。
小於等於(<=)
如果第一個操作數小於或者等於第二個操作數,則“<=”運算符的計算結果為 true;否則為false。
大於等於(>=)
如果第一個操作數大於或者等於第二個操作數,則“>=”運算符的計算結果為 false;否則為false。
比較操作符的操作數可能是任意類型。然而,只有數字和字符串才能真正執行比較操作,因此那些不是數字和字符串的操作數都將進行類型轉換,類型轉換規則如下:
- 如果操作數為對象,那么這個對象將依照JavaScript類型轉換規則轉換為原始值:如果valueOf( )返回一個原始值,那么直接使用這個原始值。否則,使用toString( )的轉換結果進行比較操作。
- 在對象轉換為原始值之后,如果兩個操作數都是字符串,那么將依照字母表的順序對兩個字符串進行比較,這里提到的“字母表順序”是指組成這個字符串的16位Unicode字符的索引順序。
- 在對象轉換為原始值之后,如果至少有一個操作數不是字符串,那么兩個操作數都將轉換為數字進行數值比較。0和-0是相等的。Infinity比其他任何數字都大(除了Infinity本身),Infinity比其他任何數字都小(除了它自身)。如果其中一個操作數是(或轉換后是)NaN,那么比較操作符總是返回false。
需要注意的是,JavaScript字符串是一個由16位整數值組成的序列,字符串的比較也只是兩個字符串中的字符的數值比較。由Unicode定義的字符編碼順序和任何特定語言或者本地語言字符集中的傳統字符編碼順序不盡相同。注意,字符串比較是區分大小寫的, 所有的大寫的ASCII字母都“小於”小寫的ASCII字母。如果不注意這條不起眼的規則的話會造成一些小麻煩。比如,使用“<”小於運算符比較“Zoo”和“aardvark”,結果 為true。
參照String.localCompare( )方法來獲取更多字符串比較的相關信息,String. localCompare( )方法更加健壯可靠,這個方法參照本地語言的字母表定義的字符次序。對於那些不區分字母大小寫的比較來說,則需要首先將字符串轉全部換為小寫字母或者大寫字母,通過String.toLowerCase( )和String.toUpperCase( )做大小寫的轉換。
對於數字和字符串操作符來說,加號運算符和比較運算符的行為都有所不同,前者更偏愛字符串,如果它的其中一個操作數是字符串的話,則進行字符串連接操作。而比較運算符則更偏愛數字,只有在兩個操作數都是字符串的時候,才會進行字符串的比較:
1 + 2 // 加法。結果是3
"1" + "2" // 字符串連接,結果是"12"
"1" + 2 // 字符串鏈接,2轉換為"2",結果是"12"
11 < 3 // 數字的比較,結果為false
"11" < "3" // 字符串比較,結果為true
"11" < 3 // 數字的比較,"11"轉換為11,結果為false
"one" < 3 // 數字的比較,"one"轉換為NaN,結果為false
最后,需要注意的是,“<=”(小於等於)和“>=”(大於等於)運算符在判斷相等的時候,並不依賴於相等運算符和嚴格相等運算符的比較規則。相反,小於等於運算符只是簡單的“不大於”,大於等於運算符也只是“不小於”。只有一個例外,那就是當其一個操作數是(或者轉換后是)NaN的時候,所有4個比較運算符均返回false。
in運算符
in運算符希望它的左操作數是一個字符串或可以轉換為字符串,希望它的右操作數是一個對象。如果右側的對象擁有一個名為左操作數值的屬性名,那么表達式返回true,例如:
var point = { x:1, y:1 }; // 定義一個對象
"x" in point // => true:對象有一個名為"x"的屬性
"z" in point // => false:對象中不存在名為"z"的屬性
"toString" in point // => true:對象繼承了toString()方法
var data = [7,8,9]; // 擁有三個元素的數組
"0" in data // => true:數組包含元素"0"
1 in data // => true:數字轉換為字符串
3 in data // => false:沒有索引為3的元素
instanceof 運算符
instanceof運算符希望左操作數是一個對象,右操作數標識對象的類。如果左側的對象 是右側類的實例,則表達式返回true;否則返回false。第9章將會講到,JavaScript中對 象的類是通過初始化它們的構造函數來定義的。這樣的話,instanceof的右操作數應當 是一個函數。比如:
var d = new Date(); // 通過Date()構造函數來創建一個新對象
d instanceof Date; // 計算結果為true,d是由Date()創建的
d instanceof Object; // 計算結果為true,所有的對象都是Object的實例
d instanceof Number; // 計算結果為false,d不是Number一個對象
var a = [1 ,2 ,3]; // 通過數組直接量的寫法創建一個數組
a instanceof Array; // 計算結果為true,a是一個數組
a instanceof Object; // 計算結果為true,所有的數組都是對象
a instanceof RegExp; // 計算結果為false,數組不是正則表達式
需要注意的是,所有的對象都是Object的實例。當通過instanceof判斷一個對象是否是一個類的實例的時候,這個判斷也會包含對“父類”(superclass)的檢測。如果instanceof的左操作數不是對象的話,instanceof返回false( )如果右操作數不是函數, 則拋出一個類型錯誤異常。
為了理解instanceof運算符是如何工作的,必須首先理解“原型鏈”(prototype chain)。原型鏈作為JavaScript的繼承機制。為了計算表達式o instanceof f,JavaScript首先計算f.prototype,然后在原型鏈中査找o,如果找到,那么o是f(或者f的父類)的一個實例,表達式返回true。如果f.prototype不在o的原型鏈中的話,那么o就不是f的實例,instanceof返回false。
注
對象o中存在一個隱藏的成員,這個成員指向其父類的原型,如果父類的原型是另外一個類的實例的話,則這個原型對象中也存在一個隱藏成員指向另外一個類的原型,這種鏈條將許多對象或類串接起來,既是原型鏈。原文所講f.prototype不在o的原型鏈中也就是說f和o沒有派生關系。
邏輯表達式
邏輯運算符“&&”、"||”和“!”是對操作數進行布爾算術運算,經常和關系運算符一起配合使用,邏輯運算符將多個關系表達式組合起來組成一個更復雜的表達式。
邏輯與(&&)
"&&”運算符可以從三個不同的層次進行理解。最簡單的第一層理解是,當操作數都是布爾值的時候,“&&”對兩個值執行布爾與(AND)操作,只有在第一個操作數和第二個操作數都是true的時候,它才返回true。如果其中一個操作數是false,它返回false。
“&&”常用來連接兩個關系表達式:
x == 0 && y == 0 // 只有在x和y都是0的時候,才返回true
關系表達式的運算結果總是為true或false,因此當這樣使用的時候,“&&”運算符本身也返回true或false。關系運算符的優先級比“&&” (和“||”)要高,因此類似這種表達式可以放心地書寫,而不用補充圓括號。
但是“&&”的操作數並不一定是布爾值,回想一下,有些值可以當做“真值”和“假值”(假值是false、null、undefined、0、-0, NaN和“ ”,所有其他的值包括所有對象都是真值)。對“&&”的第二層理解是,“&&”可以對真值和假值進行布爾與(AND)操作。如果兩個操作數都是真值,那么返回一個真值;否則,至少一個操作數是假值的話,則返回一個假值。在JavaScript中任何希望使用布爾值的地方,表達式和語句都會將其當做真值或假值來對待,因此實際上“&&”並不總是返回true和false,但也並無大礙。
需要注意的是,上文提到了運算符返回一個“真值”或者“假值”,但並沒有說明這個 “真值”或者“假值”到底是什么值。為此,我們深入討論對“&&”的第三層(也是最后一層)理解。運算符首先計算左操作數的值,即首先計算“&&”左側的表達式。如果計算結果是假值,那么整個表達式的結果一定也是假值,因此“&&”這時簡單地返回左 操作數的值,而並不會對右操作數進行計算。
反過來講,如果左操作數是真值,那么整個表達式的結果則依賴於右操作數的值。如果右操作數是真值,那么整個表達式的值一定是真值;如果右操作數是假值,那么整個表達式的值一定是假值。因此,當左操作數是真值時,“&&”運算符將計算右操作數的值並將其返回作為整個表達式的計算結果:
var o = { x : 1 };
var p = null;
o && o.x // => 1:o是真值,因此返回值為o.x
p && p.x // => null: p是假值,因此將其返回,而並不去計算p・x
這對於理解“&&”可能不會去計算右操作數的情況至關重要,在上述示例代碼中,變量p的值是null,而如果計算表達式p.x的話則會拋出一個類型錯誤異常。但是示例代碼使用了 “&&”的一種符合語言習慣的用法,因此只有在p為真值(不能是null或者undefined)的情況下才會計算p.x。
“&&”的行為有時稱做“短路”(short circuiting),我們也會經常看到很多代碼利用了這一特性來有條件地執行代碼。例如,下面兩行JavaScript代碼是完全等價的:
if (a == b) stop(); // 只有在a==b的時候才調用stop()
(a == b) && stop(); // 同上
一般來講,當“&&”右側的表達式具有副作用的時候(賦值、遞增、遞減和函數調用表達式)要格外小心。因為這些帶有副作用的表達式的執行依賴於左操作數的計算結果。
盡管“&&”可以按照第二層和第三層的理解進行一些復雜表達式運算,但大多數情況下,“&&”僅用來對真值和假值做布爾計算。
邏輯或(||)
“||”運算符對兩個操作數做布爾或(OR)運算。如果其中一個或者兩個操作數是真值,它返回一個真值。如果兩個操作數都是假值,它返回一個假值。
盡管“||”運算符大多數情況下只是做簡單布爾或(OR)運算,和“&&” 一樣,它也具有一些更復雜的行為。它會首先計算第一個操作數的值,也就是說會首先計算左側的表達式。如果計算結果為真值,那么返回這個真值。否則,再計算第二個操作數的值,即計算右側的表達式,並返回這個表達式的計算結果。
和“&&”運算符一樣,同樣應當避免右操作數包含一些具有副作用的表達式,除非你目地明確地在右側使用帶副作用的表達式,而有可能不會計算右側的表達式。
這個運算符最常用的方式是用來從一組備選表達式中選出第一個真值表達式:
// 如果max_width已經定義了,直接使用它;否則在preferences對象中査找max_width
// 如果沒看定義它,則使用一個寫死的常量
var max = max_width || preferences.max_width || 500;
這種慣用法通常用在函數體內,用來給參數提供默認值:
// 將o的成員屬性復制到P中,並返回P
function copy(o, p) {
p = p || {}; //如果向參數p沒有傳入任何對象,則使用一個新創建的對象
//函數體內的主邏輯
}
邏輯非(!)
“!”運算符是一元運算符。它放置在一個單獨的操作數之前。它的目的是將操作數的布爾值進行求反。例如,如果x是真值,則!x返回false;如果x是假值,則!x返回true。
和“&&”與“||”運算符不同,“!”運算符首先將其操作數轉換為布爾值,然后再對布爾值求反。也就是說“!”總是返回true或者false,並且,可以通過使用兩次邏輯非運算來得到一個值的等價布爾值:! !x 。
作為一個一元運算符,“!”具有很高的優先級,並且和操作數緊密綁定在一起。如果你希望對類似p && q的表達式做求反操作,則需要使用圓括號:!(p && q)。布爾計算的更多原理性知識不必要做過多的解釋,這里僅用JavaScript代碼做簡單說明:
> // 對於p和q取任意值,這兩個等式都永遠成立
> !(p && q) === !p || !q
> !(P || q) === !p && !q
賦值表達式
JavaScript使用“=”運算符來給變量或者屬性賦值。例如:
i = 0 // 將變量i設置為0
0.X = 1 // 將對象o的屬性X設置為1
“=”運算符希望它的左操作數是一個左值:一個變量或者對象屬性(或數組元素)。它的右操作數可以是任意類型的任意值。賦值表達式的值就是右操作數的值。賦值表達式的副作用是,右操作數的值賦值給左側的變量或對象屬性,這樣的話,后續對這個變量和對象屬性的引用都將得到這個值。
盡管賦值表達式通常非常簡單,但有時仍會看到一些復雜表達式包含賦值表達式的情況。例如,可以將賦值和檢測操作放在一個表達式中,就像這樣:
(a = b) == 0
如果這樣做的話,應當清楚地知道“=”和“==”運算符之間的區別!需要注意的是, “=”具有非常低的優先級,通常在一個較長的表達式中用到了一條賦值語句的值的時候,需要補充圓括號以保證正確的運算順序。
賦值操作符的結合性是從右至左,也就是說,如果一個表達式中出現了多個賦值運算符,運算順序是從右到左。因此,可以通過如下的方式來對多個變量賦值:
i=j=k=0; //把三個變量初始化為0
帶操作的賦值運算
除了常規的賦值運算“=”之外,JavaScript還支持許多其他的賦值運算符,這些運算符將賦值運算符和其他運算符連接起來,提供一種更為快捷的運算方式。例如,運算符“+=”執行的是加法運算和賦值操作,下面的表達式:
total += sales_tax
和接下來的表達式是等價的:
total = total + sales_tax
運算符“+=”可以作用於數字或字符串,如果其操作數是數字,它將執行加法運算和賦值操作,如果操作數是字符串,它就執行字符串連接操作和賦值操作。
這類運算符還包括“-=”、"
*=
"、“&=
”等。表下列出了這一類的所有運算符。
賦值運算符
在大多數情況下,表達式為:
a op = b
這里op代表一個運算符,這個表達式和下面的表達式等價:
a=a op b
在第一行中,表達式a計算了一次,在第二行中,表達式a計算了兩次。只有在a包含具有副作用的表達式(比如函數調用和賦值操作)的時候,兩者才不等價。比如,下面兩個表達式就不等價:
data[i++] *= 2;
data[i++] = data[i++] * 2;
表達式計算
和其他很多解釋性語言一樣,JavaScript同樣可以解釋運行由JavaScript源代碼組成的字符串,並產生一個值。JavaScript通過全局函數eval( )來完成這個工作:
eval("3+2") //=> 5
動態判斷源代碼中的字符串是一種強大的語言特性,幾乎沒有必要在實際中應用。如果你使用了eval( ),你應當仔細考慮是否真的需要使用它。
下面講解eval( )的基礎用法,並且介紹嚴格使用它的兩種方法,從代碼優化的角度講, 這兩種方法對於原有代碼造成的影響是最小的。
eval( )是一個函數還是一個運算符
eval( )是一個函數,它已經被當成運算符來對待了。JavaScript語言的早期版本定義了eval( )函數,從那時起,該語言的設計者和解釋器的作者對其實施了更多限制,使其看起來更像運算符。現代JavaScript解釋器進行了大量的代碼分析和優化。而eval( )的問題在於,用於動態執行的代碼通常來講是不能分析。一般來講,如果一個函數調用了eval( ),那么解釋器將無法對這個函數做進一步優化。而將eval( )定義為函數的另一個問題是,它可以被賦予其他的名字:
var f = eval;
var g = f;
如果允許這種情況的話,那么解釋器將無法放心地優化任何調用g( )的函數。而當eval是一個運算符(並作為一個保留字)的時候,這種問題就可以避免掉。
eval( )
eval( )只有一個參數。如果傳入的參數不是字符串,它直接返回這個參數。如果參數是字符串,它會把字符串當成JavaScript代碼進行編譯(parse)。如果編譯失敗則拋出一個語法錯誤(SyntaxError)異常。如果編譯成功,則開始執行這段代碼,並返回字符串中的最后一個表達式或語句的值,如果最后一個表達式或語句沒有值,則最終返回undefined。如果字符串拋出一個異常,這個異常將把該調用傳遞給eval( )。
關於eval( )最重要的是,它使用了調用它的變量作用域環境。也就是說,它査找變量的值和定義新變量和函數的操作和局部作用域中的代碼完全一樣。如果一個函數定義了一個局部變量x,然后調用
eval("x")
,它會返回局部變量的值。如果它調用eval("x=1")
, 它會改變局部變量的值。如果函數調用了eval("var y = 3;")
,它聲明一個新的局部變量y。同樣地,一個函數可以通過如下代碼聲明一個局部函數:
eval("function f() { return x+1; }");
如果在最頂層代碼中調用eval( ),當然,它會作用於全局變量和全局函數。
需要注意的是,傳遞給eval( )的字符串必須在語法上講的通一不能通過eval( )往函數中任意粘貼代碼片段,比如,
eval("return; ")
是沒有意義的,因為return只有在函數中才起作用,並且事實上,eval的字符串執行時的上下文環境和調用函數的上下文環境是一樣的,這不能使其作為函數的一部分來運行。如果字符串作為一個單獨的腳本是有語義的(就像諸如x=0的短代碼),那么將其傳遞給eval( )作參數是完全沒有問題的,否則,eval( )將拋出語法錯誤異常。
注
var foo = function(a){
eval(a);
};
foo("return; ”);
這段代碼中執行eval(a)的上下文是全局的,在全局上下文中使用return會拋出語法錯誤:return not in function。
全局eval( )
eval( )具有更改局部變量的能力,這對於JavaScript優化器來說是一個很大的問題。然而作為一種權宜之計,JavaScript解釋器針對那些調用了eval( )的函數所做的優化並不多。但當腳本定義了eval( )的一個別名,且用另一個名稱調用它,JavaScript解釋器又會如何工作呢?為了讓JavaScript解釋器的實現更加簡化,ECMAScript 3標准規定了任何解釋器都不允許對eval( )賦予別名。如果eval( )函數通過別名調用的話,則會拋出一個EvalError異常。(這里是指那些沒有語義的代碼片段通過eval( )執行都會拋出語法錯誤異常。)
實際上,大多數的實現並不是這么做的。當通過別名調用時,eval( )會將其字符串當成頂層的全局代碼來執行。執行的代碼可能會定義新的全局變量和全局函數,或者給全局變量賦值,但卻不能使用或修改主調函數中的局部變量,因此,這不會影響到函數內的代碼優化。
ECMAScript 5是反對使用EvalError的,並且規范了eval( )的行為。"直接的eval”,當直接使用非限定的“eval”名稱(eval看起來像是一個保留字)來調用eval()函數時,通常稱為“直接eval” (direct eval)。直接調用eval( )時,它總是在調用它的上下文作用域內執行。其他的間接調用則使用全局對象作為其上下文作用域,並且無法讀、寫、定義局部變量和函數。下面有一段示例代碼:
var geval = eval; // 使用別名調用eval將是全局eval
var x = "global", y = "global"; // 兩個全局變量
function f() { // 函數內執行的是局部eval
var x = "local"; // 定義局部變量
eval("x +='changed';"); // 直接eval更改了局部變量的值
return x; // 返回更改后的局部變量
}
function g(){ // 這個函數內執行了全局eval
var = "local"; // 定義局部變量
val("y +='changed';"); // 間接調用改變了全局變量的值
return y; // 返回未更改的局部變量
}
console.log(f(),x); // 更改了局部變量:輸出"local changed global":
console.log(g(), y); // 更改了全局變量:輸出"local global changed":
我們注意到,全局eval的這些行為不僅僅是出於代碼優化器的需要而做出的一種折中方案,它實際上是一種非常有用的特性,它允許我們執行那些對上下文沒有任何依賴的全局腳本代碼段。真正需要eval來執行代碼段的場景並不多見。但當你真的意識到它的必要性時,你更可能會使用全局eval而不是局部eval。
IE 9之前的早期版本IE和其他瀏覽器有所不同,當通過別名調用eval( )時並不是全局 eval( )(它也不會拋出一個EvalError異常,僅僅將其當做局部eval來調用)。但IE的確定義了一個名叫execScript( )的全局函數來完成全局eval的功能(但和eval()稍有不同,execScript。總是會返回null)。
嚴格eval()
ECMAScript 5嚴格模式對eval( )函數的行為施加了更多的限制,甚至對標識符eval的使用也施加了限制。當在嚴格模式下調用eval( )時,或者eval( )執行的代碼段以“use strict”指令開始,這里的eval( )是私有上下文環境中的局部eval。也就是說,在嚴格模式下,eval執行的代碼段可以査詢或更改局部變量,但不能在局部作用域中定義新的變量或函數。
此外,嚴格模式將“eval”列為保留字,這讓eval( )更像一個運算符。不能用一個別名覆蓋eval( )函數。並且變量名、函數名、函數參數或者異常捕獲的參數都不能取名為“eval”。
其他運算符
條件運算符(?:)
條件運算符是JavaScript中唯一的一個三元運算符(三個操作數),有時直接稱做“三元運算符”。通常這個運算符寫成“?:”,當然在代碼中往往不會這么簡寫,因為這個運算符擁有三個操作數,第一個操作數在“?”之前,第二個操作數在“?”和“:”之間, 第三個操作數在“:”之后,例如:
X > 0 ? X : -X // 求X的絕對值
條件運算符的操作數可以是任意類型。第一個操作數當成布爾值,如果它是真值,那么將計算第二個操作數,並返回其計算結果。否則,如果第一個操作數是假值,那么將計算第三個操作數,並返回其計算結果。第二個和第三個操作數總是會計算其中之一,不可能兩者同時執行。
其實使用if語句也會帶來同樣的效果,“?:”運算符只是提供了一種簡寫形式。這里是一個的典型應用場景,判斷一個變量是否有定義(並擁有一個有意義的真值)(這里的場景其實不包括如果變量已經定義且值為false的情況。),如果有定義則使用它,如果無定義則使用一個默認值:
greeting = "hello" + (username ? username : "there");
這和下面使用if語句的代碼是等價的,但顯然上面的代碼更加簡潔:
greeting = "hello ";
if (username)
greeting += username;
else
greeting += "there";
typeof運算符
typeof是一元運算符,放在其單個操作數的前面,操作數可以是任意類型。返回值為表示操作數類型的一個字符串。下表列出了任意值在typeof運算后的返回值:
任意值在typeof運算后的返回值
x | typeof x |
---|---|
undefined | "undefined" |
null | "object" |
true或false | "boolean" |
任意數字或NaN | "number" |
任意字符串 | "string" |
任意函數 | "function” |
任意內置對象(非函數) | “object” |
任意宿主對象 | 由編譯器各自實現的字符串,但不是"undefined","boolean","number"或"string" |
typeof最常用的用法是寫在表達式中,就像這樣:
(typeof value == "string") ? "'" + value + "'" : value
typeof運算符同樣在switch語句中非常有用,需要注意的是,typeof運算符可以帶上圓括號,這讓typeof看起來像一個函數名,而不是一個運算符關鍵字:
typeof(i)
我們注意到,當操作數是null的時候,typeof將返回"object"。如果想將null和對象區分開,則必須針對特殊值顯式檢測。對於宿主對象來說,typeof有可能並不返回“object”,而返回字符串。但實際上客戶端JavaScript中的大多數宿主對象都是“object"類型。
由於所有對象和數組的typeof運算結果是“object"而不是"function",因此它對於區分對象和其他原始值來說是很有幫助的。如果想區分對象的類,則需要使用其他的手段,比如使用instanceof運算符、class特性以及constructor屬性。
盡管JavaScript中的函數是對象的一種,但typeof運算符還是將函數特殊對待,對函數做typeof運算有着特殊的返回值。在JavaScript中,函數和“可執行的對象”(callable object)有着微妙的區別。所有的函數都是可執行的(callable),但是對象也有可能是可執行的,可以像調用函數一樣調用它,但它並不是一個真正的函數。根據 ECMAScript 3規范,對於所有內置可執行對象,typeof運算符一律返回“function”。ECMAScript 5規范則擴充至所有可執行對象,包括內置對象(native object)和宿主對象(host object),所有可執行對象進行typeof運算都將返回“function”。大多數瀏覽器廠商也將JavaScript的原生函數對象(native function object)當成它們的宿主對象 的方法來使用。但微軟卻一直將非原生可執行對象(non-native callable object)當成其客戶端的方法來使用,在IE 9之前的版本中,非原生可執行對象的typeof運算將返回 “object”,盡管它們的行為和函數非常相似。而在IE 9中,這些客戶端方法是真正的內置函數對象(native function object)。
delete運算符
delete是一元操作符,它用來刪除對象屬性或者數組元素就像賦值、遞增、遞減運算符一樣,delete也是具有副作用的,它是用來做刪除操作的,不是用來返回一個值的, 例如:
var 0 = { x: 1, y: 2}; // 定義一個對象
delete o.x; // 刪除一個屬性
"x" in o // => false:這個屬性在對象中不再存在
var a = [1,2,3] // 定義一個數組
delete a[2]; // 刪除最后一個數組元素
2 in a; // => false:元素2在數組中已經不存在了
a.length // => 3:注意,數組長度並沒有改變,盡管上一行代碼刪除了這個元素,但刪除操作留下了一個“洞”,實際上並沒有修改數組的長度,因此a數組的長度仍然是3
注
如果你是C++程序員,請注意JavaScript中的delete和C++中的delete是完全不同的。在
JavaScript中,內存的回收是通過垃圾回收自動回收的,你不用擔心內存的顯式釋放問題,這樣則完全不用像C++那樣通過delete來刪除整個對象。
需要注意的是,刪除屬性或者刪除數組元素不僅僅是設置了一個undefined的值。當刪除 一個屬性時,這個屬性將不再存在。讀取一個不存在的屬性將返回undefined,但是可以通過in運算符來檢測這個屬性是否在對象中存在。
delete希望他的操作數是一個左值,如果它不是左值,那么delete將不進行任何操作同時返回true。否則,delete將試圖刪除這個指定的左值。如果刪除成功,delete將返回 true。然而並不是所有的屬性都可刪除,一些內置核心和客戶端屬性是不能刪除的,用戶通過var語句聲明的變量不能刪除。同樣,通過function語句定義的函數和函數參數也不能刪除。
在ECMAScript 5嚴格模式中,如果delete的操作數是非法的,比如變量、函數或函數參數,delete操作將拋出一個語法錯誤(SyntaxError)異常,只有操作數是一個屬性訪問表達式的時候它才會正常工作。在嚴格模式下,delete刪除不可配置的屬性時會拋出一個類型錯誤異常。在非嚴格模式下,這些delete操作都不會報錯,只是簡單地返回false,以表明操作數不能執行刪除操作。
這里有一些關於delete運算符的例子:
var 0 = {X:1, y:2}; // 定義一個變量,初始化為對象
delete o.x; // 刪除一個對象屬性,返回true
typeof o.x; // 屬性不存在,返回"undefined"
delete o.x; // 刪除不存在的屬性,返回true
delete o; // 不能刪除通過var聲明的變量,返回false
// 在嚴格模式下,將拋出一個異常
delete 1; // 參數不是一個左值,返回true
this.x = 1; // 給全局對象定義一個屬性,這里沒有使用var
delete X; // 試圖刪除它,在非嚴格模式下返回true
// 在嚴格模式下會拋出異常,這時使用"delete this.x"來代替
x; // 運行時錯誤,沒有定義X
void運算符
void是一元運算符,它出現在操作數之前,操作數可以是任意類型。這個運算符並不是經常使用:操作數會照常計算,但忽略計算結果並返回undefined。由於void會忽略操作數的值,因此在操作數具有副作用的時候使用void來讓程序更具語義。
這個運算符最常用在客戶端的URL——javascript:URL 中,在URL中可以寫帶有副作用的表達式,而void則讓瀏覽器不必顯示這個表達式的計算結果。例如,經常在HTML代碼 中的<a>標簽里使用void運算符:
<a href="javascript:void window.open();">打開一個新窗口</a>
通過給
<a>
的onclick綁定一個事件處理程序要比在href中寫 "javascript:URL" 要更加清晰,當然,這樣的話void操作符就可有可無了。
逗號運算符(,)
逗號運算符是二元運算符,它的操作數可以是任意類型。它首先計算左操作數,然后計算右操作數,最后返回右操作數的值,看下面的示例代碼:
i=0, j=1, k=2;
計算結果是2,它和下面的代碼基本上是等價的:
i = j = k = 2;
總是會計算左側的表達式,但計算結果忽略掉,也就是說,只有左側表達式具有副作用,才會使用逗號運算符讓代碼變得更通順。逗號運算符最常用的場景是在for循環中,這個for循環通常具有多個循環變量:
// for循環中的第一個逗號是var語句的一部分
// 第二個逗號是逗號運算符
// 它將兩個表達式(i++和j++)和放在一條(for循環中的)語句中
for(var i=0,j=10; i < j; i++,j--)
console.log(i+j);