ECMAScript概述
JavaScript = ECMAScript + 运行环境的API
其中,运行环境包括浏览器和Node环境
ES2015(ES6来泛指ES2015之后的新标准)新特性
解决原有用语法上的不足
let、const与块级作用域
- let、const声明的变量只在{}包裹的块级作用域内有效
// 将会打印出三次foo;
for(let i = 0; i < 3; i ++) {
let i = 'foo';
console.log(i);
}
// 这段代码相当于如下
// 第一步,声明循环变量
let i = 0;
// 第二步,判断循环条件,执行循环体,由于循环体是一个块级作用域,因此与循环变量尽管同名,但在循环体内只有一个i变量;
if(i < 3) {
let i = 'foo';
console.log(i);
}
// 第三步,循环变量的值增加
i ++;
- 不会有作用域提升效果,也就是只有执行到声明语句时,才有这个变量;
- const用来声明常量值,意味着该变量只读;对于引用类型而言,该常量值为引用地址,只要不更改引用地址就行;
在程序中,不用var,主要用const,再配合let
for of 遍历
作为遍历所有数据结构的统一方式,在Iterator对象调用next方法后返回的结果对象中如果done属性为false,则执行循环体,否则退出循环。
- 可以在遍历过程中使用break关键字终止循环。
- 可以遍历数组、类数组集合、Set、Map等等,但不能遍历对象,原因在于对象没有实现默认的Iterator接口。
- 对Map数据结构的遍历时,得到的元素是键值对构成的二元数组,因此可以使用数组解构赋值的方式来使用键值对。
对原有语法的增强
解构赋值
更加简洁地从一个复杂的数据结构中提取值,包括数组、对象这些数据结构。
数组解构
将一个已知数组中的值赋值给变量时,为了提升效率,不再利用索引一个一个赋值,而是一次性赋值。
解构时要根据位置(即数字索引)来赋值。
- 支持使用…扩展符解构
- 支持在解构时,为变量提供默认值
const arr = [1, 2, 3];
// 原始做法
const foo = arr[0];
const bar = arr[1];
const baz = arr[2];
// 解构做法
const [foo, bar, baz] = arr; // foo = 1, bar = 2, baz = 3;
// 或者只赋值某一部分
const [, , baz] = arr; // baz = 3;
// 使用...扩展符解构,...rest这种方式只能用于数组末尾
const [foo, ...rest] = arr; // foo = 1, rest = [2, 3];
// 解构时使用默认值,如果解构赋值时没有得到值,则使用默认值
const [foo, bar, baz, more = 'default'] = arr; // more = 'default';
对象解构
使用属性名(索引)来解构赋值。
const obj = { name: 'mm', age: 18 };
const { name: foo = 'ff', age: bar } = obj; // foo = 'mm', bar = 18, 其中foo = 'ff'为默认赋值
模板字符串
使用``来标识字符串
- 支持在字符串中加入换行符(而不是用\n)
- 支持插值表达式,不需要再用丑陋的拼接方式
- 支持标签函数,标签函数用于对模板字符串进行提取,并进行额外加工;
const str = console.log`hello world`; // [ 'hello world' ];
const name = 'tom';
const gender = true;
// 自定义标签函数
// 标签函数接收的第一个参数为,模板字符串被插值表达式分隔而成的字符串数组
// 标签函数接收的其他参数为,模板字符串中插值表达式的值
function myTag(strings, name, gender) {
console.log(strings, name, gender); // strings = ['hello, ', ' is a ', '.'], name = 'tom', gender = true;
const sex = gender ? 'man': 'woman';
return strings[0] + name + strings[1] + sex + strings[2]; // 'hello, tom is a men.';
}
const result = myTag`hello, ${name} is a ${gender}.`;
字符串方法扩展
- includes(str):判断字符串是否包含str
- startsWith(str):判断字符串是否以str开头
- endsWith(str):判断字符串是否以str结尾
函数扩展
参数默认值
- 带默认值的参数要放在函数形参的末尾
剩余参数
使用扩展符…rest作为形参来接收函数的实参,其中rest为数组,剩余参数…rest要放在形参的末尾
使用扩展符来传递实参
const arr = ['foo', 'baz', 'bar'];
// 通过...arr传递实参
console.log(...arr);
箭头函数
即lambda表达式形式
- 简化了回调函数的编写
- 突出了函数的本质(类型到类型的映射关系,适用于函数式编程范式)
- 不会改变this指向,箭头函数内部没有this机制,因此箭头函数体的this绑定的是函数上下文中的this,即箭头函数定义的词法作用域中的this;在使用保存this指向的场景下,都可以使用箭头函数来简化
const person1 = {
name: 'tom',
sayHi: () => {
setTimeout(function() {
console.log(this.name);
}, 1000)
}
}
person1.sayHi(); // undefined;
const person2 = {
name: 'tom',
// 注意在方法的定义上,不要使用箭头函数,否则内部的this找不到
sayHi: function() {
// 使用_this来保存this指向,通过闭包机制访问到this.name;
let _this = this;
setTimeout(function() {
console.log(_this.name);
}, 1000)
}
}
person2.sayHi(); // tom;
const person3 = {
name: 'tom',
sayHi: function() {
setTimeout(() => {
console.log(this.name);
}, 1000)
}
}
person3.sayHi(); // tom;
对象增强
动态属性名即计算属性名
在对象用字面量方式定义时,可以利用[表达式]的方式去定义动态属性名,称之为计算属性名
对象扩展方法
- Object.assign(target, source1, source2, …rest):将source1、source2等对象的属性复制到target对象,并返回target对象。同名属性将会被覆盖,覆盖以从右向左的顺序。注意:浅复制。
- 一般常用于更新对象,但不修改源对象的非侵入式修改,适合函数式编程范式。
- 这也是JavaScript扩展对象的一种方式 Mixin 。
- assign方法无法复制源对象的访问器属性,该方法会将访问器属性执行之后,将执行结果作为数据属性复制到目标对象上。
- Object.is(value1, value2): 比较两个值是否全等。主要是为了补充+0与-0、NaN与NaN的比较。因为在符号
===
的情况下,与Object.is方法刚好相反的。Object.is(+0, -0); // false
Object.is(NaN, NaN); // true
- Object.keys(obj):返回目标对象obj所有可枚举的属性名组成数组
- Object.getOwnPropertyNames(obj):返回目标对象obj不考虑可枚举性的属性名组成的数组
- Object.getOwnPropertySymbols(obj):返回目标对象obj所有Symbol类型的属性名组成的数组
全新的对象及其API
Proxy代理对象
用处
用于拦截程序中对对象的访问、设置等各种行为,使用代理对象对这些行为进行响应。每一种特定的行为都有相应的陷阱来拦截该行为,并指定对应的处理程序来响应该行为。响应程序可以自定义,也可以使用Reflect API来使用Proxy默认的响应行为。
通过Proxy代理之后,对该对象的操作转而需要通过Proxy来进行。
构造
const person = {
name: 'tom',
age: 18
}
// new Proxy(target, options)传入代理的目标对象和代理配置对象,配置对象内部定义了各种拦截器
const personProxy = new Proxy(person, {
// 拦截访问对象属性值的行为,target为要访问的目标对象,property为要访问的属性名
get (target, property) {
// 不管访问目标对象的什么属性,一律返回100; 这里就是自定义的响应;
return 100;
}
// 拦截设置对象属性值的行为,增加校验行为
set (target, property, value) {
// 可以针对age属性做一下校验
if(property === 'age') {
if(!Number.isInteger(value)) {
throw new TypeError(`${value} is not an Integer!`);
}
target[property] = value;
}
target[property] = value;
}
})
// 访问person.name的行为转而变为访问personProxy.name的行为;
const result = peronProxy.name; // 100;
与Object.defineProperty的对比
- Object.defineProperty只能拦截对属性的读写操作,Proxy可以拦截更多的操作,这取决于Proxy内部的陷阱函数
- 监视数组的操作
- Object.defineProperty,如Vue中是通过重写数组的操作方法来拦截
- Proxy 监视数组的操作更加灵活
- 非侵入式拦截对象操作
const list = [];
const listProxy = new Proxy(list, {
set(target, property, value) {
console.log('set', property, value);
target[property] = value;
return true; // 表示写入成功
}
})
// push操作可以被Proxy的set陷阱拦截,并且property自动更新;但如果是利用Object.defineProperty时,就需要重写数组的push方法了,原来的push方法无法被拦截到;
list.push(66); // set, 0, 66;
list.push(88); // set, 1, 88;
Reflect
静态类,封装一系列的对对象的底层操作API,是Proxy对象拦截行为的默认响应,每一个API都与Proxy中的陷阱函数相对应。我们在定义Proxy中的陷阱函数时,可以先自定义好需要的逻辑,然后使用Reflect的API将默认响应返回。
Reflect的意义在于,统一了针对对象的各种操作,不管是利用Object的静态方法还是实例方法,还是一些操作符如in, has等,都可以通过Reflect API找到对应的方法;Reflect API可以不和Proxy一起使用,也可以单独使用,目的是取代之前定义的对象操作行为,统一经过Reflect API来操作。
class类
静态成员
使用static关键字声明静态成员,内部的this指向类本身(构造函数)
继承
使用extends关键字实现类型继承
- 在子类构造函数中需要先调用super()函数初始化this值,才能在之后的代码中使用this。super引用父类构造函数,是构造函数借用。
Iterator对象
用于提供统一的迭代数据结构的接口,是for of 实现遍历的前提。也就是说,只有数据结构实现了Iterator接口,才能够被for of方式来遍历。
数据结构如果实现了[Symbol.iterator]属性(属性值为函数),该函数返回一个Iterator对象。Iterator对象的next()方法用来获取数据结构中的下一个元素。这也是for of 方式实现遍历的原理。
为对象实现Iterator接口
只要实现了Iterator接口,就可以使用for of 遍历;
const obj {
store: ['foo', 'bar', 'baz'],
// 实现iterable接口,用于返回Iterator对象
[Symbol.iterator]: function() {
const self = this; // 保存对象的引用;
let index = 0;
// 实现Iterator对象,用于迭代对象内部的值
return {
next: function() {
const result = {
value: self.store[index],
done: index >= self.store.length
}
index ++;
return result;
}
}
}
}
迭代器模式管理数据结构的遍历
Iterator对象是为了实现迭代器模式而创建。迭代器模式是为了将遍历代码与数据结构解耦,统一使用迭代器来进行遍历。这样如果数据结构频繁变化时,可以只修改迭代器即可,遍历代码改动很小。也就是说,迭代器模式将数据结构与遍历代码搭建了一个统一的桥梁。
Generator生成器
意义
提供更好的异步编程写法
yield关键字
使用generator实现Iterator
const obj {
store: ['foo', 'bar', 'baz'],
// 实现iterable接口,用于返回Iterator对象,这里使用了生成器函数
[Symbol.iterator]: function* () {
for(const item of this.store) {
yield item;
}
}
}
redux-saga使用generator写法
全新的数据类型
Set数据结构
数据集合,其中的数据不重复。
构造
const set = new Set();
操作方法(均为实例方法)
- add(item):增加一个元素item到Set
- forEach((item, index) => {}):遍历Set中的元素
- size属性:返回Set中元素的数量
- has(item):判断Set中item是否存在
- delete(item):删除Set中item元素
- clear():清理Set所有元素
WeakSet
Map数据结构(对象的增强或补充)
key-value数据集合,Map与对象的区别是,key可以是任意数据类型,而对象的key只能是字符串(其他数据类型会被隐式转换为字符串类型);
构造
const map = new Map();
操作方法(均为实例方法)
- get(key)
- set(key, value)
- forEach((value, key) => {})
- 其他类似于对象的方法,如keys()、values()、entries()等
WeakMap
弱引用集合。
- WeakMap的键必须是一个对象,使用非对象的键会报错
- 集合中保存的是这些对象的弱引用,如果在弱引用之外的不存在其他的强引用,则垃圾回收机制会将弱引用对象收回,同时从WeakMap中删除对应的键值对。
- 最大的用于是保存DOM元素。当DOM元素从页面移除时(外部强引用不存在了),WeakMap内部对应的DOM元素也会被垃圾回收机制自动清除。
Symbol类型
意义
- 创建独一无二的值
const s = Symbol(); // 不能使用new创建,而是直接用Symbol()
,每次使用Symbol()创建出来的值都不相同。- 创建私有属性
- 避免第三方库的命名冲突
创建
- 给
Symbol(str)
传入可选的str字符串,用于描述这个Symbol类型的值。描述文本有利于开发调试,让开发者知道该值的相关信息。 - Symbol的描述文本存储在内部的[[ Descriptor ]]属性,只有调用Symbol的toString()方法才可以读取到这个内部属性。使用console.log()时隐式调用了toString()方法。
const s1 = Symbol('name');
console.log(s1); // "Symbol('name')"
类型判断
可以使用typeof操作符来判断一个值是否是Symbol类型
const s1 = Symbol('symbol test');
console.log(typeof s1); // 'symbol'
对象私有属性的创建
- 对象的属性名现在可以接受string类型和Symbol类型了
- 通过Symbol类型可以创建对象的私有属性了,因为外部无法找到相同的Symbol类型值。
const name = Symbol(); // name变量中保存了新创建的Symbol类型值,但name变量不暴露给外部即可实现私有属性 const obj = { [name]: 'foo', say() { console.log(this.[name]); } }
Symbol共享体系
在代码中查找所创建的Symbol值。全局Symbol注册表是一个类似全局作用域的共享体系,同样需要注意避免冲突。
Symbol.for(str):
- 用于查找描述为str的Symbol值并返回该Symbol类型的值。如果没找到,则使用str参数创建一个Symbol值并返回。
- 要求传入str是字符串类型,如果不是则隐式转换为字符串类型。
- 为了查找Symbol值,其内部维护了一个描述文本与值的全局Symbol注册表,我们可以通过描述文本找到对应的Symbol值。
Symbol.keyFor(symbol):
- 用于查找symbol对应的描述文本。与Symbol.for()方法相反。
- symbol参数要求是一个Symbol值。
- 如果查找不到,则返回undefined。
Symbol与类型转换
- Symbol类型无法转换为字符串类型与数字类型,调用toString()只是返回该值的字符串描述而已。如果Symbol类型与字符串作拼接或参与数学运算都会报错。
- Symbol可以参与逻辑运算,因为Symbol等价布尔值为true
Symbol公共属性来暴露一些JavaScript内部逻辑
Symbol内置了一些常量属性来暴露JavaScript的内部逻辑,同时也指定了这些操作的接口。不同的数据类型对这些方法实现情况不同,有的时候需要自定义。如对象类型并没有[Symbol.iterator]接口。开发者可以根据情况对这些接口进行改写,这一方式也被称为元编程。
Symbol.iterator // 返回迭代器接口的方法
Symbol.hasInstance // 使用instanceof操作符时调用的方法
Symbol.match // 在调用String.prototype.match()方法时调用的方法,用于比较字符串是否匹配子串
Symbol.replace // 在调用String.prototype.replace()方法时调用的方法,用于替换字符串的子串
Symbol.search // 在调用String.prototype.search()方法时调用的方法,用于查询字符串的子串
Symbol.split // 在调用String.prototype.split()方法时调用的方法,用于分隔字符串
Symbol.isConcatSpreadable // 布尔值,用于表示当传递一个集体到Array.prototype.concat()方法的参数时,是否应该将集合内的元素规整到同一层级
Symbol.toPrimitive // 一个返回对象对应的原始类型的值的方法,即有时候特定的操作会触发将对象转换为原始类型的值。
Symbol.toStringTag // 一个在调用Object.prototype.toString()方法时使用的字符串,用于创建对象描述,对象描述指的是[object object]、[object array]之类的。如果定义[Symbol.toStringTag]: value, 则返回的对象描述为[object value]
Symbol.unscopables // 一个定义了一些不可被with语句引用的对象属性名称的对象集合
获取对象内部的Symbol类型属性名
使用Object.getOwnPropertySymbols(obj); // 传入目标对象,返回该对象的Symbol类型的属性名
来获取目标对象的Symbol类型的属性名;
使用传统的for in
循环或者Object.keys(obj)
只能获取到字符串类型的属性名;
同时,使用JSON.stringify(obj)
也不会将Symbol类型属性序列化;
ES Modules
ES2016
Array.prototype.includes
鉴于Array.prototype.indexOf(NaN)无法判断数组中的NaN元素,因此扩展出了includes方法
指数运算符
原来:Math.pow(2, 10); // 2^10
现在:2 ** 10;
ES2017
Async/Await写法
Object的扩展方法
Object.values(obj)
返回目标对象obj属性值组成数组
Object.entries(obj)
返回目标对象obj键值对组成的数组,其中键值对也是一个二元数组。
Object.getOwnPropertyDescriptors(obj)
返回目标对象obj上所有属性以及对应的属性描述符对象,其结构如下:
{
属性名1: 属性描述符对象,
属性名2: 属性描述符对象,
...
}
字符串填充
为给定的字符串在开始处(padStart)或结束处(padEnd)填充指定的字符串,直到填充的字符数达到了指定的字符数。
String.prototype.padStart(num, str)
在给定字符串的开始处填充str(可能会反复填充str)直到填充后的字符串长度达到num,返回填充后的字符串;
String.prototype.padEnd(num, str)
在给定字符串的结尾处填充str(可能会反复填充str)直到填充后的字符串长度达到num,返回填充后的字符串;
函数最后一个形式参数后添加逗号
function func(arg1, arg2, arg3,) {
}