function test() {
console.log(a) // 不会报错,直接输出undefined
if(false) {
var a = 1
}
}
test()
// 相当于
function test() {
var a
console.log(a)
if(false) {
a = 1
}
}
test()
另外,定义和声明是两码事,如果变量都还没定义就使用时,就会报错(xxx is not defined)。
let 和 const 都能够声明块级作用域,用法和 var 是类似的,let 和 const 都是不会变量提升。看个🌰:
function test() {
if(false) {
console.log(a) // 报错,a is not defined(也是传说中的临时性死区)
let a = 1
}
}
test()
let 和 const 必须先声明再访问。
所谓临时性死区,就是在当前作用域的块内,在声明变量前的区域(临时性死区只有 let 和 const 才有)。看个🌰:
if(false) {
// 该区域便是临时性死区
let a = 1
}
const 声明变量时必须同时赋值,并且不可更改。
在全局作用域中使用 var 声明的变量会存储在 window 对象中。而使用 let 和 const 声明的变量则只会覆盖 window 对象中同名的属性,而不会替换。看个🌰:
window.a = 1
let a = 2
console.log(a) // 2
console.log(window.a) // 1
let 和 const 声明的变量会存在一个单独的Script块作用域中(即[Scopes]作用域中能找到)。
function *aa() {
var a1 = yield 1
var a2 = 10
yield a1 + 10
}
var a = aa()
console.log(a.next(2)) // { value: 1, done: false }
console.log(a.next(100)) // { value: 110, done: false }
抛出异常。
function *aa() {
var a1 = yield 1
var a2 = 10
yield a1 + 10
}
var a = aa()
console.log(a.next(2)) // { value: 1, done: false }
console.log(a.throw(new Error('error'))) // error
console.log(a.next(100)) // 不再执行
生成器种遇到 return 语句时,表示退出操作。
function *aa() {
var a1 = yield 1
return
yield a1 + 10
}
var a = aa()
console.log(a.next(2)) // { value: 1, done: false }
console.log(a.next(100)) // { value: undefined, done: true }
委托生成器。其实就是生成器嵌套生成器。
function *aIterator() {
yield 1;
}
function *bIterator() {
yield 2;
}
function *cIterator() {
yield *aIterator()
yield *bIterator()
}
var i = cIterator()
console.log(i.next()) // {value: 1, done: false}
console.log(i.next()) // {value: 2, done: false}
异步任务执行器,其实就是用来循环执行生成器。
因为我们知道生成器需要执行 N 次 next() 方法才能运行完,异步任务执行器就是帮我们做这些事情的。
function run(taskFn) {
var task = taskFn() // 调用生成器
var result = task.next()
function step() {
if(!result.done) {
result = task.next(result.value)
step()
}
}
step()
}
run(function *() {
let text = yield fetch() // 异步请求获取数据
doSomething(text) // 处理返回结果
})
class Person {
constructor(name) { // 新建构造函数
this.name = name // 私有属性
}
sayName() { // 定义一个方法并且赋值到构造函数的原型中
return this.name
}
}
私有属性的定义,只需要在构造方法中定义this.xx = xx即可。
类声明和函数声明的区别,主要有:
类声明不能提升,而函数声明则会被提升。
类声明中代码会自动强行运行在严格模式下。
类中的所有方法都是不可枚举的,而函数声明的对象中方法则是可以枚举的。
类中的构造函数只能使用 new 来调用,而函数则可以普通调用或 new 来调用。
类的定义有声明式定义和表达式定义,看个🌰:
// 声明式定义
class Person {
// ...
}
// 表达式定义
let person = Class {
// ...
}
类还支持立即调用。
let person = new Class {
constructor(name) {
this.name = name
}
sayName() {
return this.name
}
}('Andraw')
console.log(person.sayName()) // Andraw
类支持在原型上定义访问器属性。看个🌰
class Person {
constructor(name) {
this.name = name
}
get myName() {
return this.name
}
set myName(name) {
this.name = name
}
}
var descriptor = Object.getOwnPropertyDescriptor(Person.prototype, 'myName')
console.log('get' in descriptor) // true
console.log(descriptor.enumerable) // false 表示不可枚举
类中定义的属性或方法名都是可支持表达式的。
const test = 'sayName'
class Person{
constructor(name) {
this.name = name
}
[test]() {
return this.name
}
}
// unhandledRejection
let rejected
rejected = Promise.reject("It was my wrong!")
process.on("unhandledRjection", function(reason, promise) {
console.log(reason.message) // It was my wrong!
console.log(rejected === promise) // true
})
// rejectionHandled
let rejected
rejected = Promise.reject("It was my wrong!")
process.on("rejectionHandled", function(reason, promise) {
console.log(reason.message) // It was my wrong!
console.log(rejected === promise) // true
})
浏览器中使用上面两个方法只是在 window 对象上监听,而 Node 中使用是在 process 对象上监听。
本文针对
Javascript
中每个版本的一些知识总结,主要为了方便知识梳理,避免遇到盲点。🤔(每一年官方出的新版本
ES
,都会同时进行更新的哈哈)目录
ES6
在
ES6
中,可以说是整个Javascript
改版中最大的一版,下面就来看看主要包含哪些内容。在 Javascript 中不存在块级作用域,只存在全局作用域和函数作用域。
使用 var 声明的变量不管在哪声明,都会变量提升到当前作用域的最顶部。看个🌰:
另外,定义和声明是两码事,如果变量都还没定义就使用时,就会报错(xxx is not defined)。
let 和 const 都能够声明块级作用域,用法和 var 是类似的,let 和 const 都是不会变量提升。看个🌰:
let 和 const 必须先声明再访问。
所谓临时性死区,就是在当前作用域的块内,在声明变量前的区域(临时性死区只有 let 和 const 才有)。看个🌰:
const 声明变量时必须同时赋值,并且不可更改。
在全局作用域中使用 var 声明的变量会存储在 window 对象中。而使用 let 和 const 声明的变量则只会覆盖 window 对象中同名的属性,而不会替换。看个🌰:
let 和 const 声明的变量会存在一个单独的
Script
块作用域中(即[Scopes]作用域中能找到)。支持 UTF-16(含义是任何一个字符都适用两个字节表示),其中方法如下:
字符串中新增的方法有:
includes(str, index):检测字符串指定的可选索引中是否存在参数文本。
startsWith(str, index):检测字符串头部是否有指定的文本。
endsWith(str, index):检测字符串尾部是否有指定的文本。
repeat(number):接受一个整数,重复对应字符串整数次。
当给正则表达式添加 u 字符时,表示从编码单元操作模式切换为字符模式。
模板字符串支持多行文本、模板中动态插入变量、模板子面量方法使用。
多行文本。
模板中动态插入变量。
模板子面量方法。
其中参数 a 表示模板字符串中静态字符。参数 b 表示模板动态变量。
支持默认参数,默认参数不仅可以为字符串、数字、数组或对象,还可以是一个函数。看个🌰:
参数默认值不能被 arguments 识别,看个🌰:
默认参数同样存在临时性死区,看个🌰:
支持展开运算符(...),其作用是解构数组和对象。看个🌰:
支持箭头函数。箭头函数和普通函数的区别有:
支持尾调用优化。(调用一个函数时都会生成一个函数调用栈,尾调用就可以很好滴避免生成不必要的尾调用阮一峰的尾调用优化讲解)
只有满足以下三个条件时,引擎才会帮我们做好尾调用优化。
看个🌰:
需要知道的是,递归都是很影响性能的,但是有了尾调用后,递归函数的性能将得到有效的提升。
对象方法和属性支持简写,以及对象属性支持可计算。看个🌰:
Object 新增了方法如下:
Object.is。判断两个值是否相等。
Object.assign。浅拷贝一个对象,相当于一个 Mixin 功能。
对象支持同名属性,不过后面的属性会覆盖前面同名的属性。看个🌰:
遍历对象时,默认都是数字属性按顺序提前,接着就是首字母排序。看个🌰:
支持实例化后的对象改变原型对象,使用方法
Object.setPrototypeOf()
。解构的定义是,从对象中提取出更小元素的过程,即对解构出来的元素进行重新赋值。
对象解构必须是同名的属性。看个🌰:
数组解构可以有效地处理交换两个变量的值。
数组解构还可以按需取值,看个🌰:
混合解构就是对象和数组解构结合,看个🌰:
解构参数就是直接从参数中获取相应的参数值。看个🌰:
Symbol 是一种特殊的、不可变的数据类型,可作为对象属性的标识符使用,也是一种原始数据类型。
Symbol 的语法格式为:
创建一个 Symbol,如下:
创建 Symbol 不能使用 new。
Symbol 最大的用处在于创建对象一个唯一可计算的属性名。看个🌰:
有效地避免命名冲突问题。
Symbol 不支持强制转换为其他类型。
在 ES6 中提出一个
@@iterator
方法,所有支持迭代的对象(如数组、Set、Map)都要实现。其中@@iterator
方法的属性键为Symbol.iterator
而非字符串。只要对象定义有Symbol.iterator
属性就可以用for...of
进行迭代。Symbol 支持全局共享机制,使用 Symbol.for 进行注册,使用 Symbol.keyFor 进行获取。看个🌰:
Symbol.keyFor 最终获取到的是一个字符串值。
Symbol 可作为类的私有属性,使用 Object.keys 或 Object.getOwnPropertyNames 方法都无法获取 Symbol 的属性名。只能使用 for...of 或 Object.getOwnPropertySymbols 方法获取。看个🌰:
Set 常用于检查对象中是否存在某个键值,Map 则常用于获取已存的信息。
Set 是有序列表,含有相互独立的非重复值。支持的属性和方法如下:
size。返回 Set 对象中元素个数。
add(value)。在 Set 对象尾部添加一个元素。
entries()。返回 Set 对象中[值,值]形式,看个🌰:
forEach(callback)。用于遍历 Set 集合。
has(value)。判断 Set 集合中是否存在有指定的值。
Set 集合的特点是没有下标,没有 Key。Set 和 Array可以相互转换,如下:
Set 集合是一个强引用,只要 new Set 实例化的引用存在,就不会释放内存。若定义一个 DOM 元素的 Set 集合,然后在某个 js 中引用了该实例,当页面跳转时,并不会立马释放内存,因为引用还在。WeakSet 就是专门用于释放强引用的。
WeakSet 和 Set 区别:
Map 是存储键值对的有序列表,key 和 value 支持所有数据类型。对比 Set 集合,Map 集合多了 set 方法和 get 方法。看个🌰:
支持对象作为 key 值,看个🌰:
和 WeakSet 一样,也会有 WeakMap 存在,专门针对弱引用。看个🌰:
迭代器是一种特殊对象,每一个迭代器对象都有一个 next(),该方法返回一个对象,包括了 value 和 done 属性。使用 ES5 模拟实现迭代器如下:
生成器就是一个函数,用于返回迭代器的。使用 * 号声明的函数即为生成器函数,同时需要使用 yield 控制进程。看个🌰:
总结一下,迭代器执行 next() 方法时,只会执行前面到 yield 间的代码,后面代码都会被终止。
同样地,在 for 循环中使用迭代器,遇到 yield 时都会终止进程。看个🌰:
yield 只能在生成器函数内使用。
生成器函数还可以使用匿名函数形式创建,看个🌰:
凡是通过生成器得到的迭代器,都是可迭代的对象(即可迭代对象具有 Symbol.iterator 属性),可使用 for...of 进行迭代。看个🌰:
可迭代对象可访问 Symbol.iterator 直接得到迭代器,看下面
Symbol.iterator 可用于检测一个对象是否可迭代。
默认情况下定义的对象是不可迭代的,但是可以通过 Symbol.iterator 创建迭代器。看个🌰:
数组、Set、Map等可迭代对象,其内部已实现迭代器,并且提供3种迭代器调用,分别是:
entries():返回一个迭代器,用于返回键值对。
values():返回一个迭代器,用于返回键值对的value。
keys():返回一个迭代器,用于返回键值对的key。
高级迭代器功能,主要包括传参、抛出异常、生成器返回语句、委托生成器。
传参。next 方法传参数时,会作为上一轮 yield 的返回值,除了第一轮 yield 外,看个🌰:
抛出异常。
生成器种遇到 return 语句时,表示退出操作。
委托生成器。其实就是生成器嵌套生成器。
异步任务执行器,其实就是用来循环执行生成器。
因为我们知道生成器需要执行 N 次 next() 方法才能运行完,异步任务执行器就是帮我们做这些事情的。
异步任务执行器,其实就是间接实现了 async 和 await 功能。
在 ES6 中,将原型的实现写在类中,和 ES5 本质上是一致的,都是需要新建一个类名,然后实现构造函数再实现原型方法。看个🌰:
私有属性的定义,只需要在构造方法中定义
this.xx = xx
即可。类声明和函数声明的区别,主要有:
类的定义有声明式定义和表达式定义,看个🌰:
类还支持立即调用。
类支持在原型上定义访问器属性。看个🌰
类中定义的属性或方法名都是可支持表达式的。
类中定义的方法同样可以是生成器方法。
静态属性或静态方法,是在属性或方法前面使用 static 关键字。static 修饰的方法或属性只能被类本省直接访问,而不能在实例中访问。
在 React 中写一个组件
Test
时,必须得继承React.Component
。其中 Test 组件就是一个派生类。派生类中的构造函数内部必须使用 super()。关于 super 的使用需要注意:
当派生类继承于父类时,其父类中的静态成员也会被继承到派生类中,但是静态成员同样只能是被派生类访问,而无法被其实例访问。
在构造函数中可使用 new target(new target 通常表示当前的构造函数名)来阻止实例化类。看个🌰:
在 ES5 中创建数组的方式有两种,分别是数组子面量(即 var arr = [])和 Array 实例(即 new Array())。
在 ES6 中新增两种方法创建数组,分别是 Array.of() 和 Array.from()。
Array.of()。我们知道 new Array() 中传入一个数字时,表示的是生成多少长度的数组,Array.of() 就是为了处理这种尴尬场面的,看个🌰:
Array.from()。用于将类数组转换为数组。
数组新增的方法有。
find()。传入一个回调函数,找到数组中符合当前搜索规则的第一个元素,返回它,并且终止搜索。
findIndex()。传入一个回调函数,找到数组中符合当前搜索规则的第一个元素,返回它的下标,终止搜索。
fill()。用新元素替换掉数组内的元素,可以指定替换下标范围。格式和🌰如下:
copyWithin()。选择数组的某个下标,从该位置开始复制数组元素,默认从0开始复制。格式和🌰如下:
对 DOM 做事件处理操作,如点击、激活焦点、失去焦点等,再比如使用 Ajax 请求数据时利用回调函数获取返回值,都属于异步编程。
Promise 中文意思就是承诺,Javascript 对你许一个承诺,会在未来某个时刻兑现承诺。
Promise 有生命周期,分别是进行中(pending)、已经完成(fulfilled)、拒绝(rejected)。
Promise 不会直接返回异步函数的执行结果,需要使用 then 方法获取,获取异常回调时,需要使用 catch 方法获取。
结合 axios 看个🌰,axios 是前端比较热门的 http 请求插件之一。
Promise 构造函数只有一个参数,该参数为一个函数,被作为执行器。执行器有两个参数,分别是 resolve() 和 reject() ,前一个表示成功回调,后一个表示失败回调。
Promise 实例只能过 resolve 或者 reject 函数返回数据,并且使用 then 或者 catch 进行获取。
浏览器和 Node 提供了 unhandledRejection 和 rejectionHandled 两个事件处理 Promise 中没有设置 catch 问题。
浏览器中使用上面两个方法只是在 window 对象上监听,而 Node 中使用是在 process 对象上监听。
unhandledRejection 和 rejectionHandled的区别就是,前者是事件循环中触发,后者则是事件循环后触发。两者都是处理 Promise 中使用 reject 捕获错误时,而没有使用 catch 进行捕获处理。
Promise 支持链式调用,有效地解决了回调地狱问题。看个🌰:
除了 resolve 和 reject 方法外,还有两个方法,便是 Promise.all 和 Promise.race。
Promise.all。运行多个 Promise,当全部 Promise 都返回结果时,才会使用 then 进行处理。
Promise.race。和 all 方法类似,不过就是当有一个返回结果时,就可以使用 then 进行处理。
Promise 本身不是异步,只有它的 then 方法或者 catch 方法才是异步。
目前 ES7 已经支持 async 方案,该方案比 Promise 还强大啊。
代理 Proxy 就是拦截 JS 引擎内部目标的底层对象操作,反射 Reflect 就是针对 Proxy 还原原对象操作方法。
反射 Reflect 一般和代理 Proxy 结合使用,设置相应的代理方法处理数据时,需同时使用反射 Reflect 对原对象的方法操作一遍。
现在就拿 set 做个🌰。set属性是在要改变Proxy属性值时,进行的预处理,共接收四个参数:
Proxy的存在能够使函数加上钩子函数,即可理解为在执行方法前预处理一些代码,举个栗子:
在使用如 http-proxy 插件或 webpack 时,有时需要访问某个 api,通过配置 proxy 跳转到指定的 url 能解决跨域问题。但是该种模式和代理 Proxy 有异曲同工之处,但是机制是不一样的。
模块可以是函数、数据、类,需要指定导出的模块名,才能被其他模块访问。看个🌰
模块引入使用 import 关键字,导入模块方式有两种。
导入指定的模块。
导入全部模块。
模块导出使用 export 关键字,看个🌰:
需要注意的是,ES6 提供了模块的默认导出,在导出时结合 default 关键字,看个🌰:
不能在语句和函数内使用 export 关键字,只能在模块顶部使用。
ES6 提供了两种方式修改模块的导入和导出名,分别是导出时修改和导入时修改,使用 as 关键字。
导出时修改。
导入时修改。
无绑定导入,是指当模块没有可导出模块时,全都是定义的全局变量,可使用无绑定导入。看个🌰:
使用 webpack 打包 js 后,浏览器加载模块时,总是按顺序加载,先加载模块1,再加载模块2,因为 module 类型默认使用 defer 属性。
ES7
ES7 在 ES6 基础上仅仅新增了求幂运算符(**)和 Array.prototype.includes() 方法。
需要注意的是,在 ES6 中仅仅提供了字符串 includes 实现,而在 ES7 中则在数组中进行完善。
** 运算符相当于 Math 对象中的 pow 求幂方法,使用如下。
** 运算符和 +- 运算符用法一致,看个🌰:
数组中实现的 includes 方法,用于判断一个数组是否包含一个指定的值,如果包含就返回 true,否则返回 false。
includes 和 indexOf 都是使用 === 来进行比较,但是在 includes 中,NaN === NaN 返回的是 true,而 indexOf 则是返回 false。
另外,includes 和 indexOf 方法都是认为,+0 === -0。
ES8
ES8 也是在 ES6 基础上继续进行拓展。
在 ES6 中提及过,只有可迭代对象可以直接访问 keys、entries、values 三个方法。在 ES8 中在 Object 对象上实现了 values 和 entries 方法,因为 Object 已经支持了 kes 方法,直接看🌰:
其中,entries 方法还能结合 Map 数据结构。
字符串新增方法 String.prototype.padStart 和 String.prototype.padEnd,用于向字符串中追加新的字符串。看个🌰:
padStart 和 padEnd 对于格式化输出很有用。
使用 padStart 方法举个例子,有一个不同长度的数组,往前面追加 0 来使得长度都为 10。
使用 padEnd 也是同样的道理。
Object.getOwnPropertyDescriptors 直接返回一个对象所有的属性,甚至包括 get/set 函数。
ES2017 引入该函数主要目的在于方便将一个对象浅拷贝给另一个对象,同时也可以将 getter/setter 函数也进行拷贝。意义上和 Object.assign 是不一样的。
直接看个🌰:
在克隆对象方面,Object.assign 只能拷贝源对象中可枚举的自身属性,同时拷贝时无法拷贝属性的特性(如 getter/setter)。而使用 Object.getOwnPropertyDescriptors 方法则可以直接将源对象的所有自身属性(是自身属性啊,不是所有可访问属性!)弄出来,再拿去复制。
上面的栗子中就是配合原型,将一个对象中可访问属性都拿出来进行复制,弥补了 Object.getOwnPropertyDescriptors 方法短处(即无法获取可访问原型中的属性)。
若只是浅复制自身属性,还可以结合 Object.defineProperties 来实现。
听说是为了方便 git 算法更加方便区分代码职责。直接看个🌰。
在 ES8 所有更新中,最有用的一个!!!
async 关键字告诉 Javascript 编译器对于标定的函数要区别对待。当编译器遇到 await 函数时会暂停,它会等到 await 标定的函数返回的 promise,该 promise 要么 resolve 得到结果、要么 reject 处理异常。
直接上一个栗子,对比一下使用 promise 和使用 async 区别。
使用 ES8 的 async 异步编程更符合日常开发流程,而 ES6 的 promise 也是一个很好的使用, ES8 的 async 只是在 promise 基础上更上一层楼。
async 函数返回 promise。
若想获取一个 async 函数的返回结果,则需要使用 promise 的 then 方法。
接着拿上述 ES8 的 async 实现方式来举个例子。
并行处理
我们知道,每次调用 es8Fn 函数时,都需要等到至少 4 秒时间,若调用 N 次,则需要等到 4N 秒。使用 Promise.all 来并行处理,可以极大释放时间限制。
上述并行处理后,就可以很好滴避免多次调用而时间耗费的问题。
错误处理
对于 async/await 的错误处理,有三种方法可以处理,分别是在函数中使用 try-catch、catch 每一个 await 表达式、catch 整个 async-await 函数。
在函数中使用 try-catch
catch 每一个 await 表达式
由于每一个 await 表达式都返回 Promise,对每一个表达式都进行 catch 处理。
catch 整个 async-await 函数
ES9
ES9(即ES2018) 主要新增了对象的扩展运算符 Rest 以及 Spread、异步迭代器、Promise支持 finally 方法、正则的扩展。
如果使用过 Object.assign 方法合并对象,应该就很清楚。在 ES6 中,在数组中支持了 Rest 解构赋值和 spread 语法。
ES8 则在对象中支持了 Rest 解构赋值和 Spread 语法。
在 ES6 中,如果一个对象具有 Symbol.iterator 方法,那该对象就是可迭代的。目前,只有 Set、Map、数组内部实现 Symbol.iterator 方法,因此都是属于可迭代对象。
默认的对象是不支持可迭代的,若实现了 Symbol.iterator 方法,那么它也是可迭代的。那么对象的 Symbol.iterator 方法如何实现的呢?
上面的实现,还可以再完善一丢。利用生成器
由上面可以知道,同步迭代器就是一个特殊对象,里面包含有 value 和 done 两个属性(即 {value, done})。那么异步迭代器又是什么?
异步迭代器,和同步迭代器不同,不返回 {value, done} 形式的普通对象,而是直接返回一个 {value, done} 的 promise 对象。
其中,同步迭代器使用 Symbol.iterator 实现,异步迭代器使用 Symbol.asyncIterator 实现。
那么既然有了异步迭代器,就肯定有异步生成器,专门用来生成异步迭代器的。
另外,异步迭代器和同步迭代器有一样东西很类似,就是使用 next() 后,是无法知道什么时候才会到最后一个值,在同步迭代器中,需要使用 for...of 进行遍历才能有效地处理迭代器中每一项值。
在异步迭代器中,同样支持遍历,不过是 for...await...of 遍历。
for...await...of 只会在异步生成器中或异步函数中有效。
Promise 成功获取数据时使用 then 方法,处理异常时使用 catch 方法。但是在某些情况下,我们不管成功还是存在异常,都希望 Promise 能够运行一些共同的代码,finally 就是处理这些事情的。
在正则表达式中,点 . 可以表示任意单个字符。
上面代码中,为了能够匹配任意字符,ES9 提出了 s 修饰符,使得 . 可以匹配任意单个字符。
还有几个暂不讨论。可自行了解哈
ES10
ES10(即 ES2019) 新增功能相对比较少,都是一些性能的优化。
在日常开发中,我们常遇到一个问题,那就是将
[1, [1, 2], [1, [2, 3]]]
扁平化为[1, 1, 2, 1, 2, 3]
。以往的经历告诉我们,需要使用第三方库 lodash 来处理,导致了不必要的麻烦,为此,ES10 直接为数组提供了 flat 方法来实现扁平化数组。
flat 方法中参数,表示的是扁平化的层数。
另外的方法 flatMap,其实就是数组的 flat 方法和 map 方法结合。
Object.fromEntries 方法和 ES6 中的 Object.entries 功能刚好相反,Object.entries 是获取一个对象的键值对,而 Object.fromEntries 则是将键值对转化为对象。
ES10 为字符串提供了 trimStart 和 trimEnd 方法,用于去除首尾空格。
定义 Symbol 类型时,可传入一个字符串作为标志,若想获得该字符串,ES6 并没有提供方法,而 ES10 则提供了 description 属性用于获取 Symbol 的描述信息。
在 ES10 之前,使用 try...catch 块时,若不给 catch 函数传递参数,会报错。
ES10 则直接将 catch 参数作为可选。
在 ES10 前,数组的 sort 方法默认采取的是快排,但会存在不稳定性,为此,直接转为使用 Timsort。可自行了解一下。
Timsort 就是将插入排序和归并排序进行合并起来得到的好算法。
ES10 支持函数直接以字符串的形式打印出来。