28 Feb 2017 08:09 +0000

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();
}

Loading comments...