Part I 类型和语法
类型
类型和值
JavaScript 有七种内置类型:
-
空值(null)
-
未定义(undefined)
-
布尔值( boolean)
-
数字(number)
-
字符串(string)
-
对象(object)
-
符号(symbol,ES6 中新增)
注意:typeof null === “object”; //true 这是一个bug
已在作用域中声明但还没有赋值的变量,是 undefined 的。相反,还没有在作用域中声明过的变量,是 undeclared 的。
然而,JavaScript 却将它们混为一谈,当试图访问 “undeclared” 变量时这样报错:ReferenceError: a is not defifined,并且 typeof 对 undefifined 和 undeclared 变量都返回"undefined"。
数组
-
在创建“稀疏”数组时,数组中的空白单元的值为“undefined”,且与显式赋值为"undefined"还有区别
-
数组也是对象,也可以包含字符串键值和属性,也可以通过"."取值和赋值
-
需要注意,如果字符串键值能够被强制类型转换为十进制数字的话,它就会被当作数字索引来处理。
var a = [ ]; a["13"] = 42; a.length; // 14
因此要避免这种情况,建议使用对象来存放键值 / 属性值,用数组来存放数字索引值
-
将类数组(一组通过数字索引的值)转换为真正的数组,可以通过slice(…)来完成。
function foo() { var arr = Array.prototype.slice.call( arguments ); arr.push( "bam" ); console.log( arr ); } foo( "bar", "baz" ); // ["bar","baz","bam"]
字符串
-
JavaScript 中字符串是不可变的,而数组是可变的。
字符串不可变是指字符串的成员函数不会改变其原始值,而是创建并返回一个新的字符串。而数组的成员函数都是在其原始值上进行操作。
-
字符串可以借用数组的非变更方法来处理字符串
a.join; // undefined a.map; // undefined var c = Array.prototype.join.call( a, "-" ); var d = Array.prototype.map.call( a, function(v){ return v.toUpperCase() + "."; } ).join( "" ); c; // "f-o-o" d; // "F.O.O."
-
字符串反转问题
不能借用数组的reverse(…)方法,因为这是个可变更成员函数而字符串不可变
可以先将字符串转换为数组,待处理完后再将结果转换回字符串
var a = "foo"; var c = a.split("").reverse().join(""); console.log(c);
数字
-
ES6中支持的进制新格式:0x(十六进制),0o(八进制),0b(二进制)
-
tofixed(…) 方法可指定小数部分的显示位数,toPrecision(…) 方法用来指定有效数位的显示位数
-
ES6之后,可以使用 Number.EPSILON 来比较两个数字是否相等(在指定的误差范围内)
function numbersCloseEnoughToEqual(n1, n2) { return Math.abs(n1 - n2) < Number.EPSILON; } var a = 0.1 + 0.2; var b = 0.3; console.log(numbersCloseEnoughToEqual(a, b));//true
-
要检测一个值是否是整数,可以使用 ES6 中的 Number.isInteger(…) 方法
Number.isInteger( 42 ); // true Number.isInteger( 42.000 ); // true Number.isInteger( 42.3 ); // false
-
计算结果一旦溢出为无穷数(infifinity)就无法再得到有穷数
-
JavaScript中还有个负零(-0)
加法和减法运算不会得到负零,乘除法可以得到
var a = 0 / -3; // -0 var b = 0 * -3; // -0
对负零进行字符串化会返回 “0”,如果反过来将其从字符串转换为数字,得到的结果是准确的"-0"
-0的用途是可以通过它的符号位来保存方向信息。
值和引用
-
简单值(即标量基本类型值,scalar primitive)总是通过值复制的方式来赋值 / 传递,包括null、undefined、字符串、数字、布尔和 ES6 中的 symbol。
var a = 2; //a是2的一个副本 var b = a; // b是a的值的一个副本 b++; a; // 2 b; // 3
-
复合值(compound value)——对象(包括数组和封装对象,参见第 3 章)和函数,则总是通过引用的方式来赋值 / 传递。
var c = [1,2,3]; //c是[1,2,3]的一个引用 var d = c; // d是[1,2,3]的一个引用 d.push( 4 ); c; // [1,2,3,4] d; // [1,2,3,4] d = [5,6,7,8]; c; //[1,2,3,4] 一个引用无法更改另一个引用的指向
原生函数
封装对象
-
new String(“abc”) 创建的是字符串 “abc” 的封装对象,而非基本类型值 “abc”。
-
封装对象的typeof都是object,且一般不推荐直接使用
var a = "abc"; var b = new String( a ); var c = Object( a ); typeof a; // "string" typeof b; // "object" typeof c; // "object"
-
要想“拆封”,即想要得到封装对象中的基本类型值,可以使用valueOf()函数
原生函数作为构造函数
- 结论:永远不要创建和使用空单元数组
除非万不得已,否则尽量不要使用 Object(…)/Function(…)/RegExp(…)
- RegExp(…)有点用:动态定义正则表达式
var name = "Kyle";
var namePattern = new RegExp( "\\b(?:" + name + ")+\\b", "ig" );
var matches = someText.match( namePattern );
-
Date(…)和Error(…)
创建日期对象必须使用 new Date()
构造函数 Error(…)(与前面的 Array() 类似)带不带 new 关键字都可
错误对象通常与 throw 一起使用: throw new Error( “x wasn’t provided” )
-
符号:Symbol(…)
- 符号是具有唯一性的特殊值(并非绝对),用它来命名对象属性不容易导致重名
- 符号可以用作属性名,但无论是在代码还是开发控制台中都无法查看和访问它的值,只会显示为诸如 Symbol(Symbol.create) 这样的值
- Symbol(…) 原生构造函数来自定义符号,且不能带 new 关键字
- 符号并非对象,而是一种简单标量基本类型
- 一般喜欢用符号来替代有下划线(_)前缀的属性,即私有或特殊属性。
-
原生原型
原生构造函数都有自己的.prototype对象
String.prototype(简写为String#)
-
String#indexOf(…)
在字符串中找到指定子字符串的位置。
-
String#charAt(…)
获得字符串指定位置上的字符。
-
String#substr(…)、String#substring(…) 和 String#slice(…)
获得字符串的指定部分。
-
String#toUpperCase() 和 String#toLowerCase()
将字符串转换为大写或小写。
-
String#trim()
去掉字符串前后的空格,返回新的字符串
这些方法都是返回一个新的字符串,不会改变原字符串的值
很有意思的是,Function.prototype 是一个空函数,RegExp.prototype 是一个空正则表达式,而 Array. prototype 是一个空数组。
对于未赋值的变量来说是一个很好的默认值。对于需要多次调用的函数来说,可以给参数设置默认值,这样每次调用时由于.prototypes 已被创建并且仅创建一次,可以减少内存和CPU资源的浪费
function isThisCool(vals,fn,rx) { vals = vals || Array.prototype; fn = fn || Function.prototype; rx = rx || RegExp.prototype; }
-
强制类型转换
抽象值操作
-
toString(…)
-
极大和极小数都使用指数形式
var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000; a.toString(); // "1.07e21"
-
数组的默认 toString() 方法经过了重新定义,将所有单元字符串化以后再用 “,” 连接起来
var a = [1,2,3]; a.toString(); // "1,2,3"
-
-
JSON.stringfy(…) toJSON()
-
JSON.stringfy(…)和toString(…)的效果基本相同。
-
JSON.stringify(…) 在对象中遇到 undefined、function 和 symbol 时会自动将其忽略,在数组中则会返回 null(以保证单元位置不变)。
JSON.stringify( undefined ); // undefined JSON.stringify( function(){} ); // undefined JSON.stringify( [1,undefined,function(){},4] ); // "[1,null,null,4]" JSON.stringify( { a:2, b:function(){} } ); // "{"a":2}"
-
对包含循环引用的对象执行 JSON.stringify(…) 会出错。
-
对于2,3中非法JSON值的对象或对象中某些值无法被序列化时,需要定义toJSON()方法来返回一个安全的JSON值。
toJSON() 应该“返回一个能够被字符串化的安全的 JSON 值”,而不是“返回一个 JSON 字符串”。
举例:
var a = { val: [1,2,3], // 可能是我们想要的结果! toJSON: function(){ return this.val.slice( 1 ); } }; var b = { val: [1,2,3], // 可能不是我们想要的结果! toJSON: function(){ return "[" + this.val.slice( 1 ).join() + "]"; } }; JSON.stringify( a ); // "[2,3]" JSON.stringify( b ); // ""[2,3]""
-
JSON.stringfy(…)可传递一个可选参数replacer,它可以是数组或者函数,用
来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除。
var a={ b:42, c:"42", d:[1,2,3] }; console.log(JSON.stringify(a,["b","c"])); // "{"b":42,"c":"42"}" console.log(JSON.stringify(a,function(k,v){ if(k!=="c") return v; })); // "{"b":42,"d":[1,2,3]}"
-
-
toBoolean
-
JavaScript中的值可以分为两类:可以被强制类型转换为false的值(假值)和其他可以被强制类型转换为true的值。
以下这些是假值:
• undefined
• null
• false
• +0、-0 和 NaN
• “”
除此之外都是真值
-
假值对象
假值对象并非封装了假值的对象。
var a = new Boolean( false );//true
-
显式强制类型转换
- String(…). Number(…) Boolean(…)前面不用new关键字,并不创建封装对象
var a = 42;
var b = String( a );
var c = "3.14";
var d = Number( c );
b; // "42"
d; // 3.14
-
运算符的一元形式: 字符串->数字
var c = "3.14"; var d = +c; d; // 3.14
显式解析数字字符串
-
parseInt(…)
-
JavaScript 的有效数字字符范围是 0-9 和 a-i(区分大小写)
parseInt再遇到无效数字时会终止解析
因此parseInt(“42px”)中遇到"p"会终止解析,结果为42
-
不要忘了 parseInt(…) 针对的是字符串值。向 parseInt(…) 传递数字和其他类型的参数是没有用的,比如 true、function(){…} 和 [1,2,3]。
-
显示转换为布尔值
- 一般不用Boolean(…),一般用 “!!”
- !! 两个!将结果反转两次回原值,并将结果强制转换为布尔值
- 注意回顾之前提到的js中的几个假值,除了那几个假值以外全是真值
var a = "0";
var b = [];
var c = {};
var d = "";
var e = 0;
var f = null;
var g;
!!a; // true
!!b; // true
!!c; // true
!!d; // false
!!e; // false
!!f; // false
!!g; // false
隐式强制类型转换
字符串和数字之间
-
用 + 来实现 数字->字符串
如果 + 的其中一个操作数是字符串,或如果是封装对象无法拆封(valueOf() 操作无法得到简单基本类型值,比如数组),则执行字符串拼接;否则执行数字加法。
常用的可以用数字和空字符串""相 + 将其转换为字符串
var a = 42; var b = a + ""; b; // "42"
-
用 - 来实现 字符串->数字
var a = "3.14"; var b = a - 0; b; // 3.14
转换为布尔值:主要强调 | | 和&&
-
|| 和 && 首先会对第一个操作数执行条件判断,如果其不是布尔值就先进行 ToBoolean 强制类型转换,然后再执行条件判断。
-
对于 || 来说,如果条件判断结果为 true 就返回第一个操作数(a 和 c)的值,如果为false 就返回第二个操作数(b)的值。
&& 则相反,如果条件判断结果为 true 就返回第二个操作数(b)的值,如果为 false 就返回第一个操作数(a 和 c)的值。
换句话说:
a || b; // 大致相当于(roughly equivalent to): a ? a : b; a && b; // 大致相当于(roughly equivalent to): a ? b : a;
-
||用的比较多,常见的用法早已使用过,常用在给变量指定默认值。
function foo(a,b) { a = a || "hello"; b = b || "world"; console.log( a + " " + b ); } foo(); //"hello world"
-
&&用在JavaScript 代码压缩工具中
如。 if (a) { foo(); } 可以简化为 a && foo()
宽松相等和严格相等
先说结论:“== 允许在相等比较中进行强制类型转换,而 === 不允许。”
===不做讨论。下面是 == 在ES5规范中的定义:
-
数字 & 字符串
-
如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果。
-
如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果。
简单来说,数字==字符串 会把字符串强制转换为数字
-
-
其他类型 & 布尔
- 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果;
- 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果。
简单来说,布尔值在==中都会被转换为数字
注意空字符串会转换为0
-
null & undefined(这俩在==中是一回事)
- 如果 x 为 null,y 为 undefined,则结果为 true。
- 如果 x 为 undefined,y 为 null,则结果为 true。
-
对象 & 非对象
- 如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果;
- 如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPrimitive(x) == y 的结果。
注:这里的ToPrimitive就是之前提到的抽象值操作,如会把 [42]转换为 “42”
PartII 异步和性能
异步
分块的程序
-
任何时候,只要把一段代码包装成一个函数,并指定它在响应某个事件(定时器、鼠标点击、Ajax 响应等)时执行,你就是在代码中创建了一个将来执行的块,也由此在这个程序中引入了异步机制。
-
JavaScript 程序总是至少分为两个块:第一块现在运行;下一块将来运行,以响
应某个事件。
事件循环
程序通常分成了很多小块,在事件循环队列中一个接一个地执行。严格地说,和你的程序不直接相关的其他事件也可能会插入到队列中。每一个被执行的事件就是回调函数
setTimeout(…) 并没有把你的回调函数挂在事件循环队列中。它所做的是设定一个定时器。当定时器到时后,环境会把你的回调函数放在事件循环中。
特别的,setTImeout(…,0)进行异步调度,基本上它的意思是“把回调函数事件插入到当前事件循环队列的结尾处”。
JavaScript是单线程运行的
一个线程就是一个程序块。一个程序块开始运行后,它的所有代码都会在下一个程序块的任意代码运行之前完成,或者相反。这称为完整运行特性
单线程不是只有一个线程的意思,而是每次同时只会有一个线程在执行,不存在并行执行多个线程的情况。
举例:
var a = 20;
function foo() {
a = a + 1;
}
function bar() {
a = a * 2;
}
// ajax(..)是某个库中提供的某个Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
由于两个函数都需要请求响应之后才能执行,因此执行先后顺序不定。
线程 1(X 和 Y 是临时内存地址):
foo():
a. 把a的值加载到X
b. 把1保存在Y
c. 执行X加Y,结果保存在X
d. 把X的值保存在a
线程 2(X 和 Y 是临时内存地址):
bar():
a. 把a的值加载到X
b. 把2保存在Y
c. 执行X乘Y,结果保存在X
d. 把X的值保存在a
根据 JavaScript 的单线程运行特性,如果 foo() 运行在 bar() 之前,a 的结果是 42,而如果bar() 运行在 foo() 之前的话,a 的结果就是 41。并且也只会有这两种情况。
而如果是并行多线程执行,就会出现如下情况:
1a (把a的值加载到X ==> 20)
2a (把a的值加载到X ==> 20)
1b (把1保存在Y ==> 1)
2b (把2保存在Y ==> 2)
1c (执行X加Y,结果保存在X ==> 22)
1d (把X的值保存在a ==> 22)
2c (执行X乘Y,结果保存在X ==> 44)
2d (把X的值保存在a ==> 44)
a的结果将是44,或者:
1a (把a的值加载到X ==> 20)
2a (把a的值加载到X ==> 20)
2b (把2保存在Y ==> 2)
1b (把1保存在Y ==> 1)
2c (执行X乘Y,结果保存在X ==> 20)
1c (执行X加Y,结果保存在X ==> 21)
1d (把X的值保存在a ==> 21)
2d (把X的值保存在a ==> 21)
a 的结果将是 21。
并发
并发是指两个或多个事件链随时间发展交替执行
在javaScript中,进程是并发运行(任务级并行)的,但是它们的各个事件是在事件循环队列中依次运行的(单线程)
如果进程之间需要交互,则在交互之前要设置门闩,即通过if语句来判断。
例如:
var a;
function foo(x) {
if (!a) {
a = x * 2;
baz();
}
}
function bar(x) {
if (!a) {
a = x / 2;
baz();
}
}
function baz() {
console.log( a );
}
// ajax(..)是某个库中的某个Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
条件判断 if (!a) 使得只有 foo() 和 bar() 中的第一个可以通过,第二名没有任何意义
不设置这个判断,回调用两个baz,这不是我们想要的。
任务
- 对于任务队列最好的理解方式就是,它是挂在事件循环队列的每个 tick 之后的一个队列。在事件循环的每个 tick 中,可能出现的异步动作不会导致一个完整的新事件添加到事件循环队列中,而会在当前 tick 的任务队列末尾添加一个项目(一个任务)
回调
回调地狱
当你指定,或者预先计划了所有的可能事件和路径,代码就会变的非常复杂,以至于无法维护和更新。换句话说,我们的顺序阻塞式的大脑计划行为无法很好地映射到面向回调的异步代码。这是回调方式最主要的缺陷:对于它们在代码中表达异步的方式,我们的大脑需要努力才能同步的上。
因而我们需要一种更同步,更顺序,更阻塞的方式来表达异步,就像我们的大脑一样。
Promise
-
Promise 决议后就是外部不可变的值,我们可以安全地把这个值传递给第三
方,并确信它不会被有意无意地修改。
Promise未完,之后再看一遍再做笔记
生成器
打破完整运行
-
yield的含义就是暂停
var x = 1; function *foo() { x++; yield; // 暂停! console.log( "x:", x ); } function bar() { x++; } // 构造一个迭代器it来控制这个生成器 var it = foo(); // 这里启动foo()! it.next(); //执行到yeild暂停 x; // 2 bar(); x; // 3 //恢复生成器的执行,并运行console.log()打印 it.next();
-
消息的双向传递
function *foo(x) { var y = x * (yield "Hello"); // <-- yield一个值! return y; } var it = foo( 6 ); var res = it.next(); // 第一个next(),并不传入任何东西 res.value; // "Hello" res = it.next( 7 ); // 向等待的yield传入7 res.value; // 42
- 我们并没有给第一个next()调用发送值,只有暂停的yeild才能接受这样一个通过next()传递的值
- 第一个 next() 调用(没有参数的)基本上就是在提出一个问题:“生成器 *foo(…) 要给我的下一个值是什么”。谁来回答这个问题呢?第一个 yield “hello” 表达式。
- 最后一个多出来的next()所提出的问题由return语句回答(return undefined也算)
-
多个迭代器
考虑这一段完整运行的场景
var a = 1; var b = 2; function foo() { a++; b = b * a; a = b + 3; } function bar() { b--; a = 8 + b; b = a * 2; }
这两个函数肯定会一个执行完之后才会执行另外一个,想要交替执行可以通过生成器实现
var a = 1; var b = 2; function *foo() { a++; yield; b = b * a; a = (yield b) + 3; } function *bar() { b--; yield; a = (yield 8) + b; b = a * (yield 2); }
我们想要达到一个目的,yeild处只起到暂停作用。也就是说,yeild处在原来的代码中该是多少就是多少,因此需要构建一个step(…)的辅助函数用于控制迭代器
function step(gen){ var it=gen(); var last; return function(){ //前面yeild发出的值可以在下一步发送回去 last=it.next(last).value; } }
-
step(…) 初始化了一个生成器来创建迭代器 it,然后返回一个函数,这个函数被调用的时候会将迭代器向前迭代一步。
-
前面的 yield 发出的值会在下一步发送回去。于是,yield 8 就是 8,而 yield b 就是 b(yield 发出时的值)。
先基本运行看看
a = 1; b = 2; var s1=step(foo); var s2=step(bar); s1(); //a=2 b=2 s1(); //a=2 b=4 s1(); //a=7 b=4 s2(); //a=7 b=3 s2(); //a=7 b=3 s2(); //a=11 b=3 s2(); //a=11 b=22 console.log(a,b); //11 22
再交替运行看看
a = 1; b = 2; var s1=step(foo); var s2=step(bar); s2(); // b--; a=1 b=1 s2(); // yield 8 a=1 b=1 s1(); // a++; a=2 b=1 s2(); // a=8+b; // yield 2 a=9 b=1 s1(); //b=b*a; //yield b a=9 b=9 s1(); //a=b+3; a=12 b=9 s2(); //b=a*2 a=12 b=18 console.log(a,b);
-
生成器产生值
一个标准的迭代器接口
var something = (function(){
var nextVal;
return {
// for..of循环需要
[Symbol.iterator]: function(){ return this; },
// 标准迭代器接口方法
next: function(){
if (nextVal === undefined) {
nextVal = 1;
}
else {
nextVal = (3 * nextVal) + 6;
}
//返回一个对象。这个对象有两个属性:done 是一个 boolean 值,标识迭代器的完成状态;value 中放置迭代值。
return { done:false, value:nextVal };
}
};
})();
ES6新增的for…of循环,可以直接通过原生循环语法自动迭代标准的迭代器
for (var v of something) {
console.log( v );
// 不要死循环!
if (v > 500) {
break;
}
}
for…of 循环在每次迭代中自动调用 next(),它不会向 next() 传入任何值,并且会在接收
到 done:true 之后自动停止
这里的something是一个iterable迭代器,因为接口中有一个next方法。
注意:生成器并不是iterable,当你执行了一个生成器,就得到了一个迭代器。
可以通过生成器实现前面的这个something无线数字序列生产者
function *something() {
var nextVal;
while (true) {
if (nextVal === undefined) {
nextVal = 1;
}
else {
nextVal = (3 * nextVal) + 6;
}
yield nextVal;
}
}
注意这里的something是生成器,在使用的时候要先调用生成器以得到它的迭代器供for…of使用,生成器调用得到的迭代器也是一个iterable
//注意这里something要调用
for (var v of something()) {
console.log( v );
// 不要死循环!
if (v > 500) {
break;
}
}
// 1 9 33 105 321 969
也可以像这样终止生成器
var it = something();
for (var v of it) {
console.log( v );
// 不要死循环!
if (v > 500) {
console.log(
// 完成生成器的迭代器
it.return( "Hello World" ).value
);
// 这里不需要break
}
}
// 1 9 33 105 321 969
// 清理!
// Hello World
It.return()后会立即终止生成器,他还会把返回的value值设置为传入return(…)的内容。
现在就不需要break了,因为生成器的迭代器已经被设置为done:true,所以循环会在下一个迭代终止
异步迭代生成器
考虑以下代码
function foo(x,y) {
ajax(
"http://some.url.1/?x=" + x + "&y=" + y,
function(err,data){
if (err) {
// 向*main()抛出一个错误
it.throw( err );
}
else {
// 用收到的data恢复*main()
it.next( data );
}
}
);
}
function *main() {
try {
var text = yield foo( 11, 31 );
console.log( text );
}
catch (err) {
console.error( err );
}
}
var it = main();
// 这里启动!
it.next();
启动后,生成器在yield处暂停,本质上提出了一个问题:“我应该返回什么值来赋值给变量text?”赋值语句暂停来等待foo完成。
紧接着在foo中,如果ajax请求成功,调用it.next(data)会用响应数据恢复生成器,意味着我们暂停的yield表达式直接接收到了这个值,吧这个值赋给局部变量text。
这里实现了可以以同步顺序的形式追踪流程控制:发出一个ajax请求,等它完成之后打印响应结果。
生成器 X Promise
获得 Promise 和生成器最大效用的最自然的方法就是 yield 出来一个 Promise,然后通过这个 Promise 来控制生成器的迭代器。
我们可以这样死板的来实现这一想法
function foo(x,y) {
//前面提到过,request返回一个Promise
return request(
"http://some.url.1/?x=" + x + "&y=" + y
);
}
function *main() {
try {
var text = yield foo( 11, 31 );
console.log( text );
}
catch (err) {
console.error( err );
}
}
var it = main();
//这一步拿到request返回的Promise
var p = it.next().value;
// 等待promise p决议
p.then(
function(text){
it.next( text );
},
function(err){
it.throw( err );
}
);
也可以定义一个run工具来实现
function run(gen){
var args=[].slice.call(arguments,1),it;
//在当前上下文中初始化生成器
it=gen.apply(this,args);
//返回一个promise用于生成器完成
//一旦生成器完成,这个promise就会决议,或收到一个生成器没有处理的未捕获异常
return Promise.resolve()
.then(function handleNext(value){
//对下一个yield出的值运行
var next=it.next(value);
return(function handleResult(next){
//生成器运行完了?(出口)
if(next.done){
return next.value;
}
//继续运行
else{
return Promise.resolve(next.value)
.then(
handleNext,
function handleErr(err){
return Promise.resolve(it.throw(err))
.then(handleResult);
}
);
}
})(next)
})
}
那么就直接调用run就可以实现前面的功能
run( main );
对此,ES7提供的async和await就可以实现上述功能
function foo(x,y) {
return request(
"http://some.url.1/?x=" + x + "&y=" + y
);
}
async function main() {
try {
var text = await foo( 11, 31 );
console.log( text );
}
catch (err) {
console.error( err );
}
}
main();
只要把main函数声明为async函数,就不需要yield出Promise,而是用await等待决议
如果你await了一个 Promise,async 函数就会自动获知要做什么,它会暂停这个函数(就像生成器一样),直到 Promise 决议。
调用一个像 main() 这样的 async 函数会自动返回一个 promise。在函数完全结束之后,这个 promise会决议。
生成器XPromise => 并发
场景:你需要从两个不同的来源获取数据,然后把响应组合在一起以形成第三个请求,最 终把最后一条响应打印出来。
第一反应:
function *foo() {
var r1 = yield request( "http://some.url.1" );
var r2 = yield request( "http://some.url.2" );
var r3 = yield request( "http://some.url.3/?v=" + r1 + "," + r2 );
console.log( r3 );
}
// 使用前面定义的工具run(..)
run( foo );
但是这样,两个request是依次执行的,但其实性能更高的方案是这俩request并行。
最简单的方法:
function *foo() {
// 让两个请求"并行"
var p1 = request( "http://some.url.1" );
var p2 = request( "http://some.url.2" );
// 等待两个promise都决议
var r1 = yield p1;
var r2 = yield p2;
var r3 = yield request(
"http://some.url.3/?v=" + r1 + "," + r2
);
console.log( r3 );
}
// 使用前面定义的工具run(..)
run( foo );
-
为什么这样就可以实现并行了?
p1,p2是并发执行的用于ajax请求的promise。哪一个先完成都无所谓,因为promise会单招需要在决议状态保持任意时间。
-
接下来两个yield等待两个promise都决议,p1,p2两者虽然并发执行,无论顺序如何,但是两者都要全部完成之后,才能进到r3的ajax请求。
这样来说,也可以使用Promise.all([…])来实现,因为Promise.all也会等待所有promise都决议后总promise才会决议
function *foo() {
// 让两个请求"并行",并等待两个promise都决议
var results = yield Promise.all( [
request( "http://some.url.1" ),
request( "http://some.url.2" )
] );
var r1 = results[0];
var r2 = results[1];
var r3 = yield request(
"http://some.url.3/?v=" + r1 + "," + r2
);
console.log( r3 );
}
// 使用前面定义的工具run(..)
run( foo );
实际开发中,我们把异步,实际上是 Promise,作为一个实现细节看待。这意味着,我们通常会把Promise逻辑隐藏在一个只从生成器代码中调用的函数内部,像下面这样:
function bar(url1,url2) {
return Promise.all( [
request( url1 ),
request( url2 )
] );
}
function *foo() {
// 隐藏bar(..)内部基于Promise的并发细节
var results = yield bar(
"http://some.url.1",
"http://some.url.2"
);
var r1 = results[0];
var r2 = results[1];
var r3 = yield request(
"http://some.url.3/?v=" + r1 + "," + r2
);
console.log( r3 );
}
// 使用前面定义的工具run(..)
run( foo );
生成器委托
委托-从一个生成器中调用另一个生成器
实现:用yield *实现
function *foo() {
console.log( "*foo() starting" );
yield 3;
yield 4;
console.log( "*foo() finished" );
}
function *bar() {
yield 1;
yield 2;
yield *foo(); // yield委托!
yield 5;
}
var it = bar();
it.next().value; // 1
it.next().value; // 2
it.next().value; // *foo()启动
// 3
it.next().value; // 4
it.next().value; // *foo()完成
// 5
可用于双向消息传递
function *foo(){
console.log("inside *foo():",yield "B");
console.log("inside *foo():",yield "C");
return "D";
}
function *bar(){
console.log("inside *bar():",yield "A");
console.log("inside *bar():",yield *foo());
console.log("inside *bar():",yield "E");
return "F";
}
var it=bar();
console.log("outside:",it.next().value);
console.log("outside:",it.next(1).value);
console.log("outside:",it.next(2).value);
console.log("outside:",it.next(3).value);
console.log("outside:",it.next(4).value);
// outside: A
// inside *bar(): 1
// outside: B
// inside *foo(): 2
// outside: C
// inside *foo(): 3
// inside *bar(): D
// outside: E
// inside *bar(): 4
// outside: F
异常也可以被委托
function *foo(){
try{
yield "B";
}catch(err){
console.log("error caught inside *foo():",err);
}
yield "C";
throw "D";
}
function *bar(){
console.log("inside *bar():",yield "A");
try{
yield *foo();
}catch(err){
console.log("error caught inside *bar():",err);
}
console.log("inside *bar():",yield "E");
yield *baz();
yield "G";
}
function *baz(){
throw "F";
}
var it=bar();
console.log( "outside:", it.next().value );
console.log( "outside:", it.next(1).value );
console.log( "outside:", it.throw(2).value );
console.log( "outside:", it.next(3).value );
try{
console.log( "outside:", it.next(4).value );
}catch(err){
console.log("error caught outside:",err)
}
// outside: A
// inside *bar(): 1
// outside: B
// error caught inside *foo(): 2
// outside: C
// error caught inside *bar(): D
// outside: E
// inside *bar(): 4
// error caught outside: F
生成器并发
给下面这个例子:
// request(..)是一个支持Promise的Ajax工具
var res = [];
function *reqData(url) {
var data = yield request( url );
// 控制转移
yield;
res.push( data );
}
var it1 = reqData( "http://some.url.1" );
var it2 = reqData( "http://some.url.2" );
var p1 = it.next();
var p2 = it.next();
//控制转移给迭代器用于把data传递进去
//且二者可并发执行
p1.then( function(data){
it1.next( data );
} );
p2.then( function(data){
it2.next( data );
} );
Promise.all( [p1,p2] )
.then( function(){
//恢复响应顺序
it1.next();
it2.next();
} );
形实转换程序(thunk)
JavaScript中的thunk是指一个用于调用另外一个函数的函数,没有任何参数。
当然,thunk中也可以指定回调,像下面这样
function foo(x,y,cb){
setTimeout(function(){
cb(x+y);
},1000)
}
function fooThunk(cb){
foo(3,4,cb);
}
fooThunk(function(sum){
console.log(sum);
})
我们可以发明一个工具来封装thunk
function thunkify(fn){
//拿到第二个开始的参数
var args=[].slice.call(arguments,1);
return function(cb){
//thunk指定的回调加入参数数组
args.push(cb);
//参数数组拆开后作为本例中foo的参数调用返回
return fn.apply(null,args);
}
}
thunkify实际上是得到一个产生thunk的工厂:thunkory
var fooThunkory = thunkify( foo );
var fooThunk1 = fooThunkory( 3, 4 );
var fooThunk2 = fooThunkory( 5, 6 );
// 将来
fooThunk1( function(sum) {
console.log( sum ); // 7
} );
fooThunk2( function(sum) {
console.log( sum ); // 11
} );
给它加上err-first之后,会和promisory很像:
function foo(x,y,cb) {
setTimeout( function(){
// 假定cb(..)是error-first风格的
cb( null, x + y );
}, 1000 );
}
对比一下thunkify和promisify:
// 对称:构造问题提问者
var fooThunkory = thunkify( foo );
var fooPromisory = promisify( foo );
// 对称:提问
var fooThunk = fooThunkory( 3, 4 );
var fooPromise = fooPromisory( 3, 4 );
// 得到thunk答案
fooThunk( function(err,sum){
if (err) {
console.error( err );
}
else {
console.log( sum ); // 7
}
} );
// 得到promise答案
fooPromise
.then(
function(sum){
console.log( sum ); // 7
},
function(err){
console.error( err );
}
);
小结
生成器的关键优点:
生成器内部的代码是以自然的同步 / 顺序方式表达任务的一系列步骤。
其技巧在于,我们把可能的异步隐藏在了关键字 yield 的后面,把异步移动到控制生成器的迭代器的代码部分。
换句话说,生成器为异步代码保持了顺序、同步、阻塞的代码模式,这使得大脑可以更自然地追踪代码,解决了基于回调的异步的两个关键缺陷之一。
程序性能的进一步提升
WebWorker
Web Worker 让你可以在独立的线程运行一个 JavaScript 文件(即程序),使用异步事件在线程之间传递消息。
它们非常适用于把长时间的或资源密集型的任务卸载到不同的线程中,以提高主 UI 线程的响应性。
SIMD
SIMD 打算把 CPU 级的并行数学运算映射到 JavaScript API,以获得高性能的数据并行运算,比如在大数据集上的数字处理。
ams.js
asm.js 描述了 JavaScript 的一个很小的子集,它避免了 JavaScript 难以优化的部分(比如垃圾收集和强制类型转换),并且让 JavaScript 引擎识别并通过激进的优化运行这样的代码。
可以手工编写 asm.js,但是会极端费力且容易出错,类似于手写汇编语言(这也是其名字的由来)。实际上,asm.js 也是高度优化的程序语言交叉编译的一个很好的目标,比如 Emscripten 把 C/C++ 转换成 JavaScript。
性能测试与调优
微性能
先强调一点:在更大的上下文中,引擎可能并不会运行你编写的一行代码,而是把它们都重写了。
比如递归,有些引擎会进行递归展开的动作,自动优化。比如你还在纠结是++i还是i++的时候,可能引擎看到了一个i++,就已经把它替换为等价的++i了。
其次,不要过度关注非关键路径上的微性能优化,这都是在浪费时间。
尾调用优化(TCO)
尾调用就是一个出现在另一个函数结尾处的函数调用,这个调用结束后就没有其余事情要做了。
function foo(x) {
return x;
}
function bar(y) {
return foo( y + 1 ); // 尾调用
}
function baz() {
return 1 + bar( 40 ); // 非尾调用,调用完bar之后还要+1
}
baz(); // 42
支持TCO的引擎意识到尾调用后,不会再创建一个新的栈帧,而是可以重用已有的栈帧,这样速度更快,节省内存。在处理递归中,可以省掉成百上千个栈帧。