ES6学习笔记

let和const命令:

let命令:

基本用法:

ES6中增加了let命令, 用来声明局部变量。用法跟var类似,但是所声明的变量,只在let命令所在的代码块中有效。

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

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

上面代码在代码块之中,分别用let和var声明两个变量。然后在代码块之外调用这两个变量,结果
let声明的变量报错,var声明的变量返回了正确的值。let变量只在所在代码块有效。

for循环的计数器,使用let命令比较合适。

for (let i = 0; i < 10; i++) {
 console.log(i);
}

在上面代码中,i只在for循环内生效,在for循环之外,i变量会失效

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

上面代码中,i由var命令声明,在全局范围内有效,所以全局只有一个变量i。每次循环,变量i的值会发生改变,而循环内被赋给数组a的函数内部的i,是全局i,也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮i的值,也就是10.如果使用let,变量只在块级作用域里有效,最后输出的是6.

不存在变量提升:

var命令会发生变量提升的现象,可以在声明之前被使用,值为undefined,这种现象很奇怪,变量应该在声明语句之后才能使用。
let纠正了这种现象,改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。

但是实际上在浏览器调试运行时,发现var变量也会报错,不知道是不是已经修正了

// var 的情况
console.log(foo); // 输出undefined
var foo = 2;

// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

暂时性死区:

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

var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

不允许重复声明:

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

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

块级作用域:

内部变量可能会覆盖全局变量:

var tmp = new Date();

function f() {
  console.log(tmp);
  if (false) {
    var tmp = 'hello world';
  }
}

f(); // undefined

在上面的代码中,内层tmp覆盖了外层tmp

或者内部变量泄露成为全局变量:

var s = 'hello';

for (var i = 0; i < s.length; i++) {
  console.log(s[i]);
}

console.log(i); // 5

ES6的块级作用域:

function f1() {
  let n = 5;
  if (true) {
    let n = 10;
  }
  console.log(n); // 5
}

const命令:

const表明生成一个常量,只要赋值,就不可再改变,且const一旦声明变量,就必须进行初始化,不能留到以后赋值。const的作用域与let相同,只在块级作用域内有效。const常量不存在提升,存在暂时性死区。

const PI = 3.1415;
PI // 3.1415

PI = 3;
// TypeError: Assignment to constant variable.

数组的解构和赋值:

基本用法:

原来的形式:

let a = 1;
let b = 2;
let c = 3;

现在新的形式:

let [a, b, c] = [1, 2, 3];

如果解构不成功,赋值undefined,以下foo的值都会等于undefined

let [foo] = [];
let [bar, foo] = [1];

当不完全解构时,即等号左边的模式,只匹配一部分的等号右边的数组,解构也会成功。

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

let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4

像下面这种对数组进行非数组形式赋值时也会失败:

// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};

事实上,只要某种数据结构具有 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,则不会生效
如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。

对象的解构赋值

let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"

数组解构必须排序,但是对象解构可以根据名字解构,顺序不影响:

let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"

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

如果对象解构失败,赋值undefined

let {foo} = {bar: 'baz'};
foo // undefined

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

// 例一
let { log, sin, cos } = Math;

// 例二
const { log } = console;
log('hello') // hello

对象解构的默认值:

var {x = 3} = {};
x // 3

var {x, y = 5} = {x: 1};
x // 1
y // 5

var {x: y = 3} = {};
y // 3

var {x: y = 3} = {x: 5};
y // 5

var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"

注意点

当变量已经声明时,在进行解构:

// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error

在上面代码中,js会将{x}理解成一个代码块,从而发生语法错误,要避免将大括号写在行首,正确的写法如下:

// 正确的写法
let x;
({x} = {x: 1});

解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。

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

上面的表达式虽然毫无意义,但是语法是合法的,可以执行。
----------------------------------------------
因为数组本身也是对象,可以对数组进行对象的解构:

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"
b // "e"
c // "l"
d // "l"
e // "o"

数值和布尔值的解构赋值:

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

let {toString: s} = 123;
s === Number.prototype.toString // true

let {toString: s} = true;
s === Boolean.prototype.toString // true

上面代码中,数值和布尔值的包装对象都有toString属性,因此变量s都能取到值。

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

let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

函数的参数也可以进行解构赋值:

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 ]

函数参数的解构也可以使用默认值:

function move({x = 0, y = 0} = {}) {
  return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]

在上面,函数move的参数是一个对象,通过对对象进行解构,得到变量x和y的值。如果解构失败,x和y等于默认值。但是,下面的写法会得到不一样的结果:

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]

因此,这两个函数的区别在于对解构赋值语法中的默认值的使用和位置。第一个函数在解构赋值中为每个属性设置了默认值,而第二个函数只在整个解构赋值表达式失败时才会使用默认值。

[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]

在这个例子中,我们使用了数组的 map() 方法来对数组的每个元素进行处理并生成一个新的数组。箭头函数 (x=‘yes’) => x 是传递给 map() 方法的处理函数。

圆括号问题:

解构很方便,但是解析不容易,对于编译器来说,一个式子到底是表达式还是模式,并不清楚,必须解析到或者解析不到等号才能知道。
所以在ES6中的处理规则是,只要有可能导致解构的歧义,就不能使用圆括号。
但是这样也不是很好分辨,所以只要有可能,就不要在模式中放置圆括号。

不能使用圆括号的情况:

当进行变量声明时:

// 全部报错
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; }
// 报错
function f([z,(x)]) { return x; }

赋值语句的模式也不可以用圆括号:

// 全部报错
({ p: a }) = { p: 42 };
([a]) = [5];

可以使用圆括号的情况:

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

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

上面三行语句都可以正确执行,因为首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是p,而不是d;第三行语句与第一行语句的性质一致。

用途:

  1. 交换变量的值:
let x = 1;
let y = 2;

[x, y] = [y, x];
  1. 函数返回多个值,原来只能放在数组或者对象中:
// 返回一个数组

function example() {
  return [1, 2, 3];
}
let [a, b, c] = example();

// 返回一个对象

function example() {
  return {
    foo: 1,
    bar: 2
  };
}
let { foo, bar } = example();
  1. 函数参数的定义
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);

// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
  1. 提取json数据
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. 函数参数的默认值:
jQuery.ajax = function (url, {
  async = true,
  beforeSend = function () {},
  cache = true,
  complete = function () {},
  crossDomain = false,
  global = true,
  // ... more config
} = {}) {
  // ... do stuff
};
  1. 遍历map
const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');

for (let [key, value] of map) {
  console.log(key + " is " + value);
}
// first is hello
// second is world

如果只想获取键或者值,可以写成下面这样:

// 获取键名
for (let [key] of map) {
  // ...
}

// 获取键值
for (let [,value] of map) {
  // ...
}

字符串的扩展:

字符串的Unicode表示法:

ES6加强了对Unicode的支持,允许采用uxxxx的形式表示一个字符

"u0061"
// "a"

但是,这种表示法只限于码点在u0000~uFFFF之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。

"uD842uDFB7"
// "?"

"u20BB7"
// " 7"

上面代码表示,如果直接在u后面跟上超过0xFFFF的数值(比如u20BB7),JavaScript 会理解成u20BB+7。打印出来就是₻7,ES6 对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符。

"u{20BB7}"
// "?"

"u{41}u{42}u{43}"
// "ABC"

let hello = 123;
hellu{6F} // 123

'u{1F680}' === 'uD83DuDE80'
// true

上面代码中,最后一个例子表明,大括号表示法与四字节的UTF-16编码是等价的。有了这种表示法之后,js一共有以下6中方法可以表示一共字符:

'z' === 'z'  // true
'172' === 'z' // true
'x7A' === 'z' // true
'u007A' === 'z' // true
'u{7A}' === 'z' // true

字符串的遍历器接口

ES6为字符串添加了遍历器接口:

for (let codePoint of 'foo') {
  console.log(codePoint)
}
// "f"
// "o"
// "o"

除了遍历字符串,还可以识别大于0xFFFF的码点:

let text = String.fromCodePoint(0x20BB7);

for (let i = 0; i < text.length; i++) {
  console.log(text[i]);
}
// " "
// " "

for (let i of text) {
  console.log(i);
}
// "?"

如上面的代码,传统的for循环无法识别Unicode转移编码,但是迭代器for of可以

直接输入U+2028和U+2029:

js允许直接输入字符和字符的转移,两者等价:

'中' === 'u4e2d' // true

但是js中有5个字符,不能直接在字符串里使用,只能转义使用:

U+005C:反斜杠(reverse solidus)
U+000D:回车(carriage returnU+2028:行分隔符(line separator)
U+2029:段分隔符(paragraph separator)
U+000A:换行符(line feed)

字符串里边不能直接包含反斜杠,一定要转义写成或者u005c。但是有一个问题,JSON格式允许字符串里面直接使用行分隔符和段分隔符。服务器输出的JSON数据被接卸时,可能直接报错。

但是实际在测试的时候,行分隔符和段分隔符输出了,并没有报错

const json = '"u2028"';
JSON.parse(json); // 可能报错

json.stringfy()的改造:

UTF-8规定,0下D800到0xDFFF之间的码点,不能单独使用(具体而言,这些码点主要用于表示一些特殊的字符集,如一些古代文字、表意文字扩展区(Ideographic Description Characters)、表情符号等。这些字符无法用BMP中的16位编码表示,因此需要使用辅助平面字符来表示它们。)

JSON.stringify()的问题在于,它可能返回0xD800到0xDFFF之间的单个码点。

为了确保返回的是合法的 UTF-8 字符,ES2019 改变了JSON.stringify()的行为。如果遇到0xD800到0xDFFF之间的单个码点,或者不存在的配对形式,它会返回转义字符串,留给应用自己决定下一步的处理。

JSON.stringify('u{D834}') // ""\uD834""
JSON.stringify('uDF06uD834') // ""\udf06\ud834""

模板字符串:

传统js的输出模板:

$('#result').append(
  'There are <b>' + basket.count + '</b> ' +
  'items in your basket, ' +
  '<em>' + basket.onSale +
  '</em> are on sale!'
);

上面这种写法相当繁琐不方便,ES6 引入了模板字符串解决这个问题。

$('#result').append(`
  There are <b>${basket.count}</b> items
   in your basket, <em>${basket.onSale}</em>
  are on sale!
`);

相比于传统的字符串拼接使用+和对象.属性的方式,模板字符串使用了反引号 ` 和占位符${}来代替

// 普通字符串
`In JavaScript 'n' is a line-feed.`

// 多行字符串
`In JavaScript this is
 not legal.`

console.log(`string text line 1
string text line 2`);

// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

如果需要打印反引号 ` ,可以使用反斜杠 来进行转义。