四.JavaScript表达式和运算符

256 阅读23分钟

表达式(expression)JavaScript中的一个短语,JavaScript解释器会将其计算(evaluate)出一个结果。

复杂表达式是由简单表达式组成的。比如,数组访问表达式是由一个表示数组的表达式、左方括号、一个整数表达式和右方括号构成。它们所组成的新的表达式的运算结果是该数组的特定位置的元素值。同样的,函数调用表达式由一个表示函数对象的表达式和0个或多个参数表达式构成。

将简单表达式组合成复杂表达式最常用的方法就是使用运算符(operator)。

1. 原始表达式

最简单的表达式是“原始表达式”(primary expression)。原始表达式是表达式的最小单位——它们不再包含其他表达式。

JavaScript中的原始表达式包含常量或直接量、关键字和变量。

1.23; //数字直接量
"hello"; //字符串直接量
/pattern/; //正则表达式直接量

true; //返回一个布尔值:真
false; //返回一个布尔值:假
null; //返回一个值:空
this; //返回"当前"对象

i; //返回变量i的值
sum; //返回sum的值
undefined; //undefined是全局变量,和null不同,它不是一个关键字

2. 属性访问表达式

属性访问表达式运算得到一个对象属性或一个数组元素的值。JavaScript为属性访问定义了两种语法:

  • 表达式后跟随一个句点和标识符。表达式指定对象,标识符则指定需要访问的属性的名称。
  • 使用方括号,方括号内是另外一个表达式(这种方法适用于对象和数组)。括号内的表达式指定要访问的属性的名称或者代表要访问数组元素的索引。
var o = { 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[1]; //=>4:表达式a中索引为1的元素
a[2]["1"]; //=>6:表达式a[2]中索引为1的元素
a[0].x; //=>1:表达式a[0]的x属性

不管使用哪种形式的属性访问表达式,在“.”和“[”之前的表达式(数组或对象)总是会首先计算。如果计算结果是null或者undefined,表达式会抛出一个类型错误异常,因为这两个值都不能包含任意属性。如果运算结果不是对象(或者数组),JavaScript会将其转换为对象。

  • 如果属性名称是一个保留字或者包含空格和标点符号,或是一个数字(对于数组来说),则必须使用方括号的写法。
  • 当属性名是通过运算得出的值而不是固定的值的时候,这时必须使用方括号写法

3. 调用表达式

JavaScript中的调用表达式(invocation expression)是一种调用(或者执行)函数或方法的语法表示。

f(0); //f是一个函数表达式;0是一个参数表达式
Math.max(x, y, z); //Math.max是一个函数;x,y和z是参数
a.sort(); //a.sort是一个函数,它没有参数

当对调用表达式进行求值的时候,首先计算函数表达式,然后计算参数表达式,得到一组参数值。如果函数表达式的值不是一个可调用的对象,则抛出一个类型错误异常。

然后,实参的值被依次赋值给形参,这些形参是定义函数时指定的,接下来开始执行函数体。如果函数使用return语句给出一个返回值,那么这个返回值就是整个调用表达式的值。否则,调用表达式的值就是undefined。

4. 对象创建表达式

对象创建表达式(object creation expression)创建一个对象并调用一个函数(这个函数称做构造函数)初始化新对象的属性。

new Object();
new Point(2, 3);

当计算一个对象创建表达式的值时,JavaScript首先创建一个新的空对象,然后,JavaScript通过传入指定的参数并将这个新对象当做this的值来调用一个指定的函数。这个函数可以使用this来初始化这个新创建对象的属性。

那些被当成构造函数的函数不会返回一个值,并且这个新创建并被初始化后的对象就是整个对象创建表达式的值。

如果一个构造函数确实返回了一个对象值,那么这个对象就作为整个对象创建表达式的值,而新创建的对象就废弃了。

5. 运算符

需要注意的是,大多数运算符都是由标点符号表示的,比如“+”和“=”。而另外一些运算符则是由关键字表示的,比如delete和instanceof。

表4-1是按照运算符的优先级排序的,前面的运算符优先级要高于后面的运算符优先级。被水平分割线分隔开来的运算符具有不同的优先级。

标题为A的列表示运算符的结合性,L(从左至右)或R(从右至左),标题为N的列表示操作数的个数。

标题为“类型”的列表示期望的操作数类型,以及运算符的结果类型(在“→”符号之后)。 image.png

5.1 操作数的个数

运算符可以根据其操作数的个数进行分类。

  • JavaScript中的大多数运算符(比如“*”乘法运算符)是一个二元运算符(binary operator),将两个表达式合并成一个稍复杂的表达式。换言之,它们的操作数均是两个。
  • JavaScript同样支持一些一元运算符(unary operator),它们将一个表达式转换为另一个稍复杂的表达式。表达式-x中的“-”运算符就是一个一元运算符,是将操作数x求负值。
  • 最后,JavaScript支持一个三元运算符(ternary operator),条件判断运算符“?:”,它将三个表达式合并成一个表达式。

JavaScript运算符通常会根据需要对操作数进行类型转换

5.2 左值

表4-1中的赋值运算符和其他少数运算符期望它们的操作数是lval类型。左值(lvalue)是一个古老的术语,它是指“表达式只能出现在赋值运算符的左侧”。

简单来说左值表示一个内存地址值,并且通过这个内存地址,就可以对内存进行读并且写(主要是能写),这也是左值可以被赋值的原因。

在JavaScript中,变量、对象属性和数组元素均是左值。ECMAScript规范允许内置函数返回一个左值,但自定义的函数则不能返回左值。

5.3 运算符优先级

运算符优先级控制着运算符的执行顺序。优先级高的运算符(表格的顶部)的执行总是先于优先级低(表格的底部)的运算符。

w=x+y*z;

乘法运算符“*”比加法运算符“+”具有更高的优先级,所以乘法先执行,加法后执行。然后,由于赋值运算符“=”具有最低的优先级,因此赋值操作是在右侧的表达式计算出结果后进行的。

属性访问表达式和调用表达式的优先级要比表4-1中列出的所有运算符都要高。

typeof my.functions[x](y)

尽管typeof是优先级最高的运算符之一,但typeof也是在两次属性访问和函数调用之后执行的。

5.4 运算符的结合性

结合性指定了在多个具有同样优先级的运算符表达式中的运算顺序。

  • 从左至右是指运算的执行是按照由左到右的顺序进行。例如,减法运算符具有从左至右的结合性,因此:
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));

6. 算术表达式

运算符“%”计算的是第一个操作数对第二个操作数的模。换句话说,就是第一个操作数除以第二个操作数的余数。结果的符号和第一个操作数(被除数)的符号保持一致。例如,5%2结果是1,-5%2的结果是-1。

求余运算符的操作数通常都是整数,但也适用于浮点数。比如,6.5%2.1结果是0.2。

6.1 “+”运算符

加号的转换规则优先考虑字符串连接,如果其中一个操作数是字符串或者转换为字符串的对象,另外一个操作数将会转换为字符串,加法将进行字符串的连接操作。如果两个操作数都不是类字符串(string-like)的,那么都将进行算术加法运算。

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"

6.2 一元运算符 递增(++)递减(--)

递增“++”运算符对其操作数进行增量(加一)操作,运算符将操作数转换为数字,然后给数字加1。

递增“++”运算符的返回值依赖于它相对于操作数的位置。

  • 当运算符在操作数之前,称为“前增量”(pre-increment)运算符,它对操作数进行增量计算,并返回计算后的值。(前置++)
  • 当运算符在操作数之后,称为“后增量”(post-increment)运算符,它对操作数进行增量计算,但返回未做增量计算的(unincremented)值。(后置++)
var i = 1, j = ++i; //i和j的值都是2
var i = 1, j = i++; //i2,j是1

需要注意的是,表达式++x并不总和x=x+1完全一样,“++”运算符从不进行字符串连接操作,它总是会将操作数转换为数字并增1。如果x是字符串“1”,++x的结果就是数字2,而x+1是字符串“11”。

"b=(a++)+a;"将如何计算结果呢?

image.png

6.3 位运算符

  • 位运算符可以对由数字表示的二进制数据进行更低层级的按位运算。
  • 它们作用于数值类型的操作数并返回数字。
  • 前4个运算符都是对操作数的每个位进行布尔运算,这里将操作数的每个位当做布尔值(1=true,0=false),其他三个位运算符用来进行左移位和右移位。

位运算符要求它的操作数是整数,需要注意的是,位运算符会将NaN、Infinity和-Infinity都转换为0。

6.3.1 按位与(&)

位运算符“&”对它的整型操作数逐位执行布尔与(AND)操作。只有两个操作数中相对应的位都是1,结果中的这一位才是1。例如,0x1234&0x00FF=0x0034。

0x1234 = 0001 0010 0011 0100
0x00FF = 0000 0000 1111 1111
0x0034 = 0000 0000 0011 0100 // 十进制52 

6.3.2 按位或(|)

位运算符“|”对它的整型操作数逐位执行布尔或(OR)操作。如果其中一个操作数相应的位为1,或者两个操作数相应位都是1,那么结果中的这一位就为1。例如:0x1234|0x00FF=0x12FF。

0x1234 = 0001 0010 0011 0100
0x00FF = 0000 0000 1111 1111
0x12FF = 0001 0010 1111 1111

6.3.3 按位异或(^)

位运算符“|”对它的整型操作数逐位执行布尔异或(XOR)操作。异或是指第一个操作数为true或第二个操作数为true,但两者不能同时为true。如果两个操作数中只有一个相应位为1(不能同时为1),那么结果中的这一位就是1。例如,0xFF00^0xF0F0=0x0FF0。

0xFF00 = 1111 1111 0000 0000
0xF0F0 = 1111 0000 1111 0000
0x0FF0 = 0000 1111 1111 0000

6.3.4 按位非(~)

运算符“”是一元运算符,位于一个整型参数之前,它将操作数的所有位取反。根据JavaScript中带符号的整数的表示方法,对一个值使用“~”运算符相当于改变它的符号并减1。例如,

~0x0F=0xFFFFFFF0或-16。

image.png

6.3.5 左移(<<)

将第一个操作数的所有二进制位进行左移操作,移动的位数由第二个操作数指定,移动的位数是0~31之间的一个整数。

将一个值左移1位相当于它乘以2,左移两位相当于乘以4,以此类推。例如,7<<2=28。

image.png

6.3.6 带符号右移(>>)

右边溢出的位将忽略。填补在左边的位由原操作数的符号决定,以便保持结果的符号与原操作数一致。

如果第一个操作数是正数,移位后用0填补最高位;如果第一个操作数是负的,移位后就用1填补高位。

将一个值右移1位,相当于用它除以2(忽略余数),右移两位,相当于它除以4,以此类推,例如,7>>1=3,-7>>1=-4。

image.png

6.3.7 无符号右移(>>>)

运算符“>>>”和运算符“>>”一样,只是左边的高位总是填补0,与原来的操作数符号无关,例如,-1>>4=-1,但是-1>>>4=0x0FFFFFFF。

image.png

7. 关系表达式

7.1 相等和不等运算符

  • ===”也称为严格相等运算符(strict equality)(有时也称做恒等运算符(identity operator)),它用来检测两个操作数是否严格相等。
  • ==”运算符称做相等运算符(equality operator),它用来检测两个操作数是否相等,这里“相等”的定义非常宽松,可以允许进行类型转换。
  • !=”和“!==”运算符的检测规则是“==”和“===”运算符的求反。“!=”称做“不相等”、“!==”称做“不严格相等”。

7.2 比较运算符

加号运算符和比较运算符的行为有所不同,前者更偏爱字符串,如果它的其中一个操作数是字符串的话,则进行字符串连接操作。

比较运算符则更偏爱数字,只有在两个操作数都是字符串的时候,才会进行字符串的比较:

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

7.3 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的元素

7.4 instanceof 运算符

instanceof运算符希望左操作数是一个对象,右操作数标识对象的类。如果左侧的对象是右侧类的实例,则表达式返回true;否则返回false。

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。如果右操作数不是函数,则抛出一个类型错误异常。

对象o中存在一个隐藏的成员,这个成员指向其父类的原型,如果父类的原型是另外一个类的实例的话,则这个原型对象中也存在一个隐藏成员指向另外一个类的原型,这种链条将许多对象或类串接起来,既是原型链。

为了计算表达式o instanceof f,JavaScript首先计算f.prototype,然后在o的原型链中查找f.prototype,如果找到,那么o是f(或者f的父类)的一个实例,表达式返回true。如果f.prototype不在o的原型链中的话,那么o就不是f的实例,instanceof返回false。

8 逻辑表达式

8.1 逻辑与(&&)

  • 运算符首先计算左操作数的值,即首先计算“&&”左侧的表达式。如果计算结果是假值,那么整个表达式的结果一定也是假值,因此“&&”这时简单地返回左操作数的值,而并不会对右操作数进行计算。
  • 当左操作数是真值时,“&&”运算符将计算右操作数的值并将其返回作为整个表达式的计算结果
var o = { x: 1 };
var p = null;
o && o.x; //=>1:  o是真值,因此返回值为o.x
p && p.x; //=>null:  p是假值,因此将其返回,而并不去计算p.x

&&”的行为有时称做“短路”(short circuiting),下面两行JavaScript代码是完全等价的:

if (a == b) stop(); //只有在a==b的时候才调用stop()
a == b && stop(); //同上

8.2 逻辑或(||)

它会首先计算第一个操作数的值,也就是说会首先计算左侧的表达式。如果计算结果为真值,那么返回这个真值。否则,再计算第二个操作数的值,即计算右侧的表达式,并返回这个表达式的计算结果。

这个运算符最常用的方式是用来从一组备选表达式中选出第一个真值表达式:

//如果max_width已经定义了,直接使用它;否则在preferences对象中查找max_width
//如果没有定义它,则使用一个写死的常量
var max = max_width || preferences.max_width || 500;

这种惯用法通常用在函数体内,用来给参数提供默认值:

//将o的成员属性复制到p,并返回p
function copy(o, p) {
  p = p || {}; //如果向参数p没有传入任何对象,则使用一个新创建的对象
  //函数体内的主逻辑
}

8.3 逻辑非(!)

  • 和“&&”与“||”运算符不同,“!”运算符首先将其操作数转换为布尔值,然后再对布尔值求反。
  • 可以通过使用两次逻辑非运算来得到一个值的等价布尔值:!!x

9 赋值表达式

赋值操作符的结合性是从右至左,也就是说,如果一个表达式中出现了多个赋值运算符,运算顺序是从右到左。

i = j = k = 0; //把三个变量初始化为0

除了常规的赋值运算“=”之外,JavaScript还支持许多其他的赋值运算符,这些运算符将赋值运算符和其他运算符连接起来,提供一种更为快捷的运算方式。

image.png

10 表达式运算

和其他很多解释性语言一样,JavaScript同样可以解释运行由JavaScript源代码组成的字符串,并产生一个值。JavaScript通过全局函数eval()来完成这个工作:

eval("3+2"); //=>5

eval()是一个函数,但它已经被当成运算符来对待了。

10.1 eval()

eval()只有一个参数。如果传入的参数不是字符串,它直接返回这个参数。

如果参数是字符串,它会把字符串当成JavaScript代码进行编译。如果编译失败则抛出一个语法错误(SyntaxError)异常。如果编译成功,则开始执行这段代码,并返回字符串中的最后一个表达式或语句的值,如果最后一个表达式或语句没有值,则最终返回undefined。

eval()最重要的是,它使用了调用它的变量作用域环境。

如果一个函数定义了一个局部变量x,然后调用eval("x"),它会返回局部变量的值。如果它调用eval("x=1"),它会改变局部变量的值。如果函数调用了eval("var y=3;"),它声明一个新的局部变量y。

eval的字符串执行时的上下文环境和调用函数的上下文环境是一样的

10.2 全局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 y = "local"; //定义局部变量
  geval("y+='changed';"); //间接调用改变了全局变量的值
  return y; //返回未更改的局部变量
}
console.log(f(), x); //更改了局部变量:输出"localchanged global":
console.log(g(), y); //更改了全局变量:输出"local globalchanged":

image.png

10.3 严格eval()

在严格模式下,eval执行的代码段可以查询或更改局部变量,但不能在局部作用域中定义新的变量或函数。

严格模式将"eval"列为保留字,这让eval()更像一个运算符。不能用一个别名覆盖eval()函数。并且变量名、函数名、函数参数或者异常捕获的参数都不能取名为"eval"。

11 其他运算符

11.1 条件运算符

这个运算符拥有三个操作数,第一个操作数在“?”之前,第二个操作数在“?”和“:”之间,第三个操作数在“:”之后,

greeting="hello"+(username?username:"there");

11.2 typeof运算符

typeof是一元运算符,放在其单个操作数的前面,操作数可以是任意类型。返回值为表示操作数类型的一个字符串。

image.png

typeof最常用的用法是写在表达式中,就像这样:

typeof value == "string" ? "'" + value + "'" : value;

11.3 delete运算符

delete是一元操作符,它用来删除对象属性或者数组元素

var o = { 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

当删除一个属性时,这个属性将不再存在。读取一个不存在的属性将返回undefined,但是可以通过in运算符来检测这个属性是否在对象中存在。

并不是所有的属性都可删除,一些内置核心和客户端属性是不能删除的,用户通过var语句声明的变量不能删除。同样,通过function语句定义的函数和函数参数也不能删除。

var o = { 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

11.4 void 运算符

void是一元运算符,它出现在操作数之前,操作数可以是任意类型。这个运算符并不是经常使用:操作数会照常计算,但忽略计算结果并返回undefined。

a href="javascript:void window.open();">打开一个新窗口</a

image.png

11.5 逗号运算符

逗号运算符是二元运算符,它的操作数可以是任意类型。它首先计算左操作数,然后计算右操作数,最后返回右操作数的值

i=0,j=1,k=2; //等价于i=0;j=1;k=2;

image.png

逗号运算符最常用的场景是在for循环中,这个for循环通常具有多个循环变量:

//for循环中的第一个逗号是var语句的一部分
//第二个逗号是逗号运算符
//它将两个表达式(i++和j--)放在一条(for循环中的)语句中
for (var i = 0, j = 10; i < j; i++, j--) {
  console.log(i + j);
}