作者简介
Douglas Crockford (中文简称“老道”)是Web开发领域最知名的技术权威之一,ECMA JavaScript2.0标准化委员会委员。被JavaScript之父Brendan Eich称为JavaScript的大宗师(Yoda)。
曾任Yahoo!资深JavaScript架构师,于2012.05.14加入Paypal,是PayPal现任高级JavaScript架构师。
他是JSON、JSLint、JSMin和ADSafe的创造者,也是名著《JavaScript: The Good Parts》(中文版《JavaScript语言精粹》)的作者。
撰写了许多广为流传、影响深远的技术文章:
- 世界上最被误解的编程语言
JavaScript:The World's Most Misunderstood Programming Language
链接:http://javascript.crockford.com/javascript.html
- 世界上最被误解的编程语言已经变成世界上最流行的变成语言
The World's Most Misunderstood Programming Language Has Become the World's Most Popular Programming Language
链接:http://javascript.crockford.com/popular.html
带下划线的蓝色段落为本人补充笔记,其余皆摘自原书
第1章 精华
大多数编程语言都有精华和糟粕。但是,你有权力定义你自己的子集。你完全可以基于精华的那部分去编写更好的程序。
JavaScript中糟粕的比重超出了预料。在JavaScript中,美丽的、优雅的、富有表现力的语言特性就像一堆珍珠和鱼目混杂在一起。本书的目的就是要揭示JavaScript中的精华,让大家知道它是一门杰出的动态编程语言。我相信我精雕细琢出来的优雅子集大大地优于这门语言的整体,它更可靠、更易读、更易于维护。
JavaScript的函数是(主要)基于词法作用域的顶级对象。JavaScript是第一个成为主流的Lambda语言。实际上,相对于Java而言,JavaScript与Lisp和Schema有更多的共同点。它是披着C外衣的Lisp。这使得JavaScript成为一个非常强大的语言。
如果你尝试对JavaScript直接应用基于类的设计模式,你将会遭受挫折。但是,如果你学会了自如地使用JavaScript原型,你的努力将会有所回报。
第2章 语法
空白
两种注释:块注释 /* */ 和行注释 //。没有用的注释比没有注释更糟糕。块注释对于被注释的代码块来说是不安全的,例如:
/*
var rm_a = /a*/.match(s);
*/
建议避免使用块注释,而用行注释代替它。
标识符
不允许使用保留字来命名变量或参数。不允许在对象字面量中,或者用点运算符提取对象属性时,使用保留字作为对象的属性名。
原书说标识符由一个字母开头,但实际上标识符可以有一个字母,下划线 _ 或者 $符号开头,如下所示:
<span style="color:#3333FF;">var _a = {"123":"string"};
console.log(_a["123"]); // string
var $c = 123;
console.log($c); // 123</span>
数字
JavaScript只有一个数字类型,它在内部被表示为64位浮点数,由于没有分离出整数类型,所以1和1.0的值相同,同时也避免了短整型溢出问题。Infinity表示所有大于1.79769313486231570e+308的值。
<span style="color:#3333FF;"> alert(Number.POSITIVE_INFINITY); //Infinity
alert(Number.NEGATIVE_INFINITY); //-Infinity
alert(Number.MAX_VALUE); //1.7976931348623157e+308
alert(Number.MIN_VALUE); //5e-324
alert(1.7976931348623158e+308); //1.7976931348623157e+308
alert(1.7976931348623159e+308); //infinity</span>
字符串
JavaScript中的所有字符都是16位的,字符串字面量可以被包在一对单引号或双引号中。\ (反斜杠)是转义字符。字符串有一个length属性,例如"seven".length是5。字符串有一些方法(参见第八章),例如 'cat'.toUpperCase() === 'CAT'
语句
在Web浏览器中,每个<script>标签提供一个被编译且立即执行的编译单元。因为缺少链接器,JavaScript把它们一起抛到一个公共的全局名字空间中。
当 var 语句被用在函数内部时,它定义的是这个函数的私有变量。
switch、while、for 和 do 语句允许有一个可选的前置标签(label),它配合 break 语句来使用。
return 语句可以指定要被返回的值,如果没有指定返回表达式,那么返回值是 undefined。不允许在 ruturn 关键字和标签之间换行。否则会在return和表达式之间自动插入分号,从而存在bug可能性,参见附录A(P102)。
break 语句可以指定一个可选的标签,程序将跳往带该标签的语句处继续执行。不允许在 break 关键字和标签之间换行。
关于标签跳转,下面有个小例子可以帮助理解:
<span style="color:#3333FF;"><span style="background-color: rgb(255, 255, 255);">var iNum = 0;
var i = 0, j = 0;
jumpTag:
for (i = 0; i < 10; ++i) {
for (j = 0; j <10; ++j) {
if (i === 5 && j === 5) {
//case 1
//break jumpTag;
//case 2
continue jumpTag;
}
++iNum;
}
}
console.log(iNum); // case 1 iNum = 55 | case 2 iNum = 9</span></span>
表达式
. [] () | 提取属性与调用函数 |
delete new typeof + - ! | 一元运算符 |
* / % | 乘法、除法、求余 |
+ - | 加法/连接、减法 |
>= <= > < | 不等式运算符 |
=== !== | 等式运算符 |
&& | 逻辑与 |
|| | 逻辑或 |
?: | 三元 |
<span style="color:#3333FF;"><span style="background-color: rgb(255, 255, 255);">var a = 1;
var b = 2;
alert(-a++); //-1, a=2
alert(a);
void b++; //b=3
alert(b);</span></span>
字面量 与 函数 部分忽略
第3章 对象
JavaScript的简单数据类型包括数字、字符串、布尔值(true和false)、null值和undefined值,其他所有的值都是对象,数组、函数、正则表达式都是对象。
如果属性名是一个合法的标识符且不是保留字,则不强制要求用引号括住属性名。"first-name" 必须用引号括住,而是否括住 first_name 则是可选的。因为标识符中包含连接符 - 是不合法的,但是允许包含下划线 _ 。
同理可得,如果属性名是数字,则必须用引号括住属性名,例如obj={"123":"abc"}; console.log(obj["123"]); 因为全部只有数字的属性名不是一个合法的标识符。
获取一个不存在的成员属性的值时将返回undefined。设置对象属性值时,可以使用 || 来设置默认值。如下所示:
var status = flight.status || "unknown";
<span style="color:#3366FF;">var status = flight.status ? flight.status : "unknown"; //等价写法</span>
从 undefined 成员属性中取值会导致TypeError异常,因此从成员属性中取值之前可用 && 运算符来判别该成员属性是否为 undefined,如下所示:
flight.equipment && flight.equipment.model;
<span style="color:#3333FF;">flight.equipment ? flight.equipment.model : undefined; //等价写法</span>
使用赋值语句更新对象的属性值时,如果对象没有该属性名,则
该属性就会被扩充到对象中。
对象通过引用来传递,它们永远不会被复制。如下所示:
var a = {}, b = {}, c = {};
//a, b和 c 每个都引用一个不同的空对象
a = b = c = {};
//a, b和 c 每个都引用同一个空对象
每个对象都连接到一个原型对象,并且它可以从中继承属性。所有通过对象字面量创建的对象都连接到Object.prototype,它是JavaScript中的标配对象。当创建一个新对象时,可以选择某个对象作为它的原型。若添加一个新的属性到原型中,则该属性对所有基于该原型创建的对象可见。原型连接在更新时是不起作用的,对象的更新不会触及该对象的原型。原型连接只有在检索值的时候才会被用到。即如果从对象中获取某个不存在的属性时,那么JavaScript会沿着原型连接依次往上寻找该属性,直到Object.prototype为止;若依然找不到,则返回 undefined。这个过程称为 委托。
原型链中的任何属性都会产生值:
typeof flight.toString // 'function'
typeof flight.constructor // 'function'
使用 hasOwnProperty 方法可以检测对象是否独有某个属性,且
该方法不会检查原型链。如下所示:
flight.hasOwnProperty('number') //true
flight.hasOwnProperty('constructor') //false
for in 语句可以用来遍历一个对象中的所有属性名,该遍历过程将会列出所有属性,
包括函数和原型中的属性,可以使用 hasOwnProperty 方法或者 typeof 方法来排除函数:
var name;
for (name in obj){
if (typeof obj[name] !== 'function') {
document.writeIn(name + ': ' + obj[name]);
}
}
delete 运算符可以删除对象属性,并且
不会触及原型链中的任何对象。如果对象的属性屏蔽了原型链中的同名属性,则可以用delete 删除该对象属性,使得原型链中的属性透现出来。
为了减少全局变量污染,可以为应用只创建一个唯一的全局变量,既把所有全局性的资源都归纳入一个名称空间之下,好处是与其他组件或类库的冲突可能性显著降低。
关于原型的文章:
- 理解 Javascript 的原型概念 http://www.oschina.net/translate/understanding-javascript-prototypes
- JavaScript探秘:强大的原型和原型链 http://www.nowamagic.net/librarys/veda/detail/1648
第4章 函数
一般来说,所谓编程,就是将一组需求分解为一组函数与数据结构的技能。
函数对象
函数就是对象,对象是“名/值”对的集合并拥有一个连接到原型对象的隐藏链接,例如对象字面量产生的对象连接到Object.prototype,函数对象链接到Function.prototype(该原型对象本身连接到Object.prototype)。
四种函数调用方式
函数调用没有参数个数与参数类型的检查。实参个数与形参个数不匹配时,不会导致运行错误。实参过多被忽略,实参过少,缺失参数为undefined。
- 方法调用模式
当一个函数被保存为对象的一个属性时,我们称它为一个方法。当一个方法被调用时,this被绑定到该对象。例如下面代码myObject调用方法increment,因此this绑定到myObject这个对象。this到对象的绑定发生在调用的时候。这个超级延迟绑定使得函数可以对this高度复用。
var myObject = { value: 0, increment: function (inc) { this.value += typeof inc === 'number' ? inc : 1; } }; myObject.increment(); document.writeln(myObject.value); // 1 myObject.increment(2); document.writeln(myObject.value); // 3
- 函数调用模式
当函数并非对象属性时,通过函数名直接调用函数,this被绑定到全局对象。
这是语言设计上的一个错误,导致方法不能利用内部函数来帮助他们工作,因为内部函数中的this被绑定到全局对象,不能共享该方法对对象的访问权。解决方案是在方法中定义一个that变量并赋值为this,那么内部函数可以通过that这个变量访问到对象。var add = function (a, b) { return a + b; }; var sum = add(3, 4); // sum 的值为7
myObject.double = function () { //给myObject增加一个double方法, var that = this; //解决方法,因为double方法中的this绑定到myObject这个对象本身 var helper = function (){ //内部函数通过that访问myObject对象的属性value that.value = add (that.value, that.value); }; helper(); //以函数形式调用helper,因此helper函数中的this指向全局对象window }; myObject.double(); //以方法形式调用double document.writeln(myObject.value); // 6
- 构造器调用模式
如果通过 new 来调用函数,那么隐性地创建一个连接到该函数的prototype成员的新对象,并且this会被绑定到那个新对象上。
一个函数,如果创建的目的就是希望结合new前缀来调用,那它就被称为构造器函数。按照约定,它们保存在以大写格式命名的变量里。如果调用构造器函数时没有加上new,会发生糟糕的事情,【污染同名的全局变量】,既没有编译时的警告,也没有运行时的警告,所以大写约定非常重要。var Quo = function (string) { // 创建一个名为 Quo 的构造器函数,它构造一个带有 status 属性的对象 this.status = string; }; Quo.prototype.get_status = function () { //给 Quo 的所有实例提供一个名为 get_status 的公共方法 return this.status; }; var myQuo = new Quo("confused"); //构造一个 Quo 实例 document.writeln(myQuo.get_status()); // 打印 confuse
<span style="color:#3333FF;">var status = 1; // 定义一个全局变量 status document.writeln(window.status); // 1 var myBug = Quo("FireBug"); //忘记了new关键字,污染了全局变量status document.writeln(window.status); // 打印 FireBug</span>
- Apply调用模式
apply(this, arguments) 方法支持自定义绑定this值与参数数组。apply方法接受两个参数,第1个是要绑定给this的值,第2个就是一个参数数组。
var array = [3, 4]; var sum = add.apply(null, array); document.writeln(sum); // sum=7
函数参数与返回值
函数作用域内部可通过arguments伪数组来访问函数参数列表。arguments拥有一个length属性,但它没有任何数组的方法。函数如果没有返回值,则返回undefined。
如果函数通过 new 关键字去调用,若返回值不是一个对象,则返回 this(该新对象)。【即在构造器函数中return 数字字面量(123)、字符串字面量("abc")、布尔字面量(true/false)、null、undefined、NaN时,这些返回值不是对象,则构造器函数返回新对象。如果构造器函数中return Array、Object、Function、[]、{}这些对象时,那么构造器函数不会返回新建的对象,而是返回return语句中的对象。】
异常
throw语句抛出一个exception对象{name:"XXX", message:"xxx", other:"XXX"}并中断函数的执行,该exception对象将被传递到一个try语句的catch从句。如果在try代码块内抛出一个异常,控制权就会跳转到catch从句。一个try语句只会有一个捕获所有异常的catch代码块。【C++/java中一个try可以有多个catch子句,JavaScript已支持try{}catch{}finally{}语法】。
扩充类型的功能
通过给Object.prototype添加方法,则该方法对所有对象都可用。同理,通过给Function.prototype扩展方法使得该方法对所有函数都可用。
Function.prototype.method = function (name, func) {
if(!this.prototype[name]) { //只有当prototype原本就不存在该同名方法时,才允许增加新的方法进来
this.prototype[name] = func;
}
return this;
};
//给Number.prototype增加一个interger取整方法
Number.method('integer', function () {
return Math[this < 0 ? 'ceil' : 'floor'](this);
});
document.writeln((-10/3).integer()); // -3
//给String.prototype增加一个trim方法移除字符串首尾空白
String.method('trim', function () {
return this.replace(/^\s+|\s|$/g, '');
});
document.writeln('"' + " neat ".trim() + '"'); // neat
递归
著名的“汉诺塔”问题递归代码:
var hanoi = function (disc, src, aux, dst) { //把disc个盘子从src柱子接住aux柱子移动到dst柱子
if (disc > 0) {
hanoi(disc-1, src, dst, aux);
document.writeln('Move disc ' + disc + ' from ' + src + ' to ' + dst);
hanoi(disc-1, aux, src, dst);
}
};
hanoi(3, 'Src', 'Aux', 'Dst');
一些语言提供尾递归优化,意味着如果一个函数返回自身递归调用的结果,那么调用过程会被替换为一个循环,可以显著提高速度。
JavaScript没有提供尾递归优化,深度递归的函数可能因为堆栈溢出而运行失败。下面就整数的阶乘就是一个尾递归函数。
var factorial = function factorial(i, a) {
a = a || 1;
if (i < 2) {
return a;
}
return factorial(i - 1, a * i); // 返回对自身调用的结果,因此是尾递归,并且不会被优化
};
document.writeln(factorial(4)); // 24
作用域
JavaScript不存在块作用域,但是存在函数作用域,即函数中的变量在函数外部是不可见的,而在函数内部任何位置定义的变量,在该函数内部任何地方都可见。因此最好做法是在函数体的顶部声明函数中可能用到的所有变量。
闭包
关于闭包的博客推荐:
http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Closures
在前面的构造器调用模式中,构造器函数Quo有一个status属性和get_status方法。但是status属性是共有的,可直接被访问。可以通过闭包来实现status为私有属性。
var quo = function (status) { // quo无需结合new使用,所以首字母无需大写
return {
get_status : function () {
return status;
}
};
};
var myQuo = quo("amazed"); // quo函数调用,返回一个包含get_status方法的对象。
document.writeln(myQuo.get_status())
即使quo已经返回,但是get_status方法依然享有访问quo对象的status属性的特权。
因为该函数可以访问它被创建时所处的上下文环境,这被称为闭包。
避免在循环中创建函数,会引起混淆。【在循环中创建闭包也是个常见的错误】
模块
模块是一个提供接口却隐藏状态与实现的函数或对象。通过使用函数来产生模块,几乎可以完全摒弃全局变量的使用。例如给String增加一个deentityify方法,用来寻找字符串中的HTML字符实体并把它们替换为对应的字符。
String.method('deentityify', function () {
var entity = { // 字符实体表,它映射字符实体的名字到对应的字符
quot: '"',
lt: '<',
gt: '>',
};
return function () { // 返回deentityify方法
return this.replace(/&();/g, //查找以&开头和以;结尾的子字符串,并替换为字符实体表中对应的字符
function (a, b) {
var r = entity[b];
return typeof r === 'string' ? r : a;
}
);
};
}());
document.writeln('<">'.deentityify()); // <">
模块模式利用了函数作用域和闭包来创建被绑定对象与私有成员的关联,在这个例子中,只有deentityify方法有权访问字符实体表这个数据对象。
模块模式的一般形式是:一个定义了私有变量和函数的函数;利用闭包创建可以访问私有变量和函数的特权函数;最后返回这个特权函数,或者把它们保存到一个可访问到的地方。例如构造一个用来生产序列号的对象:
var serialMaker = function () {
var prefix = '';
var seq = 0;
return {
setPrefix: function (p) {
prefix = String(p);
},
setSeq: function (s) {
seq = s;
},
gensym: function () {
var result = prefix +seq;
seq += 1;
return result;
}
};
};
var seqer = serialMaker();
seqer.setPrefix('Q');
seqer.setSeq(1000);
document.writeln(seqer.gensym()); // Q1000
级联
如果让方法返回this【P48页级联demo】,就可以启用级联。每一次方法调用都返回该对象本身,所以每次调用返回的结果可以被下一次调用所用。
柯里化
柯里化,就是把多参数转换为一系列单参数函数并进行调用的技术。【思想:先把局部形参设置实参并构造出一个新的函数,而新函数的形参其实就是旧函数未绑定实参的形参,当调用这个新函数时,剩余的形参也绑定了实参。】
var add = function (a, b) {
return a + b;
};
Function.method('curry', function () {
var slice = Array.prototype.slice;
var args = slice.apply(arguments); //args为第一次调用curry时的参数
var that = this;
return function () {
return add.apply(null, args.concat(slice.apply(arguments))); // arguments为新函数add1中的参数
};
});
var add1 = add.curry(1);
document.writeln(add1(6)); // 7
记忆
函数可以将先前的操作结果记录在某个对象里,从而避免无谓的重复运算。这种优化被称为记忆。例如计算斐波拉契数列是,通过记忆可以减少重复计算。
var fibonacci = function (n) {
return n < 2 ? n : fibonacci(n-1) + fibonacci(n-2);
};
for (var i = 0; i <=10; i+=1) {
document.writeln('// ' + i + ': ' + fibonacci(i));
}
上面的方法存在大量重复计算,可通过记忆来减少重复计算:
var fibonacci = function () {
var meno = [0, 1]; //记忆数组
var fib = function (n) {
var result = meno[n]; //从记忆数组里取值,减少重复计算
if (typeof result != 'number') { //若记忆数组里不存在该值,则计算并把结果存在记忆数组里
result = fib(n-1) + fib(n-2);
meno[n] = result;
}
return result;
};
return fib;
} ();
老道把这种技术推而广之,编写了一个函数工厂,用于帮助我们构造带记忆功能的函数。
var memozier = function (memo, formula) { //参数memo为计算结果的记忆对象,formula为函数形参
var recur = function (n) {
var result = memo[n];
if (typeof result != 'number') {
result = formula(recur, n); // formula函数自身带recur这个递归函数和n这两个形参用于构造递归函数recur
memo[n] = result;
}
return result;
};
return recur;
};
var fibonacci = memozier([0, 1], function(recur, n) { //计算斐波拉契数
return recur(n - 1) + recur(n - 2);
});
document.writeln('// ' + 10 + ': ' + fibonacci(10)); // 10: 55
var factorial = memozier([1, 1], function(recur, n) { //计算阶乘
return n * recur(n-1);
});
document.writeln(factorial(5)); // 120
<span style="color:#3333FF;">var sum = memozier([0, 1], function(recur, n) { //计算1+2+..n
return n + recur(n-1);
});
document.writeln(sum(5)); // 15</span>
第5章 继承
基于类的继承提供了2个好处,首先它是代码重用的一种形式,即子类重用了父类的大部分代码,其次引入了一套类型系统的规范,无需编写显示类型转换的代码。JavaScript是一门基于原型的语言,这意味着对象直接从其他对象继承。
伪类
pending
对象说明符
pending原型
pending
函数化
pending
部件
pending
第6章 数组
第7章 正则表达式
第8章 方法
第9章 代码风格
优秀的程序拥有一个前瞻性的结构,它会预见到在未来才可能需要的修改,但不会让其成为过度的负担。
- 对代码块内容和对象字面量缩进4个空格。
- 放一个空格在if和 ( 之间,以致if不会看起来像一个函数调用。函数调用时才把函数名和 ( 相毗连。
- 除 . 和 [ 外所有中置运算符的两边都放了空格,它俩无空格是因为它们有更高的优先级。
- 在每个, 和 ; 后面都使用一个空格。
- 每行最多放一个语句
- 如果一个语句一行放不下,在一个 : 或二元运算符后拆开它,可防止自动插入分号带来的错误(参见附录A)。折断后的语句其余部分多缩进4个空格,如果4个不够明显,就缩进8个空格。
- 在诸如if和while这样结构化的语句里,始终使用代码块{}。
- 使用K&R风格,把 { 放在一行的结尾而不是下一行的开头,因为它会避免return语句中的一个设计错误(参见附录A)。
- 努力保持注释是最新的;不要添加无用注释;程序结构应能自我说明,从而消除对注释的需要。
- 把块注释 /**/ 用于正式的文档记录,倾向于多用行注释 //
- JavaScript有函数作用域,但是没有块级作用域,所以在每个函数的开始部分声明所有变量。
- 绝不允许switch语句块中的条件穿越到下一个case语句
- 对一个脚本应用或工具库,只用唯一一个全局变量。使用闭包能提供进一步的信息隐藏,增加模块的健壮性。
第10章 优美的特性
大量特性驱动的产品设计,特性成本没有被正确计算。特性有规定成本、设计成本和开发成本、还有测试成本、可靠性成本和文档成本。只对少数用户有价值的特性增加了所有用户的成本。所以在设计产品和编程语言时,我们希望直接使用核心的精华部分,因为是这些精华创造了大部分的价值。
老道把提炼后的JavaScript子集叫做精简的JavaScript (Simplified JavaScript),主要包括以下内容:
- 函数是顶级对象,是有词法作用域的闭包
- 基于原型继承的动态对象,可以通过普通赋值给任何对象增加一个新成员属性
- 对象字面量与数组字面量使得创建新的对象和数组非常方便。
子集包含了JavaScript精华中最好的部分,在子集中没有丑陋或糟糕的内容,它们全部都被筛除了。子集添加了少许新特性,例如增加了pi作为常量,增加了块级作用域。
附录A 毒瘤
-
全局变量
在JavaScript所有的糟糕特性之中,最为糟糕的一个就是它对全局变量的依赖。JavaScript没有链接器,所有的编译单元都载入一个公共全局对象中。
共有3种方式定义全局变量。
第1种是在任何函数之外放置一个var语句:
第2种是直接给全局对象添加一个属性:var foo = value;
window.foo = value;
第3种是直接使用未经声明的变量(隐式的全局变量) :
JavaScript的策略是 让那些忘记预先声明的变量变成全局变量,这导致查找bug非常困难。如果某些全局变量的名称碰巧和子程序中的变量名称相同,那么它们将会互相冲突,可能导致程序无法运行,而且通常难以调试。foo = value;
-
作用域
C语言里代码块作用域中的声明的变量在外部是不可见的。JavaScript采用了这样的块语法,却没有提供块级作用域: 代码块中声明的变量在包含此代码块的函数的任何位置都是可见的。因此,JavaScript书写好习惯鼓励在每个函数的开头部分声明所有变量。
-
自动插入分号
JavaScript有一个自动修复机制,它视图通过自动插入分号来修正有缺损的程序。有时它会不合时宜地插入分号。例如下面的代码本意是要返回一个包含status成员元素的对象,但是自动插入分号让它返回了undefined。
return { status:true };
改为如下格式即可避免该问题:
如果一个return语句返回一个值,这个值表达式的开始部分必须和return位于同一行。return { status:true };
-
保留字
当保留字被用作对象字面量的健值时,它们必须 被引号括起来。
-
Unicode
JavaScript的字符是16位,最多只能覆盖65536(2^16)个字符。Unicode字符集已经拥有百万个字符,剩余的百万字符在JavaScript中都可以用一对字符来表示。Unicode把一对字符视为一个单一的字符,而JavaScript认为一对字符是两个不同的字符。
-
typeof
typeof不能辨别出null与对象。可以用null为假值进一步区分,如下所示:
typeof用于正则表达式的类型识别上,因浏览器不同而呈现不同的结果。例如:if (my_value && typeof my_value === 'object') { // my_value是一个对象或数组! }
一些浏览器会返回'object',其他的会返回'function'。Firefox下返回'object'。typeof /a/
-
parseInt
parseInt是一个把字符串转换为整数的函数,它在遇到非数字时会停止解析,所以parseInt("16")与parseInt("16 tons")产生相同的结果。
如果该字符串第1个字符是0,那么该字符串会基于八进制而不是十进制来求值。因此parseInt("08")和parseInt("09")都产生0作为结果。建议使用parseInt时总是加上第二个参数来显示指定转换的进制。
-
+运算符
如果两个运算数都是数字,则返回两者之和。否则,把两个运算数都转化为字符串并连接起来。使用+运算符做加法运算时, 必须确保两个运算数都是整数。
-
浮点数
两个浮点数做运算时,会出现精度丢失的问题。比如0.1+0.2结果不等于0.3(笔者在firefox上尝试得到0.30000000000000004)。但是浮点数中的整数运算是精确的,所以小数表现出来的错误可以 通过指定精度来避免。或者把浮点小数乘以一个倍数放大到整数做运算后,再除以相应的倍数转化为浮点数。
-
NaN
NaN是IEEE 754中定义的一个特殊的数量值,尽管typeof不能辨别数字和NaN,但是NaN表示的“不是一个数字”(Not A Number),如下所示:
在试图把非数字形式的字符串转化为数字时可能会产生NaN,例如:typeof NaN === 'number' // true
如果NaN是数学运算中的一个运算数,那么结果就是NaN。NaN也不等于它自己,如下所示:+ '0' // 0 + 'oops' // NaN
可以通过isNaN()来辨别数字与NaN,如下所示:NaN === NaN // false NaN !== NaN // true
判断一个值是否可用做数字的最佳方法是使用isFinite(),它会删除掉NaN和Infinity 。isNaN(NaN) // true isNaN('oops') // true isNaN(0) // false isNaN('0') // false
isFinite(N),如果N是有限数字或者可转换为有限数字,那么返回true;如果N是NaN或者是正、负无穷大的数,则返回false。
isFinite()缺点是会试图把他的运算数转换成一个数字,因此可以自定义函数来区分数字与NaN,如下所示:
var isNumber = function isNumber(value) { return typeof value === 'number' && isFinite(value); }
-
伪数组
JavsScript没有真正的数组,数组本质是对象,不必设置数组维度,而且永远不会产生越界错误,但是性能比真正数组可能相当糟糕。
typeof运算符不能辨别数组和对象,需要通过constructor属性来判断一个值是否为数组,如下所示:
上面的检测对于在不同帧或窗口创建的数组将会给出false,下面的检测更为可靠:if (my_value && typeof my_value === 'object' && my_value.constructor === Array) { // my_value 是一个数组。 }
arguments数组不是数组,是一个有着length成员属性的对象,上面的检测能够分辨出arguments并不是一个数组。if (Object.prototype.toString.apply(my_value) === '[object Array]') { // my_value 确实是一个数组! }
-
假值
JavaScript众多假值 值 类型 0 Number NaN Number ' '(空字符串) String false Boolean null Object undefined Undefined
undefined是缺失的成员属性的值,用null来测试缺失的成员属性是一种错误的方式,如下所示:
value = myObject[name]; if (value == null) { alert(name + ' not found.'); }
笔者在Firefox下尝试了上面代码依然可行,原因是使用了会强制转换类型的==运算符,因此 undefined == null结果为true,如果使用更严格的比较运算符,undefined === null结果为false。
-
hasOwnProperty
hasOwnProperty是一个方法,而不是一个运算符,所以在任何对象中,hasOwnProperty可能会被一个不同的函数甚至一个非函数的值所替换:
var name; another_stooge.hasOwnProperty = null; //地雷 for (name in another_stooge) { if (another_stooge.hasOwnProperty(name)) { //地雷 document.writeIn(name + ': ' + another_stooge[name]) } }
-
对象
JavaScript的对象永远不会是真的空对象,因为它们可以从原型链中取得成员属性。给对象添加属性时应注意避免使用对象的同名成员属性,如下所示:
var count = {}; count['constructor'] += 1; alert(count.constructor); // Firefox下的结果为function Object() { [native code]}1 alert(count['constructor']); // Firefox下的结果为function Object() { [native code]}1
书中例子使用一个程序统计文本中每个单词的出现次数,如下所示:
用hasOwnProperty方法检测成员关系来规避上述问题。对于上述代码,可以进一步追加测试条件,如下所示:var i; var word; var text = "This oracle of comfort has so pleased me, " + "That when I am in heaven I shall desire " + "To see what this child does, " + "and praise my Constructor."; var words = text.toLowerCase().split(/[\s,.]+/); var count = {}; for (i = 0; i < words.length; i += 1){ word = words[i]; if (count[word]) { count[word] += 1; } else { count[word] =1; } }
if (typeof count[word] === 'number') {
附录B 糟粕
-
==与===
如果两个运算数类型一致且拥有相同的值,那么 === 返回true, !== 返回false。而 == 和 != 只有在两个运算数类型一致时才会做出正确判断,如果两个运算数是不同类型,它们 试图去强制转换值的类型。有些转换规则复杂难记,如下所示:'' == '0' // false 0 == '' // true 0 == '0' // true false == 'false' // false false == '0' // true false == undefined // false false == null // false null == undefined // true ' \t\r\n ' == 0 // true
以上的比较若使用 === 运算符,所有结果都是false。
运算符的传递性规则对于 == 号并不总是成立,“老道”的建议是永远不要使用 == 和 != ,请始终使用 === 和 !== 。
-
with语句
with语句本意是使用它来快捷地访问对象的属性。不幸的是,它的结果可能有时不可预料,所以应该避免使用它。
下面的语句:
和下面的代码做的是同样的事情:with (obj) { a = b; }
所以它的功能等于下面语句的某一条:if (obj.a === undefined) { a = (obj.b === undefined ? b : obj.b); } else { obj.a = (obj.b === undefined ? b : obj.b); }
通过阅读代码,不可能辨别出程序到底是运行上面哪一条语句。因此with语句让代码的可阅读性变差。with语句本身就 严重影响了javaScript处理器的速度,因为它阻断了变量名的词法作用域绑定。如果没有它,JavaScript语言会更好一点。a = b; a = obj.b; obj.a = b; obj.a = obj.b;
-
eval
eval函数传递一个字符串给JavaScript编译器,并且执行其结果。它经常被滥用,那些对JavaScript语言一知半解的人们最常用到它(sad..,看来笔者还没到一知半解的水准)。例如,你可能写出如下糟糕代码:
而不是像下面这样正确的写法:eval("myValue = myObject." + myKey + ";");
eval函数缺陷:myValue = myObject[myKey];
- 使代码可读性差。
- 使得性能显著降低,因为它需要运行编译器。
- 减弱了应用程序的安全性,它给被求值的文本受于太多的权利。
Function构造器也是eval的另一种形式,同样也应该避免使用它。setTimeout 和 setInterval 函数,它们的形参可以使字符串参数或函数参数。当形参是字符串参数时,它们会像 eval 那样去处理。所以调用该函数时,应该避免使用字符串参数。
-
continue语句
continue语句跳到循环的顶部。“老道”发现一段代码通过重构移除continue语句之后,性能都会得到改善。【笔者与“老道”观念不符,毕竟C/C++语言也有continue关键字,也没听说过这是个糟粕】
-
switch穿越
switch语句每次条件判断后都会穿越到下一个case条件,除非你明确地中断流程,比如使用break语句。 “老道”从他码农经验告诉我们不要刻意地使用case条件穿越。
-
缺少块的语句
if、while、do或for语句可以接受一个括在花括号中的代码块,也可以接受单行语句(节约括号所占的2个字节)。制定严格的规范 要求始终使用代码块会使得代码更容易理解。
-
++与--
“老道”的码农实践中,他觉得他用 ++ 和 -- 时,代码往往过于拥挤、复杂和隐晦。因此,为了让代码风格变得更为整洁,作为一条原则,“老道”不再使用它们。
【笔者觉得无所谓,C|C++这两个一元运算符很常用且效率高,JavaScript中 ++ 与 -- 还有前置和后置运算功能上的区别,当作糟粕实在欠妥。】
-
位运算符
JavaScript有如下位运算符
& and 按位与 | or 按位或 ^ xor 按位异或 ~ not 按位非 >> 带符号的右位移 >>> 无符号的(用0补足的)右位移 << 左位移 JavaScript没有整数类型,只有双精度浮点数。在大多数语言中,这些位运算符接近于硬件处理,所以非常快。但JavaScript的执行环境一般接触不到硬件,所以非常慢。JavaScript很少被用来执行位操作。
-
function语句对比function表达式
pending
-
类型的包装对象
JavaScript有一套类型的包装对象,例如: new Boolean(false) 会返回一个对象,该独享有一个 valueOf 方法会返回被包装的值。这其实完全没有必要,并且有时还令人困惑。不要使用 new Boolean、new Number 或 new String。此外也请避免使用 new Object 和 new Array,可使用 {} 和 [] 来代替。
-
new
new运算符创建一个继承与其运算数原型的新对象,然后调用该运算数,把新创建的对象绑定给this。如果忘记了只用new运算符,则是普通的函数调用,并且this被绑定到全局对象,而不是新创建的对象,这意味着函数中尝试去初始化新成员属性时它将会污染全局变量。与 new 结合使用的函数应该以首字母大写的形式命名。一个更好的应对策略就是根本不去使用 new。
-
void
在很多语言中,void是一种类型,表示没有值。而在JavaScript里,void是一个运算符,它接受一个运算数并返回 undefined。这没有什么用,令人非常困惑。应避免使用它。
void并不是一无是处,还是有点存在价值的,笔者找到一篇关于void用途的博客:JavaScript:void 运算符
原文版: http://www.2ality.com/2011/05/void-operator.html
译文版: http://www.cnblogs.com/ziyunfei/archive/2012/09/23/2698607.html
附录C JSLint