第一部分 编程风格
每个人都有自己独特的编程风格,有情有独钟亦或恨之入骨的,每个人都希望用自己的方式制定规范来实施。这些应当被归为个人编程嗜好。我们需要的是在编程过程中尽早的确定统一的编程风格。
在团队开发中,编程风格一致性变得尤为重要,因为:
1 任何开发者都不在乎某个文件的作者是谁,也没有必要花费额外的精力去理解代码的逻辑并且重新排版,因为所有代码的排版风格都是非常一致的,因为风格不一致会导致我们打开代码文件的第一件事情不是立即工作,而是进行缩进的排版整理。
2 能很容易的辨别出有问题的代码并且发现错误,如果所有的代码风格很像,当看到一段与众不同的代码时,很肯能问题就出在这里。
JSLint 和 JSHint 是两个检查编程风格的工作。不仅找出代码中潜在的错误,还能对潜在的风格问题给予提示警告。
“程序是写给人读的,只是偶尔让计算机执行一下” - Donald Knuth
第一章 基本的格式化
编程风格的核心就是基本的格式化规范,这些规范决定着如何编写高水准的代码。
1.1 缩进层级
所有语言都是如此,都会讨论如何对代码缩进。
if(wl && wl.length){
for(var i=0; i<wl.length; i++){
p = wl[i];
type=Y.Lang.type(wl[i]);
...
}
}
稍微编写过程序的都不会使用上面的格式,害人害己。
if(wl && wl.length){
for(var i=0; i<wl.length; i++){
p = wl[i];
type=Y.Lang.type(wl[i]);
...
}
}
代码应该如何统一缩进其实一直没有统一的共识。1.使用制表符缩进 。 2.使用空格缩进。
jQuery 明确规定使用制表符缩进。
Dauglas Crockford 的 JavaScript 规定使用4个空格缩进。
Google 的 JavaScript规定使用2个空格缩进。
Dojo 编程风格指南使用制表符缩进。
尼古拉斯(作者)建议4个空格,或在编辑器中设置一个 制表符 替换为 4个空格。为一个缩进层。
1.2 语句结尾
强制规定,语句结束插入分号。
function(){
return
{
...
}
}
上面代码就会引起问题。
1.3 行的长度
很少有JS的规范提及到行的长度的风格的,很多语言都建议行的字数不超过 80 字,因为很早以前的编辑器,这是最大的行长度。
Java语言规范中 源码单行长不超过 70 字,文档中单行长不超过 80 字。
Android开发者规范中规定不超过 100 字。
非官方的Ruby规定不操过 80 字。
Python编程规范规定单行不超过 79 字。
JavaScript 在 Crockford 代码规范中规定为 80 字符。
1.4 换行
当一行字字符数超过了最大限制的时候就需要换行。通常会在运算符之后换行,下一行会增加2个层级的缩进。
callFunction(document,"aaaaaaaaaaaaaaa","bbbbbbbbbbbbbbbb","cccccccccccccccccccc",
"bbbbbbbbbbbbbbbbbbb");//超出首字母c的2个层级
需要注意的是规范为在运算符之后换行,以及增加2个层级的缩进。
当然如果是赋值的话就保持赋值运算符对其。
var result = "aaaaaaaaaaaaaaaa" + "bbbbbbbbbbbbbbbb" + "ccccccccccccccccccc" +
"ddddddddddddddddddddddddd";
1.5 空行
空行常常被忽略,但是空行也是提高代码可读性的强大武器。
if(wl && wl.length){
for(var i=0; i<wl.length; i++){
p = wl[i];
type=Y.Lang.type(wl[i]);
if(a){
...
}
}
}
空行一般来说不是很好在规范中规定,
1.方法之间
2.方法中局部变量可第一条语句之间。
3.多行或者单行注释之前。
4.在方法内的逻辑片段之前。
目前没有规范对空行进行明确的规定。
1.6 命名
"计算机科学只存在两个难题:缓存失效和命名" - Phil Karlton
只要写代码就会涉及到变量和函数,所以变量和函数的命名对于增强代码的可读性至关重要。
JavaScripe 遵循 ECMA ,即驼峰式大小写。
var myName;
var anotherName;
google Dojo等规范都规定使用小驼峰命名法。,当然还有一种曾经风光一时的 匈牙利命名法。
1.6.1 变量和函数
变量名总是应该遵循驼峰大小写命名法,并且命名的前缀应当是名词,使得可以将变量和函数区分开。
// 以下OK
var count = 10;
var myName = "Ni";
var found = true;
// 以下就需要重新命名
var getCount = 10;
var isFound = true;
//函数应当以动词开头
var getName = function(){
return myName;
};
还有一点示范
can 函数返回布尔值
has 函数返回布尔值
is 函数返回布尔值
get 函数返回非布尔值
set 函数用来保存值
1.6.2 常量
JS中没有常量的概念。来自C语言的约定,常量使用大写字母并且使用下划线命名。
var MAX_COUNT = 10;
var URL = "http://www.guokr.com";
google Dojo 等编程风格指南都规定如此。
1.6.3 构造函数
JS中的构造函数只不过是冠以 new 运算符的函数而已。采用大驼峰命名法。
function Person( name ){
this.name = name;
}
1.7 直接量
JS中的原始值:字符串、数字、布尔值、null和underfined。
1.7.1 字符串
JS中的字符串使用双引号和单引号是完全相同的。对于规范只需要统一即可。
jQuery 和 Crockford规范为双引号来括住字符串,google规范为单引号括住字符串。
1.7.2 数字
JS中整数和浮点数都存储为相同的数据类型。
不推荐使用 10. 和 .1 的写法。会产生歧义,是否是我们漏掉了数字。
1.7.3 null
null 常会与 undefined 搞混淆。以下场景才应当使用null:
1. 用来初始化变量,该变量可能被赋值为对象。
2. 用来和一个已经初始化的变量进行比较,这个变量可以是非对象。
3. 当函数期望的参数是对象,用作参数传入。
4. 当函数的返回值期望是对象时,用作返回值传出。
以下场景不应当使用 null:
1. 不要使用null来检测是否传入了参数。
2. 不要用null来检测一个未初始化的变量。
var person = null;
function getPerson(){
if( condition ){
return new Person("ABC");
}else{
return null;
}
}
对于 null 的理解最好是当做对象的占位符使用,主流的规范没有提及。
1.7.4 undefined
由于
var person;
typeof person; // undefined
typeof foo; // undefined
以及
null == undefined
所以,禁止使用特殊值 undefined 可以有效地确保只在一种情况下 typeof 才返回 undefined。
1.7.5 对象直接量 与 数组直接量(字面量)
创建对象与数组最流行的方法为对象字面量和数组字面量,比相应的构造函数式创建更高效直接。
第二章 注释
2.1 单行注释
三种使用方法:
1. 独占一行,用来解释下一行,并且这行注释之前总有一行空行,缩进的层级与下一行代码保持一致。
2.在代码行的尾部注释。代码与注释之间至少有一个缩进。注释包括之前本行的代码不应该超过最大字符数限制。
3.注释掉大块代码。
// 好的注释方法
if( condition ){
// 解释下一行代码
func();
}
// 不好的注释方法
if( condition ){
// 解释下一行代码
func();
}
if( condition ){
// 解释下一行代码
func();
}
2.2 多行注释
偏向使用 Java 风格的注释
/*
* 这里是注释
* 这里是注释
* 这里是注释
*/
缩进的格式与单行注释表示一致。
2.3 使用注释
当代码不够清晰的时候使用注释,当代码很明了的时候不应当添加注释。
并且注释不应该只是解释变量名称或者函数名称式的那种废话。
当代码很难理解或者代码可能被误认为错误的代码的时候,需要添加注释。
2.4 文档注释
技术角度来说,文档注释并不是 JS 的组成部分,但是是一种普遍的实践。
1.应当对所有的方法和可能的返回值添加描述。
2.对自定义的构造函数类型和期望的参数添加描述。
3.对于包含对象或者方法的对象进行描述。
第三章 语句和表达式
JS中,诸如 if 和 for 之类的规定不管是单行代码还是多行代码,均使用 { }
// 好的写法
if( contidion ){
foo();
}
if( conidion ){
foo();
foo();
}
// 不好的写法
if( contidion ){ foo(); }
if( contidion ) foo();
所有的语句块都应当使用 { }, if , for , while ,do while , try catch finnally 。
3.1 花括号对齐方式
有两种对齐方式
if( conidion ){
foo();
foo();
}
和
if( conidion )
{
foo();
foo();
}
很显然由于第二种又时候会会让浏览器执行代码的时候产生不一样的解析,所以使用第一种花括号对齐方式。
3.2 块语句间隔
语句块间隔有3中形式:
1.语句名 圆括号 左花括号间没有空格间隔。
2.左圆括号之前 右圆括号之后添加一个空格。
3.左圆括号 右圆括号前后都添加一个空格。
作者推荐一致使用第二种形式。
3.3 switch 语句
JS中任何表达式都可以合法的用于case从句中,其他语言必须使用原始值和常量。
3.3.1 缩进
JS的switch缩进一致是有争议的话题。
switch (condition) {
case "a":
//代码
break;
case "b":
//代码
break;
default :
// 代码
}
每个case相对于switch都有一个缩进层。
第二个case开始之前都有一个空行。
另一种 Crockford 和Dojo编程风格为
switch (condition) {
case "a":
//代码
break;
case "b":
//代码
break;
default :
// 代码
}
3.3.2 case语句的连续执行(贯穿)
switch (condition) {
case "a":
case "b":
//代码
break;
default :
// 代码
}
Douglas 在 Crockford中禁止出现贯穿现象, 但是Nichlas认为只要程序逻辑很清楚,贯穿完全是一个可以接受的编程方式。Jquery 编程风格中也是允许的。
3.3.3 default
是否在任何时候都需要 default。
switch (condition) {
case "a":
case "b":
//代码
break;
//没有 default :
}
更倾向于没有默认行为并且在注释中写入省略 default。
3.4 with
with 语句可以更改包含上下文解析变量的方式。通过with可以使用局部变量的方式和函数的形式来访问特定对象中的属性和方法。可以缩短代码的长度。
但是是的JS引擎与压缩工具无法压缩。严格模式中 with 是禁止的。 基本所有的规范中都禁止使用 with。
3.5 for 循环
对于for循环中的 break 和 return 会改变循环的方向,尽量避免使用,但是不禁止。
3.6 for-in 循环
for-in循环建议与 hasOwnProperty 一起检测属性,除非你需要检查原型。
for-in是用来遍历对象的,不要用其来遍历数组。
第四章 变量,函数和运算符
JS编程的本质是编写一个个函数来完成任务。在函数内部,变量和运算符可以通过移动操作字节来使某个事件发生。通过书写格式化来减少复杂度,增强可读性显得十分重要。
4.1 变量声明
变量声明是通过 var 语句来完成的。变量声明不管是在何处声明,所有的 var 语句都会提前到代码顶部执行。
变量声明提升意味着:在函数任何地方定义变量和在函数顶部定义变量是完全一样的。所以有了一种风格就是将所有变量声明集中在函数顶部而不是散落在代码各个地方进行声明。
由而产生了一种单一的 var 风格声明。
function doSomeThing (item){
var i, len, value = 10, result = value+10 ;
for(i=10, len=item.length; i<len; i++){
do(item[i]);
}
}
尼古拉斯推荐 合并var声明,可以清晰看到所有变量名 以及缩小代码量。并且建议每个变量都独占一行,统一缩进。
function doSomeThing (item){
var i,
len,
value = 10,
result = value+10 ;
for(i=10, len=item.length; i<len; i++){
do(item[i]);
}
}
记得有位牛人博客曾经指出一些较为牛逼的建议,其中一条就是攻击单一 var 声明。 认为比如 for 循环中的() 中 i 就应该在使用的时候声明逻辑才可以更清晰,
而很多代码量是很大的,在前面声明之后有可能会忘了一些变量名的本意。
4.2 函数声明
和变量声明一样,函数声明也会被JS引擎提升。所以在函数声明之前与函数声明之后调用函数都是一样的。
推荐总是先声明函数,再使用函数。Crockford规范还推荐在函数内部的函数应当紧跟着变量声明之后声明。
function doSomeThing (item){
var i,
len,
value = 10,
result = value+10 ;
function do(item){
...
}
for(i=10, len=item.length; i<len; i++){
do(item[i]);
}
}
当函数在声明之前使用的话,JSLint和JSHint都会给出警告。
函数声明不应当出现在语句块之内,会有兼容方面的问题导致各个浏览器解析不一致。
if(condition){
function doThing(){
...
}
} else {
function doThing(){
...
}
}
该种场景目前属于ECMA的灰色地带,应当避免。也是google规范中禁止的。
4.3 函数调用隔离
一般情况下函数调用的写法是,在函数名和左括号之间没有空格,为了将他与块语句区分开来。
// good
doThing(item);
// not good
doThing (item);
//用来作比较的语句应当加入空格
while (condition){
// code
}
对于jQuery的风格就是 在内部参数的前后加入空格,是个代码比较透气,增强可读性和读代码的舒适度。
doThing( item );
当然不是所有的情况都应当如此,对于jQuery其他情况如下
doThing(function() {});
doThing({ item: item });
doThing([ item ]);
doThing("Hi !");
4.4 立即调用的函数
可能省略圆括号其效果是一样的,但是会带来可读性上的降低。
// 好的写法
var value = (function(){
//函数体
return {
message: "Hi !"
}
}());
// 不好的写法
var value = function(){
//函数体
return {
message: "Hi !"
}
}();
4.5 严格模式
ECMA5 中引入的严格模式,可以通过这种方式来谨慎的解析 JS 代码,以减少错误。
"use strict";
遵循ECMA5的浏览器会将其识别为指令,使用严格模式进行解析代码,而不支持的浏览器只会当做普通代码块。
不推荐将严格模式使用在全局之中,可能产生的问题就是,如果我有11个文件需要做合并处理,那么当有一个文件是使用的全局的严格模式,那么合并后的文件整体就会使用严格模式,也行我其余的10个文件不遵循严格模式呢?
function sayHi(){
"use strict";
return "Hi !";
}
在JSLint与JSHint中,也会对出现在函数体之外的严格迷模式声明发出警告。当然,推荐使用严格模式。
4.6 相等
JS具有强制类型转换的机制,所以判断相等的操作还是很微妙的
5 == "5" // true
25 == "0x19" // true
2 == true // false
对于对象,则会首先调用对象的 valueOf() 方法,得到原始类型值比较,如果没有定义则调用 toString()。
var object ={
toString: function(){
return "0x19";
}
}
object == 25; // true
由于强类型转换的情况,所以需要使用 === 和 !== 来处理比较的情况。而不推荐使用 == 和 != 。
4.6.1 eval()
eval() , Function构造函数 , setTimeout() , setInterval() 都是可以讲一段字符串作为代码来执行。
执行的代码可能产生变量名污染和安全性的问题。
当然也不完全封杀 eval 因为对于一些特殊情况还是需要使用 eval 来处理代码的,一般只是用在没有别的方法来处理的时候才使用。
4.6.2 原始包装类型
对于 字符串构造函数, 函数构造函数等,一律使用字面量形式,不适用构造函数形式。对于正则表达式,在有些情况下比如动态创建正则的时候还是需要使用的。
第二部分 编程实践
构建软件设计的方法有两种:一种是把软件做的很简单以至于明显找不到缺陷,另一种是把它做的很复杂以至于找不到明显的缺陷。
- C.A.R.Hoare 80年图灵奖获得者
上一部分编程风格只关心代码的呈现,而这部分编程实践则关心编码的结果。引导开发者以某种方式进行编码,设计模式是编程实现的组成部分,专用于解决和软件组织的相关特定问题。
第五章 UI层的松散耦合
在实际的场景中,css 与 js 应该为兄弟关系,而不是依赖关系。
需要增加分层的合理性和消除依赖。
5.1 什么是松耦合
很多的设计模式就是为了解决紧耦合的问题。如果2个组件耦合太紧密,就说明一个组件和另一个组件直接相关。
比如有一个名为 error 的className,贯穿整个站点。有一天觉得他命名不合理需要更改的时候,有可能我们需要做更改上百个使用到它的组件,也有可能只需要更改一个组件。
你能做到只修改一个组件而不是说修改 N 个组件,那么就做到了松耦合。对于一些大型系统来说这是至关重要的。
5.2 将 JS 从 CSS中抽离
IE 8 中可以使用 css 表达式(CSS expression)。允许将JS代码直接插到 css 中。
.box{
width: expression(document.body.offsetWidth + "px");
}
这是绝对不建议这么做的。 当然 IE 9 已经不再支持了。
5.3 将 CSS 从 JS 中抽离
两门语言相互协作的很不错,通过 JS 控制样式最流行的一种做法就是 直接修改 DOM 的 style 属性
element.style.color = "#FFF";
element.style.display= "block"
有些还使用 cssText 来进行对 style 的赋值。
以上是不建议 在各个方面都不如以下方式:
将需要改变的样式放入新的 css 选择器中,需要更新样式的时候只需要加入或者替换该 classname即可。
JS不应当直接操作样式,以便与 css保持松耦合。
有一种情况是可以的,就是将页面元素重新定位的时候,比如一些动画变幻等效果。
5.4 将 JS 从 HTML 中分离
在学习 JS 之初,经常将事件绑定放入页面的 HTML 中。
<button οnclick="doSomething()" id="action-btn">Click Me</button>
首先,是需要 doSomething 函数已存在,就需要将 JS 代码放置在 HTML 之前。
在次,对于可维护性方面有很大的弊端。
所以应当使用事件处理程序的绑定机制。addEventListener removeEventListener。当然一些JS库将其封装的更好。
5.5 将 HTML 从 JS 中抽离
正如将 JS 从 HTML 中抽离一样,最好也将 HTML 从 JS 中抽离。因为在我们对文本或者页面结构进行调试的时候我们更希望直接面对的是 HTML 而不是一些 JS 语句动态的修改结构。
在 JS 中使用最多的情况就是给 innerHTML 赋值。
var div = document.getElementById("myDiv");
div.innerHTML = "<p>abc</p><p>efg</p>";
将HTML嵌入JS中是非常不好的实践,原因有:
1.大大增加了跟踪文本和结构性问题的复杂度。
2.加大了代码的维护成本。
3.相对于只修改 HTML,当JS和HTML混淆在一起时,修改代码更容易引发其他问题。
将HTML从JS抽离
1.从服务器加载。将模板放置于原创服务器。使用Ajax方式加载模板文件。
2.简单的客户端模板。带有一些替换标签的片段,<li><a href="%s">%s</a></li> 然后通过函数将模板和数据结合。
3.复杂的客户端模板。目前一些较为健全的开源模板库,可以很好地帮助实现所需要的较为大的项目需求。
第六章 避免使用全局变量
JS执行环节有很多独特之处相对于其他语言来说,如全局变量和函数的使用。
JS本身的初始执行环境就是有多种多样的全局变量所定义的,这些全局变量在环境创始之初就存在了。
全局对象是一个神秘的对象,表示脚本的最外层上下文。
浏览器中 window对象往往重载并等于全局对象,因此任何在全局对象中声明的变量或者函数都为window对象的属性。
而不需要显式的将这些挂在到 window 对象上。
var color = "red";
function getColor(){
alert(color);
}
color === window.color;
getColor === window.getColor;
6.1 全局变量带来的问题
随着代码的增长,全局变量毫无疑问的会导致一些非常重要的可维护性问题。
6.1.1 命名冲突
当全局变量和全局函数越来越多的时候,发生命名冲突概率就会越来越大。各种变量,函数将会被重置,那么很多各种各样的BUG就会随之而来。
比如:
function getColor(){
alert(color);
}
中的 color 由于依赖与全局的变量,那么这种依赖关系将很难被追踪到,我在不小心重置了 color 后不知道有多少像该函数一样的函数进行了全局依赖的行为。
接下来,全局变量与一些浏览器内置API冲突的风险。
6.1.2 代码的脆弱性
一个依赖于全局变量的函数即是深耦合于上下文环境之中。环境的变化就可能会导致函数的失效。
但是当 color 被作为参数传递进函数的话,那么情况就大大不一样了。
function getColor( color ){
alert(color);
}
不在依赖全局变量,并且从与上下文的深耦合之中脱离出来。所以当定义函数尽可能的将数据置于局部变量之中,任何外部数据都应当以参数的形式传递进入函数,保证函数与其外部环境隔离开,不至于形成深度耦合的关系。
6.1.3 难以测试
任何依赖于全局变量才能正常工作的函数,只有为其重新创建完整的全局变量才能正确的测试,然后,就木有然后了。
保证函数不多全局变量有依赖,将大大增强代码的可测试性,当然不包括JS中原生的对象,如Date,Array等。
6.2 意外的全局变量
JS 中有很多陷阱会使我们一不小心就创建了全局变量,如:
function doSomething(){
var count = 10;
title = "abcdefg";
var a = b = 0;
}
对于意外的全局变量一些工具,比如JSLint和JSHint就可以起到作用了,因为意外创建全局变量并不会引起JS引擎的报错,有时候很难察觉到,而这些工具就是我们很好的预防,消除一些意外创建的情况。还有严格模式下也会报错来提醒程序猿。
6.3 单全局变量方式
YUI 引入 唯一 YUI全局变量。
jQuery 引入 $ 和 jQuery 全局变量。
Dojo 引入 dojo 全局变量。
Closure 引入 goog 全局变量。
单全局变量意味着创建一个唯一的全局对象名,将所有你的功能代码挂在到该对象上,都成为该对象的属性,从而不创建其他的全局变量。
function Book( title ){
this.title = title;
this.page = 1;
}
Book.prototype.turnPage = function( desc_num ){
this.page += desc_num;
}
var chapter1 = new Book("one");
var chapter2 = new Book("two");
var chapter3 = new Book("three");
会有好多全局变量,那么:
var MainJS = {};
MainJS.Book = function( title ){
this.title = title;
this.page = 1;
}
MainJS.Book.prototype.turnPage = function( desc_num ){
this.page += desc_num;
}
MainJS. chapter1 = new MainJS.Book("one");
MainJS. chapter2 = new MainJS.Book("two");
MainJS. chapter3 = new MainJS.Book("three");
其实,很简单,但是带来的效果确是非常不错的。
6.3.1 命名空间
JS中的命名空间,其实质就是不断的往一个定义的全局对象中,合理有规则的塞东西。
var MyGlobal = {
namespace : function( ns ){
var parts = ns.split("."),
object = this,
i, len;
if( parts[0] === "MyGlobal" ){
parts = parts.slice(1);
}
for( i = 0,len = parts.length; i<len; i++ ){
if( !object[parts[i]] ){
object[parts[i]] = {};
}
object = object[parts[i]];
}
return object;
}
}
然后可以自由的创建命名空间了,
MyGlobal.namespace("aa.bb.cc.dd");
MyGlobal.namespace("MyGlobal.aa.bb.cc.dd");
6.3.2 模块
另外一种基于单全局变量的扩充方法是使用模块。
模块是一种通用的功能片段,并不创建全局变量和命名空间,而是将这些代码存放在一个表示执行一个任务或者发布一个借口的单函数中。两种流行的模块是 YUI模块 和 异步模块定义(AMD)。
YUI模块
是使用YUI JS类库创建新模块的一种模式,写法:
YUI.add("module-name", function(Y){
// 模块正文
}, "version", { requires: ["depend1", "depend2"] });
异步模块定义 AMD
define( "module-name", ["depend1", "depend2"] , function(d1, d2){
// 模块正文
});
6.4 零全局变量
JS代码注入到页面的时候可以实现不创建全局变量。当然使用的场景不会非常多。在一段完全独立的代码,或者代码非常小且不提供任何接口的时候。
(function(win){
// 代码 不暴露任何接口
}( window ));
只要代码需要被其他的代码所依赖的时候,就不可以使用零全局变量方式。在对于代码块的代码合并时候有挺大作用。
第七章 事件处理
事件处理在JS中是至关重要的,影响着网站的各个方面。所有的JS代码均通过事件绑定到UI上,所以大多前端工程师需要花费很多的事件来编写和修改事件处理程序。
大多事件处理程序相关代码和事件环境紧紧偶合在一起,导致了可维护性很糟糕。
7.1 典型用法
开发人员基本都了解,事件触发的时候事件对象event会作为回调函数参数传入事件处理程序之中。event对象中含有的大量信息基本上我们只会用到很小一部分。
// 不好的例子
function handleClick( event ){
var popup = document.getElementById( "popup" );
popup.style.left = event.clientX + "px";
popup.style.top = event.clientY + "px";
popup.className = "reveal";
}
addListener(element, "click", handleClick);
这是比较普遍的做法,实际上并不是很好,具有局限性。
7.2 规则1:隔离应用逻辑
上一段代码中,第一个问题是事件处理程序中包含了应用逻辑(application logic),应用逻辑是与应用相关的功能性代码,而不是用户行为相关的。
将应用逻辑从所有的事件处理程序中抽离出来是一种最佳实践。因为很有可能在之后的某段代码中我们会使用到同一段逻辑,抽离就降低了代码的耦合度,增强了可读性和维护成本。
如:
var MyApplication = {
handleClick : function(event){
this.showPopup( event );
} ,
showPopup : function(event){
var popup = document.getElementById( "popup" );
popup.style.left = event.clientX + "px";
popup.style.top = event.clientY + "px";
popup.className = "reveal";
}
}
addListener(element, "click", function(event){
MyApplication.handleClick( event );
});
这样就将应用逻辑从事件处理程序中剥离,如果在某些地方又要使用该逻辑就可以很方便的使用方法。
7.3 不要分发事件对象
再拨离了应用逻辑之后上段代码又引发了一个问题,event对象被无节制的分发,所以,应用逻辑不应当依赖于event对象。
1.方法接口没有表明那些数据是必要的。
2.影响测试方面效率。
var MyApplication = {
handleClick : function(event){
this.showPopup( event.clientX, event.clientY );
} ,
showPopup : function(x, y){
var popup = document.getElementById( "popup" );
popup.style.left = x + "px";
popup.style.top = y + "px";
popup.className = "reveal";
}
}
这样就很不错了,再加上进入程序时,阻止默认事件和事件冒泡即可。
清楚地展示了事件处理程序和应用逻辑之间的分工。应用逻辑也不需要对event产生任何依赖,进而很多地方都可以使用相同的应用逻辑
第八章 避免空比较
JS中经常会有变量与null比较。来判断变量是否被赋予合理的值。
var Controller = {
process : function( items ){
if( items !== null ){
items.sort();
item.forEach(function( item ){
// ... Code
});
}
}
}
很明显我们本意是需要检测,传入的参数是否为数组,但是仅仅和null进行判断并不能够提供足够的信息来保证后续代码执行的安全性。还好,灵活的JS提供了多种检测的方式。
8.1 检测原始值
5种原始类型 字符串、数字、布尔值、null和undefined。
typeof 可以检测各个原始值的类型,并且返回相应类型字符串。而 typeof null 会返回 "object"。
if( typeof str === "string" ){
// ... Code
}
需要注意的是,typeof 一个未声明的变量也不会报错,会返回 "undefined" ,所以也需要注意该种情况,对于检测 null,则直接与 null 进行比较即可。
var element = document.getElementById("my-div");
if( element === null ){
element.className = "abc";
}
8.2 检测引用值
对于检测引用值,typeof会力不从心,基本都会返回"object",那么对于引用值检测最好的方法是使用 instanceof。
if( value instanceof Date ){
// ... Code
}
if( value instanceof RegExp ){
// ... Code
}
if( value instanceof Error){
// ... Code
}
instanceof 还有个特性就是,它不只是检测构造这个对象的构造器,还检测原型链。
var now = new Date();
now instanceof Date;
now instanceof Object;
均为 true。由于这个原因也使得使用 instanceof 来检测引用值也不是最佳的方法。
对于自定义的构造函数来说,instancdof 检测是唯一的方法。当然需要注意一下 跨帧 使用时的问题。
8.2.1 检测函数
JS中同样存在 Function 构造函数,每个函数均是其实例。
myFunc instanceof Function
当然也是不适用与跨帧的情况。
还有一个好的写法就是使用 typeof
typeof myFunc === "function"
但是对于 IE8以及以下浏览器来说,
typeof document.getElementById 会返回 "object".( 还有几个其他的DOM操作 )。
8.2.2 检测数组
JS最古老的跨域问题在于 帧 frame 之间来回传递数组。每个帧有自己的 数组构造函数,所以在一个帧中的数组实例是不被另外帧所承认的。
优雅的解决方案:
Object.prototype.toString.call( value ) === "[object Array]"
对于检测数组的需求 ES5 中引入了 Array.isArray() 方法来检测。
优雅方案
function idArray( value ){
if( typeof Array.isArray === "function" ){
return Array.isArray( value );
}else{
return Object.prototype.toString.call( value ) === "[object Array]";
}
}
8.3 检测属性
很多时候我们会使用与 null undfined 对比来判断是否属性存在。
if( obj[prop] ){ }
if( obj[prop] != null ){ }
if( obj[prop] != undefined){ }
这样会导致代码有漏洞,不能将所有的情况都覆盖到。
对于属性的检测,最好的方法是使用 in,in运算符仅仅会简单的判断 属性 是否存在,而不会去读属性的值。当然 in是会在原型链中进行查找的。
如果只是希望检测实例属性,那么使用 hasOwnPrototype() 进行检测。
注意,在IE8以及之前浏览器,DOM元素并非继承自 Object 所以没有该方法。
最后,不管什么时候需要检测属性是否存在的时候,请使用 in 或者 hasOwnProperty()。
第九章 将配置数据从代码中分离出来
代码就是一些计算机运行的指令,当我们传递数据进入计算机的时候,指令对数据进行操作产生结果。
那么当我们在修改一些数据问题的时候就会带来一些源代码引入BUG的风险,所以对于应用来说,应当将一些关键数据从主要的源码中抽离出来,这样就可以是我们在修改数据或者源码的时候更放心。
9.1 什么是配置数据
配置数据就是我们在应用中写死(hardcoded)的值。
// 将配置数据藏于源码之中
function validate( value ){
if( !value ){
alert("Invalid value");
location.href = "/errors/invalid.html";
}
}
上面的函数具有2个配置数据片段, 字符串 "Invalid value" 和 URL地址 "/errors/invalid.html"。
之所以将其认为是配置数据是因为它们都写死在代码里,并且在将来可能需要修改。下面是一些配置数据的例子:
1. URL
2. 需要展示给用户看的字符串
3. 重复的值
4. 设置的值,一些配置选项
5.任何可能发生变化的值
我们需要记住,配置数据随时都有可能做修改,我们不希望因为有人需要修改页面的提示信息而需要修改JS源码。
9.2 抽离配置数据
其实,抽离配置数据非常简单,只需要创建一个管理整体的配置数据的对象即可。
var config = {
MSG_INVALID_VALUE : "Invalid value",
URL_INVALID : "/errors/invalid.html"
}
再在需要使用配置数据的地方使用即可。
注意统一在配置数据对象中的命名。将配置数据提取出来之后任何人需要做修改都不会引起一些代码的逻辑错误问题,对于很大的系统,可以将配置数据专门使用其他单独文件,使其于代码隔离。
9.3 保存配置数据
配置数据最好放在单独的文件之中,以便清晰地分隔数据和应用逻辑。一种值得尝试的方法是将这些配置数据放于非JS的文件中。
1. 使用各种流行的属性文件来存放。(如 Java属性文件)
2. JSONP,将JSON结构使用函数调用包装起来。
3. 纯JS对象
还有一些专门可以管理配置文件的库可以使用。
第十章 抛出自定义错误
编程语言具有“创建”错误的能力,在JS中抛出错误是一门艺术。再合适的时间抛出错误可以大大减少我们调试的事件,对代码的满意度也将急剧提升。
10.1 错误的本质
当某些非期望的事情发生的时候,程序就会要发一个错误。这也许是传入了非法的值,或许是遇到无效的操作符等等。
编程语言定义了一组基本的规则,当偏离了这些规则的时候将导致错误,只有当错误被抛出来时,我们才能有地方入手解决,如果错误无声无息,那么解决的代价可想而知。所以错误时开发者的朋友而非敌人。
JS错误消息信息稀少,并且在一些老版本的IE中更是令人深痛恶绝。所以在代码中的特殊之处有计划的抛出一个错误总比在所有地方有可能抛出错误简单得多。
10.2 在 JS 中抛出错误
毫无疑问,在 JS 中抛出错误比任何语言中做同样的事情更有价值,这要归结于Web端调试的复杂性。可以使用 throw 操作符,将提供一个对象作为错误抛出,Error对象时最常用的。
throw new Error("bad happened");
还有种方式就是直接抛出字符串
throw "bad happened";
但会有兼容性的问题,所以不建议使用。
10.3 抛出错误的好处
可以看一下一下两个函数的优略:
function getDivs( element ){
return element.getElementByTagName("div");
}
function getDivs( element ){
if( element && element.getElementsByTagName ){
return element.getElementByTagName("div");
} else {
throw new Error("getDivs() : Argument must be a DOM element");
}
}
虽然chrome等控制台以及对于错误的抛出做的非常好了,但是也还是有一些浏览器在一些特殊情况下会起到很大作用。
10.4 何时抛出错误
// 不好的写法 检测太多
function addClass(element, className){
if( !element || typeof element.className != "string" ){
throw new Error("addClass() : First argument must be a DOM element");
}
if( typeof element.className != "string" ){
throw new Error("addClass() : First argument must be a DOM element");
}
element.className +=" " + className;
}
原本只是需要加一个class 但是加了那么多的抛出错误,就会适得其反。会引起过度杀伤。
// 好的写法
function addClass(element, className){
if( !element || typeof element.className != "string" ){
throw new Error("addClass() : First argument must be a DOM element");
}
element.className +=" " + className;
}
1. 当修复了一个难以调试的错误的时候,尝试增加一两个自定义错误,当再次发生错误的时候有助于解决问题。
2. 如果正在编写代码,思考一下我希望哪些事情不会发生,如果发生将引发较大问题,在这个某些问题上抛出错误。
3. 如果在编写别人的代码,思考下他们的使用方式在特定的情况下抛出错误。
这些我们所做的都不是为了防止错误,而是在有错误 时候更快更容易的找到错误的原因。
10.5 try-catch 语句
try-catch 可以在浏览器抛出错误之前解析,将可能引发错误的代码放在 try 块中,将处理错误的代码放在 catch 中。当然还可以加入 finally 块。
在catch中不要为空,应该总要写点声明老处理错误,不然就会依旧不知道错误在哪里。
10.6 错误的类型
Error : 所有错误的基本类型,引擎不会抛出此类型。
EvalError : 通过 eval() 函数执行的代码发生错误。
RangeError : 一个数字超出边界时抛出,试图创建一个长度为-20的数组。
ReferenceError : 期望对象不存在,例如试图在null上调用函数。
SyntaxError : 语法上的错误。
TypeError : 变量不是期望类型的时候。
URIError : 给一些内置函数传入非法URL时抛出的错误。
第十一章 不是你的对象不要动
JS独特之处就是任何东西的都不是神圣不可侵犯的,默认情况下,你可以修改任何你能触及到的对象。那么,在一个大型项目之中,对象的修改变成了一个大问题。
11.1 什么是你的
当你的代码创建了这些对象,那么你拥有这些对象。维护是你的责任。牢记,如果你的代码没有创建这些对象,那么不要修改它们。
1. 原生对象。 2. DOM对象 3. 浏览器对象模式对象(BOM)。 4. 类库对象。
11.2 原则
在JS中,我们将已经存在的对象视为一种背景,在此之上开发代码,应该将JS对象当做一个实用的工具函数来看待。
不覆盖方法,不增加方法,不删除方法。
11.2.1 不覆盖方法
JS有史以来最糟糕的实践就是 覆盖一个非自己拥有的对象。(作者说在 Yahoo 便遇到了很多此类问题) 。
神圣的 document.getElementById 都可以被轻易覆盖。
11.2.2 不新增方法
几乎无法阻止你向任何对象新添方法,为对非自己的对象添加新方法是一个大问题,命名冲突。虽然对象目前可能没有该方法,但是不能保证未来也没有。更糟糕的是,一旦未来的原生方法和你的方法行为不一致,那么你整个代码将陷入维护的噩梦。
Prototype库就是一个不好的例子,很随意的修改了大量的原生对象和方法。导致在JS的历史上遇到很多问题。
在小于1.6的版本中,Prototype定义了一个document.getElementsByClassName() 方法,当然在很早以前这完全没有问题,因为本身还没有该方法,随后当在H5中官方定义后,其团队不得不加上防守性的代码
if( !document.getElementsByClassName ){
document.getElementsByClassName = function(){
// 非原生实现
}
}
然而,之前的Prototype所使用的方法结果是可以使用 each的,H5定义的可没有 each。所以结果可想而知。
11.2.3 不删除方法
删除JS方法其实和新增一个方法一样简单,当然覆盖也算删除的一种。不管方法有多糟糕,我们都不容易知道是否真的没有代码使用它,很多库都如此,即使很糟糕,在版本更新中也不会立即删除掉。
11.3 更好的途径
修改非自己拥有的对象是解决某些问题的很好的方案。可能有一些方法,所谓的设计模式,不直接修改这些对象而是扩展这些对象。
最受欢迎的对象扩充方式就是继承。一种类型的对象如果已经做到了你想要的大多数工作,那么继承,再加一些新的功能即可。JS中有两种基本形式:基于对象的继承和基于类型的继承。
11.3.1 基于对象的继承
基于对象的继承也就是原型继承,即ES5中的 Object.create() 方法。
var person = {
name : "Nicholas",
sayName : function(){
alert( this.name );
}
}
var ni = Object.create( person );
当然可以扩展对象
var ni = Object.create( person, {
name : {
value : "Greg"
}
} );
一旦以这种方式创建了对象,那么你可以随意修改新对象,毕竟你是对象的拥有者。
11.3.2 基于类型的继承
基于类型的继承是通过构造函数而非对象
function MyError( message ){
this.message = message;
}
MyError.prototype = new Error();
11.3.3 门面模式
门面模式是一种流行的设计模式,他为一个已经存在的对象创建新的接口,门面模式是一个全新的对象,背后是一个已经存在的对象在工作。如果你的用例中无法使用继承满足需要,可以使用门面模式。
funtion DOMWrapper( element ){
this.element = element;
}
DOMWrapper.prototype.addClass( className ){
this.element.classmName += " " + className;
}
var wrapper = new DOMWrapper( document.getElementBuId("myDiv") );
wrapper.addClass("selected");
从JS的可维护性来说,门面模式是非常合适的方式,你可以完全控制这些接口。你可以允许访问任何底层对象的方法或者属性,反之也可以有效地过滤对象的访问。
门面实现一个特定的接口,让一个对象看上去像另一个对象就是适配器。两者的差别就是前者创建接口,后者实现已经存在的接口。
11.4 关于Polyfill的注解
随着ES5和H5的特性在各个浏览器中实现,JS polyfills(shims)开始就行起来,polyfill是指一种功能的模拟,这些功能在新版本的浏览器中已经实现,然后用自定义的方式使其在老版本中兼容实现。
11.5 阻止修改
在ES5中已经引入了几个方法来防止对对象的修改。
有三种级别: 防止扩展, 密封, 冻结。
var person = {
name : "Nicholas"
}
Object.preventExtension(person);
person.age = 25;
就会悄悄的失败,如果是在 strict 的情况下会报错。
第十二章 浏览器嗅探
浏览器嗅探始终是web领域的一个热门话题。
12.1 User-Agent 检测
用户代理检测是更具客户端浏览器的 user-agent 字符串进行检测,但是最大的问题就是解析 user-agent 并不是很容易,有些浏览器为了保证兼容性,会复制另一个浏览器的 user-agent 字符串,每当一个新的浏览器出来的时候我们都需要对 user-agent 检测的代码进行重新修正。意味着我们无法预期的出了问题,只能等待问题出现后再进行处理。
如果你选用了 user-agent 检测,那么最安全的方式就是只去检测就浏览器,比如只针对 IE8及以下浏览器等。而不要判断比如 IE 的整个系列等。
12.2 特性检测
特性检测的原理就是为特定浏览器的各个特性进行检测,并当特性存在时处理问题。
比如早期的根据ID获取元素的特性检测
function getById( id ){
var element = null;
if( document.getElementById ){
element = document.getElementById( id );
}else if( document.all ){
element = document.all[id];
}else if( document.layers ){
element = document.layers[id];
}
return element;
}
特性检测的重要组成部分
1. 探测标准方法
2. 探测不同浏览器的特点方法
3. 当被检测方法不存在的时候提供合乎逻辑的备用方案。
12.3 避免特性推断
特性推断是更具一个特性的存在,推断另外一个特性是否存在。“如果他看起来像鸭子,就必定会像鸭子一样嘎嘎叫”
12.4 避免浏览器推断
12.5 如何取舍
特性推断和浏览器推断是非常糟糕的做法,应当不惜一切代价避免他,纯粹的使用特性检测是一种很好的方式,几乎在任何情况下都是你想要的结果。
对于 user-agent 检测,从来都不禁止他,因为总有非常适合的场景需要使用他。