ECMAScript 和 JavaScript 的关系
前者是后者的规格,后者是前者的一种实现(另外的 ECMAScript 方言还有 JScript 和 ActionScript)。
日常场合,这两个词是可以互换的。
ES6 与 ECMAScript 2015 的关系
2011 年,ECMAScript 5.1 版发布后,就开始制定 6.0 版了。因此,ES6 这个词的原意,就是指 JavaScript 语言的下一个版本。
标准委员会最终决定,标准在每年的 6 月份正式发布一次,作为当年的正式版本。接下来的时间,就在这个版本的基础上做改动,直到下一年的 6 月份,草案就成了新一年的版本。这样一来,就不需要以前的版本号了,只要用年份标记就可以了。
ES6 的第一个版本,在 2015 年 6 月发布了,正式名称就是《ECMAScript 2015 标准》(简称 ES2015)
ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准
1997 年,发布 ECMAScript 1.0
1998 年 6 月,发布 ECMAScript 2.0
1999 年 12 月,发布 ECMAScript 3.0
ECMAScript 3.0 版是一个巨大的成功,在业界得到广泛支持,成为通行标准,奠定了 JavaScript 语言的基本语法,以后的版本完全继承。直到今天,初学者一开始学习 JavaScript,其实就是在学 ECMAScript 3.0 版的语法。
2000 年,ECMAScript 4.0 开始酝酿。这个版本最后没有通过,但大部分内容被 ES6 继承了。因此,ES6 制定的起点其实是 2000 年。
2007 年 10 月,ECMAScript 4.0 版草案发布。
2008 年 7 月 ECMA 开会决定,中止 ECMAScript 4.0 的开发,将其中涉及现有功能改善的一小部分,发布为 ECMAScript 3.1,而将其他激进的设想扩大范围,放入以后的版本,由于会议的气氛,该版本的项目代号起名为 Harmony (和谐)。
会后不久,ECMAScript 3.1 就改名为 ECMAScript 5。
2009 年 12 月,ECMAScript 5.0 版正式发布。
2011 年 6 月,ECMAScript 5.1 版发布,并且成为 ISO 国际标准(ISO/IEC 16262:2011)。
2013 年 3 月,ECMAScript 6 草案冻结,不再添加新功能。新的功能设想将被放到 ECMAScript 7。
2013 年 12 月,ECMAScript 6 草案发布。然后是 12 个月的讨论期,听取各方反馈。
2015 年 6 月,ECMAScript 6 正式通过,成为国际标准。从 2000 年算起,这时已经过去了 15 年。
关键字分成不同用途,有的是命令(delete
),有的是语言结构(比如if
)。见此 issue
ES6 新增了let
关键字,用来声明变量。它的用法类似于var
,但是所声明的变量,只在let
关键字所在的代码块内有效。
标准for
循环中,每一轮循环的变量i
都是重新声明的,JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
for ( let i = 0 ; i < 3 ; i ++ ) {
let i = 'abc' ;
console . log ( i ) ;
}
上面代码正确运行,输出了 3 次 abc
。这表明函数内部的变量i
与循环变量i
不在同一个作用域,有各自单独的作用域。
// let 的情况
console . log ( bar ) ; // 报错ReferenceError
let bar = 2 ;
代码块内,使用let
命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
“暂时性死区”也意味着typeof
不再是一个百分之百安全的操作。
ES6 规定暂时性死区和let
、const
语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。
let不允许在相同作用域内,重复声明同一个变量。因此,不能在函数内部重新声明参数。
ES6 允许块级作用域的任意嵌套。
内层作用域可以定义外层作用域的同名变量。
块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了。
ES6 引入了块级作用域,明确允许在块级作用域之中声明函数
为了减轻因此产生的不兼容问题,ES6 在附录 B 里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式 。
ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。
const
一旦声明变量,就必须立即初始化。
const
的作用域与let
命令相同:只在声明所在的块级作用域内有效。
const
命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。
ES5 只有两种声明变量的方法:var
命令和function
命令。
ES6 除了添加let
和const
命令,还有:import
命令和class
命令。
顶层对象,在浏览器环境指的是window
对象,在 Node 指的是global
对象。
ES5 之中,顶层对象的属性与全局变量是等价的。
顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。
ES6 为保持兼容性,var
命令和function
命令声明的全局变量,依旧是顶层对象的属性;:star:
let
命令、const
命令、class
命令声明的全局变量,不属于顶层对象的属性。
从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。
很难找到一种方法,可以在所有情况下,都取到顶层对象。
ES2020 在语言标准的层面,引入globalThis
作为顶层对象。也就是说,任何环境下,globalThis
都是存在的,都可以从它拿到顶层对象,指向全局环境下的this
。
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,称为解构(Destructuring)。属于“模式匹配”
let [ a , b , c ] = [ 1 , 2 , 3 ] ;
let [ foo , [ [ bar ] , baz ] ] = [ 1 , [ [ 2 ] , 3 ] ] ;
let [ , , third ] = [ "foo" , "bar" , "baz" ] ;
let [ head , ...tail ] = [ 1 , 2 , 3 , 4 ] ; // tail: [2, 3, 4]
let [ x , y , ...z ] = [ 'a' ] ; // y: undefined, z: []
let [ x , y , z ] = new Set ( [ 'a' , 'b' , 'c' ] ) ; // x: "a"
如果解构不成功,变量的值就等于undefined
。
不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。
如果等号的右边不是数组(或者严格地说,不是可遍历的结构,参见《Iterator》一章),那么将会报错。
let [ foo = true ] = [ ] ;
let [ x = 1 ] = [ undefined ] ; // x: 1
let [ x = 1 ] = [ null ] ; // x: null
ES6 内部使用严格相等运算符(===
),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined
,默认值才会生效。
如果默认值是一个表达式,那么这个表达式是惰性求值的。
默认值可以引用解构赋值的其他变量,但该变量必须已经声明。
let { foo, bar } = { foo : 'aaa' , bar : 'bbb' } ;
如果解构失败,变量的值等于undefined
。
对象的解构赋值是下面形式的简写
let { foo : foo , bar : bar } = { foo : 'aaa' , bar : 'bbb' } ;
对象的解构也可以指定默认值。
默认值生效的条件是,对象的属性值严格等于undefined。
var { x = 3 } = { x : undefined } ; // x: 3
var { x = 3 } = { x : null } ; // x: null
将大括号写在行首,JavaScript 会将其解释为代码块。
let x ;
{ x } = { x : 1 } ; // SyntaxError: syntax error
( { x} = { x : 1 } ) ; // right way!
由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。
let arr = [ 1 , 2 , 3 ] ;
let { 0 : first , [ arr . length - 1 ] : last } = arr ; // first: 1, last: 3
const [ a , b , c , d , e ] = 'hello' ; // a: "h", ...
let { length : len } = 'hello' ; // len: 5
字符串也可以解构赋值。此时,字符串被转换成了一个类似数组的对象。
数组的对象都有一个length属性,因此还可以对这个属性解构赋值。
解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。
由于undefined
和null
无法转为对象,所以对它们进行解构赋值,都会报错。
函数的参数也可以使用解构赋值。
函数参数的解构也可以使用默认值。
ES6 的规则是,只要有可能导致解构的歧义,就不得使用圆括号。
建议只要有可能,就不要在模式中放置圆括号。
变量声明语句:
函数参数 (也属于变量声明)
赋值语句的模式(整个或部分模式放在圆括号之中)
let [ ( a ) ] = [ 1 ] ; // 1
function f ( [ ( z ) ] ) { return z ; } // 2
( [ a ] ) = [ 5 ] ; // 3a
[ ( { p : a } ) , { x : c } ] = [ { } , { } ] ; // 3b
[ ( b ) ] = [ 3 ] ; // 正确
( { p : ( d ) } = { } ) ; // 正确 模式是p,而不是d
[ ( parseInt . prop ) ] = [ 3 ] ; // 正确
交换变量的值
let x = 1 , y = 2 ;
[ x , y ] = [ y , x ] ; // 简洁、易读、语义清晰
从函数返回多个值
函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。
函数参数的定义
function f1 ( [ x , y , z ] ) { ... } // 参数是一组有次序的值
f1 ( [ 1 , 2 , 3 ] ) ;
function f2 ( { x, y, z} ) { ... } // 参数是一组无次序的值
f2 ( { z : 3 , y : 2 , x : 1 } ) ;
提取 JSON 数据
函数参数的默认值
避免了在函数体内部再写var foo = config.foo || 'default foo';
这样的语句。
遍历 Map 结构
Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便
输入模块的指定方法
const { SourceMapConsumer, SourceNode } = require ( "source-map" ) ;