简单来说,两者的区别:ECMAScript 是语言规范,JavaScript 是编程语言。
ECMAScript 的新特性:不同的时间有不同的新特性加入,比如ES5、ES6/ES2015、ES7/ES2016、ES2018等等,常被大家熟知的ES6是一个重要的更新,所以这个比较重要。
目录
一、ECMAScript 与JavaScript 的区别
我之前在学习ES6时,看过一个网站,这个网站里的学习内容很丰富,从JS的制造者开始讲,将ECMA的起源讲清楚,这里也很直白地解释两者的关系,分享链接如下:
阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版
两者的区别,总计一句话就是我开头说的那样。
稍微详细来说:
ECMAScript:
ECMAScript 是由 Ecma International 组织标准化的脚本语言规范。它定义了脚本语言的语法、类型、对象模型等核心特性。
ECMAScript 是 JavaScript 的标准化基础,也就是说 JavaScript 是 ECMAScript 的一种实现。每当 ECMAScript 标准更新时,JavaScript 就会添加这些新的特性。
ECMAScript 的版本——ES6 是一个重要的更新,带来了很多新的特性,如箭头函数、模板字符串、let 和 const 关键字、类和模块等。
JavaScript:
- JavaScript 是一种编程语言,最初由 Netscape 公司在 1995 年开发,用于在浏览器中添加动态内容。
- JavaScript 是 ECMAScript 标准的最广泛的实现之一。除了遵循 ECMAScript 标准,JavaScript 还添加了一些浏览器相关的特性和 API,比如 DOM 操作、事件处理等。
- JavaScript 是在 Web 开发中使用最广泛的编程语言,几乎所有现代浏览器都支持它。
总结:ECMAScript 是一套语言规范,而 JavaScript 是 ECMAScript 规范的实现。ECMAScript 定义了这门语言的基础,而 JavaScript 则在此基础上扩展并添加了一些浏览器特性。
二、ECMAScript的新特性
这里只对特性进行简单总结,也算是我自己学习的查漏补缺。
1.引入新变量——let、const
总所周知,ES6之前只有var,所以引入了let const。为什么引入?从使用var来定义变量存在的问题的角度来讲——块级作用域、不可重复声明、暂时性死区:
1.变量提升(Hoisting):被提升到函数或全局作用域 的顶部,但不会提升赋值。也就是说,变量可以在声 明之前被访问,但值为 undefined。
2.作用域:var 定义的变量是函数作用域或全局作用 域,不是块作用域。也就是说,即使在块级作用域内 (例如,if 或 for 语句内)声明的变量,依然可以在块外访问,引入了let const能避免不必要的变量污染和冲突(前者是块级作用域的局部变量,后者是块级作用域的只读常量)。
3.重复声明:使用 var 可以重复声明同一个变量,不会报错,这可能导致意外的错误和难以调试的问题。
相比之下,let 和 const 引入了块级作用域和更严格的 变量管理,避免了上述问题:块级作用域、不可重复声明、let 和 const 在声明之前不可访问,避免了变量提升带来的问题——暂时性死区(Temporal Dead Zone),也更安全、简洁和规范。
let const 实现原理:
let(const差不多同理)
的底层实现涉及 JavaScript 引擎在编译和执行代码时如何处理变量声明和作用域。在 JavaScript 引擎中,每一个变量都会被封装在一个称为“变量对象”的容器中,这个对象包含了所有当前上下文中定义的变量与函数。虽然具体的实现细节因引擎而异,但基本的过程可以概括为以下几个步骤:1. 编译阶段
在 JavaScript 代码执行之前,JavaScript 引擎会先进行编译。这个阶段主要包括:
- 创建作用域:引擎会为每个作用域(全局作用域、函数作用域、块级作用域)创建一个“环境记录”(Environment Record),用来存储变量和函数声明。
- 识别变量声明:在这一步,
let
声明的变量会被添加到其所在的块级作用域中,但不会被初始化。这与var
不同,var
声明的变量在编译阶段就被提升并初始化为undefined
。- 暂时性死区(TDZ):在作用域内,从变量声明开始到变量被初始化的这段时间,被称为“暂时性死区”(Temporal Dead Zone)。在 TDZ 内,访问
let
声明的变量会导致运行时错误(ReferenceError)。2. 执行阶段
在执行阶段,JavaScript 引擎会逐行执行代码:
- 初始化变量:当执行到
let
声明的那一行时,引擎会为该变量分配内存,并根据赋值操作进行初始化。此时,变量离开了暂时性死区,可以正常访问了。- 块级作用域:
let
声明的变量在其所在的块级作用域内有效,不会影响到块外。每次进入新的块作用域时,都会创建新的环境记录,let
声明的变量存储在这个新的环境中。
此外,let
与var
的区别
var
在编译阶段会被提升并初始化为undefined
,而let
只会被提升但不会初始化。let
声明的变量在 TDZ 内无法被访问,而var
声明的变量即使在声明前访问,值也只是undefined
。let
遵循块级作用域,而var
仅遵循函数作用域。
2.面向对象编程——class
这里也推荐去阅读这个文档,更清晰:
Class 的基本语法
简单来说,JavaScript 的类最终也是一种函数, class 语法只是语法糖——简化了原型继承 的写法。类的构造函数 (constructor)用于初始化对象的 属性,类的方法定义在构造函数的 prototype 上,并且不可枚举。尽管语法上更为简洁,但本质上仍然 基于原型继承机制。并且使用 class 关键字创建的类会被编译成一个函数,因此其底层原理与函数有一些相似之处。
关键字:类、静态方法、属性、继承、可枚举——这些听起来是不是很像JAVA?这也是class引入原因之一
此处,插播一条:for of 和 for in 的区别——主要在,可枚举属性
for...of 和 for...in 是 JavaScript 中的两种循环语法,用于遍历对象或集合。它们有不同的用途和行为。
for...in 用途:for...in 用于遍历对象的可枚举属性。它不仅可以遍 历对象的自有属性,还可以遍历继承自原型链的属性。 行为:遍历对象的键(属性名)。 不会遍历 Symbol 属性。 遍历顺序不一定按插入顺序,特别是在不同的 JavaScript 引擎中,可能会有所不同。 不适用于 Array、Map、Set 等集合类型,它主要用于对 象。
for...of 用途:for...of 用于遍历可迭代对象(iterable objects), 如 Array、String、Map、Set 等。它不会遍历对象的属 性。 行为:遍历可迭代对象的值。 只能用于可迭代对象。普通对象(如字面量对象)不能使 用 for...of。 遍历顺序按对象的实际顺序。
3.模板字符串
ES6 引入的一种字符串表示方式,提供了多行字符串、内插表达式和标签模板等功能。
底层原理:
- 在编译阶段,JavaScript 引擎会识别模板字符串中的插值表达式
${}
并将其拆分为静态字符串和插值部分。- 在执行阶段,插值表达式内的代码会被求值,求值结果将被转换为字符串,并插入到模板字符串中相应的位置。
上面是普通的模板字符串的原理,还有一种高级的模板字符串——标签模板
允许你在模板字符串之前加上一个函数,用来处理模板字符串和插值表达式:
function tag(strings, ...values) {
console.log(strings); // ["Hello, ", "!"]
console.log(values); // ["Alice"]
}const name = "Alice";
tag`Hello, ${name}!`;
底层原理:
- 在编译阶段,模板字符串会被分解为一个字符串数组(包含静态部分)和一个值数组(包含插值表达式的求值结果)。
- 标签函数
tag
在执行阶段被调用,接收这些数组作为参数,开发者可以在函数中自定义处理逻辑。
4.解构语法
JavaScript 的解构是 ES6 中新增的一个语法特性,它可以将数组或对象中的元素提取出来并赋值给变量。解构语法使得对数组和对象的操作更加灵活和便捷。
关键词:数组解构、对象解构、嵌套解构、混合解构、函数参数解构——如果以上都能理解,就看下面的原理吧。
底层原理:
解构赋值的底层原理是基于对象和数组的迭代与属性访问:
- 对于数组解构,JavaScript 引擎通过数组索引依次将值赋给变量。
- 对于对象解构,引擎根据属性名称匹配对象中的属性,然后将对应的值赋给变量。
如果在解构过程中找不到对应的值,且没有提供默认值,则变量被赋值为
undefined
。
前提是,如果右边(要解构的对象)是一个具有 Iterator 接口的对象,如果为无法解构的值(如 undefined 或 null),则抛出 TypeError。
5.箭头函数
JavaScript中的箭头函数新增的一种函数定义方式,它可以创建一个函数并赋值给变量,使用箭头语法'=>'。在箭头函数中,this 关键字的作用域与它周围代码作用域相同,因而有时也被称为“词法作用域函数”。
箭头函数的原理是基于JavaScript中的闭包、this和参数作用域。在箭头函数中,this关键字始终指向函数所在上下文的this指针,而不是所在作用域的this指针——这个不好理解的话,建议去看一个专题,仔细研究一下实例就能理解了。
虽然箭头函数在许多应用场景中表现出极大的优势,但在某些情况下还是不能正常使用。常见的限制场景如下:
- 不能作为构造函数——不能使用new关键字来调用箭头函数来创建一个新对象。
在箭头函数中,函数的参数为指定的参数,没有额外的 arguments 对象。如果需要使用 arguments 参数,必须使用常规的函数语法。
对于箭头函数,它的 this 指针指向词法作用域中的this值,无法通过call()、apply()方法来修改。
6.生成器 generator
生成器(Generator)是 ES6 引入的一种可以在执行过程中暂停和恢复的函数。它们使用特殊的语法来定义,并提供了一个可以按需生成值的迭代器。这使得生成器特别适用于处理惰性计算、异步编程和复杂迭代等场景。
生成器与 yield
配合可以简化异步代码的书写,形成类似同步的代码风格(如与 Promise
或 async/await
结合)。著名案例:co Promise + generator 实现异步编程。
7.异步处理
都知道callback是一种异步编程模式,通过回调函数的方式实现,但是有回调地狱。于是 ES6提供的一种处理异步操作的机制——Promise ,用于解决 callback 回调函数嵌套过多的问题,但是并没有很好地解决,因为有then,catch,写多了还是不美观简洁,当然它功能比callback,更加强大和易用。
于是 ES8 的新特性——async/await出现了 ,它是基于 Promise 的一种异步编程方式,它可以使异步代码看起来像同步代码(看起来像是链式调用),语法简单易懂,可读性较高。async 是用于定义一个异步函数,await 用于等待一个异步操作完成。async 函数返回一个 Promise 对象,await 关键字只能在 async 函数中使用。
8.Reflect
是 ES6 引入的一个全局对象,它提供了用于操作 JavaScript 对象的通用方法。它包含了一组与对象操作相关的静态方法,这些方法与 Object
对象中的方法类似,但 Reflect
中的方法设计得更一致,并且通常直接反映底层的 JavaScript 引擎操作,并且好像有看过,说是Object
对象中的方法,Reflect
都有,甚至还要慢慢替换它。
Reflect API 实现了对象的反射、代理等功能,它为我们提供了一些强大而便捷的工具,使得我们可以在运行时动态地查看、检查和修改对象的属性和行为。Reflect 反射在 JavaScript 中的应用非常广泛,可以用于类似响应式编程、面向对象编程等各种场景。
著名案例,vue3的响应式原理:
基于Proxy
和Reflect
实现的,Reflect
在 Vue 3 的响应式系统中起到了关键作用,主要用于拦截和执行对象属性的操作,可以拦截到数组索引的修改、长度变化等操作。这与 Vue 2 使用的Object.defineProperty
方法有很大的不同,它只能拦截对象的属性修改,对于数组的索引操作无法直接侦听,只能通过特定的方法(如push
、splice
)来触发更新
9.BigInt
JavaScript Number 类型的限制——2^53 - 1,在某些场景下,数不够用,所以在 ES10 中引入的一种新类型,它可以用来表示任意大的整数,不受 JavaScript 中 Number 类型的 2^53 - 1 限制。
算上这个,现在JS的变量类型,一共可以分为以下几种:
- 原始类型(7种):
number
、string
、boolean
、undefined
、null
、symbol
、bigint
- 对象类型(若干):
object
、array
、function
、date
、regexp
、map
、set
、weakmap
、weakset
、proxy
、reflect
对了还有,在 ES2021中引入了一种新特性,允许在数字字面量中使用下划线 (_
) 作为分隔符,以提高数字的可读性。这并不是 ES6 中的特性,而是更晚一些版本的 ECMAScript 中加入的。
总结
这里主要对ES6以来的一些新特性进行了总结和记录,查漏补缺吧。