JS高程:读书摘要(二十)终章
一、可维护性
松散耦合
- 解耦
HTML/JavaScript
HTML
是数据,JavaScript
是行为。因为它们天生就需要交互,所以有多种不同的方法将这两个技术关联起来,一般来说,你应该避免在JavaScript
中创建大量HTML
,也避免在HTML
使用JS
字符串处理事件。
将HTML
和JavaScript
解耦可以在调试过程中节省时间,更加容易确定错误的来源,也减轻维护的难度:更改行为只需要在JavaScript
文件中进行,而更改标记则只要在渲染文件中。
- 解耦
CSS/JavaScript
由于CSS
负责页面的显示,当显示出现任何问题时都应该只是查看CSS
文件来解决。然而,当使用了JavaScript
来更改某些样式的时候,就出现了第二个可能已更改和必须检查的地方。结果是JavaScript
也在某种程度上负责了页面的显示,并与CSS
紧密耦合。
通过只修改某个元素的CSS
类,就可以让大部分样式信息严格保留在CSS
中。JavaScript
可以更改样式类,但并不会直接影响到元素的样式。只要应用了正确的类,那么任何显示问题都可以直接追溯到CSS
而非JavaScripts
。
- 解耦应用逻辑/事件处理程序
在事件处理程序中包含大量应用逻辑的弊病就是,如果没有发生预想的结果怎么办?是不是表示事件处理程序没有被调用还是指应用逻辑失败?其次,如果有操作需要同样的应用逻辑,那就必须复制功能代码或者将代码抽取到一个单独的函数中。
解耦需要注意的是
- 勿将
event
对象传给其他方法;只传来自event
对象中所需的数据; - 任何可以在应用层面的动作都应该可以在不执行任何事件处理程序的情况下进行;
- 任何事件处理程序都应该处理事件,然后将处理转交给应用逻辑。
编程实践
-
- 尊重对象所有权
- 不要为实例或原型添加属性;
- 不要为实例或原型添加方法;
- 不要重定义已存在的方法。
- 避免全局量
-
3.避免与
null
进行比较
因为排除null
并不代表数据类型的确定,例:期望得到数组却得到了字符串,通过了与null
相比较的验证,但依然可能出现错误。如果看到了与null
比较的代码,尝试使用以下技术替换:
- 如果值应为一个引用类型,使用
instanceof
操作符检查其构造函数; - 如果值应为一个基本类型,使用
typeof
检查其类型; - 如果是希望对象包含某个特定的方法名,则使用
typeof
操作符确保指定名字的方法存在于对象上。
- 使用常量
- 重复值——任何在多处用到的值都应抽取为一个常量。这就限制了当一个值变了而另一个没变的时候会造成的错误。这也包含了
CSS
类名。 - 用户界面字符串 —— 任何用于显示给用户的字符串,都应被抽取出来以方便国际化。
-
URLs
—— 在Web
应用中,资源位置很容易变更,所以推荐用一个公共地方存放所有的URL
。
二、性能
注意作用域
- 避免全局查找
随着作用域链中的作用域数量的增加,访问当前作用域以外的变量的时间也在增加。访问全局变量总是要比访问局部变量慢,因为需要遍历作用域链,在一个函数中将用到多次的全局对象存储为局部变量。
- 避免
with
语句
with
语句会创建自己的作用域,增加了其中执行的代码作用域链长度,况且with
主要用来消除额外的字符串,使用局部变量可以完成相同的事情。
选择正确的方法
- 避免不必要的属性查找
算法类型:O(1)、O(log n)、O(n)、O(n²)
常量值查找和访问数组元素都是O(1)
操作,而对象上的任何属性查找是O(n)
操作,要比访问变量或者数组花费更长时间,因为必须在原型链中对拥有该名称的属性进行一次搜索。简而言之,属性查找越多,执行时间就越长。
一旦多次用到对象属性,应该将其存储在局部变量中。第一次访问该值会是O(n)
,然而后续的访问都会是O(1)
,就会节省很多。
-
优化循环
- 简化终止条件:由于每次循环过程都会计算终止条件,所以必须保证它尽可能快。例:存储
length
- 简化循环体:循环体是执行最多的,所以要确保其被最大限度地优化。
- 使用后测试循环:如
do-while
这种后测试循环,可以避免最初终止条件的计算,因此运行更快。
- 简化终止条件:由于每次循环过程都会计算终止条件,所以必须保证它尽可能快。例:存储
-
展开循环
如果需要遍历的对象的项是规定且数量不大时,展开循环可以消除建立循环和处理终止条件的额外开销,使代码运行得更快。
Duff
装置技术,以8
次为限进行循环展开。
var iterations = Math.floor(values.length / 8);
var leftover = values.length % 8; // 余数
var i = 0;
if (leftover > 0){ // 在这里处理多余出来的项
do {
process(values[i++]); // i++ 在当行不会加1 当行执行完才加1
} while (--leftover > 0);
}
do {
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
} while (--iterations > 0);// 用整除数 来决定进行8次循环的次数
- 避免双重解释 :
//某些代码求值——避免!!
eval("alert('Hello world!')");
//创建新函数——避免!!
var sayHi = new Function("alert('Hello world!')");
//设置超时——避免!!
setTimeout("alert('Hello world!')", 500);
-
原生方法较快——只要有可能,使用原生方法而不是自己用
JavaScript
重写一个。原生方法是用诸如C/C++
之类的编译型语言写出来的,所以要比JavaScript
的快很多很多。 -
Switch
语句较快 —— 如果有一系列复杂的if-else
语句,可以转换成单个switch
语句则可以得到更快的代码。还可以通过将case
语句按照最可能的到最不可能的顺序进行组织,来进一步优化switch
语句。 -
位运算符较快 —— 当进行数学运算的时候,位运算操作要比任何布尔运算或者算数运算快。
最小化语句数
- 多个变量声明使用一个语句
- 插入迭代值:
var name = values[i++];
- 使用数组和对象字面量
优化DOM交互
- 使用文档片段,尽量减少
DOM
操作,避免回流次数过多。
var list = document.getElementById("myList"),
fragment = document.createDocumentFragment(),
item,
i;
for (i=0; i < 10; i++) {
item = document.createElement("li");
fragment.appendChild(item);
item.appendChild(document.createTextNode("Item " + i));
}
list.appendChild(fragment);
或者使用字符串拼接 + innerHTML
- 使用事件代理,减少事件处理程序的数量。
- 注意
HTMLCollection
任何时候要访问HTMLCollection
,不管它是一个属性还是一个方法,都是在文档上进行一个查询,这个查询开销很昂贵。最小化访问HTMLCollection
的次数可以极大地改进脚本的性能。
1、 进行了对getElementsByTagName()
的调用;
2、 获取了元素的 childNodes
属性;
3、 获取了元素的 attributes
属性;
4、 访问了特殊的集合,如document.forms
、document.images
等。
以上方法都都会返回HTMLCollection
对象,应该使用变量来存储遍历元素集合时中的元素,避免过多直接访问元素集合的变量。
var images = document.getElementsByTagName("img"), // HTMLCollection对象
image,
i, len;
for (i=0, len=images.length; i < len; i++){
// 取出变量的对象 避免之后的循环体过多的直接方法HTMLCollection对象变量
image = images[i];
//处理
}
三、新兴API
Page Visibility API
-
document.hidden
:表示页面是否隐藏的布尔值。页面隐藏包括页面在后台标签页中或者浏览器最小化。 -
document.visibilityState
:各浏览器实现不同,代表页面状态的值。 -
visibilitychange
事件:当文档从可见变为不可见或从不可见变为可见时,触发该事件。
Geolocation API 地理定位
1、navigator.geolocation
对象
- 1.1
getCurrentPosition()
方法
会触发请求用户共享地理定位信息的对话框。接受三个参数,成功回调,失败回调【可选】,选项对象【可选】。
成功回调接受一个Position
对象参数,拥有coords
和timestamp
两个属性。
cords
是一个对象,包含主要信息。
-
latitude
:以十进制度数表示的纬度。 -
longitude
:以十进制度数表示的经度。 -
accuracy
:经、纬度坐标的精度,以米为单位。 -
altitude
:以米为单位的海拔高度,如果没有相关数据则值为null
。 -
altitudeAccuracy
:海拔高度的精度,以米为单位,数值越大越不精确。 -
heading
:指南针的方向,0°
表示正北,值为NaN
表示没有检测到数据。 -
speed
:速度,即每秒移动多少米,如果没有相关数据则值为null
。
失败回调接受一个错误信息对象,拥有code
和message
两个属性,code
表示错误的类型:用户拒绝共享(1)、位置无效(2)或者超时(3)。message
保存着错误文本信息。
选项对象:
-
enableHighAccuracy
是一个布尔值,表示必须尽可能使用最准确的位置信息。 -
timeout
是以毫秒数表示的等待位置信息的最长时间; -
maximumAge
表示上一次取得的坐标信息的有效时间,以毫秒表示,如果时间到则重新取得新坐标信息
除非确实需要非常精确的信息,否则建议保持enableHighAccuracy
的false
值(默认值)。将这个选项设置为true
需要更长的时候,而且在移动设备上还会导致消耗更多电量。类似地,如果不需要频繁更新用户的位置信息,那么可以将maximumAge
设置为Infinity
,从而始终都使用上一次的坐标信息。
- 1.2
watchPosition()
方法
用于跟踪用户的位置,接受的参数与getCurrentPosition()
方法完全相同,可以使用定时调用getCurrentPosition()
方法来替代。
调用watchPosition()
会返回一个数值标识符,用于跟踪监控的操作。基于这个返回值可以取消监控操作,只要将其传递给clearWatch()
方法即可(与使用setTimeout()
和clearTimeout()
类似)。
File API
File 对象
通过选择文件输入框选择一个或多个文件时,files
集合中包含的就是一组File
对象。
有下列只读属性:
-
name
:本地文件系统中的文件名。 -
size
:文件的字节大小。 -
type
:字符串,文件的MIME
类型。 -
lastModifiedDate
:字符串,文件上一次被修改的时间(只有Chrome
实现了这个属性)。
FileReader
类型 : 实现一种异步文件读取机制。
为了读取文件,提供了以下方法
-
readAsText(file,encoding)
:以纯文本形式读取文件,将读取到的文本保存在result
属性中。第二个参数用于指定编码类型,是可选的。 -
readAsDataURL(file)
:读取文件并将文件以数据URI
的形式保存在result
属性中。 -
readAsBinaryString(file)
:读取文件并将一个字符串保存在result
属性中,字符串中的每个字符表示一字节。 -
readAsArrayBuffer(file)
:读取文件并将一个包含文件内容的ArrayBuffer
保存在result
属性中。
事件:progress
、error
和load
,分别表示是否又读取了新数据、是否发生了错误以及是否已经读完了整个文件。
progress
事件的事件对象,包含lengthComputable
(进度信息是否可用的布尔值)、loaded
(已经加载的字节数)和total
(总字节数)三个属性。
读取错误触发error
事件,相关信息保存在FileReader
的error
属性中,1 表示未找到文件,2 表示安全性错误,3 表示读取中断,4 表示文件不可读,5 表示编码错误。
文件成功加载后会触发load
事件;如果发生了error
事件,就不会发生load
事件。
读取部分内容
这个方法在Firefox
中的实现叫mozSlice()
,在Chrome
中的实现叫webkitSlice()
,Safari
的5.1
及之前版本不支持这个方法。slice()
方法接收两个参数:起始字节及要读取的字节数。这个方法返回一个Blob
的实例,Blob
是File
类型的父类型。
对象URL
对象URL
也被称为blob URL
,指的是引用保存在File
或Blob
中数据的URL
。使用对象URL
的好处是可以不必把文件内容读取到JavaScript
中而直接使用文件内容。为此,只要在需要文件内容的地方提供对象URL
即可。要创建对象URL
,可以使用window.URL.createObjectURL()
方法,并传入File
或Blob
对象。
let blob = new Blob([response.data], {type: 'application/x-xls'}) // 后台返回的数据response.data
let link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = 'xx模板.xls' // 文件名
link.click() // 立即下载文件
var fileList = document.getElementById('fileInput')
fileList.onchange = function(e){
var files = e.files
var url = createObjectURL(files[0]);
if (url){
if (/image/.test(files[0].type)){
output.innerHTML = "<img src=\"" + url + "\">";
} else {
output.innerHTML = "Not an image.";
}
} else {
output.innerHTML = "Your browser doesn't support object URLs.";
}
}
读取拖放的文件
从桌面上把文件拖放到浏览器中也会触发drop
事件。而且可以在event.dataTransfer. files
中读取到被放置的文件,当然此时它是一个File
对象,与通过文件输入字段取得的File
对象一样。但是必须取消dragenter
、dragover
和drop
的默认行为。在drop
事件中,可以通过event.dataTransfer.files
读取文件信息。
使用XHR上传文件
首先,要创建一个FormData
对象,通过它调用append()
方法并传入相应的File
对象作为参数。然后,再把FormData
对象传递给XHR
的send()
方法,结果与通过表单上传一模一样。
if (event.type == "drop"){
data = new FormData();
files = event.dataTransfer.files;
i = 0;
len = files.length;
while (i < len){
data.append("file" + i, files[i]);
//第一个参数需要与后台一致,不然后台读不到
i++;
}
data.append("otherParams" , "otherValue");// 也可以同时传其他参数
xhr = new XMLHttpRequest();
xhr.open("post", "FileAPIExample06Upload.php", true);
xhr.onreadystatechange = function(){
if (xhr.readyState == 4){
alert(xhr.responseText);
}
};
xhr.send(data);
}
Web 计时
Web
计时机制的核心是window.performance
对象。对页面的所有度量信息,包括那些规范中已经定义的和将来才能确定的,都包含在这个对象里面。Web Timing
规范一开始就为performance
对象定义了两个属性。
-
performance.navigation
属性包含的信息:
redirectCount
:页面加载前的重定向次数。
type
:数值常量,表示刚刚发生的导航类型。 0代表页面第一次加载,1代表页面重载过,2页面是通过“后退”或“前进”按钮打开的。
-
performance.timing
属性包含的信息:-
navigationStart
:开始导航到当前页面的时间。 -
unloadEventStart
:前一个页面的unload
事件开始的时间。但只有在前一个页面与当前页面来自同一个域时这个属性才会有值;否则,值为0。 -
unloadEventEnd
:前一个页面的unload 事件结束的时间。但只有在前一个页面与当前页面来自同一个域时这个属性才会有值;否则,值为0。 -
redirectStart
:到当前页面的重定向开始的时间。但只有在重定向的页面来自同一个域时这个属性才会有值;否则,值为0。 -
redirectEnd
:到当前页面的重定向结束的时间。但只有在重定向的页面来自同一个域时这个属性才会有值;否则,值为0
。 -
fetchStart
:开始通过HTTP GET
取得页面的时间。 -
domainLookupStart
:开始查询当前页面DNS
的时间。 -
domainLookupEnd
:查询当前页面DNS
结束的时间。 -
connectStart
:浏览器尝试连接服务器的时间。 -
connectEnd
:浏览器成功连接到服务器的时间。 -
secureConnectionStart
:浏览器尝试以SSL 方式连接服务器的时间。不使用SSL 方式连接时,这个属性的值为0。 -
requestStart
:浏览器开始请求页面的时间。 -
responseStart
:浏览器接收到页面第一字节的时间。 -
responseEnd
:浏览器接收到页面所有内容的时间。 -
domLoading
:document.readyState
变为"loading"
的时间。 -
domInteractive
:document.readyState
为"interactive"
的时间,可以开始交互了。 -
domContentLoadedEventStart
:发生DOMContentLoaded
事件的时间。 -
domContentLoadedEventEnd
:DOMContentLoaded
事件已经发生且执行完所有事件处理程序的时间。 -
domComplete
:document.readyState
变为"complete"的时间。 -
loadEventStart
:发生load
事件的时间。 -
loadEventEnd
:load
事件已经发生且执行完所有事件处理程序的时间。
-
-
DOMContentLoaded
事件:DOM
加载完成,但是有些资源不一定加载完成了,此时可以正常交互。
当文档中没有脚本时,浏览器解析完文档便能触发 DOMContentLoaded
事件;如果文档中包含脚本,则脚本会阻塞文档的解析,而脚本需要等位于脚本前面的css
加载完才能执行。在任何情况下,DOMContentLoaded
的触发不需要等待图片等其他资源加载完成。
Web Workers
通过使用Web Workers
,Web
应用程序可以在独立于主线程的后台线程中,运行一个脚本操作。这样做的好处是可以在独立线程中执行费时的处理任务,从而允许主线程(通常是UI
线程)不会因此被阻塞/放慢。
// main.js
var worker = new Worker("stufftodo.js");
//这行代码会导致浏览器下载stufftodo.js,但只有worker 接收到消息才会实际执行文件中的代码。
worker.postMessage('start! ');
// 消息内容可以是任何能够被序列化的值 发送到stufftodo.js的数据
worker.onmessage = function(event){
var data = event.data;
//对数据进行处理
}
// 接受来自stufftodo.js传回的值
worker.onerror = function(event){
console.log("ERROR: " + event.filename + " (" + event.lineno + "): " +
event.message);
};
// filename、lineno 和message,分别表示发生错误的文件名、代码行号和完整的错误消息。
worker.terminate(); //立即停止Worker 的工作
// 后续的所有过程都不会再发生(包括error 和message 事件也不会再触发)。
关于Web Worker
,最重要的是要知道它所执行的JavaScript
代码完全在另一个作用域中,与当前网页中的代码不共享作用域。在Web Worker
中,同样有一个全局对象和其他对象以及方法。但是,WebWorker
中的代码不能访问DOM
,也无法通过任何方式影响页面的外观。
在这个特殊的全局作用域中,this
和self
引用的都是worker
对象。为便于处理数据,Web Worker
本身也是一个最小化的运行环境。
- 最小化的
navigator
对象,包括onLine
、appName
、appVersion
、userAgent
和platform
属性; - 只读的
location
对象; -
setTimeout()
、setInterval()
、clearTimeout()
和clearInterval()
方法; -
XMLHttpRequest
构造函数。
// stufftodo.js
// 接受来自主进程的数据
self.onmessage = function(event){
var data = event.data;
//别忘了,默认的sort()方法只比较字符串
data.sort(function(a, b){
return a – b;
});
self.postMessage(data); // 给主进程发送计算过的数据
// 会触发main.js中的onmessage事件
};
// main.js
var data = [23,4,7,9,2,14,6,651,87,41,7798,24],
worker = new Worker("stufftodo.js");
worker.onmessage = function(event){
var data = event.data;
//对排序后的数组进行操作
};
//将数组发送给worker 排序
worker.postMessage(data);
包含其他脚本
// stufftodo.js
importScripts("file1.js", "file2.js");
// 在work中引入其他脚本
即使file2.js
先于file1.js
下载完,执行的时候仍然会按照先后顺序执行。而且,这些脚本是在Worker
的全局作用域中执行,如果脚本中包含与页面有关的JavaScript
代码,那么脚本可能无法正确运行。请记住,Worker
中的脚本一般都具有特殊的用途,不会像页面中的脚本那么功能宽泛。
web worker
可以运行异步JavaScript
代码,避免阻塞用户界面。在执行复杂计算和数据处理的时候,这个API
非常有用;要不然,这些任务轻则会占用很长时间,重则会导致用户无法与页面交互。