JavaScript 从入门到放弃 - 3 - 表达式和运算符
表达式和运算符
- 程序中最简单的表达式就是,程序中的常量
- 变量名也是一种简单的表达式
- 复杂的表达式是由简单的表达式组成的
- 函数调用表达式是由函数对象的表达式和0个或多个参数表达式构成
- 可以使用运算符来将简单的表达式来组合成复杂的表达式
原始表达式
“原始表达式”,说简单点就是最简单的表达式,并且不再包含其他表达式
js中原始表达式有以下:
- 常量
- 直接量
- 关键字
- 变量
栗子:
//直接量
1.23 // 数字直接量
"hello" // 字符串直接量
/pattern/ // 正则表达式直接
//保留字
true
false
null // 返回空
this // 返回“当前对象”
//变量
i // 返回i的值
sum // 返回sum的值
undefined // undefined是全局变量,和null不同,不是一个关键字
对象和数组的初始化表达式
对象和数组的初始化表达式实际上是一个新创建的对象和数组,并不是原始表达式
数组初始化表达式
栗子:
[] //空数组
[1+2,3+4] // 2个元素数组 [3,7]
var matrix = [[1,2],[3,4]] // 数组可嵌套
js对数组初始化表达式进行求值时候,数组表达式中的所有元素表达式也会各自计算一次
数组直接量的元素表达式在逗号之间元素可以省略,空位默认填充undefined。
var array = [1,,3] // 数组包含3个元素。[1,undefined,3]
但是列表结尾处可以留下单个逗号,这时不会创建新的undefined元素
对象初始化表达式
对象初始化表达式与数组初始化表达式非常接近,将[]替换成{},每个子表达式都包含一个属性名和冒号为前缀
栗子
var p = { x:2, y:3 }; // 带2个属性的对象p
var q = {}; // 空对象
q.x = 2; q.y =3; // q的属性成员与p一致
对象直接量也是允许嵌套的
var rect = {
upLeft: { x:2, y:3},
bottomRight: { x:4, y:1}
};
js求对象初始化表达式时候,对象元素表达式也会各自都计算一次,并且元素表达式不必包含常数值,可以是
任意的js表达式。
对象直接量中属性的名字可以是字符串而不是标示符
>```
var side = 1;
var square = {
"upLeft": { x: p.x, y: p.y},
"bottomRight": {x: p.x+side, y: p.y+side}
};
函数表达式
函数定义表达式定义一个js函数,表达式的值是这个新定义的函数。如下
var square = function (x){
return x * x;
};
属性访问表达式
属性访问表达式得到一个对象属性或一个数组元素的值。主要有.
和[]
两种形式
栗子:
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,表达式会抛出类型错误异常。
如果运算结果不是对象或者数组,js会将其转换为对象。
如果对象表达式后面跟随句点和标示符,则会查找由这个标示符所指定的属性的值,然后作为整个表达式的值返回。
如果表达式后面跟随一对括号,则会计算方括号里面表达式值并转换为字符串,然后查找对应属性的值
如果以上两种情况,命名属性并不存在,则整个属性访问表达式的值就是undefined
-
.
写法适合要访问的属性名是合法标示符,并且需要知道要访问属性名字 -
[]
写法适合要访问的属性名不是合法字符,或者访问的属性名是需要运算得出的。对于数组则必须使用这种写法
调用表达式
js调用表达式,是一种调用(执行)函数或方法的语法。如下
func(0) // f是一个函数表达式,0是一个参数表达式
Math.max(x,y,z) // Math.max是一个函数,x,y,z是参数
a.sort() // a.sort是一个函数。没有参数
对调用表达式进行求值时候,首先计算函数表达式,然后计算参数表达式,得到一组参数值。
如果函数表达式不是一个可以调用的对象,会抛出类型错误异常
如果函数表达式使用return语句返回一个值,那么这个值就是整个调用表达式的值,否则表达式的值就是undefined
对象创建表达式
对象创建表达式,顾名思义,就是创建一个对象,并且调用一个函数,初始化新对象的属性。
new Object()
new Point(2,6)
如果一个对象创建表达式不需要传入任何参数给构造函数,那么空括号可以省略
new Object
new Date
运算符概述
大多数运算符都是由标点符号表示的,如"+","="。而另外一些运算符则是由关键字表示的,比如delete和instanceof。
优先级从高到低,虚线分割开的运算符不同优先级。
运算符 | 操作 | 结合性 | 操作数个数 | 类型 |
---|---|---|---|---|
++ | 前/后增量 | R | 1 | lval => num |
-- | 前/后减量 | R | 1 | lval => num |
- | 求反 | R | 1 | num => num |
+ | 转换为数字 | R | 1 | num => num |
~ | 按位求反 | R | 1 | int => int |
! | 逻辑非 | R | 1 | bool => bool |
delete | 删除属性 | R | 1 | lval => bool |
typeof | 检测操作数类型 | R | 1 | any => str |
void | 返回undefined | R | 1 | any => undef |
--------------- | --------------- | --- | -- | --------- |
*,/,% | 乘、除、取余 | L | 2 | num,num => num |
--------------- | --------------- | --- | -- | --------- |
+、- | 加、减 | L | 2 | num,num => num |
+ | 字符串连接 | L | 2 | str,str => str |
--------------- | --------------- | --- | -- | --------- |
<< | 左移位 | L | 2 | int,int => int |
>> | 无符号右移位 | L | 2 | int,int => int |
>>> | 有符号右移位 | L | 2 | int,int => int |
--------------- | --------------- | --- | -- | --------- |
<,<=,>,>= | 比较数字顺序 | L | 2 | num,num => bool |
<,<=,>,>= | 比较在字母表顺序 | L | 2 | str,str => bool |
instanceof | 测试对象类 | L | 2 | obj,func => bool |
in | 测试属性是否存在 | L | 2 | str,obj => bool |
-------------------- | --------------- | --- | -- | --------- |
== | 判断相等 | L | 2 | any,any =>bool |
!= | 判断不等 | L | 2 | any,any => bool |
=== | 判断恒等 | L | 2 | any,any => bool |
!== | 判断非恒等 | L | 2 | any,any => bool |
--------------- | --------------- | --- | -- | --------- |
& | 按位与 | L | 2 | int,int => int |
--------------- | --------------- | --- | -- | --------- |
^ | 按位异或 | L | 2 | int,int => int |
--------------- | --------------- | --- | -- | --------- |
| | 按位或 | L | 2 | int,int => int |
--------------- | --------------- | --- | -- | --------- |
&& | 逻辑与 | L | 2 | any,any => any |
--------------- | --------------- | --- | -- | --------- |
|| | 逻辑或 | L | 2 | any,any => any |
--------------- | --------------- | --- | -- | --------- |
?: | 条件运算符 | L | 3 | bool,any,any => any |
--------------- | --------------- | --- | -- | --------- |
= | 赋值运算符 | R | 2 | lval,any => any |
*=,/=,%=,+=,-=,&= | 运算且赋值 | R | 2 | lval,any => any |
^=,||=,<<=,>>=,>>>= | 运算且赋值 | R | 2 | lval,any => any |
--------------- | --------------- | --- | -- | --------- |
, | 忽略第一个操作数,返回第二个操作数 | L | 2 | any,any => any |
左值
上表中出现的lval指的是左值,意思是表达式只能出现在赋值运算符的左侧
在js中,变量、对象属性、和数组元素都是左值。
ECMAScript允许内置函数返回左值,但自定义函数不能返回左值
操作数类型和结果类型
js运算符通常会根据需要对操作数进行类型转换
*
希望操作数为数字,但是表达式"3"*"5"
却是合法的,因为js会把操作数转换为数字
有些操作符对操作数类型有一定程度依赖,比如+
运算符。可以对数字进行加法运算,也可以对字符串进行连接。
运算符优先级
上表中运算符按照优先级从高到低排序,每个虚线内的一组运算符具有相同优先级。
优先级高的运算符执行总是先于优先级低的运算符
举个栗子:m = x + y*z;
*
运算符比+
运算符优先级高,优先计算y*z
,获得结果再与x相加。=
赋值运算符优先级最低,右侧表达式
计算出结果后赋值给m
很多时候为了代码逻辑清晰,加上一些括号来重写优先级,来避免一些优先级引起的bug或者执行顺序与设计不符
m = (x + y) * z
运算符的结合性
上表中说明了运算符的结合性。
- L 指从左到右结合,执行时按照从左到右的顺序进行
- R 指从右到左结合,执行时按照从右到左的顺序进行
举个栗子:-
运算符从左到右结合,因此w = x - y - z
等价于 w = ((x - y) - z)
运算顺序
运算符的优先级和结合性规定了在复杂表达式中的运算顺序,但是没有规定子表达式的计算过程中的运算顺序
js中,总是严格按照从左到右计算子表达式。例如w=x+y*z
,首先计算w,然后计算x,y,z的值,然后y的值和z的值相承
之后,再加上x的值,最后将其结果赋值给w。给表达式加括号会改变乘法加法和赋值运算的顺序,但是子表达式的计算
顺序仍然是从左至右的顺序
只有一种情况例外,当任何一个表达式具有副作用而影响其他表达式时候,求值顺序才会有所不同。
例如,表达式中x的一个变量自增1,这个变量在z中使用,那么实际上是先计算了x的值再计算z的值,这一点一定要注意
下面这个栗子:
a = 1;
b = (a++) + a;
如果按照前面那种不考虑副作用时的顺序是 1) 计算b, 2)计算a++为c, 3)计算a,4)计算c+a, 5)将c+a结果赋值给b
按照++
的影响,1) 计算b, 2)a++结果仍然为1,c=1,随即a立即自增1, 3)计算a,a已经是2,4)计算c+a=3,5)将c+a结果赋值给b,所以b=3
切记,a增1的操作是在表达式计算中就已经执行了,不是在整个表达式计算完成之后执行的
算术表达式
基本算数运算符包括+
-
*
/
%
"+"运算符
- 对2个数字进行加法操作
- 字符串连接操作
针对不同操作数,+运算符行为表现有些不同
- 一个操作数是对象,对象会遵循对象到原始值的转换规则转换为原始值
- 日期对象:toString()执行转换
- 其他对象通过valueOf()转换,如果valueOf()不可用,会通过toString()方法转换
- 在进行对象到原始值的转换后,如果其中一个操作数是字符串,另一个操作数也会转换成字符串,然后连接
- 否则,两个操作数都转换成数字或者NaN,然后进行加法操作
下面是一些栗子:
1 + 2 // =>3
"1" + 2 // => "12"
"12" + "3" // => "123"
1 + {} // "1[object object]",对象转换为字符串
true + true // 2 ,bool转换为数字做加法
2 + null // =>2,null转换为0
2 + undefined // => NaN, undefined转换为NaN后做加法
最后,还需要考虑,加法的结合性对运算顺序的影响
1 + 2 + "hello" // "3hello"
1 + (2 + "hello") // "12hello"
一元算术运算符
一元运算符作用于一个单独操作数,产生一个新值
js中一元运算符优先级很高,并且都是右结合
+
/-
,既是一元运算符,也是二元运算符
- 一元加法(
+
)
操作数转换为数字(或者NaN),并且返回这个转换后的数字。如果已经是数字,直接返回
- 一元减法(
-
)
操作数转换为数字(或者NaN),并且返回这个转换后的数字,然后改变运算结果符号
- 递增(
++
)
前增量
++a
,先进行增量运算并且返回运算结果
后增量a++
,先进行增量计算,返回未做增量运算的值
var i=1, j=i++; // i=2,j=1
var i=1, j=++i; // i=2,j=2
- 递减(`--`)
> 前减量 `--a`,先进行减量运算并且返回运算结果
> 后减量 `a--`,先进行减量计算,返回未做减量运算的值
> ```
var i=1, j=i--; // i=0,j=1
var i=1, j=--i; // i=0,j=0
位运算符
-
&
按位与
0x1234 & 0x00ff = 0x0034
-
|
按位或
0x1234 | 0x00ff = 0x12ff
-
^
按位异或
0xff00 ^ 0xf0f0 = 0x0ff0
-
~
按位非
~0x0f = 0xfffffff0
-
<<
左移
7 << 2 = 28
,左移一位相当于第一个操作数乘以2
移动位数 0~31
-
>>
带符号右移
带符号右移时候填补在左边的位由原来的数的符号决定,以便保持和原操作数一致
移动位数 0~31
7 >> 1 = 3
-7 >> 1 = -4
-
>>>
无符号右移
无符号右移时候填补在左边的位直接填补0,与原操作数无关
移动位数 0~31
-1 >> 4 = 1
-1 >>> 4 = 0x0fffffff
关系表达式
主要包括相等和不相等运算符、比较运算符、in、instanceof
相等和不相等运算符
js定义了4个符号==
,===
,!=
,!==
-
==
:相等 -
===
: 恒等 -
!=
: 不相等 -
!==
: 不恒等
严格相等运算符===
首先计算其操作数的值,然后比较这两个值,没有类型转换
- 如果两个值类型不相同,则它们不相等
- 如果两个值都是null或者都是undefined,则它们不相等
- 如果两个值都是布尔值true或者false,,则它们相等
- 如果其中一个值是NaN,或者2个值都是NaN,则它们不相等。NaN和其他任何值都是不相等的,包括自身。通过X!==X来判断x是否为NaN,只有x为NaN时候,表达式才为true
- 如果两个值为数字且数值相等,则他们相等。如果一个值为0,另一个为-0,同样相等
- 如果两个值为字符串,且所含对应位上16位数完全相等,则他们相当。如果它们长度或内容不同,则它们不相等。
- 两个字符串可能含义完全一样且显示字符一样,但具有不同编码的16位值。js不会对Unicode进行标准化转换,像这样字符串通过"==="和"=="运算符比较结果也是不相等
- 如果两个引用值指向同一个对象、数组或者函数,则相等。如果指向不同对象,则它们不相等,尽管两个对象完全一样的属性。
相等运算符==
和恒等运算符相似,但相等运算符并不严格。如果两个操作数不是同一类型,那么相等的运算符会进行一些类型转换,然后进行比较
- 如果两个操作数的类型相同,则和上文所属的严格相等的比较规则一样。如果严格相等,则比较结果相等,如果不严格相等,则它们不相等
- 如果两个操作数类型不同,
==
相等操作符也可能认为他们相等。检测相等会遵守以下规则和类型转换
- 如果一个值是null,另一个是undefined,则他们相等
- 如果一个值是数字,另一个是字符串,先将字符串转换成数字,然后使用转换后的值,进行比较
- 如果其中一个值是true,则将其转换为1再进行比较。如果其中一个值是false,则将其转化为0,在进行比较
- 如果一个值是对象,另一个值是数字或字符串,则会使用之前提到的对象到数字或字符串的转换规则将对象转换为原始值,然后进行比较。
- 其他的类型之间的比较均不相等
举个栗子:"1" == true
这个表达式结果是true,表明不同类型之间的值比较结果相等。布尔值首先转换为数字1,然后字符串1也转换成数字1,因为两个数字相等,所以结果为true
比较运算符
比较运算符有4个
-
<
小于 -
>
大于 -
<=
小于等于 -
>=
大于等于
比较运算符的操作数可以是任何类型,然后只有字符串和数字才能真正的执行比较操作,不是这两种类型的都将进行类型转换。
类型转换规则:
- 如果操作数为对象,这个对象将按照对象到原始值的转换(具体可以看上篇)
- 在对象转换到原始值后,如果两个操作数都是字符串,那么将按照字母表顺序进行比较(字母表指的unicode 16位字符的索引顺序)
- 对象转换为原始值后,如果一个操作数不是字符串,那么两个操作数转换为数字之后进行比较。
- 0和-0是相等的
- Infinity比其他任何数字都大(除了自身)
- -Infinity比其他数字都小(除了自身)
- 如果一个操作数转换成数字之后是NaN,那么比较操作符总是返回false
在上面规则中,字符串比较需要注意:
- 字符串比较是区分大小写的,所有的大写ascii字符都是小于小写的ascii字符
- 对于数字和字符串比较,只有两个操作数都是字符串时,才会进行字符串比较
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 = [1,2,3]
"0" in data // true,数组包含索引0的元素
1 in data // true,数组包含索引1的元素
3 in data // false 数组不包含索引3的元素
instanceof
运算符
instanceof
运算符希望左操作数是一个对象,右操作数标识对象的类,如果左侧的对象是右侧类的实例,则返回true,否则返回false
var d = new Date();
d instanceof Date; // true,d是由Date()创建的
d instanceof Object; // true,所有对象都是Object实例
d instanceof Number; // false, d不是Number的实例
逻辑表达式
逻辑运算符是进行布尔运算使用的,主要有
-
&&
逻辑与 -
||
逻辑或 -
!
逻辑非
逻辑与(&&)
当操作数都是布尔值时,&&
对两个值进行布尔与操作,第一个与第二个操作数都是true时,才返回true,其中一个是false,就会返回false
当操作数不都是布尔值时,&&
不一定会返回布尔值。
- 逻辑与运算符,首先计算左侧的值,如果计算结果是假植,则整个表达式都是假植,因此会简单的返回左侧操作数的值
- 如果左侧值是真值,那么整个表达式结果依赖于右侧的值,因此,
&&
运算符符会计算右侧操作数的值,并且将其返回作为整个表达式的计算结果
var o ={ x:1 };
var p = null;
o && o.x; // =>1 ,o对象是真值,返回o.x
p && p.x; // => null ,p是假值,将其返回,不会计算p.x
上面那种行为,被称为 短路,这一特性非常有用,可以选择性执行代码。 例如:
if ( a == b ) stop();
等价于 ( a == b ) && stop();
逻辑或(||)
当操作数都是布尔值时,||
对两个操作数作布尔或运算,两个操作数有一个为真,返回true,两个操作数都是假,才会返回false
当操作数不都是布尔值,||
不一定返回布尔值
- 逻辑或运算符,首先计算左侧的值,如果计算结果是真值,则整个表达式都是真值,因此会返回这个真值
- 否则再计算第二个操作数的值,再返回这个表达式的计算结果
||
同样是非常有用,比如从一组备选表达式中选出第一个真值表达式。这种做法经常在函数体内,给参数提供默认值
var max = max_width || preference.max_width || 500;
逻辑非(!)
!
运算符是一元运算符,放在一个单独的操作数前,对操作数布尔值进行求反
!
运算符首先将操作数转换为布尔值,再进行求反,最终只会返回true或者false
作为一元运算符,优先级非常高,并且和操作数密切绑定。
德摩根公式:
!(p && q) === !p || !q
!(p || q) === !p && !q
赋值表达式
js 使用=
运算符给变量或者属性进行赋值
i = 0;
0.x = 1;
除了常规的赋值运算符,还有一些带赋值操作的运算符+=
,-=
,*=
,&=
等等
只有+=
可以用于数字或字符串连接,其他都偏向于数值操作
a = 10;
b = '1';
a += 10; // => 10 + 10 =20
b += 10; // => '1'+10 = "110"
a -= 10; // => 20 - 10 = 10
b -= 10; // => 110 - 10 = 10
表达式计算
js可以通过eval()
来动态判断源代码中的字符串,并且执行
eval()
只有一个参数,如果传入的参数不是字符串,直接返回这个参数。如果参数是字符串,则会把字符串当成代码进行编译。
如果编译失败,则返回一个语法错误异常。如果编译成功,则会执行这段代码,并且返回字符串最后一个表达式或者语句的值。
如果最后一个表达式或语句没有值,则最终返回undefined
eval()
使用来调用它的变量作用域环境,也就是说查找变量会和局部作用域代码完全一样。
如果将eval()
重命名为其他方式来调用,则使用全局对象作为上下文作用域,并且无法读、写、定义局部变量
var geval = eval; // geval 是调用全局eval
var x = "global";
var y = "global";
function f(){
var x = "local"; // 定义局部变量 x,局部作用域x = “local”
eval("x+='changed';"); // 直接eval更改局部变量x的值
return x; // 返回更改后的x值
}
function g(){
var y = "local"; // 定义局部变量 y,局部作用域y = “local”
geval("y+='changed';"); // 间接调用改变全局变量的值
return y; // 返回为更改的局部变量
}
console.log(f(),x); // 更改来局部变量,输出"localchanged global"
console.log(g(),y); // 更改全局变量,输出“local globalchanged”
其他运算符
条件运算符(?:)
条件运算符是唯一一个三元运算符
(---1---) ? (---2---) : (---3---)
第一个操作数当成布尔值,如果是真值,那么计算第二个操作数,并返回结果。否则如果第一个操作数是假植,那么计算第三个数,返回计算结果
x > 0 ? x : -x // 求x的绝对值
typeof运算符
x | typeof x |
---|---|
undefined | "undefined" |
null | "object" |
true or false | "boolean" |
任意数字或NaN | "number" |
任意字符串 | "string" |
任意函数 | "function" |
任意内置对象 | "object" |
任意宿主对象 | 由编译器实现的字符串,但不是以上出现的字符串 |
这是一个一元运算符,返回表示操作数类型的一个字符串
x | typeof x |
---|---|
undefined | "undefined" |
null | "object" |
true or false | "boolean" |
任意数字或NaN | "number" |
任意字符串 | "string" |
任意函数 | "function" |
任意内置对象 | "object" |
任意宿主对象 | 由编译器实现的字符串,但不是以上出现的字符串 |
delete运算符
delete运算符,用来删除对象属性或者数组元素
var o = { x:1, y:2};
delete o.x; // 删除一个属性
"x" in o; // false,o对象中不存在属性x
var a = [1,23,4];
delete a[2]; //删除第三个元素
2 in a ; // false, 索引2的元素在数组中已经不存在
delete删除成功会返回true。然后不是所有属性都可删除的
- 一些内置核心和客户端属性是不能删除的
- 使用var语句生声明的变量不能删除
- 通过function定义的函数和函数参数不能删除。
var 0 = { x:1, y:2};
delete o.x; // true, 删除对象属性成功
typeof o.x; // undefined , 属性不存在
delete o.x; // true, 删除不存在的属性,返回true
delete o; // false ,不能删除var声明变量
delete 1; // true, 参数不是一个左值,返回true
this.x = 1; // 定义全局对象的一个属性
delete x; // 试图删除全局变量的属性,非严格模式下,返回true
x; // 运行时错误,没有定义x
void 运算符
void 是一元运算符,操作数照常计算,但是忽略计算结果并且返回undefined
这个操作符经常用作客户端URL-javascript:URL
,通过使用void则让浏览器不必显示这个表达式计算结果
<a href="javascript:void window.open();">打开一个新窗口</a>
逗号运算符
逗号运算符,首先计算左操作数,然后计算右操作数,最后返回右操作数的值。
总会计算左侧的表达式,但计算结果忽略掉,也就是说,只有左侧表达式有副作用时,,才会使用逗号表达式让代码更通顺。
经常在for循环中使用
for(var i=0,j=10;i<j;i++,j--){
console.log(i+j);
}