第一章 加载和执行
大多数浏览器都是用单一进程处理UI界面的刷新和JavaScript的脚本执行,所以同一时间只能做一件事,Javascript执行过程耗时越久,浏览器等待响应的时间就越长。
所以,HTML页面在遇到
无阻塞的脚本
尽管减少Javascript文件的大小并限制HTTP请求次数仅仅只是第一步,下载单个较大的Javascript脚本执行也许要锁死大量的事件,所以无阻塞的脚本的意义在于页面加载完成之后再下载脚本。
延迟的脚本
<script defer>
这是告知,延迟脚本内的内容不会更改DOM,只有IE 4+和Firefox 3.5+浏览器支持。
defer意味着脚本会先下载,但只有到DOM加载完成之前才会执行,不与页面的其他资源冲突,可并行。
动态脚本元素
XMLHttpRequest脚本注入
这种方法的优点是,可以下载Javascript代码但不立即执行,而且几乎适用所有主流浏览器。
局限性在于Javascript文件必须与所请求的页面处于相同的域,所以Javascript文件不能从CDN下载。
推荐的无阻塞模式
向页面中添加大量的JavaScript的推荐做法只需两步:先添加动态加载所需要的代码,再加载初始化页面所需要的代码。
前者代码精简,执行很快。
<script type="text/javascript">
function loadScript(url, callback) {
var script = document.createElement("script");
script.type = "text/javascript";
if (script.readyState) {
script.onreadstatechange = function() {
if (script.readyState == "loaded" || script.readyState = "complete") {
script.onreadystatechange = null;
callback();
}
}
} else {
script.onload = function() {
callback()
}
}
script.src = url;
document.getElementsByTagName("head")[0].appendChild(script);
}
loadScript("the-rest.js", function() {
Application.init();
});
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
第二章 数据存取
数据的存储位置不同,代码执行时的数据检索速度也不同。
对于Javascript来说,有下面四种基础的数据存取位置。
字面量
字面量只代表自身,不存储在特定位置。JS中的字面量有:字符串、数字、布尔值、对象、数组、函数、正则表达式,以及特殊的null和undefined值。
本地变量
开发人员使用关键字var定义的数据存储单元。
数组元素
存储在JS数组对象内部,以数字作为索引。
对象成员
存储在JS对象内部,以字符串作为索引。
管理作用域
作用域概念对于理解Javascript至关重要,不仅在性能方面,还在功能方面。
作用域链和标识解析
每个函数都是Function的实例,Function对象与其他对象一样,拥有可以编写的属性以及,一系列只供JavaScript引擎存储的内部属性,其中一个是[[scope]]。
[[scope]]包含了一个函数被创建的作用域中对象的集合。
在函数执行过程中,每遇到一个变量,都会经历一次标识符解析过程以及从哪里获取存储数据。如果当前执行环境(作用域头)找不到该变量,就搜索下一个作用域,如果都找不到则为undefined。
标识符解析的性能
标识符所在的位置越深,读写速度也就越慢。所以,读取局部变量时最快的,而读取全局变量时最慢的。
对此有个经验法则:如果某个跨作用域的值在函数中被引用一次以上,那么就把它存储到局部变量里。
改变作用域链
一般来说,一个执行环境的作用域链是不会改变的。但是,有两个语句可以在执行时临时改变作用域链。
一个是with语句,另一个是try-catch语句。
with(context),with语句有一个问题,那就是with()里的参数作为作用域链头后,函数的局部变量都会变成第二个作用域链对象中,这样每次访问都得访问两次。
try-catch的catch子句也有这种效果,它把一个异常对象推到作用域链首部,但是子句执行完毕,作用域链就会返回之前的状态。
动态作用域
with、try-catch和eval()都是动态作用域。
闭包、作用域和内存
闭包是JS最强大的特性之一,它允许函数访问局部作用于之外的数据。
通常执行环境一销毁,活动对象也应该销毁,但是因为闭包中,对活动对象的引用依旧存在,所以活动对象并不会被销毁,因此也需要更高的内存开销。
对象成员
原型
JS对象基于原型,实例属性proto指向原型对象且只对开发者可见。
hasOwnProperty()区分原型属性和实例属性。
原型链
不多解释。
嵌套成员
不太常见的写法:window.location.href,嵌套成员会导致JS引擎搜索所有对象成员。嵌套得越深,读取速度就会越慢。
缓存对象成员值
不多解释
小结
在JS中,数据存储的位置会对代码整体性能产生重大的影响。数据存储有4种方法:字面量、变量、数组项和对象成员。它们有着各自的性能特点。
- 访问字面量和局部变量的速度最快,相反,访问数组元素和对象成员相对较慢。
- 访问局部变量比访问跨作用域变量更快。变量在作用域链中的位置越深,访问时间越长。
- 避免使用动态作用域链
- 嵌套的对象成员会明显影响性能,少用
- 属性或方法在原型链中位置越深,访问速度越慢
- 可把常用元素保存在局部变量中改善性能
第三章 DOM编程
首先必须先明确一点:用脚本进行DOM操作的代价非常昂贵。
DOM相当于浏览器HTML文档,XML文档与JS的程序接口(API),与语言无关。所以DOM与JS之间的交流消耗费用也就越高。
DOM访问与修改
通用的经验法则:减少访问DOM的次数,把运算尽量留在ECMAScript这端来处理。
innerHTML与DOM方法的对比
innerHTML非标准但是支持性良好,在老浏览器中innerHTML比DOM更加高效,但是innerHTML最好与数组结合起来使用。这样效率会更高。
节点克隆
element.cloneNode()
方法克隆节点。
HTML集合
HTML集合是包含了DOM节点引用的类数组对象。以下方法的返回值就是一个集合。
类数组对象没有push.slice等方法,但是有length属性且可遍历。
HTML集合与文档时刻保持连接,所以需要最新的消息需要时刻查询。
昂贵的集合
在循环语句中读取数组的length是不推荐的做法,最好是把数组的长度存储在一个局部变量中。
访问集合时使用局部变量
第一优化原则是把集合存储在局部变量中,并把length缓存在循环外部,然后用局部变量替代这些需要多次读取的元素。
举个例子:
// 较慢
function collectionGlobal() {
var coll = document.getElementsByTagName('div'),
len = coll.length,
name = '';
for (var count = 0; count < len; count++) {
name = document.getElementsByTagName('div')[count].nodeName;
name = document.getElementsByTagName('div')[count].nodeType
}
}
// 很快
function collectionGlobal() {
var coll = document.getElementsByTagName('div'),
len = coll.length,
name = '',
el = null;
for (var count = 0; count < len; count++) {
el = coll[count];
name = el.nodeName;
name = el.nodeType
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
遍历DOM
获取DOM元素
通常你需要从某一个DOM元素开始,操作周围的元素,或者递归查找所有子节点。你可以使用childNodes得到元素集合,或者用nextSibling来获取每个相邻元素。
元素节点
DOM元素属性诸如childNodes,firstChild和nextSibling并不区分元素节点和其他类型节点,如果要过滤的话,其实是不必要的DOM操作。
现在能区分元素节点和其他节点的DOM属性如下:
属性名 | 被替代的属性 |
---|---|
children | childNodes |
childElementCount | childNodes.length |
firstElementChild | firstChild |
lastElementChild | lastChild |
nextElementSibling | nextSibling |
previousElementSibling | previousSibling |
children属性的支持率较高。
选择器API
document.querySelectorAll(‘#menu a’);
document.querySelector(”) 选择匹配的第一个节点
重绘与重排(Repaints and Reflows)
浏览器下载完页面中的所有组件,之后会解析并生成两个内部数据结构:
DOM树
表示页面结构
渲染树
表示DOM节点如何显示
DOM树中所有需要显示的节点在渲染树中至少存在一个对应的节点(隐藏的DOM元素在渲染树中没有对应的节点)。
一旦DOM树与渲染树构建完成,浏览器就开始绘制页面元素。
重排
当DOM的变化影响了元素的几何属性(宽和高)——比如改变边框宽度或给段落增加文字,导致行数增加——浏览器需要重新计算元素的几何属性,同样其他元素的集合属性位置也会因此受到影响。浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树,这个过程被称为重排。
重绘
完成重排后,浏览器会重新绘制受影响的部分到屏幕中,这个过程被称为重绘。有些样式的改变,比如背景的变化不影响到布局,所以只会重绘。
渲染树变化的排队和刷新
由于每次重排都会产生计算消耗,大多数浏览器通过队列化修改并批量执行来优化重排过程。然而,你可能会不自觉得强制刷新队列并要求计划任务立刻执行。获取布局信息的操作会导致列队刷新,比如:
- offsetTop,offsetLeft…
- scrollTop,scrollLeft…
- clientTop,clientLeft…
- getComputedStyle()
因为以上方法要求返回最新的布局信息,所以浏览器不得不把“待处理变化”触发重排。
最小化重绘和重排
el.style.cssText = "";
修改样式信息。- 修改css的class名称
el.className = "";
批量修改DOM
当你需要对DOM元素进行一系列操作时,可以通过以下步骤来减少重绘和重排的次数:
1.使元素脱离文档流
2.对其应用多重改变
3.把元素带回文档
有三种方法可以使DOM脱离文档:
- 隐藏元素,应用修改,重新显示
- 使用文档片段在当前DOM之外构建一个子树,再把它拷贝回文档
- 将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素
举个例子:
<ul id="mylist">
<li><a></a></li>
<li><a></a></li>
<li><a></a></li>
</ul>
// 假设要将更多的数据插入到这个列表中
var data = [
{
"name": "Nicholas",
"url": "...."
}
]
// 用来更新指定节点数据的通用函数
function appendDataToElement(appendToElement, data) {
var a, li;
for (var i = 0, max = data.length; i < max; i++) {
a = document.createElement('a');
a.href = data[i].url;
a.appendChild(document.createTextNode(data[i].name));
li = document.createElement('li');
li.appendChild(a);
appendToElement.appendChild(li);
}
}
// 不考虑重排
var ul = document.getElementById('mylist');
appendDataToElement(ul, data);
// 第一种方法
var ul = document.getElementById('mylist');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';
// 第二种方法(推荐)
var fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
document.getElementById('mylist').appendChild(fragment);
// 第三种方法
var old = document.getElementById('mylist');
var clone = old.cloneNode(true);
appendDataToElement(clone, data);
old.parentNode.replaceChild(clone, old);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
缓存布局信息
对于需要操作布局信息的地方,最好用一个局部变量来缓存,不然查询一次就会刷新一次渲染队列并应用所有变更。
让元素脱离动画流
一般而言动画的展开与隐藏会影响大量元素的重排,使用一下步骤可以避免页面中大部分重排。
- 使用绝对位置定位页面上的动画元素,使其脱离文档流。
- 让元素动起来。当它扩大时,会临时覆盖部分页面,但这只是页面一个小区域的重绘。
- 当动画结束时恢复定位,从而只会下移一次文档的其他元素。
IE和:hover
尽量避免使用hover。
事件委托
绑定的事件越多,代价也越大,要么加重了页面负担,要么是增加了运行期的执行时间。
在父元素绑定个事件,利用冒泡。
小结
访问和操作DOM是现代Web应用的重要部分。但每次穿越连接ECMAScript和DOM两个岛屿之间的桥梁,都会被收取“过桥费”。
- 最小化DOM访问次数,尽可能在JavaScript端处理。
- 如果需要多次访问某个DOM节点,请使用局部变量存储它的引用。
- 小心处理HTML集合,因为它实时连接着底层文档。把集合的长度缓存到一个变量中,并在迭代中使用它,如果需要经常操作集合,建议把它拷贝到一个数组中。
- 如果可能的话,使用速度更快的API
- 要留意重绘和重排,批量修改样式时,“离线”操作DOM树,使用缓存,减少访问布局信息的次数
- 动画中使用绝对定位,使用拖放代理
- 使用事件委托来减少事件处理器的数量
第四章 算法和流程控制
代码的数量不是影响代码运行速度的必然因素,代码的组织结构和解决具体问题的思路才是。
循环
大多数编程语言中,代码执行时间都消耗在循环中。
循环的类型
for循环
while循环
do-while循环
for-in循环
循环性能
不断引发循环性能争论的源头是循环类型的选择。在JS提供的四种循环类型中,只有for-in明显比其他的慢。
优化循环的第一步就是减少对象成员和数组项的查询次数。比如把数组长度赋值给一个局部变量。
第二步是颠倒数组的顺序,同样可以提升循环性能。
第三步是减少迭代次数,达夫设备。
达夫设备实际上是把一次迭代操作展开成多次迭代操作。
思路是,每次循环中最多可调用8次process(),如果有余数,则表示第一次process()执行几次。
var iterations = Math.floor(items.length / 8),
startAt = items.length % 8,
i = 0;
do {
switch(startAt) {
case 0: process(items[i++]);
...
...
case 8: process(items[i++]);
}
startAt = 0;
} while (--iterations);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
达夫设备对于1000次以上的循环有很大的提升。
基于函数的迭代
Array.forEach(function(value, index, array){
process(value);
})
- 1
- 2
- 3
条件语句
if-else对比switch
条件数量少时用if-else,多时用switch。
优化if-else
目标:最小化到达正确分之前所需判断的条件数量。
最简单的优化方法是确保最可能出现的条件放在首位。
还有一种是增加if-else的嵌套,尽可能减少判断次数。
查找表
当有大量离散值需要测试时,或者条件语句数量很大时,JS可以通过数组和普通对象来构建查找表,速度要快得多。
switch(value) {
case 0:
return result0;
case 1:
return result1;
}
var results = [results0, results1]
return results[index]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
递归
阶乘就是用递归实现的,但是递归的问题在于终止条件不明确或缺少终止条件会导致函数长时间运行,而且可能会遇到浏览器的“调用栈大小限制”。
调用栈限制
浏览器有栈限制。
递归模式
有两种递归模式。直接递归和隐伏模式。
迭代
任何递归能实现的算法同样可以用迭代来实现。
Memoization
把计算结果缓存,运行前先判断。
functiom memfactorial(n) {
if (!memfactorial.cache) {
memfactorial.cache = {
"0": 1,
"1": 1
}
}
if (!memfactorial.cache.hasOwnProperty(n)) {
memfactorial.cache[n] = n * memfactorial(n-1)
}
return memfactorial.cache[n];
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
小结
- for/while/do-while循环性能特性相当,并没有一种循环类型明显快于或慢于其他类型。
- 避免使用for-in循环,除非你需要遍历一个属性数量未知的对象
- 改善循环性能的最佳方式是减少每次迭代的运算量和减少循环迭代次数。
- 通常来说,switch总是比if-else快,但并不总是最佳决绝方案。
- 再判断条件较多时,使用查找表比if-else和switch快
- 浏览器的调用栈大小限制了递归算法在JavaScript中的应用;栈溢出错误会导致其他代码中断运行。
- 如果你遇到栈溢出错误,可将方法改为迭代算法,或使用Memoization来避免重复计算。
运行的代码量数量越大,使用这些策略所带来的性能提升也就越明显。
第六章 快速响应的用户界面
浏览器UI线程
UI线程把一个个JS或者UI渲染任务放到队列中逐个执行,最理想的情况就是队列为空,这样任务可以即刻执行。
浏览器限制
浏览器对JS任务的执行时间有限制,从栈大小限制和运行时间两方面来限制。
多久才算“太久”
“如果JS运行了整整几秒钟,那么很可能是你做错了什么….”
单个JS文件的操作总时间不能超过100毫秒。
使用定时器让出事件片段
如果100毫秒内不能解决JS任务,那么就把线程让出来执行UI渲染。
定时器基础
setTimeout()和setInterval()会告诉JS引擎等待一段时间,然后添加一个JS任务到UI队列。
定时器的精度
定时器不可用于测量实际时间,有几毫秒的偏差。
使用定时器处理数组
如果第四章的循环优化还是没有将JS任务缩短到100毫秒以内,那么下一步的优化步骤就是定时器。
分割任务
可以的话,将一个大任务分割成无数小任务。
记录代码运行时间
不超过50毫秒的JS任务是非常好的用户体验,但是有时候一次性只执行一个任务,这样执行效率反而不高。
定时器与性能
多个重复的定时器同时创建往往会出现性能问题。间隔在1s或者1s以上的重复定时器不会影响Web应用得响应速度。
Web Workers
Web Workers是HTML5最初的一部分,它的出现意味着JS任务可以单独分离出去而不占用浏览器的UI渲染。
Worker运行环境
Web Workers不能处理UI进程,这就意味着它不能接触很多浏览器的资源。
Web Workers的运行环境由如下部分组成:
- navigator对象,包括appName、appVersion、user Agent和platform。
- 一个location对象,(与window.location同,但是只读)
- 一个self对象,指向全局worker对象。
- 一个importScripts()方法,用来加载Worker所用到外部JS文件
- 所有的ECMAScript对象
- XMLHttpRequest构造器
- setTimeout()和setInterval()方法
-一个close()方法,它能立刻停止Worker运行
var worker = new Worker("code.js")
- 1
与Worker通信
Worker与网页代码通过事件接口进行通信,网页代码通过postMessage()方法给Worker传递数据,它接收一个参数,即需要传给Worker的数据。此外,Worker还有一个用来接收信息的onmessage事件处理器。
var worker = new Worker('code.js')
worker.onmessage = function(event) {
process(event.data)
}
worker.postMessage('data')
//code.js
self.onmessage = function(event) {
self.postMessage("Hello" + event.data)
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
加载外部文件
importScripts()阻塞
- 1
实际应用
var worker = new Worker("jsonparser.js");
worker.onmessage = function(event) {
var jsonData = event.data
evaluateData(jsonData)
}
worker.postMessage(jsonText)
self.onmessage = function(event) {
var jsonText = event.data
var jsonData = JSON.parse(jsonText)
self.postMessage(jsonData)
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
小结
- 任何JavaScript任务都不应当执行超过100毫秒,过长的运行时间会导致UI更新出现明显的延迟,从而对用户体验产生负面影响。
- 定时器可以用来安排代码延迟执行,把一个大任务分割成多个小任务,不影响UI渲染
- Web Worker是新版浏览器支持的特性
第七章 Ajax
Ajax可以通过延迟和异步加载大资源。
数据传输
Ajax从最基本的层面来说,是一种与服务器通信而无需重载页面的方法,数据可以从服务器获取或发送给服务器。
请求数据
有五种常用技术用于向服务器请求数据:
- XMLHttpRequest(XHR)
- Dynamic script tag insertion 动态脚本注入
- iframes
- Comet
- Multipart XHR
XHR
var url = '/data.php';
var params = [
'id=934875',
'limit=20'
];
var req = new XMLHttpRequest();
req.onreadystatechange = function() {
if (req.readystate === 4) {
}
}
req.open('get', url + '?' + params.join('&'), true)
req.setRequestHeader('X-Requested-with', 'XMLHttpRequst');
req.send(null)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
XHR的get请求是幂等行为,即一次请求和多次请求并不会有副作用。
动态脚本注入
这个与XHR不同的地方在于它不用在意跨域问题。
var scriptElement = document.createElement('script');
scriptElement.src = '';
document.getElementsByTagName('head')[0].appendChild(scriptElement)
- 1
- 2
- 3
- 4
- 5
不能设置头信息,参数传递也只能用GET,不能设置请求的超时处理。
因为响应消息作为脚本标签的源码,它必须是可执行的JS代码。
Multipart XHR
MXHR允许客户端只用一个HTTP请求就可以从服务端向客户端传送多个资源。它通过在服务端将资源打包成一个由双方约定的字符串分割的长字符串并发送到客户端,然后用JS代码处理这个长字符串,并根据它的mime-type类型和传入的其他“头信息”解析出每个资源。
发送数据
数据格式
唯一需要比较的标准就是速度。
XML
需要解析结构,才能读取值。
JSON
JSON-P JSON填充,在动态脚本注入时必须放在回调函数里,不然就会被当做另外一个JS文件执行(所以也要尤其注意脚本攻击。)
HTML
Ajax性能指南
缓存数据
最快的Ajax请求就是没有请求。有两种主要的方法可以避免发送不必要的请求:
- 在服务端,设置HTTP头信息以确保响应会被浏览器缓存
- 在客户端,把获取到的信息存储到本地,从而避免再次请求。
设置HTTP头信息
如果你希望Ajax响应能够被浏览器缓存,那么你必须使用GET方式发出请求。但这还不够,你还必须在相应中发送正确的HTTP头信息。
Expires头信息会告诉浏览器应该缓存响应多久,它的值是一个日期,过期之后,对该URL的任何请求都不再从缓存中获取,而是会重新访问服务器。
本地数据存储
可以把响应文本保存到一个对象中,以URL为键值做索引。
var localCache = {};
function xhrRequest(url, callback) {
// 检查此URL的本地缓存
if (localCache[url]) {
callback.success(localCache[url]);
return;
}
// 此URL对应的缓存没有找到,则发送请求
var req = createXhrObject();
req.onerror = function() {
callback.error();
}
req.onreadystatechange = function() {
...
localCache[url] = req.responseText;
callback.success(req.responseText)
}
req.open("GET", url, true)
req.send(null)
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
了解Ajax类库的局限
浏览器之间有些差异,不过大多数类库都有封装。
小结
- 减少请求数,可通过合并JavaScript和CSS文件,或使用MXHR
- 缩短页面的加载时间,页面主要内容加载完成后,用Ajax获取次要文件
- 代码错误不会输出给用户
- 知道何时使用类库,何时编写自己的底层Ajax代码
第八章 编程实践
避免双重求值(Double Evaluation)
JS和其他很多脚本语言一样,允许你在程序中提取一个包含代码的字符串,然后动态执行。
有四种标准方法可以实现:eval(),Function(),setTimeout(),setInterval()。
在JS代码中执行另外一段JS代码就会造成双重求值,所以eval和Function不推荐使用,setTimeout和setInterval的第一个参数最好是回调函数。
使用Object/Array 直接量
使用直接量创建Object/Array
避免重复工作
延迟加载
当一个函数在页面中不会被立即调用时,延迟加载时最好的选择,即如果需要针对不同的浏览器写不同的代码,在第一次就判断用的是哪种,然后在内部重写函数。
条件预加载
var addHandler = document.body.addEventListener ? function1 : function2
- 1
使用速度快的部分
位操作
原生方法
小结
- 通过避免使用eval()和Function()构造器来避免双重求值带来的性能消耗。同样的,给setTimeout()和setInterval()传递函数而不是字符串作为参数。
- 尽量使用直接量创建对象和数组
- 避免做重复的工作。当需要检测浏览器时,可以使用延迟加载或条件预加载。
- 进行数学计算时,考虑使用直接操作数字的二进制形式的位运算。
- 尽量使用原生方法