[关闭]
@Bios 2021-04-01T03:50:17.000000Z 字数 3771 阅读 759

Vue3 源码中的位运算,又一个面试考点

Vue


在计算机中,存储一个数的时候,都是以补码的形式存储的。而正数和负数的补码表示方式是不一样的。正数的补码就等于它的原码,而负数的补码是原码除符号位以外都取反,然后 + 1 得来的。

Vue3中的位运算

在vue3的源码中,实现了一个ShapeFlags(对元素进行标记判断是普通元素、函数组件、​插槽、 keep alive 组件等等)这里不展开讲。

  1. export const enum ShapeFlags {
  2. ELEMENT = 1,
  3. FUNCTIONAL_COMPONENT = 1 << 1,
  4. STATEFUL_COMPONENT = 1 << 2,
  5. TEXT_CHILDREN = 1 << 3,
  6. ARRAY_CHILDREN = 1 << 4,
  7. SLOTS_CHILDREN = 1 << 5,
  8. TELEPORT = 1 << 6,
  9. SUSPENSE = 1 << 7,
  10. COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
  11. COMPONENT_KEPT_ALIVE = 1 << 9,
  12. COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
  13. }
  14. if (shapeFlag & ShapeFlags.ELEMENT || shapeFlag & ShapeFlags.TELEPORT) {
  15. ...
  16. }
  17. if (hasDynamicKeys) {
  18. patchFlag |= PatchFlags.FULL_PROPS
  19. } else {
  20. if (hasClassBinding) {
  21. patchFlag |= PatchFlags.CLASS
  22. }
  23. if (hasStyleBinding) {
  24. patchFlag |= PatchFlags.STYLE
  25. }
  26. if (dynamicPropNames.length) {
  27. patchFlag |= PatchFlags.PROPS
  28. }
  29. if (hasHydrationEventBinding) {
  30. patchFlag |= PatchFlags.HYDRATE_EVENTS
  31. }
  32. }

上面的代码都是vue3源码中的片段,其中有<<,&,|=这几个平时基本上不会用到运算法,本文就总结一下前端需要掌握的位运算。

Tips

原码,反码,补码

  1. 10 = 01010 // 原码
  2. -10 = 11010 // 原码
  1. 10 = 01010 // 反码
  2. -10 = 10101 // 反码
  3. 10 + (-10)= 01010 + 10101 = 11111 = -0
  1. -10 = 10110 // 补码

进制转换

  1. /**
  2. * 十进制转其他进制
  3. * @param {[type]} num [数字]
  4. * @param {[type]} radix [进制]
  5. * @return {[type]} [返回一个radix进制的数]
  6. */
  7. function convert(num,radix){
  8. return parseInt(num).toString(radix);
  9. }
  10. convert(123,2);
  11. /**
  12. * 其他进制转10进制
  13. * @param {[type]} num [数字]
  14. * @param {[type]} radix [进制]
  15. * @return {[type]} [返回一个10进制数字]
  16. */
  17. function convert10(num, radix){
  18. return parseInt(num, radix);
  19. }

&(按位与),~(按位非),^(异或),|(或)

&(按位与)

  1. const a = 5; // 00101
  2. const b = 3; // 00011
  3. console.log(a & b); // 00001
  4. // 1

真值表: (都为1,值才为1)

a b a AND b
0 0 0
0 1 0
1 0 0
1 1 1

注意:负数按补码形式参加按位与运算。

  1. const a = 5; // 00101
  2. const b = -3; // 10011(原),11100(反),11101(补)
  3. console.log(a & b); // 00101
  4. // 5

~(按位非)

把每一位都反转一下:

  1. const a = 5; // 00101
  2. console.log(~a); // 11010 != -6
  3. // -6

就来看看~5的计算步骤:

  1. 将5转二进制 = 00000101
  2. 按位取反 = 11111010
  3. 发现符号位(即最高位)为1(表示负数),将除符号位之外的其他数字取反 = 10000101
  4. 末位加1取其补码 = 10000110
  5. 转换回十进制 = -6

负数的运算,要先找出补码,然后再取反。

~-5的计算步骤:

  1. 将-5转二进制 = 10000101
  2. 按位取反得到反码 = 11111010
  3. 末位加1取其补码 = 11111011
  4. 再次取反 = 00000100
  5. 转换回十进制 = 4

按位非运算时,任何数字x的运算结果都是-(x + 1)。例如,〜-5运算结果为4。

~~ 和 parseInt :

  1. ~~ 5.2 = - ((-(5 + 1)) + 1) = - (-6 + 1) = 5 === parseInt(5.2)

在 ECMAScript® Language Specification 中是这样描述位操作符的:

  1. The production A : A @ B, where @ is one of the bitwise operators in the productions above, is evaluated as follows:
  2. 1. Let lref be the result of evaluating A.
  3. 2. Let lval be GetValue(lref).
  4. 3. Let rref be the result of evaluating B.
  5. 4. Let rval be GetValue(rref).
  6. 5. Let lnum be ToInt32(lval).
  7. 6. Let rnum be ToInt32(rval).
  8. 7. Return the result of applying the bitwise operator @ to lnum and rnum. The result is a signed 32 bit integer.

需要注意的是第5和第6步,按照ES标准,两个需要运算的值会被先转为有符号的32位整型。所以超过32位的整数会被截断,而小数部分则会被直接舍弃。

位元素是会直接弃掉小数部分的,具体的可以看看这里为什么不要在 JavaScript 中使用位操作符?

^(异或)

同一位置不相等,则为1。

  1. const a = 5; // 00101
  2. const b = 3; // 00011
  3. console.log(a ^ b); // 00110
  4. // 6

真值表:

a b a XOR b
0 0 0
0 1 1
1 0 1
1 1 0

将任一数值 x 与 0 进行异或操作,其结果为 x。将任一数值 x 与 -1 进行异或操作,其结果为 ~x。

leetcode: 只出现一次的数字

  1. 给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
  2. 说明:
  3. 你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
  4. 示例 1:
  5. 输入: [2,2,1]
  6. 输出: 1
  1. var singleNumber = function(nums) {
  2. for(let i = 1;i<nums.length;i++) {
  3. nums[0] = nums[0] ^ nums[i]
  4. }
  5. return nums[0]
  6. };

|(或)

同一位置有一个1则为1.

  1. const a = 5; // 00101
  2. const b = 3; // 00011
  3. console.log(a | b); // 00111
  4. // 7

5 | -3负数的按位或:

  1. 5的原码 = 0000101
  2. -3的原码 = 1000011,反码 = 1111100, 补码 = 1111101
  3. 5 | -3 = 1111101
  4. 发现符号位(即最高位)为1(表示负数),将除符号位之外的其他数字取反 = 1000010
  5. 末位加1取其补码 = 1000011
  6. 转换回十进制 = -3

真值表:

a b a OR b
0 0 0
0 1 1
1 0 1
1 1 1

<<(左移),>>(右移), >>>(无符号右移),

<<(左移)

左移操作符 (<<) 将第一个操作数向左移动指定位数,左边超出的位数将会被清除,右边将会补零。

  1. const a = 5; // 00000101
  2. a << 2 // 00010100 = 20

a << 2 = a * 22 = a * 4 = 20;
a << 4 = a * 24 = a * 16 = 80;

>>(右移)

该操作符会将第一个操作数向右移动指定的位数。向右被移出的位被丢弃,拷贝最左侧的位以填充左侧。由于新的最左侧的位总是和以前相同,符号位没有被改变。所以被称作“符号传播”。

  1. const a = 5; // 00000101
  2. a >> 2 // 00000001 = 1

>>>(无符号右移)

该操作符会将第一个操作数向右移动指定的位数。向右被移出的位被丢弃,左侧用 0 填充。因为符号位变成了 0,所以结果总是非负的。(译注:即便右移 0 个比特,结果也是非负的。)

  1. const a = -5; // 100...00101原 - 11...111010反 - 11...111011补
  2. a >>> 2 // 0011...1110 = 1073741822

总共是有32位,中间的省略了。无符号位移最有可能在实际开发中使用的就是x >>> 0取整。

lodash 中的 slice 方法

  1. function slice(array, start, end) {
  2. ...
  3. length = start > end ? 0 : ((end - start) >>> 0)
  4. start >>>= 0
  5. ...
  6. }
  7. export default slice
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注