Author: Nicholas C. Zakas
编程实践
第 5 章, UI 层的松耦合
Web 开发中, 用户界面 (User Interface, UI) 是由三个彼此隔离又相互作用的层定义的:
- HTML, 定义页面数据和语义
- CSS, 给页面添加样式, 创建视觉特征
- JavaScript, 给页面添加行为, 使其更具交互性
松耦合
耦合就是组件间的相关性.
因为耦合的定义, 不可能做到组件间无耦合. 同时从本质上讲, 每个组件需要保持足够瘦身来确保松耦合. 即, 组件功能越单一, 就越有利于形成松耦合的系统.
将 JavaScript 从 CSS 中抽离
现在已经没有人将 JavaScript 嵌入 CSS 中了, 以后也永远不要这么做.
将 CSS 从 JavaScript 中抽离
尽量不要用 JavaScript 直接操作样式, 取而代之的, 操作 class:
// 不好的写法
element.style.color = "red";
// 好的写法 (样式在 CSS 中定义)
$(element).addClass("reveal");
当然, 在完成一些 CSS 无法达成的操作时, 用 JavaScript 操作样式也是可取的.
将 JavaScript 从 HTML 从抽离
<!-- 不好的写法 -->
<button onclick="doSomething()" id="action-btn">Click Me</button>
// 好的写法
function doSomething() {
// something
}
let btn = document.getElementById("action-btn");
btn .addEventListener("click", doSomething, false);
将 HTML 从 JavaScript 中抽离
// 不好的写法
let div = document.getElementById("my-div");
div.innerHTML = "<p>Hello</p>";
相比于将 HTML 嵌入 JavaScript 代码, 采用这些方法把 HTML 从 JavaScript 中抽离:
1. 从服务器加载
2. 简单的客户端模版
HTML:
<li><a href="%s">%s</a></li>
JavaScript:
function sprintf(text) {
let i=1, args=arguments;
return text.replace(/%s/g, function() {
return (i < args.length) ? args[i++] : "";
});
}
// 用法
let result = sprintf(templateText, "/item/4", "Fourth item");
HTML 可以放在 HTML 代码中, 进行注释或者设置样式使其不直接显示于页面上.
3. 复杂的客户端模版
可以考虑用 Handlebars 等模版引擎在客户端形成更健壮的 HTML 模版.
第 6 章, 避免使用全局变量
全局变量的问题
滥用全局变量容易造成:
- 命名冲突
- 代码脆弱, 难以维护
- 难以测试
意外的全局变量
对一个未初始化的变量赋值, 会自动创建一个全局变量. 应该避免这样的情况. 在严格模式下这样的行为是不允许的:
function doSomething() {
let count = 10;
title = "Max"; // 意外创建了全局变量
}
function doSomething() {
"use strict";
title = "Max"; // 在严格模式下会报错, 因为 title 未声明
}
单全局变量
// 不推荐, 创建了多个全局变量
let nameA = "Max",
nameB = "Jack",
nameC = "Tom";
// 推荐, 单全局变量
let personName = null;
personName.a = "Max";
personName.b = "Jack";
personName.c = "Tom";
扩充方法 1: 命名空间
即使代码只有一个全局变量, 当多人合作时, 仍然存在全局污染的可能. 而通过对单全局变量细分命名空间的方式, 可以大大减少命名冲突问题.
扩充方法 2: 模块
JavaScript 本身不包含正式的模块概念, 也没有对应的模块语法. 但可以借助流行的框架进行模块管理和加载.
零全局变量
在一种特殊情况下, 可以做到不创建任何全局变量. 即在一个完全独立的文件, 或者仅在单页面中使用的, 不提供任何接口的代码中, 用匿名函数包裹所有脚本的方式创建书签:
(function(win) {
let doc = win.document;
// something
}(window));
这样的代码没有任何全局变量, 不会和其他任何代码发生冲突, 但是它不能被其他代码所依赖, 也不能在运行过程中扩展或修改.
第 7 章, 事件处理
典型用法
// 不好的写法
function handleClick(event) {
let popup = document.getElementById("popup");
popup.style.left = event.clientX + "px";
popup.style.top = event.clientY + "px";
popup.className = "reveal";
}
addListener(element, "click", handleClick);
规则1: 隔离应用逻辑
将应用逻辑从事件处理程序中分离, 可以使应用逻辑更易复用:
let MyApplication = {
handleClick: function(event) {
this.showPopup(event);
},
showPopup: function(event) {
let 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));
});
以上代码将应用逻辑分离到MyApplication.showPopup()
方法中, 使MyApplication.handleClick()
方法的功能简化为一件事情: 调用应用逻辑MyApplication.showPopup()
. 这样, 其他地方可以很方便地调用showPopup()
逻辑.
规则2: 不要分发事件对象
以上代码的问题在于无节制地分发事件对象, 因为MyApplication.showPopup()
中用到的数据只有两个, 因此, 应该将事件对象中有用的数据抽离出来:
let MyApplication = {
handleClick: function(event) {
this.showPopup(event.clientX, event.clientY);
},
showPopup: function(x, y) {
let popup = document.getElementById("popup");
popup.style.left = x + "px";
popup.style.top = y + "px";
popup.className = "reveal";
}
};
addListener(element, "click", function(event) {
MyApplication.handleClick(event));
});
这样我们可以在需要扩展的时候, 方便地调用这段逻辑:
MyApplication.showPopup(10, 20);
最佳实践是: 使事件处理程序成为接触事件对象的唯一函数, 然后根据应用逻辑将数据提取并进行分发. 阻止默认事件, 或阻止事件冒泡等针对事件的操作, 应该在事件处理程序中完成.
let MyApplication = {
// 只负责处理事件, 不关心应用逻辑
handleClick: function(event) {
// 针对事件的操作
event.preventDefault();
event.stopPropagation();
// 传入应用逻辑
this.showPopup(event.clientX, event.clientY);
},
// 只负责应用逻辑
showPopup: function(x, y) {
let popup = document.getElementById("popup");
popup.style.left = x + "px";
popup.style.top = y + "px";
popup.className = "reveal";
}
};
addListener(element, "click", function(event) {
MyApplication.handleClick(event));
});
第 8 章: 避免 "空比较"
有一种典型的不好的做法:
function doSomething(item) {
if (item !== null) {
// something
}
}
因为 JavaScript 的弱类型特性, 一个变量非 null 并不能说明任何问题, 相比以上 "空比较" , 采用以下方法:
检测原始值
JavaScript 有 5 种原始类型: 字符串, 数字, 布尔值, null 和 undefined. 在判断字符串, 数字, 布尔值和 undefined 时, 最佳选择是使用 typeof 运算符:
// 检测字符串
if (typeof name === "string") {
anotherName = name.substring(3);
}
// 检测数字
if (typeof count === "number") {
count++;
}
// 检测而尔值
if (typeof found === "boolean" && found) {
alert("Found!");
}
// 检测 undefined
if (typeof MyApp === "undefined") {
MyApp = {
// something
}
}
前面提到过的, 最好养成使用
===
的习惯. 即使在==
和===
效果完全相同的情况下, 也不要省这一个字符的代码.
typeof 的一个好处是, 对未定义的变量运算也不会报错, 而且对于未声明的变量和声明却未初始化的变量, 都是返回 undefined.
但对于 null, 用typeof null === "null"
是一种低效的做法, 检测 null 可以直接使用===
和!==
二者.
// 检测 null
if (element !== null) {
// something
}
检测引用值
引用值即对象 (object) , JavaScript 中除了原始值之外的值都是引用. 内置的引用类型包括: Object, Array, Date, Error 等. typeof 检测这些引用值都会返回 object, 因此需要另外的手段检测引用值:
// 检测日期
if (value instanceof Date) {
console.log(value.getFullYear());
}
// 检测正则表达式
if (value instanceof RegExp) {
return value.test(anotherValue);
}
// 检测 Error
if (value instanceof Error) {
throw value;
}
instanceof 不仅检测对象直接构造器, 也检测原型链.
instanceof 也用于检测自定义类型:
function Person(name) {
this.name = name;
}
let me = new Person("Nicholas");
console.log(me instanceof Object); // true
console.log(me instanceof Person); // true
但用 instanceof 检测自定义类型有一个问题:
假设一个浏览器帧 (frame A) 里的一个对象被传入到另一个帧 (frame B) 中. 两个帧里都定义了构造函数 Person. 如果来自帧 A 的对象是帧 A 的 Person 的实例, 则:
// true
frameAPersonInstance instanceof frameAPerson;
// false
frameAPersonInstance instanceof frameBPerson
instanceof 无法辨别 frameAPersonInstance 是否是 frameBPerson 的实例. 默认地, frameAPersonInstance 被认为是 frameAPerson 的实例, 尽管有可能 frameAPerson 和 frameBPerson 的定义是完全相同的.
检测函数
检测一个函数的最佳方法是使用 typeof. 尽管函数也是引用, 但 typeof 对函数会返回 "function" .
检测数组
检测数组的最佳方法是使用isArray()
方法, 其使用与内部逻辑如下 (ECMAScript5 后isArray
已经被引入了 JavaScript 中) :
if (isArray(items)) {
doSomething();
}
function isArray(value) {
return Object.prototype.toString.call(value) === "[object Array]";
}
检测属性
用 in 或hasOwnProperty()`方法检测对象是否有属性:
if ("name" in object) {
doSomething();
}
if (object.hasOwnProperty("name")) {
doSomething();
}