介绍
本文是 JavaScript 高级深入浅出系列的第十二篇,将介绍关于 ES6 中的扩展语法(除 class 外,class 在上一篇)
正文
1. 对象字面量的增强写法
var message = 'abc'
var age = 20
var foo = {
message: message,
age: age,
bar: function () {},
}
foo[message + '123'] = '这是 abc123 对应的值'
在 ES6+ 中,也可以直接这样写,等同于上面的形式
var message = 'abc'
var age = 20
var foo = {
// property shorthand,属性的简写
message,
age,
// method shorthand,方法的简写
bar() {},
// computed property name 计算属性名,可以将某个变量对应的值作为键
[message + "123"]: '这是 abc123 对应的值',
}
console.log(foo.abc123) // 这是 abc 对应的值
2. 解构
解构(Destructuring),是 ES6 中新增的从数组或对象中获取数据的方法
可以分为数组的解构和对象的解构
2.1 数组的解构
var arr = [1, 2, 3]
var item1 = arr[0]
var item2 = arr[1]
var item3 = arr[2]
使用解构可以这样写 👇
// 使用解构可以这样写
var arr = [1, 2, 3]
var [item1, item2, item3] = arr
如果想解构指定位置的元素:
var arr = [1, 2, 3]
// 只想获得第一个和第三个元素,第二个就可以空出来
var [item1, , item3] = arr
解构中的特殊语法
var arr = [1, 2, 3]
// itema 放第一个元素, itemb 将剩下的值放入到一个数组中
var [itema, ...itemb] = arr
console.log(itemb) // [2, 3]
解构的默认值是 undefined,但是可以通过=来给定默认值
var arr = [1, 2]
var [item1, item2, item3 = 'aa', item4] = arr
console.log(item1, item2, item3, item4) // 1, 2, aa, undefined
2.2 对象的解构
var obj = {
message: 'obj',
greeting() {
console.log('hello world')
},
}
var message = obj.message
var greeting = obj.greeting
使用解构可以这样写 👇 和数组解构不同的是,数组解构是按照顺序即索引进行解构的,所以可以随意写变量名,而对象解构中解构出的变量名需要和对象中的键相同才可以解构出来,否则是undefined
var obj = {
message: 'obj',
greeting() {
console.log('hello world')
},
}
var { message, greeting, hello } = obj
console.log(hello) // undefined
可以:指定解构出来的名字
var obj = {
message: 'hello',
}
// 通过冒号来指定解构的名字
var { message: myMessage } = obj
console.log(myMessage) // hello
可以给定默认值
var obj = {
message: 'obj',
}
var { message, address = '北京市', gender } = obj
// obj 北京市 undefined
console.log(message, address, gender)
2.3 解构的应用场景
function foo(obj) {
// obj.name obj.age
}
foo({ name: 'alex', age: 19 })
使用解构 👇
function foo({ name, age }) {}
foo({ name: 'alex', age: 19 })
3. let/const
ES6 之前,JS 只有一种声明变量的方式var,但是由于存在变量提升等历史问题,ES6 正式引入了两种存在块级作用域的变量声明操作符let、const。
let、const关键字其他的语言也有,所以不是什么新鲜东西- 但是这两个确实给 JS 的变量声明带来了大量的提升
3.1 基本使用
// let 和 var 声明变量的方式是相同的,可以更改值
var v1 = 'abc'
let v2 = 'bca'
// const 声明一个常量,该常量不可更改值
const v3 = 'zzz'
v3 = 'abc' // 会报错
const本质上是传递的值不可以修改,但是如果传递的是一个引用类型,是可以修改的
const obj = {
message: 'obj',
}
obj.message = 'obj2'
console.log(obj.message) // obj2
// 所以 const 仅仅是冻结了该变量对应的内存地址
- 如果想要某个对象不能被修改,需要递归
Object.freeze冻结所有属性
function deepFreeze(obj) {
const allProps = Object.getOwnPropertyNames(obj)
// 同上:var allProps = Object.keys(obj);
allProps.forEach(item => {
if (typeof obj[item] === 'object') {
deepFreeze(obj[item])
}
})
return Object.freeze(obj)
}
let、const不可在同一作用域中定义同名变量
let a = 10
let a = 20 // 报错
{
let a = 10
}
{
let a = 20 // 不同作用域可以
}
3.2 作用域提升
var是有作用域提升现象的:
console.log(message) // undefined
var message = 'hello world'
从结果来看let、const并没有作用域提升现象,但是事实真的如此吗?
console.log(foo) // 报错
console.log(bar) // 报错
let foo = 'foo'
const bar = 'bar'
我们来看一下 ECMA 262 对于 let、const 的描述
The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable's LexicalBinding is evaluated.
变量在其包含的词法环境被实例化时被创建,但在变量的词法绑定被求值之前不能以任何方式访问。
从定义来看,在其执行上下文的词法环境被创建出来时,变量也被创建,只是不能以任何方式访问。而作用域提升则是在其被声明前即可访问,既然无法访问,那么就不算有作用域提升。
3.3 let/const 的存储
在全局作用域中使用var声明一个变量,其实是在window对象中添加一个属性。但是let、const是不会在window中添加属性的。那么变量储存在了哪里?我们回顾一下最新的 ECMA 规范中,对于执行上下文的描述:
第一篇介绍的GO以及执行上下文中的VO则是 ECMAScript 旧版规范,以下为新版规范
Every execution context has an associated VariableEnvironment. Variables and functions decleared in ECMAScript code evaluated in an execution context are added as bindings in that VariableEnvironment's Environment Record. For function code, paramters are also added as bindings to that Environment Record.
每一个执行上下文会关联到一个变量环境(VariableEnvironment)中,在执行代码中变量和函数的声明会作为环境记录(Environment Record)添加到变量环境中。
对于函数来说,参数也会作为环境记录添加到变量函数中。
let、const声明的变量仍然会存储到 VE 中,使用 var 定义的变量仍然会同步到 window 中,但是全局执行上下文的 VE 在最新的规范中已经不再等于 window !!
在 V8 的内部实现中,通过VariableMap的一个 hashmap 来实现对于变量的存储。
window 对象,也是早期规范中的 GO 对象,在最新的实现中其实是浏览器添加的全局对象,并且保持了 window 和 var 变量的同步
3.4 关于块级作用域
在 ES6 之前,JS 只存在两种作用域:全局作用域和函数作用域。
// var 没有块级作用域的概念
{
var foo = 'foo'
}
console.log(foo) // foo
ES6 中存在块级作用域,但是只对于let/const/function/class有效
{
let foo = 'foo'
}
console.log(foo) // 报错
关于块级作用域的 function,不同的浏览器会有不同的实现,大部分的浏览器为了兼容之前的代码,让function没有块级作用域。
if、switch等的流程控制也会生成块级作用域
for、while也会生成块级作用域
4.1 块级作用域的应用
// for 延时打印
for (var index = 0; index < 10; index++) {
setTimeout(() => {
console.log(index)
}, 500)
}
// 结果:500 毫秒后一次性打印了 10 次 10
抛开宏任务微任务不谈,之所以打印了 10 次 10,原因在于 for 块级作用域外面也能访问index的值,如果将其修改为let声明,你就会获得:
for (let index = 0; index < 10; index++) {
setTimeout(() => {
console.log(index)
}, 500)
}
// 结果:500 毫秒后打印 0 - 9
使用 let 声明,相当于每次循环都创建了一个代码块,因此打印的结果是正确的
4.2 暂时性死区
在 ES6 中,还有一个概念是暂时性死区
- 它表达的意思是在一个块级作用域中,使用
let、const声明的变量,在声明之前,变量都是不可被访问的 - 我们把这个现象称之为
temporal dead zone(暂时性死区,TDZ)
var foo = "foo"
if (true) {
console.log(foo) // 无法访问
let foo = "bar"
}
3.5 var、let、const 的选择
来到实际开发的应用中,我们该使用哪种方式来定义变量?
对于 var 的使用:
- 我们需要了解到一个事实,var 所表现出的特殊性:作用于提升、挂载在 window 上、没有块级作用域是历史遗留问题,这也是 JS 在设计角度的缺陷
- 有了 let、const 之后,就避免不写 var
对于 let、const 的使用
- 变量声明请优先使用 const ,提高可读性
- 如果确定该变量的值就是变动的,那么就使用
let
4. 模板字符串
在 ES6 之前,如果想要字符串和变量进行拼接是非常麻烦和丑陋的
const username = 'alex'
const age = 18
console.log('hello, my name is ' + username + ', my age is ' + age)
ES6 提供了模板字符串(template literals),支持在字符串中嵌入变量、表达式,同时也支持多行文本,使用
console.log(`hello, my name is ${username}, my age is ${age}`)
const info = `age double is ${age * 2}`
const msg = `
hello world
my name is
${username}
my age is
${age}
4.1 标签模板字符串使用
模板字符串还有另外的用法:标签模板字符串(Tagged Template Literals)
function foo(m, n) {
console.log(m, n)
}
// 可以直接这样调用函数
foo`22`
// 打印出来是这样的
// [ '22' ] undefined
const world = 'world'
foo`hello ${world}`
// 打印之后是这样的
// [ 'hello ', '' ] world
const username = 'alex'
foo`hello world ${username} haha hahha hah`
// 打印之后是这样的
// [ 'hello world ', ' haha hahha hah' ] alex
因此我们可以判断出来,标签字符串会向函数中传递的参数如下:
- 第一个参数是一个数组,该数组的每一项是独立的字符串(如果遇到
${}会分割) - 后面的参数是每一个
${}表达式的结果 - 这个在
css in js中有用到
例如styled components这个库,就用到了这种方法
const container = styled.div`
background: url(${props => props.bgImage}) center center/6000px;
.banner {
height: 270px;
background-color: red;
}
`
5. 函数的参数默认值
// 如果这两个参数都有用到,但是使用者只传入了一个参数
// 那么就可以使用默认参数来避免报错
function sum(m, n = 0) {
return m + n
}
sum(1) // 没问题
sum(2, 3) // 没问题
通过 babel 转换是这样的
"use strict";
function sum() {
var m = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
var n = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2;
return m + n;
}
5.1 对象参数和默认值以及解构
function foo(obj) {
console.log(obj.name, obj.age)
}
// 这种用到了参数的默认值
function bar({ name, age } = { name: 'alex', age: 18 }) {
console.log(name, age)
}
// 这种也可以,这种是用到了解构的默认值
function baz({ name = 'alex', age = 18 } = {}) {
console.log(name, age)
}
有默认值的形参最好放在最后,因为有默认值的参数表示就是可以不用传的,如果放在前面就会出现问题
5.2 有默认值的函数的 length
我们知道,通过函数.length可以获取到函数参数的个数,但是有默认值的参数不包含在函数的length中
function foo(bar, baz) {}
console.log(foo.length) // 2.
function bar(foo, baz = '') {}
console.log(bar.length) // 1
6. 函数的剩余参数
ES6 引入了 rest parameter,可以将不定数量的参数放入到一个数组中:
- 如果函数的最后一个参数是
...作为前缀的,剩余的参数将会放到这个参数中,并且作为一个数组
function foo(bar, ...baz) {
console.log(bar, baz)
}
// 1, [2, 3, 4, 5, 6, 7, 8, 9]
foo(1, 2, 3, 4, 5, 6, 7, 8, 9)
剩余参数和 arguments 的区别:
- 剩余参数只包含没有对应形参的实参,而 arguments 则是所有的实参
- arguements 对象不是一个真正的数组,而 rest 参数则是一个真正的数组,可以进行数组的相关操作
- arguments 是早期 ECMAScript 中为了方便获取所有参数提供的一个真正的数组,而 rest 参数则是 ES6 中想要代替 arguments 的
剩余参数必须是最后一个参数,否则会报错
7. 箭头函数的补充
箭头函数没有this,更准确的来说,是共享外部作用域的this
function foo() {
return () => {
console.log(this.username)
}
}
const obj = {
username: 'obj',
}
const log = foo.bind(obj)()
log()
箭头函数没有显式原型,无法作为构造函数
function bar() {}
console.log(bar.prototype) // {}
new bar()
const foo = () => {}
new foo() // 报错
console.log(foo.prototype) // undefined
8. 展开语法
展开语法(Spread Syntax)
- 可以在函数调用/数组构造时,将数组表达式或者 string 在语法层面展开
- 还可以在构造字面量对象时,将对象表达式按
key-value的方式展开
展开语法使用场景:
- 在函数调用时使用
- 在数组构造时使用
- 在构建对象字面量时,也可以使用展开运算符,这是 ES2018 中添加的新特性
const names = ['Alex', 'John', 'Tom']
const username = 'Jason'
// 1. 函数调用时
function foo(x, y, z) {
console.log(x, y, z)
}
// 使用展开运算符
foo(...names)
// 通过 apply 的方式也可以
foo.apply(null, names)
// 2. 构造数组时
const newNames = [...names, username]
console.log(newNames) // [ 'Alex', 'John', 'Tom', 'Jason' ]
// 3. 构建对象时 ES2018(ES9)
const info = {
name: 'alex',
age: 18,
greeting() {},
}
// 只是浅拷贝
const newInfo = { ...info }
console.log(newInfo)
9. 数值的表示
在 ES6 中规范了二进制和八进制的写法:
const num1 = 100 // 十进制
const num2 = 0b100 // 二进制
const num3 = 0o100 // 八进制
const num4 = 0x100 // 十六进制
// 100 4 64 256
console.log(num1, num2, num3, num4)
// ES2021 大的数值连接符
const num5 = 1_000_000_000_000_000
10. Symbol 的基本使用
Symbol 是 ES6 中新增的一个基本数据类型
为什么需要 Symbol:
- 在 ES6 之前,对象的属性名都是字符串,很容易就会造成属性名的冲突
- 比如如果想要对某个对象进行扩展,在不知道该对象有哪些属性的情况下,就容易会会造成覆盖
Symbol 就是用于解决以上的场景的,Symbol 生成一个独一无二的值:
- Symbol 值是通过 Symbol 函数来生成的,生成后可作为属性名
- 也就是在 ES6 中,对象的属性名除了可以使用字符串,还可以使用 Symbol 值
const s1 = Symbol()
const s2 = Symbol()
console.log(s1 === s2) // false
在 ES2019 中,Symbol 还可以传入一个参数,该参数是该 Symbol 的描述(description)
const s1 = Symbol('aa')
console.log(s1, s1.description)
10.1 Symbol 作为对象的 key
Symbol 值作为 key
const s1 = Symbol('username')
const s2 = Symbol('age')
const s3 = Symbol('gender')
const obj = {
[s1]: 'aaa',
}
obj[s2] = 'bbb'
Object.defineProperty(obj, s3, {
writable: true,
configurable: true,
enumerable: true,
value: 'ccc',
})
获取 Symbol key 的值
// 不可使用 obj.s1 的方式
console.log(obj[s1], obj[s2], obj[s3])
注意:使用 Symbol 作为 key,在部分情况下是获取不到的,例如 遍历、Object.keys() 等
通过 Object.getOwnPropertySymbols() 来获取,使用以下的方式来遍历:
for (const key of Object.getOwnPropertySymbols(obj)) {
console.log(obj[key])
}
10.2 创建相同的 Symbol
使用Symbol.for()可以创建一个 Symbol 的 key,如果两个 Symbol 的 key 相同,就是相同的
const s1 = Symbol.for('aaa')
const s2 = Symbol.for('aaa')
console.log(s1 === s2) // true
10.3 获取 Symbol 的 key
const s2 = Symbol.for('aaa')
console.log(Symbol.keyFor(s2)) //aaa
总结
本文作为介绍 ES6-7 的扩展语法的上篇,主要介绍了 ES6 的扩展语法,包括了:
- 对象字面量的增强写法
- 解构语法
- let/const
- 模板字符串
- 函数的参数默认值
- 函数的剩余参数
- 箭头函数补充只是
- 展开语法
- 不同进制的数值表示
- Symbol 的基本使用
下篇,将会继续介绍剩余的 ES6-7 的语法
特别鸣谢:
- 催学社(崔大,cuixiaorui)