前言:
本篇文章的学习建议是搭配 MDN 文档来学习~
祝您阅读愉快~
通过前面对 ECMASCRIPT 的介绍,我们了解到了JAVASCRIPT 包含了 ES (以下都简写为ES)的。但是以我们对 JAVASCRIPT 的了解我们知道 其还可以 操作 DOM ,BOM, 文件读写 等等 ,上述的这些功能都是 ES 这门语言所不能够提供的,要实现上诉复杂的功能是需要依靠宿主环境(JS 的运行环境)所提供的API。应该是基本上大部分语言都是建立在 API 上的。 哈哈哈意识到自己的渺小,我不是工程师,只是 API 的调用者~
开句玩笑话~
千万不能够妄自菲薄 ~
加油~ 我们一起开启接下来的学习
通过下面的两张图你可以了解到不同环境下的 JS 的组成部分
在说到 JS 与 宿主环境之间的关系时,请大家先思考下为什么不同的 JS API 在不同浏览器的兼容性会有不同?
答: 这是因为浏览器的内核不相同,浏览器的内核有分为渲染引擎与JS引擎。JS引擎负责对JS进行解析,编译,执行。并且还会暴露出一些API,供JavaScript操作DOM、CSSOM,BOM,因此不同的内核的JS引擎不尽相同,所支持的API也不相同。
仅举浏览器为例:
看过某乎举关于浏览器与JS的两个例子:
作者:木瓜 && Curly Cheng
链接:https://www.zhihu.com/question/426297292/answer/1546477210
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
总结:
ECMASCRIPT 2015 概括下来主要以下几方面有改进
ES6 变化如下:
Promise 可以参考笔者的另外一篇文章
在ES6以前,ES仅提供了两种作用域
在ES6中新增了用来定义变量的 let和const 关键字,使用这两个关键字会形成块级作用域
let 特性如下
const {
log } = console
if (true) {
var name = 'reborn'
}
if (true) {
let age = '18'
}
log(name) // reborn
log(age) // age is not defined
let name = '3'
let name = '4' // Identifier 'name' has already been declared
// for循环有两层作用域
let arr = [{
}, {
}, {
}]
for (let i = 0; i < arr.length; i++) {
const i = 'hhh'
console.log(i)
}
// 打印结果如下:
// hhh
// hhh
// hhh
const {
log } = console
log(name1) // undefined
log(name2) // Cannot access 'name2' before initialization
var name1 = 'reborn'
let name2 = 'david'
var tmp = 123;
if (true) {
console.log(tmp) // 报错
let tmp
}
解:
会报错,块级作用域如果存在let关键字的话,会将将let所声名的这个变量绑定在当前块级作用域,不受外界影响。(专有名词叫做 暂时性死区)
var tmp 的时候虽然说tmp是全局变量,但因为在{}中用let 声明了 tmp, tmp就被绑定在当前的{}作用域中,就变成了局部作用域,所以在let tmp 前访问tmp的话就会报错,这是因为let关键字不会提升变量。
const 特性
// 简单数据类型
const number = 1
log(number++) // TypeError: Assignment to constant variable.
// 引用数据类型
// 此时的常量obj存储的是{name: 'reborn'}的引用地址,这个对象的内容是可以被修改的。
// 因为这不会导致引用地址发生变化。
const obj = {
name: 'reborn'}
obj.name = 'david'
log(obj) // { name: 'david' }
// 如果修改引用地址将会报错
obj = {
} // TypeError: Assignment to constant variable.
解构赋值属于新语法,目的是快速从数组中或是对象中获取对应的元素
// 数组的结构
// 数组的结构是根据位置进行提取
let ages = [18, 20, 22]
// 1. 语法如下:
// const [reborn , david, molly] = ages
// log(reborn) // 18
// log(david) // 20
// log(molly) // 22
// 2. 如果只想要获取第3个元素
// const [, , reborn] = ages
// log(reborn) // 22
// 3. 如果想给赋值的变量赋默认值
const [, , , vickey = 30] = ages
// 将在没有提取到数组的值的时候,会将默认值给变量进行赋值
log(vickey) // 30
// 对象的结构是根据属性名进行提取既结构出来的变量名要是对象中的属性
const obj = {
name: 'reborn', age: 18, job: 'FE' }
// 1. 基本语法
// const {name, age, job} = obj
// log(name) // reborn
// log(age) // 18
// log(job) // FE
// 2. 起别名
// const {name:name1, age, job} = obj
// log(name1) // reborn
// log(age) // 18
// log(job) // FE
// 3. 默认值
const {
name:name1, age=20, job, tall = 180} = obj
log(name1) // reborn
log(age) // 18
log(job) // FE
log(tall) // 180
增强了字符串的功能
// 1 2 小点一起演示了
// 1. 可以直接换行
const str = `今天天气真的好,
倾盆大雨那`
log(str)
// 打印结果:
// 今天天气真的好,
// 倾盆大雨那
// 2. 可以使用插值表达式
const obj = {
name: 'reborn' ,sex : 1}
const str1 = `这小子名叫${
obj.name},是名${
obj.sex === 1 ? '男' : '女'}生`
log(str1)
// 打印:这小子名叫reborn,是名男生
// 标签函数
// 1. 要使用标签函数首先需要定义一个函数
// 2. 定义的该函数的参数第一个为 通过插值表达式分割之后的数组
// 3. 后面的参数是插值表达式的值
// 4. 注意的点: 如果使用了标签函数,就必须要用返回值,否则就会像log(str1) 得到 undefined
// 5. 在vue的中的插值表达式页用不了,会有编译错误的问题出现。
const obj = {
name: 'reborn' ,sex : 1}
function myTagFn(strArr, name, sex) {
log(strArr) // [ '这小子名叫', ',是名', '生' ]
log(name) // reborn
log(sex) // 男
return strArr[0] + name + strArr[1] + sex + strArr[2]
}
const str1 = myTagFn`这小子名叫${
obj.name},是名${
obj.sex === 1 ? '男' : '女'}生`
// 标签函数无返回值的情况
// log(str1) // undefined
// 有返回值的情况
log(str1) // 这小子名叫reborn,是名男生
总结:
个人认为使用标签函数的意义在于避免插值表达式有很多复杂的表达式不易于阅读
更改上面的代码
const obj = {
name: 'reborn' ,sex : 1}
function myTagFn(strArr, name, sex) {
sex = sex === 1? '男' : '女'
return strArr[0] + name + strArr[1] + sex + strArr[2]
}
const str1 = myTagFn`这小子名叫${
obj.name},是名${
obj.sex}生`
log(str1) // 这小子名叫reborn,是名男生
includes() 方法用于判断一个字符串是否包含在另一个字符串中,根据情况返回 true 或 false。
// includes方法
let str = "reborn's age is 18"
log(str.includes('reborn')) // true
log(str.includes('reborn1')) // false
startsWith() 方法用来判断当前字符串是否以另外一个给定的子字符串开头,并根据判断结果返回 true 或 false。
// startWith
const str = '今天天气真的好~'
log(str.startsWith('今天')) // true
log(str.startsWith('昨天')) // false
endsWith()方法用来判断当前字符串是否是以另外一个给定的子字符串“结尾”的,根据判断结果返回 true 或 false。
// endsWith
const str = '今天天气真的好~'
log(str.endsWith('今天')) // false
log(str.endsWith('好~')) // true
函数的默认值
// 函数参数的默认值
function fn(name = 'reobrn', age = 18) {
log(name, age) // reborn 18
}
fn()
// ... 语法的应用
// 1. 可以接受剩余参数
// 1) 在函数的形参中使用,可以接受任意传递过来的实参
function fn(...args) {
log(args) // [ 'reborn', 18, 180 ]
log(args instanceof Array) // true
}
fn('reborn', 18 ,180)
const [reobrn, ...args] = [18, 19,29]
log(reobrn) // 18
log(args) // [19, 29]
// 2. 可展开数组
const arr1 = [1,2,3,4]
const arr2 = [5,6,7,8]
log([...arr1, ...arr2]) // [1,2,3,4,5,6,7,8]
简化了函数的表达式,函数的定义。
// 箭头函数的应用
// 1. 语法
// 传统定义函数
function fn1() {
log('hi')
}
// 箭头函数
fn2 = () => {
log('reborn')
}
fn1() // hi
fn2() // reborn
// 2. 箭头函数简化规则
// 1) 一个参数的情况下可以省略括号,没有参数跟多个参数不能够省略括号
fn3 = name => {
log(name)
}
fn3('hhhh') // hhhh
// 2) 函数体中仅有一行代码的情况下可以省略{}, 并且默认添加return关键字在表达式前面
fn4 = name => name
log(fn4('wo~')) // wo~
// 3. 箭头函数this指向
// 箭头函数不会改变this指向,也就是说箭头函数外层执行上下文的this指向是什么,this的指向就是什么
// 箭头函数没有this指向,所以也不能够通过函数bind call apply 方法区更改this指向, 换句话说箭头函数内的this绑定了外层的执行上下文的this指向
obj1 = {
name: 'molly'}
let obj = {
name: 'reborn',
fn1: () => {
log(`hello ${
this.name}`)
}
}
obj.fn1() // hello undefined
obj.fn1.call(obj1) // hello undefined
let name = "reborn"
const obj = {
// 普通值的简写
// name: name,
name,
// 函数的简写
// fn1: function() {}
fn1() {
log('hi', this.name)
},
// 计算属性 可以直接用中括号接表达式
[Math.random()]: 'hhhh'
}
obj.fn1() // hi reborn
log(obj) // { name: 'reborn', fn1: [Function: fn1], '0.18865525764091018': 'hhhh' }
Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
// Object.assign
// 1. Object.assign 将源对象中的数据复制到目标对象中,返回值就是目标对象。
// 2. 如果遇见源对象属性与 目标对象的属性相冲突,会将源对象的属性覆盖掉目标对象
// 3. 如果多个源对象的属性相冲突的话,会将形参中最后一个源对象的属性覆盖到前面源对象的属性既 出于后置位置的源对象会覆盖前置位置的源对象
const target = {
name: 'reborn', age: 18 }
const source = {
tall: 180, name: 'molly' }
const result = Object.assign(target, source)
log(result) // { name: 'molly', age: 18, tall: 180 }
log(result === target) // true
这个方法基本上不会用到,了解即可
Object.is() 方法判断两个值是否为同一个值。
Object.is与 == 和 === 之间的区别如下:
与== 运算不同。 == 运算符在判断相等前对两边的变量(如果它们不是同一类型) 进行强制转换 , 而 Object.is不会强制转换两边的值。
与=== 运算也不相同。 === 运算符 (也包括 == 运算符) 将数字 -0 和 +0 视为相等 ,而将Number.NaN 与NaN视为不相等.
// 了解即可,开发中使用 === 运算符
log( +0 === -0) // true
log(Object.is(+0, -0)) // false
log(NaN === NaN) // false
log(Object.is(NaN , NaN)) // true
如果你需要监视一个对象的读写你会怎么做?
通过 Object.definedProperty()可以实现,Vue3.0以前就是通过这个API实现双向数据绑定。在ES6中通过Proxy对象可以创建一个代理对象也可以实现监视对象的读写。(Vue3.0有部分双向数据绑定也是依赖Proxy实现的)
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
参数介绍
target
要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler (这个)
一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
对对象的代理
// 对象的监听
const obj = {
name: 'reborn', age: 18, tall: 180 }
// Proxy对象 ps 建议去看看MDN讲的真的很详细,我也只是简单记录下~
// 1. 通过new Proxy创建一个代理对象,个人理解返回一个被代理对象的复制引用(在源对象更改之后,代理对象也相应的更改,应该时同一个对象的引用。)
// 2. 第一个参数时目标对象target。(对象,数组,函数,另外一个proxy代理对象)
// 3. 第二个为一个handle对象,里面是对代理对象进行各种操作的各种监听的回调函数。
const objProxy = new Proxy(obj, {
// 监听对象属性的读取操作,如果没有返回值则为undefined
get(target, property) {
if (target[property]) {
return target[property]
} else {
return 'default' }
},
set(target, prop, val) {
target[prop] = val
log(`设置的属性为${
prop}: --- ${
val}`)
return true
}
})
// practice:
// 1)个人理解返回一个被代理对象的复制引用(在源对象更改之后,代理对象也相应的更改,应该时同一个对象的引用。)
log(objProxy) // { name: 'reborn', age: 18, tall: 180 }
obj.hairColor = 'lime' // 给源数据添加hairColor的属性
log(objProxy) // { name: 'reborn', age: 18, tall: 180, hairColor: 'lime' }
// 2) handle.get() 监听对象属性的读取操作
// handle中的get回调监听对象属性的读取操作,如果没有返回值则为undefined
// 实现一个功能,访问对象属性的时候如果该对象没有这个属性返回 ‘default’ 如果有就返回 该对象
log(objProxy.name) // reborn
log(objProxy.name11) // default
// 3) handler.set() 方法是设置属性值操作的捕获器。
// target 目标对象。property 将被设置的属性名或 Symbol。value 新属性值。
// 官方文档说 set 回调中设置属性之后 需要return true 设置成功,return false在非严格模式下会抛出类型错误,不过实测不需要进行返回true也能够设置属性,只要赋值了即可。
// 建议set方法务必返回true ,在数组代理对象中不返回true会出现错误
log('shoesColor' in objProxy) //false
objProxy['shoesColor'] = 'blue' // 设置的属性为shoesColor: --- blue
log('shoesColor' in objProxy) // true
log(objProxy.shoesColor) // blue
log(obj) // { name: 'reborn', age: 18, tall: 180, shoesColor: 'blue' }
// 还有很多handle对象中对代理对象操作的各种处理回调
// 4)
// handler.has() 方法是针对 in 操作符的代理方法, 某个属性 in 代理对象,会触发has回调。
// handler.deleteProperty() 方法用于拦截对对象属性的 delete 操作。
// handler.defineProperty() 用于拦截对代理对象调用 Object.defineProperty() 操作。
对数组的代理
// 创建数组的代理对象
const arr = [1, 2,3]
// 1. 创建数组的代理对象与创建对象代理对象都一样,将原数组的引用赋值给到代理数组,当原数组发生变化,代理数组也会发生变化,反之代理数组也可以影响到源数组。
const arrProxy = new Proxy(arr, {
get(target, index) {
log(`called: target数组中index为 ${
index} 元素被查看`)
// console.log(index, target[index], '看看get访问代理属性的值是啥')
return target[index]
},
set(target, index, value) {
log(target, '目标值')
log(index, '索引')
log(value, 'value值')
log(target[index], '组合target[index]')
target[index] = value
return true
}
})
// 一、 get方法
// 1. get方法的第二个参数 index 为数组的索引。 不过需要注意的是,在访问数组的属性如length 或是方法的时候push,pop,shift等等,index参数会变成property。
// 既然有可能index为property,get返回的时候是garget[property] 不就有可能会报错吗?
// 是的,我一开始也是这样想的,但是测试发现 如果单独打印 index 是 property, 但是跟目标数组组合target[index] 就变成了对应的值。
// 不知道这个底层做了啥处理。
log(arrProxy[1]) // called: target数组中index为 1 元素被查看
log(arrProxy[arr.length - 1]) // called: target数组中index为 2 元素被查看
// 二、 set回调
// 1. set回调的三个参数分别 目标数组,index, value
// 2. 如果在给 arrProxy 更新或是 赋值的时候访问了 代理对象的属性会触发get回调。
arrProxy[arrProxy.length - 1] = 5 // 会触发get回调
log(arrProxy) // [ 1, 2, 5 ]
//
// 3. 通过push给数组添加元素
// 以下是个人理解为什么会触发两次set回调与get回调
// 会触发两次get方法回调,第一次index为push ,第二次为length,这可能因为push方法底层是通过length添加的。
// set方法也会触发两次,第一次找到数组中要添加元素的位置,第二次将要添加的值插入。。。实在是想不明白,不编了。
// 我个人感觉这里应该就涉及了数组push,pop等等这些方法是怎么实现的知识了。
// arrProxy.push(4)
arrProxy.unshift('hh')
// arrProxy.pop()
log(arrProxy)
target传函数与proxy代理对象你们有兴趣可以自己试试
应用
在项目中可以多思考看看能不能够用得上该API。
Reflect 是一个内置的对象,但不是一个函数对象,因此它是不可构造的,
与Math对象是一样的,里面的属性和方法同属于静态的。
Reflect 内部封装了一系列对对象的底层操作,有13个方法。
以下的13个API其实就是Proxy对象中的handle对象里面的13个回调。
对对象的进行下面的操作都可以被监听到。
// Reflect 是一个内置对象,提供13钟操作对象的API
// 1. Reflect.apply(target, thisArgument, argumentsList)
// Function.prototype.apply() 功能类似。
const obj = {
name: 'reborn'
}
function fn1(...greetings) {
log(`${
this.name} 早啊~ 今天又是元气满满的一天鸭~`)
log(...greetings)
}
Reflect.apply(fn1, obj, ['是的拿~', '加油加油', '干巴爹'])
// print 如下:
// reborn 早啊~ 今天又是元气满满的一天鸭~
// 是的拿~ 加油加油 干巴爹
// 2. Reflect.construct(target, argumentsList[, newTarget])
// 对构造函数进行 new 操作,相当于执行 new target(...args)。
class Person {
constructor(name) {
this.name = name
}
}
const person = Reflect.construct(Person, ['RebornJiang'])
log(person.name) // RebornJiang
// 3. Reflect.defineProperty(target, propertyKey, attributes)
// 和 Object.defineProperty() 类似。如果设置成功就会返回 true
// attributes 还有很多配置项,可取MDN查看
// 需求:给对象设置一个不可枚举的属性
const obj = {
name: 'reborn'
}
const isSetOk = Reflect.defineProperty(obj, 'age', {
enumerable: false,
value: 18
})
log(isSetOk) // true
log(obj) // {name: 'reborn}
// 获取了不可枚举属性
log(Object.getOwnPropertyNames(obj)) // [ 'name', 'age' ]
// 4. Reflect.deleteProperty(target, propertyKey)
// 作为函数的delete操作符,相当于执行 delete target[name]。
const obj = {
name: 'reborn'
}
Reflect.deleteProperty(obj, 'name')
log(obj) // {}
// 5. Reflect.get(target, propertyKey[, receiver])
// 获取对象身上某个属性的值,类似于 target[name]
const obj = {
name: 'reborn'
}
log(Reflect.get(obj, 'name')) // 'reborn'
// 6. Reflect.getOwnPropertyDescriptor(target, propertyKey)
// 获取对象中某个属性的描述信息,如是否可枚举,可读写等等。
// 如果该属性在不在对象中返回undefined。
const obj = {
name: 'reborn',
age: 18,
bi: true
}
// 1)
log(Reflect.getOwnPropertyDescriptor(obj, 'name'))
// {
// value: 'reborn',
// writable: true,
// enumerable: true,
// configurable: true
// }
// 2)给obj添加一个枚举属性在打印
Reflect.defineProperty(obj, 'test', {
enumerable: true, value: 111 })
log(Reflect.getOwnPropertyDescriptor(obj, 'test')) {
value: 111, writable: false, enumerable: true, configurable: false }
// 7. Reflect.getPrototypeOf(target)
// 获取target对象的原型对象
function Test() {
}
Test.prototype.sayHi = () => {
log('hiihi')
}
const test = new Test()
log(Reflect.getPrototypeOf(test)) // { sayHi: [Function (anonymous)] }
// 8. Reflect.has(target, propertyKey)
// 判断某个属性在不在对象上, 和 in 运算符 的功能完全相同。
const obj = {
name: 'reborn'}
log(Reflect.has(obj,'name')) // true
// 9.Reflect.preventExtensions(target)
// 禁止给目标对象扩展属性
const obj = {
}
log(Reflect.preventExtensions(obj))
obj.name= 'reborn'
log(obj) // {}
// 10.Reflect.isExtensible(target)
// 判断目标对象是否可以扩展
const obj = {
}
obj.name= 'reborn'
log(obj) // {name: 'reborn'}
log(Reflect.isExtensible(obj)) // true
log(Reflect.preventExtensions(obj)) // true
log(Reflect.isExtensible(obj)) // false
// 11. Reflect.ownKeys(target)
// 返回一个包含所有自身属性(不包含继承属性)的数组。(类似于 Object.keys(), 但不会受enumerable影响).
const obj = {
name: 'reborn'}
Reflect.defineProperty(obj, 'age', {
enumerable: false, value: 18})
log(obj) // { name: 'reborn' }
log(Reflect.ownKeys(obj)) // [ 'name', 'age' ]
// 12. Reflect.set(target, propertyKey, value[, receiver])
// 给对象添加属性和值。返回一个Boolean,如果更新成功,则返回true。
const obj = {
}
log(Reflect.set(obj, 'age', 18))
log(obj) // { age: 18 }
// 13. Reflect.setPrototypeOf(target, prototype)
// 给一个对象设置原型,返回一个 Boolean, 如果更新成功,则返回true。
// 更改Test的原型
function Test() {
}
Test.prototype.saiHi = () => {
log('hihi')}
const otherPrototype = {
name: 'reborn'}
log(Reflect.setPrototypeOf(Test, otherPrototype))
log(Reflect.getPrototypeOf(Test)) // {name: 'reborn'}
总结:
Reflect对象提供的对对象操作的静态方法我们通过其他的API也同样可以实现,为什么还需要用到Reflect提供的方法。这是因为ECMASCRIPT 官方希望提供一个统一对对象操作的API集合(对象)。
我们之前操作对象的api 各种各样 的都有,比如说: 要删除对象的属性 用 关键字 delete,判断某个属性在不在对象上要用 in关键字,获取对象的所有属性调用Object.keys(target),调用函数要调用函数对象原型的方法等等。
现在官方希望有Reflect这样的对象集成对对象所有操作,官方也希望后续的版本操作对象的其他方法都能够被废弃掉,被Reflect所取代。
从现在开始,应该要习惯用这个方法了~
class出现是为了取代构造函数定义类。完全可以看作构造函数的另一种写法。
// class
// 1. 类的基本语法
class Person {
// constructor通过new命令生成对象实例时,自动调用该方法。
constructor(name) {
// 定义实例属性
this.name = name
}
// 原型方法 (不可枚举属性)
sayHi() {
log(`hi~${
this.name}`)
}
// 通过static 定义静态方法
// 静态方法只能够通过 类对象取到,不能够通过实例取到。
static createPerson(name) {
return new Person(name)
}
}
const reborn = new Person('reborn')
log(reborn.name) // reborn
reborn.sayHi() // hi~reborn
// reborn.createPerson('david') // TypeError: reborn.createPerson is not a function
const david = Person.createPerson('david')
david.sayHi() // hi~david
// 2. 类的继承
// class 当前类 extends 需要继承的类 {}
class OtherPerson extends Person {
// otherPerson构造函数
constructor(weight, tall,color) {
// 子类在继承父类的时候,其构造函数内必须先调用super()方法,这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。
// this.hairColor = color // 会报引用错误 this拿不到
super('reborn') // 调用父类Person的constructor()
this.weight = weight
this.tall = tall
}
otherFn() {
// super这个关键字,既可以当作函数使用,也可以当作对象使用。
super.sayHi()
}
}
const other = new OtherPerson(170, 180, 'black')
log(other.name) // reborn
log(other.weight) // 170
other.otherFn() // hi~reborn
Set是新的数据结构,里面存储的值是唯一的。
// Set
// 1. 接受一个数组
const set = new Set([1,2,3,3,3,2])
log(set) // Set(3) { 1, 2, 3 }
// 2. set实例的原型方法 常用
// add 添加元素
set.add(4) // Set(4) { 1, 2, 3, 4 }
// delete 删除元素
set.delete(4) // Set(3) { 1, 2, 3 }
// has set中是否存在该元素
log(set.has(3)) // true
// forEach, 因为set原型上有提供Symbol(Symbol.iterator) 可迭代的接口,可以使用 for of 循环遍历
// 遍历集合中的数据
set.forEach((item) => {
console.log(item)
})
// clear()
// 清空集合中的数据
set.clear() // Set(0) {}
// size()
// 获取set的长度
log(set)
// 应用: 数组的去重
const set = new Set([1,2,3,3,3,2])
log(Array.from(set))
Map数据结构与Object数据结构很相似,都是存储键值对。
但又有很大差别,下面贴一张MDN的比较图。
API就不过多介绍了,基本上跟Set数据结构的API一样的
// 1. Map 数据的添加
// 1) 通过map.set() 添加
const map = new Map()
map.set('name', 'reborn') // Map { 'name' => 'reborn' }
// 2) 在new实例的时候传入一个 二位数组创建map数据结构
const map = new Map([['name', 'reborn'], ['age', 18]])
log(map) // Map { 'name' => 'reborn', 'age' => 18 }
// 3) 直接为map对象设置对象属性, 此方法不推荐一写map方法的结果会不符合预期
const map = new Map()
map['name'] = 'reborn'
map['age'] = 18
log(map) // Map { name: 'reborn', age: 18 }
log(map.get('name')) // undefined
log(map.has('name')) // false
log(map.delete('name')) // false
log(map) // Map { name: 'reborn', age: 18 }
// 2. map 对象的复制
// 直接将 map对象 在 new Map() 的时候当作参数就可以复制一个map对象
const map1 = new Map([[undefined, 'hhhh'], [NaN, 'hahhaha']])
log(map1.get(undefined)) // hhhh
const map2 = new Map(map1)
log(map2.get(undefined)) // hhhh
// 3. map 对象可以向数组一样合并
const map1 = new Map([['name', 'reborn'], ['age', 18], ['weight', 150]])
const map2 = new Map([['fvrtColor', 'lime'], ['tall', 180], ['weight', 150]])
const map3 = new Map([...map1, ...map2])
// 合并两个Map对象时,如果有重复的键值,则后面的会覆盖前面的。
log(map3) //
// Map {
// 'name' => 'reborn',
// 'age' => 18,
// 'weight' => 150,
// 'fvrtColor' => 'lime',
// 'tall' => 180
// }
// 展开运算符本质上是将Map对象转换成数组。
log(...map3) // [ 'name', 'reborn' ] [ 'age', 18 ] [ 'weight', 150 ] [ 'fvrtColor', 'lime' ] [ 'tall', 180 ]
Symbol是一种全新的数据类型
// Symbol
// 1. 是一种全新的数据类型
const symbolKey = Symbol('name')
log(typeof symbolKey) // symbol
// 2. 通过symbol() 可以创建独一无二的值
log(Symbol() === Symbol()) // false
// 接受一个str作为该值的标识,但是同标识的值是不想等的。
log(Symbol('22')) // Symbol(22)
log(Symbol('name') === Symbol('name')) // false
// 3. 该数据类型的应用主要是给对象创建独一无二的Key,进行第三方模块的扩展时就不用担心key相互冲突的问题。
// 目前JS Object仅支持用字符串和Symbol类型的数据进行定义对象的KEY
// 除此之外symbol还可以创建对象的私有成员。
const greeting = Symbol()
const obj = {
[greeting]: 'hello',
sayHi() {
log(this[greeting])
}
}
obj.sayHi() // hello
// // symbol作为key值,Object.keys()是拿不到的。
log(Object.keys(obj)) // [ 'sayHi' ]
log(Reflect.ownKeys(obj)) // [ 'sayHi', Symbol() ]
// 4. 全局如何使用Symbol类型的数据;
// 1) 可以定义全局变量来接受symbol值
// 2) 可以通过Symbol.for()来创建相同的symbol值。Symbol.for(params) 所接受的任何参数都会被转换为字符串。
// 之所以能够创建相同值,这是因为会在全局维护一个注册表,当所传的字符串相同时会返回对应的symbol值。
log(Symbol.for('name') === Symbol.for('name')) // true
log(Symbol.for('true') === Symbol.for(true)) // true
// 5. ECMASCRIPT 还内置很多Symbol类型的常量
// 1). 迭代器接口, 通过迭代器接口可以实现for of 遍历
log(Symbol.iterator) // Symbol(Symbol.iterator)
// 2). 字符串标签
log(Symbol.toStringTag) // Symbol(Symbol.toStringTag)
const obj = {
[Symbol.toStringTag]: '更改对象toString的结果'
}
log(obj.toString()) // [object 更改对象toString的结果]
ECMASCRIPT 已经有for循环,for…in 循环了,为什么还需要for…of?
这与Reflect对象的出现时一样的,ECMASCRIPT 官方希望推出一种统一遍历所有数据结构的语法。
// for of
// 1. 遍历数组
// item 为数组中的元素
// 可以使用break 关键字
// const arr = [1,2,3,4,5]
// for (let item of arr) {
// log(item)
// if (item > 3) break
// }
// 2. 遍历set
// const sets = new Set([1,2,3,4,4,4,4])
// for(let item of sets) {
// log(item)
// }
// 3. 遍历Map数据结构
// map数据结构遍历的时候 item 拿到的时 键值 数组,我们可以配合数组的结构取到key value
// const maps = new Map([['name', 'reborn'], ['age', '18']])
// for (let [key, value] of maps) {
// log(key, value)
// }
// 4. 遍历对象
const obj = {
name: 'reborn',
age: 18
}
// TypeError: obj is not iterable
for (let key of obj) {
log(key)
}
既然ECMASCRIPT官方希望 for of能够统一遍历数据结构,但是Object数组结构类型却是遍历不了,报了类型错误。这是为什么?
先给出结论,这是因为对象没有实现可迭代接口iterator。接口是啥?就好像任何数据类型都能够调用toString方法,这是因为任何数据类型都有提供toString接口,所谓的接口可以理解为功能。所以 能够被for of循环遍历的前提是当前数据结构要能够提供 可迭代接口。
通过刚刚上面的截图我们知道Symbol.iterator是一个函数,调用试试看。
根据上面分析我们可以得到如下代码
const obj = {
[Symbol.iterator]: function() {
// iterator
return {
next: function() {
// iterationResult
return {
value: 'reborn',
// 迭代是否结束
done: false
}
}
}
}
}
for (let item of obj) {
log('循环开始', item)
break
}
// 打印结果如下:
// 循环开始 reborn
只要实现了可迭代接口,在使用for of 遍历对象就不会报错了。
给自定义对象实现可迭代接口的这种设计成为迭代器模式。
可迭代接口这有什么用处?课程中老师讲到一个场景,向外提供一个统一遍历的接口。
目前的我还是不能够体会/(ㄒoㄒ)/~~
// 两个人协同开发一个任务清单
// A负责定义数据结构
// B负责拿到所有的任务
// 如果没有向外提供一个统一遍历obj 中所有任务清单的接口,B需要知道A同学定义的数据结构是怎么样的,如果A同学的数据结构发生变化,B同学的代码也要发生更改。
// 如果提供了统一遍历的接口
const obj = {
life: ['吃饭', '睡觉', '打豆豆'],
learn: ['语文', '数学', '英语'],
// 提供可迭代接口
[Symbol.iterator]: function() {
// 当我们调用for of 时候遍历对象,该对象会调用他的iterator方法
const taskList = [...this.life, ...this.learn]
log(taskList)
// 定义index, 当数组都遍历结束之后,将iterationResult 对象的 done 设置为true
let index = 0
return {
next: function() {
const result = {
value: taskList[index],
done: index >= taskList.length
}
index++
return result
}
}
}
}
// 这里就不需要关注 obj的数据结构式怎么样了
for(const item of obj) {
log(item)
}
Generator是为了解决异步代码产生的“回调地狱”问题,ES2017 的async await 语法糖底层实现就是依赖Generator生成器函数的。
// Generator 生成器函数
// 1. 生成器函数的定义
// 1)生成器函数会返回一个生成器对象
// 2)生成器对象中有一个next方法,当调用next方法的时候函数体才会开始执行
// 3)当碰见函数体代码中的yield关键字的时候会暂停执行,并将yield关键字后面的值作为next方法的返回值{value: '', done: true || false}
// 4)再次调用next方法会接着上次yield关键字的后面的代码开始执行,循环执行 3 4 步骤,直至代码执行结束
function * fn() {
log('第一次执行')
yield 100
log('第二次执行')
yield 200
}
const generator = fn() // Object [Generator] {}
log(generator.next()) // 第一次执行 { value: 100, done: false }
log(generator.next()) // 第二次执行 { value: 200, done: false }
log(generator.next()) // { value: undefined, done: true }
生成器函数的应用
// 生成器函数的应用
// 1. 发号器的应用
function* createId() {
let id = 0
while (true) {
yield id++
}
}
const generator = createId()
log(generator.next())
log(generator.next())
log(generator.next())
log(generator.next())
// { value: 0, done: false }
// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
// 2. 因为生成器函数返回的结构与迭代器返回的结构都是一样的,也可以使用生成器函数实现迭代器模式
const obj = {
life: [
'吃饭啊',
'睡觉啊',
]
,
[Symbol.iterator]: function* () {
const taskList = this.life
for (const item of taskList) {
yield item
}
}
}
for (let item of obj) {
console.log(item)
}
Promise 可以参考笔者的另外一篇文章
可选链式操作符作用:
// 可选链式操作符
// 1. 在对象上的应用 obj?.[expr] obj?.prop
const userInfo = [{
roleInfo: {
roleId: 1
}
}, {
}]
for (let len = userInfo.length; len; len--) {
console.log(userInfo[len - 1].roleInfo?.roleId)
}
// 输出结果
// undefined
// 1
// 2. 在数组上的应用 可选链访问数组元素 arr?.[index]
const arr = null
console.log(arr?.[1])
// 3. 在函数调用上的应用 func?.(args)
// 处理可选的回调函数或者事件处理器
// 函数作为参数的时候,你不确定所传如的参数是否一个函数,也可以使用链式操作符
function doSothings(fn) {
fn?.()
}
doSothings() // 没有输出