作者: Nicholas C. Zakas

第 9 章, 将配置数据从代码中分离出来

什么是配置数据

配置数据就是在应用中写死 (hardcoded) 的值, 如警告信息, 地址, 某个循环次数限制等.

抽离配置数据

// 将配置数据埋在代码中
function validate(value) {
    if (!value) {
        alert("数据不合法");
        location.href = "/errors/invalid.php";
    }
}

// 抽离配置数据
let config = {
    MSG_INVALID_VALUE: "数据不合法",
    URL_INVALID: "/errors/invalid.php",
};

function validate(value) {
    if (!value)
        alert(config.MSG_INVALID_VALUE);
        location.href = config.URL_INVALID;
    }
}

这样人们可以放心地修改这些配置数据而不用担心引起代码错误.

保存配置数据

配置数据最好放在单独的文件中, 以便清晰地分隔数据和应用逻辑. 用 Java 属性文件, JSON 文件, 或 JSONP 文件中都是可取的选项.

第 10 章, 抛出自定义错误

在 JavaScript 中抛出错误

throw语句在 JavaScript 中抛出一个错误, 错误可以是任意类型, 但是推荐以Error对象的形式抛出.

错误的好处

在合适的地方抛出恰当的错误可以使调试变得无比简单.

错误不一定要在程序中被 catch, 因为我们的任务不是避免和处理错误情况, 而是在错误发生时更加容易调试.

何时抛出错误

应该在最容易导致程序失败的地方抛出错误. 以下是一些抛出错误的良好经验:

  • 在修复一个很难高度的错误时, 在错误发生的地方抛出错误, 可以使下次发生相同错误时更容易调试
  • 在 "如果这地方出错, 逻辑会变得很混乱" 的地方抛出错误
  • 在会被别人使用的代码的容易误用的地方抛出错误

try-catch 语句

错误只应该在应用程序栈最深的部分抛出. 任何处理应用程序特定逻辑的代码都应该有错误处理有能力, 并且捕获从底层组件中抛出的错误.

错误类型

ECMA-262 规范指出了 7 种错误类型:

Error: 所有错误的基本类型, 引擎本身不会抛出此类型错误.

EvalError: 通过eval()函数执行代码发生错误时抛出.

RangeError: 一个数字超出其边界时抛出.

ReferenceError: 期望的对象不存在时抛出, 如试图在 null 对象上调用一个函数.

SyntaxError: 给eval()函数传递的代码中有语法错误时抛出.

TypeError: 变量不是期望的类型时抛出, 如"prop" in 3.

URIError: 给encodeURI(), encodeURIComponent(), decodeURI()或者decodeURIComponent()等函数传递格式非法的 URI 字符串时抛出.

自定义错误类型:

function MyError(message) {
    this.message = message;
}

MyError.prototype = new Error();

// 使用
throw new MyError("Something Wrong!");

第 11 章, 不是你的对象不要动

JavaScript 的一大特性, 或者说一大缺陷是, 所有东西都是可以修改的, 只要能接触到的东西, 都可以被无节制地修改. 因此, 统一一些代码的原则至关重要.

什么是你的

只有你创建的对象是你的!

原则

  • 不要覆盖别人的方法
  • 不要向别人的对象添加方法
  • 不要删除别人的对象的方法

更好的途径

1. 基于对象的继承
let person = {
    name: "Nicholas",
    sayName: function() {
        alert(this.name);
   }
};

let myPerson = Object.create(person, {
    name: {
        value: "Greg"
    }
});
myPerson.sayName();
2. 基于类型的继承
function MyError(message) {
    this.message = message;
}

MyError.prototype = new Error();

相较于原生类型, 继承开发者定义构造函数的类型的继承一般需要两步: 原型继承, 和构造器继承:

function Person(name) {
    this.name = name;
    this.sayHi = function () {
        alert(this.name);
    }
}

function Author(name) {
    Person.call(this, name); // 继承构造器
}

Author.prototype = new Person();

let me = new Author("Jack");
me.sayHi();
3. 门面模式

门面模式是一种流行的设计模式, 它为一个已存在的对象创建一个新的接口. 门面是一个全新的对象, 其背后有一个已存在的对象在工作. 门面有时也叫包装器, 它们用不同的接口来包装已存在的对象.

function DOMWrapper(element) {
    this.element = element;
}

DOMWrapper.prototype.addClass = function (className) {
    element.className += " " + className;
};

DOMWrapper.prototype.remove = function () {
    this.element.parentNode.removeChild(this.element)
};

// 用法
let wrapper = new DOMWrapper(document.getElementById("my-div"));
wrapper.addClass("selected");
wrapper.remove();

阻止修改

ECMAScript 5 引入了几个方法来防止对对象的修改. 包括:

  • 防止扩展, Object.preventExtension(person);, 禁止向对象添加属性和方法;
  • 密封, Object.seal(person);, 禁止删除对象属性和方法, 同时防止扩展;
  • 冻结, Object.freeze(person);, 禁止修改任何对象属性和方法, 同时密封;
let person = {
    name: "Nicholas"
};

Object.freeze(person);
console.log(Object.isExtensible(person)); // false
console.log(Object.isSealed(person)); // true
console.log(Object.isFrozen(person)); // true
person.name = "Greg"; // 正常情况下默默失败, strict 模式下抛出错误
person.age = 25; // 同上
delete person.name; // 同上

第 12 章, 浏览器嗅探

浏览器嗅探是保证页面正常工作的有效手段. 通过各种方法进行浏览器嗅探, 可以根据客户端的不同作出不同的响应.

User-Agent 检测

用户代理检测根据 user-agent 字符串navigator.userAgent来确定浏览器的类型. 该方法的缺陷在于用户代理可以被任意修改, 而且对于新浏览器的检测始终需要更新判断规则. 推荐只用在识别老旧版本浏览器的场景.

特性检测

特性检测是指直接调用某项特性, 根据返回结果正确与否判断浏览器类型的方法. 在某些情况下这是最优的浏览器嗅探手段.

特性推断 (应该避免)

特性推断指在特性检测外, 根据一项特性存在与否判断另一项特性存在与否的方法. 特性推断有很大的不稳定性, 不建议实际使用.

浏览器推断 (应该避免)

浏览器推荐和特性推断类似并且更加武断, 根据一项特性存在与否直接判断浏览器类型, 并默认该浏览器支持的特性全部可用. 同样不建议使用.

实际上, 因为逻辑不严密 (命题与其反命题无确定联系) , 使用特性推断和浏览器推断是不可能严谨地保证代码正常工作的.