原文:http://www.basecss.net/article/mini-code-with-great-learing.html
这几天粗略的阅读了一下AngularJS的源码,在这个过程中发现有这么两段代码挺有意思的:
var manualLowercase = function(s) { return isString(s) ? s.replace(/[A-Z]/g, function(ch) { return String.fromCharCode(ch.charCodeAt(0) | 32); }) : s; }; var manualUppercase = function(s) { return isString(s) ? s.replace(/[a-z]/g, function(ch) { return String.fromCharCode(ch.charCodeAt(0) & ~32); }) : s; };
这两段代码用来处理字母大小写转换,由于某些国家(土耳其)使用
toLowerCase()
和toUpperCase()
不能正确的转换字母大小写,因而需要手动的处理。
为什么说这两段代码有意思?其实是觉得其中用位运算处理字母大小写的代码很巧妙,其核心代码如下:
ch.charCodeAt(0) | 32 // 大写转小写 ch.charCodeAt(0) & ~32 // 小写转大写
在分析两段代码之前,先来回顾一下JavaScript中的两个概念:整数和位运算。
从严格意义上讲,ECMAScript中有两种类型的整数:有符号的整数(正数和负数)和无符号的整数(只有正数)。而默认情况下JavaScript中的整数都是有符号的。
而在不考虑ECMAScript中数字格式存储与转换(为32位)的情况下,实际上我们操作的都是32位的整数。而对于上面提到的有符号整数而言,其中前31位(end<-start
)表示数字的值,最后1位表示符号位(0
表示正,1
表示负)。
这里提到的32位的整数在计算机底层都是使用二进制格式存储的,而这个二进制由0
和1
组成,其中每一位都有对应的十进制数字结果,整个二进制数值代表的十进制结果由所有这些位对应的十进制数字之和。
这篇文章中不考虑负数的情况,一个32位二进制格式的数字看起来如下所示,这里以10为例:
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0
二进制数字计算的方式:
number ( Math.pow(2, index))
,这里的number
表示二进制中对应位上的数值0/1,index
表示该数值在整个二进制格式的数字中的索引。注意一个二进制格式的起始点在右侧。那么上面的数字就等于:
1 * Math.pow(2,3) + 1 * Math.pow(2, 1) = 10
。
前面提到了,这些二进制的数字实际上都是在计算机的底层完成的,而ECMAScript中刚好提供了二进制运算相关的操作符,这些操作符都是直接对运算数进行二进制操作的,并且都是发生在幕后的。
JavaScript中有7个位运算相关的运算符:
~
”表示,对二进制的每一位进行取反操作,即将0
变成1
,将1
变成0
。&
”表示,必须有两个操作数,先对齐二进制位,然后把对应位都为1
的为筛下来,其他的都为0
。|
”表示,也必须有两个操作数,对齐位之后只要对应位有1就筛下来,只有同时位0时才返回0
。^
”表示,也必须有两个操作数,对齐位之后不同的返回1,相同的返回0。<<
表示,顾名思议,将操作数左移指定位数,右侧空位用0补齐。>>
”表示,保留符号位,剩下的右移指定位。>>>
”表示,往右侧移动指定位数。以上这些位运算符,最终操作的都是二进制数值。
在上面的代码中分别涉及到了按位非,按位与,按位或三种运算。先来针对上面的两段代码讲解一下这三个位运算符:
ch.charCodeAt(0) | 32
这段代码通过正则表达式匹配到给定字符串中的每个大写字母: A-Z
;接下来使用字符串对象的charCodeAt()
方法拿到该字符对应的Unicode编码,恰好这个编码是一个数字;最后使用按位或运算获取到另外一个数字。
为什么这里执行对数值32的按位或运算呢?当然这肯定不是空穴来风。那么我们先从大写字母及对应的Unicode值分析看看。不难发现,A-Z
对应的Unicode编码分别为65-90
;而这写编码对应的二进制表示分别为:1000001
… 1011010
。再看看小些字母对应的数据:其Unicode编码分别为:97-122
,对应的二进制表示分别为:1100001
…1111010
。最后将它们放入一张表格中对比如下:
提示:使用
(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中的位运算远远不止这一点,我们还可以使用其他位运算符做到很多事情。下面是一些例子,不妨分析一下其运算原理:
// 获取0-max之间随机整数 function random(max) { return Math.random() * max | 0; // 获取 1-max之间的随机整数 // return Math.random() * max | 1 } // 奇偶判断 function isOdd(number) { return (parseInt(number) & 1) === 0; } function isEven(number) { return (parseInt(number) & 1) === 1; } // 取整 function int(number) { return number | 0; } // 取半 number >> 1; // 2x number << 1; // 随机颜色 '#'+ ('000000' + (Math.random()*0xFFFFFF<<0).toString(16)).slice(-6); // 还可以挖掘更多的技巧....
一些本文中用到的代码片段:
// 获取字符Unicode编码值 str.charCodeAt(0); // 获取字符二进制数值有效位 str.charCodeAt(0).toString(2); // 解析二进制数值 parseInt(binaryNumber, 2); // 解析Unicode数值位对应的字符 String.fromCharCode(unicodeNumber);