<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执行前插入到队列里的事件会被先处理,这样一来,子任务的执行之间就会执行其他的任务,就不会阻碍其他任务了。上面这段代码也是可以进一步改进的,比如每次子任务里循环的次数。
- <Effective JavaScript>
- <Effective JavaScript>
- <Effective JavaScript>
- effective javascript(-)
- 《Effective Javascript》
- Effective JavaScript 学习笔记
- effective javascript第一章
- effective javascript 第二章
- Effective JavaScript 笔记(总)
- Effective Javascript (类型转换原理)
- Effective JavaScript 读书笔记 1 严格模式
- Effective JavaScript 读书笔记 2 浮点数
- Effective Javascript (类型转换原理)
- Effective JavaScript Basics Item 1-6
- Effective JavaScript String Encoding Item 7
- Effective JavaScript Item 10 避免使用with
- Effective JavaScript Item 11 掌握闭包
- Effective JavaScript Item 12 理解Variable Hoisting
- JavaWeb开发知识总结(tomcat)
- JavaWeb开发知识总结(HTTP,servlet)
- Tomcat、JVMj的监控
- 从NPM到CNPM
- JavaWeb开发知识总结-HttpServletRequest,HttpServletResponse
- <Effective JavaScript>
- Tomcat的环境变量配置
- 使用express作为前端和后台的中间层Demo
- 素数筛选法
- MySQL数据库
- 子类转换成父类的规则
- 464. Can I Win
- 变量名命名规则,构造方法的作用,类名命名规则
- 多线程 countDownLatch