# 编写可维护的 JavaScript

# 第 1 章.基本的格式化
# 第 2 章.注释
# 第 3 章.语句和表达式
所有的块语句都应当使用花括号
- if...else
- for
- while
- do...while
- try...catch.finally
# 3.1 花括号的对齐方式
// 继承自Java风格
if (condition) {
// do sth...
} else {
// do sth...
}
2
3
4
5
6
# 3.2 Switch 语句
所有的
case从句都应当以break、return、或throw结尾case的连续执行是一种可接受的编程方法,只要程序逻辑清晰即可如果
default不进行逻辑执行,也应该写上注释表明情况
# 3.4 with 语句
with语句可以更改包含的上下文解析变量的方式。
// bad
var book = {
title: 'Mainainable JavaScript',
author: 'Nicholas C. Zakas'
}
var message = 'The book is'
with (book) {
// 不清晰, 不容易分辨出以下变量出现的位置
message += title
message += 'by' + author
}
2
3
4
5
6
7
8
9
10
11
- 严格模式中,
with语句是被明确禁止的,如果使用则报语法错误 - Douglas Crockford 的编程规范和 Google 的 JavaScript 风格指南禁止使用
with
# 3.5 for 循环
- 使用
break语句可以立即退出循环 - 使用
continue语句可以跳过当前循环迭代 - 尽量避免使用
continue而使用条件分支来做替代
# 3.6 for-in 循环
for-in 循环有一个缺陷,它不仅遍历对象的实例属性,同样也会遍历从原型继承来的属性。当遍历自定义对象的属性时,最好使用hasOwnProperty()方法做循环过滤,除非你想查找原型链。
var prop
for (prop in object) {
if (object.hasOwnProperty(prop)) {
console.log('Property name is' + prop)
console.log('Property value is' + object[prop])
}
}
2
3
4
5
6
7
# 第 4 章.变量、函数和运算符
JavaScript 编程的本质是编写一个个函数来完成任务。在函数内部,变量和运算符可以通过移动操作字节来完使某件事发生。
# 4.1 变量声明
- 变量提升意味着在函数内部任意地方定义变量和在函数顶部定义变量效果是完全一样的。
- 建议将局部变量的定义作为函数内第一条语句
# 4.2 函数声明
- 先声明 JavaScript 函数,后使用函数。
- 函数应该在条件语句的外部声明。
# 4.3 函数调用间隔
// bad 注意空格
doSomething(item)
// good
doSomeThing(item)
2
3
4
# 4.4 立即调用函数
// bad
var value = (function() {
// do sth...
return {
message: 'hi'
}
})()
// good 使用圆括号包裹
var value = (function() {
// do sth...
return {
message: 'hi'
}
})()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 4.5 严格模式
'use strict'是 ECMASript5 引入的,ECMAScript 引擎会将其识别为一条指令,以严格模式来解析代码。它不仅可作用于全局,也可以使用于局部。
- 避免在全局中开启
- 如果希望在多个函数中应用严格模式而不必写多处
'use strict',可以使用 IIFE。
// good
;(function() {
'use strict'
function doSth() {
// do sth...
}
function doSthElse() {
// do sth
}
})()
2
3
4
5
6
7
8
9
10
# 4.6 相等
- 由于强类型转换的缘故,推荐使用
===和!==
# 第 5 章.UI 层的松耦合
很多设计模式就是为了解决紧耦合的问题。如果两个组件耦合太紧密,则说明一个组件和另一个组件直接相关,这样的话,修改一个组件的逻辑,那么另一个组件的逻辑也需要修改。
本质上讲,每个组件需要保持足够瘦身来确保松耦合。
将 JavaScript 从 CSS 中抽离
作者举了一个比较老的例子,IE8 里的
expression()表达式,已经不可考,所以不做过多展开将 CSS 从 JavaScript 中抽离
做
className的切换而不是更改style属性将 JavaScript 从 HTML 中抽离
对于 1 级 DOM 模型 使用
element.onclick对于 2 级 DOM 模型 使用
addEventListener将 HTML 从 JavaScript 中能抽离
因作者列出的问题和解决方案已经相当过时,不做过多展开记录了。
# 第 6 章.避免使用全局变量
# 6.1 全局变量带来的问题
一般来讲,创建全局变量被认为是糟糕的实践,尤其是在团队开发的大背景下更是问题多多。随着代码量的增长,全局变量会导致一些非常重要的可维护性难题。全局变量越多,引入错误的概率也将会因此变得越来越高。
命名冲突
代码的脆弱性
难以测试
任何依赖全局变量才能正常工作的函数,只有为其重新创建完整的全局环境猜能正确的测试它。这意味着除了要管理全局环境的修改,还要在多个全局环境中维护它们:开发环境、测试环境和生产环境。
# 6.2 意外的全局变量
function func() {
var count = 10
title = 'Mintainable JavaScript' // 创建了全局变量
name = 'Nicholas' //创建了全局变量, 并且覆盖了window的默认name属性
}
2
3
4
5
避免意外的全局变量:
- 使用 JSLint 等工具来协助
- 启用严格模式 'user strict'
# 6.3 单全局变量方式
- YUI 定义了唯一一个
YUI全局对象 - jQuery 定义了
$和jQuery - Dojo 定义了一个
dojo - ...
# 6.3.1 命名空间
将对象按照功能维度进行命名空间分组:
例如 YUI 里 Y.DOM 下所有方法都是和 DOM 操作相关的,Y.Evnet 下的所有方法都是和事件相关的,以此类推。
# 6.3.2 模块
- YUI 例如在 YUI 中添加模块,参数包括:
- 模块名称
- 待执行函数(被称作工厂方法)
- 版本号
- 可选依赖列表
YUI.add(
'my-book',
function(Y) {
Y.namespace('Book.MaintainableJavaScript')
Y.Books.MaintainableJavaScript.author = 'Nicholas C.Zakas'
// do sth...
},
'version',
{ requires: ['dependency1', 'dependency2'] }
)
2
3
4
5
6
7
8
9
10
然后通过 YUI().use()方法并传入要加载的模块名称来使用创造的模块。
YUI.use('my-book', 'another-module-name', function(Y) {
console.log(Y.Books.MaintainableJavaScript.author)
})
2
3
除了 YUI,早期还有以下类似的模块解决方案
- AMD
- CMD
- UMD
# 6.4 零全局变量 IIFE
- 使用场景有限,只要代码需要被其它代码所依赖,就不能使用这种零全局变量的方式
- 如果在运行时会被不断扩展或修改,也不能使用
(function(win)){
'use strict' // 启用严格模式避免创建全局变量
// do sth...
}(window)
2
3
4
# 第 7 章.事件处理
大多数事件处理相关代码和事件执行环境仅仅耦合在一起,导致可维护性很糟糕。
# 7.1 典型用法
// bad
function handleClick(event) {
const popup = document.getElementById('popup')
popup.style.left = event.clientX + 'px'
popup.style.top = event.clientY + 'px'
popup.className = 'vereal'
}
addListener(element, 'click', handleClick)
2
3
4
5
6
7
8
这端代码只用到了evnet对象的两个属性:clientX和clientY,这种做法可正常执行但是有明显局限性:
- 事件处理程序包含了应用逻辑
- 测试时,需要制造事件的触发来完成
# 7.2 隔离引用逻辑
- 将应用逻辑从所有事件处理程序中抽离出来的做法是一种最佳实践
- 调用功能性代码最好的做法就是单个的函数调用
// better
const Application = {
// event对象仍然存在无节制的分发的问题
handleClick: event => {
this.showPopup(event)
},
showPopup: event => {
const popup = document.getElementById('popup')
popup.style.left = event.clientX + 'px'
popup.style.top = event.clientY + 'px'
popup.className = 'vereal'
}
}
addListener(element, 'click', event => {
Application.handleClick(event)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 7.3 不要分发事件对象
- 方法接口并没有表明哪些接口数据是必要的。好的 API 一定是对于期望和依赖都是透明的。
- 事件处理程序应当在进入应用逻辑之前针对
evnet对象执行任何有必要的操作
// good
const Application = {
handleClick: event => {
event.preventDefault()
event.stopPropagation()
this.showPopup(event.clientX, event.clientY)
},
showPopup: (x, y) => {
const popup = document.getElementById('popup')
popup.style.left = x + 'px'
popup.style.top = y + 'px'
popup.className = 'vereal'
}
}
addListener(element, 'click', event => {
Application.handleClick(event)
})
// 测试时,可以直接调用,而不用制造或模拟点击触发
Application.showPopup(10, 10)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 第 8 章.避免空比较
# 8.1 检测原始值
字符串、数字、布尔值、null 和 undefined。
- 除了
null,使用type运算符,它返回一个表示值的类型的字符串 typeof运算符的独特之处在于,将其用于一个未声明的变量也不会报错。- 未定义的变量和值为
undefined的变量通过typeof都会返回"undefined"
# 8.2 检测引用值
- 杜绝使用
typeof来检查null类型 instanceof不仅能检测构造该对象的构造器,也检测其原型链,基于此instanceof也可以检测自定义类型
# 8.2.1 检测函数
JavaScript 中函数也是属于引用类型,同样存在Function构造函数。
检测函数最好的方法是使用typeof,因为它可以跨帧使用。
// 不好的写法
function func() {}
console.log(func instanceof Function)
// 好的写法
console.log(typeof func === 'function')
2
3
4
5
# 8.2.2 检测数组
Juriy Zaytsev(也被称作 Kangax)给出的最优雅方案:调用某个值的内置toString()方法在所有浏览器中都会返回标准的字符串结果。
此方法仅识别内置对象时使用,但是应该避免对于自定义对象使用。
function isArray(value) {
return Object.property.toString.call(value) === '[Object Array]'
}
2
3
| 类型 | 表达式 |
|---|---|
| number | typeof value=== 'number' |
| string | typeof value=== 'string' |
| boolean | typeof value=== 'boolean' |
| undefind | typeof value=== 'undefined' |
| function | typeof value=== 'function' |
| null | value === null 使用value ==null可同时校验undefined和null |
| array | Object.property.toString.call(value) === '[Object Array]'或者Array.isArray(value) |
| others... | Error instanceof value |
# 8.3 检测属性
- 判断属性是否存在的最好使用方法是使用
in操作符,in仅仅会简单的判断属性是否存在但是并不会去读属性的值。 - 判断非 DOM 对象,使用
obj.hasOwnProperty(key) - 判断不确定的对象,使用
'hasOwnProperty' in obj && obj.hasOwnProperty(key)
# 第 9 章.将配置数据从代码中分离出来
代码无非是定义一些指令的集合让计算机来执行。我们常常将数据传入计算机,由指令对数据数据进行操作,并产生最终结果。当不得不修改数据时,问题就来了。任何你修改源码都会有引入 bug 的风险,且只修改一些数据的值也会带来一些不必要的风险,因为数据不应当影响指令的正常运行的。精心设计的应用应当将关键数据从主要源码中抽离出来,这样我们修改源码时才能更加放心
# 9.1 什么是配置数据
- URL
- 需要展现给用户的字符串
- 重复的值
- 设置
- 任何可能发生改变的值
// bad
function validate(value) {
if (!value) {
// 此数据可以被用户接触到,所以它可能会被频繁修改
alert('Invalid value') // 报错信息容易被修改
location.href = '/error/invalid.php' // 开发过程中,请求地址可能会被频繁修改
}
}
function toggleSelected(ele) {
if (hasClass(ele, 'selected')) {
// 多处使用'selected'这个className,容易遗漏
removeClass(ele, 'selected')
} else {
addClass(ele, 'selected')
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 9.2 抽离配置数据
- 首先是将配置数据从代码中抽离出来,拿到外部
- 所有的配置数据从函数中移除,并替换成
CONFIG对象中的属性占位符。 - 将整个
CONFIG对象放到单独的文件中,这样配置数据的修改可以完全与使用这些数据的代码隔离开。
//good
const CONFIG = {
MSG_INVALID_VALUE: 'Invalid value',
URL_INVALID: '/error/invalid.php'
CSS_SELECTED: 'selected'
}
function validate(value){
if(!value){ // 此数据可以被用户接触到,所以它可能会被频繁修改
alert(CONFIG.MSG_INVALID_VALUE) // 报错信息容易被修改
location.href = CONFIG.URL_INVALID // 开发过程中,请求地址可能会被频繁修改
}
}
function toggleSelected(ele){
if(hasClass(ele, CONFIG.CSS_SELECTED)){ // 多处使用'selected'这个className,容易遗漏
removeClass(ele, CONFIG.CSS_SELECTED)
}else{
addClass(ele, CONFIG.CSS_SELECTED)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 9.3 保存配置数据
- 使用 JAVA 格式
此处有个弊端,所有配置项必须是字符串格式
# 面向用户的信息
MASG_INVALID_VALUE = Invalid value
# URLs
URL_INVALID = /error/invalid.php
# Css Classes
CSS_SELECTED = selected
2
3
4
5
6
使用 JSON
Jsonp(JSON with padding)
使用 ESM推荐使用此方法
作者开源工具Props2Js ,读取 Java 属性文件并且给出以上三种格式输出,仅做简单了解即可。
# 第 10 章.抛出自定义错误
在 JavaScript 中抛出错误是一门艺术。摸清楚代码中哪里适合抛出错误是需要时间的。因此,一旦搞清楚了这一点,调试代码的时间将大大缩短,对代码的满意度也将急剧提升。
# 10.1 错误的本质
当某些非期望的事情发生时程序就会引发一个错误。
# 10.2 在 JavaScript 中抛出错误
使用Error对象
throw new Error('Sth bad happend')
# 10.3 抛出错误的好处
- 快速定位错误所在,控制台可以显示出行列等信息
//bad
function getDivs(elment) {
return elment.getElementByTagName('div')
}
// good
function getDivs(elment) {
if (element && element.getElementByTagName) {
return elment.getElementByTagName('div')
} else {
throw new Error('getDivs(): Argument must be a Dom Element')
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 10.4 何时抛出错误
抛出错误的目的是方便调试而不是防止错误!!!
- 如果一个函数只被已知的实体调用,错误检查很可能就没有必要;相反,如果不能确定,则有必要做错误检查
- 一旦修复了一个很难调试的 bug,尝试增加一两个自定义错误
- 如果正在编码,多思考“我希望[某些事情]不会发生”
- 如果正在编写的代码别人也会使用,多思考他们的使用方式,在特定情况下抛出异常
// bad : 过度的参数校验
function addClass(element, className) {
if (!element || typeof element.className !== 'string') {
throw new Error('addClass(): First arg must be a Dom Elemnt')
}
if (typeof className !== 'string') {
throw new Error('addClass(): Second arg must be a string')
}
element.className += ' ' + className
}
2
3
4
5
6
7
8
9
10
// good
function addClass(element, className) {
if (!element || typeof element.className !== 'string') {
throw new Error('addClass(): First arg must be a Dom Elemnt')
}
element.className += ' ' + className
}
2
3
4
5
6
7
# 10.5 try-catch 语句
- 不要将
catch子句块忽略留空
# 10.6 错误类型
内置错误类型:
抛出实际的错误类型,浏览器会附加一些额外信息
- Error 所有错误的基本类型,以下所有的错误类型都继承自它。实际上引擎从来不会抛出该类型的错误。
- EvalError 通过
eval()函数执行代码发生错误时抛出。 - RangeError 一个数字超出它的边界时抛出。
- ReferenceError 期望的对象不存在时抛出。
- SyntaxError 有语法错误时抛出
- TypeError 变量不是期望的类型时抛出
- URIError 给
encodeURI、encodeURIComponent、decodeURI、decodeURIComponent非法参数时抛出
自定义错误类型:
- 可以检测自己的错误(或业务相关)
function MyError(message) {
this.message = message
}
MyError.prototype = new Error()
2
3
4
# 第 11 章.不是你的对象不要动
# 11.1 哪些对象不是你的
- 原生对象(Object、Array、Date )等等
- DOM 对象
- BOM(宿主环境 window 等)对象
- 类库的对象
以上这些对象理论上仅能做读操作,不能去修改它们。
# 11.2 原则
- 不修改方法 -- 当心函数劫持
- 不新增方法 -- 当心命名冲突
- 不删除方法 --当心后续调用
# 11.3 更好的途径
- 使用继承
- 基于对象的继承(原型继承)和基于类型的继承(实例继承)