理清 js 操作符及其在vue源码中的使用!

167 阅读4分钟

一元操作符

递增和递减操作符

无论使用前缀递增(++x)还是前缀递减操作符(--x),变量的值都会在语句被求值之前改变。

let age = 29
let antherAge = --age + 2 // 先减后加
console.log(age); // 28
console.log(antherAge); // 30

后缀版与前缀版的主要区别在于,后缀版递增(x++)和递减(x--) 在语句被求值后才发生。

let num1 = 2;
let num2 = 20;

let num3 = num1-- + num2 // 22 先加后减
let num4 = num1 + num2 // 21

console.log(num3);
console.log(num4);

这4个操作符可以作用于任何值,意思是不限于整数——字符串、布尔值、浮点值,甚至对象都可以。递增和递减操作符遵循如下规则。

  • 对于字符串,如果是有效的数值形式,则转换为数值再应用改变。变量类型从字符串变成数值。
  • 对于字符串,如果不是有效的数值形式,则将变量的值设置为NaN。
  • 对于布尔值,如果是false,则转换为0再应用改变。
  • 对于布尔值,如果是true,则转换为1再应用改变。
  • 对于浮点值,加1或减1。
  • 如果是对象,则调用其valueOf()方法取得可以操作的值。对得到的值应用上述规则。如果是NaN,则调用toString()并再次应用其他规则。
let s1 = 'z'
let o1 = {
    valueOf() {
        return -1
    }
}

s1--
o1--

console.log(s1); // NAN
console.log(o1); // -2

一元加和减

如果将一元加(x = +x)应用到非数值,则会执行与使用Number()转型函数一样的类型转换:布尔值false和true转换为0和1,字符串根据特殊规则进行解析,对象会调用它们的valueOf()和/或toString()方法以得到可以转换的值。

let s1 = 'z';
let o1 = {
    valueOf() {
        return -1;
    }
}

s1 = +s1
o1 = +o1
console.log(s1); // NAN
console.log(o1); // -1

在应用到非数值时,一元减(x = -x)会遵循与一元加同样的规则,先对它们进行转换,然后再取负值:

let s1 = 'z';
let o1 = {
    valueOf() {
        return -1;
    }
}

s1 = -s1 
o1 = -o1
console.log(s1); // NAN
console.log(o1); // 1

位操作符

按位与

按位与操作符用和号(&)表示,有两个操作数。本质上,按位与就是将两个数的每一个位对齐,然后基于真值表中的规则,对每一位执行相应的与操作。

image.png 下面看一个例子,我们对数值25和3求与操作,如下所示:

const a = 3 & 25
console.log(a); // 1

// 计算过程
// 000011 => 3
// 110001 => 25
// 000001 => 1

按位或

按位或操作在至少一位是1时返回1,两位都是0时返回0。

image.png

// 按位或运算
let result = 1 | 3

console.log(result); // 3

/**
 * 计算过程
 * 01 =》1
 * 11 =》3
 * 11
 */

左移

左移操作符用两个小于号(<<)表示,会按照指定的位数将数值的所有位向左移动。比如,如果数值2(二进制10)向左移5位,就会得到64(二进制1000000),如下所示:

// 左移
let value = 1 << 2

console.log(value); // 4
/**
 * 运算过程
 * 1 =》 100 =》 4 
 */

右移

有符号右移

// 有符号右移
let oldValue = 64 // 1000000
let newValue = oldValue >> 5 // 10

console.log(newValue); // 2

右移后空位会出现在左侧,且在符号位之后(见图3-3)。ECMAScript会用符号位的值来填充这些空位,以得到完整的数值。

无符号右移

与有符号右移不同,无符号右移会给空位补0,而不管符号位是什么。对正数来说,这跟有符号右移效果相同。但对负数来说,结果就差太多了。无符号右移操作符将负数的二进制表示当成正数的二进制表示来处理。因为负数是其绝对值的二补数,所以右移之后结果变得非常之大,如下面的例子所示:

// 无符号右移
let oldValue = -64; // 负数是用补数表示
let newValue = oldValue >>> 5;

console.log(newValue); // 134217726

vue3 源码中关于位运算的使用

在 vue3 源码中判断节点类型部分有使用到位运算,示例如下:

const type1 = 1
const type2 = 1 << 1
const type3 = 1 << 2

// 表示一个节点有 type1 的特性和 type2 的特性
const element = type1 | type2

// 判断一个节点是否拥有type1特性
if (type1 & element) {
    console.log('type1');
}
// 判断一个节点是否拥有type2特性
if (type2 & element) {
    console.log('type2');
}

// 判断一个节点是否拥有type3特性
if (type3 & element) {
    console.log('type3');
}

image.png

布尔操作符

布尔操作符一共有3个:逻辑非、逻辑与和逻辑或。

逻辑非

这个操作符始终返回布尔值,无论应用到的是什么数据类型。逻辑非操作符首先将操作数转换为布尔值,然后再对其取反。 换句话说,逻辑非操作符会遵循如下规则。

  • 如果操作数是对象,则返回false。
  • 如果操作数是空字符串,则返回true。
  • 如果操作数是非空字符串,则返回false。
  • 如果操作数是数值0,则返回true。
  • 如果操作数是非0数值(包括Infinity),则返回false。
  • 如果操作数是null,则返回true。
  • 如果操作数是NaN,则返回true。
  • 如果操作数是undefined,则返回true。
// 逻辑非
console.log(!NaN) // true

逻辑非操作符也可以用于把任意值转换为布尔值。同时使用两个叹号(! !),相当于调用了转型函数Boolean()。 无论操作数是什么类型,第一个叹号总会返回布尔值。第二个叹号对该布尔值取反,从而给出变量真正对应的布尔值。结果与对同一个值使用Boolean()函数是一样的:

console.log(!!NaN) // false

逻辑与

逻辑与操作符由两个和号(&&)表示,逻辑与操作符遵循如下真值表:

image.png 如果有操作数不是布尔值,则逻辑与并不一定会返回布尔值,而是遵循如下规则。

  • 如果第一个操作数是对象,则返回第二个操作数。
  • 如果第二个操作数是对象,则只有第一个操作数求值为true才会返回该对象。
  • 如果两个操作数都是对象,则返回第二个操作数。
  • 如果有一个操作数是null,则返回null。
  • 如果有一个操作数是NaN,则返回NaN。
  • 如果有一个操作数是undefined,则返回undefined。
// 逻辑与
const o1 = {
    a: 1,
    b: 2
}
const o2 = {
    c: 3,
    d: 4
}
console.log(o1 && 123); // 123
console.log(true && o1); // o1
console.log(o1 && o2); // o2

逻辑与操作符是一种短路操作符,意思就是如果第一个操作数决定了结果,那么永远不会对第二个操作数求值。 对逻辑与操作符来说,如果第一个操作数是false,那么无论第二个操作数是什么值,结果也不可能等于true。示例如下:

// 短路操作
const a = false

console.log(a && c); // 不会报错,因为a已经确定了结果,不会去取c

逻辑或

逻辑或操作符由两个管道符(||)表示,逻辑或操作符遵循如下真值表:

image.png 与逻辑与类似,如果有一个操作数不是布尔值,那么逻辑或操作符也不一定返回布尔值。它遵循如下规则。

  • 如果第一个操作数是对象,则返回第一个操作数。
  • 如果第一个操作数求值为false,则返回第二个操作数。
  • 如果两个操作数都是对象,则返回第一个操作数。
  • 如果两个操作数都是null,则返回null。
  • 如果两个操作数都是NaN,则返回NaN。
  • 如果两个操作数都是undefined,则返回undefined。 同样与逻辑与类似,逻辑或操作符也具有短路的特性。只不过对逻辑或而言,第一个操作数求值为true,第二个操作数就不会再被求值了。
// 短路操作
const a = true

console.log(a || c); // 不会报错,因为a已经确定了结果,不会去取c

乘性操作符

如果乘性操作符有不是数值的操作数,则该操作数会在后台被使用Number()转型函数转换为数值。 这意味着空字符串会被当成0,而布尔值true会被当成1。

乘法操作符

乘法操作符在处理特殊值时也有一些特殊的行为:

  • 如果操作数都是数值,则执行常规的乘法运算,即两个正值相乘是正值,两个负值相乘也是正值,正负符号不同的值相乘得到负值。
  • 如果ECMAScript不能表示乘积,则返回Infinity或-Infinity。
  • 如果有任一操作数是NaN,则返回NaN。
  • 如果是Infinity乘以0,则返回NaN。
  • 如果是Infinity乘以非0的有限数值,则根据第二个操作数的符号返回Infinity或-Infinity。
  • 如果是Infinity乘以Infinity,则返回Infinity。
  • 如果有不是数值的操作数,则先在后台用Number()将其转换为数值,然后再应用上述规则
const a = '12'
const b = 12

console.log(a * b); // 144

除法操作符

跟乘法操作符一样,除法操作符针对特殊值也有一些特殊的行为:

  • 如果操作数都是数值,则执行常规的除法运算,即两个正值相除是正值,两个负值相除也是正值,符号不同的值相除得到负值。
  • 如果ECMAScript不能表示商,则返回Infinity或-Infinity。
  • 如果有任一操作数是NaN,则返回NaN。
  • 如果是Infinity除以Infinity,则返回NaN。
  • 如果是0除以0,则返回NaN。
  • 如果是非0的有限值除以0,则根据第一个操作数的符号返回Infinity或-Infinity。
  • 如果是Infinity除以任何数值,则根据第二个操作数的符号返回Infinity或-Infinity。
// 除法操作符
const a = '123' / 123
console.log(a); // 1

取模操作符

取模(余数)操作符由一个百分比符号(%)表示 与其他乘性操作符一样,取模操作符对特殊值也有一些特殊的行为。

  • 如果操作数是数值,则执行常规除法运算,返回余数。
  • 如果被除数是无限值,除数是有限值,则返回NaN。
  • 如果被除数是有限值,除数是0,则返回NaN。
  • 如果是Infinity除以Infinity,则返回NaN。
  • 如果被除数是有限值,除数是无限值,则返回被除数。
  • 如果被除数是0,除数不是0,则返回0。
  • 如果有不是数值的操作数,则先在后台用Number()函数将其转换为数值,然后再应用上述规则。
// 取模操作符
const a = 0 % 123
console.log(a); // 0

指数操作符

ECMAScript 7新增了指数操作符,Math.pow()现在有了自己的操作符**,结果是一样的:

console.log(Math.pow(3, 2)); // 9
console.log(3 ** 2); // 9

加性操作符

与乘性操作符类似,加性操作符在后台会发生不同数据类型的转换。只不过对这两个操作符来说,转换规则不是那么直观。

加法操作符

如果两个操作数都是数值,加法操作符执行加法运算并根据如下规则返回结果:

  • 如果有任一操作数是NaN,则返回NaN;
  • 如果是Infinity加Infinity,则返回Infinity;
  • 如果是-Infinity加-Infinity,则返回-Infinity;
  • 如果是Infinity加-Infinity,则返回NaN;
  • 如果是+0加+0,则返回+0;❑ 如果是-0加+0,则返回+0;
  • 如果是-0加-0,则返回-0。 如果有一个操作数是字符串,则要应用如下规则:
  • 如果两个操作数都是字符串,则将第二个字符串拼接到第一个字符串后面;
  • 如果只有一个操作数是字符串,则将另一个操作数转换为字符串,再将两个字符串拼接在一起。
  • 如果有任一操作数是对象、数值或布尔值,则调用它们的toString()方法以获取字符串,然后再应用前面的关于字符串的规则。 对于undefined和null,则调用String()函数,分别获取"undefined"和"null"。
    console.log('1 + 3 =' + 1 + 3); // 13

如果有一个操作数是数值,一个数是非字符串和数值 调用Number()

console.log(null + 1) // 1

减法操作符

与加法操作符一样,减法操作符也有一组规则用于处理ECMAScript中不同类型之间的转换。

  • 如果两个操作数都是数值,则执行数学减法运算并返回结果。
  • 如果有任一操作数是NaN,则返回NaN。
  • 如果是Infinity减Infinity,则返回NaN。
  • 如果是-Infinity减-Infinity,则返回NaN。
  • 如果是Infinity减-Infinity,则返回Infinity。
  • 如果是-Infinity减Infinity,则返回-Infinity。
  • 如果是+0减+0,则返回+0。
  • 如果是+0减-0,则返回-0。
  • 如果是-0减-0,则返回+0。
  • 如果有任一操作数是字符串、布尔值、null或undefined,则先在后台使用Number()将其转换为数值,然后再根据前面的规则执行数学运算。如果转换结果是NaN,则减法计算的结果是NaN。
  • 如果有任一操作数是对象,则调用其valueOf()方法取得表示它的数值。如果该值是NaN,则减法计算的结果是NaN。如果对象没有valueOf()方法,则调用其toString()方法,然后再将得到的字符串转换为数值。
const a = {
    valueOf() {
        return -1
    }
}

console.log(1 - a); // 2

关系操作符

  • 关系操作符执行比较两个值的操作,包括小于(<)、大于(>)、小于等于(<=)和大于等于(>=),用法跟数学课上学的一样。与ECMAScript中的其他操作符一样,在将它们应用到不同数据类型时也会发生类型转换和其他行为。
  • 如果操作数都是数值,则执行数值比较。
  • 如果操作数都是字符串,则逐个比较字符串中对应字符的编码。
  • 如果有任一操作数是数值,则将另一个操作数转换为数值,执行数值比较。
  • 如果有任一操作数是对象,则调用其valueOf()方法,取得结果后再根据前面的规则执行比较。如果没有valueOf()操作符,则调用toString()方法,取得结果后再根据前面的规则执行比较。
  • 如果有任一操作数是布尔值,则将其转换为数值再执行比较。
const a = {
    valueOf() {
        return 'a'
    }
}
const b = {
    toString() {
        return 'b'
    }
}
console.log('abc' < 'b'); // true
console.log(a < b); // ‘a’ < 'b' // true

一个奇怪的现象是在比较两个数值字符串的时候,比如下面这个例子:

let result = '23' < '3' // true

这里在比较字符串"23"和"3"时返回true。因为两个操作数都是字符串,所以会逐个比较它们的字符编码(字符"2"的编码是50,而字符"3"的编码是51)