let和const命令:
let命令:
基本用法:
ES6中增加了let命令, 用来声明局部变量。用法跟var类似,但是所声明的变量,只在let命令所在的代码块中有效。
java
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
上面代码在代码块之中,分别用let和var声明两个变量。然后在代码块之外调用这两个变量,结果
let声明的变量报错,var声明的变量返回了正确的值。let变量只在所在代码块有效。
for循环的计数器,使用let命令比较合适。
java
for (let i = 0; i < 10; i++) {
console.log(i);
}
在上面代码中,i只在for循环内生效,在for循环之外,i变量会失效
java
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变量也会报错,不知道是不是已经修正了
java
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
暂时性死区:
只要块级作用域内存在let命令,它所声明的变量就绑定这个区域,不再受外部影响。
java
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
不允许重复声明:
java
// 报错
function func() {
let a = 10;
var a = 1;
}
// 报错
function func() {
let a = 10;
let a = 1;
}
块级作用域:
内部变量可能会覆盖全局变量:
java
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined
在上面的代码中,内层tmp覆盖了外层tmp
或者内部变量泄露成为全局变量:
java
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5
ES6的块级作用域:
java
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
const命令:
const表明生成一个常量,只要赋值,就不可再改变,且const一旦声明变量,就必须进行初始化,不能留到以后赋值。const的作用域与let相同,只在块级作用域内有效。const常量不存在提升,存在暂时性死区。
java
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
数组的解构和赋值:
基本用法:
原来的形式:
java
let a = 1;
let b = 2;
let c = 3;
现在新的形式:
java
let [a, b, c] = [1, 2, 3];
如果解构不成功,赋值undefined,以下foo的值都会等于undefined
java
let [foo] = [];
let [bar, foo] = [1];
当不完全解构时,即等号左边的模式,只匹配一部分的等号右边的数组,解构也会成功。
java
let [x, y] = [1, 2, 3];
x // 1
y // 2
let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
像下面这种对数组进行非数组形式赋值时也会失败:
java
// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
事实上,只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。
默认值
解构时可以设置默认值
java
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,默认值才会生效。
java
let [x = 1] = [undefined];
x // 1
let [x = 1] = [null];
x // null
如果数组成员是null,则不会生效
如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。
对象的解构赋值
java
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
数组解构必须排序,但是对象解构可以根据名字解构,顺序不影响:
java
let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined
如果对象解构失败,赋值undefined
java
let {foo} = {bar: 'baz'};
foo // undefined
对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。
java
// 例一
let { log, sin, cos } = Math;
// 例二
const { log } = console;
log('hello') // hello
对象解构的默认值:
java
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"
注意点
当变量已经声明时,在进行解构:
java
// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error
在上面代码中,js会将{x}理解成一个代码块,从而发生语法错误,要避免将大括号写在行首,正确的写法如下:
java
// 正确的写法
let x;
({x} = {x: 1});
解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。
java
({} = [true, false]);
({} = 'abc');
({} = []);
上面的表达式虽然毫无意义,但是语法是合法的,可以执行。
因为数组本身也是对象,可以对数组进行对象的解构:
java
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3
字符串也可以进行解构赋值:
java
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
数值和布尔值的解构赋值:
解构赋值时,如果等号右边是数值和布尔值,则会先转化成对象。
java
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
上面代码中,数值和布尔值的包装对象都有toString属性,因此变量s都能取到值。
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。
java
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
函数的参数也可以进行解构赋值:
java
function add([x, y]){
return x + y;
}
add([1, 2]); // 3
在上面代码中,函数add的参数表面上是一个数组,但是传入参数时,数组参数就被解构成变量x和y。对于函数内部的代码来说,他们能感受到的就是参数x和y,如下:
java
[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]
函数参数的解构也可以使用默认值:
java
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等于默认值。但是,下面的写法会得到不一样的结果:
java
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]
因此,这两个函数的区别在于对解构赋值语法中的默认值的使用和位置。第一个函数在解构赋值中为每个属性设置了默认值,而第二个函数只在整个解构赋值表达式失败时才会使用默认值。
java
[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]
在这个例子中,我们使用了数组的 map() 方法来对数组的每个元素进行处理并生成一个新的数组。箭头函数 (x='yes') => x 是传递给 map() 方法的处理函数。
圆括号问题:
解构很方便,但是解析不容易,对于编译器来说,一个式子到底是表达式还是模式,并不清楚,必须解析到或者解析不到等号才能知道。
所以在ES6中的处理规则是,只要有可能导致解构的歧义,就不能使用圆括号。
但是这样也不是很好分辨,所以只要有可能,就不要在模式中放置圆括号。
不能使用圆括号的情况:
当进行变量声明时:
java
// 全部报错
let [(a)] = [1];
let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};
let { o: ({ p: p }) } = { o: { p: 2 } };
函数的参数也不能使用:
java
// 报错
function f([(z)]) { return z; }
// 报错
function f([z,(x)]) { return x; }
赋值语句的模式也不可以用圆括号:
java
// 全部报错
({ p: a }) = { p: 42 };
([a]) = [5];
可以使用圆括号的情况:
可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号。
java
[(b)] = [3]; // 正确
({ p: (d) } = {}); // 正确
[(parseInt.prop)] = [3]; // 正确
上面三行语句都可以正确执行,因为首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是p,而不是d;第三行语句与第一行语句的性质一致。
用途:
- 交换变量的值:
java
let x = 1;
let y = 2;
[x, y] = [y, x];
- 函数返回多个值,原来只能放在数组或者对象中:
java
// 返回一个数组
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();
// 返回一个对象
function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();
- 函数参数的定义
java
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
- 提取json数据
java
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
console.log(id, status, number);
// 42, "OK", [867, 5309]
- 函数参数的默认值:
java
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
} = {}) {
// ... do stuff
};
- 遍历map
java
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
如果只想获取键或者值,可以写成下面这样:
java
// 获取键名
for (let [key] of map) {
// ...
}
// 获取键值
for (let [,value] of map) {
// ...
}
字符串的扩展:
字符串的Unicode表示法:
ES6加强了对Unicode的支持,允许采用\uxxxx的形式表示一个字符
java
"\u0061"
// "a"
但是,这种表示法只限于码点在\u0000~\uFFFF之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。
java
"\uD842\uDFB7"
// "𠮷"
"\u20BB7"
// " 7"
上面代码表示,如果直接在\u后面跟上超过0xFFFF的数值(比如\u20BB7),JavaScript 会理解成\u20BB+7。打印出来就是₻7,ES6 对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符。
java
"\u{20BB7}"
// "𠮷"
"\u{41}\u{42}\u{43}"
// "ABC"
let hello = 123;
hell\u{6F} // 123
'\u{1F680}' === '\uD83D\uDE80'
// true
上面代码中,最后一个例子表明,大括号表示法与四字节的UTF-16编码是等价的。有了这种表示法之后,js一共有以下6中方法可以表示一共字符:
java
'\z' === 'z' // true
'\172' === 'z' // true
'\x7A' === 'z' // true
'\u007A' === 'z' // true
'\u{7A}' === 'z' // true
字符串的遍历器接口
ES6为字符串添加了遍历器接口:
java
for (let codePoint of 'foo') {
console.log(codePoint)
}
// "f"
// "o"
// "o"
除了遍历字符串,还可以识别大于0xFFFF的码点:
java
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允许直接输入字符和字符的转移,两者等价:
java
'中' === '\u4e2d' // true
但是js中有5个字符,不能直接在字符串里使用,只能转义使用:
java
U+005C:反斜杠(reverse solidus)
U+000D:回车(carriage return)
U+2028:行分隔符(line separator)
U+2029:段分隔符(paragraph separator)
U+000A:换行符(line feed)
字符串里边不能直接包含反斜杠,一定要转义写成\或者\u005c。但是有一个问题,JSON格式允许字符串里面直接使用行分隔符和段分隔符。服务器输出的JSON数据被接卸时,可能直接报错。
但是实际在测试的时候,行分隔符和段分隔符输出了,并没有报错
java
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之间的单个码点,或者不存在的配对形式,它会返回转义字符串,留给应用自己决定下一步的处理。
java
JSON.stringify('\u{D834}') // ""\\uD834""
JSON.stringify('\uDF06\uD834') // ""\\udf06\\ud834""
模板字符串:
传统js的输出模板:
java
$('#result').append(
'There are <b>' + basket.count + '</b> ' +
'items in your basket, ' +
'<em>' + basket.onSale +
'</em> are on sale!'
);
上面这种写法相当繁琐不方便,ES6 引入了模板字符串解决这个问题。
java
$('#result').append(`
There are <b>${basket.count}</b> items
in your basket, <em>${basket.onSale}</em>
are on sale!
`);
相比于传统的字符串拼接使用+和对象.属性的方式,模板字符串使用了反引号 ` 和占位符${}来代替
java
// 普通字符串
`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}?`
如果需要打印反引号 ` ,可以使用反斜杠 \ 来进行转义。