ES6细节笔记(一)
在已经使用过es6中比较实用的特性,如:Promise、模板字符串、Class、结构赋值等之后,可能并未对某些功能的细节进行深究,然而其实这些细节也同样使得JavaScript越来越好了。
ES6的特性,本文中的例子可以来这里看看转成ES5的实现。
0x00 关于变量的作用域(let、const、var、function)
es5语法中可以通过var
和function
的方式声明变量,使用它们声明的变量会再其作用域中发生变量提升,其作用域也只分为函数作用域和全局作用域,用立即执行函数生成闭包。例如:
普通变量声明
var a = 0
function b() { ... }
var c = 1
其实等价于
var b = function () { ... }
var a, c // undefined
/* ---- 变量提升的分割线 ---- */
a = 0
c = 1
变量改动
b()
console.log(a) // 不报错 10
console.log(c) // 不报错 undefined
console.log(d) // 报错 Error: d is not defined
var a, c
function b() {
a = 10
}
其实等价于
var b = function() {
a = 10
}
var a, c // undefined
/* ---- 变量提升的分割线 ---- */
b()
console.log(a) // 不报错 10
console.log(c) // 不报错 undefined
console.log(d) // 报错 Error: d is not defined
局部变量声明
下面几种情况造成不同的结果很有意思:
赋值
var a = 1
function b () {
a = 10 // 外围作用域的变量a赋值
console.log(a) // 10
}
b()
console.log(a) // 10
声明局部变量
var a = 1
function b () {
var a = 10 // 声明局部变量,只在当前作用域范围内生效
console.log(a) // 10
}
b()
console.log(a) // 1
声明局部变量
var a = 1
function b () {
a = 10
console.log(a) // 10
return
function a () {}
}
b()
console.log(a) // 1
大家可能对这个结果有些疑惑,原因是它其实等价于:
var a = 1
function b () {
var a = function () {} // 声明局部变量
/* ---- 变量提升的分割线 ---- */
a = 10
console.log(a) // 10
return
}
b()
console.log(a) // 1
立即执行函数闭包
(function() {
console.log(a) // 不报错 undefined
var a
})()
console.log(a) // 报错 Error: a is not defined
灵活是灵活,但是确认很多代码看起来不够直观。
另外,上面被函数中大括号{...}
包含在内的区域是一个作用域,而再条件语句if(...){...}
或者循环语句for(...) {...}
等等中则不是。
因此,在es6中,新增了let
和const
以及块级作用域来让代码更易于逻辑梳理,也有一些前端用起来不习惯的地方,也产生了许多新的特性,例如:TDZ (Temporal Dead Zone 暂时性死区)。
let
和const
相比var
和function
有哪些不同?
三点:
- 变量提升没了
- 不允许(在同一个作用域下)重复声明
- 作用域为块级作用域
关于这三点可以看下面几个例子:
console.log(a) // 报错 Error: a is not defined
let a
a = 1
let a = 1
let a = 2 // 报错
关于作用域,看var
命令与let
命令的区别:
var a = 0
function b() {
console.log(a)
if (false) {
var a = 2
}
}
b() // 不报错 undefined
虽然if
中的语句没有执行,但是对于var
命令来说,if
语句所处的是在函数b
的作用域中,加上变量提升,其等价于:
var b = function() {
var a
/* ---- 变量提升的分割线 ---- */
console.log(a)
if (false) {
a = 2
}
}
var a
/* ---- 变量提升的分割线 ---- */
a = 0
b() // 不报错 undefined
可以看到if
语句中的变量a
实际上是被声明到了函数b
中。而如果使用let
命令进行声明的话:
let a = 0
function b() {
console.log(a)
if (false) {
let a = 2
}
}
b() // 0
因为,let
命令声明的变量其作用域是块级作用域,即if
中对a
的声明只在if
中生效。当执行到console.log(a)
时,a
是外层作用域中声明的那个,因此是0
。
为什么要用到块级作用域呢?
因为按照之前的函数作用域以及全局作用域会有一些不合理的情况存在(虽然对于前端来说已经适应并且习惯了),例如:
for (var i = 0; i < 10; i++) {
setTimeout(function(){
console.log(i)
}, 100) // 模拟异步的情况,例如ajax
}
// 10
// ... x 10
我们期待是从0到9,可是最后却得到了10个10,如果想得到期待的结果,还得吧循环块级中的内容变成函数,太不直观。
如果使用let
就可以简洁的解决这个问题:
for (let i = 0; i < 10; i++) {
setTimeout(function(){
console.log(i)
}, 100) // 模拟异步的情况,例如ajax
}
// 0
// ...
// 9
因为i
只会在for
循环的块中生效。同理,还会有下面这些变化:
var a = 1;
if(true) {
var a = 2
}
console.log(a) // 2
let a = 1;
if(true) {
let a = 2
}
console.log(a) // 1
甚至可以直接这样生成闭包:
{
let a = new Date()
...
}
但是,其实let
也并不是严格的在声明时才在作用域中产生此变量,如果是这样:
let a = 0
{
console.log(a) // 报错:暂时性死区TDZ
let a = 1
}
上面的代码就不应该报错,而应该显示0
。说明其实作用域中的a
实际上在声明前是已经存在了的,只是因为let
声明的缘故而无法取得它的值,上面这个现象就叫做『暂时性死区』(TDZ, Temporal Dead Zone):变量只要再作用域中存在let
声明,它就在这个作用域中不受外部的影响,但只有在声明语句之后才能使用,否则就会报错。
let
和const
的区别就只有一句话:const
用来声明常量。
0x01 解构赋值的圆括号
JavaScript中被大括号括起来的区域{...}
可能有多种含义:
- 代码块
- 对象
两种含义的形式不一样,可以在读取代码时便抛出错误,而在ES6中又多了一种情况:
- 解构赋值中的模式
JavaScript只能在执行中对其进行区分了。
我们先回顾一下ES6中的解构赋值
// 数组的解构赋值
let [a, b] = [1, 2, 3]
a === 1 // true
b === 2 // true
// 对象的解构赋值
let { log, sin, cos } = Math
log === Math.log // true
sin === Math.sin // true
cos === Math.cos // true
以下这种方式汇报错:
var a
{a} = {a: 1}
相当于:
var a
{a: a} = {a: 1}
但是编译器没办法知道左边的是『模式』还是『对象』,所以汇报错,所以要告诉编译器这个是『模式』
var a
({a: a} = {a: 1})
a === 1 // true
0x02 标签模板
很多人已经知道ES6中可以使用这样的字符串模板,让字符串拼接更加简单:
let w = 'world'
let a = `Hello ${w}` // Hello world
却不知还有这样的使用方式:
function c() {...}
let a = c`Hello ${1} World`
标签模板的形式
就是再字符串模板前面加一个处理的函数,这个函数的格式是
function tag(Array<String> stringArr, String value1, String value2, ...) {}
上面的标签函数对于如下例子来说。
tag`example string ${1} plus ${2} = ${1 + 2}`
参数一一对应的关系是:
即,如果tag
具体函数如下:
function tag (arr, ...values) {
console.log(arr)
console.log(values)
}
其打印结果为:
['example string ', ' plus ', ' = ', '']
[1, 2, 3]
标签模板函数
我们知道:
`1 + 1 = ${1 + 1}`
可以得到字符串'1 + 1 = 2'
,其如果通过标签模板函数实现则为:
function c (arr, ...values) {
return arr[0] // '1 + 1 = '
+ values[0] // 1 + 1
}
c`1 + 1 = ${1 + 1}` // '1 + 1 = 2'
则可的到同样的结果,更通用的实现为:
function c(arr, ...values) {
var output, index
output = ''
for (index = 0; index < values.length; index++) {
output += arr[index] + values[index]
}
output += arr[index]
return output
}
c`1 + 1 = ${1 + 1}` // '1 + 1 = 2'
用法总结
通过标签模板函数,我们可以对字符串模板的各个部分进行处理并的到新的结果,例如:
- 对字符串模板的结果过滤html字符串,方式用户输入恶意内容:
function saveHTML(arr, ...values) {
// 替换<、>、&等可能造成xss的危险HTML标签
}
saveHTML`<xss>&${'xss'}</xss>`
- 做
jsx
解析
function jsx(arr, ...values) {
// 将html标签替换成React.createElement的形式
}
class Demo extends React.Component {
render() {
return jsx`
<div></div>
`
}
}
等等,多重妙用。
0x03 数值的扩展
很多,直接列举方便使用时查看:
0b111110111 === 503 // 二进制 true
0o767 === 503 // 八进制 true
Number('0b111110111') // 转为10进制
Number.isFinite(5) // 是否为有限数 true
Number.isFinite(Infinity) // 是否为有限数 false
Number.isFinite(NaN) // 是否为有限数 false
Number.isFinite('15') // 强类型 false
Number.isFinite(true) // 强类型 false
// 只有是数,并且不是无穷数才是true
Number.isNaN(NaN) // true
Number.parseInt('12.34') // 12
Number.parseFloat('12.34') // 12.34
Number.isInteger(25) // true
Number.isInteger(25.0) // true
Number.isInteger(25.1) // false
Number.isInteger('25') // false
Number.isInteger(true) // false
Number.EPSILON // 极小常量,判断不靠谱的浮点数加减后是否相等用的
Number.MAX_SAFE_INTEGER // 最大安全整数,超过这个数就是浮点数表示了,就不准确了
Number.MIN_SAFE_INTEGER // 最小安全整数
Number.isSafeInteger(0) // 判断一个数是否为安全整数,首先入参得是一个整数
// 如果是加减乘除 (例如: a + b = c) ,要判断这个式子是不是准确,要每个数 (a, b, c) 都判断
Math.trunc()
Math.sign()
Math.cbrt()
Math.clz32()
Math.imul()
Math.fround()
Math.hypot()
Math.expm1()
Math.log1p()
Math.log10()
Math.log2()
Math.sinh(x)
Math.cosh(x)
Math.tanh(x)
Math.asinh(x)
Math.acosh(x)
Math.atanh(x)
a ** 3 === a * a * a