进阶学习9:ECMAScript——概述ES2015 / ES6新特性详解
Posted JIZQAQ
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进阶学习9:ECMAScript——概述ES2015 / ES6新特性详解相关的知识,希望对你有一定的参考价值。
目录
Proxy对比Object.defineProperty()
一、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
三、学习前的准备工作
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.对象的解构
用法和特点
对象的解构很多特点和数组的解构也是一致的,如:
- 没有匹配到的成员返回undefined
- 可以设置默认值
重复的这边就不做演示了
//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新特性详解的主要内容,如果未能解决你的问题,请参考以下文章
JavaScript ES6功能概述(ECMAScript 6和ES2015 +)
Javascript ES6 特性概述(即ECMAScript 6和ES2015+)
我的OpenGL学习进阶之旅OpenGL ES 3.0新功能