关于前端:二进制之简介和应用

29次阅读

共计 6030 个字符,预计需要花费 16 分钟才能阅读完成。

前言

计算机外部是一个由 0 和 1 组成的二进制世界,咱们所有的操作最终都会转换成二进制进行运算和存储,这是因为在电子计算机呈现时,是应用电子管来进行状态治理的,而它也就只有“”和“”(通、断电)这两种最根本的状态,这也就决定了计算机用二进制来表述数字和数据是最容易实现的,而它的通用性在科技如此发达的明天仍然无奈被代替。
二进制数据是用 01两个数码来示意的数。它的基数为 2,进位规定是“逢二进一 ”,借位规定是“ 借一当二

乏味的特色

  • 如果一个二进制第零位(最右侧)的值为 1,则这个数肯定是个奇数;而如果该位是 0,那么这个数就是偶数
  • 2ⁿ- 1 转换成二进制是 n 个 1;例:2³ = 7(十进制) = 111(二进制)
  • 将一个二进制数的所有位左移 1 位的后果是将该数乘以二;例:7<<1 等于 14;7 的二进制为 111,移位后为 1110=14

    进制间的转换

    负数间的转换

  • 十进制转二进制:除 2 取余,逆序排列
57 % 2 = 28 余 1
28 % 2 = 14 余 0
14 % 2 = 7  余 0
 7 % 2 = 3  余 1
 3 % 2 = 1  余 1
 1 % 2 = 0  余 1

后果为(倒取):111001

  • 二进制转十进制:取不为 0 的地位序号作为 2 的次方进行计算,并将后果进行相加
// 111001
Math.pow(2, 5) + Math.pow(2, 4) + Math.pow(2, 3) + Math.pow(2, 0) === 57

小数间的转换

  • 十进制转二进制:乘 2 取整,正序排列

    0.375 * 2 = 0.750 取整 0
    0.750 * 2 = 1.500 取整 1
    0.500 * 2 = 1.000 取整 1

    后果为:0.011

  • 二进制转十进制:取小数点后不为 0 的地位序号作为 2 的负次方进行计算,并将后果进行相加

    // 0.011
    Math.pow(2, -2) + Math.pow(2, -3) === 0.375

    正数间的转换

    说到二进制正数首先要介绍三个名词:原码 反码 补码,因为在计算机外部,正数是以补码的模式存在的

    原码:负数的原码为其绝对值转二进制;正数的原码为其绝对值转二进制而后最高位补 1
    反码:负数的反码和原码一至;正数的反码为其原码除符号位外各位取反
    补码:负数的补码和原码一至;正数的反码为其原码除符号位外各位取反,而后再加 1

  • 十进制转二进制(八进制为例):

    • -57 的绝对值转二进制:111001
    • 最高位补 1:10111001
    • 处符号位取反:11000110
    • 最高位加 1:11000111
      后果为:11000111
  • 二进制转十进制则返回来算就能够了

    问题剖析

    有了下面的常识,那么 0.1+0.2 !== 0.3 就有了一个最简略、容易了解的解释了:转二进制算不开,会呈现有限循环局部,所以就会精度失落,具体能够参考 为什么 0.1+0.2 不等于 0.3

    位运算介绍

    逻辑与:AND,操作符:&

两个对应的二进制位都为 1 时,后果为 1;例:

    1101   ->  13
AND 1001   ->  9
------------------
    1001   ->  9

判断一个数的奇偶就能够利用这个个性:

18 & 1 === 1 // false,19 & 1 === 1 // true,

原理就是因为所有的奇数转成二进制后最初一位为 1,而偶数最初为 0,当他们和 1 做按位与操作时,失去的后果只有”1“(奇数)和”0“(偶数)两种状况

逻辑或:OR,操作符:|

两个对应的二进制位有一个为 1 时,后果就为 1;例:

   1101   ->  13
OR 1001   ->  9
------------------
   1101   ->  13

取整是其中一种利用:

function toInt(num) {return num | 0}
console.log(toInt(3.2))     // 3
console.log(toInt(2.12345)) // 2

逻辑异或:XOR,操作符:^

两个对应的二进制位雷同为 0,相异为 1 例:

    1101   ->  13
XOR 1001   ->  9
--------------------
    0100   ->  4

能够利用此个性实现不借助新的变量来替换两个变量的值

var a = 10, b = 20
a ^= b
b ^= a
a ^= b
console.log(a,b) // 20,10
// 10 = 01010
// 20 = 10100
// a  = 11110   # a ^ b 的后果,其中的 1 是 a 和 b 中不同的局部 
// b  = 01010   # b ^ c 的后果,有没有发现和 a 是一样的
// a  = 10100   # a ^ d 的后果,有没有发现是 b 是一样的

逻辑非:NOT,操作符:~

0 变 1,1 变 0;例(八位):

NOT     1001
-------------
    11110110

按位左移:SHL,操作符:<<

各二进位全副左移若干位,右侧抛弃,左侧补 0 例:

         57:111001
-------- 57 << 1 -------
运算后果:114:1110010

一个简略的乘法小技巧:

num << 1    // num * 2
num << 2    // num * 4
num << 3    // num * 8

按位右移:SHR,操作符:>>>>>

>>:有符号位位移;各二进位全副右移若干位,右侧抛弃,左侧补符号位
>>>:无符号位位移;各二进位全副右移若干位,右侧抛弃,左侧补 0

         57:111001
-------- 57 >> 1 ---------
运算后果:28:11100

一个简略的整除小技巧:

num >> 1    // num / 2
num >> 2    // num / 4
num >> 3    // num / 8

位运算在 VUE3.0 中的利用

在 vue3.0 中,和 vnode 元素相干的判断和更新中就有大量对于位运算的操作

// packages/shared/src/shapeFlags.ts
export const enum ShapeFlags {
  ELEMENT = 1,  // 一般 HTML:0000000001
  FUNCTIONAL_COMPONENT = 1 << 1, // 函数式组件:0000000010
  STATEFUL_COMPONENT = 1 << 2,  // 有状态组件:0000000100
  TEXT_CHILDREN = 1 << 3, // 子节点为纯文本:0000001000
  ARRAY_CHILDREN = 1 << 4, // 子节点为数组:0000010000
  SLOTS_CHILDREN = 1 << 5, // 子节点为插槽:0000100000
  TELEPORT = 1 << 6, //0001000000
  SUSPENSE = 1 << 7, //0010000000
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, // 未被 keep-alive 的有状态组件:0100000000
  COMPONENT_KEPT_ALIVE = 1 << 9, //  keep-alive 中有状态组件:1000000000
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT // 有状态和无状态组件的结合体:0000000110
}
// 用于标识节点更新类型:packages/shared/src/patchFlags.ts
// 还有:packages/shared/src/slotFlags.ts

createVNode 创立节点时,会通过 shapeFlag 标记以后节点类型和其子节点类型

function createBaseVNode(type, children, patchFlag, shapeFlag) {const vnode = { type, children, patchFlag, shapeFlag}
    if (children) {
        // 通过位运算在 shapeFlag 中增加 children 的类型
        //1、如果标签被标记为 element,则二进制为:0000000001
        //1.1、若 childen 被标记为纯文本,则二进制变为:0000001001
        //1.2、若 childen 若标记为数组,则二进制变为:0000010001
        vnode.shapeFlag = vnode.shapeFlag | (isString(children) ? ShapeFlags.TEXT_CHILDREN : ShapeFlags.ARRAY_CHILDREN)
    }
    return vnode
}
function _createVNode(type, props, children){
    // 如果 type 是字符串,就将以后节点当做 element 节点
    const shapeFlag = isString(type) ? ShapeFlags.ELEMENT : ShapeFlags.FUNCTIONAL_COMPONENT // 简写,理论判断以原码为准
    return createBaseVNode(
        type,
        children,
        patchFlag,
        shapeFlag
    )
}

patch 阶段,则就会对 createVNode 时创立的shapeFlag, 进行逻辑与(&)运算来判断标签类型

const patch = (n1, n2) => {const { type, ref, shapeFlag} = n2
    switch (type) {
        // ...
        // 省略针对文本、正文、根节点等判断
        default:
            // 依据 shapeFlag 判断标签类型
            if (shapeFlag & ShapeFlags.ELEMENT) { // 打包后回变成 shapeFlag & 1
                processElement() // 解决标签时还须要解决其外部子元素} else if (shapeFlag & ShapeFlags.COMPONENT) { // 打包后会变成 shapeFlag & 6
                processComponent()} else if (shapeFlag & ShapeFlags.TELEPORT) { // 打包后会变成 shapeFlag & 64
                ;(type as typeof TeleportImpl).process()} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { // 打包后会变成 shapeFlag & 128
                ;(type as typeof SuspenseImpl).process()}
    }
}

processElement函数则会调用 mountElement 进行元素的首次渲染和外部子元素判断

const mountElement = (vnode) => {
    // ... 略
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { // 子元素是文字
        hostSetElementText(el, vnode.children as string)
    } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { // 子元素是数组
        // mountChildren(vnode.children)
        // 调用 patch 对子元素进行从新判断
        for (let i = 0; i < vnode.children.length; i++) {const child = (children[i] = optimized? cloneIfMounted(children[i]): normalizeVNode(children[i]));
            patch()}
    }
}

依据下面的逻辑能够看到:如果 shapeFlag 为 0000010001;其与ShapeFlags.ELEMENTShapeFlags.ARRAY_CHILDREN进行逻辑与运算,当后果都是非 0 的值,最初就会胜利进入 patch children 阶段;
其实 vue3.0 在解决 VNode 这种最要害的性能损耗方面做了十分多的优化,二进制运算优化只是其中一种,然而在代码中纯熟使用二进制运算,对运算复杂度、逻辑判断、运行性能和代码体积上都会有十分大的晋升

通过位运算实现简略的权限管制判断

在程序中,纯熟的应用二进制运算能够缩小代码的逻辑判断、加强代码扩展性、易于存储及晋升效率。而权限判断,算是比拟常见的一种利用场景,无论是 linux 外部的局部权限管制还是大型的管理系统都有十分多的实际利用。
假如当初零碎须要三种权限 减少 删除 批改,则咱们只须要用一个 number 类型的变量就能够管制所有权限类型,同时在将权限存储数据库时也只须要存储这一个变量即可

// 定义权限
const enum permissions {
  ADD = 1, // 1
  DELETE = 1 << 1, // 2
  UPDATE = 1 << 2 // 4
}

class userRole {
  private roles: number;
  constructor() {this.roles = 0}
  public addRole(role: number): void {
    this.roles |= role
    // 和下面等价
    // if (!this.hasRole(role)) {
    //   this.roles += role
    // }
  }
  public removeRole(role: number): void {this.roles &= (~role);
    // 和下面等价
    // if (this.hasRole(role)) {
    //   this.roles -= role 
    // }
  }
  // 有至多一个权限
  public hasRole(role: number | number[]): boolean {let roles:number[] = typeof role === 'number' ? [role] : role
    for (let i = 0; i < roles.length; i++) {if (!!(this.roles & roles[i])) {return true}
    }    
    return false
  }
  // 有所有权限
  public hasBothRole(role: number | number[]): boolean {let roles:number[] = typeof role === 'number' ? [role] : role
    for (let i = 0; i < roles.length; i++) {if (!(this.roles & roles[i])) {return false}
    }    
    return true
  }
  public resetRole(): void {this.roles = 0}
}

const myRole = new userRole()
console.log(myRole.hasRole(permissions.ADD)); // false

myRole.addRole(permissions.ADD)
myRole.addRole(permissions.DELETE)
console.log(myRole.hasRole([permissions.ADD, permissions.DELETE])); // true

myRole.removeRole(permissions.DELETE)
console.log(myRole.hasRole([permissions.ADD, permissions.DELETE])); // true
console.log(myRole.hasBothRole([permissions.ADD, permissions.DELETE])); // false

以上就是一个简略应用二进制进行权限判断的逻辑,如果尝试应用非二进制实现此函数会发现,二进制计划在权限判断时会少一些逻辑判断和代码,代码效率就更不用说了!

正文完
 0