乐趣区

从isEmpty方法深刻理解javascript运算符优先级

前言

在平时的开发中,我们经常会遇到多种运算符在同一个表达式中出现的情况,尤其是在三元条件判断运算符中。

三元条件判断运算符虽然可以让我们避免写过多的 if…else 条件判断,但多层三元运算符嵌套,其中又包含其他不同优先级的运算符时,对于阅读我们代码的人来说,简直就是噩梦。

今天我们就结合一个现实中经常用到的工具函数 isEmpty() 的实现,来讲解一下如何解读复杂的运算符嵌套

isEmpty

isEmpty 是 著名的 loadsh 库提供的一个工具方法,被应用于判定一个 javascript 对象是否为空对象。

空对象

对于空对象,loadsh 是这么解释的:

如果【对象没有自己的可枚举字符串键控属性】,则认为它们是空的,
如果参数、对象、缓冲区、字符串或类似于 jquery 的集合等【类似数组的值的长度为 0】,则认为它们为空。类似地,如果【映射和集合的大小为 0】,则认为它们是空的

isEmpty 的实现

isEmpty = function (val) {return !(!!val ? typeof val === 'object' ? Array.isArray(val) ? !!val.length : !!Object.keys(val).length : true : false);
}

解读

运算符优先级

javascript 的运算符优先级可以参考 MDN 上的说明,如下图:

运算过程解读

我们再看内部实现代码,其中 val 为要判断是否为空对象的值:

return !(!!val ? typeof val === 'object' ? Array.isArray(val) ? !!val.length : !!Object.keys(val).length : true : false);

现在根据运算符优先级一步一步解读运算过程

  1. 我们知道 return 后面应该是一个表达式的值,我们假定这个值为 X,则整个表达式可以看做:
var X = !(...);
return X;
  1. 那么接下来就要先对赋值符号(=)右边的表达式求值,即:
!(...)
  1. 可以看到,这个表达式有两个运算符——逻辑非和括号,按照优先级,括号的优先级高于逻辑非,所以这里逻辑非要等到括号内的内容运算出一个结果,然后才能对这个结果进行逻辑非运算,我们假定括号内的内容最终运算的结果为 Y,则可以写作:
X = !Y
  1. 现在来看一下 Y 表达式的内容:
Y = !!val ? typeof val === 'object' ? Array.isArray(val) ? !!val.length : !!Object.keys(val).length :true :false
  1. 接下来就有点复杂了,按照优先级,按照这些运算符里优先级最高的应该是成员属性访问(.),那应该第一步运算结果是这样:
Y = !!val ? typeof val === 'object' ? (true/false) ? !!(0/1/2/.../N) : !!(0/1/2/.../N) :true :false

​ 因为我们这里对 val 的值不一定,所以这里对 Array.isArray(val)Object.keys(val).length 最终的计算结果有多种可能,我用 / 号隔开了各种可能值,并且将他们放入同一个括号内,如果是正式的计算,我们传入的 val 是一个确定的值,那么这些运算结果也会是一个确定值,并且也不会有括号。

  1. 接下来,逻辑非和 typeof 运算符的优先级都是 16,是剩余运算符中最高的,所以对这两种进行运算,结果如下:
Y = (true/false) ? (string/object/boolean/null/undefined)==='object' ? (true/false) ? (true/false) : (true/false) : true :false

这个地方比较绕,因为!! 会将后面的值强制转换为布尔值,所以最后的结果几乎都是由 truefalse 组成的了。

  1. 接下来,整个表达式就只剩全等运算符(===)和三元条件运算符(… ? … : …)了,从上表可知,全等运算符的优先级要高一点,所以结果如下:
Y = (true/false) ?(true/false) ? (true/false) ? (true/false) : (true/false) : true :false
  1. 现在,我们的表达式里就只剩三元条件运算符和布尔值了,问题是这里有多个三元运算符嵌套,我们该从哪个开始计算呢?现在就可以看 结合性 了,我们发现条件运算符的结合性是从右至左,那么我们的表达式就变成了:
Y = (true/false) ? ... : false

我们回溯以下这里面的 (true/false) 其实就是原始表达式中 !!val的运算结果,然而这里还无法运算出整个表达式的结果,因为 ... 所代表的那部分还不是一个最终值,还需要运算,记得最开始的做做法吗?对于 !(...) 这个表达式,我们将括号内的表达式用Y 来代替了,同样地,我们把这里 ... 所代表的表达式部分用一个字母M 来代表,即:

M = (true/false) ? (true/false) ? (true/false) : (true/false) : true;
Y = (true/false) ? M : false;
  1. 到这里,计算机就开始判断了:

    • 如果!!val 的值为false,则直接返回 false(即括号后面的值),后面的 M 中的表达式就不再运算了。那么此时 Y=false, 而 !Y 相当于取反,X = !Y 的值就等于true。我们这个方法是用来判断是否为空对象的,返回结果为true,就说明这个val 是空对象。
    • 我们可以延伸一下,符合 !!val === falseval 都有哪些呢?0""false ,nullundefined 符合这个特征,我们发发现,它们都是 javascript 中的‘假值’。
    • 那么如果 !!val 的值为 true 呢,则需要返回 M 表达式的结果,我们就需要继续计算 M 表达式的值了。
  2. 现在我们再看 M 表达式的运算过程,依葫芦画瓢,我们可以得到:
M = (true/fasle) ? ... : true

通过回溯,我们可以知道,这里的(true/false) 其实就是原始表达式中的typeof val === 'object' 的最终运算结果。

同样的,我们将 ... 内的内容使用字母 N 代替,结果如下:

N = (true/fasle) ? (true/fasle) : (true/false)
M = (true/false) ? N : true;
  1. 到这里,计算机又开始判断:

    • 如果 typeof val === 'object' 的值为false , 即说明val 不是对象类型,则直接返回true(冒号后面的值),不需要再运算 N 表达式的结果。此时 Y = true, 则 X= !Y=false,最终值为false , 说明val 不是空对象。
    • 如果typeof val === 'object' 的值为 true 则需要返回 N 表达式的值作为结果,计算机需要计算运算 N 表达式的值。
  2. 对于 N 表达式,其中有三个布尔值,通过回溯,我们也可以知道他们的原始表达式分别是:
  • Array.isArray(val)
  • !!val.length
  • !!Object.keys(val).length

那么我们知道,这一步当val 为对象类型时,则需要判断它是数组还是非数组:

  • 如果是数组,则拿到数组的长度值,对长度值做 !! 操作

    • 如果长度为0,则操作结果为false, 返回后,Y=false,X=!Y=true,说明 长度为 0 的数组为空对象
    • 其它长度结果为 true,将结果返回后,Y=true, X=!Y=false,说明长度大于 0 的数组不属于空对象
  • 如果不是数组,则取它的可枚举属性的长度(Object.keys(val).length),并对长度做!! 操作

    • 如果长度为0,则操作结果为false, 返回后,Y=false,X=!Y=true,说明 可枚举属性长度(个数)为 0 的对象为空对象
    • 其它长度结果为 true,将结果返回后,Y=true, X=!Y=false,说明可枚举属性长度大于 0 的对象不属于空对象

总结

至此,我们按照程序执行的顺序步进似的完成了整个运算过程的模拟,我们学到了以下几点:

  • 代换法。当表达式非常复杂时,可以按照运算符优先级,使用变量代换法代换优先级比较低的运算,先将注意力集中到优先级比较高的运算上。
  • 回溯法。步进代换和运算到最后,再无可代换运算时,就要开始回溯,对应到原始表达式,一步步求解。
  • 结合性。以前我们偏重运算符优先级的分析,从这个例子的条件运算符三层嵌套的应用,我们看到,在复杂表达式的分析中,运算符的结合性也是非常重要的分辨运算顺序的参照标准。

理解运算过程对我们理解整个程序的实现逻辑和作者的思维方式至关重要,希望以上分析过程可以在大家阅读知名框架中大神级代码时对大家有所帮助。

本文由博客一文多发平台 OpenWrite 发布!

退出移动版