对象的属性操作
有四个操作会忽略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;
});
}
网友评论