本章内容来自 《JavaScript 高级程序设计》书,个人学习笔记总结。
最佳实践
一.可维护性
□容易理解:无须求助原始开发者,任何人一看代码就知道它是干什么的,以及它是怎么实现的。
□符合常识:代码中的一切都显得顺理成章,无论操作有多么复杂。
□容易适配:即使数据发生变化也不用完全重写。
□容易扩展:代码架构经过认真设计,支持未来扩展核心功能。
□容易调试:出问题时,代码可以给出明确的信息,通过它能直接定位问题。
->代码中变量和函数的适当命名对于可读性和可维护性至关重要,更能体现开发者的专业素养!<-
1. 变量和函数名:
1.1).变量名应该是名词,例如person和form。
1.2).函数名应该以动词开头,例如getName(),返回布尔值的函数通常以is开头,如isEnabled()。
1.3).函数、方法应该以小写字母开图,使用驼峰命名法(addHandler)命名,类名首字母则大写,如class Person,常量值应该全部大写以下划线相连,如REQUEST_TIMEOUT。
1.4).尽量用描述性和直观的词汇,但不要过于冗长。getName()一看就知道返回名称,而PersonFactory()返回产生某个Person对象或实体。
2. 变量类型透明化
2.1).第一种标明变量类型的方式是通过初始化。定义变量时,应该立即将其初始化为一个将来要使用的类型值,例如:
let found=false; //布尔值
let count=-1;//数值
let name="";//字符串
let person=null;//对象
2.2).第二种标明变量类型的方式是使用匈牙利表示法,即在变量名前面前缀加一个或多个字符表示数据类型,通常用o表示对象,s表示字符串,i表示整数,f表示浮点数,b表示布尔值。例如:
let bfound ; //布尔值
let icount;//数值
let sname;//字符串
let operson;//对象
2.3).第三种标明变量类型的方式是使用类型注释。类型注释放在变量名后面、初始化表达式前面:
let found/*:Boolean*/=false;
let count/*:int*/=-1;
let name/*String*/="";
let person/*Object*/=null;
类型注释的缺点是不能通过使用大型代码块注释掉。
3.松散藕合
应用程序的某个部分对另一个部分依赖过于紧密,代码就会变成紧密耦合,因而难以维护。典型的问题时在一个对象中直接引用另一个对象,这样修改其中一个,可能必须得修改另一个。
<!-- 使用<script>造成HTML/JavaScript紧密耦合 -->
<script>
document.write("hello,boy!");
</script>
<!-- 使用事件处理程序属性造成HTML/JavaScript紧密耦合 -->
<input type="button" value="Clik me"onclick="doSomething()">
3.1).解耦HTML/Javascript: 尽量不要将javascript直接嵌入或包含在HTML中,理想情况下HTML和Javascript应该完全分开,通过外部文件引入JavaScript。在HTML与Javascript紧密耦合的情况下,每次分析Javascript的报错都要先确定错误来自HTML还是Javascript。这样也会引入代码可用性的新错误。
还有一种情况就是把HTML包含在JavaScript中,这种情况通常发生在把一段HTML通过innerHTML插入到页面中,一般来说要尽量避免在JavaScript中创建大量HTML。
function insertMessage(mas) {
let container = document.getElementById("container");
container.innerHTML = `<div class="mas">
<p>class="post">${msg}</p>
<p><em>Latest message above.</em></p>
</div>`;
}
解耦HTML和JavaScript可以节省时间,因为更容易定位错误来源。同样解耦也有助于保证可维护下。修改行为只涉及JavaScript,修改标记只涉及要渲染的文件。
3.2).解耦CSS/JavaScript:
Css和JavaScript也会产生紧密耦合,最常见的例子就是使用JavaScript修改个别样式:
element.style.color = "red";
element.style.backgroundColor = "bule"
因为CSS负责显示页面,所有任何样式问题都应该通过css文件解决,可如果使用JavaScript直接修改个别样式,就会增加一个排错时要考虑甚至要修改的因数。后续可能带来的将是一场噩梦! 现代web应用程序经常使用JavaScript改变样式,因此虽然不太可能完全解耦CSS和JavaScript,但可以让其变得松散,这主要是通过动态修改‘类名’而不是样式来实现:
element.className="edit";
通过修改元素的CSS类名,可以把大部分样式限制在CSS文件里,JavaScript只负责修改应用样式的类名,而不直接影响元素的样式,只有应用的类名没错,那么显示的问题就在跟CSS有关,而跟JavaScript无关。行为出问题就应该只找JavaScript的问题,从而提升整个应用程序的可维护性。
3.3).解耦应用程序逻辑/事件处理程序:
function handleKeyPress(event){
if(event.keyCode==13){
let target=event.target;
let value=5*parseInt(target.value);
if(value>10){
document.getElementById("error-msg").style.display="block"
}
}
}
该事件处理程序除了处理事件,还包含了应用程序逻辑,导致了双重,首先除了事件没有办法触发应用程序逻辑,结果造成调试困难。如果没有产生预期的结果怎么办?是因为没有调试事件处理程序,还是因为应用程序逻辑有误?
其次,如果后续事件也会对应相同的应用程序逻辑,则会导致代码重复,或者把它提取到单独的函数中。都导致原本不必要的多余工作。更好的做法是将应用程序逻辑与事件处理程序分开,各自负责处理各自的事情。
function validateValue(value) {
value = 5 * parseInt(value);
if (value > 10) {
document.getElementById("error-msg").style.display = "block";
}
}
function handleKeyPress(event) {
if (event.keyCode == 13) {
let target = event.target;
validateValue(target.value);
}
}
这样修改以后,应用程序逻辑跟事件处理程序就分开了, handleKeyPress(event)函数只负责检查用户是不是按下了回车键,如果是则取得事件目标,并把目标值传给validateValue(value)函数。把应用逻辑从事件处理程序中分离有很多好处,首先可以让我们以最少的工作量轻松地修改触发某些事件。如果原来通过鼠标单击触发流程,现象又想增加键盘操作触发,修改起来也方便简单。其次可以不用在添加事件的情况下测试代码, 这样创建单元测试或自动化应用流程都会很简单。
以下是在解耦应用程序逻辑和业务逻辑时应该注意的几点:
*不要吧event对象传给其他方法,而只是传递event对象中必要的数据。
**应用程序中每个可能的操作都应该无序事件处理程序就可以执行。
***事件处理程序应该处理事件,而把后续处理交给应用程序逻辑。
4.编码惯例
4.1).不声明全局变量
尽量不声明全局变量和函数,这关系到创建一致性和可维护的脚本运行环境。最多可以创建一个全局变量,作为其他对象和函数的命名空间。例如:
var name="Nicholas"; //变量一
function sayName(){ //变量二
console.log(name);
}
var MyApplication={ //一个变量
name:"Nicholas",
sayName:function(){
console.log(this.name);
}
这个重写之后的版本只声明了一个全局对象 MyApplication。该对象包含了name和sayName(),这样可以避免前面版本的问题。首先变量name会覆盖window.name属性,而这可能会影响其他功能。其次有助于分清功能都集中在哪里,调用MyApplication()会暗示出现问题都可以在其本身代码中查找原因。
全局对象可以扩展为命名空间的概念,命名空间涉及创建一个对象,通过这个对象来暴露能力。对象就相当于一个容器,其他对象都包含在里面,只要使用对象以这种方式来组织功能,就可以称该对象为命名空间。
var work={};
work.ProJS={};
work.ProJS.EventUtil={...};
work.ProJS.CookieYtil={...};
/*即使重命名也只会出现在不同的命名空间*/
work.ProAjax={};
work.ProAjax.EventUtil={...};
work.ProAjax.CookieYtil={...};
4.2).不要比较null
现实当中,单纯比较null通常是不够的,检查值的类型就要真的检查类型,而不是检查它不能是什么。如果看到null的比较代码,则应该使用下列某种技术替换它:
-如果值应该是引用类型,则使用instanceof操作符检查其他构造函数
-如果值应该是原始类型,则使用typeof检查其类型
-如果希望值是有特定方法名的对象,则使用typeof操作符确保对象上存在给定名字的方法。
4.3).使用常量:
-重复出现的值:任何使用超过一次的值都应该提取到常量中,这样可以消除一个值改了而另一个值没有修改造成的错误。
-用户界面字符串:任何会显示给用户的字符串都应该提取出来,以方便实现国际化。
-URL:web应用程序中资源的地址经常会发生变化,因此建议把所有URL集中放在一个地方管理。
-任何可能变化的值:在代码中使用字面值,应先问问自己这个值未来会不会改变,如果会就应该把它提取到常量中。
二.性能
-1.作用域意识
访问全局变量作用域始终比访问局部变量慢,因为必须遍历作用域,任何可以缩短遍历作用域时间的举措都能提升性能代码。
function updateUI(){
let imgs=document.getElementsByTagName("img");
for(let i=0,len=imgs.length;i<len;i++){
imgs[i].title='${document.title} image ${i}';
}
let msg=document.getElementById("msg");
msg.innerHTML="Update complete.";
}
来看这个函数看起来没什么问题,但其中三个地方引用了全局document对象,如果页面中图片很多,那么for循环引用document几十次甚至更多,每次都要遍历一次作用域链,通过在局部作用域中保存document对象的引用,能够 显著提升这个函数的性能,因为只需要作用域链查找。通过一个指向document对象的局部变量,可以通过全局查找数量的限制为一个来提高该函数的性能:
function updateUI(){
let doc=document;
let imgs=doc.getElementsByTagName("img");
for(let i=0,len=imgs.length;i<len;i++){
imgs[i].title='${doc.title} image ${i}';
}
let msg=doc.getElementById("msg");
msg.innerHTML="Update complete.";
}
这样调用函数只会查找一次作用域链。经验规则就是:只要函数中引用超过两次全局对象,就应该把这个对象保存为一个局部变量。
-2.在性能中,应该避免使用with语句。与函数类似,with语句会创建自己的作用域,因此也会加长其中代码的作用域链。
-3.语句最少化
一条可以执行多个操作的语句,比多条语句中每个语句执行一个操作要快。
① 多个声明变量:
let count=5;
let color="blue";
let value=[1,2,3];
let now=new Date();
使用一个let声明所有变量,中间用逗号分隔优化,比多条执行语句速度更快:
let count=5,
color="blue",
value=[1,2,3],
now=new Date();
②插入迭代性值:
let name=value[i];
i++;
尽量把迭代性值插入到一条语句上使用:
let name=value[i++];
③使用数组和对象字面量:
//创建和初始化数组用了四条语句:浪费
let values=new Array();
values[0]=132;
values[1]=456;
values[2]=789;
//创建和初始化对象用了四条语句:浪费
let Person=new Object();
Person.name="zhangmazi";
Person.age=37;
Person.sayName=function(){
console.log(this.name)
④转换为字面量形式:
//一条语句创建并初始化数组
let values=[123,456,789];
//一条语句创建并初始化对象
let person={
name:"zhangmazi",
age:37,
sayName(){
console.log(this.name);
}}
-4.优化DOM交互
实时更新最小化-访问DOM时,只要访问的部分是显示页面的一部分,就是在执行实时更新操作。来看一个例子:
let list = document.getElementById("mylist"),
item;
for (let i = 0; i < 10; i++) {
item = document.createElement("li");
list.appendChild(item);
item.appendChild(document.createTextNode('Item ${i}'));
}
以上代码向列表添加了10项,每次添加1项就有两次实时更新,一次添加li元素,一次为它添加文本节点,最后添加了20次。为了解决这里的性能问题,需要减少实时更新的次数。
使用文档片段构建DOM结构,最后一次性将它添加到list元素。可以减少实时更新,也可以避免页面闪烁:
let list = document.getElementById("mylist"),
fragment = document.createDocumentFragment(),
item;
for (let i = 0; i < 10; i++) {
item = document.createElement("li");
fragment.appendChild(item);
item.appendChild(document.createTextNode("Item" + i));
}
list.appendChild(fragment);
-5.使用innerHTML
页面中创建新DOM的节点有两种方式:使用DOM方法如createElement()和appendChild(),以及使用innerHTML。对于少量DOM二者区别不大,但对于大量DOM更新,使用innerHTML快很多。
let list=document.getElementById("mylist"),
html="";
for(let i=0;i<10;i++){
html+='<li>Item ${i}</li>';
}
list.innerHTML=html;
以上代码构造一个HTML字符串,然后将它赋值给 list.innerHTML,结果也会创建适当的DOM解构,虽然拼接字符串也会有一些性能损耗,但这个技术仍然比执行多次DOM操作速度快。
-6.使用事件委托
大多数web应用程序会大量使用事件处理程序实现用户交互。一个页面中事件处理程序的数量与页面响应用户交互的速度有直接关系。为了减少对页面响应的影响,应该尽可能使用事件委托。事件委托利用了冒泡。任何冒泡的事件都可以不在事件目标上,而在目标的任何祖先元素上,基于这个认识可以把事件处理程序添加到负责处理多个目标的高层元素上。只要可能就应该在文档添加事件处理程序,因为在文档级可以处理整个页面的事件。
-7.注意HTMLCoollection
任何时候只要访问HTMLCollection,无论是它的属性和方法,就会触发查询文档,而这个查询相当耗时,减少对其访问可以极大的提升脚本性能! 编写JavaScript代码时,只要返回HTMLCollection对象,就尽量不要访问它。以下情形会返回HTMLCollection:
*调用getElementsByTagName();
**读取元素childNodes属性;
***读取元素attributes属性;
****访问特殊合集如document.form、document.images等。
文件结构
构建流程首先定义在源代码控制中存储文件的逻辑结构。最好不要在一个文件中包含所有 JavaScript 代码。相反,要遵循面向对象编程语言的典型模式,把对象和自定义类型保存到自己独立的 文件中。这样可以让每个文件只包含最小量的代码,让后期修改更方便,也不易引入错误。此外,在使 用并发源代码控制系统(如 Git、CVS 或 Subversion)的环境中,这样可以减少合并时发生冲突的风险。 注意,把代码分散到多个文件是从可维护性而不是部署角度出发的。对于部署,应该把所有源文件 合并为一个或多个汇总文件.Web 应用程序使用的 JavaScript 文件越少越好,因为 HTTP请求对某些 Web 应用程序而言是主要的性能瓶颈。而且,使用
模块打包器
以模块形式编写代码,并不意味着必须以模块形式交付代码,通常由大量模块组成的JavaScript 代码在构建时需要打包到一起,然后只交付一个或少数几个 JavaScript 文件。 模块打包器的工作是识别应用程序中涉及的 JaveaSeript依赖关系,将它们组合成一个大文件,完成 对模块的串行组织和拼接,然后生成最终提供给浏览器的输出文件。 能够实现模块打包的工具非常多。Webpack、Rollupt 和 Browserify 只是其中的几个,可以将基于模 块的代码转换为普遍兼容的网页脚本。