第二部分 语法
**************第八章 运算符***************
【阮一峰:运算符是处理数据的基本方法,用来从现有数据得到新的数据。】
一、加法
1.加法:js中可以完成两种运算;
*算术加法;
*字符串连接;
“+”运算符在js中的算法如下:
(1)如果运算子是对象,先自动转成原始类型的值(即先执行该对象的valueOf方法,如果结果还不是原始类型的值,再执行toString方法;如果对象是Date实例,则先执行toString方法)。
(2)两个运算子都是原始类型的值以后,只要有一个运算子是字符串,则两个运算子都转为字符串,执行字符串连接运算。
(3)否则,两个运算子都转为数值,执行加法运算。
【valueOf()起什么作用?】
'1' + {foo: 'bar'} // "1[object Object]"
'1' + true // "1true"
'1' + [1] // "11"
2.这种由于参数不同,而改变自身行为的现象,叫做“重载”(overload)。由于加法运算符是运行时决定到底执行那种运算,使用的时候必须很小心。
'3' + 4 + 5 // "345"
3 + 4 + '5' // "75"
3.变通的用法:可以用来将一个值转为字符串。
x + ''
4.
[1, 2] + [3]
// "1,23"
// 等同于
String([1, 2]) + String([3])
// '1,2' + '3'
// 结果"1,23"是由于两个数组被先转成字符串,这里js隐式调用过toString和ValueOf两种方法,但是究竟是那种方法起的作用,不同情况不一定;
var arr=[1,2,3]
alert(arr)
alert(arr)
alert(String(arr))
弹出都是//1,2,3
一个隐式调用了toString()方法转换数据类型,一个显示人为调用String方法,为什么打印到页面上不是[1,2,3],个人猜测是不同的子类在继承String方法时做了重写(规定了独属于自己的String方法),这些都是js编程语言内部事先约定好的规则,与编译器无关,与浏览器无关。
5.加法运算符一定有左右两个运算子,如果只有右边一个运算子,就是另一个运算符,叫做“数值运算符, 数值运算符用于返回右边运算子的数值形式
+ - 3 // 等同于 +(-3) //输出-3
+ 1 + 2 // 等同于 +(1 + 2)//输出+3
+ '1' // 1 无疑,这里又隐式的做了数据类型转换
+ [1,2,3] //NaN
+ [1] // 1
6.如果只有左边一个运算子,会报错。
1 +//报错
7.加法运算符以外的其他算术运算符(比如减法、除法和乘法),都不会发生重载。它们的规则是:所有运算子一律转为数值,再进行相应的数学运算。个人猜测内部调用的Number方法
实例:
var now = new Date();
typeof (now + 1) // "string"
typeof (now - 1) // "number"
二.算数运算符
JavaScript提供9个算术运算符:
加法运算符(Addition):x + y
减法运算符(Subtraction): x - y
乘法运算符(Multiplication): x * y
除法运算符(Division):x / y
余数运算符(Remainder):x % y
自增运算符(Increment):++x 或者 x++
自减运算符(Decrement):--x 或者 x--
数值运算符(Convert to number): +x (下面俩可用来转数据类型)
负数值运算符(Negate):-x
1.余数运算符 Js中%运算符代表的是求余运算,不是python中的取模,差别只在两个运算子符号不同的时候能体现出来,【整数商*除数一定比被除数更靠近数轴的原点0!】
前一个运算子被后一个运算子除所得的余数
1.1运算结果正负号由第一个运算子决定
-1 % 2 // -1 整商0
1 % -2 // 1
整商0
因此,为得到正确的负数的余数值,需要先使用绝对值函数
1.2 余数运算符还可以用于浮点数的运算。但是,由于浮点数不是精确的值,无法得到完全准确的结果。
6.5 % 2.1// 0.19999999999999973
【问题焦点在于你是否搞懂了取余和取模】
1.2自增自减
一元运算符,只需要一个运算子。它们的作用是将运算子首先转为数值,然后加上1或者减去1。它们会修改原始变量。
var x = 1;
++x // 2
x // 2
--x // 1
x // 1
【a++与++a的区别】
++ -- 放在在变量之后,会先返回变量操作前的值,再进行自增/自减操作,在变量下次调用的时候就是新值;放在变量之前,会先进行自增/自减操作,再返回变量操作后的值。
2.数值运算符、负数值运算符
数值运算符+是一元运算符,需要一个操作数
数值运算符的作用在于可以将任何值转为数值(与
Number
函数的作用相同)。内部应该是调用了一次Number函数+true //1
+[] // 0
+{} // NaN
非数值类型的值经过数值运算符以后,都变成了数值类型(最后一行
NaN
也是数值)负数值运算符(
-
),也同样具有将一个值转为数值的功能,只不过得到的值正负相反。连用两个负数值运算符,等同于数值运算符。var x = 1;
-x // -1
-(-x) // 1
数值运算符号和负数值运算符,都会返回一个新的值,而不会改变原始变量的值。
3.赋值运算符 用于给变量赋值
最常见的是等号= 表达式
x = y
表示将y
的值赋给x
。JavaScript还提供其他11个复合的赋值运算符。
x += y // 等同于 x = x + y
x -= y // 等同于 x = x - y
x *= y // 等同于 x = x * y
x /= y // 等同于 x = x / y
x %= y // 等同于 x = x % y
x >>= y // 等同于 x = x >> y
x <<= y // 等同于 x = x << y
x >>>= y // 等同于 x = x >>> y
x &= y // 等同于 x = x & y
x |= y // 等同于 x = x | y
x ^= y // 等同于 x = x ^ y
都是先进行指定运算,然后将得到值返回给左边的变量
=的优先级最低
4.比较运算符
比较运算符用于比较两个值,然后返回一个布尔值,表示是否满足比较条件。
2 > 1 // true
js提供了8个比较运算符
== 相等
=== 严格相等
!= 不相等
!== 严格不相等
< 小于
<= 小于或等于
> 大于
>= 大于或等于
4.1比较运算符的算法
比较运算符可以比较各种类型的值,不仅仅是数值。
除了相等运算符号和精确相等运算符,其他比较运算符的算法如下。
- 如果两个运算子都是字符串,则按照字典顺序比较(实际上是比较Unicode码点)。
- 否则,将两个原始类型的运算子之间的比较 都转成数值,再进行比较(等同于先调用
Number
函数)。 - 运算子是符合类型、对象,必须先将其转为原始类型的值, 即先调用
valueOf
方法,如果返回的还是对象,再接着调用toString
方法,valueOf和toString方法都使用过以后还不是原始类型会报错
之后会根据另外一个运算子决定是否该再调用number再转数字
5 > '4' // true
// 等同于 5 > Number('4')
// 即 5 > 4
true > false // true
// 等同于 Number(true) > Number(false)
// 即 1 > 0
2 > true // true
// 等同于 2 > Number(true)
// 即 2 > 1
alert(Number({x: 2}) >= Number({x: 1})) //false 两个NaN比较
alert({x: 2} >= {x: 1}) //true
两个对象都会被转成相同的字符串"[object Object]"
var x = [2];
x > '11' // true
// 等同于 [2].valueOf().toString() > '11'
// 即 '2' > '11'
var x = [2];
x > 11//false
// 等同于Number( [2].valueOf().toString() )> 11
//即2>11
x.valueOf = function () { return '1' };
x > '11' // false
// 等同于 [2].valueOf() > '11'
// 即 '1' > '11'
两个对象之间的比较
[2] > [1] // true
// 等同于 [2].valueOf().toString() > [1].valueOf().toString()
// 即 '2' > '1'
[2] > [11] // true
// 等同于 [2].valueOf().toString() > [11].valueOf().toString()
// 即 '2' > '11'
{x: 2} >= {x: 1} // true
// 等同于 {x: 2}.valueOf().toString() >= {x: 1}.valueOf().toString()
// 即 '[object Object]' >= '[object Object]'
4.2字符串的比较
字符串按照字典顺序进行比较。
比较的是每一个下标位置的字符的Unicode码点
JavaScript 引擎内部首先比较首字符的 Unicode 码点,如果相等,再比较第二个字符的 Unicode 码点,以此类推。
'cat' > 'Cat' // true'
小写的
c
的 Unicode 码点(99
)大于大写的C
的 Unicode 码点(67
),所以返回true
。由于所有字符都有 Unicode 码点,因此汉字也可以比较。
严格相等运算符(
===
)比较它们是否为“同一个值”。如果两个值不是同一类型,严格相等运算符(===
)直接返回false
相等运算符(
==
):如果统一类型的值,比较值是否相等,就是看是不是同一个值,跟严格一样,否则会将它们转化成同一个类型,再用严格相等运算符进行比较。4.3严格相等运算符
(1)不同类型的值:直接返回false
(2)同一类的原始类型值:
同一类型的原始类型的值(数值、字符串、布尔值)比较时,值相同就返回
true
,值不同就返回false
。*十进制的
1
与十六进制的1
,因为类型和值都相同,返回true
。*NaN
与任何值都不相等(包括自身)。另外,正0
等于负0
。NaN === NaN // false
+0 === -0 // true
(3)同一类的复合类型值:
两个复合类型(对象、数组、函数)的数据比较时,不是比较它们的值是否相等,而是比较它们是否指向同一个对象,(比较的是是否指向同一个内存地址)
{} === {} // false
[] === [] // false
(function (){} === function (){}) // false
原因是对于复合类型的值,严格相等运算比较的是,它们是否引用同一个内存地址,而运算符两边的空对象、空数组、空函数的值,都存放在不同的内存地址,结果当然是
false
。如果两个变量引用同一个对象,则它们相等。
var v1 = {};
var v2 = v1;
v1 === v2 // true
new Date() > new Date() // false
new Date() >= new Date() //true
new Date() < new Date() // false
new Date() === new Date() // false
上面的三个表达式,前两个比较的是值,最后一个比较的是地址,所以都返回
false
。alert(new Date()-new Date())//结果是0 中间经历了toString有没有ValueOf不确定,最后走一步Number
typeof((new Date()).valueOf()) //"number"
(4)undefined 和 null
undefined
和null
与自身严格相等。变量声明后默认值是
undefined
,因此两个只声明未赋值的变量是相等的。var v1;
var v2;
v1 === v2 // true
(5)严格不相等运算符
严格相等运算符有一个对应的“严格不相等运算符”(
!==
),两者的运算结果正好相反。4.4相等运算符
相等运算符比较相同类型的数据时,与严格相等运算符完全一样,看是不是一个值,
所以比较,同一类的复合类型值,依然比的是内存地址
比较不同类型的数据时,
(1)原始类型的值
原始类型的数据会转换成数值类型再进行比较。
'true' == true // false
// 等同于 Number('true') === Number(true)
// 等同于 NaN === 1
'' == 0 // true
// 等同于 Number('') === 0
// 等同于 0 === 0
'' == false // true
// 等同于 Number('') === Number(false)
// 等同于 0 === 0
'1' == true // true
// 等同于 Number('1') === Number(true)
// 等同于 1 === 1
'
123 ' == 123 // true
// 因为字符串转为数字时,省略前置和后置的空格
原始都转数,对象转原始再转数
(2)对象与原始类型值比较
对象(这里指的符合数据类型,包括数组和函数)与原始类型的值比较时,对象转化成原始类型的值(先valueOf再toString),再走上面一步
document.write(String([1]))这是显示调用String
document.write([1])js内部隐式调用toString,隐式转换内部转换规则都是通过调用了ValueOf 和toString来实现的
[1] == 1 // true
[1] == '1' // true
[1] == true // true
数组
[1]
分别与数值、字符串和布尔值进行比较,会先转成字符串或数值,再进行比较。比如,与数值1
比较时,数组[1]
会被自动转换成数值1
,因此得到true
。(3)undefined和null
undefined
和null
与原始类型的值比较时,结果都为false
,它们互相比较时结果为true
。false == null // falsefalse == undefined // false
0 == null // false0 == undefined // false
undefined == null // true
绝大多数情况下,对象与
undefined
与null
比较,都返回false
。只有在对象转为原始值得到undefined
时,才会返回true
,这种情况是非常罕见的。(4)由于相等运算符不限制两个操作数的数据类型变化,各种坑,各种 隐藏的类型转换
'' == '0' // false 直接比
0 == '' // true 转数值比
0 == '0' // true 转数值比
2 == true // false
2 == false // false
false == 'false' // false
false == '0' // true
false == undefined // false
false == null // false
null == undefined // true
'
' == 0 // true
(5)不相等运算符
相等运算符有一个对应的“不相等运算符”(
!=
),两者的运算结果正好相反。5.布尔运算符
用于将表达式转为布尔值。
- 取反运算符:
!
- 且运算符:
&&
- 或运算符:
||
- 三元运算符:
?:
5.1!取反运算符
用于将布尔值变为相反值,即
true
变成false
,false
变成true
对于非布尔值的数据,取反运算符会自动将其转为布尔值,然后取反。我猜内部用Boolean方法,规则是,以下六个值取反后为
true
,其他值取反后都为false
。这意味着,取反运算符有转换数据类型的作用。
!undefined // true
!null // true
!0 // true (包括+0和-0)
!NaN // true
!"" // true
!false//true
!54 // false
!'hello' // false
![] // false
!{} // false
5.1.1不管什么类型的值,经过取反运算后,都变成了布尔值。
5.1.2如果对一个值连续做两次取反运算,等于将其转为对应的布尔值,与
Boolean
函数的作用相同。这是一种常用的类型转换的写法。两次取反就是将一个值转为布尔值的简便写法。
!!x
// 等同于Boolean(x)
5.2且运算符&&
且运算符的运算规则是:如果第一个运算子的布尔值为
true
,则返回第二个运算子的值(注意是值,不是布尔值,如果是表达式则计算出结果);如果第一个运算子的布尔值为false
,则直接返回第一个运算子的值,且不再对第二个运算子求值。't' && '' // ""
't' && 'f' // "f"
't' && (1 + 2) // 3
'' && 'f' // ""
'' && '' // ""
var x = 1;
(1 - 1) && ( x += 1) // 0
x // 1
由于且运算符的第一个运算子的布尔值为
false,第一个运算子可能不是布尔值,这里会有一个转布尔值的隐式转换。
这种跳过第二个运算子的机制,被称为“短路”。有些程序员喜欢用它取代
if
结构,比如下面是一段if
结构的代码,就可以用且运算符改写。if (i) {
doSomething();
}
// 等价于
i && doSomething();
5.3或运算符 ||
且是false短路,或是true短路。
或运算符(
||
)的运算规则是:如果第一个运算子的布尔值为true
,则返回第一个运算子的值,且不再对第二个运算子求值;如果第一个运算子的布尔值为false
,还会去算一下第二个运算子的值,不管它false还是true,返回第二个运算子的值。短路规则对这个运算符也适用。
false || 0 || '' || 4 || 'foo' || true
// 4
运算符常用于为一个变量设置默认值。下面代码不严谨
function saveText(text) {
text = text || '';
// ...
}
// 或者写成
saveText(this.text || '')
上面代码表示,如果函数调用时,没有提供参数,则该参数默认设置为空字符串。默认往里传空字符串
5.4三元条件运算符
三元条件运算符用问号(?)和冒号(:),分隔三个表达式。如果第一个表达式的布尔值为
true
,则返回第二个表达式的值,否则返回第三个表达式的值。第一个值不是布尔类型会转换成布尔三元条件表达式与
if...else
语句具有同样表达效果,前者可以表达的,后者也能表达。但是两者具有一个重大差别,if...else
是语句,没有返回值;三元条件表达式是表达式,具有返回值。所以,在需要返回值的场合,只能使用三元条件表达式,而不能使用if..else
。console.log(true ? 'T' : 'F');
console.log
方法的参数必须是一个表达式,这时就只能使用三元条件表达式。如果要用if...else
语句,就必须改变整个代码写法了。6.位运算符 暂时不看
7.其他运算符
7.1void运算符
void
运算符的作用是执行一个表达式,然后不返回任何值,或者说返回undefined
。void 0 // undefined
void(0) // undefined
7.1.1void
运算符的两种写法,都正确。建议采用后一种形式,即总是使用括号。因为void
运算符的优先性很高,如果不使用括号,容易造成错误的结果。比如,void 4 + 7
实际上等同于(void 4) + 7
。var x = 3;void (x = 5) //undefined
x // 5
7.1.2这个运算符主要是用于书签工具(bookmarklet),以及用于在超级链接中插入代码,目的是返回
undefined
可以防止网页跳转。<a href="javascript:void window.open('http://example.com/')">
点击打开新窗口
</a>
上面代码用于在网页中创建一个链接,点击后会打开一个新窗口。如果没有
void
,点击后就会在当前窗口打开链接。7.1.3下面是常见的网页中触发鼠标点击事件的写法。
<a href="http://example.com" onclick="f();">文字</a>
上面代码有一个问题,函数
f
必须返回false
,或者说onclick
事件必须返回false
,内部机制如何未知,否则会引起浏览器跳转到example.com
。function f() {
// some code
return false;
}
<a href="http://example.com" onclick="f()(给click事件绑定一个函数执行某个功能,但是不让页面跳转);return false;">文字</a>
7.1.4等价写法:用void运算符<a href="javascript: void(f())">文字</a>
下面的代码会提交表单,但是不会产生页面跳转。
<a href="javascript: void(document.form.submit())">
文字</a>
7.2逗号运算符:
逗号运算符用于对两个表达式求值,并返回后一个表达式的值。
'a', 'b' // "b"
var x = 0;
var y = (x++, 10);
x // 1
y // 10
逗号运算符返回后一个表达式的值。
很难找到实际应用的场景,而且下面代码测试也不正确
alert(1,2)//1
alert((1,2))//2 【原因不详】
8,运算顺序
8.1优先级:
var x = 1;
var arr = [];
var y = arr.length <= 0 || arr[0] === undefined ? x : arr[0];
根据语言规格,这五个运算符的优先级从高到低依次为:小于等于(
<=
)、严格相等(===
)、或(||
)、三元(?:
)、等号(=
)。因此上面的表达式,实际的运算顺序如下。var y = ((arr.length <= 0) || (arr[0] === undefined)) ? x : arr[0];
只要记忆常见优先级就可以
8.2圆括号()
优先级最高,可以提高运算的优先级
括号中的表达式第一个运算
由于运算符的优先级别十分繁杂,且都是来自硬性规定,因此建议总是使用圆括号,保证运算顺序清晰可读,这对代码的维护和除错至关重要。
顺便说一下,圆括号不是运算符,不具有求值作用,而是一种语法结构。它一共有两种用法:一种是把表达式放在圆括号之中,提升运算的优先级;另一种是跟在函数的后面,作用是调用函数。
var x = 1;
(x) = 2;
如果圆括号具有求值作用,那么就会变成1 = 2
,这是会报错了。但是,上面的代码可以运行,这验证了圆括号只改变优先级,不会求值。
这也意味着,如果整个表达式都放在圆括号之中,那么不会有任何效果。
(exprssion)// 等同于expression
圆括号之中,只能放置表达式,如果将语句放在圆括号之中,就会报错。
(var a = 1)// SyntaxError: Unexpected token var
8.3左结合与右结合
对于优先级别相同的运算符,大多数情况,计算顺序总是从左到右,这叫做运算符的“左结合,从左边开始计算
少数运算符的计算顺序是从右到左,即从右边开始计算,这叫做运算符的“右结合”(right-to-left associativity)。其中,最主要的是赋值运算符(
=
)和三元条件运算符(?:
)w = x = y = z;
q = a ? b : c ? d : e ? f : g;
都是先计算最右边的那个运算符
w = (x = (y = z));
q = a ? b : (c ? d : (e ? f : g));