《JavaScript模式》读书笔记
前言:
模式是针对普遍问题的解决方案。更进一步地说,模式是解决一类特定问题的模版。
第一章:简介
在软件开发过程中,模式是指一个通用问题的解决方案。 一个模式不仅仅是一个可以用来复制粘贴的代码解决方案,更多地是提供一个更好的实践经验、有用的抽象化表示和解决一类问题的模版。关键词: 表示、解决
学习模式的好处:
- 学习模式,我们可以使用经过实践证明有效的经验来编写代码,而无需做很多无用的工作。
- 模式提供了某种程度上的抽象。 这样,在解决复杂问题时,想到这个模式即可,无需考虑太多细节。
- 模式可以改进开发者和开发团队之间的交流。 如描述“用大括号括上一个函数,并在这个刚刚定义的函数的结束位置放置另一个括号来调用这个函数”, 如果大家都学习了模式,我们直接说“立即函数”会更加简单明了。
三种类型的模式:
- 设计模式 --- 与语言无关,更多是根据强类型语言的视角研究的,而js是弱类型语言。
- 编码模式 --- 是js特有的模式, 是本书的主题。
- 反模式 --- 常见的、引发的问题比解决的问题更多的一种方法。 如在js中使用 == 而不是使用 === 判断是否相等。
对象有两种类型
- 原生的 --- 在ECMAScript中有详尽的描述,进一步分为内置对象(如数组、日期等)和 用户自定义对象 (如 var o = {};)。
- 宿主的(主机的) --- 在主机环境中定义的(例如浏览器环境),如window对象和所有的DOM对象。
第二章:基本技巧
1.尽量少用全局变量
全局变量的问题: 1. 全局变量会在整个JavaScript应用或web页面内共享。 他们生存在同一个命名空间内,总有可能发生命名冲突。尤其是一个应用程序中两个独立的部分(如两个js文件)定义了同名的全局变量,但却有不同目的时。 而我们只要减少全局变量的数目,就可以减少发生冲突的可能性。
使用var 定义的全局变量不可以删除; 不使用var的全局变量可以删除。说明,使用var 定义就是一个变量, 不使用var定义就是一个对象的属性。
2. 单一var模式
即使用一个 var 声明多个变量。
优势: 声明为局部变量、 更少的代码、 更简洁的操作。
3. 注意变量提升
考虑下面的代码:
log(c); var c = 1000; log(c);
第一个log输出 undefined; 第二个log输出 1000;
上面的代码等同于:
var c; log(c); c = 1000; log(c);
因为开始c只是声明了,没有初始化。在 c = 1000;才初始化。
4.优化for循环
for循环主要用于两种情况,遍历 数组 和 类数组对象(HTML容器对象)的。
数组就是Array对象, var arr = [1, 2, 3, 4]; 这就是一个数组。
而HTML容器对象是DOM方法返回的对象,如: document.getElementByName() document.getElementsByClassName() document.getElementsByTagName()都是 dom方法返回的HTML容器对象。
HTML容器对象的麻烦之处在于: 他们都是document(HTML页面)下活动的查询。也就是说每次再访问任何容器的长度时,都是在查询活动的DOM,并且DOM是非常耗时的。
于是,下面的循环是不可取得:
for (var i = 0; i < myArray.length; i++) { // 对myArray进行操作 }
如果myArray是一个数组还好,每次读取它的长度耗时并不是很严重。
但是如果myArray是一个HTML容器对象(类数组对象),那么操作DOM就会非常耗时。
好的方法:
for (var i = 0, len = myArray.length; i < len; i++ ) { // 对myArray进行操作 }
这种方法里,我们现将myArray.length缓存了起来,这样在后续的循环过程中, 就不会每次循环都读取length,进而减少了大量的DOM访问,这样的方法在safari中速度提高两倍,在IE7中速度提高170倍。
5. 不要将for - in 循环和for循环混用
刚刚提到,for循环主要是用于遍历数组和类数组对象的(统统与数组有关),而for-in循环主要是用于遍历一个普通对象的属性的(与数组无关)。
虽然,有时候混用是可行的,但是这样我们的代码逻辑就会大大降低。
在使用 for 循环时,我们最好使用 hasOwnProperty()方法。
6.不要增加内置的原型
增加构造函数的原型属性是一个增强功能性的强大方法,但是这样会严重影响可维护性,因为你的同伴往往希望内置的JavaScript的方法使用是一致的,而不期望您增加自己的方法。
但是如果我们发现 未来的ECMASCRIPT版本将某一方法作为统一的实现,只是现在还有一些不支持时,我们就可以自己添加构造函数上的方法。 另外,如果其他地方已经支持了此方法,您这不支持,也可以添加。
为原型添加自定义的方法:
if (typeof Object.prototype.myMethod !== "function") {
Object.prototype.myMethod = function () {
// implementation...
};
}
7. 避免使用隐式类型转换
我们应当尽量采用 == 而不是 === 来判断是否相等。
我们认为使用 == 来判断是否相等就是反模式(可能产生的问题比解决的问题要多), 而 === 是一种更为标准的模式。
8.避免使用eval()
其实这个还是很好理解的,因为大多数时候,我们很少见到使用eval()方法的情况。
并且记住:eval()是一个魔鬼
为什么? 因为eval()可以将任意字符串当作一个JavaScript代码来执行。
9. 使用parseInt数值约定
大多数情况下,parseInt顾名思义就是将接收的参数解析出整数, 一般,我们可能给传递一个字符串,但是,最佳实践是传递第二个参数表示把这个字符串中的数字看成几进制。ES3和ES5的表现是不一致的。
10. 空格在函数中的使用
下面两个函数一个是函数声明的方式定义,另一个是匿名函数的形式定义,注意空格的区别所在:
function addTwoNumber() { return 10 + 20; } var addTwoNumber = function () { return 10 + 20; }
这便是最好的空格的保留习惯 --- 在函数声明时,函数名和圆括号之间没有空格; 在匿名函数时, function 与 ()之间是有一个空格的。
11. 编写API文档
举例来说, 如果有一个名为reverse()的函数, 该函数可以将字符串翻转过来,该函数有一个字符串参数并返回另一个字符串,那么可以按照如下的方式来记录文档:
/** * 翻转一个字符串 * * @param {String} 输入需要翻转的字符串 * @return {String} 翻转后的字符串 */ var reverse = function () { //... return ouput;
};
12. 注意一个变量如果没有使用var,那么它将成为全局的,对于函数也是一样的,一个函数在定义时没有使用var,那么它也将是全局的,因为函数名就是一个指针,指针就是变量。
第三章: 字面量和构造函数
1. 相对于使用构造函数创建对象,我们更倾向于使用对象字面量的方式创建对象。
原因有二:
(1). 字面量表示法的显著优点在于它仅需要输入更少更短的字符,这是一种优美的对象创建方式
(2). 与使用object构造函数相对, 使用字面量的另一个原因在于它并没有作用域的解析。因为可能以同样的名字创建了一个局部构造函数,解析器需要从Object()的位置开始一直向上查找作用域链,直到发现全局object构造函数,但是对象字面量的方法就不会有这样的解析过程。
2. 相对于使用 new Array() 方式创建数组,我们更倾向于使用数组字面量的方式创建数组。
JavaScript中的数组也是对象, 虽然可以通过 new Array() 来创建,但这不是我们所推荐的。
原因有二:
(1). 数组字面量表示法简单、明确、优美。
(2). 使用 new Array() 会出现陷阱 --- 如 var arr = new Array(3); 这里的3是数组的长度, 而这个数组中却什么都没有。 又如使用 var arr = new Array (3.14); 那么就会报错,因为3.14不是合法的长度。
注: 一般情况下, 为了将数组和一般的对象区分开,我们可以检测它是否具有length属性和slice()方法,有的就是数组,否则是对象。另外ES5中出现了Array.isArray(arr)方法,如果arr是数组,则返回true,否则返回false。
上面的说法是错的! 因为不仅仅 对象中的Array又length属性, 对象中的 Function也是具有length属性的。Function的length属性时这个函数期望接收的参数的个数!!!
3. 相对于使用 new RegExp() 构造函数的方法创建正则表达式, 更希望是正则表示式字面量的方法。
如 var re = /ad\f/gm; 是字面量的方法。 var re = new RegExp("ad\\f","gm"); 是构造函数的方式。
原因有二(实则是一):
(1). 使用字面量的方式看上去就很简洁。
(2). 可以看到如果想要匹配 , 在字面量中需要使用 来转义,但是在构造函数中需要用四个才能达到相同的效果。
4. 对于String、Number、Boolean,我们尽量使用简单形式,而不要用包装对象。
var a = "zzw"; 就是简单的形式,而 var a = new String("zzw"); 就是复杂的使用了包装对象的形式。 两者的区别有二:
1. 前者使用typeof判断得到的是 string ,而后者使用 typeof 判断得到的是 obect。
2. 两者都可以只用方法,只是前者可用的方法存在的声明周期更短, 而后者会始终存在。
5. 错误对象
try { 什么 throw { name: "myError", message: "some error!", } } catch (e) { alert(e.name); }
如上所示: 如果出现了错误,我们在throw 就抛出来这个错误对象, 然后再catch。
注意:一个程序中最好要有 try 和 catch 的语句!
总结:
在一般情况下, 除了Date()构造函数以外,很少需要其他的内置构造函数,而是使用字面量的方法去创建。
第四章: 函数
1. 函数的两个特点
a 函数是对象中的第一等公民。
b 函数可以提供作用域。
(a)函数当然是对象,因为我们可以用new Function()来创建对象,其次函数有自己的属性(name)和方法(toString),最后函数可以作为参数向其他的函数传递。注:虽然函数可以用构造函数的方法创建,但是不推荐,因为我们得传递字符串,这和eval()一样糟糕。
(b)在js中,只有函数可以提供作用域,而其他如if、while等有{}的都不能提供,因为在js中,没有块级作用域这一说。
2. 函数中的几个常用的创建方式。
(a) 函数声明
function add () { return 1+2; }
注意:函数声明可以将函数的定义提升到其所在作用域的顶部。
注意:当使用函数声明时,函数定义也会提升,而不仅仅是函数声明被提升,这个与变量声明提升是不同的。
(b) 命名函数表达式
var add = function addSomething() { return 1+2; }
注意:前者和后者是可以相同也可以不同的, 如果调用其name属性可以得到addSomething而不是add,另外一个好处是用于求阶乘的时候(参看《JavaScript函数之美》),命名函数表达式无提升效果。
(c) 匿名函数表达式
var add = function () { return 1+2; }
注意: 无提升效果。且函数.name是空的字符串。
3. 对于2中的三种方式需要强调的几点
(a) 函数声明只能出现在“程序代码”中, 这表示它仅能在其他函数体的内部或全局空间中。
正确区分下面的几种形式是很有必要的。
callMe(function () { // 刚刚说了,函数声明只能出现在“程序代码”中,显然这里是匿名函数表达式。 }); callMe(function add() { // 同样,函数声明不可能作为一个参数,这里是命名函数表达式 });
var myObject = {
say: function () {
// 显然这里是匿名函数表达式
}
}
(b) 在不使用函数声明的时候,我们推荐使用匿名函数表达式。
var foo = function bar() {};
虽然这个在语法上没有什么问题,但是表达起来不够方便,且在IE中可能会出现问题。
4.回调模式(优点:代码重用)
什么使回调函数?当把一个函数的引用 当作一个参数传递给另一个函数时,另一个函数在执行过程中可能会执行这个函数,那么这个函数就成为了回调函数,又称回调。
看下面的这个例子(非常重要!)
大意: 先找到所需的nodes,然后隐藏这些nodes。 我们可以用两个独立的函数完成这个任务 --- 这样代码可重用!
var findNodes = function () { var nodes = [], found, status = true; for (var i = 0; i < 1000; i++) { // 复杂的逻辑 if (status) { nodes.push(found); } } return nodes; }; var hide = function (nodes) { var i = 0, max = nodes.length; for (var i = 0; i < max; i++) { nodes[i].style.display = "none"; } } hide(findNodes());
存在的问题:1. 虽然这是两个函数,可以保证代码的重用,但是我们发现每个函数都要进行一个循环 --- 很消耗性能。
2. 注意: findNodes()并不是回调函数,因为我们可以看到 传入的是一个结果,而不是函数的引用。
因为多次循环消耗性能,我们可以将隐藏的步骤放在findNodes里啊, 问题是: 如果这样,我们就不能重用代码了。
解决方法: 将hide函数作为回调函数传入findNodes中。如下:
var findNodes = function (callback) { var i = 1000, nodes = [], found; if (typeof callback !== "function") { callback = false; } while (i) { i -= 1; // 复杂的逻辑 if (callback) { callback(found); } nodes.push(); } return nodes; }; //回调函数 var hide = function (node) { node.style.display = "none"; } findNodes(hide);
这样问题就解决了 --- 两个函数既可以重用, 还无需多次循环消耗性能。
注意的问题
虽然在很多情况下这种方法都是简单且有效的,但是也经常存在一些场景,其回调并不是一次性的匿名函数或者全局函数(其中如果有this就会指向全局),而是对象的方法(那么它的this就会指向对象),这时如果作为回调函数被应用时,因为函数的特殊之处(函数时存在堆里的),这时this就会指向全局, 不会指向对象,就会出错。 方法: 使用call或者apply进行绑定this到对象上。
另外异步事件监听器也是回调函数的模式。
document.addEventListener("click", console.log, false);
其中console.log就是回调函数。
另外超时也是回调函数的模式。
var add = function () { return 1+3; }; setTimeout(add, 100);
其中setTImeout的第一个参数传递的是函数的引用(不带圆括号),如果带上圆括号,传递的就是一个结果了。
(注意: 我们知道传递回调函数的引用的目的是在这个函数中调用这个回调函数,所以可以猜测setTimeout这个函数的内部一定有调用add这个函数的语句!)
库中的回调模式
如果我们希望函数的重用,我们就要考虑回调模式。
5. 自定义函数
什么是自定义函数?
函数可以动态定义(即函数声明的方式),也可以分配给变量(即匿名函数表达式的方式)。 如果创建了一个新函数并且将其分配给保存了另外函数的同一个变量,那么就以一个新函数覆盖了旧函数,这一切发生在旧函数体的内部。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>callback</title> </head> <body> <script> var selfDefine = function () { console.log("good"); selfDefine = function () { console.log("better"); } } selfDefine(); //good selfDefine(); //better selfDefine(); //better selfDefine(); //better selfDefine(); //better </script> </body> </html>
这就是自定义函数, 我们可以看到虽然五次调用了同样的函数,但第一次的却不一样,这是因为在第一次调用函数的时候, 我们将这个函数名(指针)指向了一个新的函数,由于没有使用var, 所以这个函数就是全局的,便覆盖了第一次的函数。
但是如果我们使用var,那么这个就不会覆盖了,也就不是自定义函数了,
var selfDefine = function () { console.log("good"); var selfDefine = function () { console.log("better"); } } selfDefine(); //good selfDefine(); //good selfDefine(); //good selfDefine(); //good selfDefine(); //good
这是因为我们每次再调用的时候,在函数内部的同名函数作为外部函数的私有函数,根据作用域链的规则,显然我们是不能访问到里面的函数的。
优点: 当您的函数有一些初始化工作要做,并且只需要执行一次,那么这种模式就非常有用。 因为并没有理由去执行本可以避免的重复工作,即该函数的一些部分可能并不再需要(非常好)。
6. 即时函数
即时函数模式(Immediate Function Pattern)是一种可以支持在定义函数后立即执行该函数的语法。
思考: 既然你这个函数在定义的时候就执行了,那么你直接写成一堆语句不就行了,干嘛非要写成即时函数的形式呢?
回答: 说得好! 即时函数的唯一目的就是模仿块级作用域!
我: 嗯? 也不完全对,后面我来告诉你!
(function () { alert("good"); }());
这就是一个即时函数。下面的替代方法也是很常见的。
(function () { alert("good"); })();
其实即使函数的目的不仅仅是模仿块级作用域!因为即使函数也可以传递参数啊!如下:
(function (who, when) { // 可以看到和变量相接的字符串我们都使用一个空格来空开,这一点意识到是非常重要的。 console.log("I met " + who + " on " + when); }("zzw", "now")); // I met zzw on now
一般来说,不应该传递过多的参数到即时函数中,因为这样将迅速成为一种阅读负担!
即使函数的返回值:
var str = (function (who, when) { // 可以看到和变量相接的字符串我们都使用一个空格来空开,这一点意识到是非常重要的。 return "I met " + who + " on " + when; }("zzw", "now")); console.log(str); // I met zzw on now
我们可以看到这里即时函数的返回值赋值给了 str ,非常有效。
即时函数的优点和用法
在即时函数中定义的变量将会是用于自调用函数的局部变量,并且不会担心全局空间被临时变量所污染。
注意: 即时函数的其他名称包括 “自调用”以及“自执行”函数,因为该函数在定义之后立即执行。
可以使用下面的模版来定义一个功能,让我们称之为module1:
// 文件module.js中定义的模块 module1 (function () { //模块1中的所有代码... }());
遵循这个模版,可以编码其他的模块,然后,当将这些代码发布到在线站点时,可以决定哪些功能应用于黄金时间,并且使用构建脚本将对应文件合并。
注意: 比如在开发一个较大的网站时,我们往往需要引入一些通用的js文件,这时我们就可以将这些通用的文件使用即时函数封装起来,这同时也遵循我们之前所讲的尽量减少全局变量的原则。
7. 即时对象初始化
这种模式也可以保护全局作用域不受污染。
这种模式使用带有init()方法的对象,该方法在创建对象之后将会立即执行,init函数需要负责所有的初始化任务。
下面是一个即时对象模式的实例:
({ // 在这里可以定义设定值 // 又名配置常数 maxWidth: 600, maxHeight: 800, // 还可以定义一些实用的方法 gimmeMax: function () { return this.maxWidth + "x" + this.maxHeight; }, // 初始化 init: function () { console.log(this.gimmeMax()); // 更多初始化任务 } }).init();
这里是使用对象字面量创建了一个对象,然后用括号包裹起来是因为这样就确定是一个对象了,而不是类似与if和while的代码块, 对象一旦创建就会初始化。
8. 函数属性 --- 备忘模式
9. 配置对象
配置对象模式是一种提供更简洁的API的方法,尤其是在建立一个库或者是任何将被其他程序使用的代码的情况。
举例说明:
想象一下, 如果正在编写一个名为addPerson()的函数, 该函数接收人员的名和姓参数,并且将这个人添加到列表中,可以使用:
function addPerson(first, last) { //... }
后来,了解到实际上还要存储人员的出生日期、以及可选的性别和住址等信息,因此,可以修改函数并添加新的函数信息(将可选参数放在末尾),
function addPerson(first, last, dob, gender, address) {};
在这一点上, 该函数的参数列表就变得有点长,然后,知道需要添加一个用户名,并且这是绝对必要的而非可选的,现在的函数调用者必须传递参数,而且可选参数也要传递,同时还要注意不要混淆了参数的顺序。
addPerson("zzw", "wadf", new Date(), null, null, "batman");
使用者需要传递大量的参数并不是很方便,一个更好地办法就是仅仅使用一个参数对象来替代所有的参数,让我们称该参数为conf, 也就是配置的意思。
addPerson(conf);
然后,该函数的使用者可以这么做:
var conf = { username: "batman", first: "Bruce", last: "Wary" }; addPerson(conf);
这就是配置对象了,配置对象的优点在于:
- 不需要记住众多的参数以及顺序。
- 可以安全的忽略可选参数
- 更加易于阅读和维护
- 更加易于添加和删除函数
而配置对象的不利之处在于:
- 需要记住参数名称(即传入的参数一定要和函数内的参数匹配,否则就会出错)
- 属性名称无法压缩
需要注意的地方:
- 不难理解为什么不需要注意参数的顺序: 因为对象的属性直接用,顺序当然没有一一对应的关系
- 不难理解为什么可以不填写可选项。 前提条件是我们需要提前在函数中声明以下可选参数,例如 if (conf.alter === undefined){conf.alter == ""}。 为什么可以这么用呢?与变量不同,如果没有声明一个变量,而直接使用,会报错。 但是如果直接调用一个不曾声明过得属性, 就不会报错,而是得到undefined的结果。
10. 部分函数和函数的curry化
注:这一部分一直不是很理解,希望以后真正需要用到的时候可以边学边用。
第五章: 对象创建模式
前言:
JavaScript是一种简洁明了的语言,其中并没有在其他语言中经常使用的一些特殊的语法结构,比如命名空间(namespace)、模块(module)、包(package)、私有属性(private property)、以及静态成员等语法。
本章节会通过一些常见的模式来实现、替换那些语法特征,或者仅仅以不同于那些语法特征的方式来思考问题。
1. 命名空间模式(参考govclouds.cn中的APP.js)
JavaScript并没有内置命名空间,命名空间(namespace)有助于减少程序中所需要的全局变量的数量。 它的用途和使用即时函数有相似之处,都是为了减少全局变量的数量, 由此带来的好处就是有助于避免命名冲突或着过长的名字前缀。
补充:显然,过长的名字前缀也是一种避免冲突的方法,但是这种方法并不好。
使用js模仿命名空间还是很简单的。
方法:为应用程序或库创建一个(理想上最好只有一个)全局对象,然后可以将所有功能添加在该全局对象中,从而在具有大量函数、对象和其他变量的情况下并不会污染全局范围,如强哥的APP.js。
如下面的例子:
// 警告:反模式 // 构造函数 function Parent() {}; function Child() {}; // 一个变量 var some_var = 1; // 一些对象 var module1 = {}; module1.data = { a: 1, b: 2 }; var module2 = {};
上面的代码不是不可以这么写,但是坏处在于: 该模块的变量和引入的其他模块的变量很有可能会冲突,或者全局变量越来越多的时候,自身也会引发冲突。
可以通过为应用创建一个全局对象这种方式来重构上面的代码,比如创建全局对象MYAPP,然后改变所有的函数和变量以使其成为您的全局对象的属性。
// 全局变量,最好只有一个 var MYAPP = {}; // 构造函数 MYAPP.Parent = function () { }; MYAPP.Child = function () { }; // 一个变量 MYAPP.some_var = {}; // 一个容器对象 MYAPP.modules = {}; // 嵌套对象 MYAPP.modules.module1 = {}; MYAPP.modules.module1.data = { a: 1, b: 2 }; MYAPP.modules.module2 = {};
这就是 模仿命名空间 的模式,这里在这个项目上,只有一个全局变量MYAPP,全部大写是因为 通常程序员都会根据公约以全部大写的方式来命名全局变量,故全局变量是非常引人注目的。(别忘了,一般情况下一个常量也是使用这种方式来命名的)
主要使用场景: 在引入第三方库的时候,比如js和窗口widget的冲突,强烈推荐使用这种方式。
缺点(从强哥的APP.js就可以看出来): 1. 需要输入更多的字符,每个变量和函数都要加前缀,总体上增加了需要下载的代码量。 2. 仅有一个全局实例意味着任何部分的代码都可以修改全局实例。 3. 长嵌套名字意味着更长(更慢)的属性解析查询时间。
2. 通用命名空间函数
这一部分不做过多的介绍,只说重点的部分。
我们知道:
由于程序复杂性的增加、代码的某些部分被分隔成了不同的文件,以及使用添加包含语句等多个因素,仅假设您的代码是第一个定义某个命名空间或者它内部的一个属性(也许实际上不是的),这种做法就会导致覆盖之前的命名空间,或者自己的命名空间被覆盖 --- 总之,这都是不安全的。 因此,在添加一个属性或者创建一个命名空间之前,最好是先检查它是否已经存在,如下所示:
// 不安全的代码 --- 可能会有冲突 var MYAPP = {}; // 更好的代码风格1 if (typeof MYAPP === "undefined") { var MYAPP = {}; } // 更好的代码风格2 if (typeof MYAPP !== "object") { var MYAPP = {}; } // 或者使用更短的语句 var MYAPP = MYAPP || {};
3. 私有成员和特权方法
这都只是我们给的名称, 私有成员一般指对象的属性在外部不能访问,特权方法一般指对象的方法可以访问对象的属性(有特权)。
Javascript Page 97