【轻聊前端】那些“无理取值”的运算

现今各种框架、工具‘横行’,到处在讲原理和源码,更有跨端技术需要我们去探索,但如果基本功不好,学什么都是事倍功半,效果很不好,花费时间的同时打击自信心。此篇文章,为我所计划的【轻聊前端】系列第四篇,旨在系统地、逻辑性地把原生JavaScript知识分享给大家,帮助各位较为轻松地理清知识体系,更好地理解和记忆,我尽力而为,望不负期待。

凡有数据参与且得出某种结果的,都叫运算——灵感

本系列文章讲求循序渐进,说完变量,聊完对象,是时候讲运算了,前文说过,程序就是在做“数据存储”和“数据处理”,存储说完就是处理,运算便是处理的方式。

四则运算(之外)

说起运算,不得不让人联想到数学,最熟悉的就是“加、减、乘、除”四则运算,你一定能想到,JavaScript当中也有这几种运算,因为有数字类型,但它远不止这四种,而且就这四种而言,跟数学当中也不完全一样,在某些场景下,会发生“神奇的转换“。为什么?别忘了,作为计算机,对给它的任何一行代码,一个字符,都需要做出反应,特别是JavaScript这种动态弱类型语言,相当宽容,会在不知道变量是什么的情况下尽量做出反应。

暂把“神奇的转换”留后,四则运算也不做赘述,先看四则运算之外它还有什么。

两元运算

什么是两元,“元”就是参与运算的数据的个数,两元即有两个数据参与运算。

(完全)等于/不等于

先说等于,这很简单,但新手程序员都需要一段时间适应,即JavaScript中的等于和数学中的不一样,需要用“==”符号,而不是“=”,“=”在JavaScript中代表“赋值”。

1
2
if(a = 3){
}

这是把3赋给a,而不是判断两者是否相等,须多加注意。

另外,数学当中只有等于、大于、小于之类,没有”不等于“,而程序里加入了“不等于”,这就可以用一个运算统一两种情况。写法如下:

1
a!=b

按照功能来说,这么写就够了,但并不保险,因为一个小小的是否相等,浏览器都会试图对它进行类型转换之后再比较。比如

1
2
0 == ' '  //true  这里是空串儿,不是''
null == undefined //true

0和空格字符显然是两个概念,null 和 undefined 也不能等同。不能判定为相等,但返回是true,所以,在判断两个值是否相等时,建议使用“完全运算符”。示例如下:

1
2
0 === ‘ ’ //false
null === undefined //false

完全运算符会在比较的同时检查类型,类型不一样就肯定不等。完全不等即“!==”。

这样就能准确判定了。

取模(余数)

相比之下,取模(%)运算不是很常见,它的效果是这样的:

1
let result = 55 % 10;  // 5

即看两数是否能整除,如果不能,得到余数。

应用场景有:判断是不是某个数的整数倍(本职),通过这个本职,可以判断一个数字是不是2的整数倍,进而判断“奇/偶”。

指数

以前,我们求一个数字的多少次方,可以用上一篇文提到的Math对象的pow方法:

1
let result = Math.pow(3,2);  //9

ES7之后引进了新的操作符——**,于是可以写成下面这样。

1
let result = 3**2;

书写上更简洁。

赋值

前面说了“=”是赋值,但除此之外还有一系列有关运算后赋值的简化写法。统一的表达式是:变量 运算符= 变量(常量)

意思就是,将某变量进行运算之后再把值赋给原变量。示例如下:

1
2
3
4
5
a += 1;  //等同于 a = a+1 下同
a -= 1;
a *= 1;
a /= 1;
a %= 2

逻辑操作

什么是逻辑?在JavaScript中逻辑运算得到的结果是“真/假”、“是/否”,即判断一个条件成立与否。

“&&”——与操作,均为真,结果才是真。

最为直观的:

1
let result = true && false;  //false

但平时一般不会这么直接操作布尔值,而是使用其他运算得出的结果。比如:

1
2
let a = 4;
let result = a > 3 && a < 5; // true

“||”——或操作,有一个为真即为真,否则是假。

1
2
3
4
let a = 4;
let result1 = a < 3 || a > 5; // false
let result2 = a > 3 || a > 5; // true
let result3 = a > 3 || a < 5; // true

这两种运算符为日常开发的很多场景提供了便捷的判断方式,常用于流程控制中的条件判断,但有时候会被自己绕进去,比如,当需要a和b两种场景都适用时,可能会因为这个“和”字,本能地选择了“与”运算,即“&&”,这就不对了,都适用的意思是“a”或者“b”,要用“||”才对。

另外,逻辑运算符不仅仅能用来得到“是”或“否”,还有一种常见的用法,称为“短路运算”。

先看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
let result;

if(a){
result = b
} else {
result = a
}

if(a){
result = a;
} else {
result = b;
}

第一种情况,当a有值且不为false(包括隐式类型转换),result得到b,否则得到a。

第二种情况,当a有值且不为false(包括类型转换),就不往后看了,result得到a,否则得到b。

但上面的写法显得略繁琐,如果使用“短路运算”,可以像下面这样:

1
2
let result = a && b;
let result = a || b;

效果是一样的。

短路的写法为我们提供了赋值的“优先级”。逻辑运算的思路并没有改变,只是会发生赋值的动作。

所以,当我们要在两个值之间做取舍时,就可以考虑使用“短路算法”,一行代码搞定,简洁、清晰。

一元运算

先说两元是因为两元大家都熟悉,易接受,一元也很好理解,一个操作符,一个操作数。

+/-

“+”和”-“除了作为两元操作,同样可作为一元使用,最常见的就是“正负数”。

1
2
+1
-1

但是,如果不是直接放在数字的前面,而是变量的前面,就会有不同。

比如:

1
2
3
let a = '1'; 
let result = +a; // 1
let result = -a; //-1

这段代码里,a原本是字符串的“1”,在前面加上了“+”或“-”号之后,就转变成了数字,为什么呢?因为在字符串前面加代表“正负”的符号是没有意义的,JavaScript会试图使其变得合理。

注意两个字“试图”,说明并不总成功,如果值没法转换成数字,就会失败。

1
2
let a = 'a';
+a //NaN

虽然一元操作符有这么一个功能,仍属于隐式转换的范畴,只有当拿到的值和想要的值的类型不匹配的时候才需要用到,如果对自己的操作很确定,且从代码的易读性出发的话,可以直接使用Number()方法进行显式转换。

++/–

说完一个的,说说两个的,两个叫“自增”或“自减”,因为没有另外的数据跟它结合,也正因为没有数据结合,每次都只能“自增”或“自减”1。

1
2
a++  //自增1
a-- //自减1

功能简单,但放的位置就有讲究了,放前放后有区别。

1
2
let  result = a++;   // 符号在后,先赋值后增加
let result = ++a; // 符号在前,先增加后赋值

这两种运算符常被用在需要重复执行的代码段,经过多次运算后达到一个临界值,再进行其他操作,或停止操作,最常见的就是for循环。

1
2
3
for(let i = 0;i<=length;i++){
//执行操作
}

每循环一圈儿,i加1,直到达到length停止。

!

“!”叫“取反”运算符,按说它也属于逻辑运算,是对结果的值进行取反,结果是真,取反后是假,结果是假,取反后是真。

1
2
3
4
let a = false;
if(!a){
//a为假时执行的操作,假不只是false,还包括能够布尔转换为假的其他值
}

你可以在任何需要的时候使用“!”运算符,一个常用场景就是状态切换。

1
2
3
4
let on = true;
function changeStatus(){
on =!on;
}

给需要切换状态的元素绑定点击事件,然后执行函数changeStatus,每点击一次,就会变为相反的值,从而达到反复点击切换状态的效果。

三元运算

三元运算,又叫“条件运算”。JavaScript当中三元运算符也很常见。格式如下:

表达式?值1:值2

示例:

1
2
let a = 3;
let result = a>2?'真':'假';

这段代码表达的是,当a的值大于2,取前值,否则取后值。即根据表达式是否成立来决定值是什么。

这个运算和前面聊的“||”运算的效果有相似的地方,各位可根据具体情况选择使用。

需要注意的点

+的连接性

关于“+”,我们习惯的作用是数字相加,但到了JavaScript领域,“+”还有另一个常见的作用是“连接字符串”。如下:

1
let result = 'a'+'b';   // "ab"

虽然字符串本身也有连接的方法,相比之下,“+”更直接和简洁。

但如果仅此而已,就没什么可注意的,JavaScript世界最大的不确定就是“拿到的变量是什么”。

如果是这样:

1
let result = 1 + 'b';

数字和字符串相加,得到的什么呢?是“1b”。

可能你会说了,后面那个是b,没法转换成数字,才有的这个结果。那换一下:

1
let result = 1 + '2';

结果是“12”,显然上面的猜测并不对。

当使用“+”号进行二元运算的时候,任何一方的数据是字符串类型,或者被转换成了字符串类型,结果都是字符串类型。比如:

1
2
3
4
let result = "";
result + 2 //"2"
result + undefined // "undefined"
result + null // "null"

为什么单拎“+”号呢,因为只有“+”号有这个特殊作用,其他运算符没有。

字符串比较

在业务需求中,常有给一组数据按照大小排序的情况,这时候就有一个陷阱在等着我们——字符串的比较。

字符串比较有两个算法:

  • 逐位比较,直到得出结果
  • 比较的是ASCII码值

这样的算法就会出现如下情况:

1
100 < 20   //true

事实上这个结果是不对的,我故意这样写,因为有时候,用眼睛看着是数字的,其类型可能是字符串,可能比较的是“100”和“20”,就会出现上面的情况。

用 charCodeAt() 方法可以查看字符的ASCII码值,’1’是49,而’2’是50。比较完第一位就已经分出大小了。

再看个例子:

1
'13' > '123'   //true

首位1是相等的,第二位3的ASCII码比2的ASCII码大,出结果。

所以,在进行数据排序的时候,一定要注意类型,否则可能和预期不一致,数字如此,字母或其他特殊符号也是一样,不再赘述。

对象的隐式转换

面试题放送时间到~

问“什么情况下,a == 1 && a == 2 && a == 3 是成立的?“

这…看起来很不合常理,不是故意刁难吗?

先别急,我们讲一下上面一直没提过的东西。上面一直在说基本类型会转换,而没说对象。

拿值和对象作比较看似没有实用的地方,但程序依然允许运行,且会有相应的处理机制。

对象到数字(number)和字符串(string)类型值的转换,是直接与valueOf()以及toString()方法相关的。规则如下:

  • 如果试图转换为字符串,则先尝试toString()方法,然后再尝试valueOf()方法。

  • 否则,先尝试调用valueOf()方法,再尝试调用toString()。

有人要问了,这俩方法哪来的?还记得上篇文章的内容吗?它们是来自原型的内置方法。

于是,上面的题目便找到了使其成立的途径:

1
2
3
4
5
6
7
8
9
10
11
12
let a = {
  i: 1,
  valueOf: function () {
    return a.i++;
  }
}

if(a == 1 && a == 2 && a == 3) {
  console.log('我成真了!');
}

// 我成真了!

可以利用类型转换时会调用valueOf(也可以是toString)方法的特性,在里面写我们想要的其他效果,就可以了。

明白了这个道理,是不是一点都不怕了?

当然,这道题不仅这一种方案,只是另一种方案稍微偏离主题,我们后面聊到对象的时候再深挖~

总结

JavaScript中的运算,说多不多,说少也不少,特别是隐式类型转换的处理机制,需要较多经验的积累,大坑没有,小坑不断。

鉴于篇幅原因,这里把不常用的和一些细节略了,但不管哪种情况,可概括为两条:

  • 取值:不论数学计算,字符串连接,甚至“短路算法”,目的都是获取一个值,当无法得到合法值时,就会返回NaN或者报错之类期望之外的结果。
  • 逻辑判断:使用“&&、||、!”或者隐式转换的情况,作为流程控制的条件判断。

这样以来,我们就只需要关心自己要做什么,每碰到一种意外情况就记下,慢慢就都清楚了。

说完变量(值的存储)、对象系统(编程的土壤)、运算(数据处理的方式)之后,接下来就是对不同的类型进行各个击破了。

下篇见~