对象的属性操作
有四个操作会忽略enumerable为false的属性
- for..in循环:只遍历对象自身和继承的可枚举属性
- Object.keys():返回对象自身可枚举的属性键名
- JSON.stringify():只串行化自身可枚举的属性
- Object.assign():忽略enumerable为false的属性,只拷贝自身的可枚举属性
可枚举(enumerable),最初引入的目的是为了让某些属性可以规避for..in操作,不然所有内部属性和方法都会被遍历。如对象原型的toString方法,以及数组的length属性。而且只有for..in方法能遍历到继承的属性,其他都不行
ES6的属性遍历方法
-
for..in
for..in
循环遍历对象自身和继承的可枚举属性(不含Symbol属性) -
Object,keys(obj)
object.keys
返回一个数组,包含对象自身(不含继承)的所有可枚举属性(不含Symbol属性) -
Objcet.getOwnPropertyNames(obj)
Objcet.getOwnPropertyNames(obj)
返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)的键名 -
Objcet.getOwnPropertySymbols(obj)
Objcet.getOwnPropertySymbols(obj)
返回一个数组,包含对象自身的所有Symbol属性的键名 -
Reflect.oweKeys(obj)
Reflect.oweKeys(obj)
返回一个数组,包含对象自身所有的键名,不管是Symbol或字符串,也不管是否可枚举
以上5种方法遍历对象的键名,遵守一下规则
- 首先遍历数值键,按照数值升序排列
- 其次遍历所有字符串键,按照加入时间升序排列
- 最后遍历Symbol键,按照加入时间升序排列
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
对象新方法Object.assign
Object.assign
用来将源对象所有可枚举属性,复制到目标对象中。至少需要两个对象作为参数,第一个是目标对象,后面的都是源对象
let obj1 = { a: 1 }
let obj2 = { b: 2 }
let obj3 = { c: 3 }
Object.assign(obj1, obj2, obj3)
obj1
// { a: 1 , b: 2, c: 3 }
PS: 如果目标对象跟源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性
var obj1 = { a: 1, b:1 };
var obj2 = { b: 2, c:2 };
var obj3 = { c: 3 };
Object.assign(obj1, obj2, obj3)
obj1
// { a: 1 , b: 2, c: 3 }
如果目标对象不是对象参数的话
- 如果是首参数,那么会将其转为对象
Object.assign(2)
Number {[[PrimitiveValue]]: 2}
__proto__: Number
[[PrimitiveValue]]: 2
typeof(Object.assign(2))
// "object"
实际上的过程如下
var a = Object.assign(2)
undefined
a.__proto__
// Number {
//constructor: ƒ Number(),
//toExponential: ƒ,
//toFixed: ƒ, toPrecision, .....
//}
a.__proto__.__proto__
//{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
//constructor : ƒ Object()
//}
实际上是看非对象参数能否有对应的类进行实例,比如2是Number
型,能由Number
进行实例化得到,就相当于Number
实例化了一个2的对象,而Number
继承了Object
,就相当于转化成对象了,对应也可以用在Boolean
,String
类型
注意:但是Null和undefined是无法转为对象,没有对应的类给其实例化,所以他们作为首参数的话,会报错
var c = Object.assign(null)
var c = Object.assign(undefined)
//VM337:1 Uncaught TypeError: Cannot convert undefined or null to object
// at Function.assign (<anonymous>)
//at <anonymous>:1:16
- 非对象不出现在首参数
注意:如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有些不同。参数都会转成对象,如果无法转成对象,那就跳过,不会报错。但是除了字符串会以数组形式拷贝进目标对象,其他值(这里说的是基本数据类型)不会产生效果
var a = Object.assign( {a:1}, 2)
var b = Object.assign( {a:1}, '123')
var c = Object.assign({a:1}, true)
var d = Object.assign({a:1}, null)
var e = Object.assign({a:1}, undefined)
// a {a: 1}
// b {0: "1", 1: "2", 2: "3", a: 1}
// c {a: 1}
// d {a: 1}
// e {a: 1}
Object.assign
只拷贝自身属性,不可枚举属性(enumerable为false)和继承的属性都不拷贝
var a = Object.assign({ dwb: 'qkf'})
Object.defineProperty(a, 'zmf' , {
enumerable: false,
value: 'zmf'
})
// a {dwb: "qkf", zmf: "zmf"}
var b = Object.assign({},a)
// b {dwb: "qkf"}
class B {
}
B.prototype.zmf = 'zmf'
var b = new B()
// b.zmf "zmf"
b.dwb = 'qkf'
// b
B {dwb: "qkf"}
dwb : "qkf"
__proto__ :
zmf : "zmf"
constructor : class B
__proto__ : Object
注意:
Object.assign
可以用来处理数组,但会把数组视为对象
Object.assign([1, 2, 3], [4, 5])
// [4,5,3]
其中,4会覆盖,5会覆盖2,因为它们在数组的同一位置,对应位置覆盖,数组其实就是特殊的排列对象,只不过是有序的而已
Object.assign
方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用
var object1 = { a: { b: 1 } };
var object2 = Object.assign({}, object1);
object1.a.b = 2;
console.log(object2.a.b);
// 2
Object.assign
只能将属性值进行赋值,如果属性值是一个get(取值函数),那么会先求值,再进行赋值
// 源对象
const source = {
//属性是取值函数
get foo(){return 1}
};
//目标对象
const target = {};
Object.assign(target,source);
//{foo ; 1} 此时foo的值是get函数的求值结果
常见用途
1. 为对象添加属性
class Point{
constructor(x,y){
Object.assign(this, {x,y})
}
}
上面的方法可以为对象Point
类的实例对象添加属性x和属性y
2. 为对象添加方法
// 方法也是对象
// 将两个方法添加到类的原型对象上
// 类的实例会有这两个方法
Object.assign(SomeClass.prototype,{
someMethod(arg1,arg2){...},
anotherMethod(){...}
});
3. 克隆对象
//克隆对象的方法
function clone(origin){
//获取origin的原型对象
let originProto = Obejct.getPrototypeOf(origin);
//根据原型对象,创建新的空对象,再assign
return Object.assign(Object.create(originProto),origin);
}
4. 为属性指定默认值
// 默认值对象
const DEFAULTS = {
logLevel : 0,
outputFormat : 'html'
};
// 利用assign同名属性会覆盖的特性,指定默认值,如果options里有新值的话,会覆盖掉默认值
function processContent(options){
options = Object.assign({},DEFAULTS,options);
console.log(options);
//...
}
处于assign
浅拷贝的顾虑,DEFAULTS对象和options对象此时的值最好都是简单类型的值,否则函数会失效。
const DEFAULTS = {
url: {
host: 'example.com',
port: 7070
},
};
processContent({ url: {port: 8000} })
// {
// url: {port: 8000}
// }
上面的代码,由于url是对象类型,所以默认值的url被覆盖掉了,但是内部缺少了host属性,形成了一个bug。
Super关键字
super
关键字指向当前对象的原型对象,在ES6做继承的时候很有用,但是只能用在对象的方法中,在其他地方会报错
// 报错
const obj = {
foo: super.foo
}
// 报错
const obj = {
foo: () => super.foo
}
// 报错
const obj = {
foo: function () {
return super.foo
}
}
Class类
传统生成实例对象是通过构造函数,如
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);
而在ES6中,引入Class(类)的概念,作为对象模板,通过class
关键字,可以定义类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
上述定义一个“类”,里面有一个constructor
方法,这就是构造方法,用于定义初始化变量,this
指向实例对象。上述ES5的构造函数Point
,对应ES6Point
类的构造方法
ES6的“类”Class,可以看作构造函数的另一种写法
class Point {
// ...
}
typeof Point // 'function'
Point === Point.prototype.constructor //true
即,类的数据类型就是函数,类本身就指向构造函数
当然使用也直接对类使用new
命令,与构造函数用法一致
class Bar {
doStuff() {
console.log('stuff');
}
}
var b = new Bar();
b.doStuff() // "stuff"
构造函数的prototype
属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype
属性上面。
class Point {
constructor() {
// ...
}
toString() {
// ...
}
toValue() {
// ...
}
}
// 等同于
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
};
类的方法都是定义在prototype
对象上面,所有类的方法可以添加在prototype
对象上。Object.assign
可以很方便一次向类添加多个方法
class Point {
constructor(){
// ...
}
}
Object.assign(Point.prototype, {
toString(){},
toValue(){}
});
关于Object.assign
的详细用法,下面能介绍到
类内部所有定义的方法,都是不可枚举的(non-enumerable)
class Point {
constructor(x, y) {
// ...
}
toString() {
// ...
}
}
Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
上述中,toString
是Point
类内部定义的方法,是不可枚举的,跟ES5的行为不一致
var Point = function (x, y) {
// ...
};
Point.prototype.toString = function() {
// ...
};
Object.keys(Point.prototype)
// ["toString"]
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
ES5中prototype
写的方法是可枚举的
问题,为什么Class类的方法是不可枚举?
constructor
一个类必须有constructor
方法,没有定义的话会默认添加一个空的constructor
方法。该方法默认返回实例对象(即this
),如果返回其他对象,那这个实例对象就不是该类的对象了,也很好理解
由于constructor
方法对应的是构造函数,返回的实例都不是该构造函数了,所以肯定也不同了
class Foo {
constructor() {
return Object.create(null);
//这里本来是return this,返回Foo类实例对象的,但是返回了一个新建的空对象,所以就不是该类的实例了
}
}
new Foo() instanceof Foo
// false
与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this
对象上),否则都是定义在原型上(即定义在class
上)
//定义类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var point = new Point(2, 3);
point.toString() // (2, 3)
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
x
和y
都是实例对象point
自身的属性(定义在this
变量上的)
实际上对类实例化的化,会自动调用类的constructor
的方法,进行实例的初始化变量之类的操作,最后返回实例的this
==类不存在变量提升,与继承有关,ES5函数会进行变量提升==
静态方法
类相当于实例原型,类中定义的方法,相当于原型上的方法,都会被实例继承,但如果在类的方法前,加上static
关键字,就表示该方法不会被实例继承,而是通过类来调用,成为静态方法
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
Foo类的classMethod
方法前有static
关键字,表明该方法是一个静态方法,可以直接在Foo类上调用(Foo.classMethod()
),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。
所以,如果静态方法包含this
关键字,this
指向该类,而不是实例
class Foo {
static bar() {
this.baz();
}
static baz() {
console.log('hello');
}
baz() {
console.log('world');
}
}
Foo.bar() // hello
父类的静态方法,可以被子类继承
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
}
Bar.classMethod() // 'hello'
上面代码中,父类Foo有一个静态方法,子类Bar可以调用这个方法。
静态方法也是可以从super
对象上调用的。
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}
Bar.classMethod() // "hello, too"
Super
关键字可以说是超类,指向父类对象
问题:静态方法是怎么做到的?
私有方法和私有属性
只能在类内部访问的方法和属性,外部不能访问。这是常见需求,有利于代码封装,但ES6不提供,只能变通模拟实现
- 将私有方法移出模块,再用
call
将其上下文更改
class Widget {
foo (baz) {
bar.call(this, baz);
}
// ...
}
function bar(baz) {
return this.snaf = baz;
}
上面代码中,foo是公开方法,内部调用了bar.call(this, baz)
。这使得bar实际上成为了当前模块的私有方法。
还有一种方法是利用Symbol
值的唯一性,将私有方法的名字命名为一个Symbol
值。
const bar = Symbol('bar');
const snaf = Symbol('snaf');
export default class myClass{
// 公有方法
foo(baz) {
this[bar](baz);
}
// 私有方法
[bar](baz) {
return this[snaf] = baz;
}
// ...
};
上面代码中,bar和snaf都是Symbol
值,一般情况下无法获取到它们,因此达到了私有方法和私有属性的效果。但是也不是绝对不行,Reflect.ownKeys()
依然可以拿到它们。(关于Symbol
和Reflect
下面说)
const inst = new myClass();
Reflect.ownKeys(myClass.prototype)
// [ 'constructor', 'foo', Symbol(bar) ]
上面代码中,Symbol 值的属性名依然可以从类的外部拿到
Class继承
可以通过extends关键字实现继承
class Point{
}
class ColorPoint extends Point {
}
ColorPoint
通过extends
关键字,继承了Point
类的所有属性和方法
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
上面代码中,constructor
方法和toString
方法之中,都出现了super
关键字,它在这里表示父类的构造函数,用来新建父类的this
对象。
子类必须在constructor
方法中调用super
方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super
方法,子类就得不到this
对象。
class Point { /* ... */ }
class ColorPoint extends Point {
constructor() {
}
}
let cp = new ColorPoint(); // ReferenceError
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
this.color = color; // ReferenceError
super(x, y);
this.color = color; // 正确
}
}
上面代码中,子类的constructor
方法没有调用super之前,就使用this
关键字,结果报错,而放在super
方法之后就是正确的。
let cp = new ColorPoint(25, 8, 'green');
cp instanceof ColorPoint // true
cp instanceof Point // true
上面代码中,实例对象cp同时是ColorPoint
和Point
两个类的实例,这与 ES5 的行为完全一致。
最后,父类的静态方法,也会被子类继承。
class A {
static hello() {
console.log('hello world');
}
}
class B extends A {
}
B.hello() // hello world
ES5 的继承,实质是先创造子类的实例对象this
,然后再将父类的方法添加到this
上面(Parent.apply(this)
)。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this
上面(所以必须先调用super
方法),然后再用子类的构造函数修改this
。
Object.getPrototypeOf()
Object.getPrototypeOf
方法可以用来从子类上获取父类
Object.getPrototypeOf(ColorPoint) === Point
// true
因此,可以使用这个方法判断,一个类是否继承了另一个类。
Super关键字
super
这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
-
super
作为函数调用,代表父类的构造函数。ES6规定,子类的构造函数必须执行一次super
函数
class A {}
class B extends A {
constructor() {
super();
}
}
注意,super
虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super
内部的this
指的是B的实例,因此super()
在这里相当于A.prototype.constructor.call(this)
class A {
constructor() {
console.log(new.target.name);
}
}
class B extends A {
constructor() {
super();
}
}
new A() // A
new B() // B
new.target
指向当前正在执行的函数,可以看到,在super()
执行时,它指向的是子类B
的构造函数,而不是父类A
的构造函数。也就是说,super()
内部的this
指向的是B
作为函数时,super()
只能用在子类的构造函数之中,用在其他地方就会报错
class A {}
class B extends A {
m() {
super(); // 报错
}
}
-
super
作为对象时,在普通方法时,指向父类的原型对象;在静态方法时指向父类。
class A {
p() {
return 2;
}
}
class B extends A {
constructor(){
super()
console.log(super.p()) //2
}
}
上面代码中,子类B
当中的super.p()
,就是将super
当作一个对象使用。这时,super
在普通方法之中,指向A.prototype
,所以super.p()
就相当于A.prototype.p()
。
但是由于super
指向父类的原型对象,所以定义在父类实例上的方法或者属性,是无法通过super
调用的
class A {
constructor() {
this.p = 2;
}
}
class B extends A {
get m() {
return super.p;
}
}
let b = new B();
b.m // undefined
p
是父类A
实例的属性,super.p
引用不到它
如果属性是定义在父类的原型对象上,super
就可以取到
class A {
}
A.prototype.x = 2
class B extends A {
constructor(){
super()
console.log(super.x) //2
}
}
let b = new B()
上面代码中,属性x是定义在A.prototype
上面的,所以super.x
可以取到它的值。
ES6规定,在子类普通方法中通过super
调用父类方法的时候,方法内部this
指向当前子类实例
class A {
constructor(){
this.x = 1;
}
print(){
console.log(this.x)
}
}
class B extends A {
constructor(){
super()
this.x = 2
}
m(){
super.print()
}
}
let b = new B()
b.m() //2
上面代码中,super.print()
虽然调用的是A.prototype.print()
,但是A.prototype.print()
内部的this指向子类B
的实例,导致输出的是2,而不是1。也就是说,实际上执行的是super.print.call(this)
。
由于this
指向子类实例,所以如果通过super
对某个属性赋值,这时super
就是this
,赋值的属性会变成子类实例的属性
class A{
constructor(){
this.x = 1
}
}
class B extends A{
constructor(){
super()
this.x = 2
super.x = 3
console.log(super.x) //undefined
console.log(this.x) //3
}
}
上面代码中,super.x
赋值为3,给属性赋值为3的话,等同于对this.x
赋值为3,而当读取super.x
时,读的是A.prototype.x
,所以返回undefined
。(也就是说,'读'属性的时候是父类原型,'写'属性的时候是实例)
如果super
作为对象,用在静态方法中,super
指向父类,而不是父类的原型对象
class Parent {
static myMethod(msg) {
console.log('static', msg);
}
myMethod(msg) {
console.log('instance', msg);
}
}
class Child extends Parent {
static myMethod(msg) {
super.myMethod(msg);
}
myMethod(msg) {
super.myMethod(msg);
}
}
Child.myMethod(1); // static 1
var child = new Child();
child.myMethod(2); // instance 2
上面代码中,super
在静态方法之中指向父类,在普通方法之中指向父类的原型对象。
另外,在子类的静态方法中通过super
调用父类的方法时,方法内部的this
指向当前的子类,而不是子类的实例
class A {
constructor() {
this.x = 1;
}
static print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
}
static m() {
super.print();
}
}
B.x = 3;
B.m() // 3
let b = new B()
b.x // 2
b.m() // b.m is not a function
因为实例都获取不了子类的静态方法,所以也很好理解
注意: 使用super
的时候,要显式指定是作为函数,还是对象使用,否则会报错(函数就加括号(),对象不用,但后面要加属性)
class A {}
class B extends A {
constructor() {
super();
console.log(super); // 报错
}
}
类的 prototype
属性和 __proto__
属性
ES5中,每个对象都有__proto__
属性,指向对应构造函数的prototype
属性。Class 作为构造函数的语法糖,同时有prototype
属性和__proto__
,因此存在两条继承链
(1) 子类的__proto__
属性,表示构造函数的继承,总是指向父类
(2) 子类prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的prototype
属性
class A {
}
class B extends A {
}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
上面代码中,子类B的__proto__
属性指向父类,子类B的prototype
属性的__proto__
属性指向父类A的prototype
属性
这样的结果是因为,类的继承按照下面模式实现的
class A {
}
class B {
}
// B 是实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype)
// B 继承 A 的静态属性
Object.setPrototypeOf(B, A)
let b = new B()
Object.setPrototypeOf
方法的实现。
Object.setPrototypeOf = function(obj, proto){
obj.__proto__ = proto
return obj
}
因此得到了上述结果
Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;
Object.setPrototypeOf(B, A);
// 等同于
B.__proto__ = A;
两条继承链,可以这么理解: 作为一个对象,子类(B)的原型(__proto__
属性)是父类(A);作为一个构造函数,子类(B)的原型对象(prototype
属性)是父类的原型对象(prototype
属性)的实例
即对象对应对象,原型对应原型
B.prototype = Object.create(A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;
实例的__proto__
属性
子类实例的__proto__
属性,指向子类,其实就一个原型链的问题。不作多解释
问题:画出原型链的表达图!
Set和Map数据结构
ES6新的数据结构Set
。类似数组,但是成员值都是唯一的
Set
本身是一个构造函数,生成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
add()
方法可以向Set
结构加入成员,结果显示Set
不会添加重复的值,这个方法可以用来进行数组去重
如下,Set
函数接受一个数组(或者其他具有iterable
接口的其他数据结构)作为参数,用来初始化
const set = new Set([1,2,3,4,4,5,5])
[..set]
// [1,2,3,4]
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5
const set = new Set(document.querySelectorAll('div'));
set.size // 56
// 类似于
const set = new Set();
document
.querySelectorAll('div')
.forEach(div => set.add(div));
set.size // 56
而将Set
结构转换成数组的话可以用[...Set]
或者用Array.form
,(...
是扩展运算符,内部使用for...of
循环,一个针对iterator(可遍历)结构的循环)
// 去除数组的重复成员
[...new Set(array)]
const items = new Set([1,2,3,4,5,5,6,6])
const array = Array.from(items)
console.log(array)
//[1, 2, 3, 4, 5, 6]
当然也可以去除字符串里的重复字符
[...new Set('ababbc')].join('')
// "abc"
就本身来说,我自己项目中出现过,比如用户进行前端添加人员(后台接受的是人员ID数组,但在前端得显示添加的人员姓名),根据数据结构不同有几个不同方法
- 初始化两个数组一个人员ID数组,一个人员Name数组,当用户点击添加的时候进行任一判断存不存在数组中,不存在就
push
- 初始化一个对象,人员ID是键,值为人员Name,进行添加的时候用人员ID作为属性判断值是不是为空,空就添加
- 初始化一个
Set
结构,当用户进行添加重复人员的时候,Set
结构不会做出相应,在用户界面看到的一直都会是去重后的人员数组
==注意:第1种方法解决的话每次添加需要去循环一下数组,时间复杂度是O(n),第2种的话直接利用对象的键去查找有无值,性能上会比第一种好很多,第3种的话就省去了判断这个环节,去取值的话也直接用键去取就行了,性能也比较高==
Set实例属性和方法
-
Set.prototype.constructor
:构造函数,默认是Set
函数 -
Set.prototype.size
:返回Set
实例的成员总数
Set
实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)
操作方法:
-
Set.prototype.add(value)
: 添加某个值,返回Set结构本身 -
Set.prototype.delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功 -
Set.prototype.has(value)
:返回一个布尔值,表示该值是否为Set
的成员 -
Set.prototype.clear()
:清除所有成员,没有返回值
s.add(1).add(2).add(2);
// 注意2被加入了两次
s.size // 2
s.has(1) // true
s.has(2) // true
s.has(3) // false
s.delete(2);
s.has(2) // false
遍历方法
-
Set.prototype.keys()
: 返回键名的遍历器 -
Set.prototype.values()
: 返回键值的遍历器 -
Set.prototype.entries()
: 返回键值对的遍历器 -
Set.prototype.forEach()
: 使用回调函数遍历每个成员
keys
方法、values
方法、entries
方法返回的都是遍历器对象(详见《Iterator 对象》一章)。由于 Set
结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys
方法和values
方法的行为完全一致。
WeakSet
结构与Set
类似,也是不重复值的集合。区别在于
-
WeakSet
成员只能是对象,其他都不行,如
const ws = new WeakSet()
ws.add(1)
// TypeError: WeakSet value must be an object, got the number 1
ws.add(Symbol())
//TypeError: WeakSet value must be an object, got Symbol()
-
WeakSet
中的对象都是弱引用,即垃圾回收机制不考虑WeakSet
对该对象的引用,就是说其他对象不引用该对象的话,垃圾回收机制就会自动回收该对象所占用的内存,不考虑该对象存在于WeakSet
之中,如
var obj = { a: { 1: 1}, b: {2:2} };
var ws = new WeakSet();
ws.add(obj)
// console.log(ws)
// WeakSet(1)
// <entries>
// 0 : { a: {1:1}, b:{2:2} }
obj.a = null
obj.b = null
// console.log(ws)
// WeakSet(1)
// <entries>
// 0 : { a: null, b:null }
WeakSet
可以接受一个数组或类数组的成员对象作为参数,如
const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}
a
数组有两个成员,也是数组,数组也属于对象,则成员对象会自动成为WeakSet
成员
而如果数组成员不是对象,就会出错,就像
const b = [3,4]
const ws = new WeakSet(b)
// Uncaught TypeError: Invalid value used in weak set(…)
WeakSet
有三个方法
-
WeakSet.prototype.add(value)
: 添加新成员 -
WeakSet.prototype.delete(value)
: 删除成员 -
WeakSet.prototype.has(value)
: 返回是否在实例中的布尔值
由于WeakSet
是弱引用,所以没有遍历方法
Map
传统JS的对象,本质上是键值对(Hash)的集合,传统只能用字符串当作键,这给它使用带来限制,如
const data = {};
const element = document.getElementById('myDiv');
data[element] = 'metadata';
data['[object HTMLDivElement]'] // "metadata"
原意是想把DOM节点存为对象的键,但对象只接受字符串作为键名,所以element
被自动转为字符串[object HTMLDivElement]
而Map
数据结构的"键"可以接受各种类型的值(包括对象),是一种“值-值”对应的感觉。
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
如果Map
接受一个数组的话
const map = new Map([
['name', '张三'],
['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"
实际上,Map
构造函数接受数组作为参数的时候,执行的是下面的算法
const items = [
['name', '张三'],
['title', 'Author']
];
const map = new Map();
items.forEach(
([key, value]) => map.set(key, value)
);
事实上,不仅仅是数组,任何具有Iterator
接口、且每个成员都是一个双元素的数组的数据结构(详见《Iterator 对象》一章)都可以当作Map
构造函数的参数。这就是说,Set
和Map
都可以用来生成新的Map
。
const set = new Set([
['foo', 1],
['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1
const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
m3.get('baz') // 3
同样的值的两个实例,在 Map
结构中被视为两个键。
const map = new Map();
const k1 = ['a'];
const k2 = ['a'];
map
.set(k1, 111)
.set(k2, 222);
map.get(k1) // 111
map.get(k2) // 222
上面代码中,变量k1
和k2
的值是一样的,但是它们在 Map
结构中被视为两个键。
可以考虑到为变量开辟内存的时候,新变量开辟新的空间,就算是一样的值都好,引用都是不一样的,Map
的键实际上与变量内存地址绑定,只要内存地址不一样,就视为两个键。
但是Map
键是简单类型的值的话(数字,字符串,布尔值),两个值严格(严格的意思就是三等号为true
的情况,也就是类型都要一样)相等就视为一个键了,当然了引用类型就上述情况了。
注意: NaN
虽然不严格相等,但是Map
仍视为同一个键
let map = new Map();
map.set(-0, 123);
map.get(+0) // 123
map.set(true, 1);
map.set('true', 2);
map.get(true) // 1
map.set(undefined, 3);
map.set(null, 4);
map.get(undefined) // 3
map.set(NaN, 123);
map.get(NaN) // 123
属性和操作方法
-
size属性
size
属性返回Map
结构的成员总数 -
Map.prototype.set(key, value)
该方法设置键名key
的值为value
,返回整个Map
结构,若键key
已经有值那就更新键值,由于是返回整个Map
对象,所以可以用链式写法
let map = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c');
-
Map.prototype.get(key)
get
读取key
的键值,找不到的话返回undefinded -
Map.prototype.has(key)
has
方法返回一个布尔值,表示该键是否在当前Map
对象中 -
Map.prototype.delete(key)
删除某个键,返回成功与否的布尔值 -
Map.prototype.clear()
清除所有成员,没有返回值
遍历方法
- Map.prototype.keys(): 返回键名遍历器
- Map.prototype.values(): 返回键值遍历器
- Map.prototype.entries(): 返回键值遍历器
- Map.prototype.forEach(): 遍历Map所有成员
转换成数组的话用...
很方便
const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
[...map.keys()]
// [1, 2, 3]
[...map.values()]
// ['one', 'two', 'three']
[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]
[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]
此外,Map还有一个forEach
方法,与数组的forEach
方法类似,也可以实现遍历。
map.forEach((value, key, map) => {
console.log("Key: %s, value: %s", key,value)
})
forEach
方法还可以接受第二个参数,用来绑定this
const reporter = {
report: function(key, value) {
console.log("Key: %s, Value: %s", key, value);
}
};
map.forEach(function(value, key, map) {
this.report(key, value);
}, reporter);
第二个参数,指定对象上下文,传入是哪个对象变量,this
指向reporter
Map与其他数据结构互相转换
-
Map转为数组
前面提过,最方便的就是使用扩展运算符(...
)
const myMap = new Map()
.set(true, 7)
.set({foo: 3}, ['abc'])
[...myMap]
//[ [true, 7], [{foo: 3}, ['abc'] ] ]
-
数组转为Map
将数组传入Map构造函数,就可以转为Map
new Map([
[true, 7],
[ {foo: 3}, ['abc'] ]
])
// Map {
true => 7
Object {foo: 3} => ['abc']
}
-
Map转为对象
如果所有Map的键都是字符串,它可以无损地转为对象
function strMapToObj(strMap){
let obj = Object.create(null)
for(let [k,v] of strMap){
obj[k] = v
}
return obj
}
const myMap = new Map()
.set('yes', true)
.set('no', false)
strMapToObj(myMap)
// { yes: true, no: false }
如果有非字符串的键名,那么这个键名会先转字符串,再作为对象的键名
function strMapToObj(strMap){
let obj = Object.create(null)
for(let [k,v] of strMap){
obj[k] = v
}
return obj
}
const myMap = new Map()
.set({ foo: 3 }, true)
.set('no', false)
strMapToObj(myMap)
// { [object Object] : true, no: false }
const myMaps = new Map()
.set([ 'abc', 'bcs' ], true)
.set('no', false)
strMapToObj(myMaps)
// { abc,bcs : true no : false }
不同的是,js中万物都是继承Object
的,所以转成字符串是调用toString()
方法,对象那就是[object Object]
,数组的话直接拼接数组的值
- 对象转为Map
function objToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
objToStrMap({yes: true, no: false})
// Map {"yes" => true, "no" => false}
-
Map转为JSON
要区分两种情况,一种是Map的键名都是字符串的时候,这时可以选择转为对象JSON
function strMapToJson(strMap){
return JSON.stringify(strMapToObj(strMap)
}
let myMap = new Map().set('yes', true).set('no', false)
strMapToJson(myMap)
// '{"yes":true,"no":false}'
另一种情况是,Map的键名有非字符串,这时候可以选择转为数组JSON
function mapToArrayJson(map) {
return JSON.stringify([...map]);
}
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'
-
JSON转为Map
JSON转为Map,正常情况下,所有键名都是字符串
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}
但是,有一种特殊情况,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为 Map。这往往是 Map 转为数组 JSON 的逆操作。
function jsonToMap(jsonStr) {
return new Map(JSON.parse(jsonStr));
}
jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}
其实就相当于逆转换,吃透一边就可以理解另外一边了
WeakMap
类似Map,跟WeakSet差不多,不详细讲了
WeakMap最典型场合就是用DOM节点作为键名去保存,因为当DOM节点删除的时候,状态也会自动消失,不用担心内存泄漏
总结
Set
,WeakSet
,Map
,WeakMap
一个是类数组,一个类对象,可以说是数组和对象的扩展应用把,比数组和对象有更强大的功能,也可以互相转换成数组和对象
Promise对象
简单来说,是一个容器,保存着某个未来才会结束的事件(通常是一个异步操作)的结果,比如一个请求,请求需要时间,等到响应后才会返回结果。语法上说,Promise
是一个对象,它可以获取异步操作的消息。有了Promise
对象,可以将异步操作以同步操作流程表达出来,避免了层层嵌套的回调。
Promise
对象会将所有执行函数放在then
之后执行,而不管你的操作是不是异步的,就是说同步的,经过Promise
包装后就会变成异步执行
const f = () => console.log('now');
Promise.resolve().then(f);
console.log('next');
// next
// now
那么有没有一种方法,让同步函数同步执行,异步函数异步执行,并且让它们具有统一的 API 呢?回答是可以的,并且还有两种写法。
第一种写法是用async
函数来写
const f = () => console.log('now');
(async () => f())();
console.log('next');
// now
// next
上面代码中,第二行是一个立即执行的匿名函数,会立即执行里面的async函数,因此如果f
是同步的,就会得到同步的结果;如果f
是异步的,就可以用then指定下一步,就像下面的写法。
(async () => f())()
.then(...)
需要注意的是,async () => f()会吃掉f()抛出的错误。所以,如果想捕获错误,要使用promise.catch方法。
(async () => f())()
.then(...)
.catch(...)
Generator(生成器)函数
是ES6提供的一种异步编程解决方案
从语法上,可以理解成一个状态机,封装了多个内部状态
执行Generator函数会返回一个遍历器对象,也就是说,Generator函数除了状态机,还是一个遍历器对象生成函数。返回的都是遍历器对象,可以依次遍历Generator函数内部的每一个状态
形式上,Generator
函数是一个普通函数,但是有两个特征。一是,function
关键字与函数名之间有一个星号;二是,函数体内部使用yield
表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。
function* helloWorldGenerator(){
yield 'hello'
yield 'world'
return 'ending'
}
var hw = helloWorldGenerator()
调用Generator函数后,该函数并不执行,返回的是一个指向内部状态的指针对象
,也就是遍历器对象
下一步,必须调用遍历器对象的next
方法,使得指针移向下一个状态。就是说,每一次调用next
方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield
表达式(或return
语句)为止。换言之,Generator函数是分段执行,yield
表达式是暂停执行的标记,而next
可以恢复执行
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
总结一下,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next
方法,就会返回一个有着value
和done
两个属性的对象。value
属性表示当前的内部状态的值,是yield
表达式后面那个表达式的值;done
属性是一个布尔值,表示是否遍历结束
yield表达式
yield
表达式相当于一个暂停标志,遇到它,便返回一个对象,value
值就是yield
后面带的值,继续调用next
往下执行到下一个yield
表达式,没有的话就一直运行到结束,直到return
,将return
后的值作为value
,如果没有return
,返回对象的value
属性值就是undefined
function* helloWorldGenerator() {
console.log('111');
yield 'hello';
yield 'world';
console.log('end')
}
var hw = helloWorldGenerator();
hw.next()
// 111
// Object { value: "hello", done: false }
hw.next()
// Object { value: "world", done: false }
hw.next()
// 'end'
// Object { value: undefined, done: true }
由于这个功能,等于给javascript提供了手动的惰性求值(Lazy Evaluation)语法功能
注意:
yield
表达式只能用在Generator函数中
如果yield
表达式放在另一个表达式中,要放圆括号
function* demo(){
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield));
console.log('Hello' + (yield 123));
}
var dm = demo()
dm.next()
// Object { value: undefined, done: false }
dm.next()
// Helloundefined
// Object { value: 123, done: false }
dm.next()
// Helloundefined
// Object { value: undefined, done: true }
dm.next()
// Object { value: undefined, done: true }
yield
表达式用作函数参数或放在赋值表达式的右边可以不加括号
function foo(a,b){
return a+b;
}
function* memo(){
foo(yield 'a',yield'b')
var input = yield
}
var mm = memo()
mm.next()
// Object { value: "a", done: false }
mm.next()
// Object { value: "b", done: false }
mm.next()
// Object { value: undefined, done: false }
mm.next()
// Object { value: undefined, done: true }
与 Iterator
接口的关系
任何一个对的Symbol.iterator
方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象
由于Generator函数就是遍历器生成器,可以将Generator函数赋值给对象的iterator
属性,从而使该对象具有Iterator
接口
var myIterable = {}
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1,2,3]
上面代码中,Generator 函数赋值给Symbol.iterator
属性,从而使得myIterable
对象具有了 Iterator
接口,可以被...
运算符遍历了。
next方法的参数
yield
表达式本身没有返回值,或者说总是返回undefined
。next
方法可以带一个参数,该参数就会被当作上一个yield
表达式的返回值。
function* f() {
for(var i = 0; true; i++) {
var reset = yield i;
if(reset) { i = -1; }
}
}
var g = f();
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }
上面代码先定义了一个可以无限运行的 Generator 函数f,如果next
方法没有参数,每次运行到yield
表达式,变量reset
的值总是undefined
。当next
方法带一个参数true
时,变量reset
就被重置为这个参数(即true
),因此i会等于-1,下一轮循环就会从-1开始递增
for...of循环
for...of
循环可以自动遍历 Generator 函数运行时生成的Iterator
对象,且此时不需要调用next
方法
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
使用for...of
循环,依次显示 5 个yield
表达式的值。这需要注意的是,一旦next
方法的返回对象的done
属性为true
,for...of
循环就会中止,且不包含该返回对象,所以return
语句返回的6,不包括在for...of
循环中
原生的js对象没有遍历接口,无法使用for...of
循环,通过Generator函数为它加上这个接口就可以用了。
除了for...of
循环以外,扩展运算符(...)
、解构赋值
和Array.from
方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的 Iterator
对象,作为参数。
function* numbers () {
yield 1
yield 2
return 3
yield 4
}
// 扩展运算符
[...numbers()] // [1, 2]
// Array.from 方法
Array.from(numbers()) // [1, 2]
// 解构赋值
let [x, y] = numbers();
x // 1
y // 2
// for...of 循环
for (let n of numbers()) {
console.log(n)
}
// 1
// 2
throw()
next(),throw(),return()共同点
next()
,throw()
,return()
这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让Generator函数恢复执行,并且使用不同的语句替换yield
表达式。
next()
是将yield
表达式替换成一个值
const g = function* (x, y) {
let result = yield x + y;
return result;
};
const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}
gen.next(1); // Object {value: 1, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = 1;
上面代码中,第二个next(1)
方法就相当于将yield
表达式替换成一个值1
。如果next
方法没有参数,就相当于替换成undefined
throw
是将yield
表达式替换成一个throw
语句。
gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));
return()
是将yield
表达式替换成一个return
语句
gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;
** yield*
表达式**
如果在Generator函数内部,调用另一个Generator函数。需要在前者函数体内部,自己手动完成遍历
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
// 手动遍历 foo()
for (let i of foo()) {
console.log(i);
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// x
// a
// b
// y
上面代码中,foo
和bar
都是Generator
函数,在bar
里面调用foo
,需要手动遍历foo
。如果多个Generator
函数嵌套,写起来就比较麻烦
ES6提供yield*
表达式,作为用在在一个Generator
函数中调用另一个Generator
函数
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"
实际上,任何数据结构,只要有Iterator
接口,都可以被yield*
遍历,也就是说yield*
可以遍历含有遍历器对象或遍历器对象接口的对象
在yield*
后面的Generator函数没有return语句时,相当于是for..of
的简写形式,有return语句的时候,则将这个数值返回获取,如果被代理的 Generator 函数有return语句,那么就可以向代理它的 Generator 函数返回数据。
function* foo() {
yield 2;
yield 3;
return "foo";
}
function* bar() {
yield 1;
var v = yield* foo();
console.log("v: " + v);
yield 4;
}
var it = bar();
it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}
上面代码在第四次调用next
方法的时候,屏幕上会有输出,这是因为函数foo的return
语句,向函数bar提供了返回值。
function* genFuncWithReturn() {
yield 'a';
yield 'b';
return 'The result';
}
function* logReturned(genObj) {
let result = yield* genObj;
console.log(result);
}
[...logReturned(genFuncWithReturn())]
// The result
// 值为 [ 'a', 'b' ]
上面代码有两次遍历。第一次是扩展运算符遍历函数logReturned
返回的遍历器对象,第二次是yield*
语句遍历函数genFuncWithReturn
返回的遍历器对象。但是,函数genFuncWithReturn
的return
语句的返回值The result
,会返回给函数logReturned
内部的result
变量,因此会有终端输出。
而且是先执行一次函数代码,再执行遍历
yield*
可以很方便取出嵌套数组的所有成员
function* iterTree(tree) {
if (Array.isArray(tree)) {
for(let i=0; i < tree.length; i++) {
yield* iterTree(tree[i]);
}
} else {
yield tree;
}
}
const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
for(let x of iterTree(tree)) {
console.log(x);
}
// a
// b
// c
// d
// e
内部运行,如果是数组的话执行if操作,不然就输出
使用扩展运算符...
也是一样的效果
[...iterTree(tree)] // ["a", "b", "c", "d", "e"]
下面是一个稍微复杂的例子,使用yield*
语句遍历完全二叉树。
// 下面是二叉树的构造函数,
// 三个参数分别是左树、当前节点和右树
function Tree(left, label, right) {
this.left = left;
this.label = label;
this.right = right;
}
// 下面是中序(inorder)遍历函数。
// 由于返回的是一个遍历器,所以要用generator函数。
// 函数体内采用递归算法,所以左树和右树要用yield*遍历
function* inorder(t) {
if (t) {
yield* inorder(t.left);
yield t.label;
yield* inorder(t.right);
}
}
// 下面生成二叉树
function make(array) {
// 判断是否为叶节点
if (array.length == 1) return new Tree(null, array[0], null);
return new Tree(make(array[0]), array[1], make(array[2]));
}
let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);
// 遍历二叉树
var result = [];
for (let node of inorder(tree)) {
result.push(node);
}
result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']
Generator 函数的this
Generator 函数总是返回一个遍历器,ES6规定这个遍历器是Generator的实例,也继承了Generator函数的prototype
对象上的方法
function* g() {}
g.prototype.hello = function () {
return 'hi!';
};
let obj = g();
obj instanceof g // true
obj.hello() // 'hi!'
上面代码表明,Generator 函数g
返回的遍历器obj
,是g
的实例,而且继承了g.prototype
。但是,如果把g
当作普通的构造函数,并不会生效,因为g
返回的总是遍历器对象,而不是this
对象。
function* g() {
this.a = 11;
}
let obj = g();
obj.next();
obj.a // undefined
上面代码中,Generator 函数g在this对象上面添加了一个属性a
,但是obj
对象拿不到这个属性。
Generator 函数也不能跟new命令一起用,会报错,因为不是构造函数。
function* F() {
yield this.x = 2;
yield this.y = 3;
}
new F()
// TypeError: F is not a constructor
问题:能否让Generator函数返回一个正常的对象实例,既可以用next
方法,又可以获得正常的this
下面一个变通方法。首先,生成一个空对象,使用call
方法绑定Generator函数内部的this
,这样,构造函数调用以后,这个空对象就是Generator函数的实例对象了
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var obj = {};
var f = F.call(obj);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
obj.a // 1
obj.b // 2
obj.c // 3
上面代码中,首先是F
内部的this
对象绑定obj对象,然后调用它,返回一个 Iterator
对象。这个对象执行三次next
方法(因为F内部有两个yield
表达式),完成 F
内部所有代码的运行。这时,所有内部属性都绑定在obj
对象上了,因此obj
对象也就成了F的实例。
上面代码中,执行的是遍历器对象f
,但是生成的对象实例是obj
,有没有办法将两个对象统一
一个办法是将obj
换成F.prototype
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var f = F.call(F.prototype);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
再将F
改成构造函数,就可以对它执行new
命令了
function* gen() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
function F() {
return gen.call(gen.prototype);
}
var f = new F();
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
Generator 函数的应用
(1)异步操作的同步化表达
如Ajax,是个典型的异步操作,通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达。
function* main() {
var result = yield request("http://some.url");
var resp = JSON.parse(result);
console.log(resp.value);
}
function request(url) {
makeAjaxCall(url, function(response){
it.next(response);
});
}
var it = main();
it.next();
上面代码的main函数,就是通过 Ajax 操作获取数据。可以看到,除了多了一个yield
,它几乎与同步操作的写法完全一样。注意,makeAjaxCall
函数中的next
方法,必须加上response
参数,因为yield
表达式,本身是没有值的,总是等于undefined
。
(2)控制流管理
如果有一个多步操作非常耗时,采用回调函数,可能会像下面这样
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
});
采用 Promise 改写上面代码
Promise.resolve(step1)
.then(step2)
.then(step3)
.then(step4)
.then(function (value4) {
// Do something with value4
}).catch(Error = > { })
.done();
上面代码已经把回调函数,改成了直线执行的形式,但是加入了大量 Promise 的语法。Generator 函数可以进一步改善代码运行流程。
function* longRunningTask(value1) {
try {
var value2 = yield step1(value1);
var value3 = yield step2(value2);
var value4 = yield step3(value3);
var value5 = yield step4(value4);
// Do something with value4
} catch (e) {
// Handle any error from step1 through step4
}
}
然后,使用一个函数,按次序自动执行所有步骤。
scheduler(longRunningTask(initialValue));
function scheduler(task) {
var taskObj = task.next(task.value);
// 如果Generator函数未结束,就继续调用
if (!taskObj.done) {
task.value = taskObj.value
scheduler(task);
}
}
(3)部署Iterator接口
利用Generator函数,可以在任何对象上部署Iterator接口
function* iterEntries(obj) {
let keys = Object.keys(obj);
for (let i=0; i < keys.length; i++) {
let key = keys[i];
yield [key, obj[key]];
}
}
let myObj = { foo: 3, bar: 7 };
for (let [key, value] of iterEntries(myObj)) {
console.log(key, value);
}
// foo 3
// bar 7
Generator函数的异步应用
async函数
使异步操作更加方便,是 Generator 函数的改进
前文有一个 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());
};
上面代码的函数gen可以写成async函数,就是下面这样。
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
async
函数对 Generator 函数的改进,体现在四点:
- 内置执行器
Generator 函数的执行必须依靠执行器,或者自行调用next
方法, 而async
函数内置执行器,与普通函数一模一样,只需要一行
asyncReadFile();
- 更好的语义
async
和await
,比起星号和yield
,语义更清楚了。async
表示函数里有异步操作,await
表示紧跟在后面的表达式需要等待结果。
更广的适用性
async
函数的await
命令后面,可以是Promise
对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即resolved
的Promise
对象)。返回值是 Promise。
async
函数的返回值是 Promise
对象,这比 Generator 函数的返回值是 Iterator
对象方便多了。你可以用then
方法指定下一步的操作。
进一步说,async
函数完全可以看作多个异步操作,包装成的一个 Promise
对象,而await
命令就是内部then
命令的语法糖。
基本用法
async
函数有多种使用形式。
// 函数声明
async function foo() {}
// 函数表达式
const foo = async function () {};
// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)
// Class 的方法
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}
async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}
const storage = new Storage();
storage.getAvatar('jake').then(…);
// 箭头函数
const foo = async () => {};
语法
async
函数返回一个 Promise 对象
async
函数内部return
语句返回的值,会成为then
方法回调函数的参数
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
上面代码中,函数f
内部return
命令返回的值,会被then
方法回调函数接收到
async
函数内部抛出错误,会导致返回的 Promise 对象变为reject
状态。抛出的错误对象会被catch
方法回调函数接收到
async function f() {
throw new Error('出错了');
}
f().then(
v => console.log(v),
e => console.log(e)
)
// Error: 出错了
await 命令
正常情况下,await
命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值
async function f(){
// 等同于
// return 123
return await 123;
}
f().then(v => {console.log(v)})
// 123
上面代码中,await命令的参数是数值123,这时等同于return 123。
另一种情况是,await
命令后面是一个thenable
对象(即定义then
方法的对象),那么await会将其等同于 Promise 对象
class Sleep {
constructor(timeout) {
this.timeout = timeout;
}
then(resolve, reject) {
const startTime = Date.now();
setTimeout(
() => resolve(Date.now() - startTime),
this.timeout
);
}
}
(async () => {
const sleepTime = await new Sleep(1000);
console.log(sleepTime);
})();
// 1000
上面代码中,await
命令后面是一个Sleep
对象的实例。这个实例不是 Promise 对象,但是因为定义了then
方法,await
会将其视为Promise
处理。
这个例子还演示了如何实现休眠效果。JavaScript 一直没有休眠的语法,但是借助await
命令就可以让程序停顿指定的时间。下面给出了一个简化的sleep
实现。
function sleep(interval) {
return new Promise(resolve => {
setTimeout(resolve, interval);
})
}
// 用法
async function one2FiveInAsync() {
for(let i = 1; i <= 5; i++) {
console.log(i);
await sleep(1000);
}
}
one2FiveInAsync();
使用注意点
第一点, await
命令后面的 Promise 对象,运行结果可能是rejected
,所以最好把await
命令放在try...catch
代码块中
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}
// 另一种写法
async function myFunction() {
await somethingThatReturnsAPromise()
.catch(function (err) {
console.log(err);
});
}
第二点,多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
let foo = await getFoo();
let bar = await getBar();
上面代码中,getFoo
和getBar
是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo
完成以后,才会执行getBar
,完全可以让它们同时触发。
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
上面两种写法,getFoo和getBar都是同时触发,这样就会缩短程序的执行时间。
第三点,await
命令只能用在async
函数之中,普通函数中的话会报错。
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 报错
docs.forEach(function (doc) {
await db.post(doc);
});
}
上面代码会报错,因为await
用在普通函数之中了。但是,如果将forEach
方法的参数改成async函数,也有问题。
function dbFuc(db) { //这里不需要 async
let docs = [{}, {}, {}];
// 可能得到错误结果
docs.forEach(async function (doc) {
await db.post(doc);
});
}
上面代码可能不会正常工作,原因是这时三个db.post操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用for循环。
async function dbFuc(db) {
let docs = [{}, {}, {}];
for (let doc of docs) {
await db.post(doc);
}
}
如果确实希望多个请求并发执行,可以使用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);
}
async 函数的实现原理
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}
所有的async
函数都可以写成上面的第二种形式,其中的spawn
函数就是自动执行器。
下面给出spawn
函数的实现,基本就是前文自动执行器的翻版。
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
与其他异步处理方法的比较
我们通过一个例子,来看 async 函数与 Promise、Generator 函数的比较。
假定某个 DOM 元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。
首先是 Promise 的写法
function chainAnimationsPromise(elem, animations) {
// 变量ret用来保存上一个动画的返回值
let ret = null;
// 新建一个空的Promise
let p = Promise.resolve();
// 使用then方法,添加所有动画
for(let anim of animations) {
p = p.then(function(val) {
ret = val;
return anim(elem);
});
}
// 返回一个部署了错误捕捉机制的Promise
return p.catch(function(e) {
/* 忽略错误,继续执行 */
}).then(function() {
return ret;
});
}
虽然 Promise 的写法比回调函数的写法大大改进,但是一眼看上去,代码完全都是 Promise 的 API(then
、catch
等等),操作本身的语义反而不容易看出来。
接着是 Generator 函数的写法。
function chainAnimationsGenerator(elem, animations) {
return spawn(function*() {
let ret = null;
try {
for(let anim of animations) {
ret = yield anim(elem);
}
} catch(e) {
/* 忽略错误,继续执行 */
}
return ret;
});
}