ECMAScript和JavaScript的关系?
1996年11月,JavaScript的创造者—-Netscape公司,决定将JavaScript提交给国际标准化组织ECMA,希望这种语言能成为国际标准。次年,ECMA发布262号文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言成为ECMAScript,这个版本就是1.0版。
该标准一开始就是针对JavaScript语言制定的,但是没有称为JavaScript,原因有二:
因此ECAM和JavaScript的关系是,前者是后者的规格,后者是前者的一种实现(另外还有JScript和ActionScript)。
下表是各个版本ES发布时间:
标准标题 | 发布时间 |
---|---|
ECMA-262 | 1997年7月 |
ECMA-262 Edition2 | 1998年8月 |
ECMA-262 Edition3 | 1999年12月 |
ECMA-262 Edition5 | 2009年12月 |
ECMA-262 Edition6 | 2015年6月 |
ECMA-262 Edition7 | 2016年7月 |
ECMAScript 2016(ES7)开始,版本的发布将会变得更加频繁, 这也意味着未来每个新的发行版本都会包含尽可能少的特性,而发行周期则缩短为1年,并且每年只发行确保一年期限内能够完成的所有特性。
ES6新增了let命令,用法和var类似,区别是let声明的变量只在其所在的作用域有效。
{
var a = 3;
}
console.log(a); // 3
{
let a = 3;
}
console.log(a); // ReferenceError: a is not defined
一个典型的例子,看看var和let的区别:
var a = [];
for(var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i)
}
}
a[6](); // 10
上述i是全局声明的,a[0]到a[10]绑定的都是同一个函数function(){console.log(i)}
,所以在执行的时候i变成10了。会打印出10.
var b = [];
for(let i = 0; i < 10; i++) {
b[i] = function () {
console.log(i)
}
}
b[6](); // 6
使用let声明变量,let劫持了for循环的作用域,所以每个i都是有自己的作用域的。于是最后输出的结果是6。
let不像var会存在变量提升。所以必须先声明变量才能使用,否则报错。
console.log(x); // undefined
var x;
console.log(x); // ReferenceError: x is not defined
let x;
ES6明确规定,如果区块中存在let和const命令,则这个区块对这些命令声明的变量从一开始就形成封闭作用域。只要在声明之前使用这些变量,就报错。
上面那个例子也算TDZ。
if(true) {
tmp = 'abc'; // ReferenceError: tmp is not defined
console.log(tmp); // ReferenceError: tmp is not defined
let tmp; // TDZ结束
console.log(tmp);
}
还有些闭包较为隐蔽,不易察觉。函数参数默认赋值时es6才出现的,使用let。:)
function foo(x = y, y = 2) { console.log(x); } foo(); // ReferenceError: y is not defined
因为函数参数的赋值从左到右,给x赋值时,y还处于TDZ。所以赋值失败。
ES6规定暂时性死区和不存在变量提升,主要是为了减少运行时错误,毕竟这样的失误在ES5是存在的。现在有了这种规定,浏览器会自动避免此类错误。
关于是否存在变量提升,这里有一篇文章。
{
var a = 1;
let a = 1; // SyntaxError: Identifier 'a' has already been declared
}
ES5没有块级作用域,只有函数作用域和全局作用域。比如形成变量覆盖。
var a = 1;
function foo(){
console.log(a);
if(false) {
var a = 2; // 变量提升
}
}
foo(); // undefined
再比如循环中的变量泄露到全局变量。
ES6新增了块级作用域,使用let可以形成块级作用域。
function foo(){
let n = 5;
if(true) {
let n = 10;
}
console.log(n); // 5
}
上述如果使用var定义输出结果是10。
ES6允许作用域任意嵌套。
{{{{ let a = 1; }}}}
函数能不能在块级作用域之中声明,是一个相当令人混淆的问题。
ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。
if (true) {
function f() {}
}
// 情况二
try {
function f() {}
} catch(e) {
// ...
}
上面两种函数声明,根据 ES5 的规定都是非法的。
但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。ES不过,“严格模式”下还是会报错。(我在chrome50.0没报错,默认转换为es6了吗???)
// ES5严格模式
'use strict';
if (true) {
function f() {}
}
ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。
// ES6
if (true) {
function f() {} // 不报错
}
ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}
f();
}());
上面代码在 ES5 中运行,会得到“I am inside!”,因为在if内声明的函数f会被提升到函数头部,实际运行的代码如下。
// ES5 版本
function f() { console.log('I am outside!'); }
(function () {
function f() { console.log('I am inside!'); }
if (false) {
}
f();
}());
ES6 的运行结果就完全不一样了,会得到“I am outside!”。因为块级作用域内声明的函数类似于let,对作用域之外没有影响,实际运行的代码如下。
// ES6 版本
function f() { console.log('I am outside!'); }
(function () {
f();
}());
但是,如果你真的在 ES6 浏览器中运行一下上面的代码,是会报错的,这是为什么呢?
原来,ES6 改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6在附录B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。
注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作let处理。
根据这三条规则,在浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于var声明的变量。
// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}
f();
}());
// Uncaught TypeError: f is not a function
上面的代码在符合 ES6 的浏览器中,都会报错,因为实际运行的是下面的代码。
// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }
(function () {
var f = undefined;
if (false) {
function f() { console.log('I am inside!'); }
}
f();
}());
// Uncaught TypeError: f is not a function
考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。
// 函数声明语句
{
let a = 'secret';
function f() {
return a;
}
}
// 函数表达式
{
let a = 'secret';
let f = function () {
return a;
};
}
另外,还有一个需要注意的地方。ES6 的块级作用域允许声明函数的规则,只在使用大括号的情况下成立,如果没有使用大括号,就会报错。
// 不报错
'use strict';
if (true) {
function f() {}
}
// 报错
'use strict';
if (true)
function f() {}
const定义一个常量,定义的同时必须赋值,不赋值就会报错。其值不会改变,给它重新赋值会引起错误。
const PI = 3.14;
PI = 3; // TypeError: Assignment to constant variable.
但是对于一个复杂数据类型(对象),我们知道它在内存中是有一个地址的(即指针),const即是指向这个地址,保证这个地址是常量即可。对于对象的属性,却是可以定义的。
const obj = {};
obj.a = 3; // 这是可以的
如果真的想将对象冻结,应该使用Object.freeze方法。
const foo = Object.freeze({});
foo.prop = 123; // 不起作用
const的作用域和let命令相同:只在声明所在的块级作用域内有效。
if(true) {
const Max = 5;
}
console.log(Max); // ReferenceError
const命令声明的变量也不提升,同样存在暂时性死区。
if(true) {
console.log(MAX); // ReferenceError
const MAX = 5;
}
const也不可重复声明变量,这点和let一样。无论是用var还是let声明。
var a = 1;
const a = 1; // SyntaxError: Identifier 'a' has already been declared
ES5只有两种声明变量的党阀,var命令和function命令。ES6除添加了let和const命令。还有import命令和class命令。所以,ES6一共有6中声明变量的方法。
ES6规定,var和function声明的全局变量依旧是全局对象的属性,而const和let命令声明的变量不属于全局对象的属性。
var a = 1;
console.log(window.a); // 1
let a = 1;
console.log(window.a); // undefined
以前为变量赋值,只能直接指定值,一个等号只能赋值一次。而在ES6中,
var [a, b, c] = [1, 2, 3];
本质上,这种写法属于“模式匹配”,只要等号两边等边的模式相同,就可以赋值。
var [a, [b], [c]] = [1, [2], [3]];
如果变量没有对应到值(解构不成功),那么变量默认赋值为undefined。
let [a, b, c] = [1,2];
console.log(a,b,c); // 1 2 undefined
另一个例子是不完全解构,解构是可以成功的。
var [a, [b]] = [1, [2, 3]];
console.log(a, b); // 1 2
解构对var、let、const命令都适合。
事实上,只要某种数据结构具有Iterator接口,都可以采用数组形式的解构赋值。
解构赋值允许制定默认值。
var [foo = true] = [];
console.log(foo); // true
注意,ES6内部使用严格相等运算符(===)判断一个位置是否有值。所以,只有一个数组成员严格等于undefined,默认值才会生效(如上述的什么都不写)。
var [x = 1, y = 2] = [undefined, null];
console.log(x, y); // 1, null
解构不仅可以用于对象,还可以用于数组。
var { foo, bar } = { foo: "aaa", bar: "bbb" } ;
console.log(foo, bar); // aaa bbb
对象的解构赋值与数组有一个重要的不同。数组的元素是有序的,变量的取值有它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
var { foo, bar } = { bar: 'bbb', foo: 'aaa' };
console.log(foo, bar); // aaa bbb
如果变量名和属性名不一致,必须写成下面这样。
var {foo: baz} = {foo: 1};
console.log(baz); // 1
console.log(foo); // ReferenceError: foo is not defined
这实际上说明,对象的解构赋值时一下形式的缩写:
var { foo: foo, bar: bar} = { foo: 'aaa', bar: 'bbb'};
也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
对于解构赋值这种写法,变量的声明和赋值时一体的。对于let和const而言,变量不能重新声明,否则会报错。
let foo;
let { foo } = { foo: 1 }; // SyntaxError: Identifier 'foo' has already been declared
数组和对象 的结构赋值可以结合起来使用。
const [a, b] = 'hi';
console.log(a, b); // h i
let {length: len} = 'hello';
console.log(len); // 5
let { toString: s } = 123;
console.log(s === Number.prototype.toString); // true
let { toString: s } = true;
console.log(s === Boolean.prototype.toString); // true
解构赋值的规则是:如果等号右边不是对象,就先将右边转换为对象。对于undefined和null无法转为对象,所以对它们解构赋值会报错。
function add([x, y]) { return x + y; } console.log(add([2 ,3])); // 5
参数的解构赋值也可以使用默认值,只有undefined会触发默认值。
function move({x, y} = { x: 0, y: 0}) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]
对于上述代码,我的理解是调用函数时,先将函数实参与实参匹配,如果可以匹配,即前三个。实参对象取代了默认值,然后对x,y进行赋值,正如第三个,实参中的x,y都是undefined。如果没匹配上,就是用默认参数,再对x、y赋值。
以下三种解构赋值不得使用圆括号。
// 全部报错
let [(a)] = [1];
let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};
let { o: ({ p: p }) } = { o: { p: 2 } };
上面三个语句都会报错,因为它们都是变量声明语句,模式不能使用圆括号。
函数参数也属于变量声明,因此不能带有圆括号。
// 报错
function f([(z)]) { return z; }
// 全部报错 ({ p: a }) = { p: 42 }; ([a]) = [5];
上面代码将整个模式放在圆括号之中,导致报错。
// 报错 [({ p: a }), { x: c }] = [{}, {}];
上面代码将嵌套模式的一层,放在圆括号之中,导致报错。
[(b)] = [3]; // 正确
({ p: (d) } = {}); // 正确
[(parseInt.prop)] = [3]; // 正确
上面三行语句都可以正确执行,因为首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是p,而不是d;第三行语句与第一行语句的性质一致。
[x, y] = [y, x];
const { Component } = require('react-native');
http://es6.ruanyifeng.com/