《编写可维护的 JavaScript》 读书笔记(编程实践)

最后更新:
阅读次数:

构建软件设计的方法有两种:一种是把软件做到很简单以至于找不到缺陷;另一种是把软件做到很复杂以至于找不到明显的缺陷。(C.A.R Hoare,1980 图灵奖得主)

UI 层的松耦合

  • 在 Web 开发中,用户界面是由三个彼此隔离又相互作用的层定义的。

    • HTML:定义页面数据和语义。现在流行把某些数据从 HTML 中分离出去,比如可以成为页面配置数据(比如页面标题)的数据,当然这些都是通过 HTML 模板中完成的。编译完的代码数据还是在 HTML 中。
    • CSS:给页面添加样式
    • JavaScript:给页面添加行为
  • 将 CSS 从 JavaScript 中抽离,即不要通过元素的 style 属性来为元素添加或删除 CSS,而是通过动态为元素添加或删除某个 className 来达到同样的效果。

// bad
element.style.color = "green";
element.style.cssText = "color:red;display:block";

// good
element.classList.add("className");

当然,上面的规则仅供参考。因为有时候直接操作元素的 style 属性会比添加类名方便很多(比如 JavaScript 利用绝对定位实现瀑布流时)。

  • 将 JavaScript 从 HTML 中抽离
// bad,新手写法
<button onclick="alert('已提交')">提交</button>
  • 将 HTML 从 JavaScript 中抽离
// bad
var el = document.getElementById('container');
el.innerHTML = '<h3>Error</h3><p>无效的邮件名~</p>';

// 使用 HTML 模板解决上面的问题
获取元素 element
获取模板
更改模板数据
插入页面指定位置

事件处理

  • 隔离应用逻辑(即将应用逻辑从事件处理程序中取出)
// bad

function handler(event) {
let el = document.getElementById("box");
el.style.left = event.clientX + "px";
el.style.top = event.clientY + "px";
el.classList.add("show");
}

element.addEventListener("click", handler);

// good

var handle = {
// 事件处理程序
handleClick: function(event) {
this.showPopup(event);
},

// 应用逻辑
showPopup: function(event) {
let el = document.getElementById("box");
el.style.left = event.clientX + "px";
el.style.top = event.clientY + "px";
el.classList.add("show");
}
};

element.addEventListener("click", function(event) {
handle.handleClick(event);
});

这样做有什么优势呢?一个优势就是增强代码的复用性。比如现在通过某个按键可以触发与点击相同的效果,隔离应用逻辑的代码直接调用即可。而前者还需要重新复制一遍程序,造成资源浪费。

  • 不要分发事件对象

上面的代码在剥离出应用逻辑后,还存在一个问题,就是 event 事件对象被无节制分发。简单阐明问题:就是我们实际用到的事件对象的属性就几个,所以不需要把整个 event 对象都传给应用逻辑。

解决上面的问题的最佳办法就是:让事件处理程序成为接触到 event 对象的唯一函数,然后拿到所有需要的数据(特指 event 对象的属性)传给应用逻辑。并且事件处理程序应当在进入应用逻辑之前针对 event 对象执行任何必要的操作(比如阻止默认事件、阻止事件冒泡等)。

// better

var handle = {
// 事件处理程序
handleClick: function(event) {
// 做一些事件对象的必要处理
event.preventDefault(); // 阻止默认事件
event.stopPropagation(); // 阻止事件冒泡

// 将必要数据传入应用逻辑
this.showPopup(event.clientX, event.clientY);
},

// 应用逻辑
showPopup: function(x, y) {
let el = document.getElementById("box");
el.style.left = x + "px";
el.style.top = y + "px";
el.classList.add("show");
}
};

element.addEventListener("click", function(event) {
handle.handleClick(event);
});

检测

  • Object.prototype.toString.call(value): 可以用来检测基本类型和内置的引用类型,很有效。但是请不要用它来检测自定义对象类型。

  • Object.prototype.hasOwnProperty(obj,prop..): 检测某个属性是不是对象自己的(这不是书中作者给出的,书中给出的 hasOwnProperty() 有漏洞)

将配置数据从代码中分离出来

配置数据就是指在应用中写死的值,比如 URL、设置(每页的配置项)、需要展现给用户的固定值等。

// 配置数据

var config = {
ERROR_MESSAGE: 'Ooop, something wrong here~',
ERROR_URL; '/errors/error.php'
};

// some code

function detect(){
// do something
}

注意,平常开发时,也可将配置数据单独写成一个文件(比如 JSON 文件),方便模块化开发。

抛出自定义错误

  • try-catch: 捕获一个错误
  • throw: 抛出一个错误

  • 如何正确地抛出一个错误呢?

// bad
throw "error happened~"; // 报错

// good
try {
throw "error happened~";
} catch (e) {
console.log(e);
}

// good
throw new Error("error happened~"); // 正确地抛出错误

有一点需要注意,如果没有通过 try-catch 语句,throw 抛出任何值(除过 Error 对象)都将引发一个错误。

别动别人的对象

  • 不重写、不新增、不删除 JavaScript 内置的对象的属性方法(包括 DOM API 的属性方法)

  • 阻止别人对你的对象的修改

    • Object.preventExtensions(obj): 将对象变为不可扩展对象,即不能给对象添加新属性方法,但是可以删除原有的属性方法,也可更改属性方法的值
    • Object.seal(obj): 将对象变为密封对象,不能添加和删除属性方法,但可以该原有属性方法的值
    • Object.freeze(obj): 将对象冻结,不能添加和删除属性方法,也不能更改原有属性方法的值