笔记|ES6 新特性介绍

1、ECMAScript 与JavaScript

  • ECMAScript 也是一门脚本语言,就是JavaScript的语言本身,通常被看作JavaScript的标准化规范,实际上JavaScript是ECMAScript的扩展语言。
  • 因为ECMAScript 中只提供了最基本的语法,而JavaScript实现了ECMAScript 这种语言的标准,并且在此基础之上,做了些扩展。
  • 总之,在浏览器中的JavaScript = ECMAScript + web APIS提供的(BOM+DOM);在node环境中,JavaScript = ECMAScript + Node APIS(fs,net…)。笔记|ES6 新特性介绍
    文章图片
    笔记|ES6 新特性介绍
    文章图片
2、ECMAScript 的发展过程
  • 从2015年开始ES保持每年一个版本的迭代,并且从2015年开始,ECMAScript 决定不再按照版本号命名,而是使用发行年份命名。由于这样的决定是在ES2015诞生的过程当中产生的,所以很多人也习惯把ES2015称之为ES6。笔记|ES6 新特性介绍
    文章图片
3、ECMAScript 2015/ES6的新特性 重点介绍ES5.1基础之上的变化,这些变化可归纳为4类:
  • 解决原有语法上的一些问题或者不足
  • 对原有语法进行增强,使之变得更为便捷、易用
  • 全新的对象、全新的方法、全新的功能
  • 全新的数据类型和数据结构
let与块级作用域
  • 作用域:某个成员能够起作用的范围。
  • 在ECMAScript 2015之前,ES只有两种作用域(全局作用域、函数作用域)。
  • 在ECMAScript 2015中,新增了块级作用域。块:代码中用一对花括号{}包裹起来的范围。例如:
//块级作用域--if语句 if (true) { console.log('hello, baby'); }//块级作用域--for循环 for (let i = 0; i < 8; i++) { console.log(i); }

  • 以前块是没有独立的作用域的,导致我们在块中定义的成员,外部也可以访问到,这点对于复杂代码是非常不利的,也是非常不安全的。
if (true) { var bar = 'hello' } console.log(bar); //hello

  • 有了块级作用域,使用let定义变量,就解决了上述问题。
if (true) { let bar = 'hello' } console.log(bar); //ReferenceError: bar is not defined

  • let这个关键字的特性非常适合我们声明for循环中的计数器。
//for循环内部有两层作用域 for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) { console.log(i); } console.log('内存结束 i = ' + i); } // 内存结束 i = 0 // 0 // 1 // 2 // 内存结束 i = 1 // 0 // 1 // 2 // 内存结束 i = 2

  • let不存在变量声明的提升。必须先声明再使用。
console.log(foo); //ReferenceError: Cannot access 'foo' before initialization let foo = 'aa'

const
  • ES2015中新增了一个const关键字,用来声明一个只读的恒量/常量。
  • 它的特点就是在let的基础上多了【只读】特性,只读就是变量一旦声明过后就不允许再被修改。
const name = 'aaa' name = 'bbb'//TypeError: Assignment to constant variable.

  • 既然const是恒量,那么const在声明的同时,就必须要去设置一个初始值。而且const不能像var一样,声明和赋值不能放在两个语句中。
  • const所声明的成员不能被修改,只是说不允许在声明过后,重新去指向一个新的内存地址,并不是说不允许修改恒量中的属性成员。
const obj = {} obj.name = 'eeee' console.log(obj); //{ name: 'eeee' }obj = {}//TypeError: Assignment to constant variable.

最佳实践:不用var,主用const,配合let 。
数组的解构
通过数组解构的方式,可以从数组当中去快速提取数组中的成员。(与元素的顺序有关,需通过元素的位置去匹配)
//数组解构 const arr = [2, 4, 5]//传统做法 const foo = arr[1] console.log(foo); //4//数组解构方式 const [, baz] = arr console.log(baz); //4//剩余参数 const [f, ...rest] = arr console.log(f); //2 console.log(rest); //[ 4, 5 ]//超过 const [one, two, three, more] = arr console.log(more); //undefined//默认值 const [a, b, c, d = 'ok'] = arr console.log(d); //ok//适用场景-如拆分字符串 const path = '/foo/bar/baz' // const tmp = path.split('/') // const dir = tmp[1]const [, dir] = path.split('/') console.log(dir); //foo

对象的解构
通过对象解构的方式,可以从对象当中去快速提取对象中的属性。(与属性的顺序无关,只需通过属性名去匹配)
//对象解构 const obj = { age: 18, class: '科技一班' } // const obj = { age: 18 } const { age } = obj console.log(age); //18//属性重命名 const { age: myAge } = obj console.log(myAge); //18//默认值 // const { class: myClass = '太空二班' } = obj // console.log(myClass); //太空二班//适用场景-简化console.log方法 const { log } = console log('foo') log('aaa') log('bbb') // foo // aaa // bbb

模板字符串字面量
模板字符串需要使用`来表示。模板字符串的新特性:
  • 支持换行,可以直接在模板字符串中敲入换行符
//支持换行 const str = `hello, 2020, we are here` console.log(str); // hello, 2020, // we are here

  • 支持插值表达式方式去字符串中插入JS表达式
//支持插入JS表达式 const name = 'jack' const msg = `hey, ${name}, welcome to ${new Date().getFullYear()}` console.log(msg); //hey, jack, welcome to 2020

  • 带标签的模板字符串:作用是对模板字符串进行加工
//带标签的模板字符串 const str = console.log`hello`//[ 'hello' ]const name = 'rock' const gender = truefunction myTagFunc(strings, name, gender) { // console.log(strings, name, gender); //[ 'hey,', ' is a ', '' ] rock male // return 123 const sex = gender ? 'man' : 'woman' return strings[0] + name + strings[1] + sex + strings[2] }const res = myTagFunc`hey,${name} is a ${gender}` // console.log(res); //123 console.log(res); //hey,rock is a man

字符串的扩展方法
  • includes()
  • startsWith()
  • endsWith()
//字符串的扩展方法 const msg = 'Error: a is not defined.'console.log(msg.startsWith('Error')); //true console.log(msg.endsWith('ok')); //false console.log(msg.includes('a')); //true

函数参数的默认值
//函数参数的默认值 function foo(bar, enable = true) { console.log(bar, enable); } foo(3) //3 true foo(3, false) //3 false

剩余参数
ES2015新增了个…操作符,有两个作用:
  • rest作用,即剩余操作符
//以前的做法 function foo() { console.log(arguments); } foo(1, 2, 3)//[Arguments] { '0': 1, '1': 2, '2': 3 },伪数组//剩余参数做法 function bar(first, ...args) { console.log(first, args); } bar(3, 4, 5)//3 [ 4, 5 ]

  • spread作用,即展开操作符
//展开数组参数 const arr = ['foo', 'bar', 'baz']console.log( arr[0], arr[1], arr[2] ); //foo bar baz//以前做法 console.log.apply(console, arr); //foo bar baz //ES6 console.log(...arr); //foo bar baz

箭头函数
//以前定义函数 function func(num) { return num + 2 } //箭头函数 const func1 = n => n + 2console.log(func(3)); //5 console.log(func1(3)); //5//使用箭头函数大大简化了回调函数的编写 const arr = [3, 2, 4, 6, 7] const even = arr.filter(function (item) { return item % 2 })console.log(even); //[ 3, 7 ]const even1 = arr.filter(item => item % 2) console.log(even1); //[ 3, 7 ]

箭头函数会让我们的代码变的更简短而且更易读,并且,箭头函数不会改变this的指向。
const person = { name: 'Rock', sayHi1: function () { console.log(`Hi, this is ${this.name}`); }, sayHi2: () => { console.log(`Hi, this is ${this.name}`); }, sayHiAsync1: function () { setTimeout(function () { console.log(this.name); }, 1000) }, sayHiAsync2: function () { setTimeout(() => { console.log(this.name); }, 1000) } } person.sayHi1()//Hi, this is Rock person.sayHi2()//Hi, this is undefined person.sayHiAsync1()//undefined person.sayHiAsync2()//Rock

对象字面量增强
ECMAScript 2015升级了对象字面量的语法。
//对象字面量 const bar = 123 const obj = { foo: 34, bar, func: function () { console.log(this.foo); }, func2() { console.log(this); }, [bar]: 888 }//可以使用表达式返回值作为对象的属性名 obj[Math.random()] = 123console.log(obj); //{ '123':888, foo: 34, bar: 123, func: [Function: func], func2: [Function: func2], '0.5719176856362458': 123} obj.func2()//{ '123':888, foo: 34, bar: 123, func: [Function: func], func2: [Function: func2], '0.5719176856362458': 123}

对象扩展方法
  • Object.assign():将多个源对象中的属性复制到一个目标对象中。
    const source = { a: 2, b: 3 }const source1 = { c: 222, d: 21 }const target = { a: 888, c: 999 }const res = Object.assign(target, source, source1) console.log(res); //{ a: 2, c: 222, b: 3, d: 21 } console.log(res === target); //true//可以用来复制对象 function func(obj) { obj.name = 'func obj' console.log(obj); }function func2(obj) { const funcObj = Object.assign({}, obj) funcObj.name = 'func obj' console.log(funcObj); }const obj = { name: 'global obj' } // func(obj)//{ name: 'func obj' } // console.log(obj); //{ name: 'func obj' }func2(obj)//{ name: 'func obj' } console.log(obj); //{ name: 'global obj' }

  • Object.is():用来去判断两个值是否相等。
    console.log( 0 == false,//true 0 === false, //false +0 === -0,//true NaN === NaN,//false Object.is(+0, -0),//false Object.is(NaN, NaN)//true );

Proxy(代理对象)
  • 想要监视一个对象中属性的读写,可以使用Object.defineProperty来为对象添加属性。
  • 在ECMAScript 2015中设计了Prixy,专门用来为对象设置访问代理器。可看作门卫,使用它,可以轻松监视到对象的读写过程。
    const person = { name: 'Rock', age: 5 }const personProxy = new Proxy(person, { get(target, prop) { return prop in target ? target[prop] : 'default' }, set(target, prop, value) { if (prop === 'age') { //校验 if (!Number.isInteger(value)) { throw new TypeError(`${value} is not int`) } } target[prop] = value } })console.log(personProxy.name); //Rock console.log(personProxy.xxx); //default// personProxy.age = 'ss' //TypeError: ss is not int personProxy.age = 20 console.log(personProxy); //{ name: 'Rock', age: 20 }

  • Proxy的优势:
    1. Proxy更为强大:defineProperty只能监视属性的读写,Proxy能够监视到更多对象操作。
      //delete操作 const personProxy2 = new Proxy(person, { defineProperty(target, prop) { delete target[prop] } })delete personProxy.age console.log(person); //{ name: 'Rock' }

      Proxy的其他对象操作 : 笔记|ES6 新特性介绍
      文章图片
    2. Proxy更好的支持数组对象的监视
      //数组操作 const list = [] const listProxy = new Proxy(list, { set(target, prop, value) { console.log('set', prop, value); target[prop] = value return true } }) listProxy.push(100) // set 0 100 // set length 1

    3. Proxy是以非侵入的方式监管了对象的读写。
Reflect
  • 统一的对象操作API。
  • Reflect属于一个静态类,不能通过new Reflect()方式去构造一个实例对象,只能去调用静态类中的方法,如Reflect.get()。
  • Reflect内部封装了一系列对对象的底层操作,提供了13个静态方法。
  • Reflect成员方法就是Proxy处理对象的默认实现。
    const obj = { a: 123, b: '346' }const proxy = new Proxy(obj, { get(target, prop) { return Reflect.get(target, prop) } })console.log(proxy.a); //123

  • 统一提供了一套用于操作对象的API
    const obj2 = { name: 'Rock', age: 20 } //传统做法 console.log('name' in obj2); //true console.log(delete obj2['age']); //true console.log(Object.keys(obj2)); //[ 'name' ]//Reflect方式 console.log(Reflect.has(obj2, 'name')); //true console.log(Reflect.deleteProperty(obj2, 'age')); //true console.log(Reflect.ownKeys(obj2)); //[ 'name' ]

    Reflect的对象操作: 笔记|ES6 新特性介绍
    文章图片
Promise
提供了一种更优的异步编程解决方案,通过链式编程方式解决了传统异步编程中回调函数嵌套过深的问题。
class类
在此之前,ECMAScript中都是通过定义函数以及函数的原型对象来去实现的类。
//传统实现类--通过函数及函数的原型对象 function Person(name) { this.name = name } Person.prototype.say = function () { console.log(`hi, my name is ${this.name}`); }const person = new Person('Jack') person.say()//hi, my name is Jack//ES6实现 class Person1 { constructor(name) { this.name = name } say() { console.log(`hi, my name is ${this.name}`); } } const per = new Person1('Rock') per.say()//hi, my name is Rock

静态方法和实例方法
实例方法:需要通过这个类构造的实例对象去调用。
静态方法:直接通过这个类本身去调用。
ES2015中新增添加静态成员与静态方法的static关键字。
//静态方法和实例方法 class Person { constructor(name) { this.name = name } //实例方法 say() { console.log(`hi, my name is ${this.name}`); } //静态方法 static create(name) { return new Person(name) } } const tom = Person.create('Tom') tom.say()//hi, my name is Tom

类的继承
继承是面向对象中一个非常重要的特性。通过继承能够抽象出来相似类型之间重复的地方。
ES2015之前,大多数情况都是使用原型实现继承,但在Es2015中新增关键字extends关键字实现继承。
class Person { constructor(name) { this.name = name } //实例方法 say() { console.log(`hi, my name is ${this.name}.`); } }class Student extends Person { constructor(name, age) { super(name) this.age = age } hello() { super.say() console.log(`my age is ${this.age}.`); } }const student = new Student('Jerry', 6) student.hello() // hi, my name is Jerry. // my age is 6.

Set
可以理解为集合,与传统的数组比较类似,但是Set中的元素不允许重复。每一个值在Set当中都是唯一的。
//Set数据结构 const set = new Set() set.add(1).add(2).add(3).add(4) console.log(set); //Set(4) { 1, 2, 3, 4 }//遍历 set.forEach(i => console.log(i)) // 1 // 2 // 3 // 4//for...of遍历 for (let item of set) { console.log(item); } // 1 // 2 // 3 // 4//获取set的长度 console.log(set.size); //4//判断是否包含某个元素 console.log(set.has(19)); //false//删除元素 console.log(set.delete(3)); //true//清空元素 set.clear() console.log(set); //Set(0) {}

常见应用场景:为数组去重
//数组去重 const arr = [1, 2, 3, 2, 4, 1, 2, 5] const result = new Set(arr) console.log(result); //Set(5) { 1, 2, 3, 4, 5 }//set转换为数组 const resArr1 = Array.from(new Set(arr)) const resArr2 = [...new Set(arr)] console.log(resArr1, resArr2); //[ 1, 2, 3, 4, 5 ] [ 1, 2, 3, 4, 5 ]

Map
与ECMAScript中对象非常类似,都是键值对集合,ECMAScript中键只能为字符串类型,不方便存放复杂数据结构。
//ES6以前 const obj = {} obj[true] = 'value' obj[123] = 'value' obj[{ a: 1 }] = 'value' console.log(Object.keys(obj)); //[ '123', 'true', '[object Object]' ] console.log(obj['[object Object]']); //value

Map用来去映射两个任意类型数据之间的映射关系。与对象最大的区别就是,它可以用任意类型的数据作为键,而对象只能用字符串作为键。
//ES6 const map = new Map() const jack = { name: 'jack' } map.set(jack, 90) map.set('age', 19) console.log(map); //Map(1) { { name: 'jack' } => 90, 'age' => 19 } console.log(map.get(jack)); //90console.log(map.has(jack)); //true // console.log(map.delete('age')); //true // map.clear() // console.log(map); //Map(0) {}//遍历 map.forEach((value, key) => { console.log(value, key); }) // 90 { name: 'jack' } // 19 age

Symbol
一种全新的原始数据类型
在ES2015之前,对象的属性名都是字符串,而字符串是有可能重复的,容易引起冲突。
ES2015为了解决这个问题,提供了一种全新的数据类型,叫Symbol(符号)。作用是表示一个独一无二的值。
最主要的作用就是为对象添加独一无二的属性名。
//ES2015以前 //store.js const cache = {} //a.js cache['a_foo'] = Math.random() //b.js cache['b_foo'] = '123'console.log(cache); //{ a_foo: 0.8887530628553206, b_foo: '123' }//ES2015 const s = Symbol() console.log(s); //Symbol() console.log(typeof s); //symbol console.log(Symbol() === Symbol()); //false

截止到ES2019,一共定义了7种原始数据类型,未来还会新增BigInt原始数据类型,用于去存放更长的数字。目前处于stage-4阶段,预计下个版本被标准化,标准化过后就是8种数据类型了。
每次通过Symbol去创建的值一定是唯一的,如果想全局复用一个相同的Symbol值,可以使用全局变量方式或使用Symbol提供的静态方法:
console.log(Symbol('foo')); //Symbol(foo) console.log(Symbol('bar')); //Symbol(bar) console.log(Symbol('car')); //Symbol(car)const obj = {} obj[Symbol()] = '123' obj[Symbol()] = '456' console.log(obj); //{ [Symbol()]: '123', [Symbol()]: '456' }const obj1 = { [Symbol()]: 888 } console.log(obj1); //{ [Symbol()]: 888 }//a.js const name = Symbol() const person = { [name]: 'abc', say() { console.log(this[name]); } } //b.js person.say()//abcconst obj2 = { [Symbol()]: 'symbol value', foo: 'normal value' } //不能遍历普通对象 // for (let i of obj2) { //console.log(i); //TypeError: obj is not iterable // } console.log(Object.keys(obj2)); //[ 'foo' ] console.log(JSON.stringify(obj2)); //{"foo":"normal value"}console.log(Object.getOwnPropertySymbols(obj2)); // {"foo":"normal value"} // [ Symbol() ]

提供了一些内置的Symbol常量:
  • Symbol.iterator
  • Symbol.hasInstance
for…of
在ECMAScript中遍历数据的方法:
  • for:遍历普通的数组
  • for…in:遍历键值对
  • 一些对象的遍历方法(如 forEach等)
    这些遍历方式都有一定的局限性,E2015引入了全新的for…of循环。这种遍历方式以后会作为遍历所有数据结构的统一方式。
const obj = { [Symbol()]: 'symbol value', foo: 'normal value' } //不能遍历普通对象 // for (let i of obj) { //console.log(i); //TypeError: obj is not iterable // }const arr = [23, 45, 67, 87]// for (const item of arr) { //console.log(item); // } // 23 // 45 // 67 // 87//forEach不能跳出循环 // arr.forEach(item => console.log(item))//for...of能跳出循环 for (const item of arr) { console.log(item); if (item > 50) { break } } // 23 // 45 // 67const s = new Set(['aaa', 'bbb']) for (let item of s) { console.log(item); } // aaa // bbbconst m = new Map() m.set('foo', 123) m.set('bar', 345) for (let [key, value] of m) { console.log(key, value); } // foo 123 // bar 345

可迭代接口
ES中能够表示有结构的数据类型越来越多,为了给各种各样的数据结构提供统一的遍历方式,ES2015提供了Iterable接口,即可迭代的。实现Iterable接口就是for…of的前提。
//迭代器(iterator) const set = new Set(['aa', 'bb', 'cc']) const iterator = set[Symbol.iterator]()console.log(iterator.next()); //{ value: 'aa', done: false } console.log(iterator.next()); //{ value: 'bb', done: false } console.log(iterator.next()); //{ value: 'cc', done: false } console.log(iterator.next()); //{ value: undefined, done: true }

只要对象实现了Iterator,那对象也可以使用for…of方法。
//实现可迭代接口 const obj = { //iterable [Symbol.iterator]: function () { //iterator return { next: function () { //IterationResult return { value: 'aaa', done: true } } } } }const obj2 = { store: ['foo', 'bar', 'baz'],[Symbol.iterator]: function () { let index = 0 const self = thisreturn { next: function () { const result = { value: self.store[index], done: index >= self.store.length } index++ return result } } } }for (const item of obj2) { console.log('循环体', item) } // 循环体 foo // 循环体 bar // 循环体 baz

for…of可以取代forEach,而且for…of内可以使用break。
迭代器模式
迭代器的核心就是对外提供统一遍历接口,让外部不用再去关心内部这个数据结构是啥样的。
// 场景:你我协同开发一个任务清单应用 //实现代码 const todos = { life: ['阅读', '运动', '音乐'], learn: ['JS', '算法', 'Vue'], work: ['编码'], // 提供统一遍历访问接口 each: function (callback) { const all = [].concat(this.life, this.learn, this.work) for (const item of all) { callback(item) } }, // 提供迭代器(ES2015 统一遍历访问接口) [Symbol.iterator]: function () { const all = [...this.life, ...this.learn, ...this.work] let index = 0 return { next: function () { return { value: all[index], done: index++ >= all.length } } } } }// for (const item of todos.life) { //console.log(item) // } // for (const item of todos.learn) { //console.log(item) // } // for (const item of todos.work) { //console.log(item) // }todos.each(function (item) { console.log(item) })console.log('-------------------------------')for (const item of todos) { console.log(item) } // 阅读 // 运动 // 音乐 // JS // 算法 // Vue // 编码

生成器(Generator)
  • 避免异步编程中回调嵌套过深,提供更好的异步编程解决方案。
  • 生成器对象其实也实现了Iterator接口协议。
    function* foo() { console.log('qqq')//qqq return 100 }const result = foo() console.log(result.next())//{ value: 100, done: true }

  • 生成器一般与yield关键字配合使用。
    function* foo() { console.log('1111') yield 100 console.log('2222') yield 200 console.log('3333') yield 300 }const generator = foo()console.log(generator.next()) // 第一次调用,函数体开始执行,遇到第一个 yield 暂停 console.log(generator.next()) // 第二次调用,从暂停位置继续,直到遇到下一个 yield 再次暂停 console.log(generator.next()) // 。。。 console.log(generator.next()) // 第四次调用,已经没有需要执行的内容了,所以直接得到 undefined // 1111 // { value: 100, done: false } // 2222 // { value: 200, done: false } // 3333 // { value: 300, done: false } // { value: undefined, done: true }

  • 生成器函数会自动帮我们返回一个生成器对象,调用该对象的next方法才会让这个生成器函数的函数体开始执行,遇到yield函数执行会暂停下来。yield后的值会作为next()的返回值。
    // Generator 应用 // 案例1:发号器 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) // 1 // 2 // 3 // 4// 案例2:使用 Generator 函数实现 iterator 方法 const todos = { life: ['阅读', '运动', '音乐'], learn: ['JS', '算法', 'Vue'], work: ['编码'], [Symbol.iterator]: function* () { const all = [...this.life, ...this.learn, ...this.work] for (const item of all) { yield item } } }for (const item of todos) { console.log(item) } // 阅读 // 运动 // 音乐 // JS // 算法 // Vue // 编码

惰性执行。
4、ECMAScript 2016概述 发布于2016/6,是个小版本,仅实现了两个特性:
  • includes()
    const arr = [2,3,'aa',4] console.log(arr.indexOf(2)); //0 console.log(arr.includes(1)); //-1 console.log(arr.includes(2)); //true

  • 指数运算符
    console.log(Math.pow(2,10)); //1024 console.log(Math.pow(2,10)); //1024

5、ECMAScript 2017概述 【笔记|ES6 新特性介绍】发布于2017/6,也是小版本,带来了新功能:
  • Object对象的三个方法
    Object.values()、Object.entries()、Object.getOwnPropertyDescriptors()(获取对象中完整信息的描述)
  • 字符串填充方法
    String.prototype.padStart()、String.prototype.padEnd()
  • 允许函数参数中添加尾逗号
  • Async/Await
附录
  • ECMAScript 2015 Language Specification
  • Flow
  • Flow-type

    推荐阅读