<Effective JavaScript> - Note 03

本文探讨了JavaScript编程中的多个实用技巧,包括避免在Object.prototype上添加可枚举属性、使用迭代方法代替循环、统一编码风格、正确处理undefined、使用参数对象传递参数、避免不必要的状态等。

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(); // undefined
    g(); // undefined
  • 调用一个函数却没有给它的参数传值:
    function f(x) {
      return x;
    }
    f(); // undefined


作者讲到一种设计思路:

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

element.highlight(); // use the default color
element.highlight("yellow"); // use a custom color
element.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; // 320
c1.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; // 0
c1.height; // 0
var c2 = new Element();
c2.width; // 320
c2.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.1
name=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 name
function downloadABC(url, file) {
  downloadAsync("a.txt",
  // duplicated bindings
  downloadFiles23.bind(null, url, file));
}
// awkward name
function downloadBC(url, file, a) {
  downloadAsync("b.txt",
  // more duplicated bindings
  downloadFile3.bind(null, url, file, a));
}
// awkward name
function 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执行前插入到队列里的事件会被先处理,这样一来,子任务的执行之间就会执行其他的任务,就不会阻碍其他任务了。上面这段代码也是可以进一步改进的,比如每次子任务里循环的次数。











评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值