ES6笔记2

运算符的扩展
//链判断运算符?. :有三种写法:
obj?.prop //对象属性是否存在
obj?.[expr] //同上
func?.(...args) //函数或对象方法是否存在
//Null判断运算符?? 读取对象属性的时候,如果某个属性的值是null或undefined,有时候需要为它们指定默认值,常见的作法是通过||运算符指定默认值
const headerText = response.settings.headerText || 'hello world'
//上面这种写法如果属性的值为空字符串或false或0,默认值也会生效。为了避免这种情况,引入了一个新的Null判断运算符??。他的行为类似||,但是只有运算符左侧的值为null或undefined时,才会返回右侧的值。
//这个运算符的一个目的,就是跟链判断运算符?.配合使用,为null或undefined的值设置默认值。
const headerText = response.settings?.headerText ?? 'hello world'
Symbol
ES5的对象属性名都是字符串,这容易造成属性名的冲突,所以ES6引入了Symbol(原始数据类型)。Symbol()值通过Symbol函数生成。Symbol哈桑农户前不能使用new命令,否则会报错。因为生成的Symbol是一个原始类型的值,不是对象。也就是说由于Symbol值不是对象,所以不能添加属性,基本上它是一种类似于字符串的数据类型。Symbol函数可以接受一个字符串作为参数,表示对Symbol实例的描述,比较容易区分。如果Symbol的参数是一个对象,就会调用该对象的toString方法,将其转为字符串,然后才生成一个Symbol值。
//作为属性名的Symbol
let mySymbol = Symbol();
//第一种写法
let a = {};
a[mySymbol] = 'hello';
//第二种写法
let a = {
[mySymbol]: 'hello'
};
//第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'hello' });
//以上写法都能得到同样结果
a[mySymbol] //'hello' 必须放在方括号中
Symbol作为属性,遍历对象的时候,该属性不会出现在for...in、for...of循环中,也不会被Object.keys(),Object.getOwnPropertyNames(),JSON.stringify()返回,但是它也不是私有属性,有一个Object.getOwnPropertysSymbols()方法,可以获取指定对象的所有Symbol属性名,该方法返回一个数组,成员是当前对象的所有用作属性名的Symbol值。
Symbol.for()接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值,如果有,就返回这个Symbol值,否则新建一个以字符串为名称的Symbol值,并将其注册到全局。Symbol.keyFor()方法返回一个已登记的Symbol类型值的key。
Set和Map数据结构
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 4 5
Set函数可以接受一个数组(或者具有iterable接口的其他数据结构)作为参数,用来初始化。
//去除数组的重复成员
[...new Set(array)]
//去除字符串里面的重复字符
[...new Set('ababbc')].join('')
//Set内部判断两个值是否不同,使用的是类似于精确相等运算符(===),主要的区别是向Set加入值时认为NaN等于自身,而精确相等运算符认为NaN不等于自身,另外两个对象总是不相等的。
//Set实例的属性和方法
//属性
Set.prototype.constructor: 构造函数,默认就是Set函数
Set.prototype.size: 返回Set实例的成员总数
//方法
Set.prototype.add(value): 添加某个值,返回Set结构本身
Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功
Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员
Set.prototype.clear():清除所有成员,没有返回值。
//Array.from方法可以将Set结构转为数组、
Array.from(new Set(array)); //数组去重
//遍历操作
Set.prototype.keys(): 返回键名的遍历器
Set.prototype.values(): 返回键值的遍历器
Set.prototype.entries(): 返回键值对的遍历器
Set.prototype.forEach((value, key, set)=>{}, [绑定处理函数内部的this对象]): 返回回调函数遍历每个成员
//Set结构没有键名,只有键值,所以keys()方法和values()方法的行为完全一致。
WeakSet与Set有两个区别,首先,WeakSet的成员只能是对象。WeakSet适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在WeakSet里面的引用就会自动消失。WeakSet的成员不适合引用。WeakSet是个构造函数,可以使用new命令,创建WeakSet数据结构。可以接受一个数组或类似数组的对象作为参数。(实际上任何具有Iterable接口的对象,都可以作为WeakSet的参数)该数组的所有成员,都会自动成为WeakSet实例对象的成员。
const a = [[1,2], [3,4]];
const ws = new WeakSet(a);
//WeakSet {[1,2], [3,4]}
//注意:是a数组的成员成为WeakSet的成员,而不是a数组本身。这意味着,数组的成员只能是对象。
const b = [3,4];
const ws = new WeakSet(b) //报错,因为b的成员不是对象
//WeakSet结构有以下三个方法
WeakSet.prototype.add(value): 向WeakSet实例添加要给新成员
WeakSet.prototype.delete(value): 清除WeakSet实例的指定成员
WeakSet.prototype.has(value): 返回一个布尔值,表示某个值是否在WeakSet实例之中
//WeakSet没有size属性,没有办法遍历它的成员。因为成员都是弱引用,随时可能消失,遍历机制无法保证成员的存在。
//WeakSet的一个用处,是储存DOM节点,而不用担心这些节点从文档移除时,会引发内存泄漏。
//WeakSet的另一个例子
const foos = new WeakSet();
class Foo {
constructor() {
foos.add(this);
}
method() {
if(!foos.has(this)) {
throw new TypeError('Foo.prototype.method 只能在Foo的实例上调用!');
}
}
}
//上面代码保证了Foo的实例对象,只能在Foo的实例上调用。这里使用WeakSet的好处是,foos对实例的引用,不会被计入内存回收机制,所以删除实例的时候,不用考虑foos,也不会出现内存泄漏。
JavaScript的对象,本质上是键值对的集合,但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。所以引入Map数据结构。它类似于对象,也是键值对的集合,但是键的范围不限于字符串,各种类型的值(包括对象)都可以当作键。Map作为构造函数,可以接受一个数组作为参数(事实上,不仅仅是数组,任何具有Iterator接口,且每个成员都是一个双元素的数组的数据结构都可以作为Map构造函数的参数),该数组的成员是一个个表示键值对的数组。
//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
//实例的属性和操作方法
size属性
//方法
Map.prototype.set(key, value);
//set方法返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。set方法返回的是当前的Map对象,因此可以采用链式写法。
Map.prototype.get(key) //如果找不到返回undefined
Map.prototype.has(key)
Map.prototype.delete(key) //返回布尔值
Map.prototype.clear() //清除所有成员,没有返回值
//遍历方法
keys(), values(), entries(), forEach()
//Map转为数组最方便的方法,就是使用扩展运算符
//数组转为Map,将数组传入Map构造函数
//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);
setMapToObj(myMap);
//对象转为Map,可以使用Object.entries()
let obj = {"a": 1 "b": 2};
let map = new Map(Object.entries(obj));
//也可以自己实现一个转换函数
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});
WeakMap结构与Map结构类似,与Map的区别有两点,首先WeakMap只接受对象作为键名(null除外),其次WeakMap的键名所指向的对象,不计入垃圾回收机制。如果要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用WeakMap。WeakMap与Map在API上的区别主要是两个,一是没有遍历操作(即没有keys(),values(),entries()方法),也没有size属性,因此,WeakMap只有四个方法可用:get(),set(),has(),delete(), WeakMap的用途,存放DOM节点,另一个用处是部署私有属性。
Proxy
Proxy可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
//ES6原生提供Proxy构造函数,用来生成Proxy实例。都是上面这种形式,不同的只是handler参数的写法。
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});
proxy.time //35
proxy.name //35
//作为构造函数,Proxy接受两个参数。第一个参数是所要代理的目标对象,第二个参数是一个配置对象,对于每一个需要被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。
//注意,要使得Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。
//Proxy支持的拦截操作:
get(target,propKey,receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']
set(target,propKey,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(...)。
constructor(target,args):拦截Proxy实例作为构造函数调用的操作,比如new proxy(...args)。
Reflect
Reflect对象的设计目的:1. 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。2. 修改某些Object方法的返回结果,让其变得合理。3. 让Object操作都变成函数行为。4. Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。
Promise
Promise是异步编程的一种解决方案,比传统的解决方案--回调函数和事件--更合理和更强大。两个特点:1. 对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中),fulfilled(已成功),rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。Promise缺点:首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反映到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段。
const promise = new Promise(function(resolve, reject) {
if() {
resolve(value);
}else {
reject(error);
}
});
//Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject,他们是两个函数。
//Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
//用Promise是对象实现Ajax
const getJson = function(url) {
const promise = new Promise(function(resolve, reject) {
const handler = function() {
if(this.readyState !== 4) {
return;
}
if(this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open('GET', url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
});
return promise;
};
getJson("/posts.json").then(function(json) {
console.log("content:" + json);
}, function(error) {
console.error('出错了', error);
});
//Promise实例具有then方法,也就是,then方法是定义在原型对象Promise.prototype上的。他的作用是为Promise实例添加状态改变时的回调函数。then方法返回一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面调用另一个then方法。
//Promise.prototype.catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。
getJson('/posts.son').then(function(posts) {
...
}).catch(function(error) {
console.log('发生错误', error);
});
//上面代码中,getJson()方法返回一个Promise对象,如果该对象状态变为resolved,则会调用then()方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch()方法指定的会带哦函数,处理这个错误。另外,then()方法指定的回调函数,如果运行中抛出错误,也会被catch()方法捕获。Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。一般来说,不要在then()方法里面定义Reject状态的回调函数(即then第二个参数),总是使用catch方法。
//finally()方法用于指定不管Promise对象最后状态如何,都会执行的操作。
//Promise.all()方法用于将多个Promise实例,包装成一个新的Promise实例。Promise.all()方法的参数可以不是数组,但必须具有Iterator接口,且返回的每个成员都是Promise实例。如果作为参数的Promise实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法。
//Promise.race()方法同样是将多个Promise实例,包装成一个新的Promise实例。只要其中一个实例率先改变状态,包装的状态就跟着改变。
//Promise.allSettled():Promise.all()可以确定所有请求都成功了,但是只要有一个请求失败,他就会报错,而不管另外的请求是否结束。所以该方法用来确定一组异步操作是否都结束了(不管成功或失败)。该方法接受一个数组作为参数,数组的每个成员都是一个Promise对象,并返回一个新的Promise对象。只有等到参数数组的所有Promise对象都发生状态变更(不管fulfilled还是rejected),返回的Promise对象才会发生状态变更。
//Promise.any()该方法接受一组Promise实例作为参数,包装成一个新的Promise实例返回。只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。Promise.any()跟Promise.race()方法很像,只有一点不同,就是Promise.any()不会因为某个Promise变成rejected状态而结束,必须等到所有参数Promise变成rejected状态才会结束。
//Promise.resolve():有时需要将现有对象转为Promise对象,Promise.resolve()方法就起到这个作用。Promise.resolve()方法的参数分为四种情况:1. 参数是一个Promise实例,那么不做任何修改,原封不动地返回这个实例 2. 参数是一个thenable对象(具有then方法的对象),将这个对象转为Promise对象,然后就立即执行thenable对象的then()方法。3. 参数不是具有then()方法的对象,或根本就不是对象,返回一个新的Promise对象,状态为resolved。4. 不带有任何参数,直接返回一个resolved状态的Promise对象。
//Promise.reject():返回一个新的Promise实例,该实例的状态为rejected。
Iterator和for...of循环
Iterator遍历器是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。Iterator的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按照某种次序排列;三是ES6创造了一种新的遍历命令for...of循环,Iterator接口主要提供for...of消费。
//Iterator的遍历过程是这样的
//1. 创建一个指针对象,指向当前数据结构的起始位置。
//2. 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
//3. 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
//4. 不断调用指针对象的next方法,直到它指向数据结构的结束位置
//每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。
默认Iterator接口:默认的Iterator接口部署在数据结构的Symbol.iterator属性,或者说一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”。原生具备Iterator接口的数据结构如下:Array、Map、Set、String、TypedArray、函数的arguments对象、NodeList对象。对象之所以没有默认部署Iterator接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。一个对象如果要具备可被for...of循环调用的Iterator接口,就必须在Symbol.iterator的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)
Generator
Generator函数会返回一个遍历器对象,Generator函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号,二是,函数体内部使用yield表达式,定义不同的内部状态。调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上面的遍历器对象。
协程与普通线程的差异:协程适用于多任务运行的环境,在这个意义上,它与普通的线程很相似,都有自己的执行上下文,可以分享全局变量。他们的不同之处在于,同一时间可以有多个线程处于运行状态,但是运行的协程只能有一个,其他协程处于暂停状态。此外,普通的线程是抢先式的,到底哪个线程优先得到资源,必须有运行环境决定,但是协程是合作式的,执行权由协程自己分配。由于JavaScript是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束。
Generator函数是ES6对协程的实现,但不属于完全实现,Generator函数被称为“半协程”意思是只有Generator函数的调用者,才能将程序的执行权还给Generator函数。如果将Generator函数当做协程,完全可以将多个需要互相协作的任务写成Generator函数,他们之间使用yield表达式交换控制权。
应用:Generator可以暂停函数执行,返回任意表达式的值、。这种特点使得Generator有多种应用场景。(1)异步操作的同步化表达(处理异步操作,改写回调函数)(2)控制流管理(3)部署Iterator接口
//Generator函数的异步应用
//传统方法:
//1.回调函数
//2.事件监听
//3.发布/订阅
//4.Promise对象
async函数
async函数是什么?一句话,他就是Generator函数的语法糖。async函数对Generator函数的改进,体现在以下四点:(1)内置执行器:Generator函数的执行必须依靠执行器,所以才有了co模块,而async函数自带执行器,也就是说,async函数的执行,与普通函数一模一样,只要一行。(2)更好的语义:语义更清楚。(3)更广的适用性:co模块的约定,yield命令后面只能是Thunk函数或Promise对象,而async函数的await命令后面,可以使Promise对象和原始类型的值(4)返回值时Promise:async函数的返回值是Promise,这比Generator函数的返回值是Iterator对象方便很多,可以用then方法指定下一步操作。
class
ES6的类,完全可以看做构造函数的另一种写法
class Point{
}
typeof Point //function
Point === Point.prototype.constructor //true
//累的数据类型就是函数,类本身就指向构造函数
//类的所有方法都定义在类的prototype属性上面
class Point{
constructor() {
}
toString() {
}
toValue() {
}
}
//等同于
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
};
//因此,在类的实例上面调用方法,其实就是调用原型上的方法
class B {}
const b = new B();
b.constructor === B.prototype.constructor //true
//类的内部所有定义的方法,都是不可枚举的,ES5的写法,是可以枚举的
class Point {
constructor(x, y) {
}
toString() {
}
}
Object.keys(Point.prototype) //[]
Object.getOwnPropertyNames(Point.prototype) //["constructor", "toString"]
//静态方法:如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用。父类的静态方法可以被子类继承。静态方法也是可以从super对象上调用的。
//实例属性的新方法:实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层
class IncreasingCounter{
constructor() {
this._count = 0;
}
}
class IncreasingCounter {
_count = 0;
}
//class的继承
//class可以通过extends关键字实现继承。
class ColorPoint extends Point{
constructor(x, y, color) {
super(x, y);
this.color = color;
}
}
//上面代码中,constructor方法出现了super关键字,他在这里表示父类的构造函数,用来新建父类的this对象。子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后在对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。
//Object.getPrototypeOf()方法用来从子类上获取父类。
//super关键字:既可以当做函数使用,也可以当做对象使用。super作为函数调用时,代表父类的构造韩式,此时只能出现在子类的constructor(构造函数)中。super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。