总结16个es6+新特性中容易被忽视的问题

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

1. let 块级作用域

块作用域:用一对大括号包裹起来的范围

2. 数组/对象解构

如果解构成员大于原数组成员数量,如:

const arr=[1,2,3]
const [foo,bar,baz,more]=arr
console.log(more) // undefined
复制代码

类似于取数组下标,可以添加默认值来处理这种不确定元素是否存在的问题。

const arr=[1,2,3]
const [foo,bar,baz,more=4]=arr
console.log(more) // 4
复制代码

解构时会出现赋值冲突,如:

const obj={name:'jjj',age:4}
const name='ppp'
const {name}=obj
console.log(name) // 冲突
复制代码

可以用一个变量来处理冲突,因为const {name}=obj句相当于const {name:name}=obj,是把后面的name作为解构之后赋值的变量名。

const obj={name:'jjj',age:4}
const name='ppp'
const {name:newName}=obj
console.log(name)  // jjj
console.log(newName)  // ppp
复制代码

3.带标签的模板字符串

const str=console.log`hhh`
// ['hhh']
复制代码

此处打印数组

const name='tom'
const gender=true
function myTagFunc(str,name,gender){
    console.log(str)
    console.log(name)
    console.log(gender)
}
const result=myTagFunc`hey ${name} is a ${gender}`
//打印
[ 'hey ', ' is a ', '' ]
tom
true
复制代码

带标签的模板字符串用于对模板字符串加工

4.为字符串提供的扩展方法

startsWith\endsWith

const str='Error: xxxx'
console.log(str.startsWith('error')) //false
console.log(str.startsWith('Error')) //true
console.log(str.endsWith('xxx ')) //false
console.log(str.endsWith(' xxxx')) //true
复制代码

5. 箭头函数与this

箭头函数不会改变this的指向

const obj={
    name:'a',
    sayHi:()=>{
        console.log(this.name)
    },
    sayHiAsync(){
        setTimeout(()=>{
            console.log('sayHiAsync',this)
            console.log(this.name)
        },1000)
    },
    sayHi2:function(){
        console.log(this.name)
    },
    sayHi2Async:function(){
        setTimeout(function(){
            console.log('sayHi2Async',this)
            console.log(this.name)
        },1000)
    }
}

obj.sayHi() 
obj.sayHiAsync() 
obj.sayHi2() 
obj.sayHi2Async() 

// 打印
undefined
a
a // sayHiAsync 
undefined // sayHi2Async 
复制代码

sayHiAsync中的settimeout的回调函数是箭头函数,其this捕获到了其定义位置上下文的this值,作为自己的this,也就是obj。对于sayHi2Async指向其执行上下文,即全局。

6. 计算属性名

属性名可以直接使用动态值,如:

const obj={
    [Math.random()]:123
}
console.log(obj) //{ '0.9557466338895144': 123 }
复制代码

在属性名内用一对方括号包括任意表达式,该表达式执行结果就是作为该属性名。

7. Object方法扩展

Object.assign

可以多层覆盖,也可以覆盖__proto__上的属性

const target={a:1}
const source1={a:2,b:3}
source1.__proto__.hasOwnProperty=1
const source2={a:4,c:9}
Object.assign(target,source1,source2)
console.log(target)
console.log(target.__proto__.hasOwnProperty)
// 打印
{ a: 4, b: 3, c: 9 }
1
复制代码

Object.is

Object.is(+0,-0) // false
Object.is(NaN,NaN) // true
复制代码

8.Proxy


const person={
    name:'tom',
    age:10,
}

const personProxy=new Proxy(person,{
    get(target,property){
        console.log(target,property)
        return 10
    },
     set(target,property,value){
        console.log(target,property,value)
    }
})

console.log(personProxy.name)
personProxy.gender='female'

//打印
{ name: 'tom', age: 10 } name
10
{ name: 'tom', age: 10 } gender female
复制代码

可以修改get方法来处理不存在的属性,如:

const person={
    name:'tom',
    age:10,
}

const personProxy=new Proxy(person,{
    get(target,property){
        return property in target?target[property]:'default'
    },
})

console.log(personProxy.gender)
复制代码

可以修改set方法来对某些属性的赋值进行校验,如:

const personProxy=new Proxy(person,{
    set(target,property,value){
        if(property==='age'){
            if(!Number.isInteger(value))
            throw new TypeError(`${value} is not an int`)
        }
    }
})

personProxy.age=10.7
// 报错
TypeError: 10.7 is not an int
复制代码

Proxy对比defineProperty

Proxy对比defineProperty要更强大,defineProperty只能监视对象的读写,但Proxy可以监视更多的行为,如:delete、new、in等。

const person={
    name:'tom',
    age:10,
}

const personProxy=new Proxy(person,{
    deleteProperty(target,property){
        console.log('delete',property)
    }
})

delete personProxy.name
// 打印
delete name
复制代码

其次,proxy支持更好的支持数组对象的监视

const arr=[]
const arrProxy=new Proxy(arr,{
    set(target,property,value){
        console.log('set',property,value)
        target[property]=value
        return true
    }
})

arrProxy.push(4)
arrProxy.push(6)

// 打印
set 0 4
set length 1
set 1 6
set length 2
复制代码

Proxy是以非侵入的方式监管了对象的读写,Proxy并没有改变原对象,不需要对对象本身做任何操作,就可以监视到其内部的读写。

9. Reflect

Reflect属于一个静态类,也就是说无需new一个对象,Reflect内部封装了一系列对对象的底层操作,Reflect成员方法就是Proxy处理对象的默认实现。如下面的程序和上面的代码等价。

const arr=[]
const arrProxy=new Proxy(arr,{
    set(target,property,value){
        console.log('set',property,value)
        return Reflect.set(target,property,value)
    }
})

arrProxy.push(4)
arrProxy.push(6)
复制代码

Reflect最大的意义是,统一提供一套用于操作对象的API,比如对于处理对象的某些操作来说要更加规范,ecmascript也建议操作对象行为要用Reflect,原操作符很有可能会在将来被废弃。

const person={
    name:'tom',
    age:10,
}

/* console.log('name' in person)
console.log(delete person.name)
console.log(Object.keys(person)) */

console.log(Reflect.has(person,'name'))
console.log(Reflect.deleteProperty(person,'name'))
console.log(Reflect.ownKeys(person))
复制代码

10. Set

集合,set的一系列操作,如:

const s=new Set()
s.add(1).add(2).add(6)
console.log(s) // Set { 1, 2, 6 }

for(let i of s){
    console.log(i)
} // 1 2 6 

console.log(s.size) // 3

console.log(s.delete(3)) //false
console.log(s.delete(2)) // true
console.log(s) // Set { 1, 6 }
复制代码

利用Set进行数组去重,如:

const arr=[1,2,3,3,4,4]
const s=new Set(arr)
// console.log([...s])
console.log(Array.from(s)) //[ 1, 2, 3, 4 ]
复制代码

11. Map

Map是真正意义上的键值对集合,用来映射两个任意类型的键值对的关系。它与Object的区分是,Object会对任意类型的键名调用toString方法,将键名转为字符串类型,而Map则不会,如下:

const obj={
    [{a:1}]:1,
    [true]:2
}
// 打印
console.log(obj) // { '[object Object]': 1, true: 2 }
console.log(obj['[object Object]']) // 1
console.log(obj[{}]) // 1

// 用Map替代
const m=new Map()
const obj={a:1}
m.set(obj,1)
m.set(true,2)
console.log(m.get(obj)) // 1
console.log(m.get({})) // undefined
console.log(m.get(true)) // 2
console.log(m.get('true')) // undefined

console.log(m.has(obj)) //true  拥有某属性 
console.log(m.delete(obj)) //true  删除某键 
m.forEach((value,key)=>{
    console.log(key,value)
}) // true 2
m.clear() // 清空
复制代码

12. Symbol

Symbol是独一无二的值,Symbol可用来处理属性名重复的问题,如:

console.log(Symbol(1)===Symbol(1))
const obj={
    [Symbol('aaa')]:1
}
复制代码

Symbol也可以用来创建私有成员的属性名,如:

const name=Symbol()
const obj={
    [name]:'tom',
    sayName(){
        console.log(this[name])
    }
}

obj.sayName() // tom
复制代码

如果想要多次复用某个Symbol,可以使用全局变量,也可以使用Symbol.for(),其方法内部接受一个字符串,如果传入其他类型会转化成字符串,这里需要特别注意。

console.log(Symbol.for('a')===Symbol.for('a')) // true
console.log(Symbol.for('true')===Symbol.for(true)) // true
console.log(Symbol.for({})===Symbol.for({a:1})) // true
复制代码

Symbol适合作为私有属性名,不会被某些方法检测到,如下

const name=Symbol()
const obj={
    [name]:'tom',
    sayName(){
        console.log(this[name])
    }
}
for(let i in obj){
    console.log(i)
} // sayName
console.log(Object.keys(obj)) // [ 'sayName' ]
console.log(JSON.stringify(obj)) // {}

console.log(obj.hasOwnProperty(name)) // true
console.log(Object.getOwnPropertyNames(obj)) // [ 'sayName' ]
console.log(Object.getOwnPropertySymbols(obj)) // [ Symbol() ]
复制代码

13. for...of...和迭代器

for...of...不仅可以遍历数组、类数组、也可以遍历set、map等类型。for...of循环是一种数据统一遍历的方式,由于es提供的数据结构越来越多,es2015提供了iterable接口来统一进行迭代操作。(可以类比toString方法)。

具体到代码中,Symbol.iterator就是iterable接口,当执行数据类型的这个接口时

const set=new Set([1,2])
const iterator=set[Symbol.iterator]()
console.log(iterator)
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
// 打印
[Set Iterator] { 1, 2 }
{ value: 1, done: false }
{ value: 2, done: false }
{ value: undefined, done: true }
复制代码

实现可迭代接口 (Iterable)

const obj={
    store:['foo','bar','baz'],
    [Symbol.iterator]:function(){
        let index=0
        const self=this
        return {
            next:function(){
                const ret= {
                    value:self.store[index],
                    done:index===self.store.length
                }
                index++
                return ret
            }
        }
    }
}

for(const i of obj){
    console.log(i)
}

// 打印
foo
bar
baz
复制代码

迭代器模式

迭代器模式就是对外提供一个可迭代的接口,外部环境不用关心该数据类型内部的结构是怎样的,比如:我们有一个任务清单,这个清单里包含不同的类型,如果外部需要遍历这个清单里的每一个子项就要依次去遍历每一个分类:

const todos={
    learn:['maths','english','c++'],
    life:['meal','sleep'],
    work:['task1','task2']
}

for(const i of todos.learn){
    console.log(i)
}
for(const i of todos.life){
    console.log(i)
}
for(const i of todos.work){
    console.log(i)
}
复制代码

如果在数据内部设置一个可迭代的接口,那么在外部环境就可以直接遍历:

const todos={
    learn:['maths','english','c++'],
    life:['meal','sleep'],
    work:['task1','task2'],
    [Symbol.iterator]:function(){
        const arr=[...this.learn,...this.life,...this.work]
        let index=0;
        return {
            next:function(){
                const ret={value:arr[index],done:index++>=arr.length}
                return ret
            }
        }
    }
}

for(const i of todos){
    console.log(i)
}

// 打印
maths
english
c++
meal
sleep
task1
task2
复制代码

14. 生成器的应用

利用generator双向通信的特性,可以制造一个发号器:

// 发号器
function *createIdMaker(){
    let id=0
    while(true){
        yield id++
    }
}

const idMaker=createIdMaker()
console.log(idMaker.next().value)
console.log(idMaker.next().value)
console.log(idMaker.next().value)
console.log(idMaker.next().value)
// 打印
0
1
2
3
复制代码

改写todo内置可迭代接口:

const todos={
    learn:['maths','english','c++'],
    life:['meal','sleep'],
    work:['task1','task2'],
    [Symbol.iterator]:function *(){
        const arr=[...this.learn,...this.life,...this.work]
        for(const item of arr){
            yield item
        }
    }
}

for(const i of todos){
    console.log(i)
}
// 打印
maths
english
c++
meal
sleep
task1
task2
复制代码

15. Object.entries

返回键值对数组


const todos={
    learn:['maths','english','c++'],
    life:['meal','sleep'],
    work:['task1','task2'],
}

console.log(Object.entries(todos))
// 打印
[
  [ 'learn', [ 'maths', 'english', 'c++' ] ],
  [ 'life', [ 'meal', 'sleep' ] ],
  [ 'work', [ 'task1', 'task2' ] ]
]
复制代码

可以将object类型转化为map类型

console.log(new Map(Object.entries(todos)))
// 打印
Map {
  'learn' => [ 'maths', 'english', 'c++' ],
  'life' => [ 'meal', 'sleep' ],
  'work' => [ 'task1', 'task2' ]
}
复制代码

15. getOwnPropertyDescriptor

获取对象的属性的完整描述信息,getter、setter不能由Object.assign()方法完整复制,在下面的程序中,在复制的时候只是把Msg当作一个普通的属性来复制了

const p1={
    name:'tom',
    age:10,
    get Msg(){
        return this.name+' is '+this.age
    }
}

console.log(p1.Msg) // tom is 10
const p2=Object.assign({},p1)
p2.name='jerry'
console.log(p2.Msg) // tom is 10
复制代码

修改成以下代码就能解决上述问题

const descriptors=Object.getOwnPropertyDescriptors(p1)
const p2=Object.defineProperties({},descriptors)
p2.name='jerry'
console.log(p2.Msg) // jerry is 10
复制代码

16. padStart/padEnd

用指定字符串填充剩余位置:

const goods={
    egg:1,
    beer:10,
    vegetables:5
}

for(const [name,price] of Object.entries(goods)){
    console.log(`${name.padEnd(16,'-')}|${price.toString().padStart(3,'0')}`)
}
// 打印
egg-------------|001
beer------------|010
vegetables------|005
复制代码

参考文档

  1. this 指向详细解析(箭头函数)
  2. MDN - Proxy
  3. MDN - defineProperty
  4. MDN - Reflect

猜你喜欢

转载自juejin.im/post/7030802982220333063