ES6学习笔记一之js发展、let、const、解构赋值

ECMAScript和JavaScript的关系?

1996年11月,JavaScript的创造者—-Netscape公司,决定将JavaScript提交给国际标准化组织ECMA,希望这种语言能成为国际标准。次年,ECMA发布262号文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言成为ECMAScript,这个版本就是1.0版。

该标准一开始就是针对JavaScript语言制定的,但是没有称为JavaScript,原因有二:

  • Java时Sun公司的注册商标,根据授权协议,只有NetScape公司可以合法使用JavaScript这个名字,而且JavaScript本身已被NetScape公司注册为商标。
  • 体现这门语言的制定者时ECMA,而不是NetScape,这样有利于保证这门语言的开放性和中立性。

因此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年,并且每年只发行确保一年期限内能够完成的所有特性。

let和const

let命令

基本用法

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;

暂时性死区(temporal dead zone, TDZ)

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里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。

  • 允许在块级作用域内声明函数。
  • 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
  • 同时,函数声明还会提升到所在的块级作用域的头部。

注意,上面三条规则只对 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定义一个常量,定义的同时必须赋值,不赋值就会报错。其值不会改变,给它重新赋值会引起错误。

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];
  • 从函数返回多个值
  • 函数参数的定义
  • 提取JSON数据
  • 函数参数的默认值
  • 遍历Map结构
    任何部署了Iterator接口的对象,都可以用for…of循环遍历。Map结构原生支持Iterator接口,配合变量的解构赋值,获取键名和键值就非常方便。
  • 输入模块
const { Component } = require('react-native');

参考

http://es6.ruanyifeng.com/

你可能感兴趣的:(JavaScript)