总结并列出 ES 5 及之后的新特性
一、理解 ES
全称: ECMAScript (/ˈɛkməskrɪpt/)
它是一种由 ECMA 组织(前身为欧洲计算机制造商协会)制定和发布的脚本语言规范
我们学习的 JavaScript 就是 ECMA 的实现
ES 的几个重要版本
ES 6(ES 2015) :2015 年发布,也称为 ECMA 2015——重点(之后的 ES 就以年份命名)
ES 7(ES 2016):2016 年发布,也称为 ECMA 2016
二、ES 5 新特性
ES 5 对 JS 进行了修修补补,大概可以分为以下几类:
1、严格模式
理解:
严格模式使得 JavaScript 在更严格的语法条件下运行
目的/作用:
消除 Javascript 语法的一些不合理、不严谨之处,减少一些怪异行为
使用:
将全局 或函数 的第一条语句 定义为:"use strict"
如果浏览器不支持,只解析为一条简单的语句,没有任何副作用
语法和行为改变:
声明定义变量必须用 var
(没有隐式的全局变量了,你要创建全局变量必须是显式的)
禁止自定义的函数中的 this
关键字指向全局对象(window
或 global
),不管是 fn()
在全局调用还是 apply()
、call()
默认都不会指向全局对象
arguments
只保存原始参数,对形参的赋值不会对 arguments
有影响,不准用 arguments.caller
和 arguments.callee
不支持八进制字面量,比如 var a = 015
会报错
如果一个属性的 writeable
是 false
,给这个属性赋值会报错
如果一个属性的 configurable
是 false
,则 delete
这个属性会报错
复制 <script>
'use strict'
var username = 'luwang'
// name = 'luwang' 在严格模式下不用 var 声明变量会报错
console.log(username)
function Person(name, age) {
this.name = name
this.age = age
}
new Person('luwang', 23)
// Person('luwang', 23) //没有 new 会报错
var str = 'web'
eval('var str = "HTML"; alert(str)') // HTML
alert(str) // web 即开启严格模式之后不会污染全局作用域
var obj = {
username: 'luwang',
username: 'luwang' // 定义重名了
}
</script>
2、JSON 序列化和反序列化
作用: 用于在 JSON 字符串与 JS 对象/数组相互转换
JSON.stringify(obj/arr)
JS 对象(数组)转换为 JSON 数据(字符串)
JSON.parse(json)
JSON 字符串转换为 JS 对象(数组)
复制 <script>
var obj = { username: 'luwang' }
obj = JSON.stringify(obj)
console.log(typeof obj)
obj = JSON.parse(obj)
console.log(typeof obj)
</script>
3、Object 扩展
ES 5 给 Object 扩展了一些静态方法
Object.keys()
, Object.create()
, Object.defineProperty
, Object.defineProperties
, Object.getOwnPropertyDescriptor()
, Object.getOwnPropertyNames(obj)
,Object.getPrototypeOf(obj)
Object.seal()
, Object.freeze()
, Object.preventExtensions()
, Object.isSealed()
, Object.isFrozen()
, Object.isExtensible()
常用的两个:
create
Object.create(prototype[, descriptors])
:创建一个新的对象,返回值是新对象
为新的对象指定新的属性,并对属性进行描述
writable
:标识当前属性值是否是可修改的,默认为false
configurable
:标识当前属性是否可以被删除,默认为false
enumerable
:标识当前属性是否能用for in枚举,默认为false
复制 var obj = {username: 'luwang', age:23}
var obj1 = {}
obj1 = Object.create(obj, { // obj的属性为obj1的原型
sex: {
value: '男',
writable: true, // 默认false
configurable: true,
enumerable: true
}
})
console.log(obj1.sex)
obj1.sex = 'nan'
console.log(obj1.sex)
delete obj1.sex
console.log(obj1)
for(var i in obj1){
console.log(i)
}
defineProperties
Object.defineProperties(object, descriptors)
:为指定对象定义扩展多个属性
复制 var obj = { username: 'luwang', age:23 }
var obj1 = {}
var obj2 = { firstName: 'lu', lastName: 'wang' }
Object.defineProperties(obj2, {
fullName: { // 此方法在原型中
get: function(){ // 获取扩展属性的值
console.log('get方法被调用')
return this.firstName + ' ' + this.lastName
},
set: function(data){ // 监听扩展属性,当扩展属性发生变化的时候会自动调用,自动调用后会讲变化的值作为实参注入到set函数
console.log('set方法被调用,', data)
var names = data.split(' ') // 根据空格拆分为数组
this.firstName = names[0]
this.lastName = names[1]
}
}
})
console.log(obj2.fullName) // get会自动调用
obj2.fullName = 'lu wang'
console.log(obj2.fullName)
惰性求值:点击才给值(什么时候要什么时候给),会再次调用 get 方法
什么时候调用:
get 方法:获取扩展属性值的时候 get 方法自动调用
存储器属性:setter、getter 一个用来存值,一个用来取值
对象本身也有两个方法:
复制 var obj = {
firstName: 'lu',
lastName: 'wang',
get fullName(){
return this.firstName + ' ' + this.lastName
},
set fullName(data){
var names = data.split(' ')
this.firstName = names[0]
this.lastName = names[1]
}
}
console.log(obj)
obj.fullName = 'lu wang'
console.log(obj.fullName)
4、Array 扩展
Array.isArray(arr)
:判断一个对象 arr
是否是数组
Array.prototype.indexOf(value)
:得到值在数组中的第一个下标
Array.prototype.lastIndexOf(value)
:得到值在数组中的最后一个下标
Array.prototype.forEach(function(item, index, array){}[, asThis])
:遍历数组,返回值是 undefined
Array.prototype.map(function(item, index, array){}[, asThis])
:遍历数组返回一个新的数组(每一项是回调函数的返回值)
Array.prototype.filter(function(item, index, array){}[, asThis])
:遍历过滤出一个子数组,返回一个由条件为 true
的元素组成的新数组
Array.prototype.reduce(function(accumulator, item, index, array)[, initValue])
:Array.prototype.reduce 对数组中的每个元素按序执行一个提供的回调函数,每次执行回调函数会将之前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值
Array.prototype.reduceRight
:从右到左
Array.prototype.some(function(){item, index, array}[, asThis])
:数组中是否至少有一个元素符合回调函数给定的条件,有则返回 true
,没有返回 false
Array.prototype.every(function(item, index, array){}[, asThis])
:判断一个数组内的所有元素是否都能通过指定函数的测试,返回布尔值(回调函数有三个参数,第一个为当前遍历的对象、第二个为当前的下标、第三个为数组本身,方法还接受第二个参数,将作为 this
)
复制 var arr = [2,4,5,1,6,7,4,3,9]
console.log(arr.indexOf(4))
console.log(arr.lastIndexOf(4))
arr.forEach(function(item, index){
console.log(item, index)
})
var arr1 = arr.map(function(item, index){
return item + 10
})
console.log(arr, arr1)
arr.filter(function(item, index){
return item > 3
})
arr.every(function(item) {
return item > 10
}) // false
5、Function 扩展
函数新增 bind
方法,Function.prototype.bind(asThis)
,将函数内的 this
绑定为 asThis
,并将函数返回
强制绑定 this
使用 call
/apply
和 bind
五种函数调用形式
fn
,this
是默认值,undefined
或 window
obj.x.fn()
,this
是前面调用的内容 obj.x
,例如 btn.addEventListener
里面 this
就是 btn
call(asThis, args)
或 apply(asThis, [args])
,this
就是传的 asThis
new fn(args)
,this
就是新创建的对象
复制 var obj = {username: 'luwang'}
function foo(){
console.log(this)
}
foo() // this-->window 全局
// call 和 apply 不传参的时候是一样的
foo.call(obj) // this-->{username: 'luwang'} obj 对象
foo.apply(obj) // this-->{username: 'luwang'} obj 对象
// bind 的特点: 绑定完 this 不会立即调用当前的函数,而是将函数返回
// var bar = foo.bind(obj)
// bar()
foo.bind(obj)()
// 传入参数的形式
var obj1 = {age: 23}
function fun(data){
console.log(this, data)
}
fun(22) // window 22
// call 直接从第二个参数开始,依次传入
fun.call(obj1, 21) // {age: 23} 21
// 第二参数必须是数组,传入放在数组里
fun.apply(obj1, [20]) // {age: 23} 20
// bind 传参的方式同 call 一样
fun.bind(obj1, 18)()
🔖 面试题:区别 bind()
与 call()
和 apply()
fn.bind(asThis, args)
:指定函数中的 this
,并返回函数(不会立即调用),一般用在回调函数绑定其他对象的 this
复制 var obj = {username: 'luwang'}
setTimeout(function(){
console.log(this) // Window
}, 1000)
setTimeout(function(){
console.log(this) // Window
}.bind(obj), 1000)
fn.call(asThis, args)
:指定函数中的 this
,并调用函数
fn.apply(asThis, [args])
:指定函数中的 this
,并调用函数
总结:三个都可以给 fn
指定 this
,bind
不会调用 fn
,call
和 apply
都会调用 fn
,call
的其他参数依次以逗号分隔,apply
的其他参数以数组形式传递
6、Date扩展
Date.now()
:得到当前时间值,之前 new Date()
Date.prototype.toISOString
:新增方法,会返回一个 ISO 格式的字符串( YYYY-MM-DDTHH:mm:ss.sssZ
)
new Date(string)
和 Date.parse(string)
新增对 ISO 格式的支持
7、其他
新增 String.prototype.trim
,去除字串头尾空格
尾逗号不报错,即多余的逗号不会报错,如 {a: 1, b: 2,}
属性名可以使用关键字和保留字了,例如 {if: 1, else: 2}
NaN
、Infinity
、undefined
都是常量了,不可更改
/regexp/
正则字面量每次都会产生一个新的对象
三、ES 6 新特性
ES 6 新增了很多特性,让 JS 变得非常好用
1、2 个声明变量的新关键字
ES 6 中新增了块作用域,{}
包裹的地方就是一个块,ES 5 中没有块级作用域(只有全局和函数作用域)
let 关键字
作用:与 var 相似,用于声明一个变量
特点:
应用:
循环遍历加监听
复制 <br/><button>按钮1</button><br/><br/>
<button>按钮2</button><br/><br/>
<button>按钮3</button>
<script>
var btns = document.getElementsByTagName('button')
for(var i = 0; i < btns.length; i++){
var btn = btns[i]
btn.onclick = function(){
alert(i)
}
}
/*
* 一直会显示3
* 点击事件对应的是回调函数,回调函数又称勾子函数,回调函数会被放到事件队列中,等主线程上的代码执行完毕之后再通过钩子一样的形式,勾出来执行
* 以前的方式是通过闭包,立即执行函数(自己的作用域)
*/
for(var i = 0; i < btns.length; i++){
var btn = btns[i]
;(function(i){ // 声明的形参
btn.onclick = function(){
alert(i)
}
})(i) // 传的实参
}
/*
* 闭包利用的是函数作用域的特点
* 因此可以直接使用let
*/
for(let i = 0; i < btns.length; i++){ // let,在块作用域内有效
var btn = btns[i]
btn.onclick = function(){
alert(i)
}
}
</script>
使用 let 代替 var 是趋势
const 关键字
作用:定义一个常量
特点
应用
保存不用改变的数据
let 和 const 声明的变量都是有块级作用域的
let 和 const 声明变量在块作用域内都有暂时性死区,即在声明之前使用该变量会报错
2、变量(对象)的解构赋值
理解:
数据源:对象/数组
目标:{a, b}
/[a, b]
对象的解构赋值:let {n, a} = {n:'tom', a:12}
把对象中的值赋值出来(根据属性名 key)
数组的解构赋值:let[a, b] = [1, 'luwang']
(根据下标)
用途:数组匹配、对象匹配、参数匹配(给多个形参赋值)
复制 let obj = {
username: 'luwang',
age: 23
}
// let username = obj.username
// let age = obj.age
// console.log(username, age)
// let {username, age} = obj // 对象,因此需要以对象的形式来接收 只需要一个就写一个,不需要按顺序
// console.log(username, age)
let {age} = obj
console.log(age)
// [b, a] = [a, b]
let arr = [1, 3, 5,'abc', true]
// let [a, b, c, d, e] = arr
// console.log(a, b, c, d, e)
// let [a, b] = arr
// console.log(a, b)
let [,,a, b] = arr
console.log(a, b)
function foo({username, age}){ // {username, age} = obj
console.log(username, age)
}
foo(obj)
3、各种数据类型的扩展
(1) 字符串
模板字符串
模板字符串必须用两个 "`" 包裹起来,ESC 下面那个键
复制 let obj = {username: 'luwang', age: 23}
/*
* 之前的写法:简单拼串
* 缺点:可能会拼错,效率低。比如,url携带10个参数,动态拼起来
*/
let str = 'My name is ' + obj.username + ', age is '+ obj.age
console.log(str)
/*
* ES6提供的模板字符串
*/
str = `My name is ${obj.username} age is ${obj.age}`
字符串支持 Unicode
String.prototype.codePointAt
新增一些方法
String.prototype.includes(str)
:判断是否包含指定的字符串
String.prototype.startsWith(str)
:判断是否以指定字符串开头
String.prototype.endsWith(str)
:判断是否以指定字符串结尾
String.prototype.repeat(count)
:重复指定次数
复制 let str = 'asdfghjkklqwrtyuiopzxcvbnm123467890'
console.log(str.includes('t')) // true
console.log(str.includes('abc')) // false
console.log(str.startsWith('a')) // true
console.log(str.endsWith('0')) // true
console.log(str.repeat(2)) // asdfghjkklqwrtyuiopzxcvbnm123467890asdfghjkklqwrtyuiopzxcvbnm123467890
(2) 数值
二进制与八进制表示法:二进制用 0b
,八进制用 0o
新增方法:
Number.isFinite(i)
:判断是否是有限大的数字
Number.isInteger(i)
:判断是否是整数
Number.parseInt(str)
:将字符串转换为对应的数值
Math.acosh
、Math.hypot
、Math.imul
、Math.sign
复制 console.log(0b1010)
console.log(0o12)
console.log(Number.isFinite(Infinity))
console.log(Number.isNaN(NaN))
console.log(Number.isInteger(123.1))
console.log(Number.isInteger(123.0))
console.log(Number.parseInt('123abc123')) // 123
console.log(Number.parseInt('a123abc123')) // NN
console.log(Math.trunc(123.123)) // 123
(3) 对象
简化的对象写法 (短语法)
复制 let name = 'Tom';
let age = 12;
/* 正常情况 */
let obj = {
name: name,
age: age,
getName: function(){
retrun this.name
}
}
console.log(obj)
/* key和value相同,可以省略 */
let person = {
name, // 同名的属性可以不写
age,
setName (name) { // 可以省略函数的function
this.name = name
}
}
属性名支持表达式,需要用方括号括起来:
复制 const name = ['ha', 'hi', 'ho']
function yes() {
return 'Yes'
}
const obj = {
[name+'llo']: 'luwang',
['not'+yes()]: 1
}
属性简写和解构赋值结合使用:
在函数的参数中使用对象的解构赋值,可以避免在函数体内部再次对参数对象进行解构赋值(const {id, name} = user
)
函数的参数如果是一个对象的成员,可以使用对象简写方式({id: id, name: name}
)
{data: res}
等同于 {data: data}
,data
是一个变量,res
是一个新的变量,可以用于解构赋值(获取到等号右边的 data.data
)
复制对象
Object.assign(target, source1, source2..)
:将源对象的属性复制到目标对象上
复制 let obj = {}
let obj1 = {username:'a', age: 20}
let obj2 = {sex: '男'}
// Object.assign(obj, obj1)
// console.log(obj) // {username: "a", age: 20}
Object.assign(obj, obj1, obj2)
console.log(obj) // {username: "a", age: 20, sex: "男"}
判断是否相等
Object.is(v1, v2)
:判断2个数据是否完全相等
复制 console.log(0 == -0) // true
console.log(NaN == NaN) //false
console.log(Object.is(0, -0)) // false
console.log(Object.is(NaN, NaN)) // true
__proto__
属性
__proto__
属性:隐式原型属性。ES 6 中能直接操作__proto__
属性,但是不推荐使用
复制 let obj = {}
let obj1 = {salary: 5000000}
obj.__proto__ = obj1
console.log(obj)
console.log(obj.salary)
(4) 数组
Array.from(v)
:将伪数组对象或可遍历对象转换为真数组
Array.of(v1, v2, v3)
:将一系列值转换成数组
find(function(value, index, arr){return true})
:找出第一个满足条件返回true的元素
findIndex(function(value, index, arr){return true})
:找出第一个满足条件返回 true 的元素下标
copyWithin
:复制数组中一系列元素到同一数组指定的起始位置
entries
:返回一个新的 Array Iterator 对象,该对象包含数组中每个索引的键/值对
keys
:返回一个新的 Array Iterator 对象,该对象包含数组中每个索引的键
values
:返回一个新的 Array Iterator 对象,该对象包含数组中每个索引的值
复制 <button>測試1</button><br>
<button>測試2</button><br>
<button>測試3</button>
<script>
let btns = document.getElementsByTagName('button')
// 偽數組 不能使用forEach(數組的方法)
Array.from(btns).forEach(function(item, index){
console.log(item)
})
let arr = Array.of(1, 4, 'abc', true)
console.log(arr)
let arr2 = [2,3,4,2,5,7,3,6]
console.log(arr2.find(function(item, index){
return item > 4
}))
console.log(arr2.findIndex(function(item, index){
return item > 4
}))
</script>
(5) 函数
Ⅰ、箭头函数
让函数写法更简便
基本语法:
没有参数:const fn = () => console.log('xxxx')
箭头前的()不能省略
大于一个参数:(i,j) => i+j
()不能省略
使用场景:多用来定义回调函数
特点:
箭头函数没有自己的 this
,箭头函数的 this
不是调用的时候决定的,而是在定义的时候所处的对象就是它的 this
复制 let fun = function(){console.log('fun')}
fun()
// 1、没有形参
let fun1 = () => console.log('fun1')
fun1()
// 2、只有一个形参
let fun2 = (a) => console.log(a)
// 可省略() let fun2 = a => console.log(a)
fun2('aaa')
// 3、两个及两个以上的形参
let fun3 = (x,y) => console.log(x, y)
fun3(1, 2)
// I、函数体只有一条语句或表达式,{}可以省略-->会自动返回语句执行的结果或表达式的结果
let foo = (x, y) => x + y
// let foo = (x, y) => {return x + y}
console.log(foo(1, 3))
// II、函数体不止一条语句或者表达式, {}不可以省略
let foo2 = (x, y) => {
console.log(x, y)
return x + y
}
console.log(foo2(3, 5))
// 箭头函数的this
<br/><button id="btn1">按钮1</button><br/><br/>
<button id="btn2">按钮2</button><br/><br/>
<button id="btn3">按钮3</button>
<script>
let btn1 = document.getElementById('btn1')
let btn2 = document.getElementById('btn2')
let btn3 = document.getElementById('btn3')
btn1.onclick = function(){
console.log(this) // <button id="btn1">按钮1</button>
}
btn2.onclick = () => {
console.log(this) // Window
}
let obj = {
name: '箭头函数',
getName: function(){
btn3.onclick = () => {
console.log(this) // {name: "箭头函数", getName: ƒ}
}
}
}
obj.getName()
let obj1 = {
name: '箭头函数',
getName: () => {
btn3.onclick = () => {
console.log(this) // Window
}
}
}
obj.getName()
</script>
Ⅱ、参数处理
3 点运算符/点点点运算符
第一种用法:在函数中,rest(可变)参数
通过形参左侧的 ...
来表达,取代 arguments
的使用
比 arguments
灵活,只能是最后部分形参参数
arguments
是伪数组,有 length
,但是没有数组的一般方法,不能使用 forEach
遍历
callee
是 arguments
的一个属性,等于函数本身,递归的时候可以写为:arguments.callee()
复制 // arguments
function foo(a, b){
console.log(arguments)
// arguments.callee() 调用自身,相当于foo(参数)
/* arguments.forEach(function(item, index){ // 会报错,伪数组并没有数组的一般方法
console.log(item, index)
}) */
}
foo(2,5)
复制 // 点点点运算符
function foo(...value){
console.log(arguments)
console.log(value) // 就是一个正常的数组
value.forEach(function(item, index){
console.log(item, index)
})
}
foo(2,5)
function foo(a, ...value){// ...value只能放在最后面
console.log(arguments)
// arguments.callee()
console.log(value) // 使用的时候不用加...
value.forEach(function(item, index){
console.log(item, index)
})
}
foo(2, 3, 5, 7) // 最前面的就是a,value就不包括它了
第二种用法——扩展/展开运算符,可以分解出数组或对象 中的数据
复制 let arr = [1, 6]
let arr1 = [2, 3, 4, 5]
arr = [1, ...arr1, 6]
console.log(arr) // (6) [1, 2, 3, 4, 5, 6] 数组
console.log(...arr) // 1 2 3 4 5 6 每项值
Ⅲ、形参的默认值
复制 // 定义一个点的坐标的构造函数
function Point(x, y){
this.x = x
this.y = y
}
let point = new Point(50, 20)
console.log(point) // Point {x: 50, y: 20}
// 忘记传参
let point1 = new Point()
console.log(point1) // Point {x: undefined, y: undefined}
/*
* 因此会有需求,在忘记传参的时候使用默认值
* 在形参的位置赋默认值
*/
function Point(x = 0, y = 0){
this.x = x
this.y = y
}
let point = new Point(50, 20)
console.log(point) // Point {x: 50, y: 20}
// 忘记传参,使用默认值
let point1 = new Point()
console.log(point1) // Point {x: 0, y: 0}
(6) 正则表达式
正则表达式字面量添加 Unicode 支持(u
标记)
4、新增数据类型
(1) Symbol 类型
前言:ES 5 中对象的属性名都是字符串,容易造成重名,污染环境
概念:ES 6 中添加了一种原始数据类型 symbol (已有的数据类型:String、Number、boolean、null、undefined、对象)
特点:
Symbol 属性对应的值是唯一 的,解决命名冲突问题
Symbol 值不能 与其他数据 进行计算 ,包括与字符串拼串
for in、for of 遍历时不会遍历 symbol 属性
使用:
内置 Symbol 值
除了定义自己使用的 Symbol 值以外,ES 6 还提供了 11 个内置的 Symbol 值(查看官方文档)
对象的 Symbol.iterator 属性,指向该对象的默认遍历器方法
复制 // 创建symbol属性值
let symbol = Symbol()
console.log(symbol) // Symbol()
let obj = {username:'kobe', age:39}
// 可以添加symbol属性——但是得用另一种方式
obj.gender = '男'
obj[symbol] = 'hello'
console.log(obj) // {username: "kobe", age: 39, gender: "男", Symbol(): "hello"}
//let symbol2 = Symbol()
//let symbol3 = Symbol()
// 并不相同,值是唯一的
//console.log(symbol2, symbol3, symbol2 == symbol3) // Symbol() Symbol() false
// 可以传参,这样就能很明显看出不同了
let symbol2 = Symbol('one')
let symbol3 = Symbol('two')
console.log(symbol2, symbol3, symbol2 == symbol3) // Symbol(one) Symbol(two) false
// 可以用来定义常量
const Person_key = Symbol('person_key')
console.log(Person_key) // Symbol(person_key)
// 等同于在指定的数据结构上部署了Iterator接口
// 当使用for of去遍历某一个数据结构的时候,首先去找Symbol.itearator,找到了就去遍历,没有找到就不能遍历
let targetData = {
[Symbol.iterator]: function(){
let nextIndex = 0
return{
next: function(){
return nextIndex < this.length ? {value: this[nextIndex++], done: false} : {value: undefined, done: true}
}
}
}
}
// 使用三点运算符、解构赋值,默认会去调用Iterator接口
let arr2 = [1,6]
let arr3 = [2,3,4,5]
arr2 = [1,...arr3,6]
console.log(arr2)
let [a,b] = arr2
console.log(a,b)
(2) Set/Map 容器结构
容器: 能保存多个数据的对象,同时必须具备操作内部数据的方法
任意对象都可以作为容器使用,但有的对象不太适合作为容器使用(如函数)
Set 的特点 :保存多个 value,value 是不重复 ====>数组元素去重
Map 的特点 :保存多个 key-value,key 是不重复,value 是可以重复的
API:
Set 容器:无序不可重复的多个 value 的集合体
Set(arr)
:创建一个 Set 容器,同时将 arr 数组(其中 arr 是一维数组)中的元素添加到 Set 容器中(去重)
add(value)
:向 Set 容器中添加一个 value
delete(value)
:删除 Set 容器中指定的 value
has(value)
:判断 Set 容器中是否存在指定的 value
Map 容器:无序的 key、不重复的多个 key-value 的集合体
Map(arr)
:创建一个 Map 容器,同时将 arr 数组(其中 arr 是二维数组)中的元素添加到 Map 容器中(去重)
set(key, value)
:向 Map 容器中添加一对 key-value
get(key)
:获取 Map 容器中指定 key 对应的 value
delete(key)
:删除 Map 容器中指定的 key-value
has(key)
:判断 Map 容器中是否存在指定的 key
复制 // let set = new Set()
let set = new Set([1,2,4,5,2,3,6]) // 重复的会去除
console.log(set)
set.add(7)
console.log(set.size, set)
console.log(set.has(8))
console.log(set.has(7))
set.delete(7)
console.log(set.size, set)
set.clear()
console.log(set.size, set)
// let map = new Map()
let map = new Map([['username', 'aaa'], ['age', 35], ['sex', 'female']]) // 二维数组,且只能有两值(一个是key,一个是value)
map.set('other', 'shuoming')
console.log(map.size, map)
map.delete('other')
console.log(map)
console.log(map.has('username'))
map.clear()
console.log(map)
(3)WeakSet 和 WeakMap 类型
WeakSet
对象是一些对象值的集合。且其与 Set
类似,WeakSet
中的每个对象值都只能出现一次。在 WeakSet
的集合中,所有对象都是唯一的。
WeakSet
只能是对象 的集合,而不能像 Set
那样,可以是任何类型的任意值。
WeakSet
持弱引用 :集合中对象的引用为弱 引用。如果没有其他的对 WeakSet
中对象的引用,那么这些对象会被当成垃圾回收掉。
WeakMap 的 key 只能是 Object
类型。 原始数据类型 是不能作为 key 的(比如 Symbol
)。
原生的 WeakMap
持有的是每个键对象的“弱引用”,WeakMap
的 key 是不可枚举的
(4)TypedArray 类型
5、class 类
ES6 中新增的语法,用于实现面向对象编程
通过 class
关键字定义类,实现类的继承
(1) 之前实现继承
回顾:原型、构造函数、构造函数+原型——继承
复制 function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.showMe = function() {
console.log(this.name)
}
let person = new Person('kobe', 39)
person.showMe()
console.log(person)
(2) class
定义类
在类中通过 constructor()
定义构造方法(相当于构造函数)
复制 // 定义一个 Person 类
class Person { // 类声明
// const Person = class { // 类表达式,类表达式的名称是可选的
// 类的构造方法,只能有一个
constructor(name, age) { // constructor(){} 里面的参数是生成实例传入的参数
// 在构造方法中通过 this 给实例对象添加属性
this.name = name
this.age = age
}
// 在类中定义的方法,都是添加到类的原型对象上的
showMe(){
console.log(this.name) // this 指向实例对象,所以可以通过 this 来访问实例对象的属性
}
}
let person = new Person('kobe', 39) // new Person() 会自动调用 constructor() 构造方法,创建实例对象,并且将参数传递给 constructor() 构造方法
console.log(person)
person.showName()
类声明和类表达式的区别
类表达式:类表达式的类名是可选的,如果有类名,类名是标识符
constructor() 构造方法
通过 new 关键字调用类时,会自动调用 constructor() 构造方法
constructor() 构造方法是类中的默认方法 ,如果类中没有显式定义构造方法,那么类中会有一个隐式的、默认的空的 constructor() 构造方法
通过 constructor() 构造方法可以给实例对象添加属性
(3) class 类的继承
子类方法自定义:将从父类中继承来的方法重新实现一遍(重写从父类继承的一般方法)
复制 // 父类
class Person{
constructor(name, age){
this.name = name
this.age = age
}
showMe(){
console.log('调用父类的方法')
console.log(this.name, this.age)
}
add(){
console.log('父类的一般方法')
}
}
let person = new Person('kobe', 39)
person.showMe()
// 子类:继承父类
class StarPerson extends Person{
constructor(name, age, salary){
// 如果子类中定义了构造方法,那么子类的构造方法中必须调用 super(),否则会报错
super(name, age) // 调用父类的构造方法,将参数传递给父类的构造方法
this.salary = salary // 子类自己的属性
}
showMe(){
// 可以通过 super 来调用父类的方法
super.add() // 调用父类的一般方法
console.log('子类的重写方法')
console.log(this.name, this.age, this.salary)
}
}
let p1 = new StarPerson('wade', 36, 10000000)
console.log(p1)
p1.showMe()
在一个构造方法中可以使用 super
关键字来调用父类的构造方法
在派生类中(子类),super()
必须要先调用,然后才能使用 this
关键字,否则会报错
extends
关键字用来实现类的继承,extends
后面跟父类的名称(也可以是内建对象)
(4) class 类的静态方法
复制 // 定义一个 Person 类
class Person{
constructor(name, age){
this.name = name
this.age = age
}
showMe(){
console.log('调用 showMe 方法')
console.log(this.name, this.age)
}
// 静态方法
static showName(){
console.log('调用 Person 的静态方法')
console.log(this) // this 指向类本身
}
}
let person = new Person('kobe', 39)
person.showMe()
Person.showName() // 通过类来调用静态方法
(5) class 类的 getter 和 setter 方法
通过 get 和 set 关键字来定义 getter 和 setter 方法
getter 和 setter 方法是通过实例对象来调用 的
getter 和 setter 方法中的 this 指向实例对象
复制 // 定义一个 Person 类
class Person{
constructor(name, age){
this.name = name
this.age = age
}
showMe(){
console.log('调用 showMe 方法')
console.log(this.name, this.age)
}
// 静态方法
static showName(){
console.log('调用 Person 的静态方法')
console.log(this) // this 指向类本身
}
// getter 方法
get info(){
console.log('调用 getter 方法')
return this.name + ' ' + this.age
}
// setter 方法
set info(value){
console.log('调用 setter 方法')
let arr = value.split(' ')
this.name = arr[0]
this.age = arr[1]
}
}
let person = new Person('kobe', 39)
person.showMe()
Person.showName() // 通过类来调用静态方法
console.log(person.info) // 通过实例对象来调用 getter 方法
person.info = 'wade 36' // 通过实例对象来调用 setter 方法
console.log(person.info)
(6) class 类的静态属性
复制 // 定义一个 Person 类
class Person{
constructor(name, age){
this.name = name
this.age = age
}
showMe(){
console.log('调用 showMe 方法')
console.log(this.name, this.age)
}
// 静态方法
static showName(){
console.log('调用 Person 的静态方法')
console.log(this) // this 指向类本身
}
// getter 方法
get info(){
console.log('调用 getter 方法')
return this.name + ' ' + this.age
}
// setter 方法
set info(value){
console.log('调用 setter 方法')
let arr = value.split(' ')
this.name = arr[0]
this.age = arr[1]
}
// 静态属性
static info = '这是一个静态属性'
}
let person = new Person('kobe', 39)
person.showMe()
Person.showName() // 通过类来调用静态方法
console.log(person.info) // 通过实例对象来调用 getter 方法
person.info = 'wade 36' // 通过实例对象来调用 setter 方法
console.log(person.info)
console.log(Person.info) // 通过类来调用静态属性
(7) 私有类字段(属性和方法)
私有类字段是指类中只能在类的内部访问的字段(属性和方法)
复制 // 定义一个 Person 类
class Person{
constructor(name, age){
this.name = name
this.age = age
}
showMe(){
console.log('调用 showMe 方法')
console.log(this.name, this.age)
}
// 静态方法
static showName(){
console.log('调用 Person 的静态方法')
console.log(this) // this 指向类本身
}
// getter 方法
get info(){
console.log('调用 getter 方法')
return this.name + ' ' + this.age
}
// setter 方法
set info(value){
console.log('调用 setter 方法')
let arr = value.split(' ')
this.name = arr[0]
this.age = arr[1]
}
// 静态属性
static info = '这是一个静态属性'
// 私有类字段
#money = 10000000
#showMoney(){
console.log('调用私有类方法')
console.log(this.#money)
}
}
let person = new Person('kobe', 39)
person.showMe()
Person.showName() // 通过类来调用静态方法
console.log(person.info) // 通过实例对象来调用 getter 方法
person.info = 'wade 36' // 通过实例对象来调用 setter 方法
console.log(person.info)
console.log(Person.info) // 通过类来调用静态属性
console.log(person.money) // undefined
person.showMoney() // 报错
6、Promise 对象
理解:
Promise 对象代表了某个将要发生的事件(通常是一个异步操作)
ES 6 的 Promise 是一个构造函数,用来生成 promise 实例
解决回调地狱 (回调函数的层层嵌套,编码是不断向右扩展,阅读性很差);有了 promise 对象,可以将异步操作以同步的流程表达出来,避免了层层嵌套的回调函数(回调地狱)
在 ES 6 之前原生的 JS 中是没这种实现的,一些第三方框架(jQuery)实现了 Promise
在同一个 Promise 中只能 pending 到 pending、pending 到 fullfilled、pending 到 rejected,不能成功之后又失败或失败之后在成功
复制 let request = new XMLHttpRequest()
request.responseType = 'json'
request.open("GET", url)
request.send()
(1) Promise 的基本使用
ES 6 中定义实现 API(使用 Promise 基本步骤):
复制 // 1. 创建promise对象
let promise = new Promise((resolve, reject) => {
// 初始化 promise 状态为 pending
// 执行异步操作
if(异步操作成功) { // 调用成功的回调
resolve(result); // 修改 promise 状态为 fullfilled
} else { // 调用失败的回调
reject(errorMsg); // 修改 promise 的状态为 rejected
}
})
// 2. 调用 promise 对象的 then()
promise.then(function(
result => console.log(result),
errorMsg => alert(errorMsg)
))
例子:
复制 // 1、创建 promise 对象
let promise = new Promise((resolve, reject) => {
// 初始化 promise 状态 pending:初始化
console.log('11111111')
// 执行异步操作,通常是发送 Ajax 请求,开启定时器
setTimeout(() => {
console.log('3333333')
// 根据异步任务的返回结果去修改 promise 的状态
// 异步任务执行成功
// resolve('哈哈,') // 修改 promise 的状态为 fullfilled:成功
// 异步任务执行失败
reject('555, ') // 修改 promise 的状态为 rejsected:失败
}, 2000)
})
console.log('222222222')
// 2. 调用 promise 对象的 then()
promise
.then((data) => { // 成功的回调
console.log(data, '成功了~~~')
}, (error) => { // 失败的回调
console.log(error, '失败了……')
})
例如:新闻、新闻的评论,只发新闻的内容;在接着根据新闻的 id 拿取这个新闻下的评论
复制 // 定义获取新闻的功能函数
function getNews(url){
let promise = new Promise((resolve, reject) => {
// 状态:初始化
// 执行异步任务
let xmlHttp = new XMLHttpRequest()
// 绑定监听 readyState
/* xmlHttp.onreadystatechange = function(){
if(xmlHttp.readyState === 4 && xmlHttp.status == 200){
// 请求成功
console.log(xmlHttp.responseText)
// 修改状态
resolve(xmlHttp.responseText) // 修改 promise 的状态为成功
}else{
// 请求失败
reject('暂时没有新闻内容')
}
} --> 逻辑有问题*/
xmlHttp.onreadystatechange = function(){
if(xmlHttp.readyState === 4){
if(xmlHttp.status == 200){
// 请求成功
// console.log(xmlHttp.responseText)
// 修改状态
resolve(xmlHttp.responseText) // 修改promise的状态为成功
}else{
// 请求失败
reject('暂时没有新闻内容')
}
}
}
// open 设置请求得方式以及 url
xmlHttp.open('GET', url)
// 发送
xmlHttp.send()
})
return promise
}
getNews('http://localhost:3000/news?id=2')
.then((data) => {
console.log(data)
// 发送请求获取评论内容准备 url
let commentsUrl = JSON.parse(data).commentsUrl
let url = 'http://localhost:3000' + commentsUrl
// 发送请求
return getNews(url)
},(error) => {
console.log(error)
})
.then((data) => {
console.log(data)
}, () => {
})
new Promise()
创建一个新的 Promise 对象
参数:回调函数(函数中有两个参数,resolve、reject)
resolve()
将 Promise 对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去
reject()
将 Promise 对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去
创建的 Promise 对象可以调用一些方法
all()
:将多个 Promise 实例,包装成一个新的 Promise 实例
race()
:将多个 Promise 实例,包装成一个新的 Promise 实例
resolve()
:将现有对象转为 Promise 对象
reject()
:返回一个新的 Promise 实例,该实例的状态为 rejected
(2) Promise 的链式调用
生成的 Promise 对象可以进行链式调用,即 then() 方法返回的是一个新的 Promise 对象,可以继续调用 then() 方法
复制 // 1、创建 promise 对象
let promise = new Promise((resolve, reject) => {
// 初始化 promise 状态 pending:初始化
console.log('11111111')
// 执行异步操作,通常是发送 Ajax 请求,开启定时器
setTimeout(() => {
console.log('3333333')
// 根据异步任务的返回结果去修改 promise 的状态
// 异步任务执行成功
// resolve('哈哈,') // 修改 promise 的状态为 fullfilled:成功
// 异步任务执行失败
reject('555, ') // 修改 promise 的状态为 rejsected:失败
}, 2000)
})
console.log('222222222')
// 2. 调用 promise 对象的 then()
promise
.then((data) => { // 成功的回调
console.log(data, '成功了~~~')
}, (error) => { // 失败的回调
console.log(error, '失败了……')
})
7、Iterator 迭代器
概念:iterator 是一种接口机制,为各种不同的数据结构提供统一的访问机制
作用:
ES 6 创造了一种新的遍历命令 for...of 循环,Iterator 接口主要供 for...of 消费
工作原理
创建一个指针对象(遍历器对象),指向数据结构的起始位置
第一次调用 next
方法,指针自动指向数据结构的第一个成员
接下来不断调用 next
方法,指针会一直往后移动,直到指向最后一个成员
没调用 next
方法返回的是一个包含 value 和 done 的对象 {value: 当前成员的值, done: 布尔值}
value 表示当前成员的值,done 对应的布尔值表示当前的数据的结构是否遍历结束
当遍历结束的时候返回的 value 值是 undefined,done 值为 false
原生具备 Iterator 接口的数据,可用 for...of 遍历
扩展理解
当数据结构上部署了 Symbol.iterator 接口,该数据就是可以用 for of 遍历
当使用 for of 去遍历目标数据的时候,该数据会自动去找 Symbol.iterator 属性(Symbol.iterator 属性指向对象的默认遍历器方法)
for-of 循环:可以遍历任何容器(Set、Map)、数组、对象、伪/类对象、字符串、可迭代的对象
复制 let set = new Set([1, 2, 4, 3, 4, 5])
for(let i of set){
console.log(i)
}
// 可以用 Set 给数组去重
let arr = [1,2,4,5,5,6,2]
let arr1 = arr
arr = [] // 保留数组类型
let set = new Set(arr1)
for(let i of set){
arr.push(i)
}
console.log(arr)
例如
复制 // 模拟指针对象(遍历器对象)
function myIterator(arr){// Iterator接口
let nextIndex = 0 // 记录指针的位置
return{
next: function(){// 遍历器对象
return nextIndex < arr.length ? {value: arr[nextIndex++], done: false} : {value: undefined, done: true}
}
}
}
// 准备一个数据
let arr =[1,4,65,'abc']
let iteratorObj = myIterator(arr)
console.log(iteratorObj.next()) // {value: 1, done: false}
console.log(iteratorObj.next()) // {value: 4, done: false}
console.log(iteratorObj.next()) // {value: 65, done: false}
console.log(iteratorObj.next()) // {value: "abc", done: false}
console.log(iteratorObj.next()) // {value: undefined, done: true}
// 将iterator接口部署到指定的数据类型上,可以使用for of去循环遍历
// 数组、字符串、argument、set容器、map容器
for(let i of arr){
console.log(i)
}// 1 4 65 abc
let str = 'abcdefg'
for(let i of str){
console.log(i)
}// a b c d e f g
function fun(){
for(let i of arguments){
console.log(i)
}
}
fun(1,4,5,'abc') // 1 4 5 abc
// let obj = {username:'kobe', age: 39}
// for(let i of obj){
// console.log(i)
// }// Uncaught TypeError: obj is not iterable 不可迭代
8、Generator 生成器
概念:
Generator 函数是一个状态机,内部封装了不同状态的数据
可暂停函数(惰性求值),yield 可暂停,next 方法可启动。每次返回的是 yield 后的表达式结果
特点:
复制 function* generatorExample(){
let result = yield 'hello' // 状态值为hello
yield 'generator' // 状态值为generator
}
generator 函数返回的是指针对象,而不会执行函数内部逻辑
复制 function* generatorExample(){
console.log('开始执行')
let result = yield 'hello' // 状态值为hello
yield 'generator' // 状态值为generator
}
generatorExample() // 调用并不会执行函数内部逻辑
调用 next 方法函数,内部逻辑开始执行,遇到 yield 表达式停止,返回 {value: yield后的表达式结果/return后的返回结果(如果没写,返回undefined),done: boolean值(后面还有返回false,没有返回true)}
复制 function* generatorExample(){
console.log('开始执行')
let result = yield 'hello' // 状态值为hello,会执行,停止 测试yield console.log('会执行')
console.log('下次调用next执行')
yield 'generator' // 状态值为generator
console.log('下次调用next执行')
return '返回的结果'
}
let MG = generatorExample() // 返回的是指针对象
console.log(MG.next()) // 执行,遇到yield停止
console.log(MG.next('可以拿到这个值')) // 再次调用next,往下执行,可以传参
console.log(MG.next()) // 再次调用next,往下执行,返回true
再次调用 next 方法会从上一次停止时的 yield 处开始,直到最后
yield 语句返回结果通常为 undefined,当调用 next 方法时传参内容会作为启动时 yield 语句的返回值
补充:
对象的 Symbol.iterator 属性,指向遍历器对象
复制 let obj = {username:'kobe', age: 39}
obj[Symbol.iterator] = function* myTest(){
yield 1
yield 2
yield 3
}
for(let i of obj){
console.log(i)
}
例如:
新闻内容获取成功后再次发送请求,获取对应的新闻评论内容
复制 // 要比使用Promise更好
function getNews(url){
$.get(url, function(data){ // 前面引入了jQuery
console.log(data)
let url = 'http://localhost:3000' + data.commentsUrl
SX.next(url) // 放在这里也可以往下移,并且这里参数传输更方便
})
}
function* sendXml(){
let url = yield getNews('http://localhost:3000/news?id=3') // 如果这里出错,后面评论也不会再执行了
yield getNews(url)
}
// 获取遍历器对象
let SX = sendXml()
SX.next()
9、模块
导入
静态的 import
语句用于导入由另一个模块导出的绑定
在浏览器中,import
语句只能在声明了 type="module"
的 script
的标签中使用
还有一个类似函数的动态 import()
,它不需要依赖 type="module"
的 script 标签
语法:
复制 /* 默认导出的导入 */
import defaultExport from "module-name";
/* 导入全部,设置别名 */
import * as name from "module-name";
/* 导入对象中单个或多个 */
import { export } from "module-name";
import { export as alias } from "module-name";
import { export1 , export2 } from "module-name";
import { foo , bar } from "module-name/path/to/specific/un-exported/file";
import { export1 , export2 as alias2 , [...] } from "module-name";
/* 导入默认导出的和对象的 */
import defaultExport, { export [ , [...] ] } from "module-name";
import defaultExport, * as name from "module-name";
/* 可以导入其他文件 */
import "module-name";
/* 动态导入 */
var promise = import("module-name");//这是一个处于第三阶段的提案。
导出
在创建 JavaScript 模块时,export
语句用于从模块中导出实时绑定的函数、对象或原始值,以便其他程序可以通过 import
语句使用它们
复制 // 导出单个特性
export let name1, name2, …, nameN; // 也可以用 var, const
export let name1 = …, name2 = …, …, nameN;
export function FunctionName(){...}
export class ClassName {...}
// 导出列表
export { name1, name2, …, nameN };
// 重命名导出
export { variable1 as name1, variable2 as name2, …, nameN };
// 解构导出并重命名
export const { name1, name2: bar } = o;
// 默认导出
export default expression;
export default function (…) { … } // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };
// 导出模块合集
export * from …; // does not set the default export
export * as name1 from …; // Draft ECMAScript® 2O21
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
export { default } from …;
10、其他
五、ES 2016
新增了两个新特性
1、指数操作
指数运算符(幂): **
复制 console.log(3 ** 3) // 27 (3 的 3 次方)
2、数组新方法
Array.prototype.includes(value)
: 判断数组中是否包含指定 value
复制 let arr = [1,2,3,'abc']
console.log(arr.includes('a')) // false
六、ES 2017
1、async/await 函数
概念:真正意义上去解决异步回调的问题,同步流程表达异步操作
本质:Generator 的语法
语法:
复制 async function foo(){
await 异步操作;
await 异步操作;
}
特点:
不需要像 Generator 去调用 next 方法,遇到 await 等待,当前的异步操作完成就往下执行
返回的总是 Promise 对象 ,可以用 then 方法进行下一步操作
async 取代 Generator 函数的 *
,await 取代 Generator 的 yield
复制 // async基本使用
async function foo(){
return new Promise(resolve => {
// setTimeout(function(){
// resolve()
// }, 2000)
// 可以写成下方这种
setTimeout(resolve, 2000)
})
}
async function test(){
console.log('开始执行', new Date().toTimeString())
await foo()
console.log('执行完毕', new Date().toTimeString())
}
test()
// async 里 await 返回值
function test2(){
return 'xxx'
}
async function asyncPrint(){
/* let result = await test2()
console.log(result) // 普通函数没有返回值
*/
/*
let result = await Promise.resolve()
console.log(result) // Promise对象成功状态返回undefined
*/
let result = await Promise.resolve('promise')
console.log(result) // Promise对象成功状态传参返回参数 promise
result = await Promise.reject('失败了……')
console.log(result) // 失败状态,返回出错,且能将参数返回 Uncaught (in promise) 失败了……
}
asyncPrint()
获取新闻内容案例
复制 // async 比 generator 又更简单
async function getNews(url){
return new Promise((resolve, reject) => {
$.ajax({ // 前面已经引入jQuery
method: 'GET',
url, // 这是ES6中简写
/* success: function(data){
resolve()
},
error: function(error){
reject()
}*/
// 简写
success: data => resolve(data),
error: error => reject(error)
})
})
}
async function sendXml(){
let result = await getNews('http://localhost:3000/news?id=7')
console.log(result) // {id: "7", title: "news title1...", content: "news content1...", commentsUrl: "/comments?newsId=7"}
result = await getNews('http://localhost:3000' + result.commentsUrl)
console.log(result)
}
sendXml()
改进一下,由于这种写法 error 并不会显示错误信息
复制 <script src="./jquery-3.1.0.min.js"></script>
<script>
async function getNews(url){
return new Promise((resolve, reject) => {
$.ajax({
method: 'GET',
url,
success: data => resolve(data),
// error: error => reject(error)
error: error => resolve(false) // 不用reject,而是返回false
})
})
}
async function sendXml(){
let result = await getNews('http://localhost:30010/news?id=7')
console.log(result) // {id: "7", title: "news title1...", content: "news content1...", commentsUrl: "/comments?newsId=7"}
if(!result){ // 出错就弹窗
alert('暂时没有新闻……')
}
result = await getNews('http://localhost:3000' + result.commentsUrl)
console.log(result)
}
sendXml()
</script>
给个模板
复制 const onError = reason => {
handleError(reason) // 具体操作
throw reason
}
async function fetchSome() {
showLoading() // Loading 处理
const response = await axios.get('/xxx') // 成功的
.catch(onError) // 失败的
.finally(hideLoading) // 处理 Loading
/* doSomething */
console.log(response)
}
fetchSome()
2、对象
Object.entries
:返回对象的所有键值对
3、字符串填充
padStart()
、padEnd()
分别可以在字符串位数不足的时候向头部和尾部进行填充内容
第一个参数是总位数,第二个参数为位数不足时填充的内容
例如:
复制 let str = 1
str.padStart(2, '0') // 01
str.padEnd(4, '*') // 1***
4、参数可以有多余的逗号
复制 function f(p1, p2, p3,) {}
5、其他
getOwnPropertyDescriptors
:获取对象的所有属性,包括不可枚举的属性
后续好用的新特性
问号
可选链 :?.
复制 let a = user && user.name && user.name.firstName
let a = user?.name?.firstName
双问号 :??
复制 let a = a || b // 有个问题,0 是 false
let a = a ?? b // undefined、null
两个结合使用
复制 let a = user?.name ?? 'default' // 如果 user.name 存在就取 user.name,否则取 'default'
由于现在开发都会用到 Babel 转译代码,所以可以放心使用这些新特性