ECMAScript6 __学习笔记

ECMAScript6

资料来源

6可以转为5.1,babel转码器可以完成。traceur转码器也可以

let , const

let的特性如下:

  • let命令生成的变量,只在let所在代码块有作用。
  • for循环的特别之处,循环变量的部分是一个父作用域,循环体内部是一个单独的子作用域。
  • let不能变量提升,
  • let会绑定块作用域,不受外部影响(暂时性死区)
  • 不允许重复声明
  • ES6中对块作用域的概念进行了强调,几乎禁止了变量提升,只有在定义了变量之后才能使用。声明函数必须要在大括号中,否则报错

const的特性如下:

  • 与let基本特性相同,特点是声明一个只读变量。
  • const本质上是保证变量指向的那个内存地址所保存的数据不得改动。

如果是简单类型(数值,字符串,布尔值),值就保存在变量指向的地址中,因此不能改变。

但如果是对象,数组,那么固定的只是一个指针,里面的值是可以变化的。当然所固定的指针是不能指向别的地方的。

如果真的想将对象冻结,应该使用Object.freeze方法。

const foo = Object.freeze({
    
    });

// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;

上面代码中,常量foo指向一个冻结的对象,所以添加新属性不起作用,严格模式时还会报错。

除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。

var constantize = (obj) => {
    
    
  Object.freeze(obj);
  Object.keys(obj).forEach( (key, i) => {
    
    
    if ( typeof obj[key] === 'object' ) {
    
    
      constantize( obj[key] );
    }
  });
};

ES6声明变量的六种方法

var , function , let , const , import , class

顶层对象

在浏览器中是window,在Node 中是 global

var 声明全局变量时的还是属于顶层对象,let声明时,就变成不再属于顶层对象了。

统一顶层对象,用global,但还在提议,需要用时可以查看此处。

变量的解构赋值

解构:从数组和对象中取值,对变量赋值

数组:

  • 基本用法:两边解构一样,就可以直接赋值。(有Iterator接口,都可以采用数组形式解构赋值。)
  • 默认值:解构赋值允许有默认值。undefined时,默认值(可以是函数)生效,

对象:

  • 用法:变量与属性相同则能赋值。
  • 好处:可以将现有的方法赋值到某个变量里,使用起来很方便。
  • 特点:匹配模式:变量。
  • 默认值:同上
  • 注意: { 在行首的就是代码块,let就不起作用。

字符串:

  • 用法:相当于类似的数组对象。有length属性。

数值,布尔值:

  • 用法:先转化为对象。

函数参数:

  • 特点:可指定默认值,解构失败后采用默认值。

圆括号:

  • 不要在解构体内部加括号,真的要加时,要特别注意

用途:

  • 交换变量值
  • 返回多个值
  • 方便参数与变量名对应起来。
  • 提取JSON数据
  • 设置函数默认值
  • 遍历Map结构
  • 输入模块的指定方法

字符串的拓展

1.字符的unicode表示法

  • \uxxxx的形式表示一个字符
  • 大于FFFF的,需要加大括号。

2.codePointAt()

  • charCodeAt方法无法识别4个字节的字符,需要用codePointAt
  • 字符转化为编码

3.String.fromCodePoint()

  • fromCharCode这个方法不能识别4个字节的字符,fromCodePoint可以
  • 编码转换为字符

4.遍历器接口

for … of,优点是可以识别4个字节的码点。

for (let codePoint of 'foo') {
    
    
  console.log(codePoint)
}
// "f"
// "o"
// "o"

5.normalize()

解决语义相同,编码方式不一致导致的问题。

6.includes() , startsWith() , endsWith()

用于判断一个字符串是否包含另一个字符串,以前只有indexOf()

7.repeart()

表示一个字符的重复次数

8.padStart() , padEnd()

补全字符串

9.matchAll()

返回正则表达式对当前字符串的所有匹配

10.模板字符串

反引号 ` 可以多行输出字符串,.trim()方法可以消除换行。

如果字符串中有变量,就把变量写在&{}之中

11.标签模板

非常牛逼,可应对各种转换,各种交叉应用,需要时翻看。

12.String.raw()

模板字符串的处理函数。

总结:模板字符串里面有很多规则,需要花更多时间研究。

正则拓展

1.RegExp构造函数

ES5对于有flag(修饰符)的正则,用RegExp()不能更改,ES6可以,处理方式是忽略第一次的。

2.字符串的正则方法

match() , replace() , search() , split() ,ES5中定义在String,ES6定义在RegExp上。

3.u修饰符

字符拓展。

4.unicode属性

判断表达式中是否使用了u修饰符

5.y修饰符

y→sticky→"粘连"修饰符

与g,global类似,但前后必须粘连才可以识别

sticky属性判断正则是否用来y修饰符

6.flags属性

source返回正则正文,flags返回修饰符

7.s修饰符dotAll

解决.无法匹配行终止符的问题。dotAll属性就是判断是否用s修饰符

8.支持后行断言

9.unicode属性类

可以匹配某种属性的所有字符,就是系统把部分字符定义为一种属性,非常的方便使用。(ES2018)

10.具名组匹配

用圆括号进行组匹配,由此方法也可以应用到解构赋值和替换中。

11.matchAll()

返回一个遍历器,可以一次过找到所有匹配结果,比数组的好处在于,当匹配结果是一个很大的数组,那么遍历器比较节省资源。

数值拓展

1.二进制与八进制

要用0b 和0o 开头来表示。转为10进制要用Number。

2.isFinite() , isNaN()

判断是否有限,和是否为NaN

3.parseInt(), parseFloat()

移植到Number对象上。

4.isInteger()

判断是否为整数。如果对精度要求太高,不适合使用。

5.EPSILON

最小常量,小于该数,均无意义。

6.安全整数,isSafeInteger()

JavaScript 能够准确表示的整数范围在-2^532^53之间(不含两个端点),超过这个范围,无法精确表示这个值。

isSafeInteger()用来判断数是否落在这个范围。

7.Math对象的扩展

trunc() , sign() , cbrt() , clz32() , imul() , fround() , hypot() , expm1() , log1p() , log10() , log2() .

sinh() , cosh() , tanh() , asinh() , acosh() , atanh()

指数运算符:** ;

函数拓展

1.可以设定默认值,用解构赋值与默认值结合,undefined才能触发默认值。

2.length属性

返回函数中没有指定默认值的参数个数

注意:如果设置默认值不是尾参数,length属性就不再计算后面的参数了。

3.作用域

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域。等初始化结束,这个作用域就会消失。如果没有设置默认值,是不会出现的。

4.rest

…变量名,可以把输入的参数转化为数组。

5.严格模式

ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。

6.name属性,一直支持,有些修改

7.箭头函数

var f = v => v;

// 等同于
var f = function (v) {
    
    
  return v;
};

var f = () => 5;
// 等同于
var f = function () {
    
     return 5 };

var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
    
    
  return num1 + num2;
};

不可以用new, 不可以用arguments,不能用yield,this是固定的,指向定义时指向的对象。总之,不会用,就不要用。

嵌套箭头函数,可以改写多重嵌套的函数。

双冒号运算符

为了实现函数绑定,绑定后,后面函数的this指向前面的对象,

foo::bar;
// 等同于
bar.bind(foo);

foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);

const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
    
    
  return obj::hasOwnProperty(key);
}

8.尾调用优化

Tail Call ,函数执行到最后一句,调用别的函数。

如果是尾调用,就不需要保留调用者,内存直接释放,提高性能。

尾递归,特别的,尾递归可以防止栈溢出。

注意:尾调用优化只在严格模式下可以执行

数组的扩展

1.扩展运算符

... spread 扩展运算符 , 必须放在函数的调用参数中,一定是被函数的括号包围

替代apply

// ES5 的写法
function f(x, y, z) {
    
    
  // ...
}
var args = [0, 1, 2];
f.apply(null, args);

// ES6的写法
function f(x, y, z) {
    
    
  // ...
}
let args = [0, 1, 2];
f(...args);

其他应用:

  • 复制数组
  • 合并数组
  • 与解构赋值结合,用于生成数组
  • 将字符串转化为真正的数组
  • 实现了与遍历器的接口的对象,可以通过…转化为真正的数组
  • 只要有遍历器的对象都可以使用... ,例如:Map,Set ,Generator函数

2.Array.from()

Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。

3.Aarry.of()

Array.of方法用于将一组值,转换为数组。

4.copyWithin()

数组实例的copyWithin方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。

5.find(),findIndex()

数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined

数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1

6.fill()

fill方法使用给定值,填充一个数组。

7.数组实例的entries() , keys() , values()

keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历

8.flat() ,flatMap()

数组的成员有时还是数组,Array.prototype.flat()用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。

flatMap()方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。

9.数组的空位

ES5对空位定义不明确,ES6中,明确转为undefined

对象扩展

1.属性的简洁表示法

const foo = 'bar';
const baz = {
    
    foo};
baz // {foo: "bar"}

// 等同于
const baz = {
    
    foo: foo};
//就是说,{foo}蕴含着{foo:foo}。不会就先不这么写,有需要时可以用一下。

2.属性名表达式

支持[] 表示属性名,[]里面的内容要先进行运算

let lastWord = 'last word';

const a = {
    
    
  'first word': 'hello',
  [lastWord]: 'world'
};

a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"

3.方法的name属性

注意:getter , setter ,取值,存值函数在对象中定义函数时,不可以直接用name属性

const obj = {
    
    
  get foo() {
    
    },
  set foo(x) {
    
    }
};

obj.foo.name
// TypeError: Cannot read property 'name' of undefined

const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');

descriptor.get.name // "get foo"
descriptor.set.name // "set foo"

4.属性的可枚举性和遍历

可枚举性

对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。

目前,有四个操作会忽略enumerablefalse的属性。

  • for...in循环:只遍历对象自身的和继承的可枚举的属性。
  • Object.keys():返回对象自身的所有可枚举的属性的键名。
  • JSON.stringify():只串行化对象自身的可枚举的属性。
  • Object.assign(): 忽略enumerablefalse的属性,只拷贝对象自身的可枚举的属性。

属性的遍历

  • for…in
  • Object.keys(obj)
  • Object.getOwnPropertyNames(obj)
  • Object.getOwnPropertySymbols(obj)
  • Reflect.ownKeys(obj)

5.super关键字

this关键字总是指向函数所在的当前对象,super指向当前对象的原型对象。

const proto = {
    
    
  foo: 'hello'
};

const obj = {
    
    
  foo: 'world',
  find() {
    
    
    return super.foo;
  }
};

Object.setPrototypeOf(obj, proto);
obj.find() // "hello"

对象新增方法

1.Object.is()

===基本相同,不同之处只有两个:一是+0不等于-0,二是NaN等于自身。

+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

2.Object.assign()

合并对象

const target = {
    
     a: 1 };

const source1 = {
    
     b: 2 };
const source2 = {
    
     c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

注意点:

  • 浅拷贝,只传指针
  • 同名属性替换
  • 会把数组误认为对象。
  • 对于取值函数,会先取值,再复制

常见用途:

  • 为对象添加属性
  • 为对象添加方法
  • clone对象
  • 合并多个对象
  • 为属性指定默认值

3.Object.getOwnPropertyDescriptors()

返回指定对象所有自身属性(非继承属性)的描述对象。主要是解决assign无法正确拷贝get,set的问题

4.__proto__属性,Object.setPrototypeOf() , Object.getPrototypeOf()

__prote__不要使用,用后面两个替代。

5.keys() , values() , entries()

遍历的方法,可以遍历key , value , 键值对-entries, for…of…

6.Object.formEntries()

可以将键值对数组转化为对象。

Symbol

符号,标记的意思。

是第七种数据类型,主要解决属性名冲突的问题,前面6种是:undefined,null,String,Number,boolean,Object.

必须放在方括号中。

还有一些其他特性,用到时再关注。

Set和Map数据结构

基本用法

set结构不会添加重复元素

const s = new Set();

[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));

for (let i of s) {
    
    
  console.log(i);
}
// 2 3 5 4


// 去除数组的重复成员
[...new Set(array)]

插入两个对象时,总是认为两个对象不相等,但NaN总是相等的。

Set实例的属性和方法

属性:

Set.prototype.constructor :构造函数

set.prototype.size : set实例的成员总数

方法:

分两种,操作方法,遍历方法

操作方法:

  • add
  • delete
  • has
  • clear

遍历方法:

  • keys
  • values
  • entries
  • forEach

WeakSet

可以保证一个实例只在一个类中被使用,不会出现内存泄露的问题。

MAP

JS中的对象,本质上是键值对的集合。但传统上,只能是字符串,Map数据结构可以让其他各种类型的值当做键。

Object -> 提供“字符串:值”的对应。

Map -> 提供”值:值“的对应。

Map实际上是跟内存绑定的。NaN在Map中是同一个键,因为指向同一个地方,跟内存绑定。

Map与JSON,array , object之间的转换方法。

WeakMap

只能用对象作为键,是一种弱引用,当指向的对象销毁后也会自动销毁。

proxy

翻译为代理。相当于架设一层“拦截”,外面的人要访问里面的东西,都需要经过这个代理proxy才可以。

var proxy = new Proxy({
    
    }, {
    
    
  get: function(target, property) {
    
    
    return 35;
  }
});

proxy.time // 35
proxy.name // 35
proxy.title // 35

proxy代理了target,通过proxy实例指向target,property是代理的“过滤器”,可以重写target中的一些方法,上面就是重写get,每次执行.运算时,都返回35,不在执行target中的点运算,但如果“过滤器”是一个空对象,那么就会直接访问target,proxy相当于一个指向target的指针,是target的一个引用。

Reflect

reflect 反应,体现的意思。

从reflect对象上可以拿到语言内部的方法。未来的新方法将只部署在reflect对象上。

reflect对象的方法和proxy对象的方法一一对应,在proxy中可以很方便的使用reflect对象的方法去执行默认行为。

有了这个对象,会让代码变得更加容易懂,原有的一些不方便的写法都会变得方便一些。

Promose对象

异步编程的一种解决方案。避免了层层嵌套。可以将异步操作以同步操作的流程表达出来。

缺点:一旦新建立即执行,中途无法取消,内部错误不会反应到外部,在pending状态时,无法得知目前进展到哪一个阶段。

如果某些事件不断地反复的发生,一般来说,使用Stream模式是比promise更好的选择。

//promise实例
const promise = new Promise(function(resolve, reject) {
    
    
  // ... some code,写你异步操作的事情。
    
//resolve和reject函数是系统自带的,无需自己部署,
  if (/* 异步操作成功 */){
    
    
    resolve(value);//将promise对象的status,pending->resolved
  } else {
    
    
    reject(error);//将promise对象的status,pending->rejected
  }
});

//then方法
//接收两个回调函数作为参数,参数1:当promise对象状态变为resolved时调用。参数2(可选):当promise对象状态变为rejected的时候调用。
//两个函数都接受Promise对象传出的值作为参数。
promise.then(function(value) {
    
    
  // success
}, function(error) {
    
    
  // failure
});

//promise新建后立刻执行。
let promise = new Promise(function(resolve, reject) {
    
    
  console.log('Promise');
  resolve();
});

promise.then(function() {
    
    
  console.log('resolved.');
});

console.log('Hi!');

// Promise
// Hi!
// resolved

//可以看出,promise新建后,立刻执行,then方法执行会在同步任务执行完毕后才会执行。

//promise对象与then函数的关系。
/*
	在promise对象中,resolve和reject函数可以传递参数,这个参数就是then中的两个回调函数中的参数。
	
	比较特殊的是,当这个传递的参数是另外一个promise对象时,另外一个Promise对象的状态会覆盖掉当前promsie对象的状态。下面举例说明。
*/
const p1 = new Promise(function (resolve, reject) {
    
    
  setTimeout(() => reject(new Error('fail')), 3000)
})

const p2 = new Promise(function (resolve, reject) {
    
    
  setTimeout(() => resolve(p1), 1000)
})

p2
  .then(result => console.log(result))
  .catch(error => console.log(error))
// Error: fail

//p1是一个promise,3秒之后变成rejected,p2在1秒之后resolve。由于resolve返回的方法是p1,所以p2自己的状态信息无效了,取而代之的是p1的状态,当p2执行then时,实际上,是针对p1的then。

//一般来说,调用resolve或reject以后,promise的使命就完成了,后续的操作放在then中执行。不应写在resolve和reject后面。为了避免这种问题的出现,最好在前面加return.

Promise.prototype.then()

then返回一个新的promise实例,因此可以用链式写法,then会监听promise对象的状态变化情况,变化了才会发生调用。

getJSON("/post/1.json").then(function(post) {
    
    
  return getJSON(post.commentURL);
}).then(function funcA(comments) {
    
    
  console.log("resolved: ", comments);
}, function funcB(err){
    
    
  console.log("rejected: ", err);
});
//第一个then方法指定的回调函数,返回的是另外一个promise对象,第二个then等待这个新的promise对象状态发生变化,如果变为resolved,就调用funcA,如果变为rejected,调用funcB


可以用.then方法使异步操作有序进行。

Promise.prototype.catch()

.catch()是.then(null,rejection)或.then(undefined,rejection)的别名,只是一种写法而已。

reject方法的作用,等同于抛出错误。

值得注意的是,一旦Promise的状态变成了resolved,再抛出错误就是无效的。

promise 对象的错误具有“冒泡”性质,会一直传递到下一个catch语句。(这里我的理解是,其实then中执行了两个异步任务,第一个是resolve,返回一个Promise对象,第二个是reject,同样也返回一个promise,而且这个状态一直为rejected,所以就会跳过中间的所有then中的resolve处理函数,所以会传递到catch中。)

一般定义then中的rejected状态处理函数,一般用catch,可以任务catch就是一个语法糖,可以让代码语义更清晰。

promise内部错误不会影响到promise外部的代码,通俗的说法是“promise会吃掉错误”。下面是一个例子

const someAsyncThing = function() {
    
    
  return new Promise(function(resolve, reject) {
    
    
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);
  });
};

someAsyncThing().then(function() {
    
    
  console.log('everything is great');
});

setTimeout(() => {
    
     console.log(123) }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123

//上诉promise执行失败,但是不会影响123的输出。

这个脚本放在服务器执行,退出码就是0(即表示执行成功)。不过,Node 有一个unhandledRejection事件,专门监听未捕获的reject错误,上面的脚本会触发这个事件的监听函数,可以在监听函数里面抛出错误。

两个catch级联使用时,第二个catch会捕获第一个catch抛出的错误。

promise.prototype.finally()

不管Promise最后的状态如何,执行完then,catch之后,一定会执行finally。finally不接收任何参数,与promise的状态无关,可以认为是then的特例:

promise
.finally(() => {
    
    
  // 语句
});

// 等同于
promise
.then(
  result => {
    
    
    // 语句
    return result;
  },
  error => {
    
    
    // 语句
    throw error;
  }
);

//如果没有finally,就要把同样的语句写两遍。

//finally总是返回原来的值。

// resolve 的值是 undefined
Promise.resolve(2).then(() => {
    
    }, () => {
    
    })

// resolve 的值是 2
Promise.resolve(2).finally(() => {
    
    })

// reject 的值是 undefined
Promise.reject(3).then(() => {
    
    }, () => {
    
    })

// reject 的值是 3
Promise.reject(3).finally(() => {
    
    })

promise.all()

将多个Promise实例包装成一个新的promise实例。

const p = Promise.all([p1, p2, p3]);

//可理解为:p.status = p1.status && p2.status && p3.status;其中rejected=0,resolved=1.

p的状态由p1,p2,p3决定。

(1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()catch方法。

const p1 = new Promise((resolve, reject) => {
    
    
  resolve('hello');
})
.then(result => result)
.catch(e => e);

const p2 = new Promise((resolve, reject) => {
    
    
  throw new Error('报错了');
})
.then(result => result)
.catch(e => e);
//这里catch之后,会返回一个新的promise对象,然后新的promise执行完catch之后状态变为resolve,

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 报错了]

Promise.race()

race,竞赛的意思,all,全部的意思

与all类似,race也是将多个promise实例封装成一个新的promise实例,不同的是机制不一样。

p1,p2,p3只要有一个改变状态,p的状态就跟着改变。率先改变的promise实例的返回值,就传递给p的回调函数。

下面是一个例子,如果指定时间内没有获得结果,就将promise的状态变为reject。

const p = Promise.race([
  fetch('/resource-that-may-take-a-while'),
  new Promise(function (resolve, reject) {
    
    
    setTimeout(() => reject(new Error('request timeout')), 5000)
  })
]);

p
.then(console.log)
.catch(console.error);
//由于p1在5秒内没有状态改变,p2改变为rejected,因此p也跟随p2一起改变.调用.catch。

Promise.resolve()

将现有对象转化为Promise对象,就用resolve方法

Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))

Promise.resolve()方法的参数分4种情况:

  • 参数是个promise对象 -> do nothing , return itself.
  • 参数是一个thenable对象(具有then方法的对象) -> 对象转化为promise对象,立即执行then方法
  • 参数中没有then方法,或根本不是对象。 -> 返回一个promise对象,该对象的状态从一生成就是resolved,其参数会传递给回调函数。
  • 不带任何参数 -> 返回一个resolved状态的Promise对象。

Promise.reject()

返回一个状态为rejected 的promise对象。

const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))

p.then(null, function (s) {
    
    
  console.log(s)
});
// 出错了

注意,Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致。

const thenable = {
    
    
  then(resolve, reject) {
    
    
    reject('出错了');
  }
};

Promise.reject(thenable)
.catch(e => {
    
    
  console.log(e === thenable)
})
// true
//直接把错误丢出来,不会执行then方法。

上面代码中,Promise.reject方法的参数是一个thenable对象,执行以后,后面catch方法的参数不是reject抛出的“出错了”这个字符串,而是thenable对象。

应用

加载图片

const preloadImage = function (path) {
    
    
  return new Promise(function (resolve, reject) {
    
    
    const image = new Image();
    image.onload  = resolve;
    image.onerror = reject;
    image.src = path;
  });
};

generator与promise结合:

使用 Generator 函数管理流程,遇到异步操作的时候,通常返回一个Promise对象。

function getFoo () {
    
    //7
  return new Promise(function (resolve, reject){
    
    
    resolve('foo');
  });
}

const g = function* () {
    
    
  try {
    
    
    const foo = yield getFoo();//4
    console.log(foo);
  } catch (e) {
    
    
    console.log(e);
  }
};

function run (generator) {
    
    
  const it = generator();//2
//done属性是generator函数中的一个属性,如果运行到return或者到函数结束了,done就会变成true
  function go(result) {
    
    //5//8
    if (result.done) return result.value;

    return result.value.then(function (value) {
    
    
      return go(it.next(value));//6//9
    }, function (error) {
    
    
      return go(it.throw(error));
    });
  }

  go(it.next());//3
}

run(g);//1

//这里的意思是:generator函数每次yield一个promise对象。


上面代码的 Generator 函数g之中,有一个异步操作getFoo,它返回的就是一个Promise对象。函数run用来处理这个Promise对象,并调用下一个next方法。

Promise.try()

让同步函数同步执行,异步函数异步执行,并且让它们具有统一的 API 呢?回答是可以的,并且还有两种写法。第一种写法是用async函数来写。

const f = () => console.log('now');
(async () => f())();
console.log('next');
// now
// next

//f是同步的话,上面第二行会立即执行,f是异步的话,可以用.then指定下一步。
(async () => f())()
.then(...)
.catch(...)

第二种写法是使用new Promise()

const f = () => console.log('now');
(
  () => new Promise(
    resolve => resolve(f())
  )
)();
console.log('next');
// now
// next

try就是上诉写法的一种替代的语法糖,这只是个提案。

事实上,Promise.try就是模拟try代码块,就像promise.catch模拟的是catch代码块。

Iterator 和 for…of 循环

Iterator , 遍历器,主要解决多种集合形式的数据结构(object,array,map,set)带来的问题。只要部署了Iterator接口,就可以完成遍历操作。而与之配合的语句是for … of。

遍历过程:

创建一个指针,指向初始位置,使用对象的next方法,完成遍历,直至done

next调用会返回两个值,一个是value,当前值,一个是done,是否已完成遍历。

模拟next例子如下:

var it = makeIterator(['a', 'b']);

it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }

function makeIterator(array) {
    
    
  var nextIndex = 0;
  return {
    
    
    next: function() {
    
    
      return nextIndex < array.length ?
        {
    
    value: array[nextIndex++], done: false} :
        {
    
    value: undefined, done: true};
    }
  };
}

yield 读音/yi:ld / ,意思是,生产的意思。

generator函数的语法

generator 生成器的意思。

yield,生产的意思,每次执行.next,就会执行一下。

function* helloWorldGenerator() {
    
    
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

yield可以简单的认为是在一个函数里面可return多次的语句。

next中的参数问题:

这个函数可以设置一个参数,其作用是替代掉上一次yield的值。

function* foo(x) {
    
    
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }


//当执行foo(5).next()时,这个相当于return 5+1 
//再次.next()时,这时next没有输入参数,这时yield (x + 1)这个代码块就相当于undefined,算出来y就是NaN,y/3也是NaN,输出值就是NaN

//因此next函数的参数,就是替换掉上一次yield语句,如果没有参数则传入undefined。




throw() 抛出生成器中的内部错误,

return()给定返回值,终结遍历。

next(), throw(),return()三个函数都是用不同的值去替代yield表达式。

  • next()是将yield表达式替换成一个值。
  • throw()是将yield表达式替换成一个throw语句。
  • return()是将yield表达式替换成一个return语句。

yield*表达式,用于执行generator中的generator

generator函数的异步应用

解决回调地狱 -> promise -> 未能根本解决,因为只是一种新写法 -> generator函数

协程:多线程互相协作,完成异步任务。

协程有点像函数,又有点像线程。它的运行流程大致如下。

  • 第一步,协程A开始执行。
  • 第二步,协程A执行到一半,进入暂停,执行权转移到协程B
  • 第三步,(一段时间后)协程B交还执行权。
  • 第四步,协程A恢复执行。
function* asyncJob() {
    
    
  // ...其他代码
  var f = yield readFile(fileA);
  // ...其他代码
}

上面代码的函数asyncJob是一个协程,它的奥妙就在其中的yield命令。它表示执行到此处,执行权将交给其他协程。也就是说,yield命令是异步两个阶段的分界线。

协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。

协程与generator函数实现

generator是一个封装的异步任务,相当于一个异步任务容器。

指针对象的throw方法抛出的错误可以被generator中的try…catch代码块捕获。出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。

thunk函数可以把多参数函数,替换程一个只接收回调函数作为参数的单参数函数。

// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
    
    
  return function (callback) {
    
    
    return fs.readFile(fileName, callback);
  };
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);

thunkify模块

thunkify模块,可以完成generator的自动执行,因为thunkify在函数内部,可以保证函数执行外面的操作,然后回到函数本身中来,这非常适合generator函数的管理。

yield后面是thunk函数时,可以自动执行脚本。

thunk不是唯一的解决方案,要想自动控制generator,关键是怎么完成接收和交还程序的执行权的问题。回调函数可以,promise对象也可以。

co模块

var gen = function* () {
    
    
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

var co = require('co');
co(gen);

co模块用于generator函数的自动执行的。co函数返回的是一个promise对象。

co使用的条件是:yield后面只能是thunk,promise,所有成员都是promise的数组或对象。

co可支持并发

// 数组的写法
co(function* () {
    
    
  var res = yield [
    Promise.resolve(1),
    Promise.resolve(2)
  ];
  console.log(res);
}).catch(onerror);

// 对象的写法
co(function* () {
    
    
  var res = yield {
    
    
    1: Promise.resolve(1),
    2: Promise.resolve(2),
  };
  console.log(res);
}).catch(onerror);


//另外一个例子。
co(function* () {
    
    
  var values = [n1, n2, n3];
  yield values.map(somethingAsync);
});

function* somethingAsync(x) {
    
    
  // do something async
  return y
}

node中的stream

node提供stream模式读写数据,特点是一次只处理数据的一部分,数据分成一块块依次处理。

Stream模式使用EventEmitter API,会释放三个事件。

  • data事件:下一个数据块已经准备好。
  • end事件:整个“数据流”处理完了。
  • error事件:发生错误

使用race()判断三个事件哪一个最先发生,只有当data事件最先发生时,才进入下一个数据块。从而可以用一个while循环完成所有数据的读取。

const co = require('co');
const fs = require('fs');

const stream = fs.createReadStream('./les_miserables.txt');
let valjeanCount = 0;

co(function*() {
    
    
  while(true) {
    
    
    const res = yield Promise.race([
      new Promise(resolve => stream.once('data', resolve)),
      new Promise(resolve => stream.once('end', resolve)),
      new Promise((resolve, reject) => stream.once('error', reject))
    ]);
    if (!res) {
    
    
      break;
    }
    stream.removeAllListeners('data');
    stream.removeAllListeners('end');
    stream.removeAllListeners('error');
    valjeanCount += (res.toString().match(/valjean/ig) || []).length;
  }
  console.log('count:', valjeanCount); // count: 1120
});

async函数

是generator的语法糖。

内置执行器,把yield改成await,*改成async

async返回promise对象。await后面是一个promise对象。

这个函数解决的问题是上面一直谈到的自动执行,我们要用generator方法让函数自动执行,就必须用co模块或者thunkify模块这种第三方执行器,但ES6中,async函数自带了执行器。

await应该放在try…catch中。

下面的例子使用try...catch结构,实现多次重复尝试。

const superagent = require('superagent');
const NUM_RETRIES = 3;

async function test() {
    
    
  let i;
  for (i = 0; i < NUM_RETRIES; ++i) {
    
    
    try {
    
    
      await superagent.get('http://google.com/this-throws-an-error');
      break;//await执行不成功这句不会执行。
    } catch(err) {
    
    }
  }
  console.log(i); // 3
}

test();

await互不依赖可以同时触发。

第二点,多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。

let foo = await getFoo();
let bar = await getBar();

上面代码中,getFoogetBar是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo完成以后,才会执行getBar,完全可以让它们同时触发。

// 写法一,推荐写法,比较直观。
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;

await只能用在async函数中,用在普通函数中会报错。

如果希望多个请求并发执行,可以使用Promise.all方法。当三个请求都会resolved时,下面两种写法效果相同。

async function dbFuc(db) {
    
    
  let docs = [{
    
    }, {
    
    }, {
    
    }];
  let promises = docs.map((doc) => db.post(doc));

  let results = await Promise.all(promises);
  console.log(results);
}

// 或者使用下面的写法

async function dbFuc(db) {
    
    
  let docs = [{
    
    }, {
    
    }, {
    
    }];
  let promises = docs.map((doc) => db.post(doc));

  let results = [];
  for (let promise of promises) {
    
    
    results.push(await promise);
  }
  console.log(results);
}

esm模块支持顶层async函数。

async函数可以保留运行堆栈:

const a = () => {
    
    
  b().then(() => c());
};

上面代码中,函数a内部运行了一个异步任务b()。当b()运行的时候,函数a()不会中断,而是继续执行。等到b()运行结束,可能a()早就运行结束了,b()所在的上下文环境已经消失了。如果b()c()报错,错误堆栈将不包括a()

现在将这个例子改成async函数。

const a = async () => {
    
    
  await b();
  c();
};

上面代码中,b()运行的时候,a()是暂停执行,上下文环境都保存着。一旦b()c()报错,错误堆栈将包括a()

async实现原理

async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。

async并发读取,继发输出

async function logInOrder(urls) {
    
    
  for (const url of urls) {
    
    
    const response = await fetch(url);
    console.log(await response.text());
  }
}
//上面函数是一个一个url去读取,然后顺序打印。这样很慢。

//下面函数是同步一起读取,然后顺序打印。async函数只有内部是继发执行的,外部不受影响。
async function logInOrder(urls) {
    
    
  // 并发读取远程URL
  const textPromises = urls.map(async url => {
    
    
    const response = await fetch(url);
    return response.text();
  });

  // 按次序输出
  for (const textPromise of textPromises) {
    
    
    console.log(await textPromise);
  }
}

异步遍历器

异步遍历器的最大的语法特点,就是调用遍历器的next方法,返回的是一个 Promise 对象。

asyncIterator
  .next()
  .then(
    ({
     
      value, done }) => /* ... */
  );

就是用Promise作为中介,让遍历器next()后生成promise对象,在调用then返回{value,done},样子就跟同步时很像:

async function f() {
    
    
  const asyncIterable = createAsyncIterable(['a', 'b']);
  const asyncIterator = asyncIterable[Symbol.asyncIterator]();
  console.log(await asyncIterator.next());
  // { value: 'a', done: false }
  console.log(await asyncIterator.next());
  // { value: 'b', done: false }
  console.log(await asyncIterator.next());
  // { value: undefined, done: true }
}

可以用all方法并发执行。

const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
const [{
    
    value: v1}, {
    
    value: v2}] = await Promise.all([
  asyncIterator.next(), asyncIterator.next()
]);

console.log(v1, v2); // a b

for await…of

前面介绍过,for...of循环用于遍历同步的 Iterator 接口。新引入的for await...of循环,则是用于遍历异步的 Iterator 接口。

async function f() {
    
    
  for await (const x of createAsyncIterable(['a', 'b'])) {
    
    
    console.log(x);
  }
}
// a
// b

上面代码中,createAsyncIterable()返回一个拥有异步遍历器接口的对象,for...of循环自动调用这个对象的异步遍历器的next方法,会得到一个 Promise 对象。await用来处理这个 Promise 对象,一旦resolve,就把得到的值(x)传入for...of的循环体。

也就是说,of后面是一个带了异步遍历器的对象,x用来存await处理promise对象返回的值,传入循环体。

如果next返回的promise对象被rejected了。就会报错!可以用try…catch捕捉。

async function () {
    
    
  try {
    
    
    for await (const x of createRejectingIterable()) {
    
    
      console.log(x);
    }
  } catch (e) {
    
    
    console.error(e);
  }
}

Node v10 支持异步遍历器,Stream 就部署了这个接口。下面是读取文件的传统写法与异步遍历器写法的差异。

// 传统写法
function main(inputFilePath) {
    
    
  const readStream = fs.createReadStream(
    inputFilePath,
    {
    
     encoding: 'utf8', highWaterMark: 1024 }
  );
  readStream.on('data', (chunk) => {
    
    
    console.log('>>> '+chunk);
  });
  readStream.on('end', () => {
    
    
    console.log('### DONE ###');
  });
}

// 异步遍历器写法
async function main(inputFilePath) {
    
    
  const readStream = fs.createReadStream(
    inputFilePath,
    {
    
     encoding: 'utf8', highWaterMark: 1024 }
  );

  for await (const chunk of readStream) {
    
    
    console.log('>>> '+chunk);
  }
  console.log('### DONE ###');
}

异步generator

异步generator返回一个异步遍历器对象。

在语法上,异步 Generator 函数就是async函数与 Generator 函数的结合。

async function* gen() {
    
    
  yield 'hello';
}
const genObj = gen();
//.next就是去执行下一个yield语句,.then是把返回结果当做一个promise对象。
genObj.next().then(x => console.log(x));
// { value: 'hello', done: false }

异步遍历器的设计目的之一,就是 Generator 函数处理同步操作和异步操作时,能够使用同一套接口。

// 同步 Generator 函数
function* map(iterable, func) {
    
    
  const iter = iterable[Symbol.iterator]();
  while (true) {
    
    
    const {
    
    value, done} = iter.next();
    if (done) break;
    yield func(value);
  }
}

// 异步 Generator 函数
async function* map(iterable, func) {
    
    
  const iter = iterable[Symbol.asyncIterator]();
  while (true) {
    
    
    const {
    
    value, done} = await iter.next();
    if (done) break;
    yield func(value);
  }
}

上面代码中,map是一个 Generator 函数,第一个参数是可遍历对象iterable,第二个参数是一个回调函数funcmap的作用是将iterable每一步返回的值,使用func进行处理。上面有两个版本的map,前一个处理同步遍历器,后一个处理异步遍历器,可以看到两个版本的写法基本上是一致的。

异步 Generator 函数内部,能够同时使用awaityield命令。可以这样理解,await命令用于将外部操作产生的值输入函数内部,yield命令用于将函数内部的值输出。

普通的 async 函数返回的是一个 Promise 对象,而异步 Generator 函数返回的是一个异步 Iterator 对象

js中的函数总结:

普通函数:function name( args ){}

async函数:async function name(args){//await 关键字}

generator函数:function* name(args){//yield关键字}

异步generator:async function* name(args){//yield,await关键字}

对比项/函数 普通函数 async函数 generator函数 异步generator
特殊关键字 await yield await,yield
主要方法 .then .next .next , .then
主要解决问题 一般问题 异步函数顺序执行 协程,并发处理 相同数据结构异步操作。
英文意思 await是等待的意思,每次到这里都先等待上面的完成了再往下执行,所以显然接收的是一个promise对象 yield产出的意思,每次next都产出一个东西,next传入的参数就是替代yield语句的,因为产出了的东西就放在这个位置 await可以将外面函数产生的值传入到内部,yield可以输出函数内部的值。在异步generator函数中可以使用这两个功能。
返回值 返回指定类型 await返回promise对象 yield返回{value,done},value就是返回值,done是表示生成器是否已经跑完。 与前面

由上面所述,异步generator也可以用在同步的数据结构中,只是没有异步操作,所以没有await。

yield*

yield*语句也可以跟一个异步遍历器。

async function* gen1() {
    
    
  yield 'a';
  yield 'b';
  return 2;
}

async function* gen2() {
    
    
  // result 最终会等于 2
  const result = yield* gen1();
}

上面代码中,gen2函数里面的result变量,最后的值是2

与同步 Generator 函数一样,for await...of循环会展开yield*

(async function () {
    
    
  for await (const x of gen2()) {
    
    
    console.log(x);
  }
})();
// a
// b

class的基本语法

class是第一种语法糖,需要封装时,可以用。

当然也可以用构造函数就可以了。

class的继承

跟java中的继承类似。用super建立与父类的通讯。

module的语法

模块,用import关键字完成父子模型的依赖。

模块内部的所有变量,外部无法获取。必须适用export关键字输出该变量。

as可以重命名。

输入变量应该全部当做是只读,否则很难差错。

* as 别名的办法,可以把一个模块整体加载进来,再通过.运算符就可以正常访问加载进来的变量,函数等。

//写法1
import {
    
     area, circumference } from './circle';

console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));

//写法2
import * as circle from './circle';

console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

export default 就是指定默认输出,这个输出只有一个。import的时候,就不需要加大括号。

import()可以按需加载,条件加载。

直接import语句会运行前加载,就是会无视判断,加载文件时就加载。

module的加载实现

浏览器脚本异步加载:

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

deferasync的区别是:defer要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer是“渲染完再执行”,async是“下载完就执行”。另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的

ES6模块是符号链接,不是复制。CommonJS是复制。

猜你喜欢

转载自blog.csdn.net/qq_38307618/article/details/124184554
今日推荐