<Effective JavaScript>

来源:互联网 发布:中国政治向左转知乎 编辑:程序博客网 时间:2024/06/13 23:53

Item 47:不要给Object.prototype添加可枚举的属性

归根结底,还是在说for..in的特性带来的问题。像下面的代码在Object.prototype上定义了一个方法,结果导致一些问题:

Object.prototype.allKeys = function () {  var result = [];  for (var key in this) {    result.push(key);  }  return result;};({  a: 1,  b: 2,  c: 3}).allKeys(); // ["allKeys", "a", "b", "c"]

所以说不要在原型上面定义方法,解决方法是另外定义一个独立的函数:

function allKeys(obj) {  var result = [];  for (var key in obj) {    result.push(key);  }  return result;}


但是更好的办法是使用ES5的新方法:Object.defineProperty。

Object.defineProperty(Object.prototype, "allKeys", {  value: function () {    var result = [];    for (var key in this) {      result.push(key);    }    return result;  },  writable: true,  enumerable: false,  configurable: true});

通过声明一个属性的enumerable为false,就让for..in看不见它了。


Item 50:用迭代方法代替循環來遍歷數組













Item 51:在类似数组的对象上使用数组的泛型方法

有一些对象虽然本身不是数组对象,也没有继承自Array,但它们具有一些行为类似数组对象,作者说Array.prototype上面的函数是被设计用来可以在这些类数组的对象上重用的,虽然这看起来不太符合直觉。


有两个东西是典型的“类数组”,arguments和NodeList。

function highlight() {[].forEach.call(arguments, function (widget) {    widget.setBackground("yellow");  });}

一个对象符合“类数组”有两个标准:

  • 有length属性,并且其值根据元素增减;
  • 有一个数字类型的索引属性键,它的值始终小于length。


所以普通对象也可以被改造成类数组:

var arrayLike = {  0: "a",  1: "b",  2: "c",  length: 3};var result = Array.prototype.map.call(arrayLike, function (s) {  return s.toUpperCase();}); // ["A", "B", "C"]


不修改原数组值的数组方法也可以在String上面使用:

var result = Array.prototype.map.call("abc", function (s) {  return s.toUpperCase();}); // ["A", "B", "C"]

唯一不能在类数组对象上直接使用的数组方法是concat。不过也有个解决办法,就是使用slice

function namesColumn() {  return ["Names"].concat([].slice.call(arguments));}namesColumn("Alice", "Bob", "Chris"); // ["Names", "Alice", "Bob", "Chris"]


MDN的slice页面里有关于类数组的讨论。


Item 53:统一惯例

这一节讲的跟代码运行没有关系,说的的是编码风格和规范的问题。比如命名规则和规范要有内在的一致性。另外也可以尽量与流行的框架和库的做法保持一致。这里我插一句Dogulas Crockford的相关建议,JavaScript自己有自己的风格,比如说方法用lower carmel case,首先遵循JS自身的风格。

举例来说:

如果你有些方法的签名是function(width, heihgt),那么就都保持width在前,height在后,不要定义一个函数签名为:function(height, width);

如果你有属性访问方法是叫setWidth(),来设定width的值,就不要在其他类上面定义width()来做相同操作;

CSS里,接受正方形参数的时候,顺序是top, right, bottom, left,也就是顺时针,那么如果你有方法也接受类似信息,最好就沿用这个顺序。


Item 54:把undefined理解为“没有值”

四种情况可能导致你的操作返回的是undefined:

  • 访问一个定义了但没有赋值的变量:
    var x;x; // undefined
  • 访问一个对象上没有定义变量:
    var obj = {};obj.x; // undefined
  • 访问一个直接return,或者根本没有return语句的函数:
    function f() {  return;}function g() { }f(); // undefinedg(); // undefined
  • 调用一个函数却没有给它的参数传值:
    function f(x) {  return x;}f(); // undefined


作者讲到一种设计思路:

以undefined作为一种启用特殊处理机制的信号,比如说有一个HTML元素类,它有个方法是给它上色的,如果传递进来的是一个颜色值,就使用该颜色,如果是undefined,就启用随机颜色。如下:

element.highlight(); // use the default colorelement.highlight("yellow"); // use a custom colorelement.highlight(undefined); // use a random color

这个设计当然并不好,因为在有些情况下,传递undefined值进来是由于异常,而不是调用者的本意,这样的情况将无法区分。比如这个颜色设定值来自外部IO:

var config = JSON.parse(preferences);// ...element.highlight(config.highlightColor); // may be random


关于这种情况,作者建议了两个方案:

  • 一:传递一个特殊字符串来作为信号
    element.highlight("random");
  • 二:传递一个设定对象
    element.highlight({ random: true });


另一种设计思路:

在函数里验证arguments数组的长度。

function Server(port, hostname) {  if (arguments.length < 2) {    hostname = "localhost";  }  hostname = String(hostname);  // ...}var s1 = new Server(80, "example.com");var s2 = new Server(80); // defaults to "localhost"

基于相同的原因,这个做法也不好。


总的来讲,明确地验证undefined是最万无一失的:

function Server(port, hostname) {  if (hostname === undefined) {    hostname = "localhost";  }  hostname = String(hostname);  // ...}function Server(port, hostname) {  hostname = String(hostname || "localhost");  // ...}


还有一种设计思路:

通过if(variable)或者||来验证传递的参数是否无效,这种验证在undefined的情况下会得到预期的效果,可是它却对所有falsy的值都会做同样处理,包括null,空字符串,false和0,而这不见得是预期的。

function Element(width, height) {  this.width = width || 320; // wrong test  this.height = height || 240; // wrong test  // ...}var c1 = new Element(0, 0);c1.width; // 320c1.height; // 240

总之这个方案在有些情况下是可行的,可是比如,如果是数字的话,并且0是可以作为有效值的情况下,就要改成:

function Element(width, height) {  this.width = width === undefined ? 320 : width;  this.height = height === undefined ? 240 : height;  // ...}var c1 = new Element(0, 0);c1.width; // 0c1.height; // 0var c2 = new Element();c2.width; // 320c2.height; // 240

好一些。


Item 55:用设定参数对象来传递参数

这一节关注的问题是函数的参数列表过长。参数过多会导致可读性降低,JS对此有一个策略就是传递一个对象,在对象上通过属性来表达参数。


写法就是:

function Alert(parent, message, opts) {  opts = opts || {}; // default to an empty options object  this.width = opts.width === undefined ? 320 : opts.width;  this.height = opts.height === undefined ? 240 : opts.height;  this.x = opts.x === undefined ? (parent.width / 2) - (this.width / 2) : opts.x;  this.y = opts.y === undefined ? (parent.height / 2) - (this.height / 2) : opts.y;  this.title = opts.title || "Alert";  this.titleColor = opts.titleColor || "gray";  this.bgColor = opts.bgColor || "white";  this.textColor = opts.textColor || "black";  this.icon = opts.icon || "info";  this.modal = !! opts.modal;  this.message = message;}var alert = new Alert(app, message, {  width: 150,  height: 100,  title: "Error",  titleColor: "blue",  bgColor: "white",  textColor: "black",  icon: "error",  modal: true});

这个写法也可以更好地解决默认参数值和可选参数的问题。这个做法可以进一步优化,就是使用estend()方法来合并两个对象的属性:

function Alert(parent, message, opts) {  opts = extend({    width: 320,    height: 240  });  opts = extend({    x: (parent.width / 2) - (opts.width / 2),    y: (parent.height / 2) - (opts.height / 2),    title: "Alert",    titleColor: "gray",    bgColor: "white",    textColor: "black",    icon: "info",    modal: false  }, opts);  extend(this, opts);}function extend(target, source) {  if (source) {    for (var key in source) {      var val = source[key];      if (typeof val !== "undefined") {        target[key] = val;      }    }  }  return target;}


有几点:

  • 在使用extend的版本里,无论如何计算width和height的逻辑都会被执行,而第一个里在不需要的情况下是会被省略的,不过这个问题不大,效率上的影响可以忽略;
  • 第一个版本会将空字符串也解读为没有提供值,而第二个则是验证undefined,这样更一致些。


Item 56:避免不必要的状态

这是个设计思路的问题。所谓状态指的是对象内部的属性的值,它们会影响对象上的一些方法的行为。举例来说,String.toUpperCase()的行为就是独立的,它不受对象的内部状态影响,这种就叫无状态API,相反,Date.now()返回的值就受到其内部状态的影响,这种就被称作状态化的API。


由此,作者提出的建议是,尽量令API无状态化,这样使用者会省去很多麻烦。为了做到这样的效果,往往就需要将一个操作所需要的所有数据都作为参数一次性传递给API。


作者给出了一个案例,假如我们需要设计API来读取配置文件里的设置,配置文件的格式如:

[Host]address=172.0.0.1name=localhost[Connections]timeout=10000

下面是一个不好的设计:

var ini = INI.parse(src);ini.setSection("Host");var addr = ini.get("address");var hostname = ini.get("name");ini.setSection("Connection");var timeout = ini.get("timeout");var server = new Server(addr, hostname, timeout);

下面是更好的设计:

var ini = INI.parse(src);var server = new Server(ini.Host.address, ini.Host.name, ini.Connection.timeout);



Item 57:结构类型化(看不懂)





Item 58:區別數組與類數組對象


关于类数组,也可以参考MDN。



Item 61:不要令I/O事件阻碍事件队列

先总结下作者提及的跟这个话题相关的JavaScript原理:

  • JavaScript对于那些耗时的操作,尤其是网络读写,提供了异步的机制,也就是你启动一个访问操作的时候提供一个回调函数,然后JS引擎继续去执行下面的其他代码,等这个事件完成了再去执行回调函数,这个机制最典型的就是Ajax了;
  • JavaScript其实是使用一个队列来存储所有被触发的事件的,按照先后顺序处理这些事件的回调函数;
  • 于是,看起来JS有能力并发性地接收事件。


作者这个标题的意思是,JS里有一类跟文件读写有关的函数并不遵照上面说的异步机制,而是同步的,也就是这类函数的执行会令整个程序的执行暂停下来,直到这个读写事件完成了,再继续执行下面的代码,这样的做法当然效率不高,因为耗时的读写操作阻滞了程序的运行,而作者在这一节里说的就是不要使用这类操作,可是其实我作为使用JS很多年的人,并没有听说过还有这类函数的存在,这一节似乎意义不大。


Item 62:对于一系列连续的异步调用,使用嵌套函数或者命名回调函数

这一节讲的都是关于可读性的问题。


当下面的代码:

db.lookupAsync("url", function (url) {  downloadAsync(url, function (text) {    console.log("contents of " + url + ": " + text);  });});

演变成:

db.lookupAsync("url", function (url) {  downloadAsync(url, function (file) {    downloadAsync("a.txt", function (a) {      downloadAsync("b.txt", function (b) {        downloadAsync("c.txt", function (c) {          // ...        });      });    });  });});

可以将代码改成:

db.lookupAsync("url", downloadURL);function downloadURL(url) {  downloadAsync(url, function (text) { // still nested    showContents(url, text);  });}function showContents(url, text) {  console.log("contents of " + url + ": " + text);}

或者在进一步,用bind完全抹掉嵌套回调函数:

db.lookupAsync("url", downloadURL);function downloadURL(url) {  downloadAsync(url, showContents.bind(null, url));}function showContents(url, text) {  console.log("contents of " + url + ": " + text);}

将上面那段极度长的代码按照这种思路重写就变成:

db.lookupAsync("url", downloadURLAndFiles);function downloadURLAndFiles(url) {  downloadAsync(url, downloadABC.bind(null, url));}// awkward namefunction downloadABC(url, file) {  downloadAsync("a.txt",  // duplicated bindings  downloadFiles23.bind(null, url, file));}// awkward namefunction downloadBC(url, file, a) {  downloadAsync("b.txt",  // more duplicated bindings  downloadFile3.bind(null, url, file, a));}// awkward namefunction downloadC(url, file, a, b) {  downloadAsync("c.txt",  // still more duplicated bindings  finish.bind(null, url, file, a, b));}function finish(url, file, a, b, c) {  // ...}

如果你觉得也不是很好看,就折中下命名回调函数和直接嵌套的用法:

db.lookupAsync("url", function (url) {  downloadURLAndFiles(url);});function downloadURLAndFiles(url) {  downloadAsync(url, downloadFiles.bind(null, url));}function downloadFiles(url, file) {  downloadAsync("a.txt", function (a) {    downloadAsync("b.txt", function (b) {      downloadAsync("c.txt", function (c) {        // ...      });    });  });}


Item 65:不要用复杂的计算阻滞事件队列

首先,再次强调,在JS代码运行的时候,浏览器是无暇处理其他事件的,比如用户点击一个按钮这样的用户事件,所以为了保证良好的用户体验,缩短每次JS的执行时间很重要。这个小节讲的话题是当计算量非常庞大时,如何减少执行时间。


第一个方案是Web Worker,这个在别的地方说过了,就不在这里重复了。

第二方案更通用,就是从算法上入手,如果一个复杂计算是可以分割成相互独立的步骤的话,就可以用循环与定时回调函数结合在一起把任务分割:

Member.prototype.inNetwork = function (other, callback) {  var visited = {};  var worklist = [this];  function next() {    if (worklist.length === 0) {      callback(false);      return;    }    var member = worklist.pop();    // ...    if (member === other) { // found?      callback(true);      return;    }    // ...    setTimeout(next, 0); // schedule the next iteration  }  setTimeout(next, 0); // schedule the first iteration};

先把计算任务分割成若干子任务,setTimeout会把下一次执行的子任务放在回调函数里,并且在事件队列里插入一个新事件,等它被处理时下个子任务就会被执行了。这个做法的妙处在于,后续的子任务的执行要遵循事件的排队机制,在setTimeout执行前插入到队列里的事件会被先处理,这样一来,子任务的执行之间就会执行其他的任务,就不会阻碍其他任务了。上面这段代码也是可以进一步改进的,比如每次子任务里循环的次数。











0 0