1.16 Module语法和加载实现
1.16.1 介绍
ES6之前用于JavaScript的模块加载方案,是一些社区提供的,主要有CommonJS
和AMD
两种,前者用于服务器,后者用于浏览器。
ES6提供了模块的实现,使用export
命令对外暴露接口,使用import
命令输入其他模块暴露的接口。
// CommonJS模块 let { stat, exists, readFire } = require('fs'); // ES6模块 import { stat, exists, readFire } = from 'fs';
1.16.2 严格模式
ES6模块自动采用严格模式,无论模块头部是否有"use strict"
。
严格模式有以下限制:
- 变量必须声明后再使用
- 函数的参数不能有同名属性,否则报错
- 不能使用
with
语句 - 不能对只读属性赋值,否则报错
- 不能使用前缀 0 表示八进制数,否则报错
- 不能删除不可删除的属性,否则报错
- 不能删除变量
delete prop
,会报错,只能删除属性delete * global[prop]
eval
不会在它的外层作用域引入变量eval
和arguments
不能被重新赋值arguments
不会自动反映函数参数的变化- 不能使用
arguments.callee
- 不能使用
arguments.caller
- 禁止
this
指向全局对象 - 不能使用
fn.caller
和fn.arguments
获取函数调用的堆栈 - 增加了保留字(比如
protected
、static
和interface
)
特别是,ES6中顶层this
指向undefined
,即不应该在顶层代码使用this
。
1.16.3 export命令
使用export
向模块外暴露接口,可以是方法,也可以是变量。
// 1. 变量 export let a = 'leo'; export let b = 100; // 还可以 let a = 'leo'; let b = 100; export {a, b}; // 2. 方法 export function f(a,b){ return a*b; } // 还可以 function f1 (){ ... } function f2 (){ ... } export { a1 as f1, a2 as f2 }
可以使用as
重命名函数的对外接口。
特别注意:
export
暴露的必须是接口,不能是值。
// 错误 export 1; // 报错 let a = 1; export a; // 报错 // 正确 export let a = 1; // 正确 let a = 1; export {a}; // 正确 let a = 1; export { a as b}; // 正确
暴露方法也是一样:
// 错误 function f(){...}; export f; // 正确 export function f () {...}; function f(){...}; export {f};
1.16.4 import命令
加载export
暴露的接口,输出为变量。
import { a, b } from '/a.js'; function f(){ return a + b; }
import
后大括号指定变量名,需要与export
的模块暴露的名称一致。
也可以使用as
为输入的变量重命名。
import { a as leo } from './a.js';
import
不能直接修改输入变量的值,因为输入变量只读只是个接口,但是如果是个对象,可以修改它的属性。
// 错误 import {a} from './f.js'; a = {}; // 报错 // 正确 a.foo = 'leo'; // 不报错
import
命令具有提升效果,会提升到整个模块头部最先执行,且多次执行相同import
只会执行一次。
1.16.5 模块的整体加载
当一个模块暴露多个方法和变量,引用时可以用*
整体加载。
// a.js export function f(){...} export function g(){...} // b.js import * as obj from '/a.js'; console.log(obj.f()); console.log(obj.g());
但是,不允许运行时改变:
import * as obj from '/a.js'; // 不允许 obj.a = 'leo'; obj.b = function(){...};
1.16.6 export default 命令
使用export default
命令,为模块指定默认输出,引用的时候直接指定任意名称即可。
// a.js export default function(){console.log('leo')}; // b.js import leo from './a.js'; leo(); // 'leo'
export defualt
暴露有函数名的函数时,在调用时相当于匿名函数。
// a.js export default function f(){console.log('leo')}; // 或者 function f(){console.log('leo')}; export default f; // b.js import leo from './a.js';
export defualt
其实是输出一个名字叫default
的变量,所以后面不能跟变量赋值语句。
// 正确 export let a= 1; let a = 1; export defualt a; // 错误 export default let a = 1;
export default
命令的本质是将后面的值,赋给default
变量,所以可以直接将一个值写在export default
之后。
// 正确 export default 1; // 错误 export 1;
1.16.7 export 和 import 复合写法
常常在先输入后输出同一个模块使用,即转发接口,将两者写在一起。
export {a, b} from './leo.js'; // 理解为 import {a, b} from './leo.js'; export {a, b}
常见的写法还有:
// 接口改名 export { a as b} from './leo.js'; // 整体输出 export * from './leo.js'; // 默认接口改名 export { default as a } from './leo.js';
常常用在模块继承。
1.16.8 浏览器中的加载规则
ES6中,可以在浏览器使用<script>
标签,需要加入type="module"
属性,并且这些都是异步加载,避免浏览器阻塞,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>
标签的defer
属性。
<script type="module" src="./a.js"></script>
另外,ES6模块也可以内嵌到网页,语法与外部加载脚本一致:
<script type="module"> import a from './a.js'; </script>
注意点:
- 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
- 模块脚本自动采用严格模式,不管有没有声明
use strict
。 - 模块之中,可以使用
import
命令加载其他模块(.js
后缀不可省略,需要提供绝对 UR
L 或相对 UR
L),也可以使用export
命令输出对外接口。 - 模块之中,顶层的
this
关键字返回undefined
,而不是指向window
。也就是说,在模块顶层使用this
关键字,是无意义的。 - 同一个模块如果加载多次,将只执行一次。
2. ES7
2.1 Array.prototype.includes()方法
includes()
用于查找一个值是否在数组中,如果在返回true
,否则返回false
。
['a', 'b', 'c'].includes('a'); // true ['a', 'b', 'c'].includes('d'); // false
includes()
方法接收两个参数,搜索的内容和开始搜索的索引,默认值为0,若搜索值在数组中则返回true
否则返回false
。
['a', 'b', 'c', 'd'].includes('b'); // true ['a', 'b', 'c', 'd'].includes('b', 1); // true ['a', 'b', 'c', 'd'].includes('b', 2); // false
与indexOf
方法对比,下面方法效果相同:
['a', 'b', 'c', 'd'].indexOf('b') > -1; // true ['a', 'b', 'c', 'd'].includes('b'); // true
includes()与indexOf对比:
includes
相比indexOf
更具语义化,includes
返回的是是否存在的具体结果,值为布尔值,而indexOf
返回的是搜索值的下标。includes
相比indexOf
更准确,includes
认为两个NaN
相等,而indexOf
不会。
let a = [1, NaN, 3]; a.indexOf(NaN); // -1 a.includes(NaN); // true
另外在判断+0
与-0
时,includes
和indexOf
的返回相同。
[1, +0, 3, 4].includes(-0); // true [1, +0, 3, 4].indexOf(-0); // 1
2.2 指数操作符(**)
基本用法:
let a = 3 ** 2 ; // 9 // 等效于 Math.pow(3, 2); // 9
**
是一个运算符,也可以满足类似假发的操作,如下:
let a = 3; a **= 2; // 9
3. ES8
3.1 async函数
3.1.1 介绍
ES8引入async
函数,是为了使异步操作更加方便,其实它就是Generator函数的语法糖。
async
函数使用起来,只要把Generator函数的(*)号换成async
,yield
换成await
即可。对比如下:
// Generator写法 const fs = require('fs'); const readFile = function (fileName) { return new Promise(function (resolve, reject) { fs.readFile(fileName, function(error, data) { if (error) return reject(error); resolve(data); }); }); }; const gen = function* () { const f1 = yield readFile('/etc/fstab'); const f2 = yield readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); }; // async await写法 const asyncReadFile = async function () { const f1 = await readFile('/etc/fstab'); const f2 = await readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); };
对比Genenrator有四个优点:
- (1)内置执行器 Generator函数执行需要有执行器,而
async
函数自带执行器,即async
函数与普通函数一模一样:
async f();
- (2)更好的语义
async
和await
,比起星号
和yield
,语义更清楚了。async
表示函数里有异步操作,await
表示紧跟在后面的表达式需要等待结果。 - (3)更广的适用性
yield
命令后面只能是 Thunk 函数或 Promise 对象,而async
函数的await
命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。 - (4)返回值是Promise
async
函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then
方法指定下一步的操作。
进一步说,async
函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await
命令就是内部then
命令的语法糖。
3.1.2 基本用法
async
函数返回一个Promise对象,可以使用then
方法添加回调函数,函数执行时,遇到await
就会先返回,等到异步操作完成,在继续执行。
async function f(item){ let a = await g(item); let b = await h(item); return b; } f('hello').then(res => { console.log(res); })
async
表明该函数内部有异步操作,调用该函数时,会立即返回一个Promise对象。
另外还有个定时的案例,指定时间后执行:
function f (ms){ return new Promise(res => { setTimeout(res, ms); }); } async function g(val, ms){ await f(ms); console.log(val); } g('leo', 50);
async
函数还有很多使用形式:
// 函数声明 async function f (){...} // 函数表达式 let f = async function (){...} // 对象的方法 let a = { async f(){...} } a.f().then(...) // Class的方法 class c { constructor(){...} async f(){...} } // 箭头函数 let f = async () => {...}
3.1.3 返回Promise对象
async
内部return
返回的值会作为then
方法的参数,另外只有async
函数内部的异步操作执行完,才会执行then
方法指定的回调函数。
async function f(){ return 'leo'; } f().then(res => { console.log (res) }); // 'leo'
async
内部抛出的错误,会被catch
接收。
async function f(){ throw new Error('err'); } f().then ( v => console.log(v), e => console.log(e) ) // Error: err
3.1.4 await命令
通常await
后面是一个Promise对象,如果不是就返回对应的值。
async function f(){ return await 10; } f().then(v => console.log(v)); // 10
我们常常将async await
和try..catch
一起使用,并且可以放多个await
命令,也是防止异步操作失败因为中断后续异步操作的情况。
async function f(){ try{ await Promise.reject('err'); }catch(err){ ... } return await Promise.resolve('leo'); } f().then(v => console.log(v)); // 'leo'
3.1.5 使用注意
- (1)
await
命令放在try...catch
代码块中,防止Promise返回rejected
。 - (2)若多个
await
后面的异步操作不存在继发关系,最好让他们同时执行。
// 效率低 let a = await f(); let b = await g(); // 效率高 let [a, b] = await Promise.all([f(), g()]); // 或者 let a = f(); let b = g(); let a1 = await a(); let b1 = await b();
- (3)
await
命令只能用在async
函数之中,如果用在普通函数,就会报错。
// 错误 async function f(){ let a = [{}, {}, {}]; a.forEach(v =>{ // 报错,forEach是普通函数 await post(v); }); } // 正确 async function f(){ let a = [{}, {}, {}]; for(let k of a){ await post(k); } }
3.2 Promise.prototype.finally()
finally()
是ES8中Promise添加的一个新标准,用于指定不管Promise对象最后状态(是fulfilled
还是rejected
)如何,都会执行此操作,并且finally
方法必须写在最后面,即在then
和catch
方法后面。
// 写法如下: promise .then(res => {...}) .catch(err => {...}) .finally(() => {...})
finally
方法常用在处理Promise请求后关闭服务器连接:
server.listen(port) .then(() => {..}) .finally(server.stop);
本质上,finally方法是then方法的特例:
promise.finally(() => {...}); // 等同于 promise.then( result => { // ... return result }, error => { // ... throw error } )
3.3 Object.values(),Object.entries()
ES7中新增加的 Object.values()
和Object.entries()
与之前的Object.keys()
类似,返回数组类型。
回顾下Object.keys()
:
var a = { f1: 'hi', f2: 'leo'}; Object.keys(a); // ['f1', 'f2']
3.3.1 Object.values()
返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历属性的键值。
let a = { f1: 'hi', f2: 'leo'}; Object.values(a); // ['hi', 'leo']
如果参数不是对象,则返回空数组:
Object.values(10); // [] Object.values(true); // []
3.3.2 Object.entries()
返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历属性的键值对数组。
let a = { f1: 'hi', f2: 'leo'}; Object.entries(a); // [['f1','hi'], ['f2', 'leo']]
- 用途1:
遍历对象属性。
let a = { f1: 'hi', f2: 'leo'}; for (let [k, v] of Object.entries(a)){ console.log( `${JSON.stringfy(k)}:${JSON.stringfy(v)}` ) } // 'f1':'hi' // 'f2':'leo'
- 用途2: 将对象转为真正的Map结构。
let a = { f1: 'hi', f2: 'leo'}; let map = new Map(Object.entries(a));
手动实现Object.entries()
方法:
// Generator函数实现: function* entries(obj){ for (let k of Object.keys(obj)){ yield [k ,obj[k]]; } } // 非Generator函数实现: function entries (obj){ let arr = []; for(let k of Object.keys(obj)){ arr.push([k, obj[k]]); } return arr; }
3.4 Object.getOwnPropertyDescriptors()
之前有Object.getOwnPropertyDescriptor
方法会返回某个对象属性的描述对象,新增的Object.getOwnPropertyDescriptors()
方法,返回指定对象所有自身属性(非继承属性)的描述对象,所有原对象的属性名都是该对象的属性名,对应的属性值就是该属性的描述对象
let a = { a1:1, get f1(){ return 100} } Object.getOwnPropetyDescriptors(a); /* { a:{ configurable:true, enumerable:true, value:1, writeable:true} f1:{ configurable:true, enumerable:true, get:f, set:undefined} } */
实现原理:
function getOwnPropertyDescriptors(obj) { const result = {}; for (let key of Reflect.ownKeys(obj)) { result[key] = Object.getOwnPropertyDescriptor(obj, key); } return result; }
引入这个方法,主要是为了解决Object.assign()
无法正确拷贝get
属性和set
属性的问题。
let a = { set f(v){ console.log(v) } } let b = {}; Object.assign(b, a); Object.a(b, 'f'); /* f = { configurable: true, enumable: true, value: undefined, writeable: true } */
value
为undefined
是因为Object.assign
方法不会拷贝其中的get
和set
方法,使用getOwnPropertyDescriptors
配合Object.defineProperties
方法来实现正确的拷贝:
let a = { set f(v){ console.log(v) } } let b = {}; Object.defineProperties(b, Object.getOwnPropertyDescriptors(a)); Object.getOwnPropertyDescriptor(b, 'f') /* configurable: true, enumable: true, get: undefined, set: function(){...} */
Object.getOwnPropertyDescriptors
方法的配合Object.create
方法,将对象属性克隆到一个新对象,实现浅拷贝。
const clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj)); // 或者 const shallowClone = (obj) => Object.create( Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj) );
3.5 字符串填充 padStart和padEnd
用来为字符串填充特定字符串,并且都有两个参数:字符串目标长度和填充字段,第二个参数可选,默认空格。
'es8'.padStart(2); // 'es8' 'es8'.padStart(5); // ' es8' 'es8'.padStart(6, 'woof'); // 'wooes8' 'es8'.padStart(14, 'wow'); // 'wowwowwowwoes8' 'es8'.padStart(7, '0'); // '0000es8' 'es8'.padEnd(2); // 'es8' 'es8'.padEnd(5); // 'es8 ' 'es8'.padEnd(6, 'woof'); // 'es8woo' 'es8'.padEnd(14, 'wow'); // 'es8wowwowwowwo' 'es8'.padEnd(7, '6'); // 'es86666'
从上面结果来看,填充函数只有在字符长度小于目标长度时才有效,若字符长度已经等于或小于目标长度时,填充字符不会起作用,而且目标长度如果小于字符串本身长度时,字符串也不会做截断处理,只会原样输出。
3.6 函数参数列表与调用中的尾部逗号
该特性允许我们在定义或者调用函数时添加尾部逗号而不报错:
function es8(var1, var2, var3,) { // ... } es8(10, 20, 30,);
3.7 共享内存与原子操作
当内存被共享时,多个线程可以并发读、写内存中相同的数据。原子操作可以确保那些被读、写的值都是可预期的,即新的事务是在旧的事务结束之后启动的,旧的事务在结束之前并不会被中断。这部分主要介绍了 ES8 中新的构造函数 SharedArrayBuffer
以及拥有许多静态方法的命名空间对象 Atomic
。
Atomic
对象类似于 Math
对象,拥有许多静态方法,所以我们不能把它当做构造函数。 Atomic
对象有如下常用的静态方法:
- add /sub :为某个指定的value值在某个特定的位置增加或者减去某个值
- and / or /xor :进行位操作
- load :获取特定位置的值
4. ES9
4.1 对象的拓展运算符
4.1.1 介绍
对象的拓展运算符,即对象的Rest/Spread属性,可将对象解构赋值用于从一个对象取值,搜键值对分配到指定对象上,与数组的拓展运算符类似:
let {x, y, ...z} = {x:1, y:2, a:3, b:4}; x; // 1 y; // 2 z; // {a:3, b:4}
对象的解构赋值要求等号右边必须是个对象,所以如果等号右边是undefined
或null
就会报错无法转成对象。
let {a, ...b} = null; // 运行时报错 let {a, ...b} = undefined; // 运行时报错
解构赋值必须是最后一个参数,否则报错。
let {...a, b, c} = obj; // 语法错误 let {a, ...b, c} = obj; // 语法错误
注意:
- 1.解构赋值是浅拷贝。
let a = {a1: {a2: 'leo'}}; let {...b} = a; a.a1.a2 = 'leo'; b.a1.a2 = 'leo';
- 2.拓展运算符的解构赋值,不能复制继承自原型对象的属性。
let o1 = { a: 1 }; let o2 = { b: 2 }; o2.__proto__ = o1; let { ...o3 } = o2; o3; // { b: 2 } o3.a; // undefined
4.1.2 使用场景
- 1.取出参数对象所有可遍历属性,拷贝到当前对象中。
let a = { a1:1, a2:2 }; let b = { ...a }; b; // { a1:1, a2:2 } // 类似Object.assign方法
- 2.合并两个对象。
let a = { a1:1, a2:2 }; let b = { b1:11, b2:22 }; let ab = { ...a, ...b }; // {a1: 1, a2: 2, b1: 11, b2: 22} // 等同于 let ab = Object.assign({}, a, b);
- 3.将自定义属性放在拓展运算符后面,覆盖对象原有的同名属性。
let a = { a1:1, a2:2, a3:3 }; let r = { ...a, a3:666 }; // r {a1: 1, a2: 2, a3: 666} // 等同于 let r = { ...a, ...{ a3:666 }}; // r {a1: 1, a2: 2, a3: 666} // 等同于 let r = Object.assign({}, a, { a3:666 }); // r {a1: 1, a2: 2, a3: 666}
- 4.将自定义属性放在拓展运算符前面,就会成为设置新对象的默认值。
let a = { a1:1, a2:2 }; let r = { a3:666, ...a }; // r {a3: 666, a1: 1, a2: 2} // 等同于 let r = Object.assign({}, {a3:666}, a); // r {a3: 666, a1: 1, a2: 2} // 等同于 let r = Object.assign({a3:666}, a); // r {a3: 666, a1: 1, a2: 2}
- 5.拓展运算符后面可以使用表达式。
let a = { ...(x>1? {a:!:{}), b:2 }
- 6.拓展运算符后面如果是个空对象,则没有任何效果。
{...{}, a:1}; // {a:1}
- 7.若参数是
null
或undefined
则忽略且不报错。
let a = { ...null, ...undefined }; // 不报错
- 8.若有取值函数
get
则会执行。
// 不会打印 因为f属性只是定义 而不没执行 let a = { ...a1, get f(){console.log(1)} } // 会打印 因为f执行了 let a = { ...a1, ...{ get f(){console.log(1)} } }
4.2 正则表达式 s 修饰符
在正则表达式中,点(.
)可以表示任意单个字符,除了两个:用u
修饰符解决四个字节的UTF-16字符,另一个是行终止符。
终止符即表示一行的结束,如下四个字符属于“行终止符”:
- U+000A 换行符(\n)
- U+000D 回车符(\r)
- U+2028 行分隔符(line separator)
- U+2029 段分隔符(paragraph separator)
/foo.bar/.test('foo\nbar') // false
上面代码中,因为.
不匹配\n
,所以正则表达式返回false
。
换个醒,可以匹配任意单个字符:
/foo[^]bar/.test('foo\nbar') // true
ES9引入s
修饰符,使得.
可以匹配任意单个字符:
/foo.bar/s.test('foo\nbar') // true
这被称为dotAll
模式,即点(dot
)代表一切字符。所以,正则表达式还引入了一个dotAll
属性,返回一个布尔值,表示该正则表达式是否处在dotAll
模式。
const re = /foo.bar/s; // 另一种写法 // const re = new RegExp('foo.bar', 's'); re.test('foo\nbar') // true re.dotAll // true re.flags // 's'
/s
修饰符和多行修饰符/m
不冲突,两者一起使用的情况下,.
匹配所有字符,而^
和$
匹配每一行的行首和行尾。
4.3 异步遍历器
在前面ES6章节中,介绍了Iterator接口,而ES6引入了“异步遍历器”,是为异步操作提供原生的遍历器接口,即value
和done
这两个属性都是异步产生的。
4.3.1 异步遍历的接口
通过调用遍历器的next
方法,返回一个Promise对象。
a.next().then( ({value, done}) => { //... } )
上述a
为异步遍历器,调用next
后返回一个Promise对象,再调用then
方法就可以指定Promise对象状态变为resolve
后执行的回调函数,参数为value
和done
两个属性的对象,与同步遍历器一致。
与同步遍历器一样,异步遍历器接口也是部署在Symbol.asyncIterator
属性上,只要有这个属性,就都可以异步遍历。
let a = createAsyncIterable(['a', 'b']); //createAsyncIterable方法用于构建一个iterator接口 let b = a[Symbol.asyncInterator](); b.next().then( result1 => { console.log(result1); // {value: 'a', done:false} return b.next(); }).then( result2 => { console.log(result2); // {value: 'b', done:false} return b.next(); }).then( result3 => { console.log(result3); // {value: undefined, done:true} })
另外next
方法返回的是一个Promise对象,所以可以放在await
命令后。
async function f(){ let a = createAsyncIterable(['a', 'b']); let b = a[Symbol.asyncInterator](); console.log(await b.next());// {value: 'a', done:false} console.log(await b.next());// {value: 'b', done:false} console.log(await b.next());// {value: undefined, done:true} }
还有一种情况,使用Promise.all
方法,将所有的next
按顺序连续调用:
let a = createAsyncIterable(['a', 'b']); let b = a[Symbol.asyncInterator](); let {{value:v1}, {value:v2}} = await Promise.all([ b.next(), b.next() ])
也可以一次调用所有next
方法,再用await
最后一步操作。
async function f(){ let write = openFile('aaa.txt'); write.next('hi'); write.next('leo'); await write.return(); } f();
4.3.2 for await...of
for...of
用于遍历同步的Iterator接口,而ES8引入for await...of
遍历异步的Iterator接口。
async function f(){ for await(let a of createAsyncIterable(['a', 'b'])) { console.log(x); } } // a // b
上面代码,createAsyncIterable()
返回一个拥有异步遍历器接口的对象,for...of
自动调用这个对象的next
方法,得到一个Promise对象,await
用来处理这个Promise,一但resolve
就把得到的值x
传到for...of
里面。
用途
直接把部署了asyncIteable操作的异步接口放入这个循环。
let a = ''; async function f(){ for await (let b of req) { a += b; } let c = JSON.parse(a); console.log('leo', c); }
当next
返回的Promise对象被reject
,for await...of
就会保错,用try...catch
捕获。
async function f(){ try{ for await (let a of iterableObj()){ console.log(a); } }catch(e){ console.error(e); } }
注意,for await...of
循环也可以用于同步遍历器。
(async function () { for await (let a of ['a', 'b']) { console.log(a); } })(); // a // b
4.3.3 异步Generator函数
就像 Generator 函数返回一个同步遍历器对象一样,异步 Generator 函数的作用,是返回一个异步遍历器对象。
在语法上,异步 Generator 函数就是async
函数与 Generator 函数的结合。
async function* f() { yield 'hi'; } const a = f(); a.next().then(x => console.log(x)); // { value: 'hello', done: false }
设计异步遍历器的目的之一,就是为了让Generator函数能用同一套接口处理同步和异步操作。
// 同步Generator函数 function * f(iterable, fun){ let a = iterabl[Symbol.iterator](); while(true){ let {val, done} = a.next(); if(done) break; yield fun(val); } } // 异步Generator函数 async function * f(iterable, fun){ let a = iterabl[Symbol.iterator](); while(true){ let {val, done} = await a.next(); if(done) break; yield fun(val); } }
同步和异步Generator函数相同点:在yield
时用next
方法停下,将后面表达式的值作为next()
返回对象的value
。
在异步Generator函数中,同时使用await
和yield
,简单样理解,await
命令用于将外部操作产生的值输入函数内部,yield
命令用于将函数内部的值输出。
(async function () { for await (const line of readLines(filePath)) { console.log(line); } })()
异步 Generator 函数可以与for await...of
循环结合起来使用。
async function* f(asyncIterable) { for await (const line of asyncIterable) { yield '> ' + line; } }
4.3.4 yield* 语句
yield*
语句跟一个异步遍历器。
async function * f(){ yield 'a'; yield 'b'; return 'leo'; } async function * g(){ const a = yield* f(); // a => 'leo' }
与同步 Generator 函数一样,for await...of
循环会展开yield*
。
(async function () { for await (const x of gen2()) { console.log(x); } })(); // a // b
5. 知识补充
5.1 块级作用域
通常指一个函数内部,或者一个代码块内部。
比如:
function fun1 () { // 块级作用域 if (true) { // 块级作用域 } }
缺点: 1.导致内层变量覆盖外层变量
var a1 = new Date(); function f1 (){ console.log(a1); // undefined if (false) { var a1 = 'hello' } }
输出 undefined
是因为 if
内的 a1
变量声明的变量提升,导致内部的 a1
覆盖外部的 a1
,所以输出为 undefined
。
2.变量的全局污染
var a = 'hello'; for (var i = 0; i< a.length; i++) { //... } console.log(i); // 5
循环结束后,变量 i
的值依然存在,造成变量的全局污染。
5.2 ES5/6对数组空位的处理
数组的空位不是undefined
,而是没有任何值,数组的undefined
也是有值。
0 in [undefined,undefined,undefined] // true 0 in [,,,] // false
ES5对空位的处理:
forEach()
,filter()
,reduce()
,every()
和some()
都会跳过空位。map()
会跳过空位,但会保留这个值。join()
和toString()
会将空位视为undefined
,而undefined
和null
会被处理成空字符串。
[,'a'].forEach((x,i) => console.log(i)); // 1 ['a',,'b'].filter(x => true); // ['a','b'] [,'a'].every(x => x==='a'); // true [1,,2].reduce((x,y) => x+y); // 3 [,'a'].some(x => x !== 'a'); // false [,'a'].map(x => 1); // [,1] [,'a',undefined,null].join('#'); // "#a##" [,'a',undefined,null].toString(); // ",a,,"
ES6对空位的处理:
将空位视为正常值,转成undefined
。
Array.from(['a',,'b']);// [ "a", undefined, "b" ] [...['a',,'b']]; // [ "a", undefined, "b" ] //copyWithin() 会连空位一起拷贝。 [,'a','b',,].copyWithin(2,0) // [,"a",,"a"] //fill()会将空位视为正常的数组位置。 new Array(3).fill('a') // ["a","a","a"] //for...of循环也会遍历空位。 let arr = [, ,]; for (let i of arr) { console.log(1); } // 1 1
entries()
、keys()
、values()
、find()
和findIndex()
会将空位处理成undefined
。
[...[,'a'].entries()] // [[0,undefined], [1,"a"]] [...[,'a'].keys()] // [0,1] [...[,'a'].values()] // [undefined,"a"] [,'a'].find(x => true) // undefined [,'a'].findIndex(x => true) // 0
由于空位的处理规则非常不统一,所以建议避免出现空位。