JavaScript 中的操作符

Tao-Quixote發表於2017-12-26

在 JavaScript 中使用操作符時,最經常用到,也最容易搞錯的,就是在使用操作符時 JavaScript 引擎自動進行的隱式型別轉換。

本文會從 ECMA 規範的層面來講一下不同操作符在執行時的執行規則,從規範的角度來理解一個操作符的結果為什麼是輸出的那樣,以及具體實現與規範不同的地方。

關係操作符

關係操作符主要包括如下幾個操作符(這裡介紹的關係操作符不包括 “等於”,相等操作符單獨討論):

  • 大於(>)
  • 小於(<)
  • 大於等於(>=)
  • 小於等於(<=)

以上幾個操作符都會返回一個 布林值 作為返回結果。如果只是進行基本的數值比較,跟我們在數學課上學到的內容是一樣的,結果也是一樣的;但是在 JavaScript 中,會使用關係操作符進行比較不只是純數字,還可能包括各種基本資料型別(number, string, boolean, null, undefined)以及物件(包括 Object, Function, Array)之間的比較。在比較的過程中,ECMAScript 規範中規定了針對不同資料型別使用不同操作符時 版本實現(ECMA 只規定規範,由不同廠商負責具體實現) 需要進行相應的隱式型別轉換。

接下來我們先看一下規則:

1、如果兩個運算元都是數字

執行數值比較

2、如果兩個運算元都是字串

在比較字串時,實際比較的是兩個字串對應位置字元的字元編碼的值。

例如對於應為字母組成的字串 let str1 = 'abc'let str2 = 'abd' 來說,JavaScript 引擎在處理 str1 > str2 這個表示式時,實際的處理流程為對字串 'abc''abd' 逐位求字元編碼,然後以數字的形式比較字元編碼的大小;如果兩個字串第一個字元的字元編碼相同,則比較第二個字元的字元編碼,依次向後,直到最後一個字元。

3、其中一個運算元是數字

需要將另外一個運算元轉換為數值,然後進行數值的比較

這條規則正好補上了前面兩種規則的漏洞,即當兩個運算元分別為數字與字串時的比較結果。

4、其中一個運算元是布林值

布林值線轉換為數字,再進行比較

true > false  // true
1 > false	// true
複製程式碼

上面的例子中,布林值會先轉換為數字,然後再進行數值的比較。

注:布林值轉換為數字時,false 會轉換為 0,true 會被轉換為 1。JavaScript 中的資料型別 - Number 型別

5、其中一個運算元是物件

如果其中一個運算元是物件,則會在比較時先呼叫該物件的 valueOf() 方法,對返回值按前面的規則進行比較;如果該物件沒有 valueOf() 方法,則嘗試呼叫物件的 toString() 方法,再次對返回值按前面的規則進行比較。

注意:上面的物件是廣義的物件,包括陣列和函式。

// 物件進行型別轉換,呼叫 valueOf 方法
let obj = {
	valueOf () {
		return 3
	}
}
obj > 2	// true,此時會呼叫 obj.valueOf 方法,該方法的返回值為3

// 物件進行型別轉換,呼叫 toString 方法
let obj2 = {
	toString () {
		return 2
	}
}
obj2 > 1	// true,此時會呼叫 obj2.toString 方法,該方法的返回值為 2
複製程式碼

從規範的角度理解關係操作符的執行流程

下面我們以 RelationalExpression < ShiftExpression(小於) 做例子:

主要流程步驟規範 如下

// Runtime Semantics: Evaluation

RelationalExpression: RelationalExpression < ShiftExpression

1、 Let lref be the result of evaluating RelationalExpression.
2、 Let lval be ? GetValue(lref).  // 獲取左邊運算元的值
3、 Let rref be the result of evaluating ShiftExpression.
4、 Let rval be ? GetValue(rref).  // 獲取右邊運算元的值
  // 進行抽象的關係比較
5、 Let r be the result of performing Abstract Relational Comparison lval < rval.
6、 ReturnIfAbrupt(r). 
  // 如果比較結果為 undefined,則返回 false
7、 If r is undefined, return false. Otherwise, return r.
複製程式碼

GetValue 規範(ecma262) 如下:

// GetValue 該步驟主要用來定義如何獲取運算元識別符號的值
1、ReturnIfAbrupt(V).
2、If Type(V) is not Reference, return V.
3、Let base be GetBase(V).
4、If IsUnresolvableReference(V) is true, throw a ReferenceError exception.
5、If IsPropertyReference(V) is true, then
  a、If HasPrimitiveBase(V) is true, then
    i、Assert: In this case, base will never be undefined or null.
    ii、Set base to ! ToObject(base).
  b、Return ? base.[[Get]](GetReferencedName(V), GetThisValue(V)).
6、Else base must be an Environment Record,
  a、Return ? base.GetBindingValue(GetReferencedName(V), IsStrictReference(V)) (see 8.1.1).
複製程式碼

Abstract Relational Comparison lval < rval 規範(ecma262) 如下:

  // 獲取基本資料型別,ToPrimitive 方法會將入參轉換為 JavaScript 中的基本資料型別
1、If the LeftFirst flag is true, then
  a、Let px be ? ToPrimitive(x, hint Number).
  b、Let py be ? ToPrimitive(y, hint Number).
  // 獲取基本資料型別,ToPrimitive 方法會將入參轉換為 JavaScript 中的基本資料型別
2、Else the order of evaluation needs to be reversed to preserve left to right evaluation,
  a、Let py be ? ToPrimitive(y, hint Number).
  b、Let px be ? ToPrimitive(x, hint Number).
  // 如果 px 與 px 都是字串,則按照字串的字元編碼逐一進行數值比較
3、If Type(px) is String and Type(py) is String, then
  a、If IsStringPrefix(py, px) is true, return false.
  b、If IsStringPrefix(px, py) is true, return true.
  c、Let k be the smallest nonnegative integer such that the code unit at index k within px is different
    from the code unit at index k within py. (There must be such a k, for neither String is a prefix of the other.)
  d、Let m be the integer that is the numeric value of the code unit at index k within px.
  e、Let n be the integer that is the numeric value of the code unit at index k within py.
  f、If m < n, return true. Otherwise, return false.
  // 如果並非兩個引數都是字串,首先將引數強制轉換為 Number 型別,轉換規則見 [JavaScript 中的資料型別 - Number 型別](./types.md#number)
4、Else,
  a、NOTE: Because px and py are primitive values evaluation order is not important.
  // 兩個引數都強制轉換為數字
  b、Let nx be ? ToNumber(px).
  c、Let ny be ? ToNumber(py).
  d、If nx is NaN, return undefined.
  e、If ny is NaN, return undefined.
  f、If nx and ny are the same Number value, return false.
  g、If nx is +0 and ny is -0, return false.
  h、If nx is -0 and ny is +0, return false.
  i、If nx is +∞, return false.
  j、If ny is +∞, return true.
  k、If ny is -∞, return false.
  l、If nx is -∞, return true.
  m、If the mathematical value of nx is less than the mathematical value of ny—note that these
    mathematical values are both finite and not both zero—return true. Otherwise, return false.
複製程式碼

總結

綜上可以看出,在 JavaScript 引擎對兩個運算元進行第一次隱式資料型別轉換時,會將包裝型別(Object、Array、Function) 等轉換為基本資料型別,然後開始真正的比較兩個運算元:1、如果兩個運算元在轉換之後都是字串,則逐位對字元的字元編碼進行數值比較;2、除此之外的所有資料型別,都需要轉換為數字之後進行數值比較。

其實 關係操作符 的本質是用來比較數字的,所以除了兩個運算元轉換後的的基本型別都是字串這個特例之外,其他型別的運算元都是進行數值比較的,這也符合 關係操作符 的本質作用。

相等操作符

相等操作符按嚴格程度分為兩類:

  • 相等
    • 相等(==)
    • 不等(!=)
  • 全等
    • 全等(===)
    • 不全等(!==)

相等和不等

這兩個操作符的返回結果為布林值;同關係操作符一樣,在必要時,這兩個操作符也會先轉換運算元(強制型別轉換),然後再比較運算元的相等行:

  • 如果運算元是布林值,則轉換為數字,轉換規則可見 JavaScript 中的資料型別 - Number 型別
  • 如果一個數字,一個是字串,則將字串強制轉換為數字
  • 如果一個數字,一個是物件,則呼叫物件的 valueOf 方法,然後根據前面的規則比較;如果沒有 valueOf 方法,則呼叫 toString 方法,然後將返回值根據前面的規則進行比較
  • null 與 undefined 是相等的
  • 在比較時,null 與 undefined 不進行強制型別轉換
  • NaN 與任何值都不相等,所以 NaN 與任何進行 == 比較結果都是 false,反之,進行 != 比較結果都是 true
  • 兩個物件比較時,比較是否為同一個物件(從底層來理解,就是指兩個識別符號的指標是否指向記憶體中的同一塊記憶體)

舉個例子:

'false' == false	// false,false 先轉換為 0,然後字串 ‘false’ 轉換之後 NaN,所以結果為false
'true' == true	// false,true 先轉換為 1,然後字串 ‘false’ 轉換之後 NaN,所以結果為false
0 == false	// true,因為在比較時,false 轉換為 0, true 轉換為 1
複製程式碼

⚠️ 注意:在進行操作符的比較時,不要跟 if (ifStatement) 中的條件表示式混淆,也不要類比,因為它們的規範不一致,所以,即使 if (0) {} 這個表示式中的 0 會轉換為 false,該條件不成立,也不等於 0 => false == false;因為這裡的轉換為 false 轉換為 0,0 == 0,所以結果 true;而 if 表示式中的條件表示式的返回值不要求是 布林值,引擎會自動呼叫 Boolean() 轉換條件表示式的返回值

從規範的角度理解相等操作符的執行流程

下面我們以想等操作為例子來看規範中規定的相等操作符的執行流程:

主要流程步驟如下:

// Runtime Semantics: Evaluation
EqualityExpression: EqualityExpression == RelationalExpression

  // lref 作為操作符左邊表示式的引用
1、Let lref be the result of evaluating EqualityExpression.
  // lval 作為操作符左邊表示式的值
2、Let lval be ? GetValue(lref).
  // rref 作為操作符右邊表示式的引用
3、Let rref be the result of evaluating RelationalExpression.
  // rval 作為操作符右邊表示式的值
4、Let rval be ? GetValue(rref).
  // 返回執行抽象相等比較的值
5、Return the result of performing Abstract Equality Comparison rval == lval.
複製程式碼

GetValue() 同關係操作符中的一樣,為同一個步驟,這裡不做贅述。

Abstract Equality Comparison rval == lval 步驟如下:

The comparison x == y, where x and y are values, produces true or false.
Such a comparison is performed as follows:

// 如果兩個操作符型別相同,則執行下面的嚴格相等步驟,並返回嚴格相等步驟的返回值
1、If Type(x) is the same as Type(y), then
  a、Return the result of performing Strict Equality Comparison x === y.
// null 與 undefined 相等
2、If x is null and y is undefined, return true.
3、If x is undefined and y is null, return true.
// string 型別的值要轉換為 number 型別
4、If Type(x) is Number and Type(y) is String, return the result of the comparison x == ! ToNumber(y).
5、If Type(x) is String and Type(y) is Number, return the result of the comparison ! ToNumber(x) == y.
// 布林值轉換為數字型別
6、If Type(x) is Boolean, return the result of the comparison ! ToNumber(x) == y.
7、If Type(y) is Boolean, return the result of the comparison x == ! ToNumber(y).
// 任一運算元為符合型別,則強制轉換為基本資料型別
8、If Type(x) is either String, Number, or Symbol and Type(y) is Object, return the result of the comparison x == ToPrimitive(y).
9、If Type(x) is Object and Type(y) is either String, Number, or Symbol, return the result of the comparison ToPrimitive(x) == y.
// 以上都不符合,返回 false
10、Return false.
複製程式碼

嚴格相等比較運算

The comparison x === y, where x and y are values, produces true or false.
Such a comparison is performed as follows:

1、If Type(x) is different from Type(y), return false. // 型別不一致,返回 false
// 數字型別,運算元中存在 NaN 時一律返回 false
2、If Type(x) is Number, then
3、If x is NaN, return false.
4、If y is NaN, return false.
// 數值相同,返回 true
5、If x is the same Number value as y, return true.
// +0 與 -0 相等
6、If x is +0 and y is -0, return true.
7、If x is -0 and y is +0, return true.
8、Return false.
// 非數字型別繼續比較
9、Return SameValueNonNumber(x, y).
複製程式碼

SameValueNonNumber ( x, y ):非數字型別比較

// SameValueNonNumber ( x, y )

// 判斷是否同一資料型別
1、Assert: Type(x) is not Number.
2、Assert: Type(x) is the same as Type(y).
// null 和 undefined 只有一種可能且等於自身,所以返回 true
3、If Type(x) is Undefined, return true.
4、If Type(x) is Null, return true.
// 字串逐位判斷字元編碼的數值是否相等
5、If Type(x) is String, then
  a、If x and y are exactly the same sequence of code units (same length and same code units at corresponding indices), return true; otherwise, return false.
// 布林值判斷是否相等
6、If Type(x) is Boolean, then
  a、If x and y are both true or both false, return true; otherwise, return false.
// Symbol 判斷是否為同一 Symbol 值
7、If Type(x) is Symbol, then
  a、If x and y are both the same Symbol value, return true; otherwise, return false.
// 物件判斷是否為同一個物件
8、If x and y are the same Object value, return true. Otherwise, return false.
複製程式碼

全等和不全等

其實全等在判斷過程中會執行相等的全部步驟,不過多了一個步驟,那就是型別檢查。全等會在最開始的時候檢查兩個運算元的型別是否相等,如果兩個運算元的型別不一致,則立即返回 false 結束判斷;如果型別一致,再按相等的步驟進行判斷。

總結

從規範可以看出,相等和全等是用來判斷兩個運算元的某一方面是否是一致的,與關係操作符最大的區別就在於 關係操作符 最主要的原始需求就是比較兩個數值的大小,所以規範中才會在很多情形下將運算元強制型別轉換為 Number 型別。

還需要注意的一點就是,不要拿運算元在 if 語句中的結果來作為操作符中的一種對映,這種對映關係根本就不成立,因為 if 語句中的條件表示式的計算規則與操作符的計算規則並不一樣。

不同操作符之間的計算規則雖然有相似的地方,但是並不完全一樣,所以不要盲目地生搬硬套。

逗號操作符

逗號操作符需要注意的點比較少。

1、用來宣告多個變數:

let a = 3,
	b = 4,
	c = 5
複製程式碼

2、一個比較容易忽略但是好像沒什麼用的用法是給變數賦值:

let a = (1, 2, 3, 4)	// 4
複製程式碼

上面的例子中,逗號操作符用來給變數 a 賦值。逗號操作符會取最後一個值賦值給變數 a。這是逗號操作符的一種合法用法,但是好像並沒有多大的用處。

Author Info ?

相關文章