快2021年了,必须掌握的ECMAScript 新特性

531 阅读4分钟

2020年伊始,我们经历了艰难和恐慌;

转眼间,2020年已接近尾声,继续努力,莫负时光!

回顾过往来不及细细思量,

我们曾为梦想拼搏,也曾为挫折痛哭;

时间匆匆再不说岁月漫长,

酸甜苦辣,悲喜交加,

经历的、错过的,让人对现在的生活倍感珍惜。

历数2020年的前11个月,

有收获有遗憾,有喜悦有不足,

最后的一个月,用努力对抗焦虑,

让2020充实收尾!

let & const

在之前JS只有两种作用域,全局作用域和函数作用,是没有块级作用域的

通过let声明的变量,只能在其所在的作用域中访问。

var elements = [{}, {}, {}]
for (var i = 0; i < elements.length; i++) {
	elements[i].onclick = function() {
    	console.log(i)  //这里的i其实打印的都是全局作用域的i
    }
}
elements[0].onclick() // 3  循环结束后,i已经累加到了3

这也是闭包的典型案例

var elements = [{}, {}, {}]
for (var i = 0; i < elements.length; i++) {
	elements[i].onclick = (function(i) {
    	console.log(i)  //这里的i其实打印的都是全局作用域的i
    })(i)
}
elements[0].onclick() // 3  循环结束后,i已经累加到了3

其实闭包也是借助函数作用域摆脱全局作用域产生的影响

现在有了let后就简单了

var elements = [{}, {}, {}]
for (let i = 0; i < elements.length; i++) {
	elements[i].onclick = function() {
    	console.log(i)  //这里的i其实打印的都是全局作用域的i
    }
}
elements[0].onclick()

让我们在看一下例子

for (let i = 0; i < 3; i++) {
	let i = 'foo';
    console.log(i)
}
正常输出3次foo, 这两个i是互相不影响的

代码进行拆解

let i = 0;
if (i < 3) {
  let i = 'foo'
  console.log(i)
}
i++
if (i < 3) {
  let i = 'foo'
  console.log(i)
}
i++
if (i < 3) {
  let i = 'foo'
  console.log(i)
}
i++

另外const/let不存在变量提升,所以在代码块里必须先声明然后才可以使用,这叫暂时性死区;

let name = 'test'
if (true) {
    name = '测试'
    let name
}
console.log(name)

const/let 不允许在同一个作用域内,重复声明;

const 声明时必须初始化,且后期不能被修改,但如果初始化的是一个对象,那么不能修改的是该对象的内存地址;

const person = {
    name: '测试'
}
person.name = 'test'  
console.log(person.name)  // 'test' 只是修改了内存空间的数据,没有改变内存地址
person = ''  // TypeError 这改变了内存的指向

const/let 在全局作用域中声明的常量/变量不会挂到顶层对象(浏览器中是 window )的属性中;

var name = '测试'
let age = 12
console.log(window.name)  // '测试'
console.log(window.age)  // undefined

最佳实践:不用var ,主用const,配合let,这种会使你的代码的质量会有明显的提高。

解构

数组解构

// 解构不成功的变量值为 undefined
const [a, b, c] = [1, 2]
console.log(a, b, c)  // 1, 2, undefined

// 获取余下的变量
const arr = [100, 200, 300]
const [foo, ...reset] = arr
console.log(reset)

// 赋值
const [foo, bar, baz = 123, more = 'test' ] = arr;
console.log(more)

对象解构

const obj = { name: '123', age: 12 }
const { name } = obj
console.log(name)

但是如果有个重名的name 
const name = 'memo'
const { name: objName = 'jack' } = obj  //这样就不会重名了
console.log(objName)

场景:
如果代码中出现了大量的console.log
const { log } = console
log('123')
这样我们代码的整体的体积会减少很多

函数参数解构:其实就是运用上面的对象解构和数组解构规则;

function move({x = 0, y = 0} = {}) {
    console.log([x, y])
    return [x, y];
}
move({x: 1, y: 2})  // [3, 8]
move({x: 3})        // [3, 0]
move({})            // [0, 0]
move()              // [0, 0]

模板字符串

模板字符串使用两个反引号标识(``),可以用来定义多行字符串,或者使用它在字符串中插入变量:

let name = 'hero'
let tips = `Hello ${name} ---- ${ 1 + 2 }, 
    welcome to my world.`
console.log( tips )

标签模板

const str = console.log`hello world`

const name = 'element'
const gender = true
function myFunc(strings, name, gender) {
	const sex = gender ? 'man' : 'woman'
	return strings[0] + name + string[1] + gender + strings[2]
}
const result = myFunc`hey, ${ name } is a ${ gender }.`

场景:这种模板字符串可以实现多语言(如:中英文切换)

参数默认值

之前的方式

function foo(enable) {
	enable = enable === undefined ? true : ''
	console.log(enable)
}
foo(false)

有了参数默认值

function foo(enable = true) {
	console.log(enable)
}
foo(false)


多个参数
带参数默认值的要放到最后
function foo(bar, enable = true) {
	console.log(enable)
}
foo(false)

剩余参数

之前的方式我们利用伪数组,arguments

function foo() {
	console.log(arguments) // Arguments(4) [1, 2, 3, 4, callee: ƒ, Symbol(Symbol.iterator): ƒ]
}
foo(1,2,3,4)

现在

function foo(...args) {
	console.log(args)
}
foo(1,2,3,4)

展开数组

const arr = ['foo', 'bar', 'baz']
console.log.apply(console, arr)


现在简化了操作
console.log(...arr) 

箭头函数

箭头函数语法比函数表达式更简洁,并且没有自己的 this、arguments,不能用作构造函数和用作生成器。

const arr = [1, 2, 3, 4, 5, 6]
arr.filter(i => i % 2)


const person = {
	name: 'tom',
    sayHi: () => {
    	console.log(`hi, my name is ${this.name}`)  //undefined
    },
    sayHiName: () => {
    	setTimeout(() => {
        	console.log(this.name)
        }, 1000)
    }
}
person.sayHiName()

对象字面量增强

const bar = 345
const obj = {
	//bar: bar,
    bar,
    methond() {
    	console.log(this)
    },
    [Math.random()]: 123, // 计算属性名 Math.random()的执行结果将作为属性名
    [bar]: '23ew'
}
obj[Math.random()] = 123

对象扩展方法

Object.assign()

const source = {
	a: 123,
    b: 345
}
const source1 = {
	a: 789,
    b: 789
}
const target = {
	a: 678,
    c: 908
}
const result = Object.assign(target, source, source1)


在看一个例子
function func(obj) {
	obj.name = 'tom'
   	console.log(obj)
}
const obj = { name: 'lili' }
func(obj)
console.log(obj)
这样外面的obj也改变了

改进:
function func(obj) {
	const objNew = Object.assign({}, obj)
	objNew.name = 'tom'
   	console.log(objNew)
}
const obj = { name: 'lili' }
func(obj)
console.log(obj)

Object.is() // 判断两个值是否相等 这个用的少,一般还是要用严格意义的相等===

Object.is(+0, -0)
Object.is(NaN, NaN)

proxy 代理对象

object.defineProperty

const person = {
	name: 'li',
    age: 25
}
cosnt personProxy = new Proxy(person, {
	get(target, property) {
    	return property in target ? target[property] : 'defalut'
    },
    set(target, property, value) {
    	if(property === 'age') {
        	if(!Number.isInteger(value)) {
            	throw new TypeError(`${value} is not an int`)
            }
        }
    	target[property] = value
    }
})
personProxy.gender = true
console.log(personProxy.name)

proxy优势 defineProperty只能监听到对象属性的读写

proxy优势能监视到更多对象操作例如:delete操作,

const person = {
	name: 'li',
    age: 25
}
cosnt personProxy = new Proxy(person, {
	deleteProperty(target, property) {
    	delete target[property]
    }
})

proxy是以非侵入方式监管了对象的读写

Reflect (静态类)

Reflect内部封装了一系列对对象的底层操作

const person = {
	name: 'li',
    age: 25
}
console.log('name' in person)
console.log(delete obj['age'])
console.log(Object.keys())

console.log(Reflect.has(obj, 'name'))
console.log(Reflect.deleteProperty(obj, 'age'))
console.log(Reflect.ownKeys(obj))

Set数据结构

const s = new Set()
s.add(1).add(2).add(3).add(4).add(2)
console.log(s)

console.log(s.size)

console.log(s.has(100)) // 是否包含某个值
console.log(s.delete(3)) // 删除某个值
s.clear() //全部请求

场景:
数组去重
const arr = [1,2,3,4,1,3]
const result = Array.from(new Set(arr))

const result = [...new Set(arr)]
console.log(result)

Map 数据结构 (严格意义上的键值对集合)

const obj = {}
obj[true] = 'value'
obj[123] = 'value'
obj[{ a: 1 }] = 'value'
console.log(Object.keys(obj))  // ['123', 'true', '[object, objecct]']




const m = new Map()
const tom = { name: 'tom' }
m.set(tom, 90)  // 存数据
console.log(m)

m.get(tom)
m.has()
m.delete()
m.clear()

m.forEach((vlaue, key) => {
	console.log(value, key)
})

Symbol

shared.js

const cache = {}

a.js
cache['foo'] = Math.random()

b.js
cache['foo'] = '124'

解决这种问题
约定一下
b.js
cache['b_foo'] = '123'

如果有人不遵守这种约定怎么办?

Symbol() 创建的每一个都是独一无二的
console.log(Symbol() === Symbol()) // false


场景: 模拟对象的私有成员
const name = Symbol()
const person = {
	[name]: '234',
    say() {
    	console.log(this[name])
    }
}
person[Symbol] // 不能通过这种方式调用,每个Symbol() 都是不一样的
person.say()

补充

const s1 = Symbol.for('foo')
const s2 = Symbol.for('foo')
console.log(s1 === s2) // true
console.log(Symbol.for(true) === Symbol.for('true'))  // true

Promise (一种更优的异步编程)

const promise = new Promise(function (resolve, reject) {
	resolve(100)
    
    reject(new Error('promise rejected'))
})
promise.then(function (value) {
	console.log('resolve', value)
}, function(error) {
	console.log('rejected', error)
})

基本应用:
function ajax(url) {
	return new Promise(function (resolve, reject) {
    	let xhr = new XMLHttoRequest()
        xhr.open('GET', url)
        xhr.responseType = 'json'
        xhr.onload = function() {
        	if(this.status === 200) {
            	resolve(this.response)
            } else {
            	reject(new Error(this.statusText))
            }
        }
        xhr.send()
    })
}
let promise = ajax('www.www.com')
let pronise2 = promise.then(
	function onFullfilled(value) {
    	console.log('onFullfilled', value)
    },
    function onRejected(error) {
    	console.log('onRejected', error)
    }
)
解决回调地狱
ajax('api')
	.then(function (value)) {
    	console.log(111)
        return ajax('/api/a')
    })
    .then(function (value)) {
    	console.log(222)
        return ajax('/api/b')
    })
    .then(function (value)) {
    	console.log(333)
        return ajax('/api/c')
    })
    .catch(function (error) {
    	console.log(error)
    })

并行执行

const urls = [
    'https://api.github.com',
    'https://api.github.com/users',
    'https://api.github.com/uasdfs/asdfad'
]
const promises = urls.map(item => { axios(item).catch(e => ({})) })

const promise = Promise.allSettled(urls.map(item => axios(item)))
promise.then(res => console.log(res))

Async/Await

async function main () {
  try {
      const users = await ajax('/api')
      console.log(users)
  } catch (e) {
      console.log(e)
  }
}
main()

迭代器(Iterator)

const set = new Set(['foo', 'bar', 'baz'])
const iterator = set[Symbol.iterator]()
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())

其实这就是for of的实现原理

实现可迭代接口
const obj = {
	[Symbol.iterator]: function() {
    	return {
        	next: function() {
            	return{
                	value: 'test',
                    done: true
                }
            }
        }
    }
}

for(const item of obj) {
	console.log('循环体') // 不会被执行,写死了next的返回方法,第一次done就是true
}
改进一下:
const obj = {
	store: ['foo' , 'bar', 'baz'],
	[Symbol.iterator]: function() {
    	let index = 0
        const self = this
    	return {
        	next: function() {
            	const reult = {
                	value: self.store[index],
                    done: index >= self.store.length
                }
                index++
                return result
            }
        }
    }
}
for(const item of obj) {
	console.log('循环体', item) 
}

迭代器设计模式 场景: 你我谢童开发一个任务清单应用

// 我的代码 ==================

设计一个存放所有任务的对象
const todos = {
	life: ['eat', 'sleep', 'play doung'],
    learn: ['chinese', 'math', 'english']
}

// you的代码

呈现对象到界面上
for(const item of todos.life) {
	console.log(item)
}
等等
如果我的对象结构变了怎么办呢?

对外提供一个统一遍历接口
const todos = {
  life: ['eat', 'sleep', 'play doung'],
  learn: ['chinese', 'math', 'english'],
  each: function (callback) {
      const all = [].concat(this.life, this.learn)
      for(const item of all) {
        callback(item)
      }
  }
}

todos.each(function(item) {
	console.log(item)
})

用迭代器的方式
const todos = {
  life: ['eat', 'sleep', 'play doung'],
  learn: ['chinese', 'math', 'english'],
  [Symbol.iterator]: function() {
      const all = [...this.life, ...this.learn]
      let index = 0
      return {
          next: function() {
              value: all[index],
              done: index++ >= all.length
          }
      }
  }
}
for(const item of todos) {
	console.log(item)
}

生成器(Generator)

避免异步编程中回调嵌套过深,提供更好的异步编程解决方案

function *foo() {
  console.log('test')
  return 200
}
const result = foo()
console.log(result)
console.log(result.next())


function *foo() {
  console.log('111')
  yield 100
  console.log('222')
  yield 200
}


场景
自增id
function *createIdMaker() {
  let id = 1
  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)

Array.includes

const arr = ['foo', 1, NaN, fale]
console.log(arr.indexof(NaN)) // -1 不能查找NaN
console.log(arr.includes) // true

指数运算符

console.log(Math.pow(2, 10))

console.log(2 ** 10)

Object.values

const obj = {
	foo: 'value1',
    bar: 'value2'
}

console.log(Object.values()) // ['value1', 'value2' ]

Object.entries

console.log(Object.entries()) // [['foo', 'value1'], ['bar', 'value2']]

for(const [key, value]) of Object.entries(obj) {
	console.log(key, value)
}

Object.getOwnPropertyDescriptors

const p1 = {
  firstName: 'li',
  lastName: 'xiang',
  get fullName(){
      return this.firstName + ' ' + this.lastName
  }
 }
 console.log(p1.fullName)
 
 const p2 = Object.assign({}, p1)// 把fullName当成了一个普通的属性进行复制
 p2.firstName = 'wang'
 console.log(p2.fullName) 	// li xiang

const descriptors = Object.getOwnPropertyDescriptors(p1)
const p2 = Object.defineProperties({}, descriptors)
p2.firstName = 'wang'
console.log(p2.fullName) // wang xiang

String.prototype.padStart / String.prototype.padEnd

const obj = {
	html: 5,
    css: 23,
    javascript: 234
}

for(const [name, count] of Object.entries(obj)) {
	console.log(`${name.padEnd(16, '-')} | ${count.padStart(3, '0')}`)
}