2.3 JavaScript的语法:表达式运算

在JS中,运算符大多是符号,但是也有少量单词,你应当避免把他们误解成语句

运算符\符号 运算符含义
typeof 取变量或者值得类型
void 运算表达式并忽略值
new 创建指定类的对象实例
in 检查对象属性
instanceof 检查变量是否指定类的实例
delete 删除实例属性
yield 从生成器内部返回一个值
await 在异步函数内等待一个值

yield 和 await 不是严格意义上的运算符

JS中可以存在没有运算符的表达式,被称为单值表达式,它有值的含义,表达式的结果即是该值,主要包括

  • this、super、new.target和arguments引用
  • 变量引用,即一个已经声明的标识符
  • 字面量,包括null、undefined、字符串、布尔值、数值、模板、正则表达式

2.3.1 一般表达式运算

2.3.1.1 逻辑运算

运算符会将操作数隐式转换成布尔值,以进行布尔运算

运算过程(与普通布尔运算一样)是支持布尔短路的

连续的逻辑运算可以用来替代语句

|| 如果第一个值为真,后边的表达式将不会执行

&& 如果第一个值为假,后边的表达式将不会执行

2.3.1.2 字符串运算

历史中JS有且只有一种字符串运算就是+,它总是产生一个新的字符串,它在运算效果上完全等同于调用字符串对象的concat()方法

ES5中引入了新的概念,将字符串作为类似数组的对象(to treat the string as an array-like object),引入了第二个字符串运算符[]可以通过下标存取,但你仍不能通过该运算去修改字符串中的字符值。

ES6中,字符串添加了Symbol.iterator属性,可以作为迭代对象处理,例如接受数组展开以及yield*`运算(以及for…of语句)

2.3.1.3 数值运算

包括“加减乘除”等一般性的数值运算,又包括数值的位运算,在位操作中JS强制运算目标为一个有符号的32位整型数;如果目标是非数值,那么会强制转化为数值,如果目标是浮点数,则会被取整;否则,将目标识别为有符号整型数

非数值会被转化为NaN,运算结果仍为NaN

一般表达式运算符总能成功的应用于所有的数据类型,符号例外,因为符号不能转换为数值,符号在一般表达式运算中,可以作为(或转换为)布尔值true参与运算

2.3.2 比较运算

需要关注一下JS的类型转换机制,后边会写

严格相等===会先比较数据的类型,因此这里不会发生类型转换,并且按照约定-0 === 0 以及 NaN !== NaN,于是ES6之后的版本设计了Object.is()处理类似Object.is(-0, +0) === falseObject.is(NaN,NaN) === true这样的运算逻辑

2.3.2.1 等值检测

“相等”运算规则:

类型 运算规则
值类型与引用类型进行比较 将引用类型的数据转换为与值类型相同的数据,再进行”数据等值“比较
两个值类型进行比较 转换成相同数据类型的值进行”数据等值“比较
两个引用类型进行比较 比较引用(的地址)

”数据等值”仅指针对“值类型”的比较而言,表明比较的是变量所指向的存储单元中的数据(通常指“内存数据”)。如果两个比较的值不同,那么:

  • 有任何一个是数字时,会将另一个转化为数字进行比较
  • 有任何一个是布尔值时,它将被转换为数字进行比较(由于上一个规则的存在,另一个操作数也被转换为数字)
  • 有任何一个是对象(或者函数)时,将调用该对象的valueOf()方法来将其转换为值数据进行比较,多数情况下,该值作为数字值进行处理
  • 按照特定规则返回比较结果,例如undefinednull值总是相等的

JS总是尽量使用数字值比较实现等值检测

“严格相等”运算规则:

类型 运算规则
值类型与引用类型进行比较 必然不“严格相等”
两个值类型进行比较 如果数据类型不同,则必然“不严格相等”;否则,按等值检测中“相等”的运算规则进行比较
两个引用类型进行比较 比较引用(的地址)

符号可以转换为true但是不等值于true
即使字面量相同的引用类型也是不严格相等的{} === {} // false

2.3.2.2 序列检测

Number类型是实数数轴的抽象

当等值检测中有任何一个数据是符号时。无需进行类型转换,因为它总是不等值于任何其他数据

运算符> >= < <=

任何值与NaN进行序列检测都得到false

布尔值会转换为0或者1

序列检测的运算规则:

类型 运算规则
值类型与引用类型进行比较 将引用类型的数据转换为与值类型数据相同的数据,再进行”序列大小“的比较
两个值类型进行比较 直接比较数据在序列中的大小
两个引用类型进行比较 无意义,返回false

对引用类型进行序列检测运算其实是可能的,这与valueOf()运算的效果有关,但这意味着最终比较的数据并非引用类型本身,而是它们转换后的值类型

最后,所谓的字符串的序列检测,在具体实现上是有限制的:

  • 当两个操作数都是字符串的时候,4个运算符才表示字符串检测序列
  • 当任意一个操作数时非字符串时,会将字符串转换为数值来参与运算

2.3.3 赋值运算

一般赋值运算符=

带操作的赋值运算符(复合赋值运算符)v += e

自增自减运算符(隐含赋值运算)v++

JS中赋值是一个运算而不是一个语句,所以在赋值表达式中,运算符左右都是操作数,当然,按表达式的概念,表达式的操作数可以是值也可以是引用,所以从语法上来看100 = 1000是成立的,并且可以通过JS的语法检测,但是会在运行中报错

2.3.3.1 赋值的语义

赋值运算的本质:赋值的效果时修改存储单元中的值

只是在更加术语化的表述中,这个行为也被称为”(值与标识符的)绑定“

对于引用类型来说只是复制了一个地址

这里有两个特例,值类型的字符串是一个大的,不确定长度的连续数据块,这导致复制数据的开销很大,所以在JS中,字符串的赋值也变成了复制地址——该字符串的地址引用,由此引入了三条字符串处理的限制:

  • 不能直接修改字符串中的字符
  • 字符串链接运算必然导致写复制,这将产生新的字符串
  • 不能改变字符串的长短,修改length属性是无意义的

在ES6后的结构赋值中,赋值运算符的语义不只有”复制地址、复制引用“还包括”解析数组或者对象的成员“这一行为

2.3.3.2 复合赋值运算符

除了+=能用于字符串,剩下得都只能用于数值类型,如果是非数值类型,将发生隐式类型转换

2.3.3.3 解构赋值

是对=的一个拓展,能用于所有该操作符的地方

2.3.4 函数相关的表达式

匿名函数和箭头函数不存在函数声明的语法

2.3.4.1 匿名函数和箭头函数

声明函数时

function functionName() {
  // ...
}

如果省略functionName就可以得到一个匿名函数(Anonymous Function)。这个匿名函数也可以作为字面量直接参加表达式运算——而不需要引用它的名字,例如:

void( 1 + function() {
  // ...
})

箭头函数声明出来也是匿名的,毕竟它的语法中就不不能指定函数名,完整形式为

( parameters ) => { functionBody }

parameters只是一个参数时,这对圆括号可以省略

2.3.4.2 函数调用

在函数紧跟函数运算符()时,该运算符被解释成两个含义

  • 使函数得以执行,并且,当函数执行时
  • 从左至右运算并传入()内的参数序列

函数调用运算作用于以下几个变量的效果是一致的

function func1() {};
var func2 = function() {};
var func3 = new Function('');
var func4 = () => {};

如果运算符之前既非上述几种之一也并非他们的引用,就会触发一个运行期异常

ES6之后的模板处理函数是一种隐式的函数调用。它通过在函数后跟一个模板字符串来调用该函数,并按特定格式传入参数。

function foo(tp1, ...values) {
  console.log(tp1) ;// {"try call", "."}
  console.log(values[0]) // "foo"
}

// 将foo函数作为模板处理函数使用
foo`try call ${foo.name}.`;

其他的隐式调用函数的情况包括:

  • 将函数作为属性读取器时,属性存取操作将隐式调用该函数
  • 使用.bind方法将原函数绑定为目标函数时,调用目标函数则隐式调用源函数
  • 当使用Proxy()创建源函数的代理对象时,调用代理对象则隐式调用源函数
  • 可以使用new运算符变相地调用函数
  • 可以将函数赋值给对象的符号属性,并在对象相应行为触发时调用该函数

2.3.4.3 new运算

用class声明的类也将是一个函数(构造器),但它只能被new运算调用

严格来说,在

new functionReference()

语法中functionReference后边的阔号并不是函数调用运算符,而只是new运算符语法的参数传入表——这是因为,在这一语法中决定(或启动)函数调用的是new运算符而不是后边的阔号,不过在少数情况下,new运算符会被用作隐式的函数调用

2.3.5 特殊作用的运算符

有些运算符不产生运算效果,而是用于影响运算效果,这一类运算符的操作对通常是”表达式“,而并非”表达式的值“

2.3.5.1 类型运算符

这里的typeof是运算符而不是关键字,之所以是特殊的运算符,是因为在其他运算符中,变量是以值参与运算的,而typeof运算符是尝试获取值的类型信息,甚至也可以直接对标识符进行运算,而无视该标识符是否存在,是否声明过,是否绑定值,类似的情况下,它将返回undefined

ES6之后标识符不只是简单的有或者没有,所以使用typeof就不再总是安全的,当对”使用let或者const声明但是没绑定值得标识符“运算时,将会抛出一个异常

2.3.5.2 展开语法

...称为展开”spread syntax/operator“。将它置于一个可迭代对象之前时,表明将操作数展开为一组值;将它置于一个普通对象之前时,则表明对象展开为一组属性,如果展开发生在函数调用界面上,则展开结果作为参数,如果展开在一个数组或者对象中,则结果作为他们的成员

2.3.5.3 面向表达式的运算符

尽管它们以表达式为目标,但是语义上仍然是运算符,因此他们与运算对象(表达式)结合的结果仍然是表达式(而非语句)

对于ES5之后出现的 await 和 yield 来说,强制它们的操作数发生运算并得到该运算的值是他们作为运算符的基本功能,而进行流程控制时它们的副作用

在这两个运算中,await会有两种流程控制效果

  • 运算符await总会挂起函数,直到它等待的数据就绪
  • 如果它等待的数据被rejected,那么它会触发一个throw效果,以便将rejected的原因,任何可能的值或者对象作为异常抛出
  • 运算符yield总会挂起函数(并将生成的值作为外部的迭代对象的.next()方法的结果值返回)
  • 运算符总是在外部迭代对象调用下一个.next(x)方法时恢复函数,并将传入的值作为yield运算的结果值

2.3.6 运算优先级

运算符 描述
() 成组运算
. [] new 对象成员存取、数组下标、带参数的new
() new 函数调用、不带参数的new
++ – 后置递增、递减
+ - ++ – ~ ! delete typeof void 前置加减号、递增、递减、逻辑与、按位非、其他
* / % 乘法、除法、取模
+ - + 加法、减法、字符串链接
<< >> >>> 移位
< > <= >= in instanceof 序列检测、in、instanceof
== != === !== 等值检测
& 按位与
^ 按位异或
| 按位或
&& 逻辑与
|| 逻辑或
?: 条件
= op= 赋值、运算赋值
yield yield* yield表达式
展开
, 多重求值


语言   js      js 基础 绿宝书

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!