ES全称ECMAScript,ECMAScript 和 JavaScript 的关系是,前者是后者的规范,后者是前者的一种实现,从15年es6发版后,到目前的es11,es的新特性被广泛使用,成为项目开发必不可少的工具,这段时间又系统的学习了这部分的相关知识,对其归纳总结,作为这段时间学习成果的检验。
我们如何判断一个数组是否包含一个元素?
熟悉es5的小伙伴会首先想到indexOf(),这个方法会返回当前数组元素的下标:
const arr = ["es6", "es7", "es8", "es9", "es10", "es11"]
console.log(arr.indexOf("es6")) // 0
console.log(arr.indexOf("es12")) // -1
亦或者:
const item = arr.find((item) => {
return item === "es6"
})
console.log(item) // "es6"
那么这两种方式有什么弊端呢,我们往下看:
const arr = ["es6", "es7", "es8", "es9", "es10", "es11", NaN]
console.log(arr.indexOf(NaN)) // -1
const item = arr.find((item) => {
return item === NaN
})
console.log(item) // undefined
由此可以看出,es5的传统方法不满足我们的需求,无法判断数组中是否含有NaN,由此,es7提供给数组一个新的API,就是我们所说的Array.prototype.includes。
基本用法
const arr = ["es6", "es7", "es8", "es9", "es10", "es11",NaN]
console.log(arr.includes("es6")) // true
console.log(arr.includes(NaN)) // true
console.log(arr.includes("es12")) // false
- Array.prototype.includes():可以接收两个参数,要搜索的值和搜索的开始索引。第二个参数可选,若为负数表示从末尾开始计数下标。
- 只能判断简单类型的数据,对于复杂类型的数据,比如对象类型的数组,二维数组,这些是无法判断的。
includes的用法和indexOf用法相似,都可以用来判断数组中是否包含一个元素,唯一的区别在于includes可以识别NaN。
我们如何求一个数的幂运算呢?
在es5中我们可以通过以下两种方式来实现:
// 通过Math.pow()
console.log(Math.pow(2,53))
// 自定义pow函数
function pow(base, exponent) {
let sum = 1;
for (let i = 0; i
es7提供了一种 ** 运算符,可以更简单实现
console.log(2**53)
- 幂运算符的两个*号之间不能出现空格,前后有无空格都可以。
- 注意最大安全数:Number.MAX_SAFE_INTEGER = (2**53)-1
- async/await是继es6中promise、generator后又一种更加优雅的异步编程的解决方案
- async函数是generator函数的语法糖
基本用法
// 不使用async/await
function getPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(1);
resolve(2);
}, 1000);
});
}
function foo() {
const res = getPromise();
console.log(res);
console.log(3);
}
foo();
// Promise {}
// 3
// 1
// 使用async/await
async function foo() {
const res = await getPromise();
console.log(res);
console.log(3);
}
foo();
// 1
// 2
// 3
由上面两个例子的对比就能发现,async/await可以使异步任务处理起来像是同步任务,这是因为await关键字在执行的时候会停下来,等待异步任务执行完毕(await后面一般跟的是异步任务,否则没有意义)在继续执行同步任务。
更优雅的异步编程的解决方案
在es6之前我们对于这个过程应该不陌生
ajax('xxx/a', res => {
console.log(res)
ajax('xxx/b', res => {
console.log(res)
ajax('xxx/c', res => {
console.log(res)
})
})
})
这种回调之后再回调的调用方式我们称之为“回调地狱”,这种回调方式在日常开发和项目维护当中很让人头疼。我们对比下es6中Promise的处理和es8中的async/await的处理方式就知道了为什么我们称async/await为更优雅的异步编程的解决方案。
// 以下都是模拟接口请求的代码
// Promise
function getPromise(url) {
return new Promise((resolve, reject) => {
ajax(url, res => {
resolve(res)
}, err => {
reject(err)
})
})
}
getPromise('xxx/a')
.then(res => {
console.log(res)
return getPromise('xxx/b')
}).then(res => {
console.log(res)
return getPromise('xxx/c')
}).then(res => {
console.log(res)
}).catch(err => {
console.log(err)
})
// async/await
function request(url) {
return new Promise(resolve => {
ajax(url, res => {
resolve(res)
})
})
}
async function getData() {
let res1 = await request('xxx/a')
console.log(res1)
let res2 = await request('xxx/b')
console.log(res2)
let res3 = await request('xxx/c')
console.log(res3)
}
getData()
从两者的对比可以看出,Promise虽然将回调嵌套回调的方式改成平级调用,但是这种调用方式相比于async/await还是显得繁琐,而且async/await不存在回调。
我们如何获取一个对象的每一个属性值?
在es5中我们常用Object,keys()及for in来直接获取
// Object,keys()
const obj = {
name: "张三",
age: 18,
sex: "male",
}
const values = Object.keys(obj).map(item => {
return obj[item]
})
console.log(values) // ["张三", 18, "male"]
// for in
for (let key in obj) {
console.log(obj[key])
}
// "张三", 18, "male"
es8为我们扩展了两个新的静态方法
Object.values()
Object.values() 返回一个数组,其元素是在对象上找到的可枚举属性值。
const obj = {
name: "张三",
age: 18,
sex: "male",
}
console.log(Object.values(obj)) // ["张三", 18, "male"]
Object.entries
Object.entries()方法返回一个给定对象自身可枚举属性的键值对数组。
const obj = {
name: "张三",
age: 18,
sex: "male",
}
console.log(Object.entries(obj))
// [["name", "张三"],["age", "18"], ["sex", "male"]]
for in 与Object.keys()/Object.values()/Object.entries()区别
从前面的例子可以看出for in与Object.keys()/Object.values()/Object.entries()均可以遍历对象的可枚举属性,那他们直接有什么区别呢
const obj = {
name: "张三",
age: 18,
sex: "male",
}
Object.prototype.test = "test"
for (let key in obj) {
console.log(obj[key])
}
// "张三", 18, "male","test"
console.log(Object.keys(obj).map(key => obj[key]))
// ["张三", 18, "male"]
console.log(Object.values(obj))
// ["张三", 18, "male"]
console.log(Object.entries(obj).map(([key, value]) => value))
// ["张三", 18, "male"]
for in可以遍历出原型链上的可枚举属性,而Object.keys()/Object.values()/Object.entries()只能遍历自身的可枚举属性
如何实现一个Object.values()/Object.entries()
const obj = {
name: "张三",
age: 18,
sex: "male",
}
// Object.values
function values(obj) {
return Object.keys(obj).map(key => obj[key])
}
// Object.entries
function entries(obj) {
return Object.keys(obj).map(key => [key, obj[key]])
}
console.log(values(obj))
console.log(entries(obj))
前面提到可枚举属性,我们怎么设置属性的值可枚举呢?
Object.defineProperty()可以通过对描述符的设置进行更精准的控制对象属性,所谓描述符:
- value [属性的值]
- writable [属性的值是否可被改变]
- enumerable [属性的值是否可被枚举]
- configurable [描述符本身是否可被修改,属性是否可被删除]
var test = {
name: '测试',
value: 5
}
Object.defineProperty(test, "name", {
enumerable: false
})
for (let key in test) {
console.log(key)
}
// value
Object.getOwnPropertyDescriptors ()
Object.getOwnPropertyDescriptors ()可以返回对象属性的描述符
let test = {
name: '测试',
value: 5
}
console.log(Object.getOwnPropertyDescriptors(test))
// {
// name: {value: "测试", writable: true, enumerable: true, configurable: true}
// value: {value: 5, writable: true, enumerable: true, configurable: true}
// }
Object.getOwnPropertyDescriptors(target,param)接收两个参数,返回某一个参数的描述符,通过这个方法可以实现一个Object.getOwnPropertyDescriptors ()
Object.getOwnPropertyDescriptors()实现
let test = {
name: '测试',
value: 5
}
function getOwnPropertyDescriptors(obj) {
const result = {};
for (let key of Reflect.ownKeys(obj)) {
result[key] = Object.getOwnPropertyDescriptor(obj, key);
}
return result;
}
getOwnPropertyDescriptors(test)
String.prototype.padStart()/String.prototype.padEnd()
padStart()
先看一个例子,希望把当前日期格式化:yyyy-mm-dd的格式:
// 返回一个yyyy-mm-dd格式的日期
function getTime() {
const date = new Date();
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
return `${year}-${month}-${day}`
}
console.log(getTime())
// 2020-7-9
es8 中 String 新增了两个实例函数 String.prototype.padStart() 和 String.prototype.padEnd(),允许将空字符串或其他字符串添加到原始字符串的开头或结尾。
function getTime() {
const date = new Date();
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, "0")
const day = (date.getDate()).toString().padStart(2, "0")
return `${year}-${month}-${day}`
}
console.log(getTime())
// 2020-07-09
padEnd()
在正式项目中后台返回的数据中时间一般会转为时间戳格式,处理时间戳的时候单位都是ms毫秒(13位),但有时候有可能是s秒做单位(10位),这个时候我们需要做一个13位的补全,保证单位是毫秒。
time = String(time).padEnd(13, '0')
此前,函数定义和调用时,都不允许最后一个参数后面出现逗号,es8 允许函数的最后一个参数有尾逗号
// es8以前
function foo(a, b, c, d) {
console.log(a, b, c, d)
}
// es8
function foo(a, b, c, d,) {
console.log(a, b, c, d)
}
es6中有一个新特性Iteartor,只要元素符合两个协议:
- 可迭代协议:对象包含Symbol.iterator属性;
- 迭代器协议:Symbol.iterator属性必须返回一个对象,这个对象包含一个next方法,且next方法也返回一个对象,此对象包含value,done两个属性
我们就可以使用for…of去遍历这个元素。
我们知道 for…of 可以遍历同步运行的任务,那如果是异步任务呢,如下:
function getPromise(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(time)
}, time)
})
}
const asyncArr = [getPromise(1000), getPromise(200), getPromise(3000)]
for (let item of asyncArr) {
console.log(item, item.then(res => {
console.log(res)
}))
}
// Promise {}
// Promise {}
// Promise {}
// 200
// 1000
// 3000
在上述遍历的过程中可以看到三个任务是同步启动的,我们期望的是一个异步任务执行完,在执行下一个异步任务,然而从输出可以看出不是按任务的执行顺序输出的,这显然不太符合我们的要求,在 es9 中也可以用 for…await…of 来操作:
function getPromise(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
value: time,
done: false,
});
}, time);
});
}
const asyncArr = [getPromise(1000), getPromise(200), getPromise(3000)];
asyncArr[Symbol.asyncIterator] = function () {
let nextIndex = 0;
return {
next() {
return nextIndex < asyncArr.length
? asyncArr[nextIndex++]
: Promise.resolve({
value: undefined,
done: true,
});
},
};
};
async function test() {
for await (let item of asyncArr) {
console.log(Date.now(), item);
}
}
test();
// 1594374685156 1000
// 1594374685157 200
// 1594374687157 3000
await需要在async 函数或者 async 生成器里面使用
同步迭代器/异步迭代器
类别 | 同步迭代器 | 异步迭代器 |
---|---|---|
迭代器协议 | Symbol.iterator | Symbol.asyncIteartor |
遍历 | for…of | for…await…of |
dotAll/s
一句话总结dotAll模式就是:在正则中使用(.)字符时使用s修饰符可以解决(.)字符不能匹配行终止符的例外
console.log(/./.test(1));
console.log(/./.test("1"));
console.log(/./.test("\n"));
console.log(/./.test("\r"));
console.log(/./.test("\u{2028}"));
// true
// true
// false
// false
// false
// 使用s修饰符
console.log(/./s.test(1));
console.log(/./s.test("1"));
console.log(/./s.test("\n"));
console.log(/./s.test("\r"));
console.log(/./s.test("\u{2028}"));
// true
// true
// true
// true
// true
- (.)是一个特殊字符,代表任意的单个字符,但是有两个例外。一个是四个字节的 UTF-16 字符,这个可以用u修饰符解决;另一个是行终止符
- 正则中可以使用的修饰符有i,g,m,y,u,s
具名组匹配
我们先看一个例子
console.log("2020-07-10".match(/(\d{4})-(\d{2})-(\d{2})/));
// ["2020-07-10", "2020", "07", "10", index: 0, input: "2020-07-10", groups: undefined]
按照 match 的语法,没有使用 g 修饰符,所以返回值第一个数值是正则表达式的完整匹配,接下来的第二个值到第四个值是分组匹配(2020, 07, 10),我们想要获取年月日的时候不得不通过数组的下标去获取,这样显得不灵活。仔细观察 match 返回值还有几个属性,分别是 index、input、groups。
- index [匹配的结果的开始位置]
- input [匹配的字符串]
- groups [捕获组 ]
所谓的具名组匹配就是命名捕获分组:
console.log("2020-07-10".match(/(?\d{4})-(?\d{2})-(?\d{2})/));
// groups的值
groups: {year: "2020", month: "07", day: "10"}
这样我们就可以通过groups及命名分组获取对应的年月日的值了。
后行断言
let test = 'world hello'
console.log(test.match(/(?<=world\s)hello/))
(?<)是后行断言的符号配合= 、!等使用。
一个例子对比理解对象的Rest和Spread语法
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
// 数组合并
const arr = [...arr1, ...arr2];
console.log(arr);
const obj1 = { a: 1 };
const obj2 = { b: 2 };
// 对象合并
const obj = { ...obj1, ...obj2 };
console.log(obj);
// [1, 2, 3, 4, 5, 6]
// {a: 1, b: 2}
一句话总结就是(…)运算符在数组中可以怎样使用,在对象就可以怎样使用。
不管promise状态如何都会执行的回调函数
new Promise((resolve, reject) => {
resolve(1);
})
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(err);
})
.finally(() => {
console.log("finally");
});
// 1
// promise
es9 新特性中移除了对 ECMAScript带标签的模板字符串中转义序列的语法限制。 遇到不合法的字符串转义返回undefined,并且从raw上可获取原字符串
function foo(str) {
console.log(str);
}
foo`\undfdfdf`;
// es9以前报错
// es9:[undefined, raw:["\undfdfdf"]]
es8中对象添加了一个entries()静态方法,这个方法返回一个给定对象自身可枚举属性的键值对数组 ,Object.fromEntries()方法与 Object.entries() 正好相对,可以将键值对列表转换为一个对象 。
const obj = {
x: 1,
y: 2,
};
const entries = Object.entries(obj);
console.log(entries);
console.log(Object.fromEntries(entries));
// [["x",1],["y":2]]
// {x:1,y:2}
只要符合entries结构的都可以使用Object.fromEntries(entries)将键值对列表转换为一个对象,比如Map
trimStart() /trimLeft()
trimLeft是trimStart的别名,作用是去掉字符串左边的空格
trimEnd() / trimRight()
trimEnd是trimRight的别名,作用是去掉字符串右边的空格
const str = " hello world ";
console.log(str.trimStart());
console.log(str.trimEnd());
console.log(str.trim());
// "hello world "
// " hello world"
// "hello world"
Array.prototype.flat()
flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回 。
const arr = [1, [2, [3, [4, [5, [6, 7], 8], 9]]]];
console.log(arr.flat(1));
console.log(arr.flat(5));
console.log(arr.flat(Infinity));
// [1,2,[3, [4, [5, [6, 7], 8], 9]]]
// [1,2,3,4,5,6,7,8,9]
// [1,2,3,4,5,6,7,8,9]
自定义实现flat
function flat(arr, deep = 1) {
const newArray = [];
let deepNum = 0;
const flatMap = (arr) => {
arr.map((item, index, array) => {
if (Array.isArray(item)) {
if (deepNum < deep) {
deepNum++;
flatMap(item);
} else {
newArray.push(item);
}
} else {
newArray.push(item);
if (index === array.length - 1) deepNum = 0;
}
});
};
flatMap(arr);
return newArray;
}
const arr = [1, [2, [3, [4, [5, 6], 7], 8]]];
console.log(flat(arr, 4));
Array.prototype.flatMap()
flatMap实质上包含两部分功能,一是map,二是flat
const numbers = [1, 2, 3];
console.log(numbers.map((x) => [x ** 2]).flat());
console.log(numbers.flatMap((x) => [x ** 2]));
// [1,4,9]
// [1,4,9]
可以通过 description 获取 Symbol 的描述
const symbol = Symbol("symbol");
console.log(symbol.description); // symbol
console.log(symbol.description === "symbol"); // true
在es10以前,我们只能通过调用 Symbol 的 toString() 时才可以读取这个属性
console.log(symbol.toString() === "Symbol(symbol)");
Function.prototype.toString() 方法返回一个表示当前函数源代码的字符串
function test(a) {
// es10以前不返回注释部分
console.log(a);
}
console.log(test.toString());
// function test(a) {
// // es10以前不返回注释部分
// console.log(a);
// }
es10允许我们在捕获异常时省略catch的参数
// es10以前
try {
throw new Error();
} catch (error) {
console.log("fail");
}
// es10
try {
throw new Error();
} catch {
console.log("fail");
}
JSON 内容可以支持包含 U+2028行分隔符 与 U+2029段分隔符
在 ES10 JSON.stringify 会用转义字符的方式来处理 超出范围的 Unicode 展示错误的问题 而非编码的方式
console.log(JSON.stringify('\uD83D\uDE0E')) // 笑脸
// 单独的\uD83D其实是个无效的字符串
// 之前的版本 ,这些字符将替换为特殊字符,而现在将未配对的代理代码点表示为JSON转义序列
console.log(JSON.stringify('\uD83D')) // "\ud83d"
es11为我们提供了第七种新的原始数据类型,对于js来说,他的最大取值范围是2的53次方
console.log(2 ** 53);
console.log(2 ** 53 + 1);
console.log(Number.MAX_SAFE_INTEGER);
// 9007199254740992
// 9007199254740992
// 9007199254740991
BigInt,表示一个任意精度的整数,可以表示超长数据,可以超出2的53次方 。
使用方式
// 方式一
console.log(9007199254740993);
console.log(9007199254740993n);
// 9007199254740992
// 9007199254740993n
// 方式二
console.log(9007199254740993);
console.log(BigInt(9007199254740993n));
// 9007199254740992
// 9007199254740993n
- 1==1n // true
- 1 === 1n // false
- typeof 1n // bigint
- BigInt(9007199254740993n).toString() // 9007199254740993
可选链可以使我们在查询具有多层级的对象时,不再需要进行冗余的各种前置校验。
const a = {
b: {
c: {
d: {
e: "111",
},
},
},
};
// es11前
const value = a && a.b && a.b.c && a.b.c.d && a.b.c.d.e;
console.log(value);
// es11:可选链
const value2 = a?.b?.c?.d?.e;
console.log(value2);
可选链中的 ? 表示如果问号左边表达式有值, 就会继续查询问号后面的字段 , 可以大量简化类似繁琐的前置校验操作 。
当我们查询某个属性时,经常会遇到,如果没有该属性就会设置一个默认的值。
const a = 0;
const b = a || 1;
console.log(b);
// 1
我们在使用||运算符时, 变量值为 0 就是 false ,所以我们会看到上述结果会输出1,但是很多时候我们希望b的输出结果就是a的值0,es11提出了空值合并运算符(??),当左侧操作数为 null 或 undefined 时,其返回右侧的操作数。否则返回左侧的操作数。
const a = 0;
const b = a ?? 1;
console.log(b);
// 0
es6中 Promise.all方法接受一个数组元素都是由 Promise.resolve 包装的数组, 生成并返回一个新的 Promise 对象, 如果参数中的任何一个promise为reject的话,则整个Promise.all调用会立即终止,并返回一个reject的新的 Promise 对象。
// 全部返回resolve
function getPromise(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(time);
}, time);
});
}
Promise.all([getPromise(1000), getPromise(2000), getPromise(3000)]).then(
(res) => {
console.log(res);
}
);
// [1000,2000,3000]
// 返回reject
function getPromise(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(time);
}, time);
});
}
function getReject(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(time);
}, time);
});
}
Promise.all([getPromise(1000), getReject(2000), getPromise(3000)])
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(err);
});
// 2000
从上面可以看出Promise.all只要有一个任务返回reject,整个任务都会失败, 我们需要一种机制,如果并发任务中,无论一个任务正常或者异常,都会返回对应的的状态 ,这就是Promise.allSettled()的作用。
function getPromise(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(time);
}, time);
});
}
function getReject(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(time);
}, time);
});
}
Promise.allSettled([getPromise(1000), getReject(2000), getPromise(3000)])
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(err);
});
// [{status: "fulfilled", value: 1000},
// {status: "rejected", reason: 2000},
// {status: // "fulfilled", value: 3000}]
按需加载
现代前端打包资源越来越大,打包成几M的JS资源已成常态,而往往前端应用初始化时根本不需要全量加载逻辑资源,为了首屏渲染速度更快,很多时候都是按需加载,比如懒加载图片等。
(async () => {
if (somethingIsTrue) {
// import module for side effects
await import('xxx/xxx.js');
}
})();
- matchAll() 方法返回一个包含所有匹配正则表达式及分组捕获结果的迭代器 ;
- 使用: str.matchAll(regexp) ;
字符串处理的一个常见场景是想要匹配出字符串中的所有目标子串,例如:
match()
const str =
"es2015/es6 es2016/es7 es2017/es8 es2018/es9 es2019/es10 es2020/es10";
console.log(str.match(/(es\d+)\/es(\d+)/g));
// ["es2015/es6", "es2016/es7", "es2017/es8", "es2018/es9", "es2019/es10", //"es2020/es10"]
match()方法中,正则表达式所匹配到的多个结果会被打包成数组返回,但无法得知每个匹配除结果之外的相关信息,比如捕获到的子串,匹配到的index
位置等 。
exec ()
const str =
"es2015/es6 es2016/es7 es2017/es8 es2018/es9 es2019/es10 es2020/es10";
const reg = /(es\d+)\/es(\d+)/g;
let matched;
let formatted = [];
while ((matched = reg.exec(str))) {
formatted.push(`${matched[1]}-es${matched[2]}`);
}
console.log(formatted);
//["es2015-es6","es2016-es7","es2017-es8","es2018-es9","es2019-es10",
//"es2020-es10"]
matchAll()
const str =
"es2015/es6 es2016/es7 es2017/es8 es2018/es9 es2019/es10 es2020/es10";
const reg = /(es\d+)\/es(\d+)/g;
const matchs = [];
for (let match of str.matchAll(reg)) {
matchs.push(`${match[1]}-es${match[2]}`);
}
console.log(matchs);
// ["es2015-es6", "es2016-es7", "es2017-es8", "es2018-es9", "es2019-es10", "es2020-es10"]
matchAll() 是返回一个迭代器,对大数据量的场景更友好 。
Javascript 在不同的环境获取全局对象有不通的方式:
es11提出的globalThis一句话总结就是: 无论是在node环境还是web中,全局作用域中的 this 可以通过globalThis访问, 不必担心它的运行环境 。
JavaScript 中通过for-in遍历对象时 key 的顺序是不确定的,因为规范没有明确定义,并且能够遍历原型属性让for-in的实现机制变得相当复杂,不同 JavaScript 引擎有各自根深蒂固的不同实现,很难统一
- 所以 es11不要求统一属性遍历顺序,而是对遍历过程中的一些特殊 Case 明确定义了一些规则:
- 遍历不到 Symbol 类型的属性
- 遍历过程中,目标对象的属性能被删除,忽略掉尚未遍历到却已经被删掉的属性
- 遍历过程中,如果有新增属性,不保证新的属性能被当次遍历处理到
- 属性名不会重复出现(一个属性名最多出现一次)
- 目标对象整条原型链上的属性都能遍历到
相关文档: