介紹
本篇講解第四章講到的關於 表示式 的定義、複雜表示式的計算過程。
自我提問
- new 操作符做了什麼?
function Class() { this.a = 1; return {a: 2}; } new Class(); 複製程式碼
- 複雜表示式的計算順序是怎麼斷定的?
var a = {i: 1}; var b = a; a.j = a = {k: 2}; 複製程式碼
腦圖
關鍵知識點
表示式的定義
犀牛書中對 表示式 的定義為 可以計算出結果的短語,簡單來說就是 有返回值的程式碼。
MDN 上對錶達式的定義是:
An expression is any valid unit of code that resolves to a value.
表示式是一組程式碼的集合,它返回一個值。
按照這些定義理解,只要有返回值的程式碼都可以認為是表示式。
然而不確定理解是否有誤,畢竟就好像函式宣告,也有返回值,為啥就和函式表示式區分開,查了很久也沒找到確切的定義。猜測定義應該是有操作符參與計算的才算表示式,畢竟函式表示式和宣告寫法上差的就是賦值,而且表示式和運算子一般都是一起出現的。
表示式型別
看下常見的表示式型別
原始表示式
原始表示式是表示式的 最小單位,常量、直接量、關鍵字、變數
null
undefined
true
false
this
someVar
1.2
'string'
複製程式碼
犀牛書這裡有註明,null 是關鍵字,但是 undefined 是全域性變數。確實是這樣,可以試試給 null 和 undefined 賦值,但為啥這樣設計呢,不懂。
物件、陣列初始化表示式
[]
[1, 2, 1+3]
{a: 1, b: 2}
複製程式碼
函式定義表示式
var func = function(x) {
return x * x;
}
複製程式碼
屬性訪問表示式
a.x
a[x]
複製程式碼
這裡犀牛書有說了很重要的一點:不管使用哪種形式的屬性訪問表示式,在 . 和 [ 之前的表示式總是會首先計算。本文最下方有關於這個的驗證。
呼叫表示式
fun(x)
Math.max(1, 2)
複製程式碼
呼叫表示式前的表示式是一個屬性訪問表示式時這個呼叫叫做 方法呼叫,方法呼叫會將方法中的 this 指向呼叫的物件。
非方法呼叫表示式非嚴格模式會使用全域性物件作為 this 關鍵字的值,嚴格模式下為 undefined。
物件建立表示式
new Object()
new String
複製程式碼
物件建立表示式的建立步驟:
- 建立一個新的空物件
- 將建構函式 prototype 掛到空物件的原型鏈上(這個犀牛書上沒說,因為這裡書上說的比較粗,具體的會在第 9 章講解)
- 將這個新物件作為建構函式中 this 關鍵字的值
- 通過執行建構函式初始化新物件的屬性
- 如果建構函式返回了一個值(僅限物件值,原始值會被無視),新物件會被廢棄,返回值會作為表示式的值,否則會將這個新物件作為表示式的值
算數表示式
1 + 2
's' + 'a'
1 - null
++a
12 | 1
12 ^ 4
~12
12 << 1
12 >> 1
-12 >>> 1
複製程式碼
關係表示式
null == undefined
a = 1
a === b
a != 1
a !== 1
'a' < 'b'
'a' > 'b'
'a' <= 'b'
'a' >= 'b'
'x' in y
a instanceof Date
複製程式碼
邏輯表示式
x == 0 && y == 0
x == 0 || y == 0
!x
複製程式碼
賦值表示式
x = 1
a.b = 1
a++
b--
--a
++b
a += b
b *= a
複製程式碼
eval 表示式
eval('1 + 2')
複製程式碼
其它表示式
a ? 1 : 2
typeof true
delete a.b
void a++
a++, b++
複製程式碼
複雜表示式
將簡單表示式連線在一起就可以構成複雜表示式。
var a = b = c + 1 / (1 + 2) ? 1 : 2 * 100 - typeof '123'
複製程式碼
運算順序圖解
這裡結合上一篇的運算子的相關知識對一些複雜表示式進行解析,看看複雜表示式到底是如何計算的。主要關係運算子的優先順序、結合性、左值等概念,不太瞭解的可以看下上一篇。
先來個網紅題目簡單解析一下
var a = {i: 1};
var b = a;
a.j = a = {k: 2};
複製程式碼
- 取出 a.j 的左值
- 取出 a 的左值
- 將 {k: 2} 賦值給 a
- 將 a = {k: 2} 的返回值賦值給 a.j
為什麼要先取出 a.j 的記憶體地址:
- 上方屬性訪問表示式有說過,不管使用哪種形式的屬性訪問表示式,在 . 和 [ 之前的表示式總是會首先計算
- . 的運算子優先順序高於 =,所以會優先計算,在賦值語句執行前就會執行 . 運算子取出 a.j 的左值
驗證一下第一個說法是否正確:
var b = {i: 1};
var a = {i: 1, a: b};
a.a = 2, a.a.i = 3;
複製程式碼
按照犀牛書的說法 a.a.i 中的 a.a 會優先計算為 b,然後 b 的 i 被賦值為 3,不過經驗證這個說法並不正確。 所以其實並沒有這個額外的規則,只是單純的運算子優先順序和左值的問題。
再來個複雜一點的例子,看下錶達式計算時如何推斷優先順序。
var k = {
get a() {
console.log('a');
return 'a';
},
get b() {
console.log('b');
return 'b';
},
get c() {
console.log('c');
return 'c';
}
};
var b = { b: 1 };
var a = { a: 1, b: b, c: 2 };
var c = { c: 1 };
var v = 2;
a[k.a] = b[k.b] = (v * 10 + c[k.c] / 10 + v++ - ++v) | 1;
// a
// b
// c
// { a: 19, b: { b: 19 }, c: 2 } { b: 19 } 4
console.log(a, b, v);
複製程式碼
按照運算子的 優先順序從低到高 分解成最基礎的表示式和運算子,按照 結合性結合相同優先順序 的運算子,然後按照 從左往右 的順序計運算元表示式,深度優先 計算表示式。
所以順序是
a[k.a]
b[k.b]
v * 10
c[k.c]
(c[k.c]) / 10
v++
++v
(v * 10) + ((c[k.c]) / 10) + (v++) - (++v)
((v * 10) + ((c[k.c]) / 10) + (v++) - (++v)) | 1
b[k.b] = (((v * 10) + ((c[k.c]) / 10) + (v++) - (++v)) | 1)
a[k.a] = (b[k.b] = (((v * 10) + ((c[k.c]) / 10) + (v++) - (++v)) | 1))
複製程式碼
按照上述過程將複雜表示式拆解後,計算過程就一目瞭然了。