Effective JavaScript
让自己习惯javaScript
1 - 了解你使用的javaScript版本
ES5引入的'严格模式',在js文件连接使用时可能导致问题.
如:
//file1.js "use strict"; function f(){ //... } //file2.js no strict-mode function f(){ //arguments为方法自带的,在严格模式下重新定义会报错 var arguments = []; } //两个倒过来,非严格模式的在前面,又会导致严格模式文件严格模式无效了.
解决方案:
- 避免使用严格模式的js文件和不使用严格模式的js文件连接使用.
- 通过(function(){...})();的方式包裹自身,在被连接后,都会被独立的解释执行.缺点是,这种方法会导致文件的内定义的全局变量,方法等不会被视作全局的.
2 - 理解javaScript浮点数
tips:
-
javaScript的数字都是双精度浮点数,即double(64位浮点数).可精确表示-253~253之间的数.
数字类型 .toString(n);可以把数字转换成对应进制展示 注意数字后要加一个空格
如 console.log(29 .toString(15)); => "1e"
javaScript的整数仅仅是双精度浮点数的一个子集
-
位运算会将数字视为32位有符号整数(会直接舍弃后面的小数位),计算完后把结果转换成浮点数
8.1 | 1 => 1000.0001100110011001100110011001100110011001100110011 | 1 => 1000 | 1 => 1001 => 1001(浮点数)
尽量避免使用浮点数运算,可以的话转换成整数计算.如金钱计算都乘以100,以分为单位就不会有小数,就不存在浮点精度误差了.
3 - 当心隐式类型转换
由于javaScript对类型错误极其宽容导致.JavaScript会按照多种多样的自动转换协议将值强制转换为期望的类型.
-
算数运算符( + - * / % )会尝试把两边都转换成数字(+ 既重载了数字+也重载了字符串加法,具体是数字相加还是字符串相连取决于参数类型, 从左到右)
null在算数运算中会被隐式转换成0
-
未定义的变量会变转换成NaN
-
如果知道是数字,isNaN可以判断是否是NaN;但如果不是数字,isNaN无法区分.
如: isNaN("foo")=>true ,同理undefined,{},{valueOf:"foo"}都会返回true
tips:可以利用NaN是Js中唯一不等于自身的值这个特性, 用 x !== x判断是否为真的NaN
-
位运算( ~ & ^ | << >> >>> )会把操作数转换成数字,且会转换成32位整数
-
对象隐式转换toString()方法转换成字符串,valueOf方法转换成数字.除了 "+"外,其他算数运算符会调用valueOf的方法转换成数字计算;位运算也会调用valueOf转换成数字计算;而加法的隐式转换常常会出人意料.所以建议对象有valueOf方法时,同时也要定义toString方法,返回与valueOf值一致的字符串结果.(当然,最好避免使用对象的valueOf方法)(见5)
请留意:
var x = "s"; x == "s" //true x === "s" //true new String("s") == new String("s") //false new String("s") === new String("s") //false x == new String("s") //true x === new String("s") //false
-
真值转换.JavaScript只有7个假值: false, 0, -0, "", NaN, null, undefined.其他都为真值.
if, || 和 && 等运算符时,将值转换成真值计算.
-
判断是否为undefined的两种方式
- typeof x === "undefined"
- x === undefined
4 - 原始类型优于封装对象
除对象外,JavaScript有5个原始值类型:布尔值boolean,数字number,字符串string,null和undefined
注意: typeof null会返回object
布尔值,数字,字符串有对应的包装类型Boolean,Number,String
隐式转换②: 隐式封装.
举例: "hello".toUpperCase(); //"HELLO"
会生成一个新的String对象,调用方法返回结果,且每次隐式封装都会产生一个新的对象,
所以对原始值设置属性是没有任何意义的.
所以:
"hello".someProperty = 17; "hello".someProperty; //undefined
第一次产生的对象和第二次调用时并不相同,所以第二次会返回undefined
5 - 避免对混合类型使用 == 运算符
当两个参数属于同一类型时,使用 == 和 === 运算符没有区别.
参数类型1 | 参数类型2 | 强制转换 |
---|---|---|
null | undefined | 不转换,总返回true |
null/undefined | 非null/非undefined | 不转换,总返回false |
原始类型string,number,boolean | Date对象 | 原始类型转换成数字,Date对象转换成原始类型(优先尝试toString,再尝试valueOf方法) |
原始类型string,number,boolean | 非Date对象 | 原始类型转换成数字,非Date对象转换成原始类型(优先尝试valueOf,再尝试toString方法) |
原始类型string,number,boolean | 原始类型string,number,boolean | 原始类型转换成数字 |
解决方案:
- 尽量使用 === 运算符
- 比较不同类型时,自己定义显示的强制转换方法,让比较的行为更清晰.
6 - 了解分号插入的局限
分号插入规则:
-
分号会在 } 标记之前, 一个或多个换行之后, 程序输入的结尾处被插入.
(即 你可以省略一行后、一个代码块和一段程序结束处的分号)
function square(x){ var n = +x return n * n } function area(r) { r = +r; return Math.PI * r * r} function add1(x) {return x+1}
-
分号仅在随后的输入标记不能解析时插入.
如:
a = b (f());
系统会判断 a = b(f());可以正常解析,所以此处第一行不能省略分号
5个明确有问题的开头字符: ( [ + - /
- 解决方案:在出现这五个开头字符时,显式地在前面加上分号
javaScript限制产生式(restricted production)不允许两个字符之间出现换行时,会强制插入分号
包括:
-
return语句
return {};
等价于
return ; {} ;
throw语句
带有显式标签的break或continue语句
-
后置自增或自减运算符
a ++ b
等价于
a; ++b; //因为后置++ 就会在a后面强制加上分号
-
分号不会作为分隔符在for循环空语句的头部被自动插入,空循环体的while循环也需要显式的分号
for(var i = 0, total = 1 //会报错,不会自动插入分号 i < n i++){}
function infiniteLoop(){ while(true) ; }//没有这个分号会报错
7 - 视字符串为16位的代码单元序列
Unicode为所有文字每个字符都分配了唯一整数,整数介于0和1114111之间,称为代码点.
Unicode允许多个不同二进制编码的代码点.目前最流行的有UTF-8,UTF-16,UTF-32.
将代码点与其编码元素一对一映射起来,称为一个代码单元.
由于历史原因,JavaScript字符串的元素是一个16位的代码单元,字符串属性和方法(如length,charAt,charCodeAt)都是基于代码单元层级,而不是代码点层级.所以每当字符串包含辅助平面的代码点时(即需要用两个代码单元去表示一个代码点时),javascript将每个代码点表示为两个元素而不是一个(一对UTF-16代理对的代码点).(正则表达式模式也一样有这个问题)
当出现两个代码单元表示一个代码点的时候,length,charAt,charCodeAt等方法在执行时,会出现意想不到的结果.
而例如sendcodeURI,decodeURI,encodeURIComponent和decodeURIComponent等URI操作函数的ECMAScript库正确的处理了代理对,就不会有问题.
tips:
- 使用第三方的库编写可识别代码点的字符串操作.
- 每当使用一个含有字符串操作的库时,都要查阅该库文档,看他如何处理代码点的整个范围.
变量作用域
8 - 尽量少用全局变量
定义全局变量会污染共享的公共命名空间,可能导致意外的命名冲突;不利于模块化,因为他会导致程序独立组件之间不必要的耦合.
tips:
避免声明全局变量,尽量使用局部变量
避免对全局对象添加属性
-
使用全局对象来做平台特性检测
如:ES5引入的全局JSON对象(用于读写JSON格式的数据),检测当前环境是否提供了这个对象
if(!this.JSON){ //如果没有这个对象,就定义一个. this.JSON = { parse: ..., stringify: ... } }
9 - 始终声明局部变量
不使用var,let,const声明变量,该变量会隐式的转变为全局变量
tips:
- 使用lint工具帮助检查未绑定的变量
10 - 避免使用with
with的简单介绍:
//with 扩展一个语句的作用域链 ES5的严格模式已禁用with //with 出现的目的是减少变量的长度,减少不必要的指针路径解析运算 with(expression){//将表达式添加到评估语句时使用的作用域链上 statement//任何语句 } //举例 var a, x, y; var r = 10; with(Math){//Math被作为默认对象,PI,cos,sin前面不需要添加命名空间 a = PI * r * r; x = r * cos(PI); y = r * sin(PI / 2); } //with 使程序查找变量时都会先去指定对象中找,那些不属于他的属性查找速度会很慢 //另外,with导致了语义不明的严重问题,如下: function f(x,o){ with(o){ print(x);//方法f获取的参数x可能取到值,可能为undefined;能取到时,可能在o上,也可能是函数的第一个参数(o中没有x这个属性的话),人力无法预测此处的值 } }
tips:
避免使用with语句.不仅性能差,而且有语义问题
-
使用简单的变量名代替重复访问的对象;想要实现with的初衷,可以将局部变量显式的绑定到相关属性上
如:
function(x,y){ var min = Math.min, round = Math.round, sqrt = Math.sqrt; return min(round(x), sqrt(y)); }
11 - 熟练掌握闭包
JavaScript的函数值包含了比调用他们时执行所需的代码需要的信息以外的更多的信息.
他们可以获取在其封闭作用域的变量
那些在其所涵盖的作用域内跟踪变量的函数,称为闭包.
闭包的三个事实:
//此处的外部指的是闭包函数的外部
- JavaScript允许你引用当前函数以外定义的变量.=>闭包可以引用定义在其外部作用域的变量
- 即使外部函数已经返回,当前函数仍可以引用外部函数所定义的变量.=>闭包比创建他们的函数有更长的生命周期
- 闭包可以更新外部变量的值
warning:闭包存储的外部变量的引用,而不是值!!!
举例:
//假设一个获取全名的方法 //函数柯里化 function initLastname(lastname){ return function(firstname){ return `${lastname} ${firstname}`; } } //此处获取定义了姓的方法 const getNameStartWithSheng = initLastname('sheng'); getNameStartWithSheng('wei');// sheng wei getNameStartWithSheng('jie');// sheng jie //重新定义一个新的方法... const getNameStartWithLi = initLastname('li');
12 - 理解变量声明提升
tips:
javaScript隐式地提升声明部分到封闭函数的顶部,而把赋值留在原地;提升方法声明到顶部,而不提升方法调用.
在一个作用域中重复声明变量,会被视为单个变量.
考虑手动提升局部变量,从而避免混淆.
举例:
console.log(a);//undefined 不会出现Uncaught ReferenceError错误 // console.log(b);//打开的话会出现Uncaught ReferenceError: b is not defined c();//"c" 不会出现Uncaught ReferenceError错误 var a = "a"; function c(){ console.log("c"); }
因为上面的内容等效于
var a; function c(){ console.log("c"); } console.log(a); //console.log(b); c(); a = "a";
13 - 使用立即调用的函数表达式创建局部作用域
function wrapElements(a){
var result = [];
for(var i = 0, n = a.length; i < n; i++){
result[i] = function(){return a[i];};
}
return result;
}
var wrapped = wrapElements([10,20,30,40,50]);
var f = wrapped[0];
console.log(f());//undefined 此处为什么不是10而是undefined?
原因: 闭包是由函数与对其状态即词法环境(lexical environment)的引用共同构成.所以上面产生的每一个闭包都绑定了同一个词法环境! 即每个函数都为 return a[5],因为索引越界所以返回undefined
解决方法:
-
使用更多的闭包,让回调不再共享同一个词法环境
function wrapElements(a){ var result = []; for(var i = 0, n = a.length; i < n; i++){ result[i] = returnValue(a[i]);//此处多一层闭包,强制不共享同一个词法环境 } return result; } function returnValue(value){ return function(){return value;} } var wrapped = wrapElements([10,20,30,40,50]); var f = wrapped[0]; console.log(f());//10
-
创建一个嵌套函数并立即调用它来创建一个局部作用域
warnings:要当心函数中包裹代码块导致代码块的微妙变化.
- 代码块不能包含break; continue语句.是不合法的
- 代码块如果引用了this或特别的arguments变量,这种方式会改变他们的含义!
function wrapElements(a){ var result = []; for(var i = 0, n = a.length; i < n; i++){ (function(){ var j = i; result[i] = function(){return a[j];}; })(); //此处也可以是如下形式,将局部变量作为实参传入 //(function(j){ // result[i] = function(){return a[j];}; //})(i); } return result; } var wrapped = wrapElements([10,20,30,40,50]); var f = wrapped[0]; console.log(f());//10
-
使用ES6 let关键字,绑定块作用域
function wrapElements(a){ var result = []; //仅仅是此处声明i时使用let.因为let不会提升变量的声明.每个闭包都绑定了块作用域的变量 for(let i = 0, n = a.length; i < n; i++){ result[i] = function(){return a[i];}; } return result; } var wrapped = wrapElements([10,20,30,40,50]); var f = wrapped[0]; console.log(f());//10
14 - 当心命名函数表达式笨拙的作用域
缺陷1:
命名函数表达式,即 function xxx(){};
常用于调试.在对Error对象的栈跟踪功能中,函数表达式的名称通常作为其入口使用.但是命名函数表达式是作用域和兼容性问题臭名昭著的来源.
ES3中,JavaScript引擎被要求将命名函数表达式的作用域表示为一个对象,这个对象只有单个属性,将函数名和函数自身绑定起来,但是这个对象也继承了Object.prototype的属性.因此,就把Object.prototype中的所有属性引入了作用域.
var constructor = function(){return null;}; var f = function f(){ return constructor(); }; f();//{} (in ES3 environments) //因为命名函数表达式在其作用域内集成了Object的构造函数 这种情况删除函数表达式名即可 即 var f = function(){ return constructor(); } //存在:一些不符合标准的js环境中,即使删除了函数表达式名,调用f()依旧会返回一个空对象.
避免方式:
在任何时候都避免对Object.prototype中添加属性,以及使用与Object.prototype属性同名的局部变量.
缺陷2:
对命名函数表达式的声明进行提升.
//在一些不符合标准的环境中
var f = function g(){return 17;};
g();//17
//甚至这两个对象是两个不同的对象,导致了不必要的内存分配
解决办法:
创建一个与函数表达式同名的局部变量并赋值为null;//确保重复的函数会被垃圾回收.
var f = function g(){return 17;};
var g = null;
tips:
在Error对象和调试器中使用命名函数表达式改进栈跟踪.
在ES3和有问题的js环境中谨记函数表达式作用域会被Object.prototype污染.//ES5中已解决bug
谨记在错误百出的js环境中会提升命名函数表达式声明,导致重复存储
-
考虑避免使用命名函数表达式或在发布前删除函数名
=> 如果代码发布到正确实现的ES5环境,就不必担心上述问题.
15 - 当心局部块函数声明笨拙的作用域
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);//?
//在ES5及以前,不存在块级作用域,test内部的f()作用域是整个函数.
//因此,结果为["local","local"]和"["local"]
//在ES6中,存在块级作用域.
//如上代码在执行到result.push(f())时,会报f is not a function的错
//将内部修改为 let f = function(){return "local";};则返回["local","global"]和"["global"]
16 - 避免使用eval创建局部变量
如: eval("var y = 'hello'; ");这一句在被调用后才会执行,将y变量加入到作用域.
tips:
避免使用eval函数创建的变量污染调用者的作用域.
-
如果eval函数代码可能创建全局变量,可以将此调用封装到嵌套的函数中,以防止作用域污染.
var y = "global"; function test(src){ (function(){eval(src);})(); return y; } test("var y = 'local';"); //"global" test("var z = 'local';"); //"global"
(remain learning)17 - 间接调用eval函数优于直接调用
eval函数不仅仅是一个函数,还可以访问调用它时的整个作用域.
尽可能使用间接的方式调用eval函数.
两种调用eval的方式:
-
直接调用
var x = "global"; function test(){ var x = "local"; return eval("x");//direct eval } test(); //"local",此时调用时具有完全访问局部作用域的权限
-
间接调用
var x = "global"; function test(){ var x = "local"; var f = eval;//绑定eval函数到另一个变量名 return f("x");//通过变量名调用函数,会使代码失去对局部作用域访问的能力 } test();//"global"
(0,eval)(src); //(,)表达式序列运算符总会返回最后一项,所以前面无论是0还是其他字面量都没关系,都会返回eval,整个表达式被视为间接调用eval函数 //直接使用evel(src);左侧被视为一个引用 //(0,eval)(src);左侧被视为一个值
使用函数
18 - 理解函数调用、方法调用及构造函数调用之间的不同
在javaScript中,他们只是单个构造对象的三种不同的使用模式
函数调用
function hello(username){ return "hello, " + username; } hello("keyser Soze");//"hello, keyser Soze"
方法调用
var obj = { hello: function(){ return "hello, " + this.username; }, username: "hans Gruber" }; obj.hello();//"hello, hans Gruber"//方法调用需要对象来调用,直接调用就时函数调用了 //注意以下情况 var obj2 = { hello: obj.hello, username: "Boo Radley" }; obj2.hello();//"hello, Boo Radley" //方法调用中,由调用表达式自身来确定this变量的绑定. 调用表达式为obj2.hello()所以this指向obj2,而不是obj //此处先略过call,bind,apply
构造函数调用
function User(name, password){ this.name = name; this.password = password; } var u = new User("sfalken","xxx");//构造函数需要通过new运算符调用 u.name;//"sfalken" //构造函数调用一个全新的对象作为this变量的值,并隐式地返回这个新对象作为调用的结果,即 产生的新对象传递给了u.构造函数的主要职责时初始化该新对象.
19 - 熟练掌握高阶函数
高阶函数: 将函数作为参数或返回值的函数.
学会发现可以被高阶函数取代的常见编码模式.
举例:
[3,1,4,1,5,9].sort(function(x, y){ if(x < y)return -1; if(x > y)return 1; return 0; });//1,1,3,4,5,9 //此处定义了一个判断数字大小的方法作为参数传递
20 - 使用call方法自定义接收者来调用方法
func.call(thisArg [, arg1[,arg2...]])
第一个参数显式地提供了接收者对象.
tips:
使用call方法自定义接收者来调用函数
使用call方法可以调用在给定的对象中不存在的方法
-
使用call方法定义高阶函数允许使用者给回调函数指定接收者
举例:
var table = { entries: [], addEntry: function(key, value){ this.entries.push({key:key, value: value}); }, forEach: function(f, thisArg){ var entries = this.entries; for(var i = 0, n = entries.length; i < n; i++){ var entry = entries[i]; //回调函数f,并指定接收者为thisArg f.call(thisArg, entry.key, entry.value, i); } } } //如table2赋值table1的内容 table1.forEach(table2.addEntry, table2); //如果call的第一个参数为null或undefined,则内部this指向window
call, apply, bind
call和apply的区别是参数提供的方式
bind与call,apply的区别是,bind只绑定,而call和apply会立即调用
21- 使用apply 方法通过不同数量的参数调用函数
func.apply(thisArg, [argsArray])
apply可以处理任意数量的参数
apply与call十分类似,区别就是参数为参数数组传入,而call从第二个参数开始有0至多个参数
22 - 使用arguments创建可变参数的函数
JavaScript给给个函数都隐式提供了一个名为arguments的局部变量.可以用索引获取方法的每个实参(arguments对象并不是一个数组),并且该对象还有一个length属性来指示参数的个数.
tips:
- 使用隐式的arguments对象实现可变参数的函数
- 考虑对可变参数的函数提供一个额外的固定元数的版本,从而使使用者无需借助apply方法
23 - 永远不要修改arguments对象
function callMethod(obj, method){
var shift = [].shift;
shift.call(arguments);
shift.call(arguments);
return obj[method].apply(obj, arguments);
}
var obj = {
add: function(x, y){return x + y;}
};
callMethod(obj, "add", 17, 25);
//error: cannot read property "apply" of undefined
//arguments对象并不是函数参数的副本.所有命名参数都是arguments对象中对应索引的别名
//=> 参数obj依旧是arguments[0]的别名,method依旧是arguments[1]的别名
//因此此处调用的不是obj['add']而是17['25']
//=>arguments对象和函数的命名参数之间的关系非常脆弱.所以不建议修改arguments对象
//对比严格模式下
function strict(x){
"use strict";
arguments[0] = "modified";
console.log(arguments[0],x);//modified unmodified
return x === arguments[0];
}
function nostrict(x){
arguments[0] = "modified";
console.log(arguments[0],x);//modified modified
return x === arguments[0];
}
console.log(strict("unmodified"));//false
console.log(nostrict("unmodified"));//true
//非严格模式下,arguments和函数命名参数都修改了,而严格模式下,命名参数没被修改
//严格模式下,不支持对其arguments对象取别名,即 此处x不是arguments[0].所以严格模式下返回的结果为false
tips:
使用[].slice.call(arguments)将arguments复制到一个真正的数组中再进行修改.
24 - 使用变量保存arguments的引用
//实现一个迭代器
function values(){
var i = 0, n = arguments.length;
return {
hasNext: function(){
return i < n;
},
next: function(){
if(i >= n ){
return;
}
return arguments[i++];
}
}
}
var it = values(1,4,1,4,2,1,3,5,6);
//console.log(it);
console.log(it.next());//undefined
console.log(it.next());//undefined
console.log(it.next());//undefined
//原因是一个新的arguments变量被隐式地绑定到每个函数体内
//it对象的next方法有自己的arguments对象,且为null,打印时就成了undefined
//解决方案:
//在我们需要的arguments对象的作用域中,使用一个变量去绑定,引用这个变量即可
//?感觉可以理解为闭包?访问作用域内的参数,因为it的方法中有本地的arguments对象了,所以会优先使用本地的arguments,而此处添加一个引用,在it的方法中不再拥有,就不会出问题了.
function values(){
+ var i = 0, n = arguments.length, a = arguments;
- var i = 0, n = arguments.length;
return {
hasNext: function(){
return i < n;
},
next: function(){
if(i >= n ){
return;
}
+ return a[i++];
- return arguments[i++];
}
}
}
var it = values(1,4,1,4,2,1,3,5,6);
//console.log(it);
console.log(it.next());//undefined
console.log(it.next());//undefined
console.log(it.next());//undefined
tips:
- 当引用arguments时当心函数嵌套层级
- 绑定一个明确作用域的引用到arguments变量,从而可以在嵌套的函数中引用他
25 - 使用bind方法提取具有确定接收者的方法
var buffer = {
entries:[],
add: function(s){
console.log(arguments);//Arguments(3) ["867", 0, Array(3), callee: ƒ, Symbol(Symbol.iterator): ƒ]
this.entries.push(s);
},
concat:function(){
return this.entries.join("")
}
};
var source = ["867","-","5309"];
source.forEach(buffer.add);// Cannot read property 'push' of undefined
console.log(buffer.concat());
//原因:此处buffer.add的接收者不是buffer对象
//forEach方法的实现使用全局对象作为默认的接收者,所以此处传递给了全局对象,全局对象没有entries,所以报错
//arr.forEach(callback(currentValue [, index [, array]])[, thisArg]);
//thisArg为null或者undefined时,this都指向全局对象
//解决方案:
//1.forEach提供了绑定接收者的参数
source.forEach(buffer.add, buffer);
//2.bind方法绑定this到buffer上
source.forEach(buffer.add.bind(buffer));
26 - 使用bind方法实现函数柯里化
函数柯里化: 创建一个固定需求参数子集的委托函数,即只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数(11 - 数量掌握闭包里有)
function simpleURL(protocol, domain, path){
return protocol + "://" + domain + "/" + path;
}
var urls = paths.map(function(path){
return simpleURL("http", siteDomain, path);
});
等效于
var urls = paths.map(simpleURL.bind(null, "http", siteDomain));
//因为不需要引用this变量,所以bind的第一个参数可以是任何值,使用null或undefined为习惯用法
27 - 使用闭包而不是字符串来封装代码(?现在ES6有块级作用域了,感觉答案更显而易见了?)
//这里说的字符串来封装代码使用的是eval(str)的方法来执行
不使用字符串的原因:
- eval内的代码要执行到这一行才会生效,使用的参数可能跟想象中的有差异.
- 高性能引擎很难优化eval字符串里面的代码
tips:
- 当将字符串传递给eval函数以执行他们的API时,绝不要在字符串内包含局部变量的引用
- 接受函数调用的API优于使用eval函数执行字符串的API
28 - 不要信赖函数对象的toString方法
//此处指的是用toString试图获取函数的源代码
tips:
- 当调用函数的toString方法时,并没有要求js引擎能够精确地获取到函数的源代码
- 由于在不同引擎下调用的toString方法的结果可能不同,所以绝不要信赖函数toString获得的源代码的详细细节.
- toString方法的执行结果并不会暴露存储在闭包中的局部变量值.
- 通常情况下,避免使用函数对象的toString方法
(remain learning)29 - 避免使用非标准的栈检查属性
许多javaScript环境提供检查调用栈的功能.每个arguments对象都含有两个额外的属性,arguments.callee(除了允许匿名函数递归地引用自身之外,没有用途)和arguments.caller(这个属性不可靠,大多数环境已移除),前者指向使用该arguments对象被调用的函数,后者指向调用该arguments对象的函数.
tips:
- 避免使用非标准的arguments.caller和arguments.callee属性,因为他们不具有良好的移植性.
- 避免使用非标准的函数对象caller属性,因为在包含全部栈信息方面,是不可靠的.
- 最好的策略是使用交互式的调试器.
对象和原型
30 - 理解prototype, getPrototypeOf和_proto_之间的不同
User.prototype指向由new User()创建的对象的原型
Object.getPrototypeOf(user)是ES5中用来获得user对象的原型对象的标准方法
user._proto_是获取user对象的原型对象的非标准方法
User构造函数有一个默认的prototype属性指向原型对象.
由new User()创建的user对象继承User原型对象.当查找user对象属性时,如果user没有,就会到他继承的User原型对象中去找.
ES5提供了Object.getPrototypeOf()的方法去获取对象的原型对象
Object.getPrototypeOf(user) === User.prototype; //true
一些环境提供了非标准的方法获取对象的原型
user.__proto__ === User.prototype; //true
31 - 使用Object.getPrototypeOf函数,而不要使用_proto_属性
32 - 始终不要修改_proto_属性
原因:
- _proto_属性很特殊,他提供了Object.getPrototypeOf方法不具备的修改对象原型链接的能力.因为并不是所有平台都支持改变对象原型的特性,所以有可移植性的问题
- 性能原因.因为现代的js引擎都深度优化了获取和设置对象属性的行为,更改对象的内部结构(如添加或删除该对象或其原型链中对象的属性),将会使一些优化失效.修改_proto_属性实际上改变了继承结构本身,可能是最具破坏性的修改.会导致更多的优化失效.
- 为了保持行为的可预测性.修改对象的原型链会交换对象的整个继承层次结构.
tips:
- 使用Object.create函数给新对象设置自定义的原型.
33 - 使构造函数与new操作符无关(ES6引入了class,这部分内容应该过时了)
//调用构造函数时,忘记使用new关键字
//这样函数的接收者将是全局对象
//会灾难性地创建全局变量,如果已存在则会修改其值
var u = User("name", "password");
function User(name, password){
//"use strict";
//如果使用严格模式, 则this.name = name;这一行会报错
this.name = name;
this.password = password;
}
//=>可以修改为以下方式
function User(name, password){
var self = this instanceof User ? this : Object.create(User.prototype);
self.name = name;
self.password = password;
return self;
}
34 - 在原型中存储方法
好处:
将方法存储在原型中,使其可以被所有的实例使用,减少了占用的内存.现代的js引擎深度优化了原型查找,所以相对于把方法冗余地写在实例对象中,并不一定能保证在查找速度上有明显的提升.
35 - 使用闭包存储私有数据
闭包时一种简朴的数据结构.他们将数据存储到封闭的变量中而不提供对这些变量的直接访问.获取闭包内部结构的唯一方式是该函数显式地提供获取它的途径.
//该实现以变量的方式来引用name和passwordHash变量,而不是this属性的方式
//此时User的实例中不会包含任何实例属性,因此外部的代码不能直接访问User的实例的name和passwordHash变量
function User(name, passwordHash){
this.toString = function(){return `[User ${name}]`;}
this.checkPassword = function(password){
return hash(password) === passwordHash;
}
}
//缺点:
//为了使构造函数中的变量在使用他们的方法的作用域内,那些方法必须置于实例对象中
//即不能用继承的方式去减少占用内存了.因为对象内部根本没有name和passwordHash这两个属性了.仅实例中定义的方法能访问这两个变量
tips:
- 闭包的变量是私有的,只能通过局部的引用获取
- 将局部变量作为私有属性从而通过方法实现信息隐藏.
36 - 只将实例状态存储在实例对象中
有状态的数据可以存储在原型中,只要你真的想要共享它.
一般情况下,原型对象中共享方法,而每个实例的状态存储在各自的实例对象中.
37 - 认识到this变量的隐式绑定问题
tips:
- this变量的作用域总是由其最近的封闭函数所确定.
- 使用一个局部变量(通常为self,me或that)绑定this,然后在内部函数中使用.
38 - 在子类的构造函数中调用父类的构造函数(ES6引入了class,这部分内容应该过时了)
tips:
- 在子类的构造函数中,显示传入this作为显示的接收者调用父类的构造函数
- 使用Object.create函数来构造子类的原型对象以避免调用父类的构造函数
//Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
Son.prototype = Object.create(Father.prototype);
39 - 不要重用父类的属性名
40 - 避免继承标准类
ECMAScript标准库内的类定义了很多特殊的行为,很难写出行为正确的子类.
ECMAScript定义的完整的[[Class]]属性值集合:
[[Class]] | Construction |
---|---|
"Array" | new Array(…), [...] |
"Boolean" | new Boolean(...) |
"Date" | new Date(...) |
"Error" | new Error(...), new EvalError(...), new RangeError(...), new ReferenceError(...), new SyntaxError(...), new TypeError(...), new URIError(...) |
"Function" | new Function(...), function (...){...} |
"JSON" | JSON |
"Math" | Math |
"Number" | new Number(...) |
"Object" | new Object(...), {...}, new MyClass(...) |
"RegExp" | new RegExp(...), /.../ |
"String" | new String(...) |
通过右侧的构造函数创建的才能有[[Class]]这个内部属性.
因为[[Class]]属性及其他特殊的内部属性导致继承这些类总会有问题,因此,最好避免继承: Array, Boolean, Date, Function, Number, RegExp或 String.
Math对象所有属性和方法都是静态的,可对其作拓展.
JSON对象只有parse()和stringify()两个方法,除了这两个方法外,对象本身并无其他作用,也不能被调用或作为构造函数调用.
Error, Object常被继承.
tips:
- 使用属性委托优于继承标准类.
41 - 将原型视为实现细节
因为对一个对象的操作,并不需要在意属性或方法是在原型继承结构的那个位置,修改原型上的方法/属性,将会影响依赖这个原型的对象.
tips:
- 对象是接口,原型是实现(即原型实现了实际的功能, 我们调用继承这个原型的对象,这个对象就相当于一个接口)
- 避免检查你无法控制的对象的原型结构
- 避免检查实现在你无法控制的对象内部的属性
42 - 避免使用轻率的猴子补丁
猴子补丁(monkey-patching): 对象共享原型,所以每一个对象都可以增加、删除或修改原型的属性,这个操作被称为猴子补丁.
使用场景:polyfill.即在某些js环境中,使用旧的方式实现新的API.
举例:
//先判断是否有这个方法
if(typeof Array.prototype.map !== "function"){
//没有这个方法的话,为之定义
Array.prototype.map = function(f, thisArg){
var result = [];
for(var i = 0, n = this.length; i < n; i++){
result[i] = f.call(thisArg, this[i], i);
}
return result;
};
}
tips:
避免轻率地使用猴子补丁.
记录程序库所执行的所有猴子补丁.
-
考虑通过将修改置于一个导出函数中,使猴子补丁称为可选(即可以选择执行这个方法,也可选择执行其他的,而不是直接打在原型上)
如:
//置于函数中,用户可以选择调用该函数或忽略 //如果忽略,Array原型上就不具有该方法,调用,则具有 //这样就不会有多处定义导致冲突的问题 function addArrayMethods(){ Array.prototype.split = function(i){ return [this.slice(0,i), this.slice(i)]; }; };
使用猴子补丁为缺失的API提供polyfills.
数组和字典
43 - 使用Object的直接实例构造轻量级的字典
轻量级字典的首要原则: 使用Object的直接实例作为字典,而不是其子类或者数组.这样不容易遇到原型污染的问题(因为原型污染的范围缩小到仅有Object.prototype).并且我们本身也建议不要直接添加属性到Object.prototype中.
使用for...in循环去枚举字典的属性.
44 - 使用null原型以防止原型污染
防止原型污染最简单的方式之一就是一开始就不使用原型.
ES5:
var o = Object.create(null);
Object.getPrototypeOf(o) === null;//true
存在的问题:
-
在不同环境中,还是会有一些问题.
理想中,空对象是不存在_proto_属性的. in和hasOwnProperty都应该返回false
而在一些环境中(只有in操作符为true)
"xxx" in o //true
{}.hasOwnProperty.call(o, "_proto_") //false
而在一些环境中,会存在一个实例属性_proto_
"xxx" in o //true
{}.hasOwnProperty.call(o, "_proto_") //true
ES5前不支持Object.create函数的环境:
var x = {__proto__: null};
x instanceof Object;//false
存在的问题:
- _proto_既不标准,也不是完全可移植的,并且在未来可能被移除
- 如果使用_proto_作为属性名,可能会导致一些问题,因为一些环境中把他作为特殊的属性对待.
=>推荐45中的方式定义字典.
45 - 使用hasOwnProperty方法避免原型污染
一种避免原型污染的字典的有效实践
//为了达到最大的可移植性和安全性,对__proto__属性做了特殊处理
function Dict(elements){
this.elements = elements || {};
//是否设置了__proto__属性的标识符
this.hasSpecialProto = false;
//用于存储设置的__proto__的值
this.specialProto = undefined;
}
Dict.prototype.has = function(key){
if(key === "__proto__"){
return this.hasSpecialProto;
}
//使用{}.hasOwnProperty的方式,避免原型污染
//普通的类,很可能因为原型定义了hasOwnProperty的方法导致原型污染
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, val){
if(key === "__proto__"){
this.hasSpecialProto = true;
this.specialProto = val;
}else{
this.element[key] = val;
}
}
Dict.prototype.remove = function(key){
if(key === "__proto__"){
this.hasSpecialProto = false;
this.specialProto = undefined;
}else{
delete this.elements[key];
}
}
46 - 使用数组而不要使用字典来存储 有序 集合
原因: JavaScript对象是一个无序的属性集合,获得和设置不同的属性与顺序无关,与js引擎有关.
tips:
- 使用for...in循环来枚举对象的属性应当与顺序无关
- 如果聚集运算字典中的数据,确保聚集操作与顺序无关(其实也就是不要用字典来存储有序的数据)
- 使用数组而不是字典来存储有序集合
47 - 绝不要在Object.prototype中增加可枚举的属性
原因:在Object.prototype中添加可枚举的属性,会污染所有字典的枚举遍历.所以避免在Object.prototype中增加属性
折中方式:
-
考虑编写一个函数代替Object.prototype
对象中的函数就叫做方法,通过"."的方式调用
-
ES5后提供的方法,定义不可枚举的属性
Object.defineProperty(Object.prototype, "allKeys", { value: function(){ var result = []; for(var key in this){ result.push(key); } return result; }, writable: true, enumerable: false,//这一项就是定义allKeys是不可枚举的,for...in时不会出现它 configurable: true })
48 - 避免在枚举期间修改对象
原因:for...in遍历枚举的期间,添加新的属性,不能保证在枚举期间能访问到新添加的属性.
tips:
- 使用for...in循环枚举一个对象的属性时,确保不要修改该对象.
- 迭代一个对象时,如果对象的内容在循环期间可能会改变,应该用while循环或经典for循环来代替for...in 循环.
- 为了在不断变化的数据结构中能够预测枚举,考虑使用一个有序的数据结构,如数组,而不是字典对象.
49 - 数组迭代要优先使用for循环而不是for...in循环
var scores = [98,74,85,77,93,100,89];
var total = 0;
for(var score in scores){
total += score;
}
var mean = total / scores.length;//此时的total其实是00123456
console.log(mean);//17636.571428571428而不是88
//正确的操作方式
//且在一开始就计算出数组的长度,避免写在中间,那样每次迭代都会计算一次数组长度
for(var i = 0, n = scores.length; i < n; i++){
total += scores[i];
}
50 - 迭代方法优于循环
原因:常见的终止条件的错误.
循环相对于迭代函数的唯一优势: 有控制流操作,如break和continue.
如下:
for(var i = 0; i <= n; i++){...}//尾部多了一次循环
for(var i = 1; i < n; i++){...}//头部少了第一次循环
for(var i = n; i >= 0; i--){...}//多了头部的一次循环
for(var i = n - 1; i > 0; i--){...}//少了最后一次循环
常用模式1:Array.prototype.forEach
代码简单可读并且消除了终止条件和任何数组索引
for(var i = 0, n = players.length; i < n; i++){
players[i].score++;
}
//等效于
players.forEach(function(p){
p.score++;
})
常用模式2: Array.prototype.map
对数组每个元素进行一些操作后建立一个新的数组
var trimmed = [];
for(var i = 0, n = input.length; i < n; i ++){
trimmed.push(input[i].trim());
}
//等效于
var trimmed = input.map(function(s){
return s.trim();
})
常用模式3:Array.prototype.filter
计算一个新的数组,数组中的元素为现有数组中的一部分
var newArr = listings.filter(function(listing){
return listing.price >= min && listing.price <= max;//return true的值会被留下,返回false的就不存在返回的新数组中了
})
可以提前终止循环的模式:
Array.prototype.some
返回一个布尔值表示其回调函数对数组的任何一个元素是否返回了真值
一旦产生真值就返回
[1,10,100].some(function(x){return x > 5;});//true
[1,10,100].some(function(x){return x < 0;});//false
Array.prototype.every
返回一个布尔值表示其回调函数是否对所有元素返回了一个真值
一旦产生假值就返回
[1,2,3,4,5].every(function(x){return x > 0;}); //true
[1,2,3,4,5].every(function(x){return x < 3;}); //false
tips:
- 使用迭代方法替换for循环增加代码可读性,避免思考重复循环的逻辑控制(即边界条件)
- 使用自定义迭代函数来抽象常见循环模式(注意猴子补丁的注意事项,见42)
- 在存在需要提前终止的情况下,仍推荐传统的循环方式,避免不必要的循环.另外,some和every也可用于提前退出.
51 - 在类数组对象上复用通用的数组方法
类数组对象的规则:
- 具有一个范围在0~2^32-1的整型length属性
- length属性大于该对象的最大索引.索引是一个范围在0~2^32-2的整数,它的字符串表示的是该对象中的一个key
满足上述两个条件的类数组对象使用Array.prototype任一方法都可兼容(除了concat方法必须是数组对象才行).
使用方式:
[].(forEach/some/...).call(类数组对象, ...);
常见类数组
- 函数的arguments对象
- DOM的NodeList类,如:document.getElementsByTagName,document.querySelectorAll获得的对象
- 自定义的简单对象字面量 如:var arrayLike = {0: "a",1:"b",2:"c",length:3};
- 字符串
转换类数组为真正的数组对象的方法
- [].slice.call(类数组对象)
- Array.from(类数组对象/可迭代对象Set,Map)
52 - 数组字面量优于数组构造函数
var a = [1,2,3,4,5];
//优于
var a = new Array(1,2,3,4,5);
好处:
- 使用构造函数的方式需要先确认Array没被包装过
- 使用构造函数的方式需要确认没人修改过全局的Array变量
//以上两点应该不太可能发生...
-
使用单个数组时,有明显不同
var a = [17];和var a = new Array(17);
后者会创建一个17长度的数组!
库和API设计
53 - 保持一致的约定
tips:
- 在变量命名和函数签名中使用一致的约定.如width,height等
- 不要偏移用户在他们开发平台中很可能遇到的约定.如width,height的顺序等等
54 - 将undefined看做"没有值"(缺少某个特定的值)
出现undefined的情况
- 未赋值的变量初始值为undefined
- 访问对象中不存在的属性,会产生undefined
- 函数体尾使用未带参数的return ;或者未使用return都会产生返回值undefined
- 未给函数提供实参则该函数参数值为undefined
可选参数的实现的常见做法(不要检查arguments.length,下面两种方式更健壮)
-
测试undefined
function Server(port, hostname){ if(hostname === undefined){ hostname = "localhost"; } hostname = String(hostname); // ... }
-
测试是否为真
function Server(Port, hostname){ hostname = String(hostname || "localhost"); // ... }
逻辑运算符或(||),当第一个参数为真时返回第一个参数,否则返回第二个参数
不适合使用"测试是否为真"的情况是:
如果除了undefined的其他假值(false, 0, -0, null, NaN, "")可以为函数的合法值时,就不能使用这种方式.需要选用上一种方式
55 - 接收关键字参数的选项对象
tips:
-
使用选项对象使得API更具可读性、更容易记忆;且参数都是可选的,调用者可提供任一可选参数的子集.
- 原因: 当参数过多时,位置参数的方式可读性很差
-
所有通过选项对象提供的参数应当被视为可选的
function Alert(parent, message, opts){//必选的parent和message需要抽离出来 opts = opts || {};//如果不传入,初始化一个空对象避免后面的操作报错 this.width = opts.width == undefined ? 320: opts.width;//数字参数 0一般是合法值,所以要用undefined判断 this.height = opts.height == undefined ? 240: opts.height; this.title = opts.title || "Alert";//字符串一般空串不会算作合法值,所以这里用或操作符,如果此处空串也为合法值,那应该改为测试undefined this.modal = !!opts.modal;//布尔值,使用双重否定将参数强制转换成一个布尔值 }
-
如果使用其他库,可以使用extend函数抽象出从选项对象中提取值的逻辑(前提是库/框架提供了这个函数)
//一个典型的extend实现 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函数简化 function Alert(parent, message, opts){ opts = extend({ width:320, height:240 }); opts = extend({ title: "Alert", modal: false }, opts); this.width = opts.width; this.height = opts.height; this.title = opts.title; this.modal = opts.modal; //如果options是整个复制到this对象,可以进一步简化上面四行 extend(this, opts); }
56 - 避免不必要的状态(状态有时候是必须的)
区别:无状态函数或方法的行为只取决于输入,与程序的状态改变无关
好处: 更容易学习和使用,更清晰(简洁),更不易出错; 相比于有状态的API,无状态的API会自动重用默认值,我们就不用担心默认值是否在前面某处被更改了.
tips:
- 尽可能地使用无状态的API
- 如果API是有状态的,需要标示出每个操作与哪些状态有关联
57 - 使用结构类型structural typing(鸭子类型duck typing)设计灵活的接口
区别于一般面向对象语言推荐的使用类和继承来结构化程序, javaScript动态类型使用的方式更灵活.只要有预期的结构即可.
Wiki.formats.MEDIAWIKI = function(source){
//extract contents from source
//...
return {
getTitle: function(){/* ... */},
getAuthor: function(){/* ... */},
toHTML: function(){/* ... */}
}
}
我们完全可以根据每种格式的需要,混合和匹配功能
继承有时候导致比他解决的问题外更多的问题.
静态语言迫使必须符合某个数据类型,增加了更多的代码;而动态语言可以编写更少的代码,使代码更简洁,可以花更多的精力在业务逻辑上.缺点是无法保证变量类型,可能在运行时出错.
鸭子类型是典型的面向接口编程,且不必借助超类的帮助.它只要正确的实现了我们需要的方法供我们调用即可.
tips:
- 使用结构类型来设计灵活的对象接口
- 结构接口更灵活,更轻量,所以应该避免使用继承
- 针对单元测试,使用mock对象即接口的替代实现来提供可复验的行为.
58 - 区分数组对象和类数组对象
tips:
- 绝不重载与其他类型有重叠的结构类型
- 当重载一个结构类型与其他类型时,先测试其他类型.因为结构类型没有明确的信息标记他们实现的结构类型,没有可靠的编程方法来检测该信息.
- 当重载其他对象类型时,接受真数组而不是类数组对象
- 文档标注你的API是否接受真数组或类数组值
- 使用ES5提供的Array.isArray方法测试真数组(比instance of操作符更可靠)
59 - 避免过度的强制转换
tips:
- 避免强制转换和重载混用
- 考虑防御性地监视非预期的输入
- 防御性编程:试图以额外的检查来抵御潜在的错误
60 - 支持方法链(就是函数式编程)
tips:
- 使用方法链来连接无状态的操作.
- 通过在无状态的方法中返回新对象来支持方法链
- 通过在有状态的方法中返回this来支持方法链
并发
61 - 不要阻塞I/O事件队列
系统维护了一个按事件发生顺序排列的内部事件队列,一次调用一个已注册的回调函数,在执行过程中,系统会适时地查看处理进度,会在异步执行结束后立刻调用回调函数.
好处:系统的这种运行方式有时被称为运行到完成机制担保(run-to-completion),当代码运行时,你完全可以掌握应用程序的状态,不用担心一些变量和对象的属性由于并发执行代码而超出你的控制.
不足:实际上这些异步执行的代码支撑着后续应用程序的执行(返回某些重要的参数,又会作为后面代码的实参等等).在客户端,一个阻塞的事件处理程序会阻塞任何将被处理的其他用户输入,甚至阻塞页面渲染;在服务器端,一个阻塞的事件处理程序可能会阻塞其他将被处理的网络请求,导致服务器失去响应.
tips:
- 异步API使用回调函数来延缓处理代价高昂的操作以避免阻塞主应用程序.
- JavaScript并发地接收事件,但会使用一个事件队列按序地处理事件.
- 在应用程序事件队列中绝不要使用阻塞的I/O.
62 - 在异步序列中使用嵌套或命名的回调函数
为了保证存在依赖的异步代码的执行顺序,使用嵌套或命名的回调函数.
tips:
使用嵌套或命名的回调函数按顺序地执行多个异步操作.
尝试在过多的嵌套的回调函数和尴尬的命名的非嵌套回调函数中取得平衡(后面会介绍解决该问题的ES6新特性Promise)
避免将可被并行执行的操作顺序化.
63 - 当心丢弃错误
常见的异步处理错误:
//1.定义一个附加的错误处理回调函数(errbacks)
downloadAsync("http://example.com/file.txt",function(text){
console.log(text);
}, function(error){
console.log("Error:" + error);
})
//2.异步操作出错则回调函数返回的第一个参数为真,没出错则为一个假值
//常用在Node.js平台
function onError(error){
console.log("Error:" + error);
}
downloadAsync("a.txt", function(error, a){
if(error) return onError(error);
downloadAsync("b.txt", function(error, b){
if(error) return onError(error);
console.log("contents:" + a + b);
})
})
tips:
- 通过编写共享的错误处理函数来避免赋值和粘贴错误处理代码
- 确保明确地处理所有的错误条件以避免丢失错误.
64 - 对异步循环使用递归
tips:
- 循环不能是异步的.
- 使用递归函数在事件循环的单独轮次中执行迭代.
- 在事件循环的单独轮次中执行递归,并不会导致调用栈溢出.
65 - 不要在计算时阻塞事件队列
tips:
- 避免在主事件队列中执行代价高昂的算法. (如:使用setTimeout的方式变为异步执行)
- 在支持Worker API 的平台, 该API可以用在一个独立的事件队列中运行长计算程序.
- 在Worker API不可用或代价昂贵的环境中, 考虑将计算程序分解到事件循环的多个轮次中.
66 - 使用计数器来执行并行操作
tips:
- 并行操作的结果顺序是不可预测的,这个时候我们可以使用计算器去确定代码的执行进度.
67 - 绝不要同步地调用异步的回调函数
举例:
//调用方代码
downloadCachingAsync("file.txt", function(file){
console.log("finished");//might happen first
})
console.log("starting");
//被调用方,定义的函数
function downloadCachingAsync(url, onsuccess, onerror){
if(cache.has(url)){//模拟有缓存的情况,这种情况代码走到了同步回调中
onsuccess(cache.get(url));//此处同步调用了回调!!!
return;
//正确的做法
//return setTimeout(onsuccess(cache.get(url),0));
}
return downloadAsync(url, function(file){
cache.set(url, file);
onsuccess(file);
}, onerror);
}
存在的问题:
- 如上述代码, 同步的调用异步的回调函数,导致日志消息竟然出现了错误的顺序("finish"先于"starting")
- 如64条所述, 异步回调本质上以空的调用栈来调用, 因此异步的循环实现为递归函数时安全的.但是如果同步地调用不能保障这一点,会使得表面上是异步的循环耗尽栈空间.
tips:
- 即使可以立即得到数据,也绝不要同步地调用异步回调函数
- 同步地调用异步回调函数扰乱了操作的序列,可能导致意想不到的交错代码
- 同步地调用异步的回调可能导致栈溢出或错误地处理异常
- 使用异步的API.如setTimeout函数异步回调函数
68 - 使用promise模式清洁异步逻辑
基于promise的API不接收回调函数作为参数,而是返回一个promise对象,通过自身的then方法接收回调函数.
tips:
- promise代表最终值,即并行操作完成时最终产生的结果.
- 使用promise组合不同的并行操作
- 使用promise模式的API避免数据竞争
- 在要求有意的竞争条件时使用select(也被称为choose)