小代码大学问之JavaScript位运算

原文:http://www.basecss.net/article/mini-code-with-great-learing.html

 

这几天粗略的阅读了一下AngularJS的源码,在这个过程中发现有这么两段代码挺有意思的:

这两段代码用来处理字母大小写转换,由于某些国家(土耳其)使用toLowerCase()toUpperCase()不能正确的转换字母大小写,因而需要手动的处理。

为什么说这两段代码有意思?其实是觉得其中用位运算处理字母大小写的代码很巧妙,其核心代码如下:

在分析两段代码之前,先来回顾一下JavaScript中的两个概念:整数位运算

从严格意义上讲,ECMAScript中有两种类型的整数:有符号的整数(正数和负数)和无符号的整数(只有正数)。而默认情况下JavaScript中的整数都是有符号的。

而在不考虑ECMAScript中数字格式存储与转换(为32位)的情况下,实际上我们操作的都是32位的整数。而对于上面提到的有符号整数而言,其中前31位(end<-start)表示数字的值,最后1位表示符号位(0表示正,1表示负)。

这里提到的32位的整数在计算机底层都是使用二进制格式存储的,而这个二进制由01组成,其中每一位都有对应的十进制数字结果,整个二进制数值代表的十进制结果由所有这些位对应的十进制数字之和。

这篇文章中不考虑负数的情况,一个32位二进制格式的数字看起来如下所示,这里以10为例:

二进制数字计算的方式:number ( Math.pow(2, index)),这里的number表示二进制中对应位上的数值0/1,index表示该数值在整个二进制格式的数字中的索引。注意一个二进制格式的起始点在右侧。

那么上面的数字就等于:1 * Math.pow(2,3) + 1 * Math.pow(2, 1) = 10

前面提到了,这些二进制的数字实际上都是在计算机的底层完成的,而ECMAScript中刚好提供了二进制运算相关的操作符,这些操作符都是直接对运算数进行二进制操作的,并且都是发生在幕后的。

JavaScript中有7个位运算相关的运算符:

  • 按位非(NOT) – 用一个波浪线” ~ “表示,对二进制的每一位进行取反操作,即将0变成1,将1变成0
  • 按位与(AND) – 用一个和好” & “表示,必须有两个操作数,先对齐二进制位,然后把对应位都为1的为筛下来,其他的都为0
  • 按位或(OR) – 用一个竖线” | “表示,也必须有两个操作数,对齐位之后只要对应位有1就筛下来,只有同时位0时才返回0
  • 按位异或(XOR) – 用一个插入符号” ^ “表示,也必须有两个操作数,对齐位之后不同的返回1,相同的返回0。
  • 左移 – 用两个小于号” <<表示,顾名思议,将操作数左移指定位数,右侧空位用0补齐。
  • 有符号右移 – 用两个大于号” >> “表示,保留符号位,剩下的右移指定位。
  • 无符号右移 – 用三个大于号” >>> “表示,往右侧移动指定位数。

以上这些位运算符,最终操作的都是二进制数值。

在上面的代码中分别涉及到了按位非按位与按位或三种运算。先来针对上面的两段代码讲解一下这三个位运算符:

这段代码通过正则表达式匹配到给定字符串中的每个大写字母: A-Z;接下来使用字符串对象的charCodeAt()方法拿到该字符对应的Unicode编码,恰好这个编码是一个数字;最后使用按位或运算获取到另外一个数字。

为什么这里执行对数值32的按位或运算呢?当然这肯定不是空穴来风。那么我们先从大写字母及对应的Unicode值分析看看。不难发现,A-Z对应的Unicode编码分别为65-90;而这写编码对应的二进制表示分别为:1000001 … 1011010。再看看小些字母对应的数据:其Unicode编码分别为:97-122,对应的二进制表示分别为:11000011111010。最后将它们放入一张表格中对比如下:

提示:使用(1).toString(2)便可以拿到每个数字对应的二进表示法的有效位。

大写字母二进制有效位 1000001 1011010
小写字母二进制有效位 1100001 1111010

在这个表格中没有完整列出每个字母对应的二进制有效位。但是通过完整的对比不难发现,大写字母与小写字母的二进制有效位都是7位,对这些数值进行对不不难发现大小写字母的二进制有效位中:大写字母的第6位0;而小写字母的第6位为1;而每个大小写自己的二进制有效位中刚好只有这一位不同。

因此我们在求值大写字母的对应的小写字母的二进制数值时转换大写字母的二进制数值第6位即可,其他的位是一样的不用转换。而第6位为1时,其对应的十进制数值刚好是32(1 * Math.pow(2, 5)),32对应的二进制数值的有效位为:100000

那么如何转换这里的第6位呢?我们的目的是将大写字母二进制数值第6位的0转换为1,而其他的位不变。最终我们只需要拿一个刚好第6位为1,其他位为0的二进制数值与大写字母的二进制数值进行位运算操作即可,这个能够用来进行有效位运算的二进制数值则为100000,而JavaScript中的按位或操作刚好能有做到这一点。

而在JavaScript中,我们并不能直接操作一个二进制的数值,二进制的运算都是在低层完成的,在JavaScript中这些都是按位运算符的使命。那么,在前面使用charCodeAt()方法已经拿到了大写字母对应的Unicode编码-即一个有效的十进制数字;而100000对应的十进制数字为32。

由此得出结论,使用大写字母对应的Unicode编码与32作按位或运算便能正确的拿到其对应的小写字母的Unicode编码,其操作过程如下:

以大写字母A为例

1 0 0 0 0 0 1
1 0 0 0 0 0
1 1 0 0 0 0 1

如此,便拿到了一个二进制数值:1100001,对应的十进制数字为97(parseInt(‘1100001’, 2))。最后使用String对象的fromCharCode()方法得到的字符便是大写字母A对应的小写字母a

整个转换的过程中,所有的这些操作实际上都是在底层(?内存中)完成的。

上面剖析了大写字母转小写字母的过程。接下来再看看小写字母转大写字母。在上面的代码中,我们可以看到转大写字母的代码为:

javascript ch.charCodeAt(0) & ~32

首先,同大写字母一样,使用字符串对象(String)的charCodeAt()方法拿到对应的Unicode编码(也是一个十进制数值)。在上面的字母二进制数值对比表格中我们已经找到了规律:即转换每个字母对应的二进制数值的第6即可。那么如何将小写字母的二进制数值的第6位1转换为0,而其位不变呢?

前面将大写字母的第6位0转位1,我们使用了按位或来保证将第6位正确的转换为1。而这一次小写转大写的过程中,我们必须保证正确的将第6位1转换为0,其他位不变即可。由此得出,这一次进行位运算的基本条件必须保证第二个操作数的第6位为0,而其他位该是1的是1,该是0的是0。

那么如何做到这一点呢?根据位运算的特点以及上面的分析,我们保证第6位不同即可,那么拿011111与小写字母的二进制数值进行按位与运算运算即可。而对32进行按位非运算的结果刚好为011111

以小写字母a为例

1 1 0 0 0 0 1
0 1 1 1 1 1
1 0 0 0 0 0 1

这里不一定必须是011111。比如拿一个完整的32位11111111111111111111111111011111也可以。但是在上述环境中,011111就能满足需求,而这个二进制数值对应的数值刚好是对32进行按位非的运算结果。

根据前面的分析,这样就拿到了大写字母A对应的二进制数值,再对它编码便可以返回最终的大写字母。

至此,对AngularJS中这两段代码的分析就完成了。也算是对JavaScript中的位运算做了一次巩固,温习。

其实JavaScript中的位运算远远不止这一点,我们还可以使用其他位运算符做到很多事情。下面是一些例子,不妨分析一下其运算原理:

一些本文中用到的代码片段:

 

2 thoughts on “小代码大学问之JavaScript位运算

  1. // 奇偶判断,isOdd与isEven的功能颠倒了
    // 随机颜色 直接’#’ + (Math.random()*0xFFFFFF<<0).toString(16)就行了,前面何必要有’000000′ +

  2. Thanks for finally writing about >小代码大学问之JavaScript位运算 – 木木飞_php工程师_一枚互联网技术老兵 <Loved it!

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.