let/const、解构赋值、字符串、数值扩展
一、let && const
1、let 存在块级作用域,仅在当前代码块内有效;
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
2、let不存在变量提升,即在使用let声明变量前,使用该变量会报错,如下所示:
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
上述代码中,变量foo
用var
命令声明,会发生变量提升,即脚本开始运行时,变量foo
已经存在了,但是没有值,所以会输出undefined
。变量bar
用let
命令声明,不会发生变量提升。这表示在声明它之前,变量bar
是不存在的,这时如果用到它,就会抛出一个错误。
3、let 不允许重复声明(相同作用域内)
// 报错
function func() {
let a = 10;
var a = 1;
}
// 报错
function func() {
let a = 10;
let a = 1;
}
4、ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域
// 第一种写法,报错
if (true) let x = 1;
// 第二种写法,不报错
if (true) {
let x = 1;
}
上面代码中,第一种写法没有大括号,所以不存在块级作用域,而let
只能出现在当前作用域的顶层,所以报错。第二种写法有大括号,所以块级作用域成立。
5、const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。所以,使用const
声明的数组、对象是可以继续push或者修改属性的,除非对本对象重新赋值(这是不允许的)。
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only
6、如果真的想将对象冻结,应该使用Object.freeze方法。
const foo = Object.freeze({});
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;
上面代码中,常量foo
指向一个冻结的对象,所以添加新属性不起作用,严格模式时还会报错。
除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
7.暂时性死区
只要块级作用域内存在let
命令,它所声明的变量就“绑定”(binding
)这个区域,不再受外部的影响。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
总之,在代码块内,使用let
命令声明变量之前,该变量都是不可用的。
二、变量的解构赋值
1、多变量赋值。
let [a, b, c] = [1, 2, 3];
上面代码表示,可以从数组中提取值,按照对应位置,对变量a
,b
,c
赋值。
本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [ , , third] = ["foo", "bar", "baz"];
third // "baz"
let [x, , y] = [1, 2, 3];
x // 1
y // 3
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
如果解构不成功,变量的值就等于undefined
。
借助结构赋值,可以实现变量值的互换,如下所示:
let x = 1;
let y = 2;
[x, y] = [y, x];
如果等号的右边不是数组,那么将会报错。
// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
2、默认值
解构赋值允许指定默认值。
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
。
3、对象的解构赋值
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
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'
借助对象的结构赋值,可以用来提取对象中属性值,比如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]
上面代码可以快速提取 JSON
数据的值。
默认值:
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"
默认值生效的条件是,对象的属性值严格等于undefined
。
var {x = 3} = {x: undefined};
x // 3
var {x = 3} = {x: null};
x // null
4、字符串的解构赋值
字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
类似数组的对象都有一个length
属性,因此还可以对这个属性解构赋值。
let {length : len} = 'hello';
len // 5
三、字符串新增常用方法
1、实例方法:includes(), startsWith(), endsWith();
– includes():返回布尔值,表示是否找到了参数字符串。
– startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
– endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
let s = 'Hello world!';
s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true
这三个方法都支持第二个参数,表示开始搜索的位置。
let s = 'Hello world!';
s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false
上面代码表示,使用第二个参数n
时,endsWith
的行为与其他两个方法有所不同。它针对前n
个字符,而其他两个方法针对从第n
个位置直到字符串结束。
2、实例方法:repeat()
repeat
方法返回一个新字符串,表示将原字符串重复n
次。
参数如果是小数,会被取整。
如果repeat
的参数是负数或者Infinity
,会报错。但是,如果参数是 0
到-1
之间的小数,则等同于 0
,这是因为会先进行取整运算。0
到-1
之间的小数,取整以后等于-0
,repeat
视同为 0
。
参数NaN
等同于 0
。
如果repeat的参数是字符串,则会先转换成数字。
'x'.repeat(3) // "xxx"
'na'.repeat(2.9) // "nana"
'na'.repeat(Infinity) // RangeError
'na'.repeat(-1) // RangeError
'na'.repeat(-0.9) // ""
'na'.repeat(NaN) // ""
'na'.repeat('na') // ""
'na'.repeat('3') // "nanana"
3、实例方法:padStart(), padEnd()
padStart()
用于头部补全,padEnd()
用于尾部补全。
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'
如果原字符串的长度,等于或大于最大长度,则字符串补全不生效,返回原字符串。
'xxx'.padStart(2, 'ab') // 'xxx'
'xxx'.padEnd(2, 'ab') // 'xxx'
如果用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位数的补全字符串
'abc'.padStart(10, '0123456789') // '0123456abc'
如果省略第二个参数,默认使用空格补全长度。
'x'.padStart(4) // ' x'
'x'.padEnd(4) // 'x '
padStart()的常见用途是为数值补全指定位数。下面代码生成 10 位的数值字符串。
'1'.padStart(10, '0') // "0000000001"
'12'.padStart(10, '0') // "0000000012"
'123456'.padStart(10, '0') // "0000123456"
4、实例方法:replaceAll()
不同于replace()
方法,如果不使用正则表达式,则replace()
方法默认只替换第一个匹配项。
'aabbcc'.replace('b', '_') // 'aa_bcc'
'aabbcc'.replace(/b/g, '_') // 'aa__cc'
ES2021
引入了replaceAll()
方法,可以一次性替换所有匹配。
'aabbcc'.replaceAll('b', '_') // 'aa__cc'
注意,如果replaceAll()
第一个参数使用正则表达式,则必须是一个全局的正则表达式(带有g
修饰符),否则会报错。
// 不报错
'aabbcc'.replace(/b/, '_')
// 报错
'aabbcc'.replaceAll(/b/, '_')
replaceAll()
的第二个参数replacement
是一个字符串,表示替换的文本,其中可以使用一些特殊字符串。
– $&:匹配的字符串。
– $` :匹配结果前面的文本。
– $':匹配结果后面的文本。
– $n:匹配成功的第n组内容,n是从1开始的自然数。这个参数生效的前提是,第一个参数必须是正则表达式。
– $$:指代美元符号 $。
// $& 表示匹配的字符串,即`b`本身
// 所以返回结果与原字符串一致
'abbc'.replaceAll('b', '$&')
// 'abbc'
// $` 表示匹配结果之前的字符串
// 对于第一个`b`,$` 指代`a`
// 对于第二个`b`,$` 指代`ab`
'abbc'.replaceAll('b', '$`')
// 'aaabc'
// $' 表示匹配结果之后的字符串
// 对于第一个`b`,$' 指代`bc`
// 对于第二个`b`,$' 指代`c`
'abbc'.replaceAll('b', `$'`)
// 'abccc'
// $1 表示正则表达式的第一个组匹配,指代`ab`
// $2 表示正则表达式的第二个组匹配,指代`bc`
'abbc'.replaceAll(/(ab)(bc)/g, '$2$1')
// 'bcab'
// $$ 指代 $
'abc'.replaceAll('b', '$$')
// 'a$c'
四、数值的扩展
1.Number.EPSILON
ES6
在Number
对象上面,新增一个极小的常量Number.EPSILON
。根据规格,它表示 1
与大于 1
的最小浮点数之间的差。
对于 64
位浮点数来说,大于 1
的最小浮点数相当于二进制的1.00..001
,小数点后面有连续 51
个零。这个值减去 1
之后,就等于 2
的 -52
次方。
Number.EPSILON
实际上是 JavaScript
能够表示的最小精度。引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。比如js
存在精度问题,可以使用该固定值设置误差范围,范围内的数值可以认为相等。
function withinErrorMargin (left, right) {
return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);
}
0.1 + 0.2 === 0.3 // false
withinErrorMargin(0.1 + 0.2, 0.3) // true
1.1 + 1.3 === 2.4 // false
withinErrorMargin(1.1 + 1.3, 2.4) // true
上面的代码为浮点数运算,部署了一个误差检查函数。误差范围内可以认为相等。
2、BigInt 数据类型
ES2020
引入了一种新的数据类型 BigInt
(大整数)。BigInt
只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。
为了与 Number
类型区别,BigInt
类型的数据必须添加后缀n
。
1234 // 普通整数
1234n // BigInt
// BigInt 的运算
1n + 2n // 3n
BigInt 与普通整数是两种值,它们之间并不相等。
参考文档:
ES6入门教程(阮一峰)-- let 和 const 命令、变量的解构赋值、字符串的扩展、字符串的新增方法、正则的扩展、数值的扩展