编写高质量JS代码的68个有效方法
No.1、了解你使用的JavaScript版本
Tips:
- 决定你的应用程序支持JavaScript的哪些版本。
- 确保你使用的任何JavaScript的特性对于应用程序将要运行的所有环境都是支持的。
- 总是在执行严格模式检查的环境中测试严格代码。
- 当心连接那些在不同严格模式下有不同预期的脚本。
JavaScript的普及使得它在1997年成为国际标准,官方名称为ECMAScript。除了ECMAScript标准存在多个版本之外,还存在一些JavaScript实现支持非标准特性,其他JavaScript实现不支持的情况。所以需要注意你所写的JavaScript代码所支持的版本。
/*[Sample]如下代码,在IE下会Syntax error,但是在Chrome中则是定义常量*/
const PI=3.14;PI=3;PI
由于JavaScript的主要生态系统--Web浏览器并不支持让程序员指定某个JavaScript版本来执行代码。在ES5中,引入了另外一种版本控制的考量--严格格式(strict mode),这个特性允许你选择在受限制的JavaScript版本中禁用JavaScript语言中问题较多或易于出错的特性。由于JS语法涉及向后兼容,所以在没有严格检查的环境中也能执行严格代码。
/*[Sample]如何使用严格模式,在程序/函数体的开始处加入'use strict'
使用字符串字面量作为指令看起来比较怪异,但好处是可以向后兼容,因为执行字符串字面量没有任何副作用
/*
function f(x){
'use strict';
var arguments=[];//SyntaxError:Unexpected eval or arguments in strict mode
}
"use strict"指令只有在脚本或者函数顶部才生效,这也是使用严格模式的一个陷进。脚本连接将变得颇为敏感。假如有多个js文件,一些需要执行在严格模式下,一些不需要执行在严格模式下,如何处理呢?
- 将需要严格模式检查的文件和不需要严格模式检查的文件分开连接
- 通过将自身包裹在立即调用的函数表达式中的方式来连接多个文件
/*file1.js*/
function fun1(){
var arguments=[];
}
/*file2.js*/
'use strict';
function fun2(){
console.log('strict mode!');
}
/*按照方式二连接后的文件内容应该是*/
/*fileMerge.js*/
(function(){
function fun1(){
var arguments=[];
}
})();
(function(){
'use strict';
function fun2(){
console.log('strict mode!');
}
})();
No.2、理解JavaScript的浮点数
Tips:
- JavaScript的数字都是双精度的浮点数。
- JavaScript的整数仅仅是双精度浮点数的一个子集,而不是一个单独的数据类型。
- 位运算将数字视为32位的有符号整数。
- 当心浮点运算中的精度陷进。
大部分语言都有几种数值数据类型,但是JavaScript只有一种
typeof 1; //'number'
typeof 1.1; //'number'
typeof -1; //'number'
对于位运算,JavaScript不会直接将操作数作为浮点数运算,会先转换为32位整数再进行运算
8|1; //9
8.1|1; //9
如何快速从10进制转换到2~36进制?
(100).toString(2); //1100100
(100).toString(10); //100
(100).toString(35); //2u
(100).toString(36); //2s
注意parseInt和parseFloat的用法
警告(以下为非标准特性,各浏览器执行有差异):
- 如果要转换的字符串已0x或者0X开头,那么parseInt('0xAB')等价于parseInt('0xAB',16)
- 如果遇到0开头,那么parseInt('013')等价于parseInt('013',8)
- 强烈建议在使用parseInt时指定进制
parseInt('9x'); //9 会自动忽略不能转换的字符
parseInt('x9'); //NaN 发现第一个字符就不能转换,返回NaN
parseInt('1100100',2); //100 可以在parseInt的第二个参数指定当前字符串的进制
parseInt('2xxx',2); //NaN 遇到无法转换的情况,返回NaN
parseInt('08'); //IE下:0,Chrome35下:8
浮点数是出了名的不精确,你能知道以下代码的执行结果吗?
0.1+0.2; //0.30000000000000004
(0.1+0.2)+0.3; //0.6000000000000001
0.1+(0.2+0.3); //0.6
0.3-0.2; //0.09999999999999998
当我们关心精度时,要小心浮点数的局限性。有效的方法是尽可能的采用整数值运算,整数在运算时不需要舍入。
No.3、当心隐式的强制转换
Tips:
- 类型错误可能被隐式的强制转换所隐藏。
- 重载的运算符+是进行加法运算还是字符串连接取决于其参数类型。
- 对象通过valueOf方法强制转换为数字,通过toString方法强制转换为字符串。
- 具有valueOf方法的对象应该实现toString方法,返回一个有valueOf方法产生的数字的字符串表示。
- 测试一个值是否为未定义的值,应该使用typeof或者与undeined进行比较而不是使用真值运算。
3+true; //4 true转换为数字1
'fun'(1); //TypeError:string is not a function
null.x; //TypeError: Cannot read property 'x' of null
2+3; //5
2+'3'; //'23' 偏爱字符串,遇到字符串,那么优先用字符串连接
1+2+'3'; //'33' 加法运算是从左到右,所以等价于(1+2)+'3'
1+'2'+3; //'123'
'17'*3; //51
'8'|'1' //9
如何测试一个值是NaN?
var x=NaN;
x===NaN; //false,NaN不等于自身
如果知道带测试的值是数字,那么可以使用标准库函数isNaN
isNaN(NaN); //true
但是对于其他绝对不是NaN,但会被强制转换为NaN的值,使用isNaN方法是无法区分的。
isNaN('foo'); //true
isNaN(undefined); //true
isNaN({}); //true
isNaN({valueOf:'foo'}); //true
幸运的是,有一个既简单有可靠但有点不直观的方法测试它:
JS中,NaN是唯一一个不等于其自身的值。
var x=NaN;
x!==x //true
/*测试x是否是NaN,是返回true,否则返回false*/
function isReallyNaN(x){
return x!==x;
}
如何控制对象的强制转换?
'J'+{toString:function(){return 'S'}}; //'JS'
2*{valueOf:function(){return 3;}}; //6
var obj={
toString:function(){
return '[object Obj]';
},
valueOf:function(){
return 1;
}
}
'object:'+obj; //'object:1'
解释:
1. 在需要数字的场合,优先判断valueOf,没有的话,则采用toString。
2. 如果对象同时拥有valueOf和toString方法,同时又一定是需要数字的场合,那么JavaScript盲目的选择valueOf方法而不是toString方法来解决这种含糊的情况。
3. 针对2:最好避免使用valueOf方法,除非对象的确需要一个数字的抽象,并且obj.toString()能产生一个obj.valueOf()的字符串的表示。
关于真值运算:
JavaScript中有7个假值:false、0、-0、''、NaN、null和undefined,其他都为真值
No.4、原始类型优于封装对象
Tips:
- 当做相等比较是,原始类型的封装对象与其原始值行为不一样。
- 获取和设置原始类型值的属性会隐式地创建封装对象。
除了对象以外,JavaScript有5个原始值类型:布尔值、数字、字符串、null和undefined。(令人困惑的是,对于null类型进行typeof操作得到的结果为"object",然而,ECMAScript标准描述其为一个独特的类型。)
var s='hello';
var sObj=new String(s);
typeof s; //'string'
typeof sObj; //'object' 包装对象的类型是object
var sObj1=new String(s);
var sObj2=new String(s);
sObj1==sObj2; //false
sObj1===sObj2; //false
解释:可以理解为引用类型,每个对象是单独的对象,其引用是不一致的,所以只等于自身。
JavaScript对基本类型有隐式封装,所以我们可以如下书写代码:
'test'.toUpperCase(); //'TEST'
'test'.test='test';
'test'.test; //undefined
解释:对基本类型调用方法/设置属性时,会产生隐式封装。
原始值->封装类型(产生封装对象)->封装对象执行方法/设置属性->返回原始值->抛弃封装对象。
所以更新封装不会造成持久的影响,同时对原始值设置属性是没有意义的。
No.5、避免对混合类型使用==运算符
Tips:
- 当参数类型不同时,==运算符应用了一套难以理解的隐式强制转换规则。
- 使用===运算符,使读者不需要设计任何的隐式强制转换就能明白你的比较运算。
- 当比较不同类型的值时,使用你自己的显式强制转换使程序的行为更清晰。
看代码:
'1.0e0'=={valueOf:function(){return true;}}; //true 因为通过隐式转换,就变成了1==1,所以结果为true。
转换为字符串:''+1; //'1'
转换为数字 : +'1'; //1
var date=new Date('1999/12/31');
date=='1991/12/31';//false
date=='Fri Dec 31 1999 00:00:00 GMT+0800 (China Standard Time)';//true
解释:世界上有太多的数据表现形式,JS需要知道你使用的是哪一种,==运算符并不能推断和统一所有的数据格式,所以更好的策略是显式自定义应用程序转换的逻辑,并使用严格相等运算符。
No.6、了解分号插入的局限性
Tips:
- 仅在“}”标记之前、一行的结束和程序的结束处推导分号
- 仅在紧接着的标记不能被解析的时候推导分号
- 在以(、[、+、-或/字符开头的语句前绝不能省略分号
- 当脚本连接的时候,在脚本之间显式的插入分号
- 在return、throw、break、continue、++或--的参数之前绝不能换行
- 分号不能作为for循环的头部和空语句的分隔符而被推导出
- 个人总结:尽量不要省略分号,不要让JS自动推导
分号仅在}标记之前、一个或多个换行之后和程序输入的结尾被插入,看代码:
// 能自动推导分号
function square(x){
var n = +x
return n*n
}
// Error,不能自动推导
function square(x){var n = +x return n*n}
分号仅在随后的输入标记不能被解析时插入,看代码:
a=b
(f());
此时,代码等价于 ab(f());
但是:
a=b
f()
则会被解析为a=b f();
在以(、[、+、-或/字符开头的语句前,绝不能省略分号,看代码:
a=b
['a','b','c'].forEach(function(key){
console.log(key)
})
等价于
a=b['a','b','c'].forEach(function(key){
console.log(key);
});
a=1
/Error/i.test('test')
等价于
a=1/Error/i.test('test');
合并脚本时不能省略分号,看代码:
//file1.js
(function(){console.log('file1')})()
//file2.js
(function(){console.log('file2')})()
//合并后 --输出file1,然后报错
(function(){console.log('file1')})()(function(){console.log('file2')})()
为了防止自己写的库在合并时内其他代码干扰,所以一般写法为如下代码:
;(function(){
/*Code*/
})();
在return、throw、break、continue、++或--的参数之前绝不能换行,看代码:
a
++
b
等价于:
a;++b;
for循环中不要省略分号
//Parse Error
var total=0
for(var i=0,total=1
i<n
i++){
total*=i
}
综上,再次强调,不加分号看起来代码轻量,但稍不注意就会引起很多bug,所以,建议都加上分号,不要让JS环境自行推导
No.7、视字符串为16位的代码单元序列
待定...
No.8、尽量少用全局对象
Tips:
- 避免申明全局变量
- 尽量申明局部变量
- 避免对全局对象添加属性
- 使用全局对象来做平台特性检测
定义全局变量会污染共享的公命名空间,并可能导致意外的命名冲突。全局变量不利于模块化,因为它会导致程序中独立组件间的不必要耦合。
No.9、始终声明局部变量
Tips:
- 始终使用var声明新的局部变量
- 考虑使用lint工具来帮助检查未绑定的变量
如果存在比全局变量更麻烦的事情,那就是意外的全局变量。由于不适用var申明的变量,统统为全局变量,所以一定要使用var来定义变量,防止变量污染。
function test(){
test='test';
}
test();
window.test;// 'test'
No.10、避免使用with
Tips:
- 避免使用with语句
- 使用简短的变量名代替重复访问的对象
- 显式地绑定局部变量到对象属性上,而不要使用with语句隐式地绑定他们
No.11、熟练掌握闭包
Tips:
- 函数可以引用定义在其外部的作用域变量。
- 闭包比创建它们的函数有更长的生命周期。
- 闭包在内部存储其外部变量的引用,并能读写这些变量。
//第一个事实:JavaScript允许你引用在当前函数以外定义的变量。
function testClosures(){
var all = 'Test';
function test(m){
return all + ' and ' + m;
}
return test('closures');
}
testClosures(); //'Test and closures'
//第二个事实:即使外部函数已返回,当前函数仍然可以引用在外部函数所定义的变量。
function testClosures(){
var all = 'Test';
function test(m){
return all + ' and ' + m;
}
return test;
}
var t = testClosures();
t('closures'); //'Test and closures'
//第三个事实:闭包可以更新外部变量的值
function TestClass(){
var all;
return {
set: function(value){
all = value;
},
get: function(){
return all;
}
};
}
var t = new TestClass();
t.set('555');
t.get();
闭包的优缺点: 优点: 变量保护、封装性,能够实现字段的可访问性(示例如下)
function ModelClass(){
//Property
var name,age=23;
return {
setName: function(value){ //设置名称
name = value;
},
getName: function(){ //获取名称
return name;
},
getAge: function(){ //只读
return age;
}
};
}
缺点: 常驻内存,会增加内存使用量,使用不当和容易造成内存泄露。
No.12、理解变量申明提升
- 代码块中的函数申明会提升到函数顶部
- 重复申明变量被视为单个变量
- 考虑手动提升局部变量的申明,避免混淆(将函数内所需变量集中申明到函数顶部)
JavaScript支持词法作用域,而不支持块级作用域
function test(){
alert(a); //undefined
var a = 1;
alert(a); //1
}
test();
以上代码等价于:
function test(){
var a;
alert(a); //undefined
a = 1;
alert(a); //1
}
test();
一个例外是 try...catch :catch块中的变量作用域只在catch中。
function test(){
var x = '1';
try{
throw ''
}catch(x){
alert('error');
x = '2';
}
alert(x); // 1
}
test();
No.13、使用立即调用的函数表达式创建局部作用域
- 理解绑定与赋值的区别
- 闭包通过引用而不是值捕获它们的外部变量
- 使用立即调用的函数表达式(IIFE)来创建具有作用域
- 当心在立即调用的函数表达式中包裹代码块可能改变其行为的情形
看看以下代码段输出什么?
function test(){
var arr = [1,2,3,4,5];
var result = [];
for(var i = 0, len = arr.length; i < len; i++){
result[i] = function(){
return arr[i];
}
}
return result;
}
var result = test();
result[0]();
可以通过立即调用表达式来解决JavaScript缺少块级作用域。如上代码可修改为:
function test(){
var arr = [1,2,3,4,5];
var result = [];
for(var i = 0, len = arr.length; i < len; i++){
(function(){
var j = i;
result[i] = function(){
return arr[j];
}
})(i);
}
return result
}
var result = test();
result[0]();
No.14、当心命名函数表达式笨拙的作用域
- 在Error对象和调试器中使用命名函数表达式改进栈跟踪
- 在ES3和有问题的JS环境中,函数表达式作用域会被Object.prototype污染
- 谨记在错误百出的JS环境中会提升命名函数表达式声明,并导致命名函数表达式的重复存储
- 考虑避免使用命名函数表达式或在发布前删除函数名
- 如果将代码发布到正确实现的ES5的环境中,没什么好担心的
匿名和命名函数表达式的官方区别在于后者会绑定到与其函数名相同的变量上,该变量将作为该函数内部的一个局部变量。这可以用来写递归函数表达式。
var f = function find(tree, key){
if(!tree){
return null;
}
if(tree.key === key){
return tree.value;
}
//函数内部可以访问find
return find(tree.left, key) || find(tree.right, key);
}
结论:尽量避免使用命名函数表达式
No.15、当心局部块函数声明笨拙的作用域
- 始终将函数声明置于程序或被包含的函数的最外层以避免不可移植的行为
- 使用var声明和有条件赋值语句替代有条件的函数声明
function f(){
return 'global';
}
function test(x){
var result = [];
if(x){
function f(){
return 'local';
}
result.push(f());
}
result.push(f());
return result;
}
test(true);
test(false);
结论:尽量将函数块定义为变量,防止函数提前
No.16、避免使用eval创建局部变量
Tips:
- 避免使用eval函数创建的变量污染调用者作用域。
- 如果eval函数代码可能创建全局变量,将此调用封装到嵌套的函数中已防止作用域污染。
执行eval时,eval中的变量才会被加到作用域中(函数作用域)
function fun1(){
eval('var y = 1;');
console.log('fun1->y:'+y); // 'fun->y:1'
}
fun1();
console.log('global->y:'+y); //throw Error
不要直接将不可控参数交给eval执行,可能会改变作用域对象。
//Bad code
var g = 'global';
function fun2(code){
eval(code);
}
fun2('var g="local"');
console.log(g) //'local'
//Right code
var g = 'global';
function fun2(code){
(function(){
eval(code);
})();
}
fun2('var g="local"');
console.log(g) //'global',嵌套作用域
以上Right Code,如果执行不带var的变量申明,那么也是会影响全局的g对象的。
No.17、间接调用eval函数优于直接调用
Tips:
- 将eval函数同一个毫无意义的字面量包裹在序列表达式中以达到强制使用间接调用eval函数的目的
- 尽可能间接调用eval函数,而不要直接调用eval函数
直接调用eval,那么编译器无法优化JS代码。 如何间接调用eval?
(0,eval)(code)
No.18、理解函数的调用、方法调用及构造函数调用之间的不同
Tips:
- 方法调用将被查找方法属性的对象作用调用接收者
- 函数调用将全局对象作为其接受者。一般很少使用该函数调用语法来调用方法
- 构造函数需要通过new运算符调用,并产生一个新的对象作为其接收者
在全局对象上直接定义的function被称为函数,调用则是函数调用
var fun1 = function(p){
console.log(p);
};
function fun2(p){
console.log(p);
}
//函数调用
fun1('p1');
fun2('p2');
如果对象的属性是函数,那么称之为方法,使用模式则是方法调用
var obj = {
name: 'Hello ',
fun1: function(name){
console.log(this.name + name);
}
};
//方法调用
obj.fun1('Jay');
注意:fun1中通过this来访问obj的name属性
构造函数调用将一个全新的对象作为this变量的值
fucntion User(name, age){
this.Name = name;
this.Age = age;
}
//此时,user是一个全新的对象
var user = new User('Jay', 23);
No.19、熟练掌握高阶函数
Tips:
- 高阶函数是那些将函数作为参数或返回值的函数
- 熟练掌握现有库的高阶函数
- 学会发现可以被高阶函数所取代的常见编码模式
需求:将数组元素全部转换为大写
//常规做法
var arr = ['abc', 'test', '123'];
for(var i =0, len = arr.length; i < len; i++){
arr[i] = arr[i].toUpperCase();
}
console.log(arr);
//高阶函数
var arr = ['abc', 'test', '123'];
arr = arr.map(function(item){
return item.toUpperCase();
});
console.log(arr);
注意:需要注意高阶函数使用时的返回值,有些是更改原始对象,有些是返回新对象
No.20、使用call方法自定义接收者来调用方法
Tips:
- 使用call方法自定义接收者(个人理解为作用域)来调用函数
- 使用call方法可以调用在给定对象中不存在的方法
- 使用call方法定义高阶函数允许使用者给回调函数指定接收者
function fun1(){
this.name = 'Test';
}
var obj = {
name: 'Jay'
};
console.log(obj.name);
fun1.call(obj);
console.log(obj.name);
call函数的调用方式:
f.call(obj, p1, p2, p3);
No.21、使用apply方法通过不同数量的参数调用函数
Tips:
- 使用apply方法自定一个可计算的参数数组来调用可变参数的函数
- 使用apply方法的第一个参数给可变参数的方法提供一个接收者
//示例:计算给定数据的最大值
function getMaxNum(){
var max = arguments[0];
for(var i = 1, len = arguments.length;i < len; i++){
if(max < arguments[i]){
max = arguments[i];
}
}
return max;
}
getMaxNum.apply(null,[1,3,4]);
该方法和call()方法功能基本类似,差别在于参数写法不一样。
No.22、使用arguments创建可变参数的函数
Tips:
- 使用隐式的arguments对象实现可变参数的函数
- 考虑对可变参数的函数提供一个额外的固定元数的版本,从而使用者无需借助apply方法。
每一个函数内部都有一个arguments对象包含所有传递的参数
function fun1(){
console.log(arguments);
}
fun1('1');
fun1(1,'2','str');
No.23、永远不要修改arguments的值
Tips:
- 永远不要修改arguments的值
- 使用[].slice.call(arguments)将arguments对象赋值到一个真正的数组中再进行修改
arguments看起来像是数组,但是它并不是标准的数组,所以不支持数组的原型方法
function fun1(nums){
var lastParam = arguments.pop(); //报错,undefined is not a function。
console.log(arguments);
}
fun1([1, 2, 3]);
正确的做法是,将arguments转换为真正的数组,再进行操作,代码如下:
function fun1(nums){
var argArr = [].slice.call(arguments);
var lastParam = argArr.pop();
console.log(arguments);
}
fun1([1, 2, 3]);
注意:永远不要修改arguments对象是更为安全的。
No.24、使用变量保存arguments的引用
Tips:
- 当引用arguments时当心函数嵌套层级
- 绑定一个明确作用域的引用到arguments变量,从而可以再嵌套的函数中引用它
首先,先来看一段代码的输出:
function fun1(){
var i = 0;
console.log(arguments);
return {
next:function(){
return arguments[i++];
}
}
}
var f = fun1(1,2,3,4);
console.log(f.next()); //猜猜是啥?
arguments是函数中的隐式变量,每个函数都会有这样的一个隐式对象。所以最后一个console的结果可想而知。所以遇到这种场景,是建议用变量保存arguments的引用,也能让嵌套函数正确的进行对象引用,正确代码如下:
function fun1(){
var i = 0;
var args = arguments;
return {
next:function(){
return args[i++];
}
}
}
var f = fun1(1,2,3,4);
console.log(f.next());
No.25、使用bind方法提取具有确定接收者的方法
Tips:
- 要注意,提取一个方法不会将方法的接收者绑定到该方法的对象上
- 当给高阶函数传递对象方法时,使用匿名函数在适当的接收者上调用该方法
- 使用bind方法创建绑定到适当接收者的函数
老规矩,看代码:(代码1)
var buffer = {
entries: [],
add: function(value){
this.entries.push(value);
},
concat: function(){
return this.entries.join('');
}
};
该代码在直接使用时是没有问题的,思考下,由于高阶函数将函数/方法作为变量传递,那么可以有如下用法:(代码2)
var arr = ['Jay', '.M', '.Hu'];
arr.forEach(buffer.add);
console.log(buffer.concat()); //思考下这个结果是什么?
以上代码在arr.forEach处已经报错,Cannot read property 'push' of undefined。因为这个时候的涉及到this的指向问题。我们可以改造下buffer代码,输出this让我们看看:(代码3)
var buffer = {
entries: [],
add: function(value){
console.log(this);
this.entries.push(value);
},
concat: function(){
return this.entries.join('');
}
};
从输出结果我们可以看到这个this,在(代码2)的执行环境中,指向的是window对象,所以导致了报错,那么如何避免这样的问题呢?针对forEach,我们有三个方法:(代码4)
//方式一,去掉this,直接用buffer对象引用
var buffer = {
entries: [],
add: function(value){
buffer.entries.push(value);
},
concat: function(){
return buffer.entries.join('');
}
};
//方式二,指定接收者,forEach方法提供,其他方法不一定提供
var arr = ['Jay', '.M', '.Hu'];
arr.forEach(buffer.add, buffer);
console.log(buffer.concat());
//方式三,通过用函数包装调用,来实现指定接收者
var arr = ['Jay', '.M', '.Hu'];
arr.forEach(function(s){
buffer.add(s);
});
console.log(buffer.concat());
针对这样的问题,ES5标准库中提供了一个bind()函数来实现这样的方法。只需要如下代码:
var arr = ['Jay', '.M', '.Hu'];
arr.forEach(buffer.add.bind(buffer));
console.log(buffer.concat());
该bind()函数,利用buffer.add.bind(buffer)创建了一个新函数而不是修改了buffer.add函数。新函数行为就像原来函数的行为,但它的接收者被重新指定了。所以调用bind方法是安全的,即使是一个可能在程序的其他部分被共享的函数。
No.26、使用bind方法实现函数柯里化
Tips:
- 使用bind方法实现函数柯里化,即创建一个固定需求参数子集的委托函数
- 传入null或undefined作为接收者的参数来实现函数柯里化,从而忽略其接收者
什么是函数柯里化?
将函数与其参数的一个子集绑定的技术称为函数柯里化,它是一种简洁的、使用更少引用来实现函数委托的方式。
//有一个组装URL的JS函数
function bulidURL(protocol, domain, path){
return protocol + '://' + domain + '/' + path;
}
//需要一个path数组转换为url数组,那么一般做法是:
var urls = paths.map(function(path){
return bulidURL('http', 'www.hstar.org', path);
});
如果用bind实现函数柯里化,则是:
var buildURL2 = buildURL.bind(null, 'http', 'www.hstar.org');
var urls = paths.map(buildURL2);
其中由于buildURL不引用this,那么在bind中使用null,忽略函数本身的接收者,然后用bind实现柯里化。
使用buildURL.bind的参数+buildURL2的参数结合起来调用buildURL方法。
可以在bulidURL中写console(arguments)来查看参数合集。
No.27、使用闭包而不是字符串来封装代码
Tips:
- 当将字符串传递给eval函数以执行它们的API时,绝不要在字符串中包含局部变量引用
- 接受函数调用的API优于使用eval函数执行字符串的API
JS中,函数是一个将代码作为数据结构存储的便利方式,这些代码可以后面被执行。所以可以在JS中编写富有表现力的高阶函数,如map,forEach。
比较不好的设计,使用eval函数执行字符串。
//定义一个函数,使用eval执行字符串
function fun1(code){
eval(code);
}
//用法一:
var val = 0;
fun1('console.log(val)');
//用法二:
function fun2(){
var val = 1;
fun1('console.log(val)');
}
fun2(); //Error:val is not defined
警告:在使用eval的时候,作用域是全局作用域(window),如用法一的调用,刚好能够出正常结果;如果转移到函数体内,如用法二的调用,则会出现错误;最坏的情况是用法二调用时,全局作用域上刚好有个同名的变量(本例中为val),那么将会让结果无法预期。
好的做法,就是直接传递函数
function fun1(){
}
function fun2(p, action){
if(p === 1){
action();
}
}
fun2();
No.28、不要依赖函数对象的toString方法
Tips:
- 调用函数的toString方法时,并没有要求JavaScript引擎能够精确的获取到函数的源代码
- 由于在不同的引擎下调用toString方法的结果可能不同,所以绝不要信赖函数源代码的详细细节
- toString方法的执行结果并不会暴露存储在闭包中的局部变量值
- 通常情况下,应该避免使用函数对象的toString方法
JavaScript函数有一个非凡的特性,即将其源代码重现为字符串的能力。但是ECMAScript标准对toString返回的字符串没有任何要求,所以不同引擎产生的结果可能不同。甚至返回到字符串和该函数并不相关
No.29、避免使用非标准的栈检查属性
Tips:
- 避免使用非标准的arguments.caller和arguments.callee属性,因为它们不具备良好的移植性
- 避免使用非标准的函数对象caller属性,因为在包含全部栈信息方面,它是不可靠的
基本错误(不推荐使用)
function getCallStack(){
var stack = [];
for(var f = getCallStack.caller; f; f = f.caller){
stack.push(f);
}
return stack;
}
警告:该函数非常脆弱,如果某函数叜调用栈中出现了不止一次,那么栈检查会陷入死循环。同时使用caller在ES5的严格模式下会error。
No.30、理解prototype、getPrototypeOf和proto之间的不同
Tips:
- C.prototype属性是new C() 创建的对象的原型
- Object.getPrototypeOf(obj)是ES5中检索对象原型的标准函数
- obj.__ proto__是检索对象原型的非标准方法
- 类是由一个构造函数和一个关联的原型组成的一种设计模式
简单点说,就是prototype属性直接是创建的对象的原型;getPrototypeOf()是一个标准函数,来获取对象原型;而__ proto__则是不标准的原型属性。
//定义一个类型
function User(name, age){
this.name = name;
this.age = age;
}
//实例化类型
var user = new User('Jay', 23);
//原型属性prototype作用在类对象上
User.prototype
//非标准__proto__作用在对象实例上
user.__proto__
//getPrototypeOf则是Object的一个方法,参数为实例对象
Object.getPrototypeOf(user)
Object.getPrototypeOf(user) === User.prototype; // true
User.prototype === user.__proto__; // true
No.31、使用Object.getPrototypeOf()函数而不要使用__ proto__属性
Tips:
- 使用符合标准的Object.getPrototypeOf()函数而不要使用非标准的__ proto__属性
- 在支持__ proto__属性的非ES5环境中实现Object.getPrototypeOf()函数
由于非标准属性不具有完全兼容性,所以容易出一些奇奇怪怪的问题,不建议使用。 在支持__ proto__的非ES5标准环境下,使用下面代码来实现Object.getPrototypeOf()函数:
if(typeof Object.getPrototypeOf === 'undefined'){
Object.getPrototypeOf = function(obj){
var t = typeof obj;
if(!obj || (t !== 'object' && t !== 'function')){
throw new TypeError('Not an object.');
}
return obj.__proto__;
}
}
No.32、始终不要修改__ proto__属性
Tips:
- 始终不要修改__ proto__属性
- 使用Object.create函数给对象设置自定义原型
__ proto很特殊,具有修改对象原型链的能力。修改了 proto__属性可能会造成以下几个问题:
- 可移植性问题。并不是所有平台都支持改变对象原型的特性
- 性能问题。会使得引擎对JS代码的优化失效
- 行为不可预测。修改了__ proto__可能会破坏原有的继承体系
No.33、使构造函数和new操作符无关
Tips:
- 通过使用new操作符或Object.create方法在构造函数中调用自身使得该构造函数与调用语法无关
- 当一个函数期望使用new操作符调用时,清晰地文档化该函数
同31,我们来看一下User对象:
function User(name, age){
this.name = name;
this.age = age;
}
//如果使用new,那么会创建全新对象
var user = new User('Jay', 23);
//如果忘记使用new呢?
var user = User('Jay', 23)
//这个时候,该句代码,相当于调用函数,此时this在一般情况下是window,在ES5严格模式下是undefined。
//当是window的时候,则会污染全局变量name和age,造成无法预期的问题。
//当是undefined的时候,则会直接导致一个即时错误。
//由于User没有显式return,导致等号左边的user的值为undefined。
为了避免以上问题,可能使用以下两种方式:
//方式一:
//通过在函数体判断,然后调用自身的方式来实现,一定会使用new。缺点是它需要额外的函数调用,对性能有影响。
function User(name, age){
if(!(this instanceof User)){
return new User(name, age);
}
this.name = name;
this.age = age;
}
//方式二:
//通过判断this,将正确的接收者赋值给self,其他函数体内需要用this的地方,全部用self代替。缺点是使用了再ES5环境中有效的Object.create()。
function User(name, age){
var self = this instaceof User ? this : Object.create(User.prototype);
self.name = name;
self.age =age;
}
//方式二补充,由于Object.create()只在ES5中生效,为了在旧环境中使用的话,可以使用以下方式扩充Object.create()。
if(typeof Object.create === 'undefined'){
Object.create = function(prototype){
function C(){}
C.prototype = prototype;
return new C();
}
}
No.34、在原型中存储方法
Tips:
- 将方法存储在实例对象中将创建该函数的多个副本,因为每个实例都有一份副本
- 将方法存储于原型中优于存储在实例对象中
将方法存储在原型上,那么多个实例对象会共享该原型方法。如果存储在实例上的,每创建一个实例则会创建一个函数副本,会占用更多的内存。
No.35、使用闭包存储私有数据
Tips:
- 闭包变量是私有的,只能通过局部引用获取
- 将局部变量作为私有数据从而通过方法实现信息隐藏
不多说,直接上代码:
function User(name, age){
// 私有对象
var privateObj = {
name: name,
age: age,
sex: '男'
}
// 公开属性
return {
name: privateObj.name,
age: privateObj.age,
setAge: function(age){
privateObj.age = age;
}
}
}
var user = new User('Jay', 23);
console.log(user.name); // 'Jay'
console.log(user.age); // 23
console.log(user.sex); // undefined
user.setAge(25);
console.log(user.age); // 23
思考:为什么最后一个user.age 是 23???
修改如下呢:
function User(name, age){ // 私有对象 var privateObj = { name: name, age: age, sex: '男' } // 公开属性 return { name: privateObj.name, age: function(){ return privateObj.age; } setAge: function(age){ privateObj.age = age; } } }
NO.36、只将实例状态存储在实例对象中
Tips:
- 共享可变数据可能会出问题,因为原型是被其所有的实例共享的
- 将可变的实例存储在实例对象中
一般来说,由于原型属性指向的对象是所有实例共享的。所以不建议在原型指向的对象中存储共享数据。下面给一个简单的例子:
var Person = function(name){
this.name = name;
};
Person.prototype = {
children: [],
addChild: function(childName){
this.children.push(childName);
},
getChildren: function(){
return this.children;
}
};
var p1 = new Person('P1');
var p2 = new Person('P2');
p2.addChild('P2_C1');
console.log(p1.getChildren());
结果比较明显。p2的孩子成p1的了。标准做法是将children存储在实例对象中。
var Person = function(name){
this.name = name;
this.children = [];
};
Person.prototype = {
addChild: function(childName){
this.children.push(childName);
},
getChildren: function(){
return this.children;
}
};
No.37、认识到this变量的隐式绑定问题
Tips:
- this变量的作用域总是有其最近的封闭函数所确定
- 使用一个局部变量(通常命名为self,me,that)使得this的绑定对于内部函数是可用的。
老规矩,看一个简单的示例:
var testObj = {
a1: 0,
fun1: function(){
function fun2(){
console.log(this.a1);
}
fun2();
}
};
testObj.fun1();
为什么会这样呢?因为this变量是以不同的方式被绑定的。每个函数都有一个this变量的隐式绑定。this变量是隐式的绑定到最近的封闭函数。针对以上的问题,可以有集中方法来处理,参考如下:
//通过将this用变量self保存的方式实现
var testObj = {
a1: 0,
fun1: function(){
var self = this;
function fun2(){
console.log(self.a1);
}
fun2();
}
};
testObj.fun1();
//通过call方法指定接收者(也可以用apply)
var testObj = {
a1: 0,
fun1: function(){
function fun2(){
console.log(this.a1);
}
fun2.call(this);
}
};
testObj.fun1();
//通过bind来实现
var testObj = {
a1: 1,
fun1: function(){
function fun2(){
console.log(this.a1);
}
fun2.bind(this)();
}
};
testObj.fun1();
No.38、在子类的构造函数中调用父类的构造函数
Tips:
- 在子类构造函数中显式地传入this作为显式的接收者调用父类的构造函数
- 使用Object.create函数来构造子类的原型对象以避免调用父类的构造
JS中实现的继承:
var Animal = function(){
this.weight = 50;
};
Animal.prototype.eat = function(){
console.log('eat food...');
};
var Dog = function(){
Animal.call(this);
Dog.prototype = Object.create(Animal.prototype);
};
var dog = new Dog();
console.log(dog.weight);
No.39、不要重用父类的属性名
Tips:
- 留意父类使用的所有属性名
- 不要再子类中重用父类的属性名
由于JS中,属性都是key-value存储,那么同名的属性指向同样的地址,所以以下代码:
var Animal = function(){
this.weight = 50;
this.id = ++Animal.nextId;
};
Animal.nextId = 0;
Animal.prototype.eat = function(){
console.log('eat food...');
};
var Dog = function(){
Animal.call(this);
this.id = ++ Dog.nextId;
Dog.prototype = Object.create(Animal.prototype);
};
Dog.nextId = 0;
var dog = new Dog();
console.log(dog.id);
两个类都试图给实例属性id写数据。
No.40、避免继承标准类
Tips:
- 继承标准类往往会由于一些特殊的内部属性(如[[Class]])而被破坏
- 使用属性委托优于继承标准类
扩展标注库使得其功能更强大是很有诱惑力的,但不幸的是它们的定义具有很多特殊的行为,所以很难写出正确的子类。
var ArrayEx = function(){
for(var i = 0, len = arguments.length; i<len ; i++){
this[i] = arguments[i];
}
};
ArrayEx.prototype = Object.create(Array.prototype);
var ar = new ArrayEx('1', '2');
console.log(ar.length) //猜猜结果是什么?
原因分析:length属性只对在内部标记为“真正的”数组对象才起作用。直接继承的对象并没有继承 Array的标记标签属性[[Class]]。测试如下:
var ar = new ArrayEx('1', '2');
console.log(Object.prototype.toString.call(ar)); //[object Object]
console.log(Object.prototype.toString.call([])); //[object Array]
ECMAScript标准库中干掉大多数构造函数都有类似的问题。基于这个原因,最好避免继承一下的标准类: Array,Boolean,Date,Function,Number,RegExp或String。
要想实现类似的功能,可以采用属性委托的方式:
var ArrayEx = function(){ this.array = [] for(var i = 0, len = arguments.length; i<len ; i++){ this.array[i] = arguments[i]; } }; ArrayEx.prototype.forEach = function(f, thisArg){ if(typeof thisArg === 'undefined'){ thisArg = this; } this.array.forEach(f, thisArg); }; var ar = new ArrayEx('1sfdfsd', '2fdsfs'); ar.forEach(function(item, i){ console.log(item); });
No.41、将原型视为实现细节
Tips:
- 对象是接口,原型是实现
- 避免检查你无法控制的对象的原型结构
- 避免检查实现在你无法控制的对象内部的属性
我们可以获取对象的属性值和调用其方法,这些操作都不是特别在意属性存储在原型继承结构的哪个位置。只要其属性值保存很定,那么这些操作的行为也不变。简言之,原型是一种对象行为的实现细节。
正是由于以上的特性,所以如果修改了实现细节,那么依赖于这些对象的使用者就会被破坏,而且还很难诊断这类bug。所以一般来说,对于使用者,最好不要干涉那些属性。
No.42、避免使用轻率的猴子补丁
Tips:
- 避免使用轻率的猴子补丁
- 记录程序库所执行的所有猴子补丁
- 考虑通过将修改设置于一个导出函数中,使猴子补丁成为可选的
- 使用猴子补丁为缺失的标准API提供polyfills
何为猴子补丁?
由于对象共享原型,因为每一个对象都可以增加、删除或修改原型的属性。这个有争议的实践通常被称为猴子补丁。
猴子补丁的吸引力在于它的强大,如果数组缺少一个有用的方法,那么我们可以自己扩展它。但是在多个库同时对数组进行不兼容扩展时,问题就来了,有可能调用方法之后的结果和预期不一致。
危险的猴子补丁有一个特别可靠而且有价值的使用场景:polyfill。补齐标准所支持的方法。
No.43、使用Object的直接实例构造轻量级的字典
Tips:
- 使用对象字面量构建轻量级字典
- 轻量级字典应该是Object.prototype的直接子类,以使for...in循环免受原型污染
JavaScript对象的核心是一个字符串属性名称与属性值的映射表。
var dict = {
key1: 'value1',
key2: 'value2'
};
for(var key in dict){
console.log('key='+ key + ',value=' + dict[key]);
}
在使用for...in时,要小心原型污染。
function Dict(){
Dict.prototype.count = function(){
var c = 0;
for(var p in this){
c++;
}
return c;
}
}
var dict = new Dict();
dict.name = 'jay';
console.log(dict.count()); //结果是2,因为for...in会枚举出所有的属性,包括原型上的。
所有人都不应当增加属性到Object.prototype上,因为这样做可能会污染for...in循环,那么我们通过使用Object的直接实例,可以将风险仅仅局限于Object.prototype。
No.44、使用null原型以防止原型污染
Tips:
- 在ES5中,使用Object.create(null)创建的自由原型的空对象是不太容易被污染的
- 在一些较老的环境中,考虑使用{proto: null}
- 要注意
__proto__
既不标准,也不是完全可移植的,并且可能会在未来的JavaScript环境中去除 - 绝不要使用
__proto__
名作为字典的key,因为一些环境将其作为特殊的属性对待
对构造函数的原型属性设置null或者是undefined是无效的:
function Dict(){
}
Dict.prototype = null;
var dict = new Dict();
console.log(Object.getPrototypeOf(dict) === null); // false
console.log(Object.getPrototypeOf(dict) === Object.prototype); //true
在ES5中,提供了标准方法来创建一个没有原型的对象:
var dict = Object.create(null);
console.log(Object.getPrototypeOf(dict) === null); // true
在不支持Object.create函数的旧的JS环境中,可以使用如下方式创建没有原型的对象:
var dict = {__proto__: null}
console.log(Object.getPrototypeOf(dict) === null); // true
注意:在支持Object.create函数的环境中,尽可能的坚持使用标准的Object.create函数
No.45、使用hasOwnProperty方法来避免原型污染
Tips:
- 使用hasOwnProperty方法避免原型污染
- 使用词法作用域和call方法避免覆盖hasOwnProperty方法
- 考虑在封装hasOwnProperty测试样板代码的类中实现字典操作
- 使用字典类避免将
__proto__
作为key来使用
即使是一个空的对象字面量也继承了Object.prototype的大量属性:
var dict = {}
console.log('a' in dict); // false
console.log('toString' in dict); // true
console.log('valueOf' in dict); // true
不过,Object.prototype提供了方法来测试字典条目:
var dict = {}
console.log(dict.hasOwnProperty('a')); // false
console.log(dict.hasOwnProperty('toString')); // false
console.log(dict.hasOwnProperty('valueOf')); // false
但是,如果在字典中存储一个同为“hasOwnProperty”的属性,那么:
var dict = {
hasOwnProperty: null
}
console.log(dict.hasOwnProperty('a')); // TypeError
最安全的方法则是使用call:
var dict = {
hasOwnProperty: null
}
console.log({}.hasOwnProperty.call(dict, 'hasOwnProperty')); // true、
最后,我们来看一个复杂的但更安全的字典类:
function Dict(elements){ this.elements = elements || {}; this.hasSpecialProto = false; this.specialProto = undefined; } Dict.prototype.has = function(key){ if(key === '__proto__'){ return this.hasSpecialProto; } return {}.hasOwnProperty.call(this.elements, key); }; Dict.prototype.get = function(key){ if(key === '__proto__'){ return this.specialProto; } return this.has(key) ? this.elements[key] : undefined; }; Dict.prototype.set = function(key, value){ if(key === '__proto__'){ this.hasSpecialProto = true; this.specialProto = value; }else{ this.elements[key] = value; } }; Dict.prototype.remove = function(key){ if(key === '__proto__'){ this.hasSpecialProto = false; this.specialProto = undefined; }else{ delete this.elements[key]; } }; // 测试代码 var dict = new Dict(); console.log(dict.has('__proto__')); // false
No.46、使用数组而不要使用字典来存储有序集合
Tips:
- 使用for...in 循环来枚举对象属性应当与顺序无关
- 如果聚集运算字典中的数据,确保聚集操作与顺序无关
- 使用数组而不是字典来存储有序集合
由于标准允许JavaScript引擎自由选择顺序,那么如果用字典存储有序数据,就会导致兼容性问题。
No.47、绝不要在Object.prototype中增加可枚举的属性
Tips:
- 避免在Object.prototype中增加属性
- 考虑编写一个函数代理Object.prototype方法
- 如果你是在需要在prototype中增加属性,请使用ES5中的Object.defineProperty方法将它们定义为不可枚举的属性
for...in
循环非常便利,但是容易受到原型污染。如果在Object.prorotype中增加可枚举属性的话,将会导致大多数for...in
循环受到污染。
如果是在是要在Object.prototype上定义属性的话,可以使用如下代码:
Object.defineProperty(Object.prototype, 'allKeys', {
value: function(){
var arr = [];
for(var key in this){
arr.push(key);
}
return arr;
},
writable: true,
enumerable: false, //设置属性为不可枚举
configurable: true
});
测试代码:
var obj = {a: 1, b: 2};
console.log(obj.allKeys()); // ['a', 'b']
No.48、避免在枚举期间修改对象
Tips:
- 当使用
for...in
循环枚举一个对象的属性时,确保不要修改该对象 - 当迭代一个对象时,如果该对象的内容可能会在循环期间被改变,应该使用while循环或经典的for循环来代替
for...in
- 为了在不断变化的数据结构中能够预测枚举,考虑使用一个有序的数据结构,例如数组,而不要使用字典
在大部分编译型语言中,如果在迭代时修改对象属性,是会出现编译错误的。在js中,没有这样的编译机制,但是也尽量保证不要修改迭代对象。
如果在被枚举时添加了新对象,并不一定能保证新添加的对象能被访问到:
var obj = {a: 1, b: 2};
for(var p in obj){
console.log(p);
obj[p + '1'] = obj[p] + 1;
}
遇到这样的场景,应当使用while和标准的for循环。
No.49、数组迭代要优先使用for循环而不是for...in
循环
Tips:
- 迭代数组的索引属性应当总是使用for循环而不是
for...in
循环 - 考虑在循环之前将数组的长度存储在一个局部变量中以避免重新计算数组长度
猜测下面一段代码的结果?
var arr = [5, 6, 8, 10, 9];
var sum = 0;
for(var a in arr){
sum += a;
}
console.log(sum);
要达到正确的结果,那么应该使用for循环
var arr = [5, 6, 8, 10, 9];
var sum = 0;
for(var i = 0, len = arr.length; i < len; i++){
sum += arr[i];
}
console.log(sum); //38
再看一个比较极端的例子:
var arr = [5, 6, 8, 10, 9];
arr.len = 4;
for(var p in arr){
console.log(p);
}
这个时候用for...in
,完全是达不到预期效果的
再来看一个对于数组长度缓存的测试代码:
var count = 0;
console.time('t1');
while(count < 10000){
var arr = [5, 6, 8, 10, 9];
var sum = 0;
count++;
for(var i = 0, len = arr.length; i < len; i++){
sum += arr[i];
}
}
console.timeEnd('t1');
count = 0;
console.time('t2');
while(count < 10000){
var arr = [5, 6, 8, 10, 9];
var sum = 0;
count++;
for(var i = 0; i < arr.length; i++){
sum += arr[i];
}
}
console.timeEnd('t2');
结果,请自行复制代码执行。。。
No.50、迭代方法优于循环
Tips:
- 使用迭代方法(如Array.prototype.forEach和Array.prototype.map)替换for循环使得代码更可读,并且避免了重复循环控制逻辑
- 使用自定义的迭代函数来抽象未被标准库支持的常见循环模式
- 在需要提前终止循环的情况下,仍然推荐使用传统的循环。另外some和every方法也可用于提前退出
在使用循环的时候,在确定循环的终止条件时容易引入一些简单的错误:
for(var i = 0; i <= n; i++){}
for(var i = 1; i< n; i++){}
比较庆幸的是,闭包是一种为这些模式建立迭代抽象方便的、富有表现力的手法。
我们可以用以下代码来代替:
var arr = [1, 2, 3];
arr.forEach(function(v, i){
console.log(v);
});
如果要创建新数组,那么可以用以下方式:
var arr = [1, 2, 3];
var arrNew = [];
//方式一
arr.forEach(function(v, i){
arrNew.push(v);
});
//方式二
for(var i = 0, len = arr.length; i < len; i++){
arrNew.push(arr[i]);
}
为了简化这种普遍操作,ES5中引入了Array.prototype.map方法:
var arr = [1, 2, 3];
var arrNew = arr.map(function(v){
return v;
});
同样,如果想提取满足条件的元素,ES5也提供了filter方法:
var arr = [1, 2, 3];
var arrNew = arr.filter(function(v){
return v > 1;
});
console.log(arrNew);
在ES5中,针对数组也提供了some和every ,可以用来终止循环,但是实际意义等同于C#的Linq方法All和Any:
var arr = [1, 2, 3]; //数组元素有一个>1就返回true,并终止循环 var b = arr.some(function(a){ return a>1; }); console.log(b); //true //数组元素每个都<3,则返回true,否则返回false,并提前终止循环 b = arr.every(function(a){ return a<3; }); console.log(b); //false
No.51、在类数组对象上附庸通用的数组方法
Tips:
- 对于类数组对象,通过提取方法对象并使用其call方法来复用通用的Array方法
- 任意一个具有索引属性和恰当length属性的对象都可以使用通用的Array方法
Array.proteotype中的标准方法被设计成其他对象可复用的方法,即使这些对象没有继承Array。很实际的一个例子就是 arguments
,示例如下:
//define
function fun(){
console.log(arguments); // [1, 2, 3]
console.log(arguments instanceof Array) // false
arguments.forEach(function(argv){ //TypeError
console.log(argv)
});
}
//call
fun(1, 2, 3);
从结果来看,输出arguments和数组非常相似,通过instanceof来看,确实不是数组,所以arguments是类数组对象,但是在执行forEach的时候却TypeError。why?
因为 arguments
没有继承Array.prototype,所以并不能直接调用forEach方法,但是可以提取forEach方法的引用并使用其call来调用,代码如下:
//define
function fun(){
[].forEach.call(arguments, function(argv){
console.log(argv);
});
}
//call
fun(1, 2, 3);
除了arguments之外,dom的NodeList也是类数组对象:
var nodes = document.getElementsByTagName(‘a‘);
console.log(nodes);
console.log(nodes instanceof Array); // false
那么,到底怎样使得一个对象“看起来像数组”呢?有以下两个规则:
- 具有一个范围在0到2^32 - 1 的整型length属性
- length属性大于该对象的最大索引。索引是一个范围在0到2^32 -2 的整数,它的字符串表示的是该对象的一个key。
鉴于以上规则,那么我们可以自己创建类数组对象:
var arrayLike = {0: ‘a‘, 1: ‘b‘, 2: ‘c‘, length: 3};
var result = [].map.call(arrayLike, function(el){
return el.toUpperCase();
});
console.log(result); // [‘A‘, ‘B‘, ‘C‘]
特例,数组连接方法concat不是完全通用的。因为它会检查对象的[[Class]]属性,要想连接类数组对象,我们就需要先将类数组处理为数组:
var arrLike = {0: ‘a‘, length: 1};
var arr = [].slice.call(arrLike);
console.log([‘A‘].concat(arr)); // [‘A‘, ‘a‘]
No.52、数组字面量优于数组构造函数
Tips:
- 如果数组构造函数的第一个参数是数字则数组的构造函数行为是不同的
- 使用数组字面量替代数组构造函数
原因如下:
[] 比 new Array简洁
var arr = [];
var arr = new Array();
使用new Array(),必须要确保没有人重新包转过Array变量
funciton f(Array){
return new Array(1, 2, 3, 4, 5);
}
f(String); //new String(1)
使用new Array(),必须要确保没有人修改过全局的Array变量
Array = String
new Array(1, 2, 3); // new String(1)
使用new Array时,由于第一个参数类型不同,会导致二义性
new Array(‘hello‘) 和 [‘hello‘] 等价
[1] 和 new Array(1) 不等价,前者创建包含元素的1的数组,后则创建长度为1的数组。
所以,优先使用字面量,因为数组字面量具有更规范、更一致的语义。
No.53、保持一致的约定
Tips:
- 在变量命名和函数签名中使用一致的约定
- 不要偏离用户在他们的开发平台中很可能遇到的约定
有良好的编码习惯,使用业界常规的编码规范,同时注意参数的顺序等。一句话概述:保持代码的一致性。
No.54、将undefined看做“没有值”
Tips:
- 避免使用undefined表示任何非特定值
- 使用描述性的字符串值或命名布尔属性的对象,而不要使用undefined 或 null来代表特定应用标志
- 提供参数默认值应该采用测试undefined的方式,而不是检查arguments.length。
- 在允许0、NaN或空字符串为有效参数的地方,绝不要通过真值测试来实现参数默认值。
undefined很特殊,当JavaScript无法提供具体的值时没救产生undefined。 如只定义变量,不赋值;或者是对象中不存在属性;再者,函数无return语句都会产生undefined。
var x;
console.log(x); //undefined
var o = {};
console.log(o.p1); //undefined
function fun(){
}
console.log(fun()); //undefined
未给函数参数提供实参则该函数参数值为undefined
function fun(x){
return x;
}
console.log(fun()); //undefined
将undefined看做缺少某个特定的值是公约。将它用于其他目的具有很高的风险:
//假设highlight为设置元素高亮
element.highlight(‘yellow‘); //设置为黄色
//如果要设置为随机颜色
//方式一、如果遇到undefined则设置为随机
element.highlight(undefined);
//这样的方式通常会产生歧义
element.highlight(config.highlightColor);
//使用如上语句时,我们的期望一般是没有提供配置则使用默认色,但是由于undefined代表随机,那么破坏了这种常规思维。让代码变得难以理解。
//更好的做法
element.highlight(‘random‘);
//或者是
element.highlight({random: true});
另一个提防undefined的地方是可选参数的实现。
function fun(a, b){
if(arguments.length < 2){
b = ‘xx‘;
}
}
如果使用 fun(a);调用,基本符合预期;但是如果使用fun(a, ‘undefind‘);则不会执行if之内的语句,导致结果错误,如果测试是否为undefined有助于打造更为健壮的API。
针对可选参数这个问题,另外一个合理的替代方案是:
function fun(a, b){
b = b || ‘xxx‘;
}
但是要注意,真值测试并不总是安全的。如果一个函数应该接受空字符串,0,NaN为合法值,那么真值测试就不该使用了。
//Bad Use
function Point(x, y){
this.x = x || 200;
this.y = y || 200;
}
以上代码有什么问题呢,因为使用 new Point(0, 0);会导致使用默认值,这样就偏离了预期。所以需要更严格的测试:
function Point(x, y){
this.x = x === undefined ? 200 : x;
this.y = y === undefined ? 200 : y;
}
No.55、接收关键字参数的选项对象
Tips:
- 使用选项对象似的API更具可读性、更容易记忆
- 所有通过选项对象提供的参数应当被视为可选的
- 使用extend函数抽象出从选项对象中提取值的逻辑
首先来看一个复杂的函数调用:
fun(200, 200, ‘action‘, ‘green‘, true);
一眼望去,完全不知所云。在体会到C#的可选参数的便利性的时候,肯定会想JavaScript要是有这样的用法就好了。
幸运的是,JavaScript提供了一个简单、轻量的惯用法:选项对象。基本达到了可选参数的效果。
fun({
width: 200,
height: 200,
action: ‘action‘,
color: ‘green‘,
ignoreError: true
});
相对来说,更繁琐一点,但是更易于阅读。另外一个好处就是,参数都是可选的。
如果有必选参数,那么在设计API的时候。建议将它们独立于选项之外,其他语言也可借鉴这种思路。
// options 为可选参数
function fun(width, height, options){
}
通过extend组合可选参数和默认参数,可以让函数变得简洁和健壮。
function fun(width, height, options){ var defaults = { color: ‘green‘, ignoreError: false, action: ‘‘ } //$.extend 可以理解为jQuery的方法 options = $.extend({}, defaults, options); //do something... }
No.56、避免不必要的状态
Tips:
- 尽可能地使用无状态的API
- 如果API是有状态的,标示出每个操作与哪些状态有关联
无状态的API简洁,更容易学习和使用,也不需要考虑其他的状态。如:
'test'.toUpperCase(); // 'TEST'
有状态的API往往会导致额外的声明,并增加复杂度。
No.57、使用结构类型设计灵活的接口
Tips:
- 使用结构类型(也称为鸭子类型)来设计灵活的对象接口
- 结构接口更灵活、更轻量,所以应该避免使用继承
- 针对单元测试,使用mock对象即接口的替代实现来提供可复验的行为
直接上代码:
function Wiki(format){
this.format = format;
}
Wiki.prototype.show = function(source){
var page = this.format(source);
return {
title: page.getTitle(),
author: page.getAuthor(),
content: page.getContent()
}
}
将format设计为结构类型,可以极大的增加设计的灵活性。
No.58、区分数组对象和类数组对象
Tips:
- 绝不重载与其他类型有重叠的结构类型
- 当重载一个结构类型与其他类型时,先测试其他类型
- 当重载其他对象类型时,接收真数组而不是类数组对象
API绝不应该重载与其他类型有重叠的类型
最简单的判断数组与类数组,代码如下:
x instanceof Array
但是,在一些允许多个全局对象的环境中可能会有标准的Array构造函数和原型对象的多份副本。那么就有可能导致以上的测试结果不可信,所以在ES5引入了Array.isArray函数来判断是否是Array对象,通过检查对象内部[[Class]]属性值是否为Array来判定。在不支持ES5的环境中,可以使用标准的Object.prototype.toString方法测试一个对象是否为数组。
function isArray(x){
return toString.call(x) === '[object Array]';
}
No.59、避免过度的强制转换
Tips:
- 避免强制转换和重载的混用
- 考虑防御性地监视非预期的输入
看以下的函数:
function square(x){
return x*x;
}
console.log(square('3')); // 9
强制转换无疑是很方便的。但很多时候却会导致含糊不清。
function fun(x){
x = Number(x);
if(typeof x === 'number'){
return x-1;
}else{
return x;
}
}
由于进行了Number(x),那么后面的else是无法执行到的。如果不知道这个函数的细节,那么使用该函数则具有一定的模糊性。 事实上,如果我们要更小心的设计API,我们可以强制只接受数字和对象。
function fun(x){
if(typeof x === 'number'){
return x-1;
}else if(typeof x === 'object' && x){
return x;
}else{
throw new TypeError('expected number or array-like.');
}
}
这种风格更加谨慎的示例,被称为防御性编程。
No.60、支持方法链
Tips:
- 使用方法链来连接无状态的操作
- 通过在无状态的方法中返回新对象来支持方法链
- 通过在有状态的方法中返回this来支持方法链
无状态的API部分能力是讲复杂操作分解为更小的操作。如replace:
function escapeHtml(str){
return str.replace(/&/g, '&')
.replace(/</g, '<');
}
如果不采用方法链方式,代码应该是以下这样:
function escapeHtml(str){
var str1 = str.replace(/&/g, '&');
var str2 = str1.replace(/</g, '<');
return str2;
}
同样的功能,将会产生多个临时变量。消除临时变量使得代码更加可读,中间结果只是得到最终结果中的一个重要步骤而已。
在有状态的API中设置方法链也是可行的。技巧是方法在更新对象时返回this,而不是undefined。如:
element.setBackgroundColor('gray') .setColor('red') .setFontweight('bold');
No.61、不要阻塞I/O事件队列
Tips:
- 异步API使用回调函数来延缓处理代价高昂的操作以避免阻塞主应用程序
- JavaScript并发的接收事件,但会使用一个事件队列按序地处理事件处理程序
- 在应用程序事件队列中绝不要使用阻塞的I/O
JavaScript程序是构建在事件之上的。在其他一些语言中,我们可能常常会实现如下代码:
var result = downFileSync(‘http://xxx.com‘);
console.log(result);
以上代码,如果downFileSync需要5分钟,那么程序就会停下来等待5分钟。这样的函数就被称为同步函数(或阻塞函数)。如果在浏览器中实现这样的函数,那么结果就是浏览器卡住,等待下载完成后,再继续响应。那么,这将极大的影响体验。所以,在JavaScript中,一般使用如下方式:
downFileAsync(‘http://xxx.com‘, function(result){
console.log(result);
});
console.log(‘async‘);
以上代码执行中,就算下载文件要5分钟,那么程序也会立马打印出“async”,然后在下载完成的时候,打印result出来。这样才能保证执行环境能正确响应客户的操作。
JavaScript并发的一个最重要的规则是绝不要在应用程序事件队列中使用阻塞I/O的API。在浏览器中,甚至基本没有任何阻塞的API是可用的。其中XMLHttpRequest库有一个同步版本的实现,被认为是一种不好的实现,影响Web应用程序的交互性。
在现代浏览器(IE10+(含)、Chrome、FireFox)中,提供了Worker的API,该API使得产生大量的并行计算称为可能。
如何使用?
首先,编写两个文件,第一个是task.js,如下:
//task.js
console.time(‘t1‘);
var sum = 0;
for(var i = 0; i < 500000000; i++){
sum += i;
}
console.log(‘test‘);
console.timeEnd(‘t1‘);
postMessage(‘worker result:‘ + sum);
然后是index.html,用于调用worker,代码如下:
// index.html
<button onclick="alert(‘aa‘)">Test</button>
<script>
var worker = new Worker(‘test.js‘);
worker.onmessage = function(evt){
console.log(evt.data);
};
</script>
在index.html的JavaScript脚本中。使用var worker = new Worker(‘test.js‘);
来实例化一个Worker,Worker的构造为:new Worker([string] url),然后注册一个onmessage事件,用于处理test.js的通知,就是test.js中的postMessage函数。test.js中的每一次执行postMessage函数都会触发一次Worker的onmessage回调。
在静态服务器中访问index.html,可以看到输出为:
test
t1: 2348.633ms
worker result:124999999567108900
再来看看Worker的优缺点,我们可以做什么:
- 可以加载一个JS进行大量的复杂计算而不挂起主进程,并通过postMessage,onmessage进行通信
- 可以在worker中通过importScripts(url)加载另外的脚本文件
- 可以使用 setTimeout(), clearTimeout(), setInterval(), and clearInterval()
- 可以使用XMLHttpRequest来发送请求
- 可以访问navigator的部分属性
有那些局限性:
- 不能跨域加载JS
- worker内代码不能访问DOM
- 各个浏览器对Worker的实现不大一致,例如FF里允许worker中创建新的worker,而Chrome中就不行
- 不是每个浏览器都支持这个新特性
更多信息,请参考:
- https://developer.mozilla.org/zh-CN/docs/Web/Guide/Performance/Usingwebworkers
- http://www.cnblogs.com/feng_013/archive/2011/09/20/2175007.html
No.62、在异步序列中使用嵌套或命名的回调函数
Tips:
- 使用嵌套或命名的回调函数按顺序地执行多个异步操作
- 尝试在过多的嵌套的回调函数和尴尬的命名的非嵌套回调函数之间取得平衡
- 避免将可被并行执行的操作顺序化
想象一下如下需求,异步请数据库查找一个地址,并异步下载。由于是异步,我们不可能发起两个连续请求,那么js代码很可能是这样的:
db.lookupAsync(‘url‘, function(url){
downloadAsync(url, function(result){
console.log(result);
});
});
我们使用嵌套,成功解决了这个问题,但是当这样的依赖很多时,我们的代码可能是这样:
db.lookupAsync(‘url‘, function(url){
downloadAsync(‘1.txt‘, function(){
downloadAsync(‘2.txt‘, function(){
downloadAsync(‘3.txt‘, function(){
//do something...
});
});
});
});
这样就陷入了回调地狱。要减少过多的嵌套的方法之一就是将回调函数作为命名的函数,并将它们需要的附加数据作为额外的参数传递。比如:
db.lookupAsync(‘url‘, downloadUrl);
function downloadUrl(url){
downloadAsync(url, printResult);
}
function printResult(result){
console.log(result);
}
这样能控制嵌套回调的规模,但是还是不够直观。实际上,在node中解决此类问题是用现有的模块,如async。
No.63、当心丢弃错误
Tips:
- 通过编写共享的错误处理函数来避免复制和粘贴错误处理代码
- 确保明确地处理所有的错误条件以避免丢弃错误
一般情况下,我们的错误处理代码如下:
try{
a();
b();
c();
}catch(ex){
//处理错误
}
对于异步的代码,不可能将错误包装在一个try中,事实上,异步的API甚至根本不可能抛出异常。异步的API倾向于将错误表示为回调函数的特定参数,或使用一个附加的错误处理回调函数(有时被称为errbacks)。代码如下:
downloadAsync(url, function(result){
console.log(result);
}, function(err){ //提供一个单独的错误处理函数
console.log(‘Error:‘ + err);
});
多次嵌套时,错误处理函数会被多次复制,所以可以将错误处理函数提取出来,减少重复代码,代码如下:
downloadAsync(‘1.txt‘, function(result){
downloadAsync(‘2.txt‘, function(result2){
console.log(result + result2);
}, onError);
}, onError);
在node中,异步API的回调函数第一个参数表示err,这已经成为一个大众标准
No.64、对异步循环使用递归
Tips:
- 循环不能是异步的
- 使用递归函数在时间循环的单独轮次中执行迭代
- 在事件循环的单独伦次中执行递归,并不会导致调用栈溢出
针对异步下载文件,如果要使用循环,大概是如下代码:
function downloadFilesSync(urls){
for(var i = 0, len = urls.length; i < len; i++){
try{
return downloadSync(urls[i]);
}catch(ex){
}
}
}
以上代码并不能正确工作,因为方法一调用,就会启动所有的下载,并不能等待一个完成,再继续下一个。
要实现功能,看看下面的递归代码:
function downloadFilesSync(urls){
var len = urls.length;
function tryNextURL(i) {
if (i >= n) {
console.log(‘Error‘);
return; //退出
}
downloadAsync(urls[i], function(result){
console.log(result);
//下载成功后,尝试下一个。
tryNextURL(i + 1);
});
}
tryNextURL(0);// 启动递归
}
类似这样的实现,就能解决批量下载的问题了。
No.65、不要再计算时阻塞事件队列
Tips:
- 避免在主事件队列中执行代码高昂的算法
- 在支持Worker API的平台,该API可以用来在一个独立的事件队列中运行长计算程序
- 在Worker API 不可用或代价高昂的环境中,考虑将计算程序分解到事件循环的多个轮次中
打开浏览器控制台,执行 while(true){}
,会是什么效果?
好吧,浏览器卡死了!!!
如果有这样的需求,那么优先选择使用Worker实现吧。由于有些平台不支持类似Worker的API,那么可选的方案是将算法分解为多个步骤。代码如下:
//首先,将逻辑分为几个步骤
function step1(){console.log(1);}
function step2(){console.log(2);}
function step3(){console.log(3);}
var taskArr = [step1, step2, step3];
var doWork = function(tasks){
function next(){
if(tasks.length === 0){
console.log(‘Tasks finished.‘);
return;
}
var task = tasks.shift();
if(task){
task();
setTimeout(next, 0);
}
}
setTimeout(next, 0);
}
//启动任务
doWork(taskArr);
No.66、使用计数器来执行并行操作
Tips:
- JavaScript应用程序中的事件发生是不确定的,即顺序是不可预测的
- 使用计数器避免并行操作中的数据竞争
先看一个简单的示例:
function downFiles(urls){
var result = [],len = urls.length;
if(len === 0){
console.log(‘urls argument is a empty array.‘);
return;
}
urls.forEach(function(url){
downloadAsync(url, function(text){
result.push(text);
if(result.length === len){
console.log(‘download all files.‘);
}
});
});
}
有什么问题呢?result的结果和urls是顺序并不匹配,所以,我们不知道怎么使用这个result。
如何改进?请看如下代码,使用计数器,代码如下:
function downFiles(urls){
var result = [],len = urls.length;
var count = 0;// 定义计数器
if(len === 0){
console.log(‘urls argument is a empty array.‘);
return;
}
urls.forEach(function(url, i){
downloadAsync(url, function(text){
result[i] = text;
count++;
//计数器等于url个数,那么退出
if(count === len){
console.log(‘download all files.‘);
}
});
});
}
No.67、绝不要同步地调用异步的回调函数
Tips:
- 即使可以立即得到数据,也绝不要同步地调用异步回调函数
- 同步地调用异步的回调函数扰乱了预期的操作序列,并可能导致意想不到的交错代码
- 同步地调用异步的回调函数可能导致栈溢出或错误的处理异常
- 使用异步的API,比如setTimeout函数来调用异步回调函数,使其运行于另外一个回合
如果异步下载代码,优先从缓存拿数据,那么代码很可能是:
var cache = new Dict();
function downFileWithCache(url, onsuccess){
if (cache.has(url)){
onsuccess(cache.get(url));
return;
}
return downloadAsync(url, function(text){
cache.set(url, text);
onsuccess(text);
});
}
以上代码,同步的调用了回调函数,可能会导致一些微妙的问题,异步的回调函数本质上是以空的调用栈来调用,因此将异步的循环实现为递归函数是安全的,完全没有累计赵越调用栈控件的危险。同步的调用不能保证这一点,所以,更好的代码如下:
var cache = new Dict();
function downFileWithCache(url, onsuccess){
if (cache.has(url)){
setTimeout(onsuccess.bind(null, cache.get(url)), 0)
return;
}
return downloadAsync(url, function(text){
cache.set(url, text);
onsuccess(text);
});
}
No.68、使用promise模式清洁异步逻辑
Tips:
- promise代表最终值,即并行操作完成时最终产生的结果
- 使用promise组合不同的并行操作
- 使用promise模式的API避免数据竞争
- 在要求有意的竞争条件时使用select(也被称为choose)
一直以来,JavaScript处理异步的方式都是callback,当异步任务很多的时候,维护大量的callback将是一场灾难。所以Promise规范也应运而生,http://www.ituring.com.cn/article/66566 。
Promise已经纳入了ES6,而且高版本的Chrome、Firefox都已经实现了Promise,只不过和现如今流行的类Promise类库相比少些API。
看下最简单的Promise代码(猜猜最后输出啥?):
var p1 = new Promise(function(resolve, reject){
setTimeout(function(){
console.log(‘1‘);
resolve(‘2‘);
}, 3000);
});
p1.then(function(val){
console.log(val);
});
如果代码是这样呢?
var p1 = new Promise(function(resolve, reject){
setTimeout(function(){
console.log(‘1‘);
//resolve(‘2‘);
reject(‘3‘);
}, 3000);
});
p1.then(function(val){
console.log(val);
}, function(val){
console.log(val);
});
再来看一个Promise.all的示例:
Promise.all([new Promise(function(resolve, reject){
setTimeout(function(){
console.log(1);
resolve(1);
}, 2000);
}), new Promise(function(resolve, reject){
setTimeout(function(){
console.log(2);
resolve(2);
}, 1000);
}), Promise.reject(3)])
.then(function(values){
console.log(values);
});
Promise.all([]).then(fn)
只有当所有的异步任务执行完成之后,才会执行then。
接着看一个Promise.race的示例:
Promise.race([new Promise(function(resolve, reject){
setTimeout(function(){
console.log(‘p1‘);
resolve(1);
}, 2000);
}), new Promise(function(resolve, reject){
setTimeout(function(){
console.log(‘p2‘);
resolve(2);
}, 1000);
})])
.then(function(value){
console.log(‘value = ‘ + value);
});
结果是:
p2
value = 2
p1
Promise.race([]).then(fn)
会同时执行所有的异步任务,但是只要完成一个异步任务,那么就调用then。
promise.catch(onRejected)是promise.then(undefined, onRejected) 的语法糖。
更多关于Promise的资料请参考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
第三方Promise库有许多,如:Q, when.js 等
原文:http://www.cnblogs.com/humin/p/4381350.html