第一部分 编程风格
"程序是写给人读的,只是偶尔让计算机运行一下" --- Donald Knuth
第1章 基本的格式化
1.1 注意缩进
1.使用TAB缩进
2.使用空格缩进
使用哪种缩进方式根据个人爱好,但是不要在同个文件中将两者混淆
1.2 语句结尾
虽然JavaScript的语句不以分好结尾不会引起错误,但是在某些时刻可能会引起一些不必要的错误,而且很难调试.
所以最好在语句结尾加上分号;
1.3 行的长度
一行代码太长的话不利于观看,推荐一行80个字符;
1.4 换行
通常在运算符后换行,下一行增加两个层级的缩进.
避免ASI机制自作主张的在行尾插入分号,导致一些错误
1.5 空行
在两段没有关联的代码之间添加空行.一般来讲,建议在下面的场景添加空行
- 在方法之间
- 在方法中的局部变量和第一条语句之间
- 在多行和单行注释之间
- 在方法内的逻辑片段之间插入空行,提高可读性
1.6 命名
-
驼峰式大小写命名法,由小写字母开始,后续每个单词首字母都大写.
变量的命名,命名前缀应当是名词,并且遵守驼峰式命名法
函数的命名,命名前缀应当是动词
|动词|含义|
|:---:|:---:|
|can|返回一个布尔值|
|has|返回一个布尔值|
|is|返回一个布尔值|
|get|返回一个非布尔值|
|set|用于保存一个值| -
常量
使用全大写字母与下划线命名 -
构造函数命名
首字母大写
1.7 直接量
JavaScript的原始类型: 字符串, 数字, 布尔值, null和undefined;
1.71 字符串
- 使用双引号
- 使用单引号
使用哪种方式都可以,但要风格统一
拼接字符串使用字符串连接符(+)
1.72 数字
- 不推荐省略浮点数的小数部分
- 不推荐省略小数的整数部分
- 不推荐使用八进制
1.73 null
下列场景应该使用null.
- 用来初始化一个变量,这个变量可能赋值为一个对象;
- 用来和一个已经初始化的变量比较,这个变量可以是也可以不是一个对象;
- 当函数的参数期望是对象时,用作参数传入;
- 当函数的返回值期望是对象时,用作返回值传出;
不应该使用null
- 不要使用null来检测是否传入了某个参数
- 不要用null来检测一个未初始化的变量
1.74 undefined
避免在代码中使用undefined;
1.75 对象直接量
直接声明对象来替换使用构造函数
1.76 数组直接量
直接声明数组而不是使用Array构造函数
第2章 注释
2.1 单行注释
- 独占一行的注释,用来解释下一行代码.这行注释之前总是有一个空行,且缩进层级
- 在代码的尾部注释
- 被注释的大段代码
2.2 多行注释
多行注释总是出现爱在将要描述的代码段之前,注释和代码之间没有空行间隔. 和单行注释一样,多行注释之前应当有一个空行,且缩进层级和其描述的代码保持一致.
2.3 使用注释
- 在代码难以理解时添加注释;
- 可能被认为是错误的代码
- 浏览器特性hack
- 文档注释
第3章 语句和表达式
所有的块语句都应当使用花括号,包括:
- if
- for
- while
- do...while...
- try...catch...finally
3.1 花括号的对齐方式
有两种主要的花括号对齐方式.
- 将左花括号放在快语句中第一句代码的末尾
if(condition) {
doSomething();
}
- 将左花括号放置于块语句首行的下一行.
if(condition)
{
doSomething();
}
3.2 块语句间隔
- 在语句名,圆括号,和左花括号之间没有空格间隔;
- 在左圆括号之间和右圆括号之后各添加一个空格;
- 在左圆括号之后和右圆括号之前各添加一个空格;
3.3 switch 语句
3.3.1 缩进
- 使用java风格的switch语句
switch(condition) {
case "first":
//代码
break;
case "second":
//代码
break;
default:
//代码
}
- 每天case语句相对于switch关键字都缩进了一个层级;
- 从第二天case语句开始, 每条case语句前后各有一个空行;
- 另外一种缩进风格是case之间没有空行;
个人编程风格选择,推荐使用第一种缩进风格;
3.32 case语句的"连续执行"
switch(condition) {
//明显的连续执行
case "first":
//代码
case "second":
//代码
break;
case "second":
//代码
break;
default:
//代码
}
3.4 with语句
在严格模式下,with语句是被明确禁止的.
强烈推荐避免使用with语句
3.5 for循环
for循环有两种,一种是传统的for循环,从C和java中继承来的;另外一种就是for-in循环,用来遍历对象的属性;
推荐尽可能避免使用continue,但也没有必要完全禁止,它的使用应当根据代码可读性来决定
3.6 for-in循环
for-in循环是用来遍历对象属性的;不用定义任何控制条件,循环将会有条不紊地遍历每个对象属性,并返回属性名而不是值;
for-in循环有一个问题,它不仅遍历对象的实例属性,同样还遍历从原型继承来的属性.
推荐在for-in循环使用hasOwnProperty().除非是想遍历原型,这时就应该添加注释;
第4章 变量、函数和运算符
JavaScript编程的本质是编写一个个的函数来完成任务;在函数内部,变量和运算符可以通过移动字节来使某件事发生
4.1 变量声明
因为存在变量声明提升
- 将局部变量的定义作为函数内第一条语句.
推荐使用单var语句,每个变量的初始化独占一行,赋值运算符应当对齐
var value = 10,
result = value + 1,
i,
len;
4.2 函数声明
- 推荐先声明函数再调用
函数声明不应当出现在语句块之内
4.3 函数调用间隔
对于函数调用写法推荐的风格是,在函数名和括号之间没有空格;
- 为了将函数与块语句区分开来
4.4 立即调用的函数
JavaScript中允许声明匿名函数,并将匿名函数赋值给变量或者属性;
这种匿名函数通过可以通过在最后加上一对圆括号来立即执行并返回一个值,然后将这个值赋给变量
//不好的写法
var value = function(){
//函数体
}()
- 为了让立即执行的函数能够被一眼看出来,可以将函数用一队圆括号包裹起来.
//好的写法
var value = (function(){
//函数体
}())
4.5 严格模式
不推荐将"use strict"用在全局作用域中
4.6 相等
推荐使用 === 和 !== 进行比较
第二部分 编程实践
"构建软件设计的方法有两种: 一种是把软件做得简单以至于明显找不到缺陷; 另一种就是把它做得很复杂以至于找不到明显的缺陷."
---C.A.R. Hoare, 1980年图灵奖获得者
第5章 UI层的松耦合
在Web开发中,用户界面(User Interface)是由三个彼此隔离又相互作用的层定义的.
- HTML用来定义页面的数据和语义;
- CSS用来给页面添加样式的, 创建视觉特征;
- JavaScript用来给页面添加行为, 使其更具交互性;
5.1 什么是松耦合
很多的设计模式就是为了解决紧耦合的问题, 当你能够做到修改一个组件而不需要更改组件的时候, 就做到了松耦合.
我们的目标是确保对一个组件的修改不会经常性地影响其他部分;
5.2 将JavaScript从CSS中抽离
避免使用CSS表达式.
5.3 将CSS从JavaScript中抽离
不好的写法
- 使用JavaScript直接修改DOM的style
- 使用JavaScript直接修改style的cssText属性
好的写法
- 使用JavaScript操作DOM的className属性
- 使用JavaScript操作DOM的classList属性(H5新增)
有一种使用style属性的情形是可以的: 需要给页面中的元素作定位, 使其相对于另外一个元素或整个页面重新定位.
5.4 将JavaScript从HTML中抽离
最好将所有的JavaScript代码都放入外置文件, 以确保在HTML代码中不会有内联的JavaScript代码;
5.5 将HTML从JavaScript中抽离
不好的做法
- 在JavaScript中使用innerHTML操作HTML
不利于调试, 可维护性差.
代替的做法
- 从服务器加载
- 模板字符串
第6章 避免使用全局变量
JavaScript的初始执行环境是由多种多样的全局变量所定义的, 这些全局变量在脚本环境创建之初就已经存在了
6.1 全局变量带来的问题
- 命名冲突
- 代码脆弱
- 难以测试
6.2 意外的全局变量
使用严格模式或者JSHint, JSLint等工具, 避免变量未声明直接赋值成为了意外的全局变量
6.3 单全局变量方式
"单全局变量"的意思是所创建一个独一无二的对象, 然后将所有的功能代码都挂载都该对象上;
6.31 命名空间
即使代码中只有一个全局变量, 也存在着全局污染的可能性;
命名空间是简单的通过全局对象的单一属性表示的功能性分组;
比如 YUI就是依照命名空间管理其代码, Y.DOM下的所有方法都是和DOM操作相关的
6.32 模块
JavaScript模块化开发: AMD / CMD 模式
6.4 零全局变量
实现方法是使用一个立即执行的函数调用并将所有脚本放置其中
(function(win){
//脚本
}(window))
第7章 事件处理
在所有的JavaScript应用中事件处理都是非常重要的.
7.1 典型用法
当事件触发时, 事件对象(event对象)会作为参数传入事件处理程序中
很多时候我们只用到了event对象的一部分;
function handleClick(event){
var popup = document.getElementById("popup");
popup.style.top = event.clientX + 'px';
popup.style.left = event.clientY + 'px';
popup.className = "reveal";
}
div.addEventListener('click', handleClick, false);
实际上这是非常不好的写法, 有其局限性;
- 规则: 隔离应用逻辑
将应用逻辑从事件处理程序抽离出来, 这样在其他地方触发同一段逻辑时可以灵活测试. 并且利于测试;
//拆分应用逻辑
var myHandle = {
handleClick: function(event){
this.showPopup(event);
},
showPopup: function(event){
var popup = document.getElementById("popup");
popup.style.top = event.clientX + 'px';
popup.style.left = event.clientY + 'px';
popup.className = "reveal";
}
}
现在事件处理程序handleClick只做一件事, 就是调用showPopup;
将应用逻辑剥离, showPopup的调用可以在多点触发
- 规则: 不要分发事件对象
应用逻辑不应该依赖于event对象来正确完成功能, 原因如下
- 方法接口并没有表明哪些数据是必要的.
- 因此, 想测试这个方法时, 必须重新创建一个event对象并将它作为参数传入;
//好的写法
var myHandle = {
handleClick: function(event){
this.showPopup(event.clientX, event.clientY);
},
showPopup: function(x, y){
var popup = document.getElementById("popup");
popup.style.top = x + 'px';
popup.style.left = y + 'px';
popup.className = "reveal";
}
}
- 当处理事件时, 最好让事件处理程序成为接触到event对象的唯一的函数; 事件处理程序应当在进入应用逻辑之前针对event对象执行任何必要的操作, 包括阻止默认事件或阻止事件冒泡;
//好的写法
var myHandle = {
handleClick: function(event){
event.preventDefault();
event.stopPropagation();
this.showPopup(event.clientX, event.clientY);
},
showPopup: function(x, y){
var popup = document.getElementById("popup");
popup.style.top = x + 'px';
popup.style.left = y + 'px';
popup.className = "reveal";
}
}
第8章 避免"空比较"
避免使用变量直接与null比较.
-
检测原始值, 使用typeof
对于字符串, typeof 返回 "string"
对于数字, typeof 返回"number"
对于布尔值, typeof 返回 "boolean"
对于 undefi, typeof 返回"undefined"
基本语法 typeof variable -
检测引用值使用 instanceof (Object, Array, Date等, typeof 对于这些都返回 "Object")
typeof null 返回 "Object" -
检测函数最好使用 typeof
-
检测数组, 使用 Object.prototype.toString.call(arr) 返回 "[object Array]"
-
检测属性是否在对象里使用 in
第9章 将配置数据从代码中分离出来
配置数据
- URL
- 需要展示给用户的字符串
- 重复的值
- 设置(比如每一页的配置项)
- 任何可能发生改变的值
- 将配置数据抽离保存在单独一个对象
- 将配置数据保存在一个单独文件, 如以 json格式
第10章 抛出自定义错误
- 推荐在错误消息中包含函数名称以及失败的原因
function getDivs(element) {
if (element && element.getElementsByTagName) {
return element.getElementsByTagName("div");
} else {
throw new Error("getDivs(): Argument must be a DOM element.");
}
}
何时抛出错误
- 一旦你修复了一个很难调试的错误, 尝试增加一两个自定义错误; 当再次发生错误时将有利于更好的解决问题;
- 如果正在编写代码, 思考一下: "我希望[某些事件]不会发生, 如果发生, 我的代码会一团糟糕". 这时, 如果"某些事件"发生, 就抛出一个错误;
- 如果正在编写的代码别人(不知道是谁)也会使用, 思考一下他们的使用方式, 在特定的情况下抛出错误;
try-catch 语句
当在try中发生了一个错误时, 程序立刻停止执行, 然后跳到 catch 块, 并传入一个错误对象. 还可以添加一个 finally 块, 不管是否有错发生, finally 都会执行
try {
somethingThatMightCauseAnError();
} catch (ex) {
handleError(ex);
} finally {
continueDoingOtherStuff();
}
- 何时使用throw 或者 try-catch
错误应该在程序的最深处抛出
不会catch 块中留空
错误类型
- Error: 错误的基本类型, 引擎从来不会抛出
- EvalError: eval函数执行发生错误
- RangeError: 一个数字超出它的边界
- ReferenceError: 期望的对象不存在时
- SyntaxError: 给eval函数传递的代码中有语法错误
- TypeError: 变量不是期望的类型
- URIError: 给encodeURL(), encodeURLComponent()等函数传递格式非法的URL字符
第11章 不是你的对象不要动
- 以下这些对象不要尝试去修改他们,因为他们已经存在了: 原生对象(Object, Array), DOM对象(document), 浏览器对象模型(BOM)对象(window), 类库的对象。 对待他们的原则是:不覆盖方法; 不新增方法; 不删除方法; 下面是一些不好的示例:
document.getElementById = function() {
return null; // talk about confusing
};
document._originalGetElementById = document.getElementById;
document.getElementById = function(id) {
if (id == "window") {
return window;
} else {
return document._originalGetElementById(id);
}
};
document.sayImAwesome = function() {
alert("You're awesome.");
};
Array.prototype.reverseSort = function() {
return this.sort().reverse();
};
YUI.doSomething = function() {
// code
};
document.getElementById = null;
- 更好的途经
//基于对象的继承
var person = {
name: "Nicholas",
sayName: function() {
alert(this.name);
}
};
var myPerson = Object.create(person);
myPerson.sayName(); // pops up "Nicholas"
//基于类型的继承
function MyError(message) {
this.message = message;
}
MyError.prototype = new Error();
var error = new MyError("Something bad happened.");
console.log(error instanceof Error); // true
console.log(error instanceof MyError); // true
//门面模式 与适配器唯一不同是 其创建新的接口,后者实现已经存在的接口
function DOMWrapper(element) {
this.element = element;
}
DOMWrapper.prototype.addClass = function(className) {
this.element.className += " " + className;
};
DOMWrapper.prototype.remove = function() {
this.element.parentNode.removeChild(this.element);
};
// Usage
var wrapper = new DOMWrapper(document.getElementById("my-div"));
// add a CSS class
wrapper.addClass("selected");
// remove the element
wrapper.remove();
阻止修改
- 防止拓展: Object.preventExtension() 不可添加新属性, 已存在属性可修改删除
- 密封: Object.seal() 已存在属性不可删除
- 冻结: Object.freeze() 已存在属性不可修改
第12章 浏览器嗅探
- User-agent 检测
// Bad
if (navigator.userAgent.indexOf("MSIE") > -1) {
// it's Internet Explorer
} else {
// it's not
}
// good 不要试图检测IE 9及其高版本,而应该只检测IE8以及之前的
if (isInternetExplorer8OrEarlier) {
// handle IE8 and earlier
} else {
// handle all other browsers
}
- 特性检测,推荐使用特性检测
// Bad
if (navigator.userAgent.indexOf("MSIE 7") > -1) {
// do something
}
// Good
if (document.getElementById) {
// do something
}
- 避免特性推断
// Bad - uses feature inference
// 不能从一个特性推断出另一个特性是否存在
function getById (id) {
var element = null;
if (document.getElementsByTagName) { // DOM
element = document.getElementById(id);
} else if (window.ActiveXObject) { // IE
element = document.all[id];
} else { // Netscape <= 4
element = document.layers[id];
}
return element;
}
- 避免浏览器推断,你不能根据某种方法存在就判定是某一种浏览器
// Bad
if (document.all) { // IE
id = document.uniqueID;
} else {
id = Math.random();
}
var isIE = !!document.all;
var isIE = !!document.all && document.uniqueID;
后面的章节就是文件目录, 以及自动化, 文件处理,压缩等.
网友评论