對象的屬性操作
有四個操作會忽略enumerable為false的屬性
- for..in循環(huán):只遍歷對象自身和繼承的可枚舉屬性
- Object.keys():返回對象自身可枚舉的屬性鍵名
- JSON.stringify():只串行化自身可枚舉的屬性
- Object.assign():忽略enumerable為false的屬性,只拷貝自身的可枚舉屬性
可枚舉(enumerable),最初引入的目的是為了讓某些屬性可以規(guī)避for..in操作,不然所有內部屬性和方法都會被遍歷。如對象原型的toString方法,以及數(shù)組的length屬性。而且只有for..in方法能遍歷到繼承的屬性,其他都不行
ES6的屬性遍歷方法
-
for..in
for..in循環(huán)遍歷對象自身和繼承的可枚舉屬性(不含Symbol屬性) -
Object,keys(obj)
object.keys返回一個數(shù)組,包含對象自身(不含繼承)的所有可枚舉屬性(不含Symbol屬性) -
Objcet.getOwnPropertyNames(obj)
Objcet.getOwnPropertyNames(obj)返回一個數(shù)組,包含對象自身的所有屬性(不含Symbol屬性,但是包括不可枚舉屬性)的鍵名 -
Objcet.getOwnPropertySymbols(obj)
Objcet.getOwnPropertySymbols(obj)返回一個數(shù)組,包含對象自身的所有Symbol屬性的鍵名 -
Reflect.oweKeys(obj)
Reflect.oweKeys(obj)返回一個數(shù)組,包含對象自身所有的鍵名,不管是Symbol或字符串,也不管是否可枚舉
以上5種方法遍歷對象的鍵名,遵守一下規(guī)則
- 首先遍歷數(shù)值鍵,按照數(shù)值升序排列
- 其次遍歷所有字符串鍵,按照加入時間升序排列
- 最后遍歷Symbol鍵,按照加入時間升序排列
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
對象新方法Object.assign
Object.assign用來將源對象所有可枚舉屬性,復制到目標對象中。至少需要兩個對象作為參數(shù),第一個是目標對象,后面的都是源對象
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 }
如果目標對象不是對象參數(shù)的話
- 如果是首參數(shù),那么會將其轉為對象
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()
//}
實際上是看非對象參數(shù)能否有對應的類進行實例,比如2是Number型,能由Number進行實例化得到,就相當于Number實例化了一個2的對象,而Number繼承了Object,就相當于轉化成對象了,對應也可以用在Boolean,String類型
注意:但是Null和undefined是無法轉為對象,沒有對應的類給其實例化,所以他們作為首參數(shù)的話,會報錯
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
- 非對象不出現(xiàn)在首參數(shù)
注意:如果非對象參數(shù)出現(xiàn)在源對象的位置(即非首參數(shù)),那么處理規(guī)則有些不同。參數(shù)都會轉成對象,如果無法轉成對象,那就跳過,不會報錯。但是除了字符串會以數(shù)組形式拷貝進目標對象,其他值(這里說的是基本數(shù)據(jù)類型)不會產(chǎn)生效果
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可以用來處理數(shù)組,但會把數(shù)組視為對象
Object.assign([1, 2, 3], [4, 5])
// [4,5,3]
其中,4會覆蓋,5會覆蓋2,因為它們在數(shù)組的同一位置,對應位置覆蓋,數(shù)組其實就是特殊的排列對象,只不過是有序的而已
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(取值函數(shù)),那么會先求值,再進行賦值
// 源對象
const source = {
//屬性是取值函數(shù)
get foo(){return 1}
};
//目標對象
const target = {};
Object.assign(target,source);
//{foo ; 1} 此時foo的值是get函數(shù)的求值結果
常見用途
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);
//根據(jù)原型對象,創(chuàng)建新的空對象,再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對象此時的值最好都是簡單類型的值,否則函數(shù)會失效。
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類
傳統(tǒng)生成實例對象是通過構造函數(shù),如
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的構造函數(shù)Point,對應ES6Point類的構造方法
ES6的“類”Class,可以看作構造函數(shù)的另一種寫法
class Point {
// ...
}
typeof Point // 'function'
Point === Point.prototype.constructor //true
即,類的數(shù)據(jù)類型就是函數(shù),類本身就指向構造函數(shù)
當然使用也直接對類使用new命令,與構造函數(shù)用法一致
class Bar {
doStuff() {
console.log('stuff');
}
}
var b = new Bar();
b.doStuff() // "stuff"
構造函數(shù)的prototype屬性,在 ES6 的“類”上面繼續(xù)存在。事實上,類的所有方法都定義在類的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方法對應的是構造函數(shù),返回的實例都不是該構造函數(shù)了,所以肯定也不同了
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函數(shù)會進行變量提升==
靜態(tài)方法
類相當于實例原型,類中定義的方法,相當于原型上的方法,都會被實例繼承,但如果在類的方法前,加上static關鍵字,就表示該方法不會被實例繼承,而是通過類來調用,成為靜態(tài)方法
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關鍵字,表明該方法是一個靜態(tài)方法,可以直接在Foo類上調用(Foo.classMethod()),而不是在Foo類的實例上調用。如果在實例上調用靜態(tài)方法,會拋出一個錯誤,表示不存在該方法。
所以,如果靜態(tài)方法包含this關鍵字,this指向該類,而不是實例
class Foo {
static bar() {
this.baz();
}
static baz() {
console.log('hello');
}
baz() {
console.log('world');
}
}
Foo.bar() // hello
父類的靜態(tài)方法,可以被子類繼承
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
}
Bar.classMethod() // 'hello'
上面代碼中,父類Foo有一個靜態(tài)方法,子類Bar可以調用這個方法。
靜態(tài)方法也是可以從super對象上調用的。
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}
Bar.classMethod() // "hello, too"
Super關鍵字可以說是超類,指向父類對象
問題:靜態(tài)方法是怎么做到的?
私有方法和私有屬性
只能在類內部訪問的方法和屬性,外部不能訪問。這是常見需求,有利于代碼封裝,但ES6不提供,只能變通模擬實現(xiàn)
- 將私有方法移出模塊,再用
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關鍵字實現(xiàn)繼承
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方法之中,都出現(xiàn)了super關鍵字,它在這里表示父類的構造函數(shù),用來新建父類的this對象。
子類必須在constructor方法中調用super方法,否則新建實例時會報錯。這是因為子類自己的this對象,必須先通過父類的構造函數(shù)完成塑造,得到與父類同樣的實例屬性和方法,然后再對其進行加工,加上子類自己的實例屬性和方法。如果不調用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 的行為完全一致。
最后,父類的靜態(tài)方法,也會被子類繼承。
class A {
static hello() {
console.log('hello world');
}
}
class B extends A {
}
B.hello() // hello world
ES5 的繼承,實質是先創(chuàng)造子類的實例對象this,然后再將父類的方法添加到this上面(Parent.apply(this))。ES6 的繼承機制完全不同,實質是先將父類實例對象的屬性和方法,加到this上面(所以必須先調用super方法),然后再用子類的構造函數(shù)修改this。
Object.getPrototypeOf()
Object.getPrototypeOf方法可以用來從子類上獲取父類
Object.getPrototypeOf(ColorPoint) === Point
// true
因此,可以使用這個方法判斷,一個類是否繼承了另一個類。
Super關鍵字
super這個關鍵字,既可以當作函數(shù)使用,也可以當作對象使用。在這兩種情況下,它的用法完全不同。
-
super作為函數(shù)調用,代表父類的構造函數(shù)。ES6規(guī)定,子類的構造函數(shù)必須執(zhí)行一次super函數(shù)
class A {}
class B extends A {
constructor() {
super();
}
}
注意,super雖然代表了父類A的構造函數(shù),但是返回的是子類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指向當前正在執(zhí)行的函數(shù),可以看到,在super()執(zhí)行時,它指向的是子類B的構造函數(shù),而不是父類A的構造函數(shù)。也就是說,super()內部的this指向的是B
作為函數(shù)時,super()只能用在子類的構造函數(shù)之中,用在其他地方就會報錯
class A {}
class B extends A {
m() {
super(); // 報錯
}
}
-
super作為對象時,在普通方法時,指向父類的原型對象;在靜態(tài)方法時指向父類。
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規(guī)定,在子類普通方法中通過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。也就是說,實際上執(zhí)行的是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作為對象,用在靜態(tài)方法中,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在靜態(tài)方法之中指向父類,在普通方法之中指向父類的原型對象。
另外,在子類的靜態(tài)方法中通過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
因為實例都獲取不了子類的靜態(tài)方法,所以也很好理解
注意: 使用super的時候,要顯式指定是作為函數(shù),還是對象使用,否則會報錯(函數(shù)就加括號(),對象不用,但后面要加屬性)
class A {}
class B extends A {
constructor() {
super();
console.log(super); // 報錯
}
}
類的 prototype 屬性和 __proto__屬性
ES5中,每個對象都有__proto__屬性,指向對應構造函數(shù)的prototype屬性。Class 作為構造函數(shù)的語法糖,同時有prototype屬性和__proto__,因此存在兩條繼承鏈
(1) 子類的__proto__屬性,表示構造函數(shù)的繼承,總是指向父類
(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屬性
這樣的結果是因為,類的繼承按照下面模式實現(xiàn)的
class A {
}
class B {
}
// B 是實例繼承 A 的實例
Object.setPrototypeOf(B.prototype, A.prototype)
// B 繼承 A 的靜態(tài)屬性
Object.setPrototypeOf(B, A)
let b = new B()
Object.setPrototypeOf方法的實現(xiàn)。
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);作為一個構造函數(shù),子類(B)的原型對象(prototype屬性)是父類的原型對象(prototype屬性)的實例
即對象對應對象,原型對應原型
B.prototype = Object.create(A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;
實例的__proto__屬性
子類實例的__proto__屬性,指向子類,其實就一個原型鏈的問題。不作多解釋
問題:畫出原型鏈的表達圖!
Set和Map數(shù)據(jù)結構
ES6新的數(shù)據(jù)結構Set。類似數(shù)組,但是成員值都是唯一的
Set本身是一個構造函數(shù),生成Set數(shù)據(jù)結構
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不會添加重復的值,這個方法可以用來進行數(shù)組去重
如下,Set函數(shù)接受一個數(shù)組(或者其他具有iterable接口的其他數(shù)據(jù)結構)作為參數(shù),用來初始化
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結構轉換成數(shù)組的話可以用[...Set]或者用Array.form,(...是擴展運算符,內部使用for...of循環(huán),一個針對iterator(可遍歷)結構的循環(huán))
// 去除數(shù)組的重復成員
[...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"
就本身來說,我自己項目中出現(xiàn)過,比如用戶進行前端添加人員(后臺接受的是人員ID數(shù)組,但在前端得顯示添加的人員姓名),根據(jù)數(shù)據(jù)結構不同有幾個不同方法
- 初始化兩個數(shù)組一個人員ID數(shù)組,一個人員Name數(shù)組,當用戶點擊添加的時候進行任一判斷存不存在數(shù)組中,不存在就
push - 初始化一個對象,人員ID是鍵,值為人員Name,進行添加的時候用人員ID作為屬性判斷值是不是為空,空就添加
- 初始化一個
Set結構,當用戶進行添加重復人員的時候,Set結構不會做出相應,在用戶界面看到的一直都會是去重后的人員數(shù)組
==注意:第1種方法解決的話每次添加需要去循環(huán)一下數(shù)組,時間復雜度是O(n),第2種的話直接利用對象的鍵去查找有無值,性能上會比第一種好很多,第3種的話就省去了判斷這個環(huán)節(jié),去取值的話也直接用鍵去取就行了,性能也比較高==
Set實例屬性和方法
-
Set.prototype.constructor:構造函數(shù),默認是Set函數(shù) -
Set.prototype.size:返回Set實例的成員總數(shù)
Set實例的方法分為兩大類:操作方法(用于操作數(shù)據(jù))和遍歷方法(用于遍歷成員)
操作方法:
-
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(): 使用回調函數(shù)遍歷每個成員
keys方法、values方法、entries方法返回的都是遍歷器對象(詳見《Iterator 對象》一章)。由于 Set 結構沒有鍵名,只有鍵值(或者說鍵名和鍵值是同一個值),所以keys方法和values方法的行為完全一致。
WeakSet
結構與Set類似,也是不重復值的集合。區(qū)別在于
-
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可以接受一個數(shù)組或類數(shù)組的成員對象作為參數(shù),如
const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}
a數(shù)組有兩個成員,也是數(shù)組,數(shù)組也屬于對象,則成員對象會自動成為WeakSet成員
而如果數(shù)組成員不是對象,就會出錯,就像
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
傳統(tǒng)JS的對象,本質上是鍵值對(Hash)的集合,傳統(tǒng)只能用字符串當作鍵,這給它使用帶來限制,如
const data = {};
const element = document.getElementById('myDiv');
data[element] = 'metadata';
data['[object HTMLDivElement]'] // "metadata"
原意是想把DOM節(jié)點存為對象的鍵,但對象只接受字符串作為鍵名,所以element被自動轉為字符串[object HTMLDivElement]
而Map數(shù)據(jù)結構的"鍵"可以接受各種類型的值(包括對象),是一種“值-值”對應的感覺。
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接受一個數(shù)組的話
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構造函數(shù)接受數(shù)組作為參數(shù)的時候,執(zhí)行的是下面的算法
const items = [
['name', '張三'],
['title', 'Author']
];
const map = new Map();
items.forEach(
([key, value]) => map.set(key, value)
);
事實上,不僅僅是數(shù)組,任何具有Iterator 接口、且每個成員都是一個雙元素的數(shù)組的數(shù)據(jù)結構(詳見《Iterator 對象》一章)都可以當作Map構造函數(shù)的參數(shù)。這就是說,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鍵是簡單類型的值的話(數(shù)字,字符串,布爾值),兩個值嚴格(嚴格的意思就是三等號為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結構的成員總數(shù) -
Map.prototype.set(key, value)
該方法設置鍵名key的值為value,返回整個Map結構,若鍵key已經(jīng)有值那就更新鍵值,由于是返回整個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所有成員
轉換成數(shù)組的話用...很方便
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方法,與數(shù)組的forEach方法類似,也可以實現(xiàn)遍歷。
map.forEach((value, key, map) => {
console.log("Key: %s, value: %s", key,value)
})
forEach方法還可以接受第二個參數(shù),用來綁定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);
第二個參數(shù),指定對象上下文,傳入是哪個對象變量,this指向reporter
Map與其他數(shù)據(jù)結構互相轉換
-
Map轉為數(shù)組
前面提過,最方便的就是使用擴展運算符(...)
const myMap = new Map()
.set(true, 7)
.set({foo: 3}, ['abc'])
[...myMap]
//[ [true, 7], [{foo: 3}, ['abc'] ] ]
-
數(shù)組轉為Map
將數(shù)組傳入Map構造函數(shù),就可以轉為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],數(shù)組的話直接拼接數(shù)組的值
- 對象轉為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
要區(qū)分兩種情況,一種是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的鍵名有非字符串,這時候可以選擇轉為數(shù)組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 就是一個數(shù)組,且每個數(shù)組成員本身,又是一個有兩個成員的數(shù)組。這時,它可以一一對應地轉為 Map。這往往是 Map 轉為數(shù)組 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節(jié)點作為鍵名去保存,因為當DOM節(jié)點刪除的時候,狀態(tài)也會自動消失,不用擔心內存泄漏
總結
Set,WeakSet,Map,WeakMap一個是類數(shù)組,一個類對象,可以說是數(shù)組和對象的擴展應用把,比數(shù)組和對象有更強大的功能,也可以互相轉換成數(shù)組和對象
Promise對象
簡單來說,是一個容器,保存著某個未來才會結束的事件(通常是一個異步操作)的結果,比如一個請求,請求需要時間,等到響應后才會返回結果。語法上說,Promise是一個對象,它可以獲取異步操作的消息。有了Promise對象,可以將異步操作以同步操作流程表達出來,避免了層層嵌套的回調。
Promise對象會將所有執(zhí)行函數(shù)放在then之后執(zhí)行,而不管你的操作是不是異步的,就是說同步的,經(jīng)過Promise包裝后就會變成異步執(zhí)行
const f = () => console.log('now');
Promise.resolve().then(f);
console.log('next');
// next
// now
那么有沒有一種方法,讓同步函數(shù)同步執(zhí)行,異步函數(shù)異步執(zhí)行,并且讓它們具有統(tǒng)一的 API 呢?回答是可以的,并且還有兩種寫法。
第一種寫法是用async函數(shù)來寫
const f = () => console.log('now');
(async () => f())();
console.log('next');
// now
// next
上面代碼中,第二行是一個立即執(zhí)行的匿名函數(shù),會立即執(zhí)行里面的async函數(shù),因此如果f是同步的,就會得到同步的結果;如果f是異步的,就可以用then指定下一步,就像下面的寫法。
(async () => f())()
.then(...)
需要注意的是,async () => f()會吃掉f()拋出的錯誤。所以,如果想捕獲錯誤,要使用promise.catch方法。
(async () => f())()
.then(...)
.catch(...)
Generator(生成器)函數(shù)
是ES6提供的一種異步編程解決方案
從語法上,可以理解成一個狀態(tài)機,封裝了多個內部狀態(tài)
執(zhí)行Generator函數(shù)會返回一個遍歷器對象,也就是說,Generator函數(shù)除了狀態(tài)機,還是一個遍歷器對象生成函數(shù)。返回的都是遍歷器對象,可以依次遍歷Generator函數(shù)內部的每一個狀態(tài)
形式上,Generator 函數(shù)是一個普通函數(shù),但是有兩個特征。一是,function關鍵字與函數(shù)名之間有一個星號;二是,函數(shù)體內部使用yield表達式,定義不同的內部狀態(tài)(yield在英語里的意思就是“產(chǎn)出”)。
function* helloWorldGenerator(){
yield 'hello'
yield 'world'
return 'ending'
}
var hw = helloWorldGenerator()
調用Generator函數(shù)后,該函數(shù)并不執(zhí)行,返回的是一個指向內部狀態(tài)的指針對象,也就是遍歷器對象
下一步,必須調用遍歷器對象的next方法,使得指針移向下一個狀態(tài)。就是說,每一次調用next方法,內部指針就從函數(shù)頭部或上一次停下來的地方開始執(zhí)行,直到遇到下一個yield表達式(或return語句)為止。換言之,Generator函數(shù)是分段執(zhí)行,yield表達式是暫停執(zhí)行的標記,而next可以恢復執(zhí)行
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 函數(shù),返回一個遍歷器對象,代表 Generator 函數(shù)的內部指針。以后,每次調用遍歷器對象的next方法,就會返回一個有著value和done兩個屬性的對象。value屬性表示當前的內部狀態(tài)的值,是yield表達式后面那個表達式的值;done屬性是一個布爾值,表示是否遍歷結束
yield表達式
yield表達式相當于一個暫停標志,遇到它,便返回一個對象,value值就是yield后面帶的值,繼續(xù)調用next往下執(zhí)行到下一個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函數(shù)中
如果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表達式用作函數(shù)參數(shù)或放在賦值表達式的右邊可以不加括號
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方法,等于該對象的遍歷器生成函數(shù),調用該函數(shù)會返回該對象的一個遍歷器對象
由于Generator函數(shù)就是遍歷器生成器,可以將Generator函數(shù)賦值給對象的iterator屬性,從而使該對象具有Iterator接口
var myIterable = {}
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1,2,3]
上面代碼中,Generator 函數(shù)賦值給Symbol.iterator屬性,從而使得myIterable對象具有了 Iterator 接口,可以被...運算符遍歷了。
next方法的參數(shù)
yield表達式本身沒有返回值,或者說總是返回undefined。next方法可以帶一個參數(shù),該參數(shù)就會被當作上一個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 函數(shù)f,如果next方法沒有參數(shù),每次運行到yield表達式,變量reset的值總是undefined。當next方法帶一個參數(shù)true時,變量reset就被重置為這個參數(shù)(即true),因此i會等于-1,下一輪循環(huán)就會從-1開始遞增
for...of循環(huán)
for...of循環(huán)可以自動遍歷 Generator 函數(shù)運行時生成的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循環(huán),依次顯示 5 個yield表達式的值。這需要注意的是,一旦next方法的返回對象的done屬性為true,for...of循環(huán)就會中止,且不包含該返回對象,所以return語句返回的6,不包括在for...of循環(huán)中
原生的js對象沒有遍歷接口,無法使用for...of循環(huán),通過Generator函數(shù)為它加上這個接口就可以用了。
除了for...of循環(huán)以外,擴展運算符(...)、解構賦值和Array.from方法內部調用的,都是遍歷器接口。這意味著,它們都可以將 Generator 函數(shù)返回的 Iterator 對象,作為參數(shù)。
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 循環(huán)
for (let n of numbers()) {
console.log(n)
}
// 1
// 2
throw()
next(),throw(),return()共同點
next(),throw(),return()這三個方法本質上是同一件事,可以放在一起理解。它們的作用都是讓Generator函數(shù)恢復執(zhí)行,并且使用不同的語句替換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方法沒有參數(shù),就相當于替換成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函數(shù)內部,調用另一個Generator函數(shù)。需要在前者函數(shù)體內部,自己手動完成遍歷
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函數(shù),在bar里面調用foo,需要手動遍歷foo。如果多個Generator函數(shù)嵌套,寫起來就比較麻煩
ES6提供yield*表達式,作為用在在一個Generator函數(shù)中調用另一個Generator函數(shù)
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"
實際上,任何數(shù)據(jù)結構,只要有Iterator接口,都可以被yield*遍歷,也就是說yield*可以遍歷含有遍歷器對象或遍歷器對象接口的對象
在yield*后面的Generator函數(shù)沒有return語句時,相當于是for..of的簡寫形式,有return語句的時候,則將這個數(shù)值返回獲取,如果被代理的 Generator 函數(shù)有return語句,那么就可以向代理它的 Generator 函數(shù)返回數(shù)據(jù)。
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方法的時候,屏幕上會有輸出,這是因為函數(shù)foo的return語句,向函數(shù)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' ]
上面代碼有兩次遍歷。第一次是擴展運算符遍歷函數(shù)logReturned返回的遍歷器對象,第二次是yield*語句遍歷函數(shù)genFuncWithReturn返回的遍歷器對象。但是,函數(shù)genFuncWithReturn的return語句的返回值The result,會返回給函數(shù)logReturned內部的result變量,因此會有終端輸出。
而且是先執(zhí)行一次函數(shù)代碼,再執(zhí)行遍歷
yield*可以很方便取出嵌套數(shù)組的所有成員
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
內部運行,如果是數(shù)組的話執(zhí)行if操作,不然就輸出
使用擴展運算符...也是一樣的效果
[...iterTree(tree)] // ["a", "b", "c", "d", "e"]
下面是一個稍微復雜的例子,使用yield*語句遍歷完全二叉樹。
// 下面是二叉樹的構造函數(shù),
// 三個參數(shù)分別是左樹、當前節(jié)點和右樹
function Tree(left, label, right) {
this.left = left;
this.label = label;
this.right = right;
}
// 下面是中序(inorder)遍歷函數(shù)。
// 由于返回的是一個遍歷器,所以要用generator函數(shù)。
// 函數(shù)體內采用遞歸算法,所以左樹和右樹要用yield*遍歷
function* inorder(t) {
if (t) {
yield* inorder(t.left);
yield t.label;
yield* inorder(t.right);
}
}
// 下面生成二叉樹
function make(array) {
// 判斷是否為葉節(jié)點
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 函數(shù)的this
Generator 函數(shù)總是返回一個遍歷器,ES6規(guī)定這個遍歷器是Generator的實例,也繼承了Generator函數(shù)的prototype對象上的方法
function* g() {}
g.prototype.hello = function () {
return 'hi!';
};
let obj = g();
obj instanceof g // true
obj.hello() // 'hi!'
上面代碼表明,Generator 函數(shù)g返回的遍歷器obj,是g的實例,而且繼承了g.prototype。但是,如果把g當作普通的構造函數(shù),并不會生效,因為g返回的總是遍歷器對象,而不是this對象。
function* g() {
this.a = 11;
}
let obj = g();
obj.next();
obj.a // undefined
上面代碼中,Generator 函數(shù)g在this對象上面添加了一個屬性a,但是obj對象拿不到這個屬性。
Generator 函數(shù)也不能跟new命令一起用,會報錯,因為不是構造函數(shù)。
function* F() {
yield this.x = 2;
yield this.y = 3;
}
new F()
// TypeError: F is not a constructor
問題:能否讓Generator函數(shù)返回一個正常的對象實例,既可以用next方法,又可以獲得正常的this
下面一個變通方法。首先,生成一個空對象,使用call方法綁定Generator函數(shù)內部的this,這樣,構造函數(shù)調用以后,這個空對象就是Generator函數(shù)的實例對象了
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 對象。這個對象執(zhí)行三次next方法(因為F內部有兩個yield表達式),完成 F 內部所有代碼的運行。這時,所有內部屬性都綁定在obj對象上了,因此obj對象也就成了F的實例。
上面代碼中,執(zhí)行的是遍歷器對象f,但是生成的對象實例是obj,有沒有辦法將兩個對象統(tǒng)一
一個辦法是將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改成構造函數(shù),就可以對它執(zhí)行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 函數(shù)的應用
(1)異步操作的同步化表達
如Ajax,是個典型的異步操作,通過 Generator 函數(shù)部署 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函數(shù),就是通過 Ajax 操作獲取數(shù)據(jù)??梢钥吹?,除了多了一個yield,它幾乎與同步操作的寫法完全一樣。注意,makeAjaxCall函數(shù)中的next方法,必須加上response參數(shù),因為yield表達式,本身是沒有值的,總是等于undefined。
(2)控制流管理
如果有一個多步操作非常耗時,采用回調函數(shù),可能會像下面這樣
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();
上面代碼已經(jīng)把回調函數(shù),改成了直線執(zhí)行的形式,但是加入了大量 Promise 的語法。Generator 函數(shù)可以進一步改善代碼運行流程。
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
}
}
然后,使用一個函數(shù),按次序自動執(zhí)行所有步驟。
scheduler(longRunningTask(initialValue));
function scheduler(task) {
var taskObj = task.next(task.value);
// 如果Generator函數(shù)未結束,就繼續(xù)調用
if (!taskObj.done) {
task.value = taskObj.value
scheduler(task);
}
}
(3)部署Iterator接口
利用Generator函數(shù),可以在任何對象上部署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函數(shù)的異步應用
async函數(shù)
使異步操作更加方便,是 Generator 函數(shù)的改進
前文有一個 Generator 函數(shù),依次讀取兩個文件。
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());
};
上面代碼的函數(shù)gen可以寫成async函數(shù),就是下面這樣。
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函數(shù)對 Generator 函數(shù)的改進,體現(xiàn)在四點:
- 內置執(zhí)行器
Generator 函數(shù)的執(zhí)行必須依靠執(zhí)行器,或者自行調用next方法, 而async函數(shù)內置執(zhí)行器,與普通函數(shù)一模一樣,只需要一行
asyncReadFile();
- 更好的語義
async和await,比起星號和yield,語義更清楚了。async表示函數(shù)里有異步操作,await表示緊跟在后面的表達式需要等待結果。
更廣的適用性
async函數(shù)的await命令后面,可以是Promise對象和原始類型的值(數(shù)值、字符串和布爾值,但這時會自動轉成立即resolved的Promise對象)。返回值是 Promise。
async函數(shù)的返回值是 Promise 對象,這比 Generator 函數(shù)的返回值是 Iterator 對象方便多了。你可以用then方法指定下一步的操作。
進一步說,async函數(shù)完全可以看作多個異步操作,包裝成的一個 Promise 對象,而await命令就是內部then命令的語法糖。
基本用法
async 函數(shù)有多種使用形式。
// 函數(shù)聲明
async function foo() {}
// 函數(shù)表達式
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(…);
// 箭頭函數(shù)
const foo = async () => {};
語法
async函數(shù)返回一個 Promise 對象
async函數(shù)內部return語句返回的值,會成為then方法回調函數(shù)的參數(shù)
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
上面代碼中,函數(shù)f內部return命令返回的值,會被then方法回調函數(shù)接收到
async函數(shù)內部拋出錯誤,會導致返回的 Promise 對象變?yōu)?code>reject狀態(tài)。拋出的錯誤對象會被catch方法回調函數(shù)接收到
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命令的參數(shù)是數(shù)值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處理。
這個例子還演示了如何實現(xiàn)休眠效果。JavaScript 一直沒有休眠的語法,但是借助await命令就可以讓程序停頓指定的時間。下面給出了一個簡化的sleep實現(xiàn)。
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命令后面的異步操作,如果不存在繼發(fā)關系,最好讓它們同時觸發(fā)。
let foo = await getFoo();
let bar = await getBar();
上面代碼中,getFoo和getBar是兩個獨立的異步操作(即互不依賴),被寫成繼發(fā)關系。這樣比較耗時,因為只有getFoo完成以后,才會執(zhí)行getBar,完全可以讓它們同時觸發(fā)。
// 寫法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 寫法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
上面兩種寫法,getFoo和getBar都是同時觸發(fā),這樣就會縮短程序的執(zhí)行時間。
第三點,await命令只能用在async函數(shù)之中,普通函數(shù)中的話會報錯。
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 報錯
docs.forEach(function (doc) {
await db.post(doc);
});
}
上面代碼會報錯,因為await用在普通函數(shù)之中了。但是,如果將forEach方法的參數(shù)改成async函數(shù),也有問題。
function dbFuc(db) { //這里不需要 async
let docs = [{}, {}, {}];
// 可能得到錯誤結果
docs.forEach(async function (doc) {
await db.post(doc);
});
}
上面代碼可能不會正常工作,原因是這時三個db.post操作將是并發(fā)執(zhí)行,也就是同時執(zhí)行,而不是繼發(fā)執(zhí)行。正確的寫法是采用for循環(huán)。
async function dbFuc(db) {
let docs = [{}, {}, {}];
for (let doc of docs) {
await db.post(doc);
}
}
如果確實希望多個請求并發(fā)執(zhí)行,可以使用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 函數(shù)的實現(xiàn)原理
async 函數(shù)的實現(xiàn)原理,就是將 Generator 函數(shù)和自動執(zhí)行器,包裝在一個函數(shù)里。
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}
所有的async函數(shù)都可以寫成上面的第二種形式,其中的spawn函數(shù)就是自動執(zhí)行器。
下面給出spawn函數(shù)的實現(xiàn),基本就是前文自動執(zhí)行器的翻版。
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 函數(shù)與 Promise、Generator 函數(shù)的比較。
假定某個 DOM 元素上面,部署了一系列的動畫,前一個動畫結束,才能開始后一個。如果當中有一個動畫出錯,就不再往下執(zhí)行,返回上一個成功執(zhí)行的動畫的返回值。
首先是 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) {
/* 忽略錯誤,繼續(xù)執(zhí)行 */
}).then(function() {
return ret;
});
}
雖然 Promise 的寫法比回調函數(shù)的寫法大大改進,但是一眼看上去,代碼完全都是 Promise 的 API(then、catch等等),操作本身的語義反而不容易看出來。
接著是 Generator 函數(shù)的寫法。
function chainAnimationsGenerator(elem, animations) {
return spawn(function*() {
let ret = null;
try {
for(let anim of animations) {
ret = yield anim(elem);
}
} catch(e) {
/* 忽略錯誤,繼續(xù)執(zhí)行 */
}
return ret;
});
}