最近因为在读 Underscore.js 的源代码,加上重拾之前没有完成 ife 中的 JavaScript 部分的 task2,其中大量简略的语句写法,尤其喜欢 ?: 这个三目运算符和其他运算符连用。因为对于运算符优先级的概念一直很模糊,然后经常被绕进圈子里面。下面整理下常用的运算符和它们的优先级差异。
一、运算符
1. 一元运算符
(1) 递增递减运算符
主要就是 a++ 和 ++a 的区别,执行前置递增和递减运算时,变量的值都是在语句被求值之前改变的。
var num1 = 10; var num2 = num1++; console.log(num1 + ‘ ‘ + num2); // ‘11 10‘ var num3 = ++num1; console.log(num1 + ‘ ‘ + num3); // ‘12 12‘
(2) 一元加减运算符
这个没有什么好说的,因为是作为一元加减运算符的存在,所以主要是用来变化 ± 号的。
2. 位运算符
理解这个位运算符还是蛮复杂的,需要理解一通诸如:二进制、补码、按位取反等等的概念,这些概念这里就不多说了,读大学的时候这几个概念就够讲个几节课了。
(1) 按位非
~ :返回数值的反码,通俗的来说就是取反减1
(2) 按位与
& :将两个数值的每一位对齐,然后两个数值对应位都是 1 时才返回 1,任何一位是 0,结果都是 0。下面有个应用,还是蛮有趣的:
// DOM .contains() 方法的兼容性写法 // 执行对象是否是参数对象的父节点 function contains(refNode, otherNode){ if(typeof refNode.contains == "function" && (!client.engine.webkit || client.engine.webkit >= 522)){ return refNode.contains(otherNode); }else if(typeof refNode.compareDocumentPosition == "function"){ return !!(refNode.compareDocumentPosition(otherNode) & 16); }else{ var node = otherNode.parentNode; do{ if(node == refNode){ return true; }else{ node = node.parentNode; } }while(node != null); return false; } } /* ** 支持contains()方法的浏览器有IE、Firefox 9+、Safari、Opera和Chrome ** 支持DOM Level3 compareDocumentPosition()方法的浏览器IE9+、Firefox、Safari、Opera 9.5+和Chrome ** compareDocumentPosition()方法的返回值如下 ** 1:无关(给定的节点不在当前的文档中) ** 2:居前(给定的节点在DOM树中位于参考节点之前) ** 4:居后(给定的节点在DOM树中位于参考节点之后) ** 8:包含(给定节点是参考节点的祖先) ** 16:被包含(给定节点是参考节点的后代) */
在上面的代码 else if 分支中,这句代码 return !!(refNode.compareDocumentPosition(otherNode) & 16) 中 compareDocumentPosition() 函数如果返回值小于16,在按位与运算之后值都是 0,如果大于等于,按位与的结果就是16(代码中的返回值是 20 = 4 + 16)。然后这里有个 !! 的运算技巧,因为返回值要求是 true 和 false,所以需要两个逻辑非运算符将该数值转换成布尔值。
(3) 按位或
| :将两个数值的每一位对齐,然后两个数值对应位都是 0 时才返回 0,任何一位是 1,结果都是 1。这个理解了按位与就蛮好理解的。
(4) 按位异或
^ :将两个数值的每一位对齐,然后两个数值对应位相同才返回 0,不同结果返回 1。这个理解了按位与/按位或者就蛮好理解的。
(5) 左移 (<<) / 有符号的右移 (>>) / 无符号的右移 (>>>)
这个说真的我用的比较少,这里就不展开描述,有兴趣可以自己查查。
3. 布尔运算符
(1) 逻辑非
! :值得注意的是,逻辑非运算符首先会将它的运算数转换为一个布尔值,然后对其求反。其中最重要的应用就是上面有提到的:同时使用两个逻辑非运算符,实际上就会模拟 Boolean() 转型函数的行为。
(2) 逻辑与
&& :逻辑与运算布尔值的情况就不多赘述了,只要有 false 就返回 false,全是 true 就返回 true。这里主要需要讨论的是逻辑与运算符应用于其他类型运算数的情况:
- 如果第一个运算数是对象,则返回第二个运算数;
- 如果第二个运算数是对象,则只有在第一个运算数求值结果返回 true 的情况下才会返回该对象;
- 如果两个运算数都是对象,则返回第二个运算数;
- 如果有一个运算数是 null / NaN / undefined,则返回 null / NaN / undefined;
同时值得注意的是,逻辑与是短路运算,第一个运算数返回值是false,就不会再对第二个运算数进行求值(注意,这里是求值)。
(3) 逻辑非
|| :和逻辑与毕竟是兄弟,运算布尔值就是,只要有 true 就返回 true,全是 false 就返回 false。详细看看逻辑非运算符应用于其他类型运算数的情况:
- 如果第一个运算数是对象,则返回第一个运算数;
- 如果第一个运算数的求值结果为 false ,则返回第二个运算数;
- 如果两个运算数都是对象,则返回第一个运算数;
- 如果两个运算数都是 null / NaN / undefined,则返回 null / NaN / undefined;
和逻辑与相似的是,逻辑非也是短路运算,这里也有个有趣的应用,在之前写 jQuery 插件的时候,向初始化函数传递参数 options 时,为了避免赋值 undefined 或者 null,常常会在初始化参数的过程中添上这么句代码:
var _options = options || {};
前面的 options 包含优先赋值给 _options 的值,后面的 {} 空对象负责在前面的 options 中不包含有效值的情况下提供后备值。因为这一句代码之后往往就是用户定义参数和默认参数的拓展,所以要求不能传递 null 值。
逻辑与和逻辑非在代码编写中的应用往往非常普遍,尤其是运算非布尔值的对象的规则还是很重要的。
凭借现在自己的水平在应用编写还是用的不够熟悉,还是要不断重构自己当初写的代码,不远总结反思。
4. 乘性运算符 / 加性运算符
这里主要是 + - * / % 这五种运算符的运算特性,相对而言除了基本的数学运算,还有字符串拼接,在我使用的过程中,其他一些比较 Geek 的用法我也没怎么涉及。下面有些值得注意的地方:
- JavaScript 里面的 / 运算不像我之前接触的带有整除的运算效果,就是普通的除法运算,除不尽会带有一长串的小数;
- 因为 JavaScript 是基于 IEEE754 数值的浮点数计算,所以会出现 0.1 + 0.2 ≠ 0.3 的情况,所以尽量使用转化为整数进行运算;解释摸我
其他数值运算中会遇到的诸如 NaN、Infinity 就不在这里的运算符讨论范围之内了。
5. 关系运算符
> < >= <= :这个相比之下也会有些少见的技巧。除了我们常见的数值比较,还有一些比较拓展的知识:
- 比较两个字符串时,实际比较的是两个字符串中对应位置的每个字符的字符编码值。这里就涉及到字符集、字符编码的概念了,这里就不展开阐述。由于大写字母的字符编码全部小于小写字母的字符编码,所以就会出现下面所示的情况:
var result = "Bob" < "alin"; // true
- 还有出现 > <= 比较两个相同运算数出现相同结果。根据规则,任何运算数与 NaN 进行比较时,结果都是返回 false ,因为字母不能转化成合理的数值,这样就出现了下面所示的情况:
var result1 = ‘a‘ > 1; // false var result2 = ‘a‘ <= 1; // false
6. 相等运算符
这里主要需要区别的是(不)相等和(不)全等。相等运算符(==)会存在一个类型转换的运算,其中主要的应用是在类似 "5" == 5 这种纯数字字符串和数字的比较判断会返回 true。值得注意的是: null == undefined 返回是 true。而全等(===)要求不仅仅两个运算数本身数值相等,还要求两者的数值类型相同。所以上面的两种情况在全等的情况下都是返回 false 。在实际的代码编写中,还是推荐全等和不全等运算符。
7. 条件运算符
variable = boolean_expression ? true_value : false_value;
基于对 boolean_expression 求值的结果,决定给变量 variable 的赋值。如果求值结果为 true ,则给 variable 赋值 true_value ,否则则是 false_value。配合上其他运算符,条件(三目)运算符可以发挥出它的灵活、高效。
8. 赋值运算符
= : 把右侧的值赋给左侧的变量。类似的还有 *= 、 /= 、 %= 、 += 、-= 、 <<= 、 >>= 、 >>>= 。使用这些运算符主要就是为了简化赋值运算,不会带来任何性能的提升。
9. 逗号运算符
使用逗号运算符可以在一条语句中执行多个运算,例如常见的赋值语句:
var num1 = 1, num2 = 2, num3 = 3;
除此之外,逗号运算符还可以用于赋值。用于赋值时,逗号运算符总会返回表达式的最后一项,例如下面的例子:
var num = (1, 2, 3, 4, 5, 0); // num 的值是0
还有这种面试题:
var num = (key = 12, 0); // num 的值是0
10. typeof 操作符
这个操作符主要是用来检测给定变量的数据类型,返回值如下:
- "undefined" - 如果这个值未定义;
- "boolean" - 如果这个值是布尔值;
- "string" - 如果这个值是字符串;
- "number" - 如果这个值是数值;
- "object" - 如果这个值是对象或者null;
- "function" - 如果这个值是函数;
这里解释下为什么 typeof null 会返回 "object" ,同样也解释下为什么 null == undefined 会返回 true:null 值本身就是表示一个空对象指针,使用 typeof 会返回 "object";实际上,undefined 是派生自 null 值的,因此两者的相等性测试要返回 true。因而,没有必要把一个变量的值显示的设置为 undefined,但是只要意在保存对象的变量还没有真正的保存对象,就应该明确的让该变量保存 null 值。
二、 运算符优先级
下面的表格按照从高到低的优先级列出 JavaScript 的运算符,具有相同优先级的运算符按照从左到右的顺序求值:
运算符 | 描述 |
. [] () | 属性访问、数组下标、函数调用以及表达式分组 |
++ -- ~ ! + - delete new typeof void | 一元运算符、返回数据类型、对象创建 |
* / % | 乘法、除法、取模 |
+ - | 加法、减法 |
>> << >>> | 移位运算 |
< <= > >= instanceof | 小于(等于)、大于(等于)、 |
== != === !== | (不)等于、(不)全等 |
& | 按位与 |
^ | 按位异或 |
| | 按位或 |
&& | 逻辑与 |
|| | 逻辑或 |
?: | 条件(三目)运算符 |
= += | 赋值、运算赋值 |
, | 多重求值(逗号运算符) |
这次遇坑的情况就是下面这种情况:
return parts[0] && result[0] ? filterParents(parts, result) : result;
其中确定 parts 数组中不含有数据对象了,所以我就掉进了死胡同,一直以为返回的是 undefined ,最后才想到可能是运算符优先级的问题,这里因为在条件运算符中判断条件直接返回 undefined,转化为 false ,之后返回结果值是 result 值。以后还是需要注意下运算符的优先级问题。