ESnext 特性一览
历史背景
1995 年,网景公司一位叫做 Brendan Eich 的工程师,为即将发布的 Netscape Navigator2 开发了一个叫做 Mocha(后来改名为 LiveScript) 的脚本语言,当时的计划是在客户端和服务端都使用它,在服务端的名字叫做 LiveWire.
后来为了赶上发布时间,网景与 Sun 公司结成开发联盟,共同完成了 LiveScript 的开发。就在 Netscape Navigator2 正式发布之前,网景把 LiveScript 改名为了 JavaScript,为的是蹭一下 Java 热度。
JavaScript1.0 非常成功,网景公司又在 Netscape Navigator3 中发布了 JavaScript1.1 版本,并且稳居浏览器领导者地位。
这时候,微软坐不住了,打算向 IE 投入更多资源。微软在 Netscape Navigator3 推出后不久,就推出了 IE3,并且包含了 JScript. 之所以叫这个名字,是为了防止和网景公司发生许可纠纷。
此时,意味着出现了两个版本的 JavaScript:
- Netscape Navigator 中的 JavaScript
- IE 中的 JScript
两个版本的 JavaScript 虽然功能上面大差不差,但是有各种细节上面的不同行为,这对于开发者来讲简直是灾难,因为这往往意味着你网页的脚本在一个浏览器中行为正常,但是在另外一个浏览器中不正常。这个问题的根本原因其实是因为当时的 JavaScript 不像 C 语言或者其他编程语言一样,有一份清晰的规范。
1997 年,JavaScript1.1 作为提案被提交给了欧洲计算机制造协会(Ecma),其中的第 39 技术委员会(TC39)负责来为这门语言制定标准,TC39 委员会的成员有网景、Sun、微软等,他们花了数月时间推出了 ECMA-262,也就是 ECMAScript. 1998 年,国际标准化组织(ISO)和国际电工委员会(IEC)也将 ECMAScript 采纳为标准(ISO/IEC-16262)。
自此,各个浏览器均已把 ECMAScript 作为自家 JavaScript 引擎的实现依据,内部实现可能不同,但是行为上一定和规范所定义的标准是相同的。
ECMAScript
首先需要说明,ECMAScript 仅仅是一个规范,这个规范甚至没有输入和输出之类的方法。该规范主要定义了:
- 语法
- 类型
- 语句
- 关键字
- 保留字
- 操作符
- 全局对象
ECMAScript 只是对实现了该规范所有方面的一门语言的称呼。JavaScript 实现了 ECMAScript,Adobe 的 ActionScript 同样也实现了 ECMAScript.
下面简单说一下 ECMAScript 版本的发展历程:
- ECMAScript1:本质上与网景的 JavaScript1.1 相同,只不过删除了所有浏览器特定的代码,外加少量细微的修改。
- ECMAScript2:只是做了一些编校工作,目的是为了符合 ISO/IEC-16262 的要求,并没有增加和改变任何特性。
- ECMAScript3:这是第一次真正对标准进行更新,更新了字符串处理、错误定义和数值输出。此外还增加了对正则表达式、新的控制语句、try/catch 异常处理的支持。此版本的更新让 ES 看上去更像是一门真正的编程语言
- ECMAScript4: 这一版是对这门语言的彻底修订,为了满足全球 Web 开发者日益增长的需求,TC39 再次被召集起来,制定新版本的规范。结果,他们制定出来的规范几乎在第 3 版的基础上完全定义了一门新的语言,包括类型变量、新语句和数据结构、真正的类和经典的继承。此时内部出现了分歧,有的人认为这样太过于激进,有的人认为这就是这门语言的未来。最终,ECMAScript4 一直没有推出。
- ECMAScript3.1: 之后,TC39 委员会的一个子委员会,提出了另外一份提案,被称之为 ECMAScript3.1,相比 ECMAScript4,ECMAScript3.1 来得更加温和,虽然也有一些改动,但是改动较小,只要在现有的 JavaScript 引擎基础上做一些增改就可以实现。最终,ECMAScript3.1 赢得了 TC39 委员会的支持,ECMAScript4 在正式发布之前被废弃。
- ECMAScript5 :ECMAScript3.1 成为了 ECMAScript5,于 2009 年 12 月 3 日正式发布。该版本主要解决第 3 版存在歧义的地方,也增加了新的功能。例如原生的解析和序列化 JSON 数据、严格模式等。另外第 5 版在 2011 年 6 月还发布了一个维护性的修订版。
- ECMAScript6 :俗称 ES6 或者 ES Harmony(和协版),于 2015 年 6 月发布。这个版本其实就是之前 ECMAScript4 所遗留的精神产物,涵盖了这个规范有史以来最多、也重要的一批增强特性,例如正式支持类、模块、迭代器、生成器、箭头函数、promise、反射、代理和新的数据类型。另外,从这个版本开始,ECMAScript 也变成了一年更新一次,以年号来命名。因此 ES6 也被称之为 ES2015.
- ECMAScript2016:也被称之为 ES7,于 2016 年 6 月发布。
- ECMAScript2017:也被称之为 ES8,于 2017 年 6 月发布。
- ECMAScript2018:也被称之为 ES9,于 2018 年 6 月发布。
- ECMAScript2019:也被称之为 ES10,于 2019 年 6 月发布。
- ECMAScript2020:也被称之为 ES11,于 2020 年 6 月发布。
- ECMAScript2021:也被称之为 ES12,于 2021 年 6 月发布。
- ECMAScript2022:也被称之为 ES13,于 2022 年 6 月发布。
- ECMAScript2023:也被称之为 ES14,于 2023 年 6 月发布。
- ECMAScript2024:也被称之为 ES15,于 2024 年 6 月发布。
ES2016
ES2016(ES7)是一个相对较小的更新版本,只引入了两个新特性:
Array.prototype.includes
Array.prototype.includes 方法用于检查一个数组中是否包含某个特定的元素,返回布尔值 true 或 false。
JavaScriptconst arr = [1, 2, 3, 4, 5]; console.log(arr.includes(3)); // true console.log(arr.includes(-1)); // false
优势:它是 indexOf 的改进版本,区别在于 indexOf 返回的是元素的索引值,而 includes 返回布尔值。这样所表达的语义更好一些。此外,includes 还可以正确处理 NaN,而 indexOf 不能。
JavaScript// 以前通常使用 indexOf 来查找特定的元素 console.log(arr.indexOf(3)); // 2 console.log(arr.indexOf(-1)); // -1 console.log([NaN].includes(NaN)); // true console.log([NaN].indexOf(NaN)); // -1
Exponentiation Operator
Exponentiation Operator 这个操作符用于求幂运算,类似于数学中的指数运算。它是 Math.pow( ) 的简化写法。
JavaScriptconsole.log(Math.pow(2, 3)); // 8 console.log(Math.pow(3, 4)); // 81 console.log(2 ** 3); // 8 console.log(3 ** 4); // 81
关于这个运算符的结合性,一定要注意,它是右结合性,计算的时候从右往左。
面试题
JavaScriptlet i = 1; let o = { // 访问器属性 get a() { return ++i; } }; console.log(o.a ** (o.a ** o.a)); // 2 ** 3 ** 4 // 2 ** 81 // 2417851639229258349412352
ES2017
ES2017(ES8)引入了一些关键特性,尤其是 async/await,极大简化了异步编程的难度。除此之外,新增的对象和字符串方法也为开发者提供了更强大的工具来操作数据。这些改进使 JS 代码更简洁、高效和易于维护。
- Async Functions
- Object.entries
- Object.values
- String.prototype.padStart
- String.prototype.padEnd
- Object.getOwnPropertyDescriptors
- Trailing Commas
Async Functions
async 函数使得异步操作变得更加简洁,await 可以暂停 async 函数的执行,等待一个 Promise 被解决,并返回结果。这大大简化了基于 Promise 的异步代码,使代码的可读性更好,结构上更像同步代码。
对比示例:找到韩梅梅的班主任,有三张表
- 学生表:通过学生表找到学生所在班级 id
- 班级表:通过班级表找到该班级的班主任 id
- 教师表:通过班主任 id 查询到班主任名称
Promise 编程
fetch('./stu.json')
.then((response) => response.json())
.then((data) => {
// 找出韩梅梅所在的班级id
let classId = null;
for (let student of data.student) {
if (student.name === '韩梅梅') {
classId = student.classId;
break;
}
}
// 返回班级ID
return classId;
})
.then((classId) => {
// 根据班级ID获取班级信息
return fetch('./classes.json')
.then((response) => response.json())
.then((data) => {
let teacherId = null;
for (let cls of data.classes) {
if (cls.id === classId) {
teacherId = cls.teacherId;
break;
}
}
// 返回老师ID
return teacherId;
});
})
.then((teacherId) => {
// 根据老师ID获取老师信息
return fetch('./teacher.json')
.then((response) => response.json())
.then((data) => {
for (let teacher of data.teachers) {
if (teacher.id === teacherId) {
console.log(`韩梅梅的班主任为${teacher.name}`);
break;
}
}
});
})
.catch((error) => {
console.error('发生错误:', error);
});
async 函数
// 使用 fetch 获取韩梅梅的班级 ID
function getClassId() {
return fetch('./stu.json')
.then((response) => response.json())
.then((data) => {
// 找出韩梅梅所在的班级id
for (let i of data.student) {
if (i.name === '韩梅梅') {
return i.classId; // resolve with classId
}
}
});
}
// 使用 fetch 获取班级对应的老师 ID
function getTeacherId(classId) {
return fetch('./classes.json')
.then((response) => response.json())
.then((data) => {
for (let i of data.classes) {
if (i.id === classId) {
return i.teacherId; // resolve with teacherId
}
}
});
}
// 使用 fetch 获取老师名字
function getTeacherName(teacherId) {
return fetch('./teacher.json')
.then((response) => response.json())
.then((data) => {
for (let i of data.teachers) {
if (i.id === teacherId) {
return i.name; // resolve with teacherName
}
}
});
}
async function getInfo() {
try {
let classId = await getClassId();
let teacherId = await getTeacherId(classId);
let teacherName = await getTeacherName(teacherId);
} catch (e) {
console.log(e);
}
}
Object.entries
Object.entries( ) 返回一个给定对象自身可枚举属性的键值对数组,其顺序与 for...in 循环一致(区别是 for...in 枚举原型链上的属性)。
const obj = {
name: '张三',
age: 18
};
console.log(Object.entries(obj)); // [['name', '张三'], ['age', 18]]
当你需要遍历对象的键值对时非常有用。
和 for... in 的区别如下:
- 是否遍历原型链上的属性:
- for...in 会遍历对象自身的可枚举属性以及其原型链上的可枚举属性。
- Object.entries( ) 只返回对象自身的可枚举属性,不会遍历原型链上的属性。
- 返回的结果:
- for...in 只遍历对象的键(属性名)。
- Object.entries( ) 返回的是键值对数组,每个元素是一个数组,包含键和值。
function Person() {
this.name = '张三';
this.age = 18;
}
Person.prototype.test = 'test';
Person.prototype.say = function () {
console.log('Hello');
};
const p = new Person();
for (const key in p) {
console.log(key);
}
console.log(Object.entries(p));
Object.values
Object.values( ) 返回一个给定对象自身可枚举属性值的数组,同样,Object.values( ) 只返回对象自身的可枚举属性值,不会遍历原型链上的属性。
const obj = {
name: '张三',
age: 18
};
console.log(Object.values(obj)); // [ '张三', 18 ]
console.log(Object.keys(obj)); // [ 'name', 'age' ]
console.log(Object.entries(obj)); // [ [ 'name', '张三' ], [ 'age', 18 ] ]
String.prototype.padStart
padStart( ) 方法用于在当前字符串的开头填充指定的字符,直到字符串达到指定的长度。不指定填充字符会自动用空格补齐。
console.log('5'.padStart(3, '0')); // '005'
console.log('123'.padStart(10, '*')); // '**********123'
console.log('123'.padStart(10)); // ' 123'
应用场景:格式化数字、字符串,例如为日期、时间或编号填充前导零。
String.prototype.padEnd
padEnd( ) 方法用于在当前字符串的末尾填充指定的字符,直到字符串达到指定的长度。不指定填充字符会自动用空格补齐。
console.log('5'.padEnd(3, '0')); // '500'
console.log('123'.padEnd(10, '*')); // '123**********'
console.log('123'.padEnd(10)); // '123 '
应用场景:用于创建固定宽度的输出,在报告生成或格式化输出时有用。
// 商品数据
const products = [
{ name: 'Apple', price: 1.5, stock: 120 },
{ name: 'Banana', price: 0.8, stock: 60 },
{ name: 'Watermelon', price: 5.0, stock: 10 },
{ name: 'Grapes', price: 2.7, stock: 30 }
];
// 打印表头
console.log('Product'.padEnd(15) + 'Price'.padEnd(10) + 'Stock'.padEnd(10));
// 打印表格内容
products.forEach((product) => {
console.log(
product.name.padEnd(15) +
product.price.toString().padEnd(10) +
product.stock.toString().padEnd(10)
);
});
Object.getOwnPropertyDescriptors
Object.getOwnPropertyDescriptors( ) 方法返回一个对象的所有自身属性的描述符。
const obj = {
name: '张三',
age: 18,
// 访问器属性
get getAge() {
return this.age;
}
};
const descriptions = Object.getOwnPropertyDescriptors(obj);
console.log(descriptions);
/*
输出:
{
name: { value: '张三', writable: true, enumerable: true, configurable: true },
age: { value: 18, writable: true, enumerable: true, configurable: true },
getAge: {
get: [Function: get getAge],
set: undefined,
enumerable: true,
configurable: true
}
}
*/
应用场景:在进行对象的浅拷贝或深拷贝时,保留属性的可配置性、可枚举性和可写性信息。
Trailing Commas
允许在函数参数列表和调用中的最后一个参数后面添加逗号。虽然这只是一个小的语法改进,但它提高了代码的一致性和可读性,特别是在使用多行参数时,未来更改时不容易出错。
function foo(param1, param2,) {
// 这里的最后一个参数后允许有逗号
}
foo('a', 'b', );