ECMAScript 即是大家熟悉的 JavaScript,简称 js,作为一个前端,如果说不熟悉 ES6 就算不上一个真正的前端。本文详细而简单地介绍了 JavaScript 的发展历程并介绍了 ECMAScript 中的里程碑版本 ES5、ES6、ES7 的常用知识
1 ES6 的发展背景
JavaScript 是大家所了解的语言名称,但是这个语言名称是商标( Oracle 公司注册的商标)。因此,JavaScript 的正式名称是 ECMAScript 。1996 年 11 月,JavaScript 的创造者网景公司将 JS 提交给国际化标准组织 ECMA(European computer manufactures association,欧洲计算机制造联合会),希望这种语言能够成为国际标准,随后 ECMA 发布了规定浏览器脚本语言的标准,即 ECMAScript。这也有利于这门语言的开放和中立。
1.1 ECMAScript 的历史
- 1997 年 ECMAScript 1.0 诞生。
- 1998 年 6 月 ECMAScript 2.0 诞生,包含一些小的更改,用于同步独立的 ISO 国际标准。
- 1999 年 12 月 ECMAScript 3.0 诞生,它是一个巨大的成功,在业界得到了广泛的支持,它奠定了 JS 的基本语法,被其后版本完全继承。直到今天,我们一开始学习 JS ,其实就是在学 3.0 版的语法。
- 2000 年的 ECMAScript 4.0 是当下 ES6 的前身,但由于这个版本太过激烈,对 ES 3 做了彻底升级,所以暂时被"和谐"了。
- 2009 年 12 月,ECMAScript 5.0 版正式发布。ECMA 专家组预计 ECMAScript 的第五个版本会在 2013 年中期到 2018 年作为主流的开发标准。2011 年 6 月,ES 5.1 版发布,并且成为 ISO 国际标准。
- 2013 年,ES6 草案冻结,不再添加新的功能,新的功能将被放到 ES7 中;2015 年 6 月, ES6 正式通过,成为国际标准。
1.2 ECMAScript 的发布周期
在 2015 年发布的 ECMAScript(ES6)新增内容很多,在 ES5 发布近 6 年(2009-11 至 2015-6)之后才将其标准化。两个发布版本之间时间跨度如此之大主要有两大原因:
- 比新版率先完成的特性,必须等待新版的完成才能发布。
- 那些需要花长时间完成的特性,也顶着很大的压力被纳入这一版本,因为如果推迟到下一版本发布意味着又要等很久,这种特性也会推迟新的发布版本。
因此,从 ECMAScript 2016(ES7)开始,版本发布变得更加频繁,每年发布一个新版本,这么一来新增内容也会更小。新版本将会包含每年截止时间之前完成的所有特性。
1.3 ECMAScript 的发布流程
每个 ECMAScript 特性的建议将会从阶段 0 开始, 然后经过下列几个成熟阶段。其中从一个阶段到下一个阶段必须经过 TC39 的批准。
- stage-0 - Strawman: just an idea, possible Babel plugin.
任何讨论、想法、改变或者还没加到提案的特性都在这个阶段。只有 TC39 成员可以提交。
- stage-1 - Proposal: this is worth working on.
什么是 Proposal?一份新特性的正式建议文档。提案必须指明此建议的潜在问题,例如与其他特性之间的关联,实现难点等。
- stage-2 - Draft: initial spec.
什么是 Draft?草案是规范的第一个版本。其与最终标准中包含的特性不会有太大差别。
草案之后,原则上只接受增量修改。这个阶段开始实验如何实现,实现形式包括 polyfill, 实现引擎(提供草案执行本地支持),或者编译转换(例如 babel)
- stage-3 - Candidate: complete spec and initial browser implementations.
候选阶段,获得具体实现和用户的反馈。此后,只有在实现和使用过程中出现了重大问题才会修改。至少要在一个浏览器中实现,提供 polyfill 或者 babel 插件。
- stage-4 - Finished: will be added to the next yearly release.
已经准备就绪,该特性会出现在下个版本的 ECMAScript 规范之中。
1.4 几个重要版本
- es5: 09 年发布
- es6(ES2015): 2015 年发布,也称之为 ECMA2015
- es7(ES2016): 2016 年发布,变化不大
- es8(es2017): 2017 发布
ES7 在 ES6 的基础上主要添加了两项内容:
Array.prototype.includes()方法
求幂运算符(**)
在 2017 年 1 月的 TC39 会议上,ECMAScript 2017 的最后一个功能“Shared memory and atomics”推进到第 4 阶段。这意味着它的功能集现已完成。
主要新功能:
异步函数 Async Functions(Brian Terlson)
共享内存和 Atomics(Lars T. Hansen)
次要新功能:
Object.values / Object.entries(Jordan Harband)
String padding(Jordan Harband,Rick Waldron)
Object.getOwnPropertyDescriptors() (Jordan Harband,Andrea Giammarchi)
函数参数列表和调用中的尾逗号(Jeff Morrison)
2 ES5 基础知识
2.1 严格模式
- 运行模式: 正常(混杂)模式与严格模式
- 应用上严格式: ‘strict mode’;
- 作用:
- 使得 Javascript 在更严格的条件下运行
- 消除 Javascript 语法的一些不合理、不严谨之处,减少一些怪异行为
- 消除代码运行的一些不安全之处,保证代码运行的安全
- 需要记住的几个变化
- 声明定义变量必须用 var
- 禁止自定义的函数中的 this 关键字指向全局对象
- 创建 eval 作用域, 更安全
"use strict" 指令只允许出现在脚本或函数的开头
严格模式的限制
- 不允许使用未声明的变量:
; |
- 对象也是一个变量
; |
3 不允许删除变量或对象
; |
4 不允许删除函数
; |
5 不允许变量重名
; |
6 不允许使用八进制
; |
7 不允许使用转义字符
; |
8 不允许对只读属性赋值
; |
9 不允许对一个使用 getter 方法读取的属性进行赋值
; |
10 由于一些安全原因,在作用域 eval() 创建的变量不能被调用
; |
11 禁止 this 关键字指向全局对象
function f(){ |
12 使用构造函数时,如果忘了加 new,this 不再指向全局对象
function f(){ |
2.2 JSON 对象
- 作用: 用于在 json 对象/数组与 js 对象/数组相互转换
- JSON.stringify(obj/arr)
js 对象(数组)转换为 json 对象(数组)
JSON.stringify(value[, replacer[, space]]) |
参数说明:
value:
必需, 要转换的 JavaScript 值(通常为对象或数组)。
replacer:
可选。用于转换结果的函数或数组。
如果 replacer 为函数,则 JSON.stringify 将调用该函数,并传入每个成员的键和值。使用返回值而不是原始值。如果此函数返回 undefined,则排除成员。根对象的键是一个空字符串:""。
如果 replacer 是一个数组,则仅转换该数组中具有键值的成员。成员的转换顺序与键在数组中的顺序一样。当 value 参数也为数组时,将忽略 replacer 数组。
space:
可选,文本添加缩进、空格和换行符,如果 space 是一个数字,则返回值文本在每个级别缩进指定数目的空格,如果 space 大于 10,则文本缩进 10 个空格。space 也可以使用非数字,如:\t。
var obj = { "name":"runoob", "alexa":10000, "site":"www.runoob.com"}; |
- JSON.parse(json)
json 对象(数组)转换为 js 对象(数组)
JSON.parse(text[, reviver]) |
参数说明:
text:必需, 一个有效的 JSON 字符串。
reviver: 可选,一个转换结果的函数, 将为对象的每个成员调用此函数。
2.3 Object 扩展
Object 构造函数为给定值创建一个对象包装器。如果给定值是 null 或 undefined,将会创建并返回一个空对象,否则,将返回一个与给定值对应类型的对象。
当以非构造函数形式被调用时,Object 等同于 new Object()。
- Object.create(prototype[, descriptors]) : 创建一个新的对象
- 以指定对象为原型创建新的对象
- 指定新的属性, 并对属性进行描述
- value : 指定值
- writable : 标识当前属性值是否是可修改的, 默认为 true
- get 方法 : 用来得到当前属性值的回调函数
- set 方法 : 用来监视当前属性值变化的回调函数
- Object.defineProperties(object, descriptors) : 为指定对象定义扩展多个属性
var obj = {name : 'curry', age : 29} |
- 对象本身的两个方法
<!-- |
一些常用的方法
-
Object.assign()
通过复制一个或多个对象来创建一个新的对象。
-
Object.getOwnPropertyNames()
返回一个数组,它包含了指定对象所有的可枚举或不可枚举的属性名。
比较两个值是否相同。所有 NaN 值都相等(这与==和===不同)。
-
Object.keys()
返回一个包含所有给定对象自身可枚举属性名称的数组。
-
Object.values()
返回给定对象自身可枚举值的数组。
其他更多方法参见 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object
2.4 Array 扩展
- Array.prototype.indexOf(value) : 得到值在数组中的第一个下标
- Array.prototype.lastIndexOf(value) : 得到值在数组中的最后一个下标
- Array.prototype.forEach(function(item, index){}) : 遍历数组
- Array.prototype.map(function(item, index){}) : 遍历数组返回一个新的数组
- Array.prototype.filter(function(item, index){}) : 遍历过滤出一个子数组
<script type="text/javascript"> |
2.5 Function 扩展
- Function.prototype.bind(obj)
- 将函数内的 this 绑定为 obj, 并将函数返回
- 面试题: 区别 bind()与 call()和 apply()?
- fn.bind(obj) : 指定函数中的 this, 并返回函数
- fn.call(obj) : 指定函数中的 this,并调用函数
<!-- |
2.6 Date 扩展
- Date.now() : 得到当前时间值
3 ES6 基础语法
3.1 let 与 const
ES2015(ES6) 新增加了两个重要的 JavaScript 关键字: let 和 const。
let 声明的变量只在 let 命令所在的代码块内有效。
const 声明一个只读的常量,一旦声明,常量的值就不能改变。
let 命令
- 基本用法:
{ |
- 代码块内有效
let 是在代码块内有效,var 是在全局范围内有效
{ |
- 不能重复声明
let 只能声明一次 var 可以声明多次:
let a = 1; |
- for 循环计数器很适合用 let
for (var i = 0; i < 10; i++) { |
变量 i 是用 var 声明的,在全局范围内有效,所以全局中只有一个变量 i, 每次循环时,setTimeout 定时器里面的 i 指的是全局变量 i ,而循环里的十个 setTimeout 是在循环结束后才执行,所以此时的 i 都是 10。
变量 j 是用 let 声明的,当前的 i 只在本轮循环中有效,每次循环的 j 其实都是一个新的变量,所以 setTimeout 定时器里面的 j 其实是不同的变量,即最后输出 12345。(若每次循环的变量 j 都是重新声明的,如何知道前一个循环的值?这是因为 JavaScript 引擎内部会记住前一个循环的值)。
数组迭代也可以
for (let item of ["zero", "one", "two"]) { |
其他更多用法参见 https://www.runoob.com/w3cnote/es6-iterator.html
- 不存在变量提升
let 不存在变量提升,var 会变量提升:
console.log(a); //ReferenceError: a is not defined |
变量 b 用 var 声明存在变量提升,所以当脚本开始运行的时候,b 已经存在了,但是还没有赋值,所以会输出 undefined。
变量 a 用 let 声明不存在变量提升,在声明变量 a 之前,a 不存在,所以会报错
- 解构赋值
let obj={'username':'yishui',age:19} |
const 命令
const 声明一个只读变量,声明之后不允许改变。意味着,一旦声明必须初始化,否则会报错。
基本用法:
const PI = "3.1415926"; |
暂时性死区:
var PI = "a"; |
ES6 明确规定,代码块内如果存在 let 或者 const,代码块会对这些命令声明的变量从块的开始就形成一个封闭作用域。代码块内,在声明变量 PI 之前使用它会报错。
注意要点
const 如何做到变量在声明初始化之后不允许改变的?其实 const 其实保证的不是变量的值不变,而是保证变量指向的内存地址所保存的数据不允许改动。此时,你可能已经想到,简单类型和复合类型保存值的方式是不同的。是的,对于简单类型(数值 number、字符串 string 、布尔值 boolean),值就保存在变量指向的那个内存地址,因此 const 声明的简单类型变量等同于常量。而复杂类型(对象 object,数组 array,函数 function),变量指向的内存地址其实是保存了一个指向实际数据的指针,所以 const 只能保证指针是固定的,至于指针指向的数据结构变不变就无法控制了,所以使用 const 声明复杂类型对象时要慎重。
3.2 对象的简写模式
<!-- |
对象的其他方法参见 https://www.runoob.com/w3cnote/es6-object.html
3.3 箭头函数
<!-- |
其他使用方法参见 https://www.runoob.com/w3cnote/es6-function.html
3.4 三点运算符
<!-- |
3.5 形参默认值
<!-- |
3.6 Promise 对象
3.6.1 理解 Promise 对象
- Promise 对象: 代表了未来某个将要发生的事件(通常是一个异步操作)
- 有了 promise 对象, 可以将异步操作以同步的流程表达出来, 避免了层层嵌套的回调函数(俗称’回调地狱’)
- ES6 的 Promise 是一个构造函数, 用来生成 promise 实例
3.6.2 使用 promise 基本步骤
* 创建promise对象 |
3.6.3 promise 对象的 3 个状态
- pending: 初始化状态
- fullfilled: 成功状态
- rejected: 失败状态
Promise 异步操作有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。除了异步操作的结果,任何其他操作都无法改变这个状态。
graph LR |
Promise 对象只有:从 pending 变为 fulfilled 和从 pending 变为 rejected 的状态改变。只要处于 fulfilled 和 rejected ,状态就不会再变了即 resolved(已定型)。
const p1 = new Promise(function(resolve,reject){ |
上述代码的执行结果为:
状态的缺点
- 无法取消 Promise ,一旦新建它就会立即执行,无法中途取消。
- 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。
- 当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
then 方法
then 方法接收两个函数作为参数,第一个参数是 Promise 执行成功时的回调,第二个参数是 Promise 执行失败时的回调,两个函数只会有一个被调用。
then 方法的特点
在 JavaScript 事件队列的当前运行完成之前,回调函数永远不会被调用。
const p = new Promise(function(resolve,reject){ |
通过 .then 形式添加的回调函数,不论什么时候,都会被调用。
通过多次调用
.then |
,可以添加多个回调函数,它们会按照插入顺序并且独立运行。 |
上述代码的执行结果为:
then 方法将返回一个 resolved 或 rejected 状态的 Promise 对象用于链式调用,且 Promise 对象的值就是这个返回值。
then 方法注意点
简便的 Promise 链式编程最好保持扁平化,不要嵌套 Promise。
注意总是返回或终止 Promise 链。
const p1 = new Promise(function(resolve,reject){ |
创建新 Promise 但忘记返回它时,对应链条被打破,导致 p4 会与 p2 和 p3 同时进行。
大多数浏览器中不能终止的 Promise 链里的 rejection,建议后面都跟上
.catch(error => console.log(error)); |
3.6.4 应用:
- 使用 promise 实现超时处理
//创建一个promise实例对象 |
代码的执行结果为
1111 |
- 使用 promise 封装处理 ajax 请求
|
示例 2
function ajax(URL) { |
上面代码中,resolve 方法和 reject 方法调用时,都带有参数。它们的参数会被传递给回调函数。reject 方法的参数通常是 Error 对象的实例,而 resolve 方法的参数除了正常的值以外,还可能是另一个 Promise 实例,比如像下面这样。
var p1 = new Promise(function(resolve, reject){ |
上面代码中,p1 和 p2 都是 Promise 的实例,但是 p2 的 resolve 方法将 p1 作为参数,这时 p1 的状态就会传递给 p2。如果调用的时候,p1 的状态是 pending,那么 p2 的回调函数就会等待 p1 的状态改变;如果 p1 的状态已经是 fulfilled 或者 rejected,那么 p2 的回调函数将会立刻执行。
更多方法参见 https://www.runoob.com/w3cnote/javascript-promise-object.html
3.7 Symbol 属性
ES5 中对象的属性名都是字符串,容易造成重名,污染环境
ES6 中的添加了一种原始数据类型 symbol(已有的原始数据类型:String, Number, boolean, null, undefined, 对象)
特点
- Symbol 属性对应的值是唯一的,解决命名冲突问题
- Symbol 值不能与其他数据进行计算,包括同字符串拼串
- for in, for of 遍历时不会遍历 symbol 属性。
Symbol 函数栈不能用 new 命令,因为 Symbol 是原始数据类型,不是对象。可以接受一个字符串作为参数,为新创建的 Symbol 提供描述,用来显示在控制台或者作为字符串的时候使用,便于区分。
let sy = Symbol("KK"); |
注意点
Symbol 值作为属性名时,该属性是公有属性不是私有属性,可以在类的外部访问。但是不会出现在 for…in 、 for…of 的循环中,也不会被 Object.keys() 、 Object.getOwnPropertyNames() 返回。如果要读取到一个对象的 Symbol 属性,可以通过 Object.getOwnPropertySymbols() 和 Reflect.ownKeys() 取到。
let syObject = {}; |
使用
1、调用 Symbol 函数得到 symbol 值
let symbol = Symbol(); |
2、传参标识
let symbol = Symbol('one'); |
3、内置 Symbol 值
- 除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。
- Symbol.iterator
对象的 Symbol.iterator 属性,指向该对象的默认遍历器方法(后边讲)
3.8 async 函数
async 函数源自 ES2017
概念: 真正意义上去解决异步回调的问题,同步流程表达异步操作
本质: Generator 的语法糖
语法:
async function name([param[, param[, ... param]]]) { |
- name: 函数名称。
- param: 要传递给函数的参数的名称。
- statements: 函数体语句。
即 async 的语法为
async function foo(){ |
返回值:
async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。
async function helloAsync(){ |
async 函数中可能会有 await 表达式,async 函数执行时,如果遇到 await 就会先暂停执行 ,等到触发的异步操作完成后,恢复 async 函数的执行并返回解析值。
await 关键字仅在 async function 中有效。如果在 async function 函数体外使用 await ,你只会得到一个语法错误
await 语法
//expression: 一个 Promise 对象或者任何要等待的值。 |
特点:
- 不需要像 Generator 去调用 next 方法,遇到 await 等待,当前的异步操作完成就往下执行
- 返回的总是 Promise 对象,可以用 then 方法进行下一步操作
- async 取代 Generator 函数的星号*,await 取代 Generator 的 yield
- 语意上更为明确,使用简单,经临床验证,暂时没有任何副作用
代码实例
async function timeout(ms) { |
上述代码的执行结果为
// await |
上述代码的执行结果为
3.9 class 简介
在 ES6 中,class (类)作为对象的模板被引入,可以通过 class 关键字定义类。
class 的本质是 function。
它可以看作一个语法糖,让对象原型的写法更加清晰、更像面向对象编程的语法。
-
- 通过 class 定义类/实现类的继承
-
- 在类中通过 constructor 定义构造方法
-
- 通过 new 来创建类的实例
-
- 通过 extends 来实现类的继承
-
- 通过 super 调用父类的构造方法
-
- 重写从父类中继承的一般方法
class Person { |
上述代码的执行结果为
3 ES7 扩展
- 指数运算符(幂): **
- Array.prototype.includes(value) : 判断数组中是否包含指定 value
console.log(3 ** 3);//27 |