表达式和类型转换
表达式和类型转换
表达式语句
表达式语句实际上就是一个表达式,它是由运算符连接变量或者直接量构成的。
PrimaryExpression 主要表达式
Primary Expression 是表达式的最小单位,它涉及的语法结构也是优先级最高的。
Primary Expression 包含了各种直接量,直接量为直接用某种语法写出来的具有特定类型的值。
任何表达式加上圆括号,都被认为是 Primary Expression。使得圆括号成为改变运算优先顺序的手段。
"abc"; // 字符串
123; // 数值
null; // null:使用 null 关键字获取 null 值
true; // 布尔值
false; // 布尔值
({}); // 对象
(function(){}); // 函数
(class{}); // 类
/abc/g; // 正则表达式
this; // this
myVar; // 变量:在语法上,把变量称作“标识符引用”
(a + b); // 任何表达式加上圆括号,都被认为是 Primary Expression
MemberExpression 成员表达式
Member Expression 通常用于访问对象成员。
a.b; // 使用标识符的属性访问
a["b"]; // 使用字符串的属性访问
f`a${b}c` // 带函数名的模板表示把模板的各个部分算好后传递给一个函数
super.b; // 构造函数中,用于访问父类的属性的语法
super['b']
new.target; // 用于判断函数是否是被new调用
new Foo(); // 带参数列表的new运算
注意,不带参数列表的 new
运算优先级更低,不属于Member Expression。
NewExpression NEW表达式
基本形式:Member Expression 加上 new
不加 new
也可以构成 New Expression,JavaScript 中默认独立的高优先级表达式都可以构成低优先级表达式.
注意,这里的 New Expression 特指没有参数列表的表达式。
// new new Cls(1);
// 可能有两种意思: new(new Cls(1)) 或者 new (new Cls)(1)
// 实际上,等价于 new(new Cls(1))
// 验证
class Cls{
constructor(n){
console.log("cls", n);
return class {
constructor(n) {
console.log("returned", n);
}
}
}
}
new (new Cls(1));
// cls 1
// returned undefined
// 从打印结果可知:1 被当做调用 Cls 时的参数传入
CallExpression 函数调用表达式
基本形式:Member Expression 后加一个括号里的参数列表,或者使用 super
关键字代替 Member Expression。
Call Expression就失去了比New Expression优先级高的特性。
a.b(c);
foo();
super();
foo()['b'];
foo().b;
foo()`abc`;
a.b(c)(d)(e);
a.b(c)[3];
a.b(c).d;
a.b(c)`xyz`;
LeftHandSideExpression 左值表达式
New Expression 和 Call Expression 统称 LeftHandSideExpression,左值表达式。
左值表达式最经典的用法是用于构成赋值表达式。
a() = b; // 这个用法符合语法,只是原生 JavaScript 函数,返回的值不能被赋值
a().c = b;
AssignmentExpression 赋值表达式
最基本的形式是使用等号赋值。
a = b;
a = b = c = d; // 连续赋值,是右结合的。
// 等价于
a = (b = (c = d)) // d 赋值给 c,然后再把整个表达式的结果赋值给 b,再赋值给 a
a += b; // 相当于 a = a + b
// 其他的运算符:
// *=、/=、%=、+=、-=、<<=、>>=、>>>=、&=、^=、|=、**=
Expression 表达式
赋值表达式可以构成 Expression 表达式的一部分。在JavaScript中,表达式就是用逗号运算符连接的赋值表达式。
逗号运算符比赋值运算符的优先级更低。“整个表达式的结果”就是“最后一个逗号后的表达式结果”。
a = b, b = 1, null; // 逗号分隔的表达式会顺次执行,表达式的结果为 null
UpdateExpression 更新表达式
左值表达式搭配 ++
--
运算符,可以形成更新表达式。
注意:在 ES2018 中,前后自增自减运算被放到了同一优先级。
-- a;
++ a;
a --;
a ++;
UnaryExpression 一元运算表达式
更新表达式搭配一元运算符,可以形成一元运算表达式。
delete a.b;
void a;
typeof a;
- a;
~ a;
! a;
await a;
ExponentiationExpression 乘方表达式
乘方表达式由更新表达式构成的。使用 **
号,结合性是右结合的。
++i ** 30;
2 ** 30; // 正确
-2 ** 30; // 报错:-2 是一元运算表达式,不可以放入乘方表达式,如果需要表达类似的逻辑,必需加括号
4 ** 3 ** 2; // 运算方式为: 4 ** (3 ** 2) = 262144
MultiplicativeExpression 乘法表达式
乘法表达式有三种运算符: * (乘)
、/ (除)
、% (除余)
,它们的优先级一样。
乘方表达式可以构成乘法表达式。
AdditiveExpression 加法表达式
加法表达式是由乘法表达式用 + (加号)
或者 - (减号)
连接构成。
a + b * c;
ShiftExpression 移位表达式
移位表达式由加法表达式构成,移位是一种位运算,分成三种:<< (向左移位)
、>> (向右移位)
、>>> (无符号向右移位)
移位运算把操作数看做二进制表示的整数,然后移动特定位数
- 左移n位相当于乘以2的n次方
- 右移n位相当于除以2取整n次
普通移位会保持正负数。无符号移位会把减号视为符号位1,同时参与移位。
二进制操作整数并不能提高性能,移位运算这里也仅仅作为一种数学运算存在,这些运算存在的意义也仅仅是照顾C系语言用户的习惯了。
-1 >>> 1 // 2147483647 (2^31)
RelationalExpression 关系表达式
关系表达式使用 > (大于)
、< (小于)
、>= (大于等于)
、<= (小于等于)
、instanceof
、in
等运算符号连接,统称为关系运算。
null <= undefined // false
null == undefined // true
EqualityExpression 相等表达式
相等表达式由四种运算符(==
、!=
、===
、!==
)和关系表达式构成。
类型不同的变量比较时 ==
运算只有三条规则:
- undefined 与 null 相等;
- 字符串和 Bool 都转为数字再比较;
- 对象转换成 primitive 类型再比较。
false == '0' // true
true == 'true' // false
[] == 0 // true
[] == false // true
new Boolean('false') == false // false
// 建议仅在确认 == 发生在Number和String类型之间时使用
document.getElementsByTagName('input')[0].value == 100
位运算表达式
位运算表达式含有三种:
- 按位与表达式(BitwiseANDExpression):使用
& (按位与运算符)
,把操作数视为二进制整数,然后把两个操作数按位做与运算。 - 按位异或表达式(BitwiseANDExpression):使用
^ (按位异或运算符)
,把操作数视为二进制整数,然后把两个操作数按位做异或运算。异或两位相同时得0,两位不同时得1。 - 按位或表达式(BitwiseORExpression):使用
| (按位或运算符)
,把操作数视为二进制整数,然后把两个操作数按位做或运算。
// 两次异或运算相当于取消。可以使用异或运算来交换两个整数的值。
let a = 102, b = 324;
a = a ^ b;
b = a ^ b;
a = a ^ b;
console.log(a, b);
逻辑与表达式和逻辑或表达式
逻辑与表达式:由按位或表达式经过逻辑与运算符连接构成 逻辑或表达式:由逻辑与表达式经逻辑或运算符连接构成
逻辑与表达式(&&)和逻辑或表达式(||)都不会做类型转换,所以尽管是逻辑运算,但是最终的结果可能是其它类型。
// 短路特性
true || foo(); // foo 将不会执行
ConditionalExpression 条件表达式
条件表达式由逻辑或表达式和条件运算符构成,条件运算符又称三目运算符。
condition ? branch1 : branch2
运算符优先级汇总
将所有运算符按照优先级的不同从高(20)到低(1)排列
优先级 | 运算类型 | 关联性 | 运算符 |
---|---|---|---|
21 | 圆括号 | n/a(不相关) | ( … ) |
20 | 成员访问 | 从左到右 | … . … |
需计算的成员访问 | 从左到右 | … [ … ] | |
new(带参数列表) | n/a | new … ( … ) | |
函数调用 | 从左到右 | … ( … ) | |
可选链(Optional chaining) | 从左到右 | ?. | |
19 | new(无参数列表) | 从右到左 | new … |
18 | 后置递增(运算符在后) | n/a | … ++ |
后置递减(运算符在后) | … -- | ||
17 | 逻辑非 | 从右到左 | ! … |
按位非 | ~ … | ||
一元加法 | + … | ||
一元减法 | - … | ||
前置递增 | ++ … | ||
前置递减 | -- … | ||
typeof | typeof … | ||
void | void … | ||
delete | delete … | ||
await | await … | ||
16 | 幂 | 从右到左 | … ** … |
15 | 乘法 | 从左到右 | … * … |
除法 | … / … | ||
取模 | … % … | ||
14 | 加法 | 从左到右 | … + … |
减法 | … - … | ||
13 | 按位左移 | 从左到右 | … << … |
按位右移 | … >> … | ||
无符号右移 | … >>> … | ||
12 | 小于 | 从左到右 | … < … |
小于等于 | … <= … | ||
大于 | … > … | ||
大于等于 | … >= … | ||
in | … in … | ||
instanceof | … instanceof … | ||
11 | 等号 | 从左到右 | … == … |
非等号 | … != … | ||
全等号 | … === … | ||
非全等号 | … !== … | ||
10 | 按位与 | 从左到右 | … & … |
9 | 按位异或 | 从左到右 | … ^ … |
8 | 按位或 | 从左到右 | `… |
7 | 逻辑与 | 从左到右 | … && … |
6 | 逻辑或 | 从左到右 | `… |
5 | 空值合并 | 从左到右 | … ?? … |
4 | 条件运算符 | 从右到左 | … ? … : … |
3 | 赋值 | 从右到左 | … = … |
… += … | |||
… -= … | |||
… **= … | |||
… *= … | |||
… /= … | |||
… %= … | |||
… <<= … | |||
… >>= … | |||
… >>>= … | |||
… &= … | |||
… ^= … | |||
`… | = …` | ||
… &&= … | |||
`… | = …` | ||
… ??= … | |||
2 | yield | 从右到左 | yield … |
yield* | yield* … | ||
1 | 展开运算符 | n/a | ... … |
0 | 逗号 | 从左到右 | … , … |
类型转换
装箱转换
每一种基本类型 Number
、String
、Boolean
、Symbol
在对象中都有对应的类。装箱转换是把基本类型转换为对应的对象。
全局的 Symbol
函数无法使用 new
来调用,可以利用装箱机制得到一个 Symbol
对象,利用一个函数的 call
方法强迫产生装箱。
var symbolObject = (function(){ return this; }).call(Symbol("a"));
console.log(typeof symbolObejct); // object
console.log(symbolObject instanceof Symbol); // true
console.log(symbolObject.constructor == Symbol); // true
装箱机制会频繁产生临时对象,在一些对性能要求较高的场景下,我们应该尽量避免对基本类型做装箱转换。
使用内置的 Object
函数,可以在 JavaScript 代码中显式调用装箱能力。
var symbolObject = Object(Symbol("a"));
console.log(typeof symbolObject); // object
console.log(symbolObject instanceof Symbol); // true
console.log(symbolObject.constructor == Symbol); // true
每一类装箱对象皆有私有的 Class
属性,这些属性可以用 Object.prototype.toString
获取,没有任何方法可以更改私有的 Class
属性,因此 Object.prototype.toString
是可以准确识别对象对应的基本类型的方法,它比 instanceof
更加准确。
var symbolObject = Object(Symbol("a"));
console.log(Object.prototype.toString.call(symbolObject)); // [object Symbol]
注意:call
本身会产生装箱操作,所以需要配合 typeof 来区分基本类型还是对象类型。
拆箱转换
在 JavaScript 标准中,规定了 ToPrimitive
函数,它是对象类型到基本类型的转换(即,拆箱转换)。
对象到 String
和 Number
的转换都遵循“先拆箱再转换”的规则:通过拆箱转换,把对象变成基本类型,再从基本类型转换为对应的 String 或者 Number。
var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}
o * 2
// 先执行 valueOf,再执行 toString,最后抛出一个 TypeError,这说明拆箱转换失败。打印结果如下:
// valueOf
// toString
// TypeError
String(o)
// 进行 String 的拆箱转换,会优先调用 toString
// toString
// valueOf
// TypeError
// 在 ES6 之后,允许对象通过显式指定 @@toPrimitive Symbol 来覆盖原有的行为。
o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}
// toPrimitive
// hello