1.10 Set和Map数据结构
1.10.1 Set
介绍:
Set
数据结构类似数组,但所有成员的值唯一。
Set
本身为一个构造函数,用来生成Set
数据结构,使用add
方法来添加新成员。
let a = new Set(); [1,2,2,1,3,4,5,4,5].forEach(x=>a.add(x)); for(let k of a){ console.log(k) }; // 1 2 3 4 5
基础使用:
let a = new Set([1,2,3,3,4]); [...a]; // [1,2,3,4] a.size; // 4 // 数组去重 [...new Set([1,2,3,4,4,4])];// [1,2,3,4]
注意:
- 向
Set
中添加值的时候,不会类型转换,即5
和'5'
是不同的。
[...new Set([5,'5'])]; // [5, "5"]
属性和方法:
- 属性:
Set.prototype.constructor
:构造函数,默认就是Set
函数。Set.prototype.size
:返回Set
实例的成员总数。
- 操作方法:
add(value)
:添加某个值,返回 Set 结构本身。delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。has(value)
:返回一个布尔值,表示该值是否为Set的成员。clear()
:清除所有成员,没有返回值。
let a = new Set(); a.add(1).add(2); // a => Set(2) {1, 2} a.has(2); // true a.has(3); // false a.delete(2); // true a => Set(1) {1} a.clear(); // a => Set(0) {}
数组去重:
let a = new Set([1,2,3,3,3,3]);
1.10.2 Set的应用
数组去重:
// 方法1 [...new Set([1,2,3,4,4,4])]; // [1,2,3,4] // 方法2 Array.from(new Set([1,2,3,4,4,4])); // [1,2,3,4]
遍历和过滤:
let a = new Set([1,2,3,4]); // map 遍历操作 let b = new Set([...a].map(x =>x*2));// b => Set(4) {2,4,6,8} // filter 过滤操作 let c = new Set([...a].filter(x =>(x%2) == 0)); // b => Set(2) {2,4}
获取并集、交集和差集:
let a = new Set([1,2,3]); let b = new Set([4,3,2]); // 并集 let c1 = new Set([...a, ...b]); // Set {1,2,3,4} // 交集 let c2 = new Set([...a].filter(x => b.has(x))); // set {2,3} // 差集 let c3 = new Set([...a].filter(x => !b.has(x))); // set {1}
- 遍历方法:
keys()
:返回键名的遍历器。values()
:返回键值的遍历器。entries()
:返回键值对的遍历器。forEach()
:使用回调函数遍历每个成员。
Set
遍历顺序是插入顺序,当保存多个回调函数,只需按照顺序调用。但由于Set
结构没有键名只有键值,所以keys()
和values()
是返回结果相同。
let a = new Set(['a','b','c']); for(let i of a.keys()){console.log(i)}; // 'a' 'b' 'c' for(let i of a.values()){console.log(i)}; // 'a' 'b' 'c' for(let i of a.entries()){console.log(i)}; // ['a','a'] ['b','b'] ['c','c']
并且 还可以使用for...of
直接遍历Set
。
let a = new Set(['a','b','c']); for(let k of a){console.log(k)}; // 'a' 'b' 'c'
forEach
与数组相同,对每个成员执行操作,且无返回值。
let a = new Set(['a','b','c']); a.forEach((v,k) => console.log(k + ' : ' + v));
1.10.3 Map
由于传统的JavaScript
对象只能用字符串当做键,给开发带来很大限制,ES6增加Map
数据结构,使得各种类型的值(包括对象)都可以作为键。
Map
结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。 基础使用:
let a = new Map(); let b = {name: 'leo' }; a.set(b,'my name'); // 添加值 a.get(b); // 获取值 a.size; // 获取总数 a.has(b); // 查询是否存在 a.delete(b); // 删除一个值 a.clear(); // 清空所有成员 无返回
注意:
- 传入数组作为参数,指定键值对的数组。
let a = new Map([ ['name','leo'], ['age',18] ])
- 如果对同一个键多次赋值,后面的值将覆盖前面的值。
let a = new Map(); a.set(1,'aaa').set(1,'bbb'); a.get(1); // 'bbb'
- 如果读取一个未知的键,则返回
undefined
。
new Map().get('abcdef'); // undefined
- 同样的值的两个实例,在 Map 结构中被视为两个键。
let a = new Map(); let a1 = ['aaa']; let a2 = ['aaa']; a.set(a1,111).set(a2,222); a.get(a1); // 111 a.get(a2); // 222
遍历方法: Map 的遍历顺序就是插入顺序。
keys()
:返回键名的遍历器。values()
:返回键值的遍历器。entries()
:返回所有成员的遍历器。forEach()
:遍历 Map 的所有成员。
let a = new Map([ ['name','leo'], ['age',18] ]) for (let i of a.keys()){...}; for (let i of a.values()){...}; for (let i of a.entries()){...}; a.forEach((v,k,m)=>{ console.log(`key:${k},value:${v},map:${m}`) })
将Map结构转成数组结构:
let a = new Map([ ['name','leo'], ['age',18] ]) let a1 = [...a.keys()]; // a1 => ["name", "age"] let a2 = [...a.values()]; // a2 => ["leo", 18] let a3 = [...a.entries()];// a3 => [['name','leo'], ['age',18]]
1.10.4 Map与其他数据结构互相转换
- Map 转 数组
let a = new Map().set(true,1).set({f:2},['abc']); [...a]; // [[true:1], [ {f:2},['abc'] ]]
- 数组 转 Map
let a = [ ['name','leo'], [1, 'hi' ]] let b = new Map(a);
- Map 转 对象 如果所有 Map 的键都是字符串,它可以无损地转为对象。
如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。
function fun(s) { let obj = Object.create(null); for (let [k,v] of s) { obj[k] = v; } return obj; } const a = new Map().set('yes', true).set('no', false); fun(a) // { yes: true, no: false }
- 对象 转 Map
function fun(obj) { let a = new Map(); for (let k of Object.keys(obj)) { a.set(k, obj[k]); } return a; } fun({yes: true, no: false}) // Map {"yes" => true, "no" => false}
- Map 转 JSON
(1)Map键名都是字符串,转为对象JSON:
function fun (s) { let obj = Object.create(null); for (let [k,v] of s) { obj[k] = v; } return JSON.stringify(obj) } let a = new Map().set('yes', true).set('no', false); fun(a); // '{"yes":true,"no":false}'
(2)Map键名有非字符串,转为数组JSON:
function fun (map) { return JSON.stringify([...map]); } let a = new Map().set(true, 7).set({foo: 3}, ['abc']); fun(a) // '[[true,7],[{"foo":3},["abc"]]]'
- JSON 转 Map
(1)所有键名都是字符串:
function fun (s) { let strMap = new Map(); for (let k of Object.keys(s)) { strMap.set(k, s[k]); } return strMap; return JSON.parse(strMap); } fun('{"yes": true, "no": false}') // Map {'yes' => true, 'no' => false}
(2)整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组:
function fun2(s) { return new Map(JSON.parse(s)); } fun2('[[true,7],[{"foo":3},["abc"]]]') // Map {true => 7, Object {foo: 3} => ['abc']}
1.11 Proxy
proxy
用于修改某些操作的默认行为,可以理解为一种拦截外界对目标对象访问的一种机制,从而对外界的访问进行过滤和修改,即代理某些操作,也称“代理器”。
1.11.1 基础使用
proxy
实例化需要传入两个参数,target
参数表示所要拦截的目标对象,handler
参数也是一个对象,用来定制拦截行为。
let p = new Proxy(target, handler); let a = new Proxy({}, { get: function (target, handler){ return 'leo'; } }) a.name; // leo a.age; // leo a.abcd; // leo
上述a
实例中,在第二个参数中定义了get
方法,来拦截外界访问,并且get
方法接收两个参数,分别是目标对象和所要访问的属性,所以不管外部访问对象中任何属性都会执行get
方法返回leo
。
注意:
- 只能使用
Proxy
实例的对象才能使用这些操作。 - 如果
handler
没有设置拦截,则直接返回原对象。
let target = {}; let handler = {}; let p = new Proxy(target, handler); p.a = 'leo'; target.a; // 'leo'
同个拦截器函数,设置多个拦截操作:
let p = new Proxy(function(a, b){ return a + b; },{ get:function(){ return 'get方法'; }, apply:function(){ return 'apply方法'; } })
Proxy
支持的13种拦截操作:
13种拦截操作的详细介绍:打开阮一峰老师的链接。
get(target, propKey, receiver)
: 拦截对象属性的读取,比如proxy.foo和proxy['foo']。set(target, propKey, value, receiver)
: 拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。has(target, propKey)
: 拦截propKey in proxy的操作,返回一个布尔值。deleteProperty(target, propKey)
: 拦截delete proxy[propKey]的操作,返回一个布尔值。ownKeys(target)
: 拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。getOwnPropertyDescriptor(target, propKey)
: 拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。defineProperty(target, propKey, propDesc)
: 拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。preventExtensions(target)
: 拦截Object.preventExtensions(proxy),返回一个布尔值。getPrototypeOf(target)
: 拦截Object.getPrototypeOf(proxy),返回一个对象。isExtensible(target)
: 拦截Object.isExtensible(proxy),返回一个布尔值。setPrototypeOf(target, proto)
: 拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。apply(target, object, args)
: 拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。construct(target, args)
: 拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。
1.11.2 取消Proxy实例
使用Proxy.revocale
方法取消Proxy
实例。
let a = {}; let b = {}; let {proxy, revoke} = Proxy.revocale(a, b); proxy.name = 'leo'; // 'leo' revoeke(); proxy.name; // TypeError: Revoked
1.11.3 实现 Web服务的客户端
const service = createWebService('http://le.com/data'); service.employees().than(json =>{ const employees = JSON.parse(json); }) function createWebService(url){ return new Proxy({}, { get(target, propKey, receiver{ return () => httpGet(url+'/'+propKey); }) }) }
1.12 Promise对象
1.12.1 概念
主要用途:解决异步编程带来的回调地狱问题。
把Promise
简单理解一个容器,存放着某个未来才会结束的事件(通常是一个异步操作)的结果。通过Promise
对象来获取异步操作消息,处理各种异步操作。
Promise
对象2特点:
- 对象的状态不受外界影响。
Promise
对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise
这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
- 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
注意,为了行文方便,本章后面的resolve
d统一只指fulfilled
状态,不包含rejected
状态。
Promise
缺点
- 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
- 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
- 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
1.12.2 基本使用
Promise
为一个构造函数,需要用new
来实例化。
let p = new Promise(function (resolve, reject){ if(/*异步操作成功*/){ resolve(value); } else { reject(error); } })
Promise
接收一个函数作为参数,该函数两个参数resolve
和reject
,有JS引擎提供。
resolve
作用是将Promise
的状态从pending变成resolved,在异步操作成功时调用,返回异步操作的结果,作为参数传递出去。reject
作用是将Promise
的状态从pending变成rejected,在异步操作失败时报错,作为参数传递出去。
Promise
实例生成以后,可以用then
方法分别指定resolved
状态和rejected
状态的回调函数。
p.then(function(val){ // success... },function(err){ // error... })
几个例子来理解 :
- 当一段时间过后,
Promise
状态便成为resolved
触发then
方法绑定的回调函数。
function timeout (s){ return new Promise((resolve, reject){ setTimeout(result,ms, 'done'); }) } timeout(100).then(val => { console.log(val); })
Promise
新建后立刻执行。
let p = new Promise(function(resolve, reject){ console.log(1); resolve(); }) p.then(()=>{ console.log(2); }) console.log(3); // 1 // 3 // 2
异步加载图片:
function f(url){ return new Promise(function(resolve, reject){ const img = new Image (); img.onload = function(){ resolve(img); } img.onerror = function(){ reject(new Error( 'Could not load image at ' + url )); } img.src = url; }) }
resolve
函数和reject
函数的参数为resolve
函数或reject
函数:
p1
的状态决定了p2
的状态,所以p2
要等待p1
的结果再执行回调函数。
const p1 = new Promise(function (resolve, reject) { setTimeout(() => reject(new Error('fail')), 3000) }) const p2 = new Promise(function (resolve, reject) { setTimeout(() => resolve(p1), 1000) }) p2 .then(result => console.log(result)) .catch(error => console.log(error)) // Error: fail
调用resolve
或reject
不会结束Promise
参数函数的执行,除了return
:
new Promise((resolve, reject){ resolve(1); console.log(2); }).then(r => { console.log(3); }) // 2 // 1 new Promise((resolve, reject){ return resolve(1); console.log(2); }) // 1
1.12.3 Promise.prototype.then()
作用是为Promise
添加状态改变时的回调函数,then
方法的第一个参数是resolved
状态的回调函数,第二个参数(可选)是rejected
状态的回调函数。
then
方法返回一个新Promise
实例,与原来Promise
实例不同,因此可以使用链式写法,上一个then
的结果作为下一个then
的参数。
getJSON("/posts.json").then(function(json) { return json.post; }).then(function(post) { // ... });
1.12.4 Promise.prototype.catch()
Promise.prototype.catch
方法是.then(null, rejection)
的别名,用于指定发生错误时的回调函数。
getJSON('/posts.json').then(function(posts) { // ... }).catch(function(error) { // 处理 getJSON 和 前一个回调函数运行时发生的错误 console.log('发生错误!', error); });
如果 Promise
状态已经变成resolved
,再抛出错误是无效的。
const p = new Promise(function(resolve, reject) { resolve('ok'); throw new Error('test'); }); p .then(function(value) { console.log(value) }) .catch(function(error) { console.log(error) }); // ok
当promise
抛出一个错误,就被catch
方法指定的回调函数捕获,下面三种写法相同。
// 写法一 const p = new Promise(function(resolve, reject) { throw new Error('test'); }); p.catch(function(error) { console.log(error); }); // Error: test // 写法二 const p = new Promise(function(resolve, reject) { try { throw new Error('test'); } catch(e) { reject(e); } }); p.catch(function(error) { console.log(error); }); // 写法三 const p = new Promise(function(resolve, reject) { reject(new Error('test')); }); p.catch(function(error) { console.log(error); });
一般来说,不要在then
方法里面定义Reject
状态的回调函数(即then
的第二个参数),总是使用catch
方法。
// bad promise .then(function(data) { // success }, function(err) { // error }); // good promise .then(function(data) { //cb // success }) .catch(function(err) { // error });
1.12.5 Promise.prototype.finally()
finally
方法用于指定不管 Promise
对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
promise .then(result => {···}) .catch(error => {···}) .finally(() => {···});
finally
不接收任何参数,与状态无关,本质上是then
方法的特例。
promise .finally(() => { // 语句 }); // 等同于 promise .then( result => { // 语句 return result; }, error => { // 语句 throw error; } );
上面代码中,如果不使用finally
方法,同样的语句需要为成功和失败两种情况各写一次。有了finally
方法,则只需要写一次。
finally
方法总是会返回原来的值。
// resolve 的值是 undefined Promise.resolve(2).then(() => {}, () => {}) // resolve 的值是 2 Promise.resolve(2).finally(() => {}) // reject 的值是 undefined Promise.reject(3).then(() => {}, () => {}) // reject 的值是 3 Promise.reject(3).finally(() => {})
1.12.6 Promise.all()
用于将多个 Promise
实例,包装成一个新的 Promise
实例,参数可以不是数组,但必须是Iterator接口,且返回的每个成员都是Promise
实例。
const p = Promise.all([p1, p2, p3]);
p
的状态由p1
、p2
、p3
决定,分成两种情况。
- 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
- 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
// 生成一个Promise对象的数组 const promises = [2, 3, 5, 7, 11, 13].map(function (id) { return getJSON('/post/' + id + ".json"); }); Promise.all(promises).then(function (posts) { // ... }).catch(function(reason){ // ... });
上面代码中,promises
是包含 6 个 Promise 实例的数组,只有这 6 个实例的状态都变成fulfilled
,或者其中有一个变为rejected
,才会调用Promise.all
方法后面的回调函数。
注意:如果Promise
的参数中定义了catch
方法,则rejected
后不会触发Promise.all()
的catch
方法,因为参数中的catch
方法执行完后也会变成resolved
,当Promise.all()
方法参数的实例都是resolved
时就会调用Promise.all()
的then
方法。
const p1 = new Promise((resolve, reject) => { resolve('hello'); }) .then(result => result) .catch(e => e); const p2 = new Promise((resolve, reject) => { throw new Error('报错了'); }) .then(result => result) .catch(e => e); Promise.all([p1, p2]) .then(result => console.log(result)) .catch(e => console.log(e)); // ["hello", Error: 报错了]
如果参数里面都没有catch方法,就会调用Promise.all()的catch方法。
const p1 = new Promise((resolve, reject) => { resolve('hello'); }) .then(result => result); const p2 = new Promise((resolve, reject) => { throw new Error('报错了'); }) .then(result => result); Promise.all([p1, p2]) .then(result => console.log(result)) .catch(e => console.log(e)); // Error: 报错了
1.12.7 Promise.race()
与Promise.all
方法类似,也是将多个Promise
实例包装成一个新的Promise
实例。
const p = Promise.race([p1, p2, p3]);
与Promise.all
方法区别在于,Promise.race
方法是p1
, p2
, p3
中只要一个参数先改变状态,就会把这个参数的返回值传给p
的回调函数。
1.12.8 Promise.resolve()
将现有对象转换成 Promise
对象。
const p = Promise.resolve($.ajax('/whatever.json'));
1.12.9 Promise.reject()
返回一个rejected
状态的Promise
实例。
const p = Promise.reject('出错了'); // 等同于 const p = new Promise((resolve, reject) => reject('出错了')) p.then(null, function (s) { console.log(s) }); // 出错了
注意,Promise.reject()
方法的参数,会原封不动地作为reject
的理由,变成后续方法的参数。这一点与Promise.resolve
方法不一致。
const thenable = { then(resolve, reject) { reject('出错了'); } }; Promise.reject(thenable) .catch(e => { console.log(e === thenable) }) // true
1.13 Iterator和 for...of循环
1.13.1 Iterator遍历器概念
Iterator是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator三个作用:
- 为各种数据结构,提供一个统一的、简便的访问接口;
- 使得数据结构的成员能够按某种次序排列;
- Iterator 接口主要供ES6新增的
for...of
消费;
1.13.2 Iterator遍历过程
- 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
- 第一次调用指针对象的
next
方法,可以将指针指向数据结构的第一个成员。 - 第二次调用指针对象的
next
方法,指针就指向数据结构的第二个成员。 - 不断调用指针对象的
next
方法,直到它指向数据结构的结束位置。
每一次调用next
方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value
和done
两个属性的对象。
value
属性是当前成员的值;done
属性是一个布尔值,表示遍历是否结束;
模拟next
方法返回值:
let f = function (arr){ var nextIndex = 0; return { next:function(){ return nextIndex < arr.length ? {value: arr[nextIndex++], done: false}: {value: undefined, done: true} } } } let a = f(['a', 'b']); a.next(); // { value: "a", done: false } a.next(); // { value: "b", done: false } a.next(); // { value: undefined, done: true }
1.13.3 默认Iterator接口
若数据可遍历,即一种数据部署了Iterator接口。
ES6中默认的Iterator接口部署在数据结构的Symbol.iterator
属性,即如果一个数据结构具有Symbol.iterator
属性,就可以认为是可遍历。
Symbol.iterator
属性本身是函数,是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator
,它是一个表达式,返回Symbol
对象的iterator
属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内(参见《Symbol》一章)。
原生具有Iterator接口的数据结构有:
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
1.13.4 Iterator使用场景
- (1)解构赋值
对数组和Set
结构进行解构赋值时,会默认调用Symbol.iterator
方法。
let a = new Set().add('a').add('b').add('c'); let [x, y] = a; // x = 'a' y = 'b' let [a1, ...a2] = a; // a1 = 'a' a2 = ['b','c']
- (2)扩展运算符
扩展运算符(...
)也会调用默认的 Iterator 接口。
let a = 'hello'; [...a]; // ['h','e','l','l','o'] let a = ['b', 'c']; ['a', ...a, 'd']; // ['a', 'b', 'c', 'd']
- (2)yield*
yield*
后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
let a = function*(){ yield 1; yield* [2,3,4]; yield 5; } let b = a(); b.next() // { value: 1, done: false } b.next() // { value: 2, done: false } b.next() // { value: 3, done: false } b.next() // { value: 4, done: false } b.next() // { value: 5, done: false } b.next() // { value: undefined, done: true }
- (4)其他场合
由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。 - for...of
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()(比如
new Map([['a',1],['b',2]])
) - Promise.all()
- Promise.race()
1.13.5 for...of循环
只要数据结构部署了Symbol.iterator
属性,即具有 iterator 接口,可以用for...of
循环遍历它的成员。也就是说,for...of
循环内部调用的是数据结构的Symbol.iterato
方法。
使用场景:
for...of
可以使用在数组,Set
和Map
结构,类数组对象,Genetator对象和字符串。
- 数组
for...of
循环可以代替数组实例的forEach
方法。
let a = ['a', 'b', 'c']; for (let k of a){console.log(k)}; // a b c a.forEach((ele, index)=>{ console.log(ele); // a b c console.log(index); // 0 1 2 })
与for...in
对比,for...in
只能获取对象键名,不能直接获取键值,而for...of
允许直接获取键值。
let a = ['a', 'b', 'c']; for (let k of a){console.log(k)}; // a b c for (let k in a){console.log(k)}; // 0 1 2
- Set和Map
可以使用数组作为变量,如for (let [k,v] of b){...}
。
let a = new Set(['a', 'b', 'c']); for (let k of a){console.log(k)}; // a b c let b = new Map(); b.set('name','leo'); b.set('age', 18); b.set('aaa','bbb'); for (let [k,v] of b){console.log(k + ":" + v)}; // name:leo // age:18 // aaa:bbb
- 类数组对象
// 字符串 let a = 'hello'; for (let k of a ){console.log(k)}; // h e l l o // DOM NodeList对象 let b = document.querySelectorAll('p'); for (let k of b ){ k.classList.add('test'); } // arguments对象 function f(){ for (let k of arguments){ console.log(k); } } f('a','b'); // a b
- 对象
普通对象不能直接使用for...of
会报错,要部署Iterator才能使用。
let a = {a:'aa',b:'bb',c:'cc'}; for (let k in a){console.log(k)}; // a b c for (let k of a){console>log(k)}; // TypeError
1.13.6 跳出for...of
使用break
来实现。
for (let k of a){ if(k>100) break; console.log(k); }
1.14 Generator函数和应用
1.14.1 基本概念
Generator
函数是一种异步编程解决方案。
原理:
执行Genenrator
函数会返回一个遍历器对象,依次遍历Generator
函数内部的每一个状态。
Generator
函数是一个普通函数,有以下两个特征:
function
关键字与函数名之间有个星号;- 函数体内使用
yield
表达式,定义不同状态;
通过调用next
方法,将指针移向下一个状态,直到遇到下一个yield
表达式(或return
语句)为止。简单理解,Generator
函数分段执行,yield
表达式是暂停执行的标记,而next
恢复执行。
function * f (){ yield 'hi'; yield 'leo'; return 'ending'; } let a = f(); a.next(); // {value: 'hi', done : false} a.next(); // {value: 'leo', done : false} a.next(); // {value: 'ending', done : true} a.next(); // {value: undefined, done : false}
1.14.2 yield表达式
yield
表达式是暂停标志,遍历器对象的next
方法的运行逻辑如下:
- 遇到
yield
就暂停执行,将这个yield
后的表达式的值,作为返回对象的value
属性值。 - 下次调用
next
往下执行,直到遇到下一个yield
。 - 直到函数结束或者
return
为止,并返回return
语句后面表达式的值,作为返回对象的value
属性值。 - 如果该函数没有
return
语句,则返回对象的value
为undefined
。
注意:
yield
只能用在Generator
函数里使用,其他地方使用会报错。
// 错误1 (function(){ yiled 1; // SyntaxError: Unexpected number })() // 错误2 forEach参数是个普通函数 let a = [1, [[2, 3], 4], [5, 6]]; let f = function * (i){ i.forEach(function(m){ if(typeof m !== 'number'){ yield * f (m); }else{ yield m; } }) } for (let k of f(a)){ console.log(k) }
yield
表达式如果用于另一个表达式之中,必须放在圆括号内。
function * a (){ console.log('a' + yield); // SyntaxErro console.log('a' + yield 123); // SyntaxErro console.log('a' + (yield)); // ok console.log('a' + (yield 123)); // ok }
yield
表达式用做函数参数或放在表达式右边,可以不加括号。
function * a (){ f(yield 'a', yield 'b'); // ok lei i = yield; // ok }
1.14.3 next方法
yield
本身没有返回值,或者是总返回undefined
,next
方法可带一个参数,作为上一个yield
表达式的返回值。
function * f (){ for (let k = 0; true; k++){ let a = yield k; if(a){k = -1}; } } let g =f(); g.next(); // {value: 0, done: false} g.next(); // {value: 1, done: false} g.next(true); // {value: 0, done: false}
这一特点,可以让Generator
函数开始执行之后,可以从外部向内部注入不同值,从而调整函数行为。
function * f(x){ let y = 2 * (yield (x+1)); let z = yield (y/3); return (x + y + z); } let a = f(5); a.next(); // {value : 6 ,done : false} a.next(); // {value : NaN ,done : false} a.next(); // {value : NaN ,done : true} // NaN因为yeild返回的是对象 和数字计算会NaN let b = f(5); b.next(); // {value : 6 ,done : false} b.next(12); // {value : 8 ,done : false} b.next(13); // {value : 42 ,done : false} // x 5 y 24 z 13
1.14.4 for...of循环
for...of
循环会自动遍历,不用调用next
方法,需要注意的是,for...of
遇到next
返回值的done
属性为true
就会终止,return
返回的不包括在for...of
循环中。
function * f(){ yield 1; yield 2; yield 3; yield 4; return 5; } for (let k of f()){ console.log(k); } // 1 2 3 4 没有 5
1.14.5 Generator.prototype.throw()
throw
方法用来向函数外抛出错误,并且在Generator函数体内捕获。
let f = function * (){ try { yield } catch (e) { console.log('内部捕获', e) } } let a = f(); a.next(); try{ a.throw('a'); a.throw('b'); }catch(e){ console.log('外部捕获',e); } // 内部捕获 a // 外部捕获 b
1.14.6 Generator.prototype.return()
return
方法用来返回给定的值,并结束遍历Generator函数,如果return
方法没有参数,则返回值的value
属性为undefined
。
function * f(){ yield 1; yield 2; yield 3; } let g = f(); g.next(); // {value : 1, done : false} g.return('leo'); // {value : 'leo', done " true} g.next(); // {value : undefined, done : true}
1.14.7 next()/throw()/return()共同点
相同点就是都是用来恢复Generator函数的执行,并且使用不同语句替换yield
表达式。
next()
将yield
表达式替换成一个值。
let f = function * (x,y){ let r = yield x + y; return r; } let g = f(1, 2); g.next(); // {value : 3, done : false} g.next(1); // {value : 1, done : true} // 相当于把 let r = yield x + y; // 替换成 let r = 1;
throw()
将yield
表达式替换成一个throw
语句。
g.throw(new Error('报错')); // Uncaught Error:报错 // 相当于将 let r = yield x + y // 替换成 let r = throw(new Error('报错'));
next()
将yield
表达式替换成一个return
语句。
g.return(2); // {value: 2, done: true} // 相当于将 let r = yield x + y // 替换成 let r = return 2;
1.14.8 yield* 表达式
用于在一个Generator中执行另一个Generator函数,如果没有使用yield*
会没有效果。
function * a(){ yield 1; yield 2; } function * b(){ yield 3; yield * a(); yield 4; } // 等同于 function * b(){ yield 3; yield 1; yield 2; yield 4; } for(let k of b()){console.log(k)} // 3 // 1 // 2 // 4
1.14.9 应用场景
- 控制流管理
解决回调地狱:
// 使用前 f1(function(v1){ f2(function(v2){ f3(function(v3){ // ... more and more }) }) }) // 使用Promise Promise.resolve(f1) .then(f2) .then(f3) .then(function(v4){ // ... },function (err){ // ... }).done(); // 使用Generator function * f (v1){ try{ let v2 = yield f1(v1); let v3 = yield f1(v2); let v4 = yield f1(v3); // ... }catch(err){ // console.log(err) } } function g (task){ let obj = task.next(task.value); // 如果Generator函数未结束,就继续调用 if(!obj.done){ task.value = obj.value; g(task); } } g( f(initValue) );
- 异步编程的使用 在真实的异步任务封装的情况:
let fetch = require('node-fetch'); function * f(){ let url = 'http://www.baidu.com'; let res = yield fetch(url); console.log(res.bio); } // 执行该函数 let g = f(); let result = g.next(); // 由于fetch返回的是Promise对象,所以用then result.value.then(function(data){ return data.json(); }).then(function(data){ g.next(data); })
1.15 Class语法和继承
1.15.1 介绍
ES6中的class
可以看作只是一个语法糖,绝大部分功能都可以用ES5实现,并且,类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。
// ES5 function P (x,y){ this.x = x; this.y = y; } P.prototype.toString = function () { return '(' + this.x + ', ' + this.y + ')'; }; var a = new P(1, 2); // ES6 class P { constructor(x, y){ this.x = x; this.y = y; } toString(){ return '(' + this.x + ', ' + this.y + ')'; } } let a = new P(1, 2);
值得注意: ES6的类的所有方法都是定义在prototype
属性上,调用类的实例的方法,其实就是调用原型上的方法。
class P { constructor(){ ... } toString(){ ... } toNumber(){ ... } } // 等同于 P.prototyoe = { constructor(){ ... }, toString(){ ... }, toNumber(){ ... } } let a = new P(); a.constructor === P.prototype.constructor; // true
类的属性名可以使用表达式:
let name = 'leo'; class P { constructor (){ ... } [name](){ ... } }
Class不存在变量提升: ES6中的类不存在变量提升,与ES5完全不同:
new P (); // ReferenceError class P{...};
Class的name属性:
name
属性总是返回紧跟在class
后的类名。
class P {} P.name; // 'P'
1.15.2 constructor()方法
constructor()
是类的默认方法,通过new
实例化时自动调用执行,一个类必须有constructor()
方法,否则一个空的constructor()
会默认添加。
constructor()
方法默认返回实例对象(即this
)。
class P { ... } // 等同于 class P { constructor(){ ... } }
1.15.3 类的实例对象
与ES5一样,ES6的类必须使用new
命令实例化,否则报错。
class P { ... } let a = P (1,2); // 报错 let b = new P(1, 2); // 正确
与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this
对象上),否则都是定义在原型上(即定义在class
上)。
class P { 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 // toString是原型对象的属性(因为定义在Point类上)
1.15.4 Class表达式
与函数一样,类也可以使用表达式来定义,使用表达式来作为类的名字,而class
后跟的名字,用来指代当前类,只能再Class内部使用。
let a = class P{ get(){ return P.name; } } let b = new a(); b.get(); // P P.name; // ReferenceError: P is not defined
如果类的内部没用到的话,可以省略P
,也就是可以写成下面的形式。
let a = class { ... }
1.15.5 私有方法和私有属性
由于ES6不提供,只能变通来实现:
- 1.使用命名加以区别,如变量名前添加
_
,但是不保险,外面也可以调用到。
class P { // 公有方法 f1 (x) { this._x(x); } // 私有方法 _x (x){ return this.y = x; } }
- 2.将私有方法移除模块,再在类内部调用
call
方法。
class P { f1 (x){ f2.call(this, x); } } function f2 (x){ return this.y = x; }
- 3.使用
Symbol
为私有方法命名。
const a1 = Symbol('a1'); const a2 = Symbol('a2'); export default class P{ // 公有方法 f1 (x){ this[a1](x); } // 私有方法 [a1](x){ return this[a2] = x; } }
1.15.6 this指向问题
类内部方法的this
默认指向类的实例,但单独使用该方法可能报错,因为this
指向的问题。
class P{ leoDo(thing = 'any'){ this.print(`Leo do ${thing}`) } print(text){ console.log(text); } } let a = new P(); let { leoDo } = a; leoDo(); // TypeError: Cannot read property 'print' of undefined // 问题出在 单独使用leoDo时,this指向调用的环境, // 但是leoDo中的this是指向P类的实例,所以报错
解决方法:
- 1.在类里面绑定
this
class P { constructor(){ this.name = this.name.bind(this); } }
- 2.使用箭头函数
class P{ constructor(){ this.name = (name = 'leo' )=>{ this.print(`my name is ${name}`) } } }
1.15.7 Class的getter和setter
使用get
和set
关键词对属性设置取值函数和存值函数,拦截属性的存取行为。
class P { constructor (){ ... } get f (){ return 'getter'; } set f (val) { console.log('setter: ' + val); } } let a = new P(); a.f = 100; // setter : 100 a.f; // getter
1.15.8 Class的generator方法
只要在方法之前加个(*
)即可。
class P { constructor (...args){ this.args = args; } *[Symbol.iterator](){ for (let arg of this.args){ yield arg; } } } for (let k of new P('aa', 'bb')){ console.log(k); } // 'aa' // 'bb'
1.15.9 Class的静态方法
由于类相当于实例的原型,所有类中定义的方法都会被实例继承,若不想被继承,只要加上static
关键字,只能通过类来调用,即“静态方法”。
class P (){ static f1 (){ return 'aaa' }; } P.f1(); // 'aa' let a = new P(); a.f1(); // TypeError: a.f1 is not a function
如果静态方法包含this
关键字,则this
指向类,而不是实例。
class P { static f1 (){ this.f2(); } static f2 (){ console.log('aaa'); } f2(){ console.log('bbb'); } } P.f2(); // 'aaa'
并且静态方法可以被子类继承,或者super
对象中调用。
class P{ static f1(){ return 'leo' }; } class Q extends P { ... }; Q.f1(); // 'leo' class R extends P { static f2(){ return super.f1() + ',too'; } } R.f2(); // 'leo , too'
1.15.10 Class的静态属性和实例属性
ES6中明确规定,Class内部只有静态方法没有静态属性,所以只能通过下面实现。
// 正确写法 class P {} P.a1 = 1; P.a1; // 1 // 无效写法 class P { a1: 2, // 无效 static a1 : 2, // 无效 } P.a1; // undefined
新提案来规定实例属性和静态属性的新写法
- 1.类的实例属性
类的实例属性可以用等式,写入类的定义中。
class P { prop = 100; // prop为P的实例属性 可直接读取 constructor(){ console.log(this.prop); // 100 } }
有了新写法后,就可以不再contructor
方法里定义。
为了可读性的目的,对于那些在constructor
里面已经定义的实例属性,新写法允许直接列出。
// 之前写法: class RouctCounter extends React.Component { constructor(prop){ super(prop); this.state = { count : 0 } } } // 新写法 class RouctCounter extends React.Component { state; constructor(prop){ super(prop); this.state = { count : 0 } } }
- 2.类的静态属性
只要在实例属性前面加上static
关键字就可以。
class P { static prop = 100; constructor(){console.log(this.prop)}; // 100 }
新写法方便静态属性的表达。
// old class P { .... } P.a = 1; // new class P { static a = 1; }
1.15.11 Class的继承
主要通过extends
关键字实现,继承父类的所有属性和方法,通过super
关键字来新建父类构造函数的this
对象。
class P { ... } class Q extends P { ... } class P { constructor(x, y){ // ... } f1 (){ ... } } class Q extends P { constructor(a, b, c){ super(x, y); // 调用父类 constructor(x, y) this.color = color ; } f2 (){ return this.color + ' ' + super.f1(); // 调用父类的f1()方法 } }
子类必须在constructor()
调用super()
否则报错,并且只有super
方法才能调用父类实例,还有就是,父类的静态方法,子类也可以继承到。
class P { constructor(x, y){ this.x = x; this.y = y; } static fun(){ console.log('hello leo') } } // 关键点1 调用super class Q extends P { constructor(){ ... } } let a = new Q(); // ReferenceError 因为Q没有调用super // 关键点2 调用super class R extends P { constructor (x, y. z){ this.z = z; // ReferenceError 没调用super不能使用 super(x, y); this.z = z; // 正确 } } // 关键点3 子类继承父类静态方法 R.hello(); // 'hello leo'
super关键字:
既可以当函数使用,还可以当对象使用。
- 1.当函数调用,代表父类的构造函数,但必须执行一次。
class P {... }; class R extends P { constructor(){ super(); } }
- 2.当对象调用,指向原型对象,在静态方法中指向父类。
class P { f (){ return 2 }; } class R extends P { constructor (){ super(); console.log(super.f()); // 2 } } let a = new R()
注意:super
指向父类原型对象,所以定义在父类实例的方法和属性,是无法通过super
调用的,但是通过调用super
方法可以把内部this
指向当前实例,就可以访问到。
class P { constructor(){ this.a = 1; } print(){ console.log(this.a); } } class R extends P { get f (){ return super.a; } } let b = new R(); b.a; // undefined 因为a是父类P实例的属性 // 先调用super就可以访问 class Q extends P { constructor(){ super(); // 将内部this指向当前实例 return super.a; } } let c = new Q(); c.a; // 1 // 情况3 class J extends P { constructor(){ super(); this.a = 3; } g(){ super.print(); } } let c = new J(); c.g(); // 3 由于执行了super()后 this指向当前实例