ES6标准入门


为什么选择ES6的Promise?——Promise能比较好地解决异步嵌套问题,ES6代表前端的未来,涉及很多现代编程语言概念中很流行的部分,让前端程序员的代码更加优美并且能够向前兼容。

ES6和JS的关系

ECMAScript和JavaScript的关系是,前者是后者的规格,后者是前者的 一 种实现。

let和const命令

let命令

基本用法

ES6中的let命令用法类似于var,但是所声明的变量只在let命令所在的代码块内生效

{
	let a = 10;
	var b = 1;
}

a // ReferenceError: a is not defined.
b // 1

for循环的计数器就很适合使用let命令:

var a = [];
for(let i = 0; i < 10; i++) // 如果此处使用var变量,则a[6]为10
{
	a[i] = function(){
		console.log(i)
	}
}

a[6]() // 6

原因如下:

  • 如果变量 i 是用var声明的,则该变量在全局范围内都有效,全局只有一个变量 i,每一次循环i的值都会发生变化。也就是说,所有数组 a 的成员中的 i 指向的都是同 一个 i ,导致运行时输出的是最后一轮的 i 值,也就是 10 。

  • 如果变量 i 是用let声明的,则当前i只在本轮循环中有效,所以每一次循环 i 都是一个新的变量,所以最终输出6

    如果每一轮循环的变量 i 都是重新声明的,那么它怎么知道上一轮的循环的值从而计算出本轮循环的值呢?

    JavaScript引擎内部会记住上一次循环的值,初始化本轮的 i 值时,就在上一轮的循环中计算。

此外,for循环还有一个特别之处:设置循环变量的那一部分是一个父作用域,循环内部是一个单独的子作用域

for(let i = 0; i < 3; i++)
{
	let i = 'abc'
	console.log(i)
}
// abc
// abc
// abc

不存在变量提升

  • var命令会出现变量提升现象,即变量可以在声明前使用,值为undefined。
  • let所声明的变量一定要在声明后使用

暂时性死区

只要块级作用域存在let命令,它所声明的变量就“绑定”这个区域,不受外部影响:

var tmp = 123;
if (true) {
	tmp = 'abc'; // ReferenceError
	let tmp; // let的声明导致tmp变量绑定这个作用域
}

总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上称为“暂时性死区”(temporal deadzone,简称TDZ)。

“暂时性死区”也意味着 typeof 不再是一个百分之百的安全操作。

typeof x; // ReferenceError
let x;

上面的代码中,变量x使用let命令声明,所以在声明之前都属于x的“死区”,只要用到该变量就会报错。

几种较为隐蔽的“死区”:

//1.
function bar(x = y, y = 2){
	return [x, y];
	// 之所以调用bar函数报错,是因为参数x的默认值等于另一个参数y,而此时y还没有声明。如果y的默认值是x,就不会报错。
}
//2.
// 不报错
var x = x;
// 报错
let x = x;
// ReferenceError:x is not defined

不允许重复声明

let不允许在相同作用域内重复声明同一个变量。

// 报错
function () {
    let a = 10;
    var a = 1;
}

// 报错
function func(arg){
    let arg = 10;
}

块级作用域

ES6的块级作用域

  1. 外层作用域无法读取内层作用域的变量
  2. 内层作用域可以定义外层作用域的同名变量

块级作用域的出现,实际上使得获得广泛应用的立即执行函数匿名函数(IIFE)不再必要了。

// IIFE写法
(function () {
	var tmp = ...;
	...
}());

// 块级作用域写法
{
	let tmp = ...;
	...
}

块级作用域与函数声明

ES6引入了块级作用域,明确允许在块级作用域之中声明函数

ES6规定,在块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。

注意:在ES6浏览器中,块级作用域内声明函数的行为类似于var声明变量!

考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式的形式,而不是函数声明语句:

{
	let a = 'abc';
	let f = function(){
		return a;
	}
}

注意:ES6的块级作用域允许声明函数的规则只在使用大括号的情况下成立,如果没用使用大括号则会报错!

do表达式

本质上,块级作用域是一个语句,将多个操作封装在一起,没用返回值。即在外部无法获取块级作用域内部变量的值。

现在有一个提案,使得块级作用域可以变为表达式,即可以返回值,方法就是在块级作用域之前加上do语句,使它变为do表达式:

let x = do{
	let t = f();
	t * t + 1;
}
// 变量x会得到整个块级作用域的返回值

const命令

基本用法

const声明一个只读的常量,一旦声明,常量的值就不能改变。

————这意味着,const一旦声明常量,就必须立即进行初始化,不能留到以后赋值。

  • const作用域与let命令相同,只在声明所在的块级作用域内有效。
  • const命令声明的常量也不会提升,同样存在暂存性死区,只能在声明后使用。
  • const声明常量和let一样,不可重复声明

本质

const实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。

对于简单类型的数据(数值、字符串、布尔值)而言,值就保存在变量指向的内存地址中,因此等同于常量。

但对于复合类型的数据(主要是对象和数组)而言,变量指向的内存地址保存的只是一个指针,const只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,这完全不能控制。——因此声明一个对象为常量时必须非常小心。

const foo = {};

// 为foo添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将foo指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

注意:不可变的只是这个对象的地址,即不能把foo指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。

如果真的想将对象冻结,应该使用Object.freeze 方法。

const foo = Object.freeze({});

// 常规模式时,下面一行不起作用,因为该对象被冻结
// 严格模式时,改行会报错
foo.prop = 123;

除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。

var constantize = (obj) => {
    Object.freeze(obj);
    Object.keys(obj).forEach((key, i) => {
        if(typeof obj[key] === 'object'){
            constantize(obj[key])
        }
    })
}

ES6声明变量的六种方法

  • var命令
  • function命令
  • let命令
  • const命令
  • import命令
  • class命令

顶层对象的属性

顶层对象在浏览器环境中指的是window对象,在Node环境中指的是global对象。在ES5中,顶层对象的属性与全局变量是等价的。

顶层对象的属性与全局变量相关,被认为是JavaScript语言中最大的设计败笔之一 :

  1. 无法在编译时就提示变量未声明的错误,只有在运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的)。
  2. 程序员很容易不知不觉地创建全局变量。
  3. 顶层对象的属性是到处都可以读写的,这非常不利于模块化编程。
  4. window对象有实体含义,指的是浏览器的窗口对象,这样也是不合适的。

ES6为了改变这一点,规定:

  • 为了保持兼容性,var命令和function命令声明的全局变量依旧是顶层对象的属性
  • 另一方面规定,let命令、const命令、class命令声明的全局变量不属于顶层对象的属性
var a = 1;
// 如果在Node的REPL环境,可以写成gloabal.a
// 或者采用通用方法,写成this.a
window.a // 1

let b = 1;
window.b // undefined

global对象

ES5的顶层对象本身也是一个问题,因为它在各种实现中是不统一的。

  • 在浏览器中,顶层对象是window,但 Node 和 Web Worker 没有 window。
  • 在浏览器和Web Worker中,self也指向顶层对象,但是Node没有self。
  • 在Node中,顶层对象是global,但其他环境都不支持。

同一段代码为了能够在各种环境中取到顶层对象,目前一般是使用this变量,但是也有局限性。

  • 在全局环境中,this 会返回顶层对象。但是,在 Node 模块和 ES6模块中,this返回的是当前模块。
  • 对于函数中的 this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。但是,严格模式下,this会返回 undefined。
  • 不管是严格模式,还是普通模式,new Function ('return this') ()总会返回全局对象。但是,如果浏览器用了 CSP(内容安全政策),那么eval、new Function 这些方法都可能无法使用。

现有一个提案,在语言标准的局面引入 global 作为顶层对象。也就是说,global 都是存在的,都可以拿到顶层对象。

// CommonJS的写法
require('system.global/shim')();

// ES6模块的写法
import shim from 'system.global/shim'; shim();

上面的代码可以保证,在各种环境中global对象都是存在的。

// CommonJS的写法
var global = require('system.global')();

// ES6模块的写法
import getGlobal from 'system.global';
const global = getGlobal()

上面的代码将顶层对象放入变量global中。

变量的解构赋值

数组的结构赋值

基本用法

  • ES6语法可以从数组中提取值,按照对应位置对变量赋值:
let [foo, [[bar], baz]] = [1, [[2], 3]];

let [, , third] = ['foo', 'bar', 'baz'];

let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]

本质上这种写法属于模式匹配,只要等号两边的模式相同,左边的变量就会赋予对应的值。

  • 如果解构不成功,变量的值就等于undefined。
let [foo] = [];
let [bar, foo] = [1];
  • 另一种情况是不完全解构,即等号左边的模式只匹配一部分的等号右边的数组,这种情况下,解构依然可以成功。
let [x, y] = [1, 2, 3];
x // 1
y // 2
  • 如果等号的右边不是数组(或者严格说不是可遍历的结构)那么将会报错:
// 报错
let [foo] = 1;
let [foo] = NAN;
let [foo] = {}

上面的语句都会报错,因为等号右边的值或是转为对象以后不具备Iterator接口,或是本身就不具备Iterator接口。

对于Set结构,也可以使用数组的解构赋值——事实上,只要某种数据结构具有Iterator接口,都可以采用数组形式的解构赋值。

默认值

  • 解构赋值允许指定默认值:
let [foo = true] = [];
foo // true
let [x, y = 'b'] = ['a']; // x = 'a', y = 'b'
let [x, y = 'b'] = ['a', undefined]; // x = 'a', y = 'b'

注意:ES6内部使用严格相等运算符(===)判断一个位置是否有值。所以,如果一个数组成员不严格等于undefined,默认值是不会生效的。

例如:

let [x = 1] = [undefined];
x // 1

let [x = 1] = [null];
x // null

上面的代码中,如果一个数组成员是null,默认值就不会生效,因为null不严格等于undefined

  • 如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到时才会求值
function f() {
	console.log('aaa');
}

let [x = f()] = [1];
  • 上面的代码中,因为x能取到值,所以函数f根本不会执行。上面的代码等价如下:
let x;
if ([1][0] === undefined) {
	x = f();
} else {
	x = [1][0];
}
  • 默认值可以引用解构赋值的其他变量,但该变量必须已经声明
let [x = 1, y = x] = []; // x=1; y=1
let [x = 1, y = x] = [2]; // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = [] // ReferenceError

对象的解构赋值

解构不仅可以用于数组,还可以用于对象。

对象的解构和数组有一个重要的不同——

数组的元素是按次序排列的,变量的取值是由它的位置决定的;

而对象的属性没有次序,变量必须与属性同名才能取到正确的值。

如果变量名和属性名不一致,必须写成下面这样:

var { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // 'aaa'

实际上,对象的赋值解解构是下面形式的简写:

var { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };

也就是说,对象的解构赋值的内部机制是先找到同名属性,然后再赋值给对应的变量。真正被赋值的是后者,而不是前者。

var { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined

上面的代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。

  • 与数组一样,解构也可以用于嵌套结构的对象。
  • 对象的解构也可以指定默认值——默认值生效的条件是,对象的属性值严格等于undefined。
  • 如果解构失败,变量的值等于undefined
  • 如果解构模式是嵌套的对象,且子对象所在的父属性不存在,那么将会报错。

如果要将一个已经声明的变量用于解构赋值,必须非常小心:

let x;
{x} = {x: 1};
// SyntaxError: syntax error

这是因为JavaScript引擎会将 {x} 理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免JavaScript将其解释为代码块,才能解决这个问题:

let x;
({x} = {x: 1});

关于圆括号与赋值结构的关系,参见下文:

解构赋值允许等号左边的模式之中不放置任何变量名:

({} = [true, false]);
({} = 'abc')

上面的表达式虽然毫无意义,但可以执行。

对象的解构赋值可以很方便地将现有对象的方法赋值到某个变量。

let { log, sin, cos } = Math;

上面的代码将Math对象的对数、正弦、余弦三个方法赋值到对应的变量上,使用起来就方便很多。

由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。

字符串的解构赋值

  • 字符串可以解构赋值,因为此时字符串被转换成了一个类似数组的对象
  • 类似数组的对象都有一个length对象,因此还可以对这个属性进行解构赋值。

数值和布尔值的解构赋值

解构赋值时,如果等号右边是数值布尔值,则会先转为对象。

赋值解构的规则是:只要等号右边的值不是对象或数组,就先将其转为对象。

由于undefined和null无法转换为对象,所以对它们进行解构赋值时会报错。

函数参数的解构赋值

function add([x, y]){
	return x + y;
}
add([1, 2]); // 3

上面的代码中,函数add的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x和y,对于函数内部的代码来说,它们能感受到的参数就是x和y。

另一个例子:

[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [3, 7]

圆括号问题

解构赋值虽然很方便,但是解析起来并不容易。对于编译器来说,一个式子到底是模式还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。
由此带来的问题是,如果模式中出现圆括号该怎么处理?

ES6的规则是,只要有可能导致解构的歧义,就不得使用圆括号

不能使用圆括号的情况

  1. 变量声明语句
  2. 函数参数
  3. 赋值语句的模式,即将整个模式放在圆括号中(而非将整个赋值语句放在圆括号中)

可以使用圆括号的情况

可以使用圆括号的情况只有 一 种 : 赋值语句的非模式部分可以使用圆括号。

[(b)] = [3];
({ p: (d)} = {});
[(parseInt.prop)] = [3];

文章作者: QT-7274
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 QT-7274 !
评论
 上一篇
ES6标准总结 ES6标准总结
熟练运用ES6标准是提升前端开发效率的必备技能,也是写出优雅高性能代码的捷径。
2023-01-08
下一篇 
算法入门-动态规划 算法入门-动态规划
动态规划也是把原问题分解为若干子问题,先求解最小的子问题,把结果存储在表格中,再求解大的子问题时,直接从表格中查询小的子问题的解。
2022-10-27
  目录