ES6规范详解 & ESNext 规范及编译工具简介
本文主题: 深入探讨了 ECMAScript、Babel 编译器及其编译流程,并详细介绍了 ES6 的主要新特性及其在 JavaScript 编程中的实际应用。
摘要: 本文探讨了 ECMAScript 与 JavaScript 的关系,并详细介绍了 Babel 的作用及其编译流程。文章还解释了垫片(Polyfill)的概念,并深入介绍了 ES6 的主要新特性,包括 const 和 let、解构赋值、箭头函数、class 语法、扩展运算符 ...,以及生成器函数的使用。最后,讨论了类数组对象如何实现迭代功能,并通过代码实例展示了各种特性的应用。
一、什么是 ECMASCript?
ECMAScript 是一种标准化的脚本语言,由欧洲计算机制造商协会(ECMA)制定,目的是为不同厂商的脚本语言提供一个统一的标准。JavaScript 是 ECMAScript 的一种实现,是由 Netscape 公司开发的。
简而言之,ECMAScript 是语言规范,而 JavaScript 是基于这个规范的具体实现。JavaScript 通过遵循 ECMAScript 标准来确保其跨浏览器的兼容性和一致性。
推荐学习书籍: 阮一峰 《 ES6 标准入门》 es6.ruanyifeng.com/#docs/gener…
二、Babel 是什么? / ˈbeɪbl /
Babel 是一个广泛使用的 JavaScript 编译器,主要用于将现代 JavaScript 代码(包括最新的 ECMAScript 标准和提案)转换为兼容旧版浏览器和运行环境的代码。这样,开发者可以使用最新的语言特性和语法,而无需担心其代码在不支持这些特性的浏览器或环境中无法运行。Babel 还支持转换 JSX 语法和其他实验性语法扩展,使其在前端开发中非常流行,特别是在使用框架如 React 时。
官网文档: babeljs.io/docs/
Babel 安装与配置命令
npm i @babel/core @babel/cli @babel/preset-env
presets: 预设,定义了一组 Babel 插件的集合。
@babel/preset-env: 一个智能预设,可以根据目标环境自动确定需要转换的 JavaScript 特性。
targets: 指定需要支持的浏览器版本。
- edge: 版本 17
- firefox: 版本 60
- chrome: 版本 67
- safari: 版本 11.1
useBuiltIns: 设置为 "usage" 表示根据代码中实际使用的特性引入所需的 polyfill。
corejs: 指定要使用的 core-js 版本,这里是 3.6.5。
这个配置的作用是将现代 JavaScript 代码编译成兼容 Edge 17、Firefox 60、Chrome 67 和 Safari 11.1 版本的代码,并根据实际使用的特性自动引入相应的 polyfill。
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1"
},
"useBuiltIns": "usage",
"corejs": "3.6.5"
}
]
]
}
Babel 编译流程
解析(Parsing) :
- 词法分析(Lexical Analysis) :首先,Babel 将输入的 JavaScript 代码拆解成一个个的标记(Tokens)。
- 语法分析(Syntactic Analysis) :然后,Babel 将这些标记转换为抽象语法树(AST),这是一种代码的树状表示,描述了代码的结构和内容。
转换(Transformation) :
- Babel 使用各种插件对 AST 进行转换。这些插件可以添加、移除或修改 AST 中的节点,从而改变代码的结构。转换过程是 Babel 强大的核心,允许开发者利用最新的 JavaScript 特性,并将其转换为兼容旧环境的代码。
生成(Generation) :
- 最后,Babel 将经过转换的 AST 转换回代码字符串。这一步生成了与输入代码功能等效但经过编译的 JavaScript 代码。
Babel 脚本命令
"build":"babel src --out-dir dist"
将 src 目录下编译到 dist
扩展: 垫片
垫片(polyfill)是指在旧环境或不支持某些现代特性的平台上,实现这些现代特性的一段代码。它的作用是为那些尚未实现新特性或标准的浏览器或运行环境提供功能支持,确保代码在各种环境中都有一致的行为。
关键点:
- 功能补充:垫片用于补充浏览器或环境中缺失的功能。例如,某些旧版浏览器不支持
Promise
对象,可以通过引入Promise
的垫片来实现这一功能。 - 向后兼容:使用垫片可以让开发者使用最新的语言特性,而不必担心这些特性在旧环境中无法运行,从而提高代码的向后兼容性。
- 库和工具支持:许多 JavaScript 库和工具(如 Babel)都支持垫片,自动引入所需的垫片以确保代码的兼容性。
案例:
假设某个旧浏览器不支持 Array.prototype.includes
方法,可以使用以下垫片代码:
if (!Array.prototype.includes) {
Array.prototype.includes = function(searchElement, fromIndex) {
if (this == null) {
throw new TypeError('"this" is null or not defined');
}
var o = Object(this);
var len = o.length >>> 0;
if (len === 0) {
return false;
}
var n = fromIndex | 0;
var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
while (k < len) {
if (o[k] === searchElement) {
return true;
}
k++;
}
return false;
};
}
这段代码会检查
Array.prototype.includes
是否存在,如果不存在,就定义一个兼容的方法。这样,即使在不支持includes
方法的旧浏览器中,这个方法也可以正常使用。
三、ES6 新特性
3.1 const、let
-
const 和 let 块级作用域
-
var 存在变量提升 - 而 const 和 let 暂时性死区
-
var 可以重复声明, const / let 不可以。
-
const 是声明常量。简单类型修改会报错。
-
引用类型的属性可以修改。
- 如何使const声明的引用类型属性不可修改? Object.freeze root 级冻结。
-
-
编译器如何用 var 实现 let 的? 改名新建
- var d = 10; { var _d = 10} 创建新变量 重命名。
3.2 解构赋值
面试官: 说说如何交换 a 和 b 的值?
let a = 0, b = 1;
[a,b] = [b,a];
console.log(a,b); // 1,0
数组的解构
const [fib1,fib2,fib3] = [1,2,3,4,5]
对象的解构
const {key, value} = {key: 10, value: 20}
对象的解构 - 别名
const {key: label, value} = {key: 10, value: 20}
对象的解构 - 嵌套
const {key,value,o1:{key:key2,value:value2}} = {key:1,value:1,o1:{key:2,value:2}}
3.3 箭头函数
const foo = ( a, b) => {
return a + b
};
const foo = (a,b) => a + b; // 直接 return
const foo = () => ({a,b})
面试官: 说说箭头函数与Function函数的区别?
箭头函数(Arrow Functions)与传统的 Function
函数在 JavaScript 中有许多重要的区别,以下是一些主要的区别:
- this 绑定: 箭头函数没有自己的
this
绑定,this
值是继承自外层作用域(即词法作用域)。这在处理回调函数时非常有用,可以避免使用self = this
或bind
。传统函数有自己的this
绑定,通常在函数被调用时动态决定this
的值。可能需要显式绑定this
。 - arguments 对象: 箭头函数没有
arguments
对象,如果需要访问参数,可以使用 REST 参数语法...args
。传统函数:有自己的arguments
对象,包含传递给函数的所有参数。 - new 关键字: 箭头函数不能用作构造函数,不能使用
new
关键字调用。传统函数可以用作构造函数,可以使用new
关键字调用。 - 原型
prototype
: 箭头函数没有prototype
属性。传统函数:有prototype
属性。 - 函数体: 箭头函数如果函数体只有一个表达式,可以省略
{}
和return
关键字。传统函数:需要显式使用{}
和return
关键字。 - 名称: 箭头函数:通常是匿名的,但可以通过赋值给变量或对象属性来使用。传统函数:可以是具名函数,具名函数在函数体内可以通过名称递归调用自己。
3.4 Class
在 ES6 中,引入了 class
语法,这是 JavaScript 的一种新的面向对象编程方式。虽然 class
语法看起来类似于其他面向对象编程语言中的类定义,但在 JavaScript 中,它本质上是一种语法糖,用于简化通过原型(prototype)和构造函数(constructor)进行对象创建和继承的过程。
class Person {
constructor(name, age) {
this.name = name || '匿名'
this.age = age || 18
}
say() {
console.log(`我叫${this.name},今年${this.age}岁`)
}
static do(){}
}
class Student extends Person {
constructor(name, age) {
super(name, age)
}
}
class Teacher extends Person {
constructor(name, age) {
super(name, age)
}
}
3.5 新数据结构 - Map | Set
Map 映射关系 Record: 键值对
const map = new Map();
map.set("1", 1)
map.set("2", 2)
map.get("1")
Set 集合: 不允许有元素重复
const set = new Set()
set.add(1)
set.add(2)
面试官: 如何给数组快速去重?
let arr =[1,2,3,1,2,3]
[...new Set(arr)] // 1,2,3
面试官: "..." 的本质是什么?
在 ES6 中,...
是一个重要的运算符,称为扩展运算符(spread operator)或剩余运算符(rest operator),根据使用场景的不同有不同的功能和用途。其本质是用于在函数调用、数组构造和对象解构中处理不定数量的参数或元素。
3.6 Generator 函数
Generator 函数是一种特殊的函数类型,可以通过 yield
关键字暂停和恢复函数的执行,具有迭代器的特性,适用于处理需要逐步执行或延迟计算的情况。通过 yield
关键字,Generator 函数可以返回多个值,并且在每次调用 next()
方法时继续执行函数体,直到遇到 return
语句或函数执行完毕。
普通函数如何实现斐波那契数列计算?
斐波那契数列是一个从 0 和 1 开始的数列,其中每个数都是前两个数之和,即 ( F(n) = F(n-1) + F(n-2) )。这个数列不仅在数学和计算机科学中具有重要应用,如递归算法和动态规划,还在自然界中广泛存在,如植物的叶序和松果的排列,与黄金比例密切相关。
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
// 动态规划
function fibonacciDP(n) {
if (n <= 1) {
return n;
}
const fib = [0, 1];
for (let i = 2; i <= n; i++) {
fib[i] = fib[i - 1] + fib[i - 2];
}
return fib[n];
}
console.log(fibonacciDP(10)); // 输出 55
generator 函数实现斐波那契数列计算1
function* genFib(){
console.log("gen 开始执行")
let a = 1
let b = 1
while (true){
console.log("即将出让执行权 yield")
yield a // 相当于一个断点 {value:any, done: boolean}
console.log("gen yield 返回")
const t = b
b += a
a = t
}
}
const gen = genFib() // 返回的只是函数的迭代器, 不执行。
let i =0
while(i<10){
console.log(gen.next())
i++
}
console.log(gen.next())
Generator 函数生成斐波那契数列2
function* fibonacci() {
let prev = 0;
let curr = 1;
while (true) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
const fibGen = fibonacci();
console.log(fibGen.next().value); // 输出 1
console.log(fibGen.next().value); // 输出 1
console.log(fibGen.next().value); // 输出 2
console.log(fibGen.next().value); // 输出 3
console.log(fibGen.next().value); // 输出 5
// 依此类推...
问: 如何重置?
const gen2 = genFib()
创建新的迭代器
问: generator 函数使用完毕后需要销毁么? 是否会造成内存泄露?
Generator 函数在使用完毕后不需要显式销毁,因为 JavaScript 引擎会自动进行垃圾回收,回收不再使用的内存资源。Generator 函数本身不会造成内存泄漏,因为它们在执行过程中不会创建额外的内存持久化对象。
Generator 函数的特性决定了它们在执行过程中会保存内部状态,包括当前执行的位置和局部变量的值。当 Generator 函数执行完成或不再被引用时,JavaScript 引擎会自动回收这些状态所占用的内存。因此,即使不显式销毁 Generator 函数,也不会造成内存泄漏。
然而,需要注意的是,如果 Generator 函数返回的迭代器对象被长期持有,并且在迭代过程中不断调用
next()
方法,可能会导致迭代器对象及其闭包中的变量无法被垃圾回收,从而造成内存泄漏。因此,在使用 Generator 函数时,需要注意及时释放对迭代器对象的引用,以确保不会产生内存泄漏问题。
普通函数与 Generator 函数的区别
Generator 函数和普通函数之间有几个重要的区别:
-
返回值:
- 普通函数:通过
return
关键字返回一个值,并且只能返回一次。 - Generator 函数:使用
yield
关键字可以产生多个值,每次调用next()
方法都会返回一个值,并在下一次调用next()
时从上一次yield
的位置继续执行。
- 普通函数:通过
-
暂停和恢复:
- 普通函数:一旦开始执行,会一直执行到结束,并且无法在执行过程中暂停。
- Generator 函数:可以在执行过程中通过
yield
暂停函数的执行,并且可以通过next()
方法恢复执行。
-
迭代器:
- 普通函数:不是迭代器,无法通过
for...of
循环进行迭代。 - Generator 函数:是迭代器,可以使用
for...of
循环进行迭代。
- 普通函数:不是迭代器,无法通过
-
状态保存:
- 普通函数:没有状态,每次调用都会重新执行函数体。
- Generator 函数:具有内部状态,可以在多次调用之间保存状态。
-
执行方式:
- 普通函数:一次性执行完毕,无法暂停和恢复。
- Generator 函数:可以通过
next()
方法逐步执行,可以随时暂停和恢复执行。
-
生成器对象:
- 普通函数:每次调用都会创建一个新的执行上下文。
- Generator 函数:每次调用都会返回一个生成器对象,该对象具有
next()
方法用于控制执行。
Generator 函数是一种特殊的函数类型,可以通过 yield
关键字暂停和恢复函数的执行,并且具有迭代器的特性,可以在多次调用之间保存内部状态。这使得它在处理需要逐步执行或延迟计算的情况下非常有用。
3.7 for-of 循环
理解方法 for-in => in 关键词 => 可以遍历对象 => 对象迭代方法
for-of ... 的 => 值 => 具有迭代器的对象 => next
for-of
是 JavaScript 中用于遍历可迭代对象(如数组、字符串、Map、Set 等)的循环语句。
for-of
循环和 async/await
来实现类似同步输出的效果
async function processData(data) {
for (const item of data) {
const result = await fetch(item.url); // 异步请求
console.log(result);
}
}
const data = [{ url: 'url1' }, { url: 'url2' }, { url: 'url3' }];
processData(data);
const api1 = fetch('http://baidu.com')
const api2 = fetch('http://sougou.com')
const api3 = fetch('http://google.com')
const apis = [api3, api1, api2]
// 将异步方法同步输出。
for await (const x of apis) {
console.log(x)
}
如何衡量一个 Object 可以迭代? 判断是否有 Symbol.iterator。
面试官: 如何让一个 Object 可以用 for-of 迭代?
const obj = {
key1: 'val1',
key2: 'val2',
[Symbol.iterator]: () => {
let i = 1
return {
next: function () {
let temp = { value: obj[`key${i}`], done: i <= Object.keys(obj).length ? false : true }
i++
return temp
}
}
}
}
for (let v of obj) {
console.log(v) // val1, val2
}
面试官: 类数组对象如何通过迭代器方法实现迭代功能?
const arrayLike = {
0: 'a',
1: 'b',
2: 'c',
length: 3
};
// 添加 [Symbol.iterator] 方法
arrayLike[Symbol.iterator] = function() {
let index = 0;
let length = this.length;
return {
next: () => {
if (index < length) {
return { value: this[index++], done: false };
} else {
return { value: undefined, done: true };
}
}
};
};
// 使用 for-of 循环迭代
for (const value of arrayLike) {
console.log(value); // 输出: a, b, c
}
总结
通过对 ECMAScript、Babel 和 ES6 新特性的探讨,我们进一步加深了对现代 JavaScript 发展和应用的理解。本文旨在为大家提供一些实用的知识和工具,帮助在实际开发中更好地应用这些语言特性。当然,前端技术日新月异,持续学习和探索仍然是每位开发者需要坚持的方向。希望这篇文章对你有所帮助,让我们一起在前端开发的道路上不断进步,共同迎接未来的挑战与机遇。
转载自:https://juejin.cn/post/7378046072953290786