线程

做一个超额的线程

( 手记 ) ECMAScript 6

变量

for 循环中,设置循环条件那部分是一个父作用域,而循环体内又是一个单独的子作用域,例子如下:

1
2
3
4
5
6
7
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc

同时 let 不再像 var 一样存在变量提升

1
2
3
4
5
6
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

let不允许在一个作用域内重复声明同一个变量,否则会报错

ES5 中的作用域

在ES5中,只有全局作用域和函数作用域两个概念,这可能会引起一些问题。

  • 内部变量覆盖外部变量
1
2
3
4
5
6
7
8
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined

函数 f() 执行时打印变量 tmp 的值,输出结果为 undefined,
原因在于变量提升,函数内的 console.log(tmp); 语句并不会输出函数外部tmp变量的值,因为函数作用域内还声明了一个 tmp ,打印语句会输出作用域内声明的tmp的值而不是外部的tmp, 由于内部的tmp声明语句在打印语句之后,所以输出结果为 undefined

  • 在循环中用于计数的变量泄露成全局变量
1
2
3
4
5
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5

循环结束后,变量 i 并没有消失,而是泄露成全局变量。

ES6 中的块级作用域

let 实际上为Javascript 新增了块级作用域

1
2
3
4
5
6
7
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
运行结果输出5,表示外层代码块不受内层代码块的影响,如果此段代码两次都使用 var 定义的话,那么输出结果为10。

ES6 允许块级作用域的任意嵌套

1
2
3
4
5
6
7
8
9
10
11
{
{
{
{
{
let insane = 'Hello World'
}
}
}
}
};

外层作用域无法读取内层作用域的变量

1
2
3
4
5
6
7
8
9
10
{
{
{
{
{let insane = "Hello World"}
console.log(insane); // 报错
}
}
}
}

不同作用域可以定义同名变量

1
2
3
4
5
6
7
8
9
10
{
{
{
{
{let insane = "Hello World"}
let insane = "Hello World";
}
}
}
}

块级作用域与函数声明

在ES5中规定,函数只能在顶层全局作用域和函数作用域内声明,不能在块级作用域声明,所以下面代码在ES5中是非法的,但都能运行,不会报错。

1
2
3
4
5
6
7
8
9
10
// 情况一
if (true) {
function f() {}
}
// 情况二
try {
function f() {}
} catch(e) {
// ...
}

而在ES6中,明确允许在块级作用域内声明函数,ES6规定,块级作用域之中,函数的生命语句行为类似于 let, 在块级作用域外不能引用。
以上说的这点存在于ES6的规范中,但浏览器为了不影响老代码,并未实现,所以如果在ES6浏览器中运行块级作用域内的函数,是会报错的

浏览器的实现可以不遵循以上特点,实行自己的行为方式。

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

以上三条规则只对ES6的浏览器实现有效,在其他环境类似于 Node.js 中不用遵守,还是将块级作用域的函数声明当作 let 处理。
考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数,如果确实需要声明,也应该写成函数表达式,而不是函数声明语句。

do 表达式

由于ES6新增的块级作用域只是一个语句,将多个操作封装在一起,形成一个作用域,它不会像函数一样有返回值。
如果想让块级作用域有返回值可以使用 do 表达式,就可以返回内部最后执行结果的值。如下:

1
2
3
4
let x = do {
let t = f();
t * t + 1;
};
const 命令

const 声明一个只读的常量,一旦声明,其值就不能改变,如果之后再赋值,就会报错。
同时由于 const 声明的变量不能再改变值,这意味着 const 必须在声明时就赋值,不能留到以后赋值。
只声明不赋值就会报错。

1
2
const foo;
// SyntaxError: Missing initializer in const declaration

const 的作用域和 let 相同:只在声明所在的块级作用域内有效。同样不会提升,存在暂时性死区,只能在声明的位置之后使用。也不可以重复声明。

const的本质

const 实际保证的,并不是变量的值不能改动,而是变量指向的那个内存地址不得改动,对于简单类型的数据,值就保存在变量指向的那个内存地址,因此等同于常量,但对于复合类型的数据(对象和数组),变量所指向的内存地址,保存的只是一个指针,const 只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制,因此,将一个对象声明为常量必须非常小心。

1
2
3
4
5
6
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

ES6 声明变量的六种方法

ES5 只有两种声明变量的方法:var 命令和 function命令。ES6 除了添加 letconst 命令,后面还会提到,另外两种声明变量的方法:import 命令和 class 命令。所以,ES6 一共有 6 种声明变量的方法。

顶层对象的属性

顶层对象,在浏览器中指 windows 对象,在 node 中指 global 对象,ES5中,顶层对象的属性和全局变量是等价的。

1
2
3
4
window.a = 1;
a // 1
a = 2;
window.a // 2

在ES6中规定,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性,另一方面规定,let 命令、const 命令、class 命令声明的全局变量,不再是顶层对象的属性(如果使用会输出undefined),也就是说,从ES6开始,全局变量将逐渐与顶层对象的属性脱钩。

global 对象

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

  • 在浏览器里面,顶层对象是 window,但 nodeweb worker 没有 window
  • 浏览器和 web worker 里面,self 也指向顶层对象,但是 node 里没有 self.
  • node 里面,顶层对象是 global,但其他环境都不支持。

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

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

解构赋值

默认值

解构赋值允许指定默认值,如果一个数组成员不严格等于(===)undefined,则不会使用默认值

1
2
3
4
let [x = 1] = [undefined];
x // 1
let [x = 1] = [null];
x // null

如果默认值是一个表达式,那么该表达式是惰性求值得,即只有在用到的时候才会求值。

由于 x 有对应的值,所以函数f()根本不会执行。

1
2
3
4
function f() {
console.log('aaa');
}
let [x = f()] = [1];
对象的解构赋值

如果你想要变量名和属性名不一致,则必须写成下面这样。

1
2
3
4
5
6
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'

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

1
2
3
4
5
6
7
8
// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error

// 正确的写法
let x;
({x} = {x: 1});
字符串的解构赋值
1
2
3
4
5
6
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

类似数组的对象都有一个 length 属性,因此还可以对这个属性解构赋值。

1
2
let {length : len} = 'hello';
len // 5
数值与布尔值的解构赋值

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

1
2
3
4
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true

解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于 undefinednull 无法转为对象,所以对它们进行解构赋值,都会报错。

函数参数的解构赋值
1
2
3
4
function add([x, y]){
return x + y;
}
add([1, 2]); // 3
解构赋值的用途

方便交换变量的值

1
2
3
let x = 1;
let y = 2;
[x, y] = [y, x];

从函数返回多个值

1
2
3
4
5
6
7
8
9
10
11
12
13
// 返回一个数组
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();
// 返回一个对象
function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();

函数参数的定义

1
2
3
4
5
6
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});

提取 JSON 数据

1
2
3
4
5
6
7
8
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
console.log(id, status, number);
// 42, "OK", [867, 5309]

函数参数的默认值

1
2
3
4
5
6
7
8
9
10
11
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
}) {
// ... do stuff
};

字符串的扩展

字符串的遍历器接口
1
2
3
4
5
6
for (let codePoint of 'foo') {
console.log(codePoint)
}
// "f"
// "o"
// "o"

参考:http://es6.ruanyifeng.com/