进阶学习9:ECMAScript——概述ES2015 / ES6新特性详解

Posted JIZQAQ

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进阶学习9:ECMAScript——概述ES2015 / ES6新特性详解相关的知识,希望对你有一定的参考价值。

目录

一、ECMAScript概述

二、ES2015 / ES6 概述

三、学习前的准备工作

Node.js 

Nodemon 

运行方式

四、ES2015 新特性

1.let与块级作用域

let 与 var的区别

例子1

例子2

例子3

例子4

总结

2.ES2015 Const

特性1

特性2

特性3

实际使用中的建议

3.数组的解构

4.对象的解构

用法和特点

重命名

实际应用

5.模板字符串

使用特点

6.带标签的模板字符串

7.字符串的扩展方法

8.参数的默认值

9.剩余参数

10.展开数组

11.箭头函数

用法

特性

12.对象字面量增强

13.对象扩展方法

Object.assign()

is

14.Proxy

基本使用

Proxy对比Object.defineProperty()

15.Reflect

16.Promise

17.Class类

18.静态方法Static

19.类的继承Extends

20.Set 数据结构

21.Map

22.Symbol

特性

全局复用同一个Symbol

23.for...of循环

基本用法

24.For...of原理 Iterable 接口

实现可迭代接口

迭代器设计模式

25.Generator生成器函数

生成器实际运用

26.Modules


一、ECMAScript概述

ECMAScript也是一门脚本语言,一般缩写为ES,通常我们把他看作为javascript的标准化规范。但实际上,JavaScript是ECMAScript的扩展语言,因为ES只提供了最基本的语法。

总的来说,在浏览器环境当中JavaScript = ECMAScript + BOM + DOM。在Node.js环境呢JavaScript = ECMAScript + fs + net + etc.

从2015年开始,ES保持着每年一个大版本的迭代。其实从ES2015开始,ES已经不再按照版本号命名,而是ES+年份。

二、ES2015 / ES6 概述

ES2015,也是我们常听到的ES6,是最新ECMAScript标准的代表版本。一是ES2015相比较ES5.1来说变化比较大,这两个版本发布时间中间隔了6年之久。二是命名也发生了变化,以前都是以版本号命名,而这一版开始准确的名称应该叫做ES2015。有很多开发者喜欢用ES6来泛指ES2015以来所有的新版本,但是有的开发者又仅称呼ES2015为ES6,所以在我们平时搜集资料的时候,如果看到ES6的称呼,可能需要做一下区别到底是泛指还是特指。

下面给出的是ES2015的官方文档,里面不仅有新特性,还有相关的语言规范。

ECMAScript® 2015 Language Specification

https://262.ecma-international.org/6.0/

三、学习前的准备工作

Node.js 

因为我之前已经安装过了,这边就不再放过程了,有需要的话,可以点下面的链接看怎么安装。

如何直接运行Js文件?Mac Node.js安装使用教程

https://blog.csdn.net/qq_43106115/article/details/116429183?spm=1001.2014.3001.5501

Nodemon 

这个工具是用来帮助我们修改完代码以后自动执行,安装命令如下:

npm install nodemon -g

运行方式

把我们平时运行node的node字样改成nodemon就行,文件发生变化之后脚本会立即重新执行。

 

四、ES2015 新特性

1.let与块级作用域

在ES2015之前,ES中只有前两种作用域,块级作用域是新增的。

  • 全局作用域
  • 函数作用域
  • 块级作用域

那么,什么是块呢?块,就是我们代码中,由{ }包裹起来的范围。

let 与 var的区别

以前块中没有独立作用域,也就是说我们在块中定义的成员,在外部也能被访问到。而现在我们可以通过使用let来让作用域变成块级作用域。

  • 例子1

if(true){
    var foo = 'test'
}
console.log(foo)

可以看到,我们即时是在外部,同样访问到了foo

把前面的var改为let

if(true){
    let foo = 'test'
}
console.log(foo)

好了控制台报not defined的错了

  • 例子2

其实很适合我们用来做循环的计数器,再看看下面这个例子

//DEMO2
for(var i = 0;i<3;i++){
    for(var i = 0;i<3;i++){
        console.log(i)
    }
    console.log('内层结束 i = '+i)
}

实际上并没有执行到9次,就是因为内外层i重名了。 

把内层的声明i改成let之后

for(var i = 0;i<3;i++){
    for(let i = 0;i<3;i++){
        console.log(i)
    }
    console.log('内层结束 i = '+i)
}

正常执行。但是在实际应用开发的过程中,即时使用let,也不建议大家嵌套的循环使用相同名字的计数器,这不便于以后代码的阅读。

  • 例子3

开始只看下面这段代码的时候,感觉i应该会有冲突了。

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

然而神奇的是,从输出结果来看并没有冲突到

这段代码,我们可以把它拆解为if的方式来理解,写成下面这种形式之后就比较明显了,其实我们循环里的i是在外部的,而我们let i = ‘foo’的作用域是在这个if内部的。所以这两个作用域是不一样的。

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++
  • 例子4

首先还是看一下下面的例子

console.log('var',foo)
var foo = 'test'
console.log('let',foo2)
let foo2 = 'test'

从输出的结果里面可以看得出,即时我们都是在声明之前已经打印变量,但是var声明的变量是不会报错的,只是返回undefined。这个呢,我们管它叫做变量声明的提升

而let是不存在变量声明的提升的,使用变量之前必须先声明变量。

总结

  • var的作用域是全局的,let的作用域是块级的
  • 不同块级中存在同名的块级作用域变量,它们相互之间是不会冲突的。
  • var存在变量声明提升的特性,也就是说在代码上可以看起来是先使用后声明。而let不行,只能先声明再使用。

2.ES2015 Const

特性1

const在let的基础上多了一个只读的特性,也就是变量一旦被声明就不能再进行修改。

//DEMO5
const name = 'zce'
name = 'jack'

特性2

const声明的时候就要设置初始值,不能像var一样声明和赋值放在两个语句当中。

const name
name = 'jack'

特性3

const只是声明过后不允许重新去指向一个新的内存地址,并不是说不允许我们修改恒量中的属性成员

const obj = {}
obj.name = 'test'
console.log(obj)

实际使用中的建议

不使用var,主用const,配合let

3.数组的解构

可以用数组的解构快速提取元素。结构相关的用法和特性,我都在下面代码的注释中标记了。

//DEMO6
const arr = [100, 200, 300]
//以前的办法
const foo = arr[0]
const bar = arr[1]
const baz = arr[2]
console.log(foo,bar,baz)
//解构的办法
const [foo2, bar2, baz2] = arr
console.log(foo2,bar2,baz2)
//解构的时候还可以前面不填,但是保留逗号表示对应的位置
const [, , baz3] = arr
console.log("baz3",baz3)
//可以用...rest来表示剩下的位置,这种用法只能在解构最后一个位置上使用
const [foo4, ...rest] = arr
console.log("foo4",foo4)
console.log("rest",rest)
//如果解构数量少于成员数,那就从前到后被提取
const [foo5] = arr
console.log("foo5",foo5)
//如果解构数量大于成员数,那超出的就是undefined
const [foo6,bar6,baz6,more] = arr
console.log("more",more)
//还可以结构的时候给赋上默认值
const [foo7,bar7,baz7 = 123,more2 = "default value"] = arr
console.log("baz7",baz7)
console.log("more2",more2)

输出结果

4.对象的解构

用法和特点

对象的解构很多特点和数组的解构也是一致的,如:

  1. 没有匹配到的成员返回undefined
  2. 可以设置默认值

重复的这边就不做演示了

//DEMO7
const obj = { name:'test',age: 25}
const { name } = obj
console.log(name)

但是要注意,像下面这种情况,有同名的变量,就会造成冲突

const obj = { name:'test',age: 25}
const { name } = obj
const name = 'tom'
console.log(name)

重命名

这种时候可以靠重命名的方法来解决冲突,重命名后面也是可以直接跟上默认值的,这边就不做演示了,和上面数组的结构用法一致。

const obj = { name:'test',age: 25}
//冒号左边是拿来匹配提取对应值的,右边是重命名的名称
const { name: objName } = obj
const name = 'tom'
console.log(objName)
console.log(name)

实际应用

像是我们平时常用的console.log()方法,也可以这么提前解构出来,这么再使用的时候代码就会简洁很多【也能少敲不少字

const {log} = console
log('TEST!')

5.模板字符串

传统定义字符串是靠单引号或者双引号,现在新增了反引号`【就是键盘1左边那个按键】

使用特点

  • 可以支持多行字符串
  • 可以使用插值表达式
//DEMO8
const str_old = 'hello \\nworld!'
console.log(str_old)
//可以直接换行,不用输入\\n
const str_new = `hello 
world!`
console.log(str_new)

//插值表达,比以前拼接方便,差值里面还可以做运算
const name = 'Tom'
const msg = `Hey ${name}! --- ${ 1+2} --- ${Math.random()}`
console.log(msg)

输出结果: 

6.带标签的模板字符串

带标签的模板字符串的标签,其实是个标签函数,对我们的字符串做加工用的。

先看看下面这个例子

//DEMO9
const str = console.log`Hello World`

如果是第一次接触这个的话,可能会感觉到意外,因为输出的并不是字符串而是数组的形式,那么具体是为什么,看接下来的例子。

const name =  'tom'
const gender = true
function myTagFunc(strings) {
    console.log(strings)
}
const result = myTagFunc`hey, ${name} is a ${gender}`

这看起来就比较好理解了,其实是模板字符串中可能会有嵌入的表达式,会按照表达式被分割成数组。

在这个函数里,我们还可以拿到插值表达式的返回值,也能return数据

const name =  'tom'
const gender = true
//strings 是按照插值表达式作为分割的数组,函数也能获得插值表达式里的数值
function myTagFunc(strings, name, gender) {
    console.log(strings,name,gender)
    //函数内部还可以返回值,返回什么我们的结果就是什么
    //return 123
    //如果需要返回正常的结果的话
    return strings[0] + name + strings[1] + gender + strings[2]
}
const result = myTagFunc`hey, ${name} is a ${gender}`
console.log(result)

7.字符串的扩展方法

  • includes()
  • startsWith()
  • endsWith()
//DEMO10
const msg = 'Error: foo is not defined.'
console.log(msg.startsWith('Error'))
console.log(msg.startsWith('Error:'))
console.log(msg.startsWith('foo'))
console.log(msg.endsWith('.'))
console.log(msg.endsWith('defined.'))
console.log(msg.endsWith('not'))
console.log(msg.includes('not'))
console.log(msg.includes('test'))

结果如下:

我试了一下,他这个startsWith和endsWith呢,只要是从开头开始连着,无论是几位,只要都能匹配得上的都能算true。endsWith就是从末尾往前数同理、

8.参数的默认值

具体使用方法和需要注意的我都写在代码的注释中了。

//DEMO11
//参数默认值
//老办法
function foo (enable) {
    //没穿传递实参数的时候调用
    enable = enable === undefined ? true : enable
    console.log('foo invoked - enable: ')
    console.log("enable1",enable)
}
foo()
//新办法,但是如果有多个参数的话,带有默认值的参数一定要放在最后否则有问题.因为参数是按照顺序传递的
function foo2 (enable = true, bar) {
    console.log('foo invoked - enable2: ')
    console.log("enable2",enable)
    console.log("bar2",bar)

}
foo2(1)
function foo3 (bar,enable = true) {
    console.log('foo invoked - enable3: ')
    console.log("enable3",enable)
    console.log("bar3",bar)

}
foo3(1)

输出结果:

9.剩余参数

如果我们需要传入不限数目的参数的话,以前只能用arguments来接收,现在的话可以使用...args

//DEMO12
//剩余参数
//老办法
function foo (){
    console.log(arguments)
}
//新方法,但是只能出现在参数的最后一位,只能出现一次
function foo2 (...args){
    console.log(args)
}
foo(1,2,3,4)
foo2(1,2,3,4)

两者接收的数据格式还是略有区别的, arguments比较熟悉就不解释, ...args接收的的参数是一个数组。

10.展开数组

//DEMO13
const arr = ['foo', 'bar', 'baz']
//老办法1
console.log(
    arr[0],
    arr[1],
    arr[2]
)
//老办法2
console.log.apply(console, arr)
//新办法
console.log(...arr)

11.箭头函数

用法

箭头函数可以使得我们的代码更加简短易读。

多个传入用括号包裹,多条处理用花括号,但是一旦用了花括号就需要手动写return作为返回值。

//DEMO14
//箭头函数
//最简单的写法
const inc = n=> n + 1
console.log(inc(100))
//多个传入用括号包裹,多条处理用花括号,但是一旦用了花括号就需要手动写return作为返回值
const inc2 = (n, m)=> {
    console.log('inc:')
    return n + 1
}
console.log(inc2(100))

特性

箭头函数里面不改变this的指向,实例如下

//DEMO15
const person = {
    name: 'tom',
    sayHi: function(){
        console.log(`hi, my name is ${this.name}`)
    }
}
//箭头函数中没有this的机制,箭头函数外面的this是什么里面就是什么,this不发生改变。
person.sayHi()
const person2 = {
    name: 'tom',
    sayHi:() => console.log(`hi, my name is ${this.name}`),
    sayHiAsync: function () {
        //因为setTimeout是异步的,等里面log运行的时候的this是指向全局的,所以拿不到name,我们通过再定义一个_this来保存this,这就是闭包的机制
        const _this = this
        setTimeout(function (){
            console.log("this:",this.name)
            console.log("_this:",_this.name)
        },1000)
    },
    sayHiAsync2: ( function () {
        //使用箭头函数可以避免this问题
        setTimeout(() => {
            console.log("this =>:",this.name)
        },1000)
    })
}
person2.sayHi()
person2.sayHiAsync()
person2.sayHiAsync2()

12.对象字面量增强

  • 和已经声明变量相同的属性,可以省略冒号
  • object里面添加function的时候可以省略冒号和function的字样
  • 新增了计算属性名,也就是obj[计算式],方括号里面计算式的结果会作为属性名

看下面的例子会更加清晰一些

//DEMO16
//对象字面量增强
//老办法
const bar = '345'
const obj = {
    foo: 123,
    bar: bar,
    method: function(){
        console.log('method111'),
        console.log(this)
    },
    //不能这么用
    //Math.random():123
}
console.log(obj)
//新办法
const bar2 = '345'
const obj2 = {
    foo2: 123,
    //这里同名的可以不用加冒号了
    bar2,
    //function可以省略冒号和function字样
    method(){
        console.log('method222'),
        console.log(this)
    },

}
//计算属性名,方括号里面的执行结果会作为属性名
obj2[Math.random()] = 123
console.log(obj2)

输出结果:

13.对象扩展方法

Object.assign()

assign:将多个源对象中的属性复制到一个目标对象中

//DEMO17
const source1 = {
    a:123,
    b:123
}
const target = {
    a:456,
    c:456
}
//将右边源对象复制到左边的目标对象中去,assign的输出结果就是目标对象
const result = Object.assign(target,source1)
console.log(target)
console.log(source1)
console.log(result === target)
//如果有多个源对象,就是把右边的都往第一个里面去复制
const source2 = {
    b:789,
    d:789
}
console.log("————————————")
console.log(target)
console.log(source1)
console.log(source2)
console.log(result === target)

实际应用:

//这个办法会在里面把全局的也影响了
function func (obj) {
    obj.name = 'func obj'
    console.log('func obj:',obj)
}

const obj = {name:'global obj'}
func(obj)
console.log('obj',obj)
//使用assign之后全局的就不会被影响到
function func2 (obj2) {
    const funcObj = Object.assign({},obj2)
    funcObj.name = 'func obj'
    console.log('func obj2:',funcObj)
}

const obj2 = {name:'global obj'}
func2(obj2)
console.log('obj2',obj2)

结果:

这边老师虽然没有展开说,但是我听完之后想到了我们平时说的深拷贝和浅拷贝的问题,那么这个assign属于深拷贝还是浅拷贝呢?

首先,我先复习一下什么是深拷贝,什么是浅拷贝。

  • 浅拷贝:浅拷贝是对象共用的一个内存地址,对象的变化相互影响。
  • 深拷贝:简单理解深拷贝是将对象放到新的内存中,两个对象的改变不会相互影响。

那么根据上面的结果,assign就应该是深拷贝了。别急,先看一下下面这个例子

let obj2 = {name:'global obj',gender:{past:true,now:true}}
let funcObj2 = Object.assign({}, obj2);
funcObj2.name = 'func obj'
obj2.name='obj';
obj2.gender.now =false;
funcObj2.gender.past =false;
console.log('funcObj2:', funcObj2)
console.log('obj2:', obj2)

输出结果:

可以看到,无论是改变目标源对象还是目标对象,修改对象的对象,结果都会互相影响。也就是说,对于Object.assign()而言,如果对象的属性值为简单类型(string,number),通过Object.assign({},srcobj);得到的新对象为深拷贝;如果属性值为对象或其他引用类型,那对于这个对象而言其实是浅拷贝的,这是Object.assign()特别需要注意的地方。

is

以前判断两个是否相等,我们都是使用==或者===,现在新加了is

  • ==

称为等值符,当等号两边的类型相同时,直接比较值是否相等,若不相同,则先转化为类型相同的值,再进行比较;

类型转换规则:1)如果等号两边是boolean、string、number三者中任意两者进行比较时,优先转换为数字进行比较。

                         2)如果等号两边出现了null或undefined,null和undefined除了和自己相等,就彼此相等

注意:NaN==NaN  //返回false,NaN和所有值包括自己都不相等。

  • ===

称为等同符,严格判断是否相等,不会发生转义的情况。当两边值的类型相同时,直接比较值,若类型不相同,直接返回false

注意:-0 === +0 返回true;NaN===NaN 返回false

  • is

类似 ===,但是有点不太一样,比如:

Object.is(NaN,NaN) 返回true

Object.is(-0,+0) 返回false

不过实际应用开发,还是建议大家使用 ===

console.log(
    0 == false,
    0 === false,
    NaN == NaN,
    NaN === NaN,
    -0 === +0,
    Object.is(0, false),
    Object.is(-0, +0),
    Object.is(NaN, NaN)
)

14.Proxy

监视某个对象中的属性读写,ES5里面有可以使用Object.defineProperty的方法,这方法非常常见,Vue3.0以前就是用的这个办法实现的数据双向绑定。ES2015全新设计了一个叫Proxy的类型,专门为对象设置访问代理器的。

基本使用

//DEMO18
const person = {
    name: 'zce',
    age: 20
}
// Proxy第一个参数是代理的目标对象,第二个参数是代理的处理对象
const personProxy = new Proxy(person, {
    //get方法监视数据的访问,接收2各参数,1.代理的目标对象 2.外部访问的属性属性名 ,返回值是外部访问返回的结果
    get(target, property){
        // console.log(target, property)
        // return 100
        return property in target? target[property] : 'default'
    },
    //set 监视设置属性的过程,传入3个对象,分别是目标对象,代理的属性名称和要写入的属性值
    set(target,property,value){
        if(property === 'age'){
            if(!Number.isInteger(value)){
                throw new TypeError(`${value} is not an int!`)
            }
        }
        target[property] = value
    },
    //在外部对代理对象进行delete操作时执行,两个参数1.代理目标对象2.要删除的属性名称
    deleteProperty(target,property){
        console.log('delete',property)
        delete target[property]
    }
})
//personProxy.age = true
personProxy.age = 10
personProxy.gender = true
console.log(personProxy)
delete personProxy.age
console.log(personProxy)
console.log(personProxy.name)
console.log(personProxy.nothing)

Proxy对比Object.defineProperty()

1.Object.defineProperty()只能监视到对象属性的读写,Proxy能监视到更多对象操作

具体参见下表。

2.Proxy能够更好的支持数组对象的监视

以前我们一般的都是通过重写数组的操作方法来监视数组的操作,就是用自定义的方法来覆盖掉数组原本的push啊shift之类的方法,以此来劫持方法调用的过程。(比如Vue)

下面这个例子展示Proxy如何对数组进行监视

//DEMO19
const list =[]
const listProxy = new Proxy(list, {
    set (target,property,value){
        console.log('set',property,value)
        target[property] = value
        return true//表示设置成功
    }
})
listProxy.push(100)
listProxy.push(200)

可以看到,proxy会自动判断需要push的下标,0就是数组的下标,length是数组的长度 

3.Proxy是以非侵入的方式监管了对象的读写

一个已经定义好的对象,使用Proxy时候,我们不用对对象本身做任何的操作,就可以监视到它内部成员的读写。而Object.defineProperty需要通过特定的方式,单独定义对象中需要被监事的属性。如下面这个例子:

//Object.defineProperty 需要单独设置即使是已经存在的属性
const person = {}
Object.defineProperty(person, 'name', {
  get () {
    console.log('name 被访问')
    return person._name
  },
  set (value) {
    console.log('name 被设置')
    person._name = value
  }
})
Object.defineProperty(person, 'age', {
  get () {
    console.log('age 被访问')
    return person._age
  },
  set (value) {
    console.log('age 被设置')
    person._age = value
  }
})

person.name = 'jack'
console.log(person.name)
const person2 = {
    name: 'zce',
    age: 20
}
const personProxy = new Proxy(person2, {
get (target, property) {
    console.log('get', property)
    return target[property]
},
set (target, property, value) {
    console.log('set', property, value)
    target[property] = value
}
})
personProxy.name = 'jack'
console.log(personProxy.name)

15.Reflect

Reflect是2015中提供的全新内置对象,Reflect是个静态类,不能通过new Reflect()去构建一个新的对象,只能通过Reflect.get()。Reflect内部封装了一系列对对象的底层操作,有13个方法(原本14个,有一个被废弃了)。Reflect成员方法就是Proxy处理对象的默认实现

//DEMO20
const obj = {
    foo:'123',
    bar:'456'
}
const proxy0 = new Proxy(obj, {
})
console.log(proxy0.foo)
const proxy1 = new Proxy(obj, {
    //如果我们在Proxy里面没有定义方法,proxy就是用Reflect来执行的,如:
    get(target, property){
        return Reflect.get(target,property)
    }
})
console.log(proxy1.foo)

输出结果:

Reflect最大的意义是统一提供一套用于操作对象的API

const obj = {
    name:'zce',
    age:'18'
}
//同样是操作对象,一会用到操作符的方式,一会又是用对象中的方法
console.log('name' in obj)
console.log(delete obj['age'])
console.log(Object.keys(obj))
//现在有了Reflect
console.log(Reflect.has(obj,'name'))
console.log(Reflect.deleteProperty(obj,'age'))
console.log(Reflect.ownKeys(obj))

Reflect更多的方法,MDN有详细的介绍,下面附上链接。

Reflect - JavaScript | MDN

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect

16.Promise

提供了一种更优的异步编程方案,解决了传统异步编程中回调函数嵌套过深的问题。因为我之前的文章已经有很详细的Promise介绍了,这边就不再写了,有兴趣的可以看一下下面的文章

进阶学习6:JavaScript异步编程——Promise、链式调用、异常处理、静态方法、并行执行、执行时序、宏任务微任务理解

https://blog.csdn.net/qq_43106115/article/details/117193117?spm=1001.2014.3001.5502

进阶学习8:手写Promise源码

https://blog.csdn.net/qq_43106115/article/details/117238163?spm=1001.2014.3001.5502

17.Class类

以前都是通过定义函数,以及函数的原型对象去实现的类型。

//DEMO21
//class 关键词
//以前定义类型的办法
//先通过定义一个函数,作为类型的构造函数
function Person (name) {
  this.name = name
}
//共享成员要通过prototype来实现
Person.prototype.say = function () {
  console.log(`hi, my name is ${this.name}`)
}

//现在可以通过class来定义
class Person {
    //构造器
    constructor (name) {
        this.name = name
    }
    say () {
        console.log(`hi, my name is ${this.name}`)
    }
}
const p = new Person('tom')
p.say()

18.静态方法Static

类型里面的方法,一般分为实例方法静态方法

  • 实例方法:需要通过类型构造的实例对象去调用
  • 静态方法:直接通过类型本身去调用

现在我们有了Static关键字,可以写静态方法


//DEMO22
// static
class Person {
    constructor (name) {
      this.name = name
    }
    say () {
      console.log(`hi, my name is ${this.name}`)
    }
    //this不会指向某个实例对象,而是类型
    static create (name) {
      return new Person(name)
    }
}
const tom = Person.create('tom')
tom.say()

19.类的继承Extends

class Person {
    constructor (name) {
      this.name = name
    }
    say () {
      console.log(`hi, my name is ${this.name}`)
    }
    //this不会指向某个实例对象,而是类型
    static create (name) {
      return new Person(name)
    }
}
// const tom = Person.create('tom')
// tom.say()
class Student extends Person {
    constructor (name, number) {
        //使用super来调用父类的属性
        super(name)
        this.number = number
    }
    hello () {
        //使用super来调用父类的函数
        super.say()
        console.log(`my school number is ${this.number}`)
    }
}
const s = new Student('jack','1000')
s.hello()
  

20.Set 数据结构

全新数据结构,可以理解为集合,里面的值不能重复。

使用方法和实际应用我都记在下面代码的注释里面了。

//DEMO23
//Set
const s = new Set()
//往set里面添加元素,可以链式调用
s.add(1).add(2).add(3).add(4).add(2)
console.log(s)
s.forEach(i => console.log(i))
//上下这两一样的
for(let i of s) {
    console.log(i)
}

console.log(s.has(100))//查看是否有xxx存在 has()
console.log(s.delete(3))//删除元素 delete()
console.log(s)
s.clear()
console.log(s)//清楚全部内容

//实际应用,给数组去重
const arr = [1,2,1,3,4,1]
const result = new Set(arr)
console.log(result)
const result_arr = Array.from(result)//使用Array.from()再次转换为数组
const result_arr2 = [...result] // 另外一种转换为数组的方式
console.log(result_arr)
console.log(result_arr2)


21.Map

ES2015新增了一个叫做Map的数据结构,这个结构与对象十分类似。

与对象最大的区别是,Map可以用任意数据类型作为键,对象只能用String

//DEMO24
//Map
const obj = {}
//用老办法设置,即时键我们想设置的不是字符串,但是在内部,都会被转化为字符串
obj[true] = 'value'
obj[123] = 'value'
obj[{a:1}] = 'value'
console.log(Object.keys(obj))
console.log(obj['[object Object]'])//这样也能获取到值,因为toString结果就是这个

//所以Map严格来说才是键值对集合,用来映射两个任意类型数据的关系
const m = new Map()
const tom = {name:'tom'}
m.set(tom,90)
m.set(true,'value')
console.log(m)
console.log(m.get(tom))//获取某个键
console.log(m.has(tom))//判断某个键是否存在
//遍历
m.forEach((value,key)=>{
    console.log(value,key)
})
console.log(m.delete(tom))//删除某个键
console.log(m.clear())//清空所有键值

22.Symbol

特性

ES2015之前,对象的属性名都是字符串,而字符串可能会重复,如果重复会冲突。 Symbol,是用来表示一个独一无二的值。

//DEMO25
//Symbol
// 场景1:扩展对象,属性名冲突问题,以前只能通过约定取不同的名字来解决冲突
// shared.js ====================================
const cache = {}
// a.js =========================================
cache['a_foo'] = Math.random()
// b.js =========================================
cache['b_foo'] = '123'
console.log(cache)

//现在:
const s = Symbol()
console.log(s)
console.log(typeof s)
// Symbol最大特点就是独一无二,用Symbol创建的每一个值都是唯一的
console.log(Symbol() === Symbol())
// Symbol函数允许我们传入一个字符串作为描述文本
console.log(Symbol('foo'))
console.log(Symbol('bar'))

//ES2015开始,对象的属性名也可以使用Symbol,所以现在可以是String和Symbol
const obj = {}
obj[Symbol()] = '123'
obj[Symbol()] = '456'
console.log(obj)

const obj2 = {
    [Symbol()]: '123'
}
console.log(obj2)
// Symbol还可以用来创建对象的私有成员
const name = Symbol()
const person = {
  [name]: 'zce',
  say () {
    console.log(this[name])
  }
}
// 只对外暴露 say
person.say()

全局复用同一个Symbol

//DEMO26
//Symbol补充
//全局复用同一个Symbol
// Symbol.for() 相同字符串返回相同的Symbol值,内部维护了一个全局的注册表提供了一一对应的关系,维护的是字符串和Symbol之间对应的关系
const s1 = Symbol.for('foo')
const s2 = Symbol.for('foo')
console.log(s1 === s2)
//也就是说维护的不是字符串也会被转化为字符串
console.log(
    Symbol.for(true) === Symbol.for('true')
)
//Symbol提供了很多内置的Symbol常量,用来作为内部方法的标识。
console.log(Symbol.iterator)
console.log(Symbol.hasInstance)

const obj = {}
console.log(obj.toString())//[object Object] 对象的标签,可以用Symbol自定义
const obj2 = {
    [Symbol.toStringTag]:'XXObject'
}
console.log(obj2.toString())

const obj3 = {
    [Symbol()]: ' symbol value',
    foo: 'normal value'
}
//用Symbol作为属性名,传统的for in循环是拿不到的
for (let key in obj3){
    console.log('key:',key)
}
//Object.keys()方法也是拿不到的
console.log(Object.keys(obj3))
//JSON.stringify()方法也会忽略Symbol属性
console.log(JSON.stringify(obj3))
//获取办法: 获取全是symbol的属性名
console.log(Object.getOwnPropertySymbols(obj3))

直到ES2019,ES一共定义了6种原始数据类型+Object,一共7种。在未来还会新增一种叫BigInt的数据类型,用来存放更长的数字。

补充:我查了一下资料,BigInt这种类型在ES2020的时候已经上,ES2021并没有新增新的数据类型,所以截至ES2021为止,一共有8中数据类型,分别是:

  • Null
  • Undefined
  • Boolean
  • Number
  • String
  • Symbol【ES2015】
  • BigInt【ES2020】
  • Object

看之后有机会再过过基础,专门写一篇数据类型相关的特性解析好了…希望不会咕咕

23.for...of循环

在ES中有几种遍历数据的方法:

  • for:适合用来遍历普通数组
  • for...in:适合用来遍历键值对

其他还有一些函数式的遍历方法,比如forEach(),但是都有一些局限性。所以ES借鉴了其他语言,引入了一种叫做for...of的遍历方式,作为遍历所有数据结构的统一方式。

基本用法

//DEMO27
//for of
const arr = [100, 200, 300, 400]
//for...of拿到的数组的元素而不是下标,可以拿来取代forEach,而且for of可以用break,forEach不能break
for(const item of arr){
    console.log(item)
    if(item > 100){
        break;
    }
}
//遍历新的set数据
const s = new Set(['foo','bar'])
for(const item of s) {
    console.log(item)
}
//遍历新的map数据
const m = new Map()
m.set('foo','123')
m.set('bar','345')
//返回的是一个数组,分别是键和值
for(const item of m){
    console.log(item)
}
//也可以解构写法
for(const [key,value] of m){
    console.log(key,value)
}
//遍历最普通的对象,报错了显示obj is not iterable
const obj = {foo:123,bar:456}
for(const item of obj){
    console.log(item)
}

输出:

可以看到对于最普通的对象,for of好像并不能直接遍历。

24.For...of原理 Iterable 接口

从ES2015开始,ES里面呢能够表示有结构的数据的类型越来越多了,从最早的数组和对象,到现在的set和map,我们还可以去组合使用这些类型。为了给各种各样的数据结构提供统一遍历方式,ES2015提供了一个叫做Iterable的接口,意思是可迭代的。编程语言里面说的实现统一接口,其实也就是实现了统一的规格标准的意思。那么实现Iterable接口就是for...of的前提,前面可以直接使用for...of方法原型里面都有一个Symbol.iterator对象

for...of原理 所以因为普通对象没有Symbol.iterator属性,也就无法直接使用for...of来循环

//DEMO28
//for...of原理
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())

实现可迭代接口

那么原本没有Symbol.iterator属性的对象呢,也可以通过给它实现可迭代接口来使用for...of

//DEMO28
//实现可迭代接口
//最外层这个,实现了iterable,可迭代接口
const obj = {
    //内部有一个返回迭代器的iterator方法
    [Symbol.iterator]:function(){
        //iterator方法返回的是Iterator 迭代器接口,内部必须有一个用于迭代的next()放大
        return {
            next: function() {
                //这里返回的是迭代结果接口 IterationResult,约定的是内部必须有一个value表示当前被迭代到的数据,和必须有一个done的布尔值,代表迭代是否结束
                return {
                    value: 'zce',
                    done: true
                }
            }
        }
    }
} 
//尝试调用for...of返回,不会报错,但是因为我们done设置的是true,说明一调用就结束了,所以循环体不会被执行
for(let item of obj){
    console.log('循环体')
}

const obj2 = {
    //添加一个数组,去存放一些被遍历的数据
    store:['foo','bar','baz'],
    //内部有一个返回迭代器的iterator方法 在这个方法中去迭代数据
    [Symbol.iterator]:function(){
        let index = 0
        const self = this
        //iterator方法返回的是Iterator 迭代器接口,内部必须有一个用于迭代的next()放大
        return {
            next: function() {
                //这里返回的是迭代结果接口 IterationResult,约定的是内部必须有一个value表示当前被迭代到的数据,和必须有一个done的布尔值,代表迭代是否结束
                const result = {
                    value: self.store[index],
                    done: index >= self.store.length
                }
                index++
                return result
            }
        }
    }
} 
//尝试调用for...of返回,不会报错,但是因为我们done设置的是true,说明一调用就结束了,所以循环体不会被执行
for(let item of obj2){
    console.log('循环体2',item)
}

迭代器设计模式

迭代器这个模式,核心就是对外提供统一遍历接口,让外部不用再去关心内的的数据是怎么样的。

// 迭代器设计模式

// 场景:你我协同开发一个任务清单应用

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

const todos = {
  life: ['吃饭', '睡觉', '打豆豆'],
  learn: ['语文', '数学', '外语'],
  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

以上是关于进阶学习9:ECMAScript——概述ES2015 / ES6新特性详解的主要内容,如果未能解决你的问题,请参考以下文章

ECMAScript 2017(ES8)特性概述

JavaScript ES6功能概述(ECMAScript 6和ES2015 +)

Javascript ES6 特性概述(即ECMAScript 6和ES2015+)

我的OpenGL学习进阶之旅OpenGL ES 3.0新功能

我的OpenGL学习进阶之旅OpenGL ES 3.0新功能

React Native之React速学教程(下)