Es5 的变量声明问题
-
允许重复的变量声明: 导致数据被覆盖
-
变量提升: 怪异的数据访问,闭包问题
-
for (var i = 0;i < 10; i++ ) { setTimeout(() => { console.log(i) // 输出 10 },1000) } - 由于var会进行变量提升,导致i放入了全局环境,由于settimeout是异步执行,在触发执行的时候,i 都是使用的全局环境的这个i,此时i 已经变为10了
-
-
全局变量挂载到全局对象造成全局对象成员污染
- var 创建的变量或者函数 会将其放入全局作用域,
- var 只有 函数作用域 和 全局作用域
es6的解决方案
-
let 声明的变量不会挂载到全局对象
-
let 声明的变量不允许在当前作用域重复声明
- Let 在声明的时候会检查当前作用域下该变量名是否已经存在
-
块级作用域概念
- 块级作用域只有let 和 const有
- 代码执行时遇到花括号,会创建一个块级作用域,花括号结束,销毁块级作用域
- 在块级作用域内用let 定义的变量,在作用域外不能访问
-
let a = 123; { let a = 456 console.log(a) // 456 } console.log(a) // 123
-
使用let 不会有变量提升,因此不能再定义let 之前使用
- 在底层实现上,let也会有变量提升,但是在提升之后,会将其放入一块特殊的内存空间(暂时性死区),如果读取在该内存空间的变量,就会提示 Identifier 'xx' has already been declared
- 当代码运行到该变量的声明语句时,会将其从暂时性死区移除
-
在循环中声明的let会特殊处理
- 在循环中,每次进入循环体,都会开启一个新的作用域,并且将循环变量绑定到该作用域
- 相当于每次循环,使用的都是一个全新的变量
- 相当于每一次都是用的形参, 在块级作用域运行结束后销毁形参
const 和 let
-
const 和 let 完全相同,区别仅在于用const声明的变量必须在声明时赋值,并且后续不能重新赋值
-
常量不可变
- 是指声明的常量的内存空间不可变,并不保证内存空间中的地址指向的其他空间不可变
-
const a = { name :"111" } a.name = '222' //const只能保证a 这个变量指向的地址不能被改变,并不能保证a指向的地址内容被改变
-
变量的命名
-
特殊的常量
- 该常量从字面意义上,一定是不可变的,比如圆周率,月底距离或其他一些绝对不可能变化的配置
- 该常量的名称全部使用大写,多个单词直接使用下划线拼接
-
const PI = 3.141 const MOON_EARTH = 384403.9
-
不能把握的常量就使用普通的命名就行
-
-
在for 循环中,循环变量不可以使用常量
- for in 里面是因为每次都是全新的常量
unicode支持
-
在js中 获取字符串长度目前还是一个码元的长度,如果是出现出汉字(一些古汉字由两个码元组成)比较特殊,就会获取到的length就是2
- 一个码元代表 16位2进制
-
新增了对 码点 查询的api(一个码点等于两个码元)
- codePointAt
-
同时 es6 为正则新增了一个flag : u,如果添加了该配置则使用码点匹配
-
/^*$/u
-
字符串新增的api
在字符串原型属性上新增的api
-
includes
- 判断字符串中是否包含指定的子字符串
-
'我是谁'.includes('我',1) // 第二个参数指定开始查找的位置
-
startswith
- 是否以指定字符串开始
- 也可以指定从哪里开始查找
-
endswith
- 是否以指定字符串结尾
- 也可以指定从哪里开始查找
-
repeat
- 将字符串重复指定的次数后返回操作后的字符串
模版字符串
es6之前处理字符串繁琐的处理方式
-
多行字符串
-
const str = '我\n是\n谁'
-
-
字符串拼接
-
const str = '我' + '1' + '2'
-
es6模版字符串使用 ` 替代之前的引号,会保留字符串直接的空格,tab,换行
const str = `sss
ccc
`
如果需要再字符串中拼接js 表达, 只需要在模版字符串中使用 ${}
-
他会把表达式的结果直接放在模版字符串中
-
表达式可以是任意有意义的数据
- 可以是计算结果
- 表达式是可以嵌套的
- 也是可以 \n 这种转义符
- 可以使用 \ $ 转义,使表达式成为字符串,不再能够进行
const test = 'cs'
// 换行处理
const str = `sss
ccc${test}
`
const str1 = ` computed${1 + 2 * 3}` //使用表达式
const str2 = `moubam${`${1 + 2}`}` // 嵌套
const str2 = `${wenzi}` //使用转义
模版字符串拓展用法
控制模版字符串结果,使用标记
-
标记是一个函数
- 参数1: 被插值分割的字符串数组,末尾有一个空字符串
- 后续参数(数组: 所有的插值
// 这个时候相当于使用一个方法解析模版字符串
const text = myTest`ceshi${1},还不错${2}`
// 会被转换成类似以下代码
// text = myTest(['cheshi',',还不错'],1,2)
//定义解析字符串的方法
function myTest(parts) {
let str = ''
// 获取模版字符串中表达式结果
const values = Array.prototype.slice.apply(arguments).slice(1)
for(let i = 0;i < values.length;i++) {
// 可以在这里修改拼接结果
str += parts[i] + values[i]
if(i === values.length - 1) {
str += parts[i + 1]
}
}
return str
}
//如果方法没有写返回值 得到就是undefind
console.log(text) //undefind
es6提供了一些内置的标记
let text = String.raw`abc\t\nbdc`
// 这个标记会让字符串模版不再有转义的能力
通过自己实现一个标记方法,删除用户输入的 html标记(删除< > )
const input = safe`<h1>input</h1>`;
function safe(parts) {
let str = "";
const values = Array.prototype.slice.apply(arguments).slice(1);
for (let i = 0; i < parts.length; i++) {
// 输入信息转换 表达式和输入内容都转换
const _v = parts[i].replace(/(</)|(<)/g, "<").replace(/>/g, "&rg;");
const _v1 = values[i]?.replace(/(</)|(<)/g, "<")?.replace(/>/g, "&rg;");
str += (_v || "") + (_v1 || "");
if (i === values.length - 1) {
str += parts[i + 1] || "";
}
}
return str;
}
console.log(input);
es6函数参数默认值
-
书写形参时,直接给形参赋值,赋的值即为默认值
-
function getContainer (a = 1) {}
-
-
在调用函数时,如果没有传入时是undefined时 就会使用默认值
-
function getContainer (a = 1) { console.log(a) // 1 } getContainer()
-
-
如果是null的不会使用默认值
-
function getContainer (a = 1) { console.log(a) // null } getContainer(null)
-
-
可以使用表达式函数形参默认值
-
function getContainer () { return document.getElementById('container') } //可以使用函数 //可以直接写 document.getElementById('container') function createElement(name = 'div',container = getContainer(),content = '') { const ele = document.createElement(name) if(content) { ele.innerHTML = content } container.appendChild(ele) } creatElement(undefind,undefind,'测试')
-
对 arguments 的影响
- 在默认情况下,arguments 和形参 是指向的同一片地址(浅拷贝)
- 在严格模式下,arguments 和形参 是两块不同的地址( 深拷贝)
- 如果函数使用了参数默认值,该函数会自动变成严格模式下的模式,形成深拷贝
暂时性死区
-
形参和 let 或者const 声明一样,具有作用域并且根据参数的声明顺序,存在暂时性死区
-
// b 使用a 做为默认值,因为运行到函数时a // 是先创建所有有了具体的值,b 就可以直接使用a作为默认值 function test(a, b = a) {} //报错 因为b还没有创建 存在暂时性死区里面不能使用 function test1(a = b,a) {} //报错 自定义的参数不能和变量重名 function test2(a) { let a = 1; } test(1)
-
函数剩余参数
使用arguments的缺陷
- 如果和形参配合使用,容易导致混乱
- 从语义上,使用arguments获取参数,由于形参缺失,无法从函数定义上理解函数的真实意图
- 在上面写了默认参数和arguments的影响关系(浅拷贝和深拷贝)
剩余参数的功能
-
专门用于收集末尾的所有参数,将其放置到一个形参数组中
-
function test(...args) {} // args 收集了所有参数,形成了一个数组 -
细节部分
- 一个函数,仅能出现一个剩余参数
- 一个函数如果有剩余参数必须是最后一个参数
展开运算符
-
函数传参展开, 可以把数组拆分成单个的参数做为实参传递进函数
-
const arr = [1,2,3,4] function test(...args) {} test(...arr)
-
-
数组克隆,存在对象时也是浅拷贝
-
const arr1 = [2,3,4,5] const arr2 = [...arr1,'2','3'] - 可以在克隆的基础上,新增属性
-
-
对象展开,对象克隆(浅拷贝)
-
const obj1 = { name :"222",age:11 } const obj2 = { ...obj1,name:"333" } // 克隆并覆盖相同属性 // 相当于是将旧的对象的指向赋值给了新的对象 -
const obj1 = { name :"222",age:11,address: { test:1} } // 通过新建属性 然后在新的属性里面使用展开对象 实现深拷贝 const obj2 = { ...obj1, address:{ ...obj1.adress } }
-
展开运算符练习
// 实现接收到指定参数长度后才执行的函数
function cal(a,b,c,d) {
return a + b * c - d
}
function curry(func,...args) {
return function (...subArgs) {
// 合并开始传入的参数,和后面调用传入的参数
const allArgus = [...args,...subArgs]
// 判断参数是否达到函数形参要求
if(allArgus.length >= func.length) {
return func(...allArgus)
}else {
// 没有的话 继续接收参数
return curry(func,...allArgus)
}
}
}
const newCal = curry(call,1,2)
newCal(3,5)
newCal(7,8)
const newCal2 = newCal(9)
newCal2(8)
明确函数的双重用途
-
在es6之前,使用普通函数会存在二义性,可以是构造函数也可以是普通函数
-
function Person() { if(!(this instanceof Proson)) { new thorr('错误使用') } } - 通过判断this的原型是否是当前构造函数可以判断当前执行的操作,但是可以通过call等方式绕过
-
const user = Person.call(new Person)
-
-
在es6提供了api 检测调用方式
-
function Person() { // 在没有使用new的情况下 都是返回的undefined if(!new.target) { new thorr('错误使用') } }
-
箭头函数
this指向(普通函数)
- 通过对象调用函数,this指向对象
- 直接调用函数,this指向window对象
- 如果通过new调用函数,this指向新创建的对象
- 如果通过apply等调用,this指向指定的数据
- 如果是dom事件函数,this指向事件源
const obj = {
count:0,
start:function () {
setInterVal(function() {
this.count++;
console.log(this.count) // NaN
},1000)
},
regEvent:function () {
window.onClick = function () {
console.log(this.count); // undefined
}
}
}
obj.start();
// 老的解决方法 使用闭包的形式,存储执行环境的this
const _this = this
console.log(_this)
箭头函数是什么
- 箭头函数是一个表达式,理论上,任何使用函数表达式的场景都可以使用箭头函数
-
(参数) => { //函数体 }
注意细节
-
箭头函数的this指向,取决于箭头函数定义的位置的this指向,而与如何调用无关
-
const obj = { count:0, start:function () { // 改为箭头函数调用 此时创建在对象内部 this指向obj setInterVal(() => { this.count++; console.log(this.count) // 0 1 2 },1000) }, } obj.start();
-
-
使用语法
-
// 如果参数只有一个,可以省略小括号 const todo = data => {} // 没有参数的情况下,不能省略小括号 const todo = () => {} // 如果箭头函数只有一条返回语句,可以省略大括号和return关键字 const todo = a => a > 2 // 箭头函数返回表达式,简化代码 const sun = (a,b) => ({ a,b,sum:a + b}) // 如果写{} 是不能被识别的,会被当做函数执行体 // 使用()包裹就把{}包装成一个表达式,箭头函数会把表达式的结果返回,此时就是一个对象
-
-
箭头函数中,没有自己的 this arguments new.target
- 如果使用了,则使用的是函数外层对应的 this arguments new.target
- 多层箭头函数嵌套,最里面的函数使用了this,依然是最外层箭头函数之外的this
-
const obj = { say:function () { const todo = () => { const test = () => { console.log(this) // obj } test() } todo() } } obj.say()
-
箭头函数没有原型
-
箭头函数不能作为构造函数使用
新增对象字面量语法
-
如果对象字面量初始化时.成员的名称来自于一个变量并且和变量的名称相同,则可以简写
-
const createObj = (name,user) => { const say = () => 'xxx' return { name, user, say } } - 实际上是语法糖,将对象的变量名作为key,变量值作为value
-
-
字面量对象初始化时,对象的方法可以省略冒号和function关键字
-
const obj = { name:"222", say() { console.log(this.name) //222 } } obj.say()
-
-
计算属性名
- 有的时候初始化对象时,某些属性名,可能来自于某个表达式的值,
- 在es6中可以使用 [] 来表示该属性是通过计算得到的
-
const a = 'name' const obj = { [a] :'test' }
es6中Object新增的api
-
Object.is
-
用于判断两个数据是否相等,基本上跟严格相等是一致的,除了一下两点
- NaN 和 NaN 相等 ( === 的结果是 false)
- +0 和 -0 不相等 ( === 结果是 true )
-
-
Object.assign
- 将对象的属性进行合并,同属性名会被覆盖
-
const obj1 = {name :"111"} const obj2 = {age :22} const obj3 = Object.assign(obj1,obj2) // 上面操作会将 obj1 和 obj2 的属性合并之后返回成新的对象 // 但是此次操作的副作用 会影响obj1(也被修改),就会出现 obj1 == obj3 // 第二个参数不会被修改,只有第一个参数会被修改 //解决方法 const obj4 = Object.assign({},obj1,obj2)
-
Object.getOwnPropertyNames 的枚举顺序(之前就存在,顺序没有规定)
-
获取对象的所有属性,不会返回原型链上的属性
-
获取属性的顺序完全由浏览器厂商决定
-
在es6统一了排序顺序如下
- 先排数字,再排其他
- 数字会进行升序排序,字母按书写顺序排
-
-
Object.setPrototypeOf
- 该函数用于设置某个对象的隐式原型
-
Object.setPrototypeOf(obj1,obj2) // 相当于 obj1.__proto__ = obj2
面向对象简介
面向对象是一种编程思想,跟具体的语言无关
对比面向过程:
-
面向过程: 思考的切入点是功能的步骤
-
const collectMonkey = () => {} collectMonkey() const feedForMonkey = () => {} feedForMonkey() - 将每一件事情当成一个独立的过程,一件件的处理
-
-
面向对象: 思考的切入点是对象的划分
-
const monkey = function (){} const zookeeper = function (){} zookeeper.prototype.collectMonkey = () => {} zookeeper.prototype.feedForMonkey = () => {} - 将猴子和管理员分为两个个体,由个体单独触发
-
构造函数的语法糖
es5的构造函数
-
会出现构造函数和原型方法之间存在别的代码,维护困难
-
function Animals(name,sex) { this.name = name this.sex = sex } console.log('aaa') //通过原型的形式新增方法 Animals.prototype.say = function () { console.log(this.name) }
-
-
原型成员可以被枚举
-
const a = new Animals('cscs',111) for(const prop in a) { console.log(prop) //会出现say方法 }
-
-
默认情况下,构造函数仍然可以被当做普通函数使用
-
Animals() // 直接调用构造函数
-
在面向对象编程思想中,将 一个对象的所有的成员的定义,统称为类
es6 构造函数
class Animal {
constructor(name,age) {
this.name = name
this.age = age
}
say() {
console.log(this.name)
}
}
const a = new Animals('cscs',111)
类的特点
-
类声明不会被提升,和 const 和 let 一样,存在暂时性死区
-
类中的所有代码均在严格模式下执行
-
类的所有方法都是不可枚举的
- 老版本需要通过 Object.defineProperty 控制 wireable
-
类的所有方法都无法被当做构造函数使用
-
class Person() { test () {} } const user = new Person() new user.test // 报错
-
-
类的构造器必须使用 new 来调用
-
写在 类里面的方法会被自动放到原型上
- 普通函数,箭头函数会放在实例上(下面有代码解释)
-
不是this绑定的属性 是不会被枚举出来的( 方法 )
类的其他语法
-
类型的成员名可以通过计算获取
-
const name = 'name' class Person{ [name] () {} } const user = new Person() user[name]()
-
-
getter 和 setter
-
es5的处理方法
-
Object.defineProperty(this,'age',{ set() {}, get() {} })
-
-
新的处理方式
-
class Person { // 创建一个age属性,并且给它加上getter,读取该属性时,会运行该函数 get age() { return this._age + '岁' } // 创建一个age属性,并且给它加上setter,给该属性复制时,会运行该函数 set age(age) { if(typeof age !== 'number') { throw new TypeError('类型错误') } if(age < 0) { age = 0 }else if (age > 1000) { age = 1000 } this._age = age } } - 使用 get 和 set 属性控制的方式,不是在原型上,是在实例上
-
-
-
静态成员
- 构造函数本身的成员
-
class Person {} Person.test = '222' // 等于下面写法 class Person { static test = '222' }
-
字段初始化语法(es7)
-
class Person { constructor() { // 需要手动往实例加属性 this.a = 1 } } // 可以使用下面写法简写, // 实际上最终都会在创建时转换成上面代码 class Person { // 直接创建字面量属性给实例 a = 1 } -
使用 static 的字段初始化器,添加的是静态成员,会往构造函数本身上新增属性
-
class Person() { static test = '22' } Person.test // '222'
-
-
没有使用 static 的字段初始化器,会往实例 上加属性
-
class Person() { test = '22' //等于以下写法 } Person.test // undefined const user = new Person() user.test // '22'
-
-
箭头函数使用字段初始化器时,会往实例上加属性,this 指向实例
-
class Person() { say = () => {} } // 以上代码相当于简写会被转换为 class Person { constructor() { this.say = () => {} } } const user = new Person(); const user1 = new Person(); console.log(user.say === user1.say); // false
-
-
如果使用普通函数创建,因为是放在原型上,所有实例的该方法都是相等的,和箭头函数有区别
-
class Person { say() { console.log(1); } } const user = new Person(); const user1 = new Person(); console.log(user.say === user1.say); // true
-
-
-
装饰器
- 通过增加标注的形式,修改绑定的方法或者属性或者class
-
class Test { @Obsolete print() { console.log("print方法"); } } function Obsolete(target, methodName, descriptor) { // function Test // print // { value: function print() {}, ...} // 跟Object.defineProperty 的第三个参数一样 const oldFunc = descriptor.value; // 保存旧的方法 //产生一个新方法去调用原来的方法,然后给一个提示 descriptor.value = function (...args) { console.warn(`${methodName} 方法已经过时`); oldFunc.apply(this, args); }; } -
// 最新ts实现装饰器 class Test { @Obsolete print() { console.log("print方法"); } } function Obsolete(originalMethod: any, context: any) { console.log("🚀 ~ Obsolete ~ originalMethod:", originalMethod); console.log("🚀 ~ Obsolete ~ context:", context); const methodName = String(context.name); function test(...args: any[]) { console.warn(`${methodName} 方法已经过时`); originalMethod.apply(this, args); } return test; } const user = new Test(); user.print();
类的继承
如果两个类 A 和 B,如果可以描述为B 是A(B 是 A 的扩展集,B 拥有A 里面的所有信息), 则A和B形成继承关系,比如说 猫(B) => 动物 (A)
继承关系专业术语(面向对象) (B 是 A的扩展集,是子类的情况下)
- B 继承自 A
- A 派生 B
- B 是 A 的子类
- A 是 B 的父类
如果 A 是 B 的父类,B 会自动拥有 A 的所有实例成员
es5实现继承
function Animal(type,name,sex) {
this.type = type
this.name = name
this.sex = sex
}
Animal.prototype.say = function () { console.log(this.name) }
// 修改 Dog的隐式原型指向 不然拿不到Animal的原型属性
Object.setPrototypeOf(Dog.prototype,Animal.prototype)
function Dog(name,sex) {
Animal.call(this,'狗',name,sex)
}
es6 实现继承
使用新的关键字:
-
extends
- 继承,用于类的定义
-
super
- 直接当做函数调用,表示父类构造函数,在表现上相当于使用call 调用了父类的构造函数
- 如果当做对象使用,则表示父类的原型
特殊注意
- es6要求,如果定义了constructor,并且该类是子类(使用了extends),则必须在constructor的第一行手动调用父类的构造函数(调用super)
- 如果不写constructor的话,则会有默认的构造器,该构造器需要的参数和父类一致,并且自动调用
class Animal {
constructor(type,name,age,sex) {
this.type = type
this.name = name
this.age = age
this.sex = sex
}
print() {
console.log(this.age,this.type,this.sex,this.name)
}
}
class Dog extends Animal {
constructor(name,age,sex) {
super('犬',name,age,sex) // 有点像使用call调用父类一样
this.abc = 123 // 子类特有的属性
}
print() {
super.print() // 调用父类原型的方法
console.log(this.abc) // 打印自己新增的属性
}
// 子类原型属性
jiao () {
console.log('aaa')
}
}
类的冷知识
js制作抽象类
- 抽象类,一般是父类,不能通过该类创建对象
- 抽象类对一个事/物的高度抽象,例如动物,人类,他不能具体到某一个实际上的东西
- 所以在概念上是不能直接进行实例化的,需要进行继承指明所对应的分类或者目标
-
class Animal { constructor(type) { // 通过判断构造器对象来确定来源,非自己才可以调用 if(new.target == 'Animal') { thorw new TypeError('你不能直接创建Animal对象') return } this.type = type } } // 不能这样直接写 const animal = new Animal()
this 的指向
- 正常情况下,this的指向,始终指向实例
- 使用super 时this也是执行当前实例
解构
对象解构
- 使用es6的一种语法规则,将对象或者数组的某个属性提取到某个变量中
- 解构不会对原始影响对象,只是读取对象的属性值
- 不能从null 或者 undefined 解构,会报错
es5处理对象属性
const obj = {
name:"222",
age:11
}
const name = obj.name
const age = obj.age
解构处理
let name,age;
// 对象解构 会根据左边的变量名去对象里面查找,然后进行赋值操作
// 需要使用小括号包裹,不然会被当做 { name,age } 和 = obj
// 右边的赋值操作就会报错,会把左边当做对象创建
({name,age} = obj)
//--------------
// 简化声明和取值 实际上跟上面执行一致
// 先定义4个变量,然后从对象中读取同名属性,放到变量中
// 使用const 的话 定义的变量就是不可变的
const {age,name} = obj
-
在解构中使用默认值
- 在解构没有找到值的时候,会是undefined(因为是先定义然后查找,定义之后默认就是undefined)
-
const {title} = obj console.log(title) // undefined // 设置默认值,拿不到同名属性的时候就使用默认值 const {type = 1} = obj console.log(type) // 1
-
非同名属性解构
- 定义的变量和获取的对象属性名不一致
-
const user = { sex:"男", name:"测试" } const {name,sex:gender} = user // 先定义两个变量, name gender // 再从对象user里面读取同名属性赋值,(其中gender读取的sex属性)
-
使用默认值加非同名属性(null 的情况不会取默认值)
-
const {name,sex:gender = '男'} = user
-
-
通过解构读取多层数据
-
const user = { name:"测试", address:{ province:"四川" } } // 解构出user 中的name,province // 定义 name 和 province // 此时address 是不能用的,这里结构拿的是 user.address.province const {name,address:{ province }} = user //如果需要address const { name,address, address: { province }} = user;
-
数组解构
在js中数组本质上也是对象,支持key是连续数字的对象而已
// 通过下标修改名称解构数组
const arr = [1,2,3,4]
const {0:n1,1:n2} = arr
console.log(n1,n2) // 1 2
es6 对数组解构提供了单独的语法糖(按顺序取值,与变量名无关)
const arr = [1,2,3,4]
let name,age;
// 按数组顺序取值,此时跟变量名无关了
([name,age] = arr)
//简化写法
let [name,age] = arr
-
跳过下标取值
-
// 需要跳过的部分使用不传入变量即可 // 取不到的话就是undefined // 可以使用默认值 let [n1,,n2,n3 = 1] = arr
-
-
数组嵌套取值
-
const numbers = ["a", "b", "c", "d", [1, 2, 3, 4]]; // 得到numbers下标为4的数组中的下标为2的数组,放到变量n中 const [, , , , [, , n]] = numbers; console.log(n); //3
-
-
数组嵌套对象取值
-
const numbers = ["a", "b", "c", "d", { a: 1, b: 2 }]; const [, , , , { a }] = numbers; console.log(a); //1
-
解构剩余参数
- 剩余项的解构放在最后面,
// 提取name属性 ,将user里面剩余参数全部放入 obj 里面
const {name ,...obj} = user
// 将数组前两项放入n1 n2,剩余数据放入 arr2 里面
const [n1,n2,...arr2] = arr
通过解构交换变量
const user = {x: 1,y: 22,};
const { x: y, y: x } = user;
console.log(x, y); // 22 1
//-------
let a = 1,b =2
[b,a] = [a,b]
console.log(a,b) // 2 1
函数参数解构
// 由于从undefined 或者 null 解构会报错
// 这个时候需要设置参数默认值,让解构发生在安全的情况下
function todo({name = '111',age} = {}) {
console.log(name,age)
}
todo({name:"11",age:222})
普通符号(symbol)
-
符号是es6新增的数据类型,它通过使用函数 Symbol 来创建
-
符号的设计初衷是为了对象设置私有属性
- 私有属性的含义是只能在对象内部使用,外面无法使用
// 函数的参数是对这个符号的描述信息
const syb1 = Symbol()
const syb2 = Symbol('aa')
// 描述信息可以写中文
const syb3 = Symbol('这是描述')
// syb1 => Symbol()
// syb2 => Symbol(aa)
符号的特点
-
符号没有字面量
-
使用typeof 得到的结果 就是 "symbol"
-
每一次调用Symbol 函数得到的结果永远是不相等的(哪怕描述信息一样),(符号是永远唯一)
- 类似于重新开辟内存地址)
-
符号可以作为对象的属性名存在,这种属性称为符号属性(以往对象的属性名一定是字符串,数组也不例外)
-
const syb1 = Symbol('属性名') const obj = { a:1, [syb1]:3 } -
可以通过符号属性实现私有属性不能被外界访问(常规方式)
-
const hero = (function () { // 由于是局部变量的形式 且是一个symbol 变量 // 内部可以通过变量访问,外部拿不到这个唯一值,无法访问 const getRandom = Symbol(); return { name: "222", [getRandom](min, max) { return Math.random() * (max - min) + min; }, }; })();
-
-
符号属性不能被枚举,(for in 拿不到) (Object.keys 也拿不到)
-
const syb = Symbol() const obj = { a:1, [syb]:2 } for(const prop in obj) { console.log(prop) // 1 }
-
-
Object.getOwnPropertyNames() 可以枚举一些无法被枚举的特殊值,但是也不能枚举符号属性
-
es6 新增的了Object.getOwnPropertySymbols() 可以读取到符号属性
- 返回了 符号属性的数组集合
-
-
符号无法被隐式转换,不能进行数学运算
-
const syb = Symbol() syb + 1 // 报错
-
-
符号可以进行显示的转换成字符串的操作
- 使用console之所以能输出,就是系统做了显示转换
-
const syb = Symbol() console.log(String(syb))
共享符号
根据符号名称(符号描述)能够得到同一个符号
// 是 Symbol.for 创建的符号和Symbol不一样,符号名一致的时候,创建的一致
const syb = Symbol.for('符号描述/符号名')
//
const syb1 = Symbol.for('测试')
const syb2 = Symbol.for('测试')
syb1 === syb2 //true
通过共享符号暴漏属性给外部
const obj = {
a:1,
[Symbol.for('c')]:3
}
console.log(obj[Symbol.for('c')])
模拟Symbol.for 实现
const SymbolFor = (() => {
const global = {}
return function (name) {
if(!global[name]) {
global[name] = Symbol(name)
}
return global[name]
}
})()
知名(公共,具名)符号
-
知名符号是一些具有特殊含义的共享符号,通过Symbol的静态属性得到
-
Es6 使用知名符号 暴露了某些场景的内部实现
- 可以通过修改这些具名符号的实现,影响js一些api的实现
-
js对外暴露的一些Symbol符号
Symbol.hasInstance
- 该符号用于定义构造函数的静态成员,它将影响 instanceof 的判断 ( 默认判断原型 )
-
// instanceof 的调用,现在可以通过修改具名符号修改计算结果 // instanceof 调用就是触发调用方的api function A() {} const obj = new A(); console.log(obj instanceof A) // true // 本质上触发的是 A[Symbol.hasInstance] 方法 // 修改hasInstance 的返回结果,只能通过Object.defineProperty Object.defineProperty(A, Symbol.hasInstance, { value: function () { return false; }, }); console.log(obj instanceof A); //false console.log(A[Symbol.hasInstance](obj)); // false
Symbol.isConcatSpreadable
- 会影响数组的concat方法
-
const arr = [12,3,45] const arr1 = arr.concat(0,[1,2,3,4]) // arr1 => [12, 3, 45, 0, 1, 2, 3, 4] arr[Symbol.isConcatSpreadable] = false const arr3 = arr.concat(0,[1,2,3,4]) //arr2 => [[12,3,45], 0, 1, 2, 3, 4] -
// 对 对象的处理 const arr = [12] const obj = { 0:3, 1:4, length:2, [Symbol.isConcatSpreadable]: true, //设置后分割 默认会把整个对象丢进去 } const arr3 = arr.concat(2,obj)
Symbol.toPrimitive
- 影响类型转换的结果
-
const obj = { a : 1,b : 2} // 会先调对象的 valueOf 然后调用 toString() 最后在运算 console.log(obj + 123) -
// 接收 默认转换行为 默认是default 后续接收的是 目标类型 obj[Symbol.toPrimitive] = function(type) { return 2 } // 函数进行转换时会调用上面方法 console.log(obj * 2) // 4 -
class Temperature { constructor(degree) { this.degree = degree; } [Symbol.toPrimitive](type) { if (type == "default") { return this.degree + "摄氏度"; } else if (type === "number") { return this.degree; } else if (type == "string") { return this.degree + "摄氏度"; } } } const temp = new Temperature(30); console.log(temp + "!"); console.log(temp + 30); console.log(String(temp));
Symbol.toStringTag
- 会影响 Object.prototype.toString 的返回值
-
const obj = { [Symbol.toStringTag]: "测试", }; console.log(Object.prototype.toString.apply(obj)); //[object 测试]
Promise
- 目的是解决es5的回调地狱问题(使用链式编程解决)
- 规范是在Api出来之前就产生了,后续在es6实现了Promise api,完成了Promise A+在js的实践
// 接收回调函数执行
const todo = (onSuccess, onFail) => {
if (Math.random() > 0.3) {
onSuccess();
} else {
onFail();
}
};
// 在执行失败后继续执行 直到全部执行失败或者执行成功
todo(
() => {
// 成功操作
console.log("成功");
},
() => {
// 失败后继续执行
todo(
() => {
console.log("成功2");
},
() => {
// 失败后继续执行
console.log("失败2");
}
);
}
);
Promise规范
- Promise 是一套专门处理异步场景的规范,可以有效的避免回调地狱的产生,使异步代码更加清晰简洁统一
- 这套规范的名称为 Promise A+
Promise A+ 规定
-
所有的异步场景,都可以看做是一个异步任务,每个异步任务在 js 中应该表现为一个对象,该对象称之为Promise对象,也叫做任务对象
- 当前拿不到结果的任务,例如 定时器,网络请求, 需要等到结果的任务
-
每个任务对象,都应该有两个阶段,三个状态
-
未决阶段 (unsettled), 还没有确切的结果正在等待
- pending (挂起)
-
已决阶段 ( settled ), 产生了结果,可以进行下一步处理
- fulfilled (完成)
- rejected (失败)
-
它们之间存在的逻辑
- 一个任务总是从 未决 阶段变成 已决 阶段, 无法逆行
- 一个任务总是 从 挂起 状态变成 完成或者 失败 状态,无法逆行
- 任务一旦完成或者失败,状态就固定了,无法再次改变
-
-
挂起到完成我们称之为 resolve
- 挂载到失败我们称之为 reject
- 任务完成时可能有一个相关数据
- 任务失败时可能有一个失败原因
-
可以针对任务进行后续处理,针对完成状态的后续处理称之为 onFulfilled ,针对失败的后续处理称之为 onRejected
-
promise对象必须要有一个then方法
总结:
- Promise A+ 认为当前直接拿不到结果的操作都可以是作为异步任务,Promise可以作为在异步任务js中具体表现
- Promise 第一阶段是状态扭转,从未决阶段到已决阶段,同时状态从pending 到 fulfilled / rejected
- Promise 第二阶段 是对状态扭转后的后续处理, 触发 onFulfilled / onRejected
Promise API
Promise 构造函数如果在状态变更之前出现内部错误, 这个时候状态会自动变更为 失败
// Promise 构造函数 接收一个函数作为参数(必填)不然会报错
// Promise 构造函数的结果是一个 promise 对象
// 执行该构造函数时 状态默认会为pending
const pro = new Promise(() => {})
// 函数用来描述任务执行过程 在构造函数执行时会立即执行
// 函数会接收两个参数,可以扭转promise的状态
// resovle 把状态 从 挂起变为 完成
// reject 把状态 从 挂起变为 失败
// Promise 构造函数如果在状态变更之前出现内部错误, 这个时候状态会自动变更为 失败
const pro1 = new Promise((resolve, reject) => {
console.log("开始跑马拉松");
const duration = Math.floor(Math.random() * 3000);
setTimeout(() => {
if (Math.random() < 0.5) {
resolve("我跑完了");
} else {
reject("脚伤了");
}
}, duration);
});
console.log(pro) // Promise {<pending>}
// 拿到promise返回值,调用 then 方法执行下一步( onFulfilled,onRejected )
// then 接收两个函数作为参数
// 参数一函数的参数 接收成功状态的数据 (resolve 里面的内容,如果没有就是空)
// 参数二函数的参数 接收失败状态的数据 (reject 里面的内容,如果没有就是空)
// 成功/失败 会执行对应的函数,另一个不会执行
pro1.then(res => { console.log(res) },reson => { console.log(reson) })
// 只处理失败 成功函数传null
pro1.then(null,reson => { console.log(reson) })
Promise 的构造函数
-
当触发状态变更操作时不会终止构造函数执行(也就是说仅是执行了Promise的状态变更)
-
而是要等所有的代码执行完毕后(状态变更不会终止)才会执行对应的then函数,注册微任务等待执行
-
也就是说先执行完函数体代码后,里面执行执行then函数,如果有处理函数就放入到微任务等待执行
-
new Promise((res) => { console.log(1); res(2); console.log(3); }).then(res => { console.log(res) }) // 1 3 2
-
-
当所有的代码执行完毕之后才会有返回值
-
let a ; a = new Promise((res) => { console.log(a) // undefined }) console.log(a) // Promise { <pending> }
-
Promise 的 catch 方法
pro1.then(null,reson => { console.log(reson) })
// 等同于
pro1.catch(reson => { console.log(reson) })
Promise 链式调用
-
then 方法必定返回一个新的Promise(用于处理 pormise 后续操作)
-
catch 方法必定返回一个新的 Promise(用于处理失败的promise)
- 也就是说 后续处理(then的返回值)也是一个 promise 任务
-
const pr1 = new Promise(res =>{ res() }) // 得到then的结果 const pr2 = pr1.then(res =>{ console.log('任务2') }).catch(err => console.log(err)) console.log(pr2) // Promise{<pending>} 此时是同步任务 拿不到变更后的状态 setTimeout(()=>{ // 延迟获取 console.log(pr2) // Promise{<fulfilled>} })
-
新任务的状态取决于后续处理
-
若没有相关的后续处理,新任务的状态和前任务一致, 数据为前任务的数据
-
也就是说,then 没有写对应的resolve 或者 reject 的处理函数,状态就是调用then的promise的状态和响应值
-
// 当 promise 变为 成功 状态之后,没有做 then 的成功操作 // 那么 then 的返回值(promise状态)就是上一个的状态 // 数据也是上一个完成传递的数据 const pr1 = new Promise(res =>{ res(1) }) const pr2 = pr1.catch(err => {}) setTimeout(() => { console.log(pr2) },1000) // Promise {<fulfilled>: 1} -
// 反过来也是同样的道理 ,promise 变为 失败 的状态之后,没有做失败的处理 const pr1 = new Promise((res,rej) =>{ rej(1) }) const pr2 = pr1.then(() => { }) setTimeout(() => { console.log(pr2) },1000) // Promise {<rejected>: 1}
-
-
如果传的不是函数会直接跳过 进入下一个then,相当于没写处理函数保留上一步的promise状态和数据
-
// 如果传的不是函数 相当于就是处理函数传了一个null ,就直接使用上一步的Promise状态 Promise.resolve(1).then(2).then(Promise.resolve(3)).then(console.log); // 1
-
-
-
若有后续处理但还未执行,新任务挂起
- 也就是说执行调用then的时候,then的调用方的promise任务还没有结果(没有发生状态变更),这个时候then返回的promise也会跟着一起等结果(状态变更)
-
// promise 在 2000 后才有结果,then这个时候做了后续处理 // 但是接收的promise 状态还没有变更,也就只能跟着一起等 const pr1 = new Promise((res) =>{ setTimeout(() => { res(1) },2000) }) const pr2 = pr1.then(() => { }) setTimeout(() => { console.log(pr2) },1000) // Promise {<pending>} // 这个时候能拿到 状态已经变更 setTimeout(() => { console.log(pr2) },4000) // Promise {<fulfilled>: undefined}
-
若后续处理执行了,则根据后续处理的情况确定新任务的状态(看then 的执行情况,报错或者不报错)
-
后续处理执行无错,新任务的状态为完成,数据为后续处理的返回值
- 假如then里面处理resolve的函数执行成功,那么then返回promise的状态就是成功.
- 而这个promise的返回值就是resolve执行函数的return数据
-
const pr1 = new Promise((res) =>{ res() }) // 执行没有报错 状态就是 pr2 promise 状态就是成功 // pr2 的promise 的 响应值 就是 then的成功函数的 return 结果 const pr2 = pr1.then(() => { }) const pr3 = pr1.then(() => { return 100 }) // pr2 Promise{<fulfilled> : undefind} // pr2 Promise{<fulfilled> : 100} //------------ // 起始状态是 失败 // then 处理失败了之后 没有发生错误 返回的就是成功 const pr1 = new Promise((res,rej) =>{ rej() }) const pr2 = pr1.catch(() => { return 123 }) //pr2 Promise {<fulfilled>: 123}
-
后续处理执行有错,新任务的状态为失败,数据为异常对象
- 和上面处理成功的情况类似,如果处理成功的函数执行失败,那么then返回promise 状态就是 失败
- 而返回的promise的响应值就是 异常对象
-
const pr1 = new Promise((res) =>{ res() }) // 在后续执行出现错误 const pr2 = pr1.then(() => { // 模拟出现错误 throw 123 }) //pr2 Promise {<rejected>: 123}
-
后续执行后返回的是一个新的任务对象,新任务的状态和数据与该任务对象一致
- then里面如果返回了新的promise对象,then的返回值取决于新的promise 的返回值(状态和数据)
-
const pr1 = new Promise((res) =>{ res() }) // 此时 跟新的 promise 的状态绑定 const pr2 = pr1.then(() => { // 此时没有变更状态 那么就是pending // 如果变更了状态 那么pr2状态就是变更后的状态 return new Promise((res) =>{}) }) // pr2 Promise {<pending>}
-
-
总结
-
调用then返回就是一个promise,但是返回的promise状态和数据有几种情况
-
如果调用then的时候 ,then的调用方此时还是 pending 状态,那么then会先返回 一个promise,状态为pending,
- 当then的调用方状态发生变更后才会执行then里面的处理函数,此时then的返回值就要看以下处理情况
-
没有写处理对应状态的函数,那么使用then的调用放的状态和数据
-
写了处理函数
-
处理函数内部执行错误,那么返回的就是 失败 状态,数据就是 异常对象
- promise的静态方法接收的promise数组中,如此存在方法执行报错,也会立即终止执行,返回失败的promise状态,响应值是错误对象
-
处理函数执行成功
- 如果 return 一个新的promise,那么then返回的状态就是这个promise 的状态和返回值
- 如果return 字面量或者不写, 那么 状态就是完成 ,返回值就是return的字面量,没写就是undefined
-
-
// 测试1
const pr1 = new Promise((res) => {
setTimeout(() => {
res(1);
}, 1000);
});
const pr2 = pr1.then((data) => {
console.log(data);
return data + 1;
});
const pr3 = pr2.then((res) => {
console.log(res);
});
console.log(pr1, pr2, pr3);
setTimeout(() => {
console.log(pr1, pr2, pr3);
}, 2002);
//Promise { <pending> } Promise { <pending> } Promise { <pending> }
//1
//2
//Promise { 1 } Promise { 2 } Promise { undefined }
// 测试2
// po的结果就是最后一个then的返回值
const po = new Promise((res) => {
res(1);
})
.then((res) => {
console.log(res);
return 2;
})
.catch((err) => {
throw 3;
})
.then((res) => {
console.log(res);
});
setTimeout(() => {
console.log(po) // Promise{<fulfilled>:undefined}
},1000)
// 1 2
// 测试3
const pro1 = () => {
return new Promise((resolve, reject) => {
console.log("开始跑马拉松");
const duration = Math.floor(Math.random() * 3000);
setTimeout(() => {
if (Math.random() < 0.1) {
resolve("我跑完了");
} else {
reject("脚伤了");
}
}, duration);
});
};
// catch 链式处理
pro1()
.catch((err) => {
console.log("失败了 继续调用");
return pro1();
})
.catch((err) => {
console.log("失败了 继续调用1");
return pro1();
})
.catch((err) => {
console.log("失败了 继续调用2");
return pro1();
})
.then(
(res) => {
console.log(res);
},
(err) => {
console.log(err);
}
);
使用catch 捕获前面任意一步的错误
- 因为then只能处理完成状态,根据没有处理函数状态会使用上一步Promise的返回值和状态的定论,状态会一直保留,直到碰到异常处理函数
new Promise((res, rej) => {
res(1);
})
.then((res) => {
throw 3;
})
.then((res) => {
return 1;
})
.catch((err) => {
console.log(err);
});
Promise的静态方法
Promise.resolve 和 Promise.reject
- 直接返回 完成/失败 状态 的promise对象,结果就是调用值
const pr = Promise.resolve(1)
const pr1 = Promise.reject(1)
// Promise { <fulfilled>1 }
// Promise { <rejected>1 }
// ---
// 实现原理
Promise.resolve = (value) => {
return new Promise(res=> {
res(value)
})
}
Promise.all
-
接收一个由promise任务组成的数组,按顺序执行这些promise,等待所有promise执行完成,然后统一执行then或者进入catch
-
如果数组里面存在传入的不是promise对象,会通过Promise.resolve包装成promise
-
会返回一个新的Promise,状态由接收的promise数组执行结果决定
-
只要其中一个promise状态处于pending 那么返回的promise 状态就是pending
-
当所有的 promise 状态变为 完成 返回的状态就是完成
- 当所有的变成完成后 得到的结果 是所有promise 数组中所有promise执行完成的结果形成的数据数组
-
当其中有一个失败,那么返回的状态就是 失败
- 此时返回的数据不是数组,只能拿到失败的哪一个数据
-
const pr = Promise.all([Promise.resolve(1), Promise.resolve(2)]);
// pr Promise{<fulfilled> [1,2]}
const pr1 = Promise.all([Promise.resolve(1), Promise.reject(2)]);
// pr Promise{<rejected> 2}
Promise.any
-
接收一个由promise任务组成的数组,按顺序执行这些promise,等待所有promise执行完成,
-
和 Promise.all 相反
- 只要有一个成功(完成状态),Promise状态就变为 完成,数据就是这个率先完成的任务的数据
- 全部任务失败 那么就失败了,返回的就是所有失败的数据
- 只要其中一个promise状态处于pending 那么返回的promise 状态就是pending
const pr = Promise.any([Promise.reject(1), Promise.reject(2)]);
// pr Promise{<rejected> [1,2]}
const pr1 = Promise.any([Promise.resolve(1), Promise.reject(2)]);
// pr Promise{<fulfilled> 1}
Promise.allSettled
- 跟上边类似, 接收一个Promise组成的数组
- 等所有promise状态变更才会返回
- 返回的数据是所有promise的状态和数据的对象数组
const pr1 = Promise.allSettled([Promise.resolve(1), Promise.reject(2)]);
pr1.then((res) => console.log(res));
// 得到结果
[
{ status: 'fulfilled', value: 1 },
{ status: 'rejected', reason: 2 }
]
Promise.race
- 跟上边类似, 接收一个Promise组成的数组
- 得到Promise中最先完成的结果,状态就是这个promise的状态和结果
const pr1 = Promise.race([
new Promise((res) => {
setTimeout(() => {
res();
}, 100);
}),
Promise.reject(2),
]);
pr1.then((res) => console.log(res)).catch((err) => console.log(err));
//2
总结
- 需要拿到所有结果,但是其中有一个请求失败了就放弃所有数据,这种情况推荐使用 Promise.all
- 需要拿到所有结果,失败的也要拿到,最终我们自己处理成功的数据,这种情况推荐使用 Promise.allSettled
- 需要拿到所有数据中第一个成功的数据, 这种情况下推荐使用 Promise.any
- 需要拿到第一个完成的数据,不区分成功或者失败, 这种情况下推荐使用 Promise.race
Promise.finally
- 无论是什么结果,都会执行,且没有任何参数
- 也会返回一个新的promise,但是状态和数据是跟调用方一样,跟函数返回值无关
- 但是如果finally执行出现错误 这个时候状态就会变为 失败 ,响应值就是 错误对象
const pr1 = new Promise((res) => {
res(1)
})
const pr2 = pr1.finally(() => {
console.log('finally')
})
// pr2 Promise{<fulfilled> 1}
async和await
消除回调
- 有了Promise后,异步任务就有了一种统一的处理方式
- es7 推出了两个关键字 async 和 await 用于消除异步场景回调写法,让代码进入同步写法
async
-
async 关键字用于修饰函数, 被它修饰的函数,一定返回Promise
-
标记的函数返回值会被当做 promise的数据,如果没有返回就是undefined
- 如果函数内部执行不报错那么返回的 promise状态就是 成功,
- 执行出现报错就是 失败
- 但是如果出现函数返回的promise,那么函数返回值就是 return 出去的 promise的结果和状态
async function m () {} // Promise {<fulfilled>: undefined}
async function m1 () { a.to() } // Promise {<rejected>: ReferenceError: a is not defined }
m()
m1()
// 使用async 标记箭头函数
const todo = async () => {}
await
- 必须写的 async 标记的函数体内部
- 消除promise的then的链式调用,直接拿到promise的执行结果
- 作用是等待所标记的 promise执行完成,然后返回 promise 的执行结果(数据)
async function test() {
return 123
}
async function m () {
const res = await Promise.resolve(1)
const res1 = await test() // 执行函数得到函数结果 Promise{<fulfilled>:1}
console.log(res) //1
console.log(res1) //123
}
m()
-
await 标记的表达式如果不是promise,在执行过程中会把等待的非Promise的内容使用Promise.resolve进行转换
-
(async () => { const data = await 1 //会把这个1 变成 Promise.resolve(1) console.log(data) //1 })() -
标记的表达式如果是函数
-
需要执行这个函数,得到函数返回的结果
-
async function async1(params) { console.log("async1 start"); // 会直接执行await 绑定的函数 await async2(); console.log("async1 end"); } async function async2(params) { console.log("async2"); } async1(); // async1 start // async2 // async1 end
-
-
执行失败会将promise的状态变为失败,同时整个代码会报错,终止代码执行
-
function todo() { a.todo(); } (async () => { const data = await todo(); console.log(data); console.log(4); })();
-
-
-
-
使用try catch 处理await的异常
-
(async () => { try { const data = await (() => Promise.reject(2))(); console.log(data); } catch (err) { // 接收失败的原因 console.log(err); // 2 } })();
-
-
await可以理解为使用 then 帮我们处理了await 后面的代码
-
(async () => { Promise.resolve().then((res) => console.log(1)); Promise.resolve().then((res) => console.log(2)); const data = await 3 console.log(data) console.log(4) })(); // 代码分析 把await的后续代码放入一个then里面执行, (async () => { Promise.resolve().then((res) => console.log(1)); Promise.resolve().then((res) => console.log(2)); Promise.resolve(3).then(data=> { console.log(data) console.log(4) }) })(); //最后输出1234 -
如果说我们在 await 后面使用的表达 一直没有返回结果,这个时候代码的执行就会被阻塞
-
function todo() { return new Promise((resolve) => { setTimeout(() => { resolve(222); }, 10000); }); } (async () => { const data = await todo(); console.log(data); console.log(4); })();
-
-
async 函数体内部的阻塞不会影响到外部,如果内部阻塞(就是遇到await)就会放入微队列,此时整个函数相当于执行完毕回到全局任务
-
const todo = async () => { // await直接放入微任务 函数体代码执行完毕 const res = await (() => { return new Promise((res) => { setTimeout(() => { res(222); }, 10000); }); })(); console.log(res); }; todo(); console.log(3); // 3 222
-
-
//代码测试1
async function m1() {
return 1;
}
async function m2() {
const n = await m1(); // 相当于 Promise.resolve(1)
console.log(n);
return 2;
}
async function m3() {
// 没有await 拿到就是m2的返回值
// 但是由于m2 async函数里面await 那么函数返回值就是undefined
const n = m2();
console.log(n); // Promise { <pending> }
return 3;
}
m3().then((m) => console.log(m));
m3();
console.log(4);
// Promise { <pending> }
// m3 因为没有await 直接拿到函数结果 输出 n
// 进入m2函数,插入一条微任务
// 此时m3函数结果已经变为fulfilled <3>
// 第一次m3执行完毕 插入一条微任务( console.log(n); )
// Promise { <pending> }
// 第二次m3执行 因为没有await 直接拿到函数结果 输出 n
// 进入m2函数,插入一条微任务
// 执行完毕
// 4 执行全局任务 console.log(4) ,后续开始执行 微任务
// 1 m2函数输出 1 (之前插入的微任务)
// 3 然后执行 (m) => console.log(m) 3
// 1 m2函数输出 1 (之前插入的微任务)
手写Promise
目的就是完成Promise A+ 规范(实现大概)
实现构造器
class MyPromise {
/**
* @param { Function } executor - 传入的执行函数
*/
constructor(executor) {
this._state = PENDING;
this._value = undefined;
this._handles = []; //处理函数形成的数组
executor(this._resolve.bind(this), this._rejected.bind(this));
}
/**
* 向处理队列里面添加一个函数
* @param {*} executor
* @param {*} state
* @param {Function} resolve 修改then函数返回的状态
* @param {Function} reject 修改then函数返回的状态
*/
_pushHandler(executor, state, resolve, reject) {
this._handles.push({
executor,
state,
resolve,
reject,
});
}
/**
* 根据实际情况执行队列
*/
_runHandlers() {
// 任务处于挂起
if (this._state == PENDING) {
return;
}
while (this._handles[0]) {
const handler = this._handles[0];
this._runOneHandler(handler);
this._handles.shift();
}
}
/**
*
* @param {object} handler
*/
_runOneHandler({ executor, state, resolve, reject }) {
runMicroTask(() => {
if (this._state !== state) {
// 状态不一致 不处理
return;
}
// 后续处理不是函数
if (typeof executor !== "function") {
this._state == FULFILLED ? resolve(this._value) : reject(this._value);
return;
}
try {
const result = executor(this._value);
if (isPromise(result)) {
result.then(resolve, reject);
} else {
resolve(result);
}
} catch (error) {
console.log("🚀 ~ MyPromise ~ runMicroTask ~ error:", error);
reject(error);
}
});
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
this._pushHandler(onFulfilled, FULFILLED, resolve, reject);
this._pushHandler(onRejected, REJECTED, resolve, reject);
this._runHandlers();
});
}
_changeState(newState, value) {
if (this._state !== PENDING) {
return;
}
this._value = value;
this._state = newState;
this._runHandlers(); // 状态变化执行队列
}
_rejected(reason) {
this._changeState(REJECTED, reason);
}
_resolve(data) {
this._changeState(FULFILLED, data);
}
}
实现catch 方法
/**
* 仅处理失败的场景
*/
MyPromise.prototype.catch = function (callback) {
return this.then(null, callback);
};
实现finally方法
/**
* 无论成功还是失败都执行
* @param {*} onSettled
*/
MyPromise.prototype.finally = function (onSettled) {
return this.then(
(data) => {
onSettled();
return data;
},
(reason) => {
onSettled();
throw reason;
}
);
};
实现Promise.resolve
/**
* 得到一个被拒绝的promise
* @param {*} reason
*/
MyPromise.reject = function (reason) {
return new MyPromise((resolve, reject) => {
reject(reason);
});
};
/**
* 返回一个已完成的promise
* 1. 传递的data本身就是es6的promise对象
* 2. 不是es6的promise,但是是PromiseLike(实现了Promise A+)
* @param {*} data
*/
MyPromise.resolve = function (data) {
// data instanceof Promise
if (data instanceof MyPromise) {
return data;
}
return new MyPromise((resolve) => {
if (isPromise(data)) {
data.then(resolve, this.reject);
} else {
resolve(data);
}
});
};
实现 Promise.all
/**
* 得到一个新的Promise
* 该Promise的状态取决于 proms 执行
* proms 是一个迭代器,包含多个Promise
* 全部Promise成功,则返回的Promise成功,数据为所有Promise成功的数据,并且顺序按照传入数组的顺序排列
* 只要有一个失败,则返回的promise失败,原因是失败的原因
* @param {Iterator} proms
*/
MyPromise.all = function (proms) {
return new MyPromise((resolve, reject) => {
try {
const result = [];
let count = 0; // promise总数
let fulfilledCount = 0; // promise完成的数量
for (const p of proms) {
let i = count;
count++;
MyPromise.resolve(p).then((data) => {
fulfilledCount++;
result[i] = data;
if (fulfilledCount === count) {
resolve(result);
}
}, reject);
}
if (count == 0) {
resolve(result);
}
} catch (error) {
reject(error);
}
});
};
实现Promise.allsettled
/**
* 等待所有promise有结果之后
* 该方法返回的promise完成
* 并且安装顺序将所有结果汇总
* @param {Iterator} proms
*/
MyPromise.allSettled = function (proms) {
const ps = [];
for (let p of proms) {
ps.push(
MyPromise.resolve(p).then(
(data) => ({
status: FULFILLED,
value: data,
}),
(reason) => ({
status: REJECTED,
value: reason,
})
)
);
}
return MyPromise.all(ps);
};
MyPromise.allSettled([
(() => {
return new MyPromise((res) => {
setTimeout(() => {
res(1);
}, 100);
});
})(),
MyPromise.resolve(2),
MyPromise.reject(3),
4,
]).then((res) => {
console.log(res);
});
实现Promise.race方法
MyPromise.race = function (proms) {
return new MyPromise((resolve, reject) => {
for (let p of proms) {
MyPromise.resolve(p).then(resolve, reject);
}
});
};
Fetch Api
XMLHttpRequest 的问题
- 所有的功能全部集中在同一个对象上,容易书写出混乱不易维护的代码
- 采用传统的事件驱动模式(回调函数),无法适配新的Promise Api
Fetch Api 的特点
-
并非取代AJAX, 而是对 AJAX 传统 Api 的改进
- AJAX 是一种标准,异步请求刷新
-
精细的功能分割,头部信息,请求信息,相应信息等均分布到不同的对象,更利于处理各种复杂的 AJAX 场景
-
使用 Promise Api 更利于异步代码的书写
-
Fetch Api 并非ES6的内容,术语HTML5 新增的 Web Api
- Es6 是语言标准,浏览器厂商根据这个标准去实现这些api
- webApi 是浏览器提供的api,包括dom bom 以及es 语法
- 只有浏览器可以使用(因为是web提供)
fetch Api 使用
使用 fetch 函数 即可立即向服务器发送网络请求
参数
- 必填,字符串,请求地址
- 选填,对象,请求配置
fetch('https://66d7d20037b1cadd80525d16.mockapi.io/api/local/getApiLocal') // 发出请求
请求配置
-
method
- 请求方式,默认是GET
-
headers
- 请求头配置,配置为对象
-
body
- 请求体配置
- 请求体必须匹配请求头中的 Content-Type
-
mode
- 请求模式,跨域和不跨域,默认是cors
-
credentials
- 如何携带凭据(cookie)
- omit 默认值,不懈怠cookie
- same-origin ,请求同源地址携带cookie
- include,请求任何地址都携带cookie
-
cache
- 缓存模式
- default ,请求之前将检查一下http的缓存
返回值
-
返回的就是一个promis 对象
-
当收到服务器的返回结果后,Promise进入resolved状态,状态数据为Response对象
- 网络请求发起成功并且已经得到响应
-
当网络发生错误(或者其他导致无法完成交互的错误) 时,Promise 进入 rejected 状态,状态数据为错误信息
- 网络请求无法到达,可能是请求错误可能是配置错误
fetch(url, config).then(res=> {
console.log(res) //Response对象
}).catch(err => console.log(err))
如何使用 Response 对象
-
ok 属性
- 当响应码在200 - 299 之间返回的都是true,其他为false
- fetch api 能拿到 ok 属性说明请求一定发送成功
-
status 属性, 响应的状态码
-
text()
- 用于处理文本格式的 AJAX 响应,他从响应中获取文本流,将其读完,然后返回一个被解决为 string 对象的Promise
-
blob()
- 用于处理二进制文件格式(图文表格之类)的Ajax 响应,它读取文件的原始数据,一旦读取完整个文件,就返回一个被解决为 blob 对象的 Promise
-
json()
- 用于处理 JSON 格式的Ajax 响应,它将json 数据流转换成一个被解决的 js 对象的promise
-
redirect()
- 用于重定向到另一个URL,他会创建一个新的 Promise.以解决来自重定向的 URL 响应
- 服务器返回302并且附带另一个接口的地址,然后我们可以直接调用这个api 自动请求
fetch(url, config)
.then((res) => res.json()) // 调用api 返回一个新的promise
.then((res) => console.log(res)) // 调用then 拿到返回数据
.catch((err) => console.log(err));
Request 对象
- 在调用fetch 的时候,在内部执行会将url和config合并创建一个Request对象
- 在调用 fetch 的时候,也可以直接传递 Request 对象
new Request(url,config)
// 封装请求配置
function getRequestInfo() {
const url =
"https://66d7d20037b1cadd80525d16.mockapi.io/api/local/getApiLocal";
const config = {
method: "POST",
headers: {
"Content-Type": "application/json",
a: 1,
},
body: JSON.stringify({
a: 1,
}),
};
const req = new Request(url, config);
return req.clone();
}
注意点:
- 尽量保证每次请求都是一个新的request对象
- 调用
req.clone();克隆一个新的对象
Response 对象
- 一般情况下 调用fetch 会自动创建Response对象
- 可以手动构建 Response对象 ,实现mock
function getData1() {
// 参数1 数据
// 参数2 跟fetch api 的返回的属性一样
const rps = new Response(`[{"id":1}]`, {
ok: true,
status: 200,
});
rps.json().then((res) => console.log(res));
}
Headers 对象
- 在Request 和 Response 对象内部,会将传递的请求头对象,转换为 Headers 对象
- 所以我们在调用 fetch 的 headers 就可以直接传入 Headers 对象
headers: {
"Content-Type": "application/json",
a: 1,
},
// 改为
headers:new Headers({
"Content-Type": "application/json",
a: 1,
})
const req = new Request(url, {
headers
});
console.log(req.headers)
Headers 对象中的方法
-
has( key )
- 检查请求头中是否存在指定的 key 值
-
get( key )
- 得到请求头中指定的key 的值
-
set( key,value )
- 修改对应的键值对,不存在的时候会新增
-
append ( key,value )
- 新增键值对,如果已经存在会追加 .value变成多个
-
keys()
- 得到所有的key
-
values()
- 得到所有的value
-
entries
- 得到所有的键值对
文件上传
流程
- 客户端将文件数据发送给服务器
- 服务端保存上传的文件数据到服务器端
- 服务器响应客户端一个文件访问地址
// 测试地址
http://101.132.72.36:5100/api/upload
// 键的名称(表单域名称)
imagefile
// 使用 Html5提供的 FormData 这个构造函数构建表单
-
请求方式 : POST
-
请求格式为表单: multipart/form-data
- 如果使用fetch,当body指定的数据格式为formData的时候,会自动设置请求头
-
请求体中必须包含一个键值对,键的名称是服务器要求的名称,值就是文件数据
- html5中,js 无法随意的获取文件,但是可以获取input组件中用户选择的文件
async function upload() {
const inp = document.getElementById("id");
if (!inp?.files?.length) {
alert("请选择文件");
return;
}
const formData = new FormData();
formData.append("imagefile", inp.files[0]);
const url = "http://101.132.72.36:5100/api/upload";
const resp = await fetch(url, {
method: "POST",
body: formData,
});
const res = await resp.json();
return res.path;
}
document.querySelector("button").onclick = async function () {
const res = await upload();
const img = document.getElementById("avater");
img.src = res;
};
迭代器
-
什么是迭代
- 从一个数据集合汇总按照一定的顺序,不断取出数据的过程
-
迭代和遍历有什么区别
-
迭代强调的是依次取数据,并不保证取多少,也不保证把所有的数据取完
- 不知道数据源的具体情况,不需要管 目标数据, 只需要从迭代器里面一直拿
-
遍历强调的是要把整个数据依次全部取出
- 清楚数据源的情况,需要一直操作 需要遍历的数据
-
-
迭代器
- 对迭代过程的封装,在不同的语言中又不同的表现形式,通常为对象
- 也就是说把 拿数据的过程 进行封装,我们操作这个封装后的对象就行了
-
迭代模式
-
是一种设计模式,用于统一迭代过程,并规范了迭代器规格
- 迭代器应该具有得到下一个数据的能力
- 迭代器应该具有判断是否还有后续数据的能力
-
js 中的迭代器
js规定,如果一个对象具有 next 方法,并且该方法返回一个对象,该对象的格式如下,则认为该对象是一个迭代器
const obj = {
next() {
return {
value:"",
done:false
}
}
}
-
迭代器对象
-
next方法 : 用于得到下一个数据
-
返回的对象
- value : 下一个数据的值
- done : boolean 标记是否迭代完成(取不到数据就是true)
-
const arr = [1, 2, 3, 4, 5]; // 用于迭代数组arr // 封装取数据的过程 形成迭代对象 const iterator = { i: -1, // 当前数组下标 next() { this.i++; return { value: arr[this.i], done: this.i >= arr.length, }; }, }; // 让迭代器不断取出下一个数据,直到没有数据为止 let data = iterator.next(); // 没有迭代完成 再次取数据 while (!data.done) { console.log(data.value); data = iterator.next(); }
-
-
迭代器函数
- 一个返回迭起器函数的函数
-
/** * 迭代器创建函数 iterator creator * @param {*} arr */ function createIterator(arr) { let i = 0; // 当前数组下标 return { next() { const result = { value: arr[i], done: i >= arr.length, }; i++; return result; }, }; }
结合链表的数据结构就很好理解,迭代就是从开始一直取,因为我们不知道链表的尾部在哪里,所有通过迭代器一直从链表里面拿数据,直到 done 标记为true,表示已经到了链表尾部,迭代结束
// 迭代器实现斐波拉契数列
const createIterator = () => {
let prv1 = 1,
prv2 = 1,
n = 1;
return {
next() {
let value;
if (n <= 2) {
value = 1;
} else {
value = prv1 + prv2;
prv1 = prv2;
prv2 = value;
}
const result = {
value,
done: false,
};
n++;
return result;
},
};
};
可迭代协议 (可迭代对象)
-
es6规定,如果一个对象具有知名符号属性
Symbol.iterato -
并且该属性值是一个迭代器创建函数,则该对象是可迭代的 ( iterable )
-
在es6中数组就是可迭代对象, 数组的隐式原型上 有 Symbol.iterator
-
const arr = [1, 2, 3, 4]; const iterator = arr[Symbol.iterator](); console.log(iterator.next());
-
-
document.querySelectorAll 也可以使用迭代器
-
// 可迭代对象
const obj = {
[Symbol.iterator]() {
return {
next() {
return {
value: "111",
done: false,
};
},
};
},
};
for-of 循环
- for-of( 语法糖) 循环专门用于遍历可迭代对象
- 在实现上就是调用迭代对象的 Symbol.iterator 拿到迭代器函数,创建迭代器,执行迭代器直到done 为true
const arr = [1, 2, 3, 4];
const iterator = arr[Symbol.iterator]();
let data = iterator.next();
while (!data.done) {
console.log(data.value);
data = iterator.next();
}
// 使用for-of处理 效果跟上边写法一样
// 要求目标对象 一定要是可迭代对象
for (const data of arr) {
console.log(data);
}
使用for-of 迭代普通对象
const obj = {
a: 1,
b: 2,
[Symbol.iterator]() {
const keys = Object.keys(this);
let i = 0;
return {
next: () => {
const result = {
value: {
propName: keys[i],
value: this[keys[i]],
},
done: i >= keys.length,
};
i++;
return result;
},
};
},
};
for (const data of obj) {
console.log(data);
}
展开运算符与可迭代对象
展开运算符可以作用于可迭代对象,实现方式就是使用迭代器一个个取值然后放入数组
-
将可迭代对象转换为数组
-
// 这个obj 是上面我们自己写的可迭代对象 // 如果不是迭代对象,是不能在数组里面展开,会直接报错 const arr = [...obj] // arr => [{propName:'a',value:1},{propName:'b',value:2}]
-
-
可以在函数传值使用展开运算符操作迭代器
-
function test(a,b) { console.log(a,b) // a => {propName:'a',value:1} // b => {propName:'b',value:2} } // 如果不是迭代器就会报错 // 数组之所以可以这么使用 是因为数组就是迭代器对象 test(...obj)
-
生成器 ( Generator )
目的: 简化创建迭代器和迭代器函数的创建流程
-
生成器是一个通过构造函数 Generator 创建的对象(没有对外暴露)
-
生成器既是一个迭代器,同时又是一个可迭代对象
-
生成器的创建必须使用生成器函数 ( Generator Function )
- 在函数名前面使用 * 标识该函数为生成器函数
-
// 这是一个生成器函数,该函数一定返回一个生成器 // * 不能和async 关键字一起使用 function *method () {} const iterator = method(); console.log(iterator.next()); //标记对象的方法为生成器函数 { * todo(){ } }
生成器函数
-
调用生成器函数,在函数体内部的代码是不会运行,调用函数只是返回一个生成器
-
函数体内部是用于给生成器的每次迭代提供数据(没有调用next是不会运行)
-
每次调用生成器的next方法,将导致生成器函数运行到下一个 yield 关键字位置
- 调用一次next 会运行到下一个yield 然后暂停(有中间代码也会执行),等待下一次next执行
- yield 关键字只能在生成器函数内部使用,表达 ' 产生 ' 一个迭代数据
- yield 后面的的数据 就是返回给next 的数据
-
迭代器函数体执行完毕 此时 迭代器属性 done 为true
function* test() {
console.log("第1次执行");
yield 1;
console.log("第2次执行");
yield 2;
console.log("第三次执行");
}
const iterator = test();
console.log(iterator.next()); // 执行开始到yield 1之间的代码,然后返回1,等待
console.log(iterator.next()); // 执行yield 1到yield2直接的代码,然后返回2,等待
console.log(iterator.next());// 由于没有yield了 执行yield 2之后的代码,然后返回undefined,结束
// 使用生成器加for-of 构建数组的迭代器函数
const arr = [1, 2, 3, 4];
function* iterator(arr) {
for (const data of arr) {
yield data;
}
}
const iterator2 = iterator(arr);
// 使用生成器器改造斐波拉契数列迭代器函数
function* iterator() {
let prv1 = 1,
prv2 = 1,
n = 1;
while (true) {
if (n <= 2) {
yield 1;
} else {
let newValue = prv1 + prv2;
yield newValue;
// 这里会在下一次yield 之前调用
prv1 = prv2;
prv2 = newValue;
}
n++;
}
}
生成器需要注意的细节
-
生成器函数可以有返回值,返回值出现在第一次 done 为true时的value属性中
- 只有第一次时候会使用这个值,后续还是undefined
-
function *todo() { yield 1 yield 2 return 10 } const handle = todo() handle.next() // 1 handle.next() // 2 handle.next() // 10 handle.next() // undefined
-
调用生成器的next方法时,可以传递参数,传递的参数会交给yield表达式的返回值
- 第一次调用next方法时,传参没有意义
- 如果调用next 没有传参,赋值运算结果就是undefined
-
function *todo() { // 第一次调用传参触发只是触发yield 1 // 第二次调用才会触发 info = 的赋值操作 由于传入了参数 这个info 接受的next的入参 // info = 2 这里如果没有传参拿到就是 undefined // 然后触发next 返回值就是 2 + 5 let info = yield 1 yield 2 + info } const handler = todo() handler.next() // 1 handler.next(5) // 7
生成器api和属性
-
return 方法,调用该方法可以提前结束生成器函数,从而提前让让整个迭代过程结束
- 可以传入参数,参数就是第一次done的value值
- 调用之后 后续触发的next 都是完成状态,value 为undefined
-
const handler = todo() handler.next() handler.return()
-
throw 方法,该方法可以在生成器中产生一个错误
- 调用该方法后,后续的next不会继续执行
-
function* todo() { yield 1; yield 2; yield 3; yield 4; } const handler = todo(); handler.next(); handler.throw(new Error("错误代码")); handler.next();
-
在生成器函数内部,可以调用其他生成器函数,但是要注意加上*号
-
function* helper() { yield 'a' yield 'b' } function* todo() { // 调用其它生成器函数 本质上可以看做吧对应函数内部代码复制过来了 yield* helper(); yield 2; }
-
生成器控制异步调用
function* task() {
const d = yield 1;
console.log(d);
const resp = yield fetch(
"https://66d7d20037b1cadd80525d16.mockapi.io/api/local/getApiLocal"
);
const result = yield resp.json();
console.log(result);
}
run(task);
function run(generatorFunc) {
const generator = generatorFunc();
let result = generator.next();
handleResult();
function handleResult() {
if (result.done) {
return;
}
/**
迭代没有完成的情况
1.迭代的数据是一个Promise
2.迭代的数据是其他数据
*/
//判断是promise
if (typeof result.value.then === "function") {
// 需要等待promise 完成后进行下一次迭代
result.value.then(
(res) => {
result = generator.next(res);
handleResult();
},
(err) => {
result = generator.throw(err);
handleResult();
}
);
} else {
result = generator.next(result.value);
handleResult();
}
}
}
Set 集合
- 用于存放不重复的数据
const sets = new Set() // 创建空的set 集合
// 使用可迭代对象创建 set 集合
// 使用迭代器挨个取数据放入集合
const set2 = new Set(iterable)
// 如果出现重复的数据会自动去除
const set3 = new Set([1,2,3,4,5,1,2])
// set3 => 1,2,3,4,5
// 字符串会被转为字符串对象,字符串对象是可迭代对象
// 可以实现字符串去重
const set4 = new Set('cscscsscscs')
// set4 => cs
-
对set进行后续操作(实例方法)
-
add(数据), 添加数据到set集合末尾,如果数据已经存在则不进行操作- set 使用Object.is 判断两个数据是否相同,但是针对 +0 和 -0 认为是相等的
-
sets.add(1) // 这一句无效 sets.add(1) sets.add(+0) // 认为相同 sets.add(-0)
-
has(数据), 判断set中是否存在对应的数据-
sets.has(-0) //true
-
-
delete(数据),删除指定数据,返回是否删除成功-
sets.delete(-0) //true
-
-
clear(), 清空整个set集合 -
size属性, 获取 set 集合的元素数量
-
-
和数组进行相互转换
-
set 本身也是可迭代对象,每次迭代的结果就是每一项的值
-
const s1 = new Set([1,2,3,4]) const arr = [...s1]
-
-
实现数组去重
-
const arr = [...new Set([1,2,3,4,1,2,3])]
-
-
字符串去重
-
const arr = [...new Set('dsadasdadada')].join('')
-
-
-
如何遍历
-
使用 for-of 循环
-
const s1 = new Set([1,2,3,4]) for(const props of s1){}
-
-
使用实例方法 forEach
- set 集合中不存在下标,index 和 item 一致,均表示set的每一项
-
const s1 = new Set([1,2,3,4]) s1.forEach((item,index,s) => {})
-
const arr1 = [2, 52, 23, 55, 77, 55, 44];
const arr2 = [33, 53, 98, 55, 77, 22, 44];
// 并集
console.log([...new Set([...arr1, ...arr2])]);
// 交集
const s1 = new Set(arr1);
const j = arr2.filter((el) => s1.has(el));
console.log(j);
// 差集
console.log(
[...new Set([...arr1, ...arr2])].filter(
(el) =>
(arr1.indexOf(el) >= 0 && arr2.indexOf(el) < 0) ||
(arr1.indexOf(el) < 0 && arr2.indexOf(el) >= 0)
)
);
手写Set
class MySet {
constructor(iterator = []) {
//验证是否是可迭代对象
if (typeof iterator[Symbol.iterator] !== "function") {
throw new TypeError(`提供的${iterator}不是一个可迭代对象`);
}
this._datas = [];
for (const item of iterator) {
this.add(item);
}
}
add(data) {
if (!this.has(data)) {
this._datas.push(data);
}
}
has(data) {
for (const item of this._datas) {
if (this.isEqual(data, item)) {
return true;
}
}
return false;
}
delete(data) {
for (let i = 0; i < this._datas.length; i++) {
const element = this._datas[i];
if (this.isEqual(data, element)) {
this._datas.splice(i, 1);
return true;
}
}
return false;
}
size() {
return this._datas.length;
}
clear() {
this._datas = [];
}
//判断数据是否相等
isEqual(data1, data2) {
if (data1 == 0 && data2 == 0) {
return true;
}
return Object.is(data1, data2);
}
*[Symbol.iterator]() {
for (const element of this._datas) {
yield element;
}
}
forEach(callback) {
for (const el of this._datas) {
callback(el, el, this._datas);
}
}
}
const s1 = new MySet([]);
s1.add(33);
for (const element of s1) {
console.log(element);
}
s1.forEach((el) => {
console.log(el);
});
Map 集合
键值对( key value )数据集合的特点 -- 键不可重复
-
map集合专门用于存储多个键值对数据
- 在map出现之前我们使用对象来存储键值对,键是属性名,值是属性值
使用对象存储有以下问题
- 键名只能是字符串
- 获取数据的数量不方便
- 属性名容易跟原型上的名称冲突
创建map
-
使用可迭代对象创建map时,需要保证每一项还是可迭代对象,并且长度为2
- [['a',{a:1}],['c',1]]
new Map() // 创建空的map
// 创建一个具有初始内容的map,初始内容来自于可迭代对象每一次迭代的结果,
// 但是它要求每一次迭代的结果必须是一个长度为2的数组,数组第一项表示键,数组第二项表示值
new Map(iterable)
// 取第一项然后迭代这个内容,拿到a 做为键,拿到{a:1}作为值,形成映射
// a => {a:1} c => 1
new Map([['a',{a:1}],['c',1]])
进行后续操作
-
size,只读属性,获取当前map的键的数量 -
set(键,值),设置一个键值对,键和值可以是任何属性- 如果键不存在,则添加一项
- 如果键已存在,则修改对应的值
- 比较键的方式和set相同,都是使用Object.is
-
const map = new Map() map.set(3,4) map.set({a:1},'123')
-
get(键),根据一个键得到对应的值 -
`has(键),判断某个键是否存在 -
`delete(键),删除指定的键 -
clear,清空map
map转数组(和set一样)
const map = new Map([['a',1],['b',2]]);
const arr = [...map] // 可迭代对象直接转数组
// arr => [ [ 'a', 1 ], [ 'b', 2 ] ]
遍历
-
for-of 循环,每次得到都是长度为2 的数组
-
for (const [key, value] of map) { console.log(key, value); }
-
-
forEach ,通过回调遍历
- 参数1 每一项的值
- 参数2 每一项的键
- 参数3 map本身####
手写map
class MyMap {
constructor(iterable = []) {
//验证是否是可迭代对象
if (typeof iterable[Symbol.iterator] !== "function") {
throw new TypeError(`你提供的${iterable}不是一个可迭代对象`);
}
this._datas = [];
for (const item of iterable) {
// item也是需要时可迭代对象
if (typeof item[Symbol.iterator] !== "function") {
throw new TypeError(`你提供的item-${item}不是一个可迭代对象`);
}
const iterator = item[Symbol.iterator]();
const key = iterator.next().value;
const value = iterator.next().value;
this.set(key, value);
}
}
set(key, value) {
const obj = this._getObj(key);
if (obj) {
// 修改
obj.value = value;
} else {
this._datas.push({
key,
value,
});
}
}
/**
* 根据key值从内部数组找到对应的数组项
* @param {*} key
*/
_getObj(key) {
for (const item of this._datas) {
if (this.isEqual(item?.key, key)) {
return item;
}
}
}
has(key) {
return !!this._getObj(key);
}
get(key) {
const item = this._getObj(key);
if (item) {
return item.value;
}
return undefined;
}
get size() {
return this._datas.length;
}
clear() {
this._datas.length = 0;
}
delete(key) {
for (let i = 0; i < this._datas.length; i++) {
const element = this._datas[i];
if (this.isEqual(element.key, key)) {
this._datas.splice(i, 1);
return true;
}
}
return false;
}
*[Symbol.iterator]() {
for (const item of this._datas) {
yield [item.key, item.value];
}
}
forEach(callback) {
for (const element of this._datas) {
callback(element.value, element.key, this);
}
}
//判断数据是否相等
isEqual(data1, data2) {
if (data1 == 0 && data2 == 0) {
return true;
}
return Object.is(data1, data2);
}
}
const mp = new MyMap([
["a", 1],
["b", 2],
]);
mp.set("name", "cccc");
console.log(mp);
mp.delete("name");
console.log(mp.has("name"));
console.log(mp.get("name"));
console.log([...mp]);
mp.forEach((value, key) => {
console.log(value, key);
});
WeakSet 和 WeakMap
weakSet
- 目的是用于观察对象垃圾回收
-
内部存储的地址不会影响垃圾回收
-
set会影响垃圾回收
-
let obj = { name: 1 }; const set = new Set(); set.add(obj); obj = null; //释放 console.log(set); // { { name: 1 } } 释放后没有被回收 因为还在set里面能找到
-
-
使用weekSet之后,就算内部存在引用也会回收
-
let obj = { name: 1 }; const set = new WeakSet(); set.add(obj); obj = null; //释放 console.log(set); // WeakSet { No properties } 释放后被回收
-
-
使用weekSet调试一些该被回收但没有被回收的场景
-
let obj = { name: 1 }; let obj2 = obj const set = new WeakSet(); set.add(obj); obj = null console.log(set); //WeakSet [[Entries]]
-
-
-
只能添加对象
-
不能遍历,不是可迭代对象,没有size属性,没有forEach方法
WeakMap
- 类似于map集合,不同的是它的键存储地址不会影响垃圾回收
- 它的键只能是对象
- 不能遍历,不是可迭代对象,没有size属性,没有forEach方法
let obj = { name: 1 };
const map = new WeakMap();
map.set(obj, "222");
obj = null;
console.log(map);
属性描述符(旧知识)
Property Descriptor(属性描述符) 是一个普通对象,用于描述一个属性的相关信息
通过Object.getOwnPropertyDescriptor(对象,属性名) 可以得到一个对象的某个属性的属性描述符
Object.getOwnPropertyDescriptors(对象) 得到对象的所有属性描述符
- value: 属性值
- configurable: 是否允许修改该属性的属性描述符
- enumerable: 该属性是否可以被枚举,(for-in ,Object.values等循环可以得到该属性)
- writable: 该属性是否可以重新赋值
let obj = { name: 1 };
const config = Object.getOwnPropertyDescriptor(obj, "name");
// { value: 1, writable: true, enumerable: true, configurable: true }
如果需要为某个对象添加属性时 或 修改属性时,可以使用 Object.defineProperty
Object.defineProperty(obj,name,{
value:3,
configurable:false, // 不能再次使用 Object.defineProperty
enumerable:false // for-in 循环会拿不到该属性
writable:false // value 不能被重新修改
})
使用 Object.defineProperties 批量操作属性操作符
Object.defineProperties(obj,{
a:{
value: 3,
configurable: false, // 不能再次使用 Object.defineProperty
enumerable: false, // for-in 循环会拿不到该属性
}
})
存取器属性
-
属性描述符中,如果配置了 get 和 set 中的任何一个,则该属性不再是一个普通属性,而变成了存取器属性
-
get 和 set 配置均为函数
- 如果一个属性是存取器属性,则读取该属性时,会允许get方法,将get方法得到的返回值做为属性值
- 如果给该属性赋值,则会运行set方法
- 没有指定的内存空间,现在相当于是两个方法,每次调用属性实际上就是执行对应的方法
-
存取器属性最大的意义就是可以控制属性的读取和赋值
-
不能和value 和 writable 共存(配置value 会开辟内存空间),一起配置会报错
Object.defineProperty(obj, "name", {
get() {
console.log("执行了a的get函数");
// 不能是直接返回 obj.name 会出现自己调自己
return obj._name
},
set(val) {
console.log("执行了a的set函数", val);
obj._name = val
},
});
console.log(obj.name); // undefined 因为get函数没有返回
Reflect
-
Reflect 是一个内置的js对象,他提供了一系列的方法,可以让开发者通过调用这些方法访问一些js底层功能
- 由于它类似于其他语言的 反射,因此取名叫 Reflect
-
他可以做什么?
- 属性的赋值和取值
- 调用普通函数
- 调用构造函数
- 判断属性是否存在对象中
- 等等
-
已经存在的功能为什么还要使用Reflect实现一次?
-
减少魔法,让代码更加纯粹
-
将一些基于底层的实现的能力提取成一个api,高度聚合到一个对象里面
- 基于函数式编程思想,所有的能力都是用函数实现
-
-
提供的api( 举例一部分 )
-
Reflect.set( target,propertyKey ,value)
- 设置对象的 propertyKey 为 value,等同于给对象赋值
-
Reflect.get( target,propertyKey )
- 获取对象的属性值
-
Reflect.apply ( target,thisArgument,argumentList )
- 调用目标函数,返回目标函数的结果
-
Reflect.deleteProperty (target,propertyKey )
- 认为 delete 关键字是魔法,使用api删除目标属性
-
Reflect.defineProperty( target,propertyKey.attributes )
- 可以理解为底层就是调用 Object.defineProperty , 但是做了优化配置错误不会报错,而是返回false
-
Reflect.construct( target,argumentsList )
- 认为 new 关键字是魔法,通过api进行实例化,返回实例对象
-
const obj = Reflect.construct(target, argumentsList); // 返回实例对象
-
Reflect.has( target,propertyKey)
- 认为 in 关键字是是魔法,通过api判断属性是否在对象内,结果是boolean 值
-
Reflect.ownKeys ( target )
Reflect.ownKeys方法返回一个由目标对象自身的属性键组成的数组。它的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))。- 一般来说 symbol 是不能被枚举的,这个api调用会返回自定义的 symbol 属性
-
Proxy
代理: 提供了修改底层实现的方式(赋值,读取属性,构造实现)
// target 目标对象
// handler 目标对象,其中可以重写底层实现(Reflect实现的api都可以重写)
// 返回代理对象
new Proxy(target,handler)
- 使用代理隔离原始对象,不再操作原始对象,后续操作都是操作代理对象
- 重写代理对象的方法之后,一旦触发代理的事件我们可以控制它做一些事情
- 不占用内存空间
const user = {
name: "22",
};
const proxy = new Proxy(user, {
set(target, propertyKey, value) {
Reflect.set(target, propertyKey, value);
},
// 重写has 不会影响Reflect.has
has(target, propertyKey) {
return false;
},
});
proxy.name = "2";
console.log(proxy.name); // 2
console.log(user.name); // 2
console.log("name" in proxy); // false
Proxy应用-观察者模式
有一个对象,是观察者,它用于观察另一个对象的属性值变化,当属性值变化后会收到一个通知,可能会做一些事
// 旧版本实现,实现了两个对象,额外占用内存空间
// 无法对新增属性进行观察
function observer(target) {
const div = document.getElementById("box");
const ob = {};
for (const element of Object.keys(target)) {
Object.defineProperty(ob, element, {
enumerable: true,
get() {
return target[element];
},
set(val) {
target[element] = val;
render();
},
});
}
render();
function render() {
let html = "";
for (const element of Object.keys(target)) {
html += `<p><span>${element}</span><span>${target[element]}</span></p>`;
}
div.innerHTML = html;
}
return ob;
}
const ob = observer({
a: 1,
b: 2,
});
// 新版实现 能够捕获到新增属性,因为代理的是整个目标而且能够修改底层
function observer(target) {
const div = document.getElementById("box");
const ob = new Proxy(target, {
set(target, prop, value) {
Reflect.set(target, prop, value);
render();
},
get(target, prop) {
return Reflect.get(target, prop);
},
});
for (const element of Object.keys(target)) {
}
render();
function render() {
let html = "";
for (const element of Object.keys(target)) {
html += `<p><span>${element}</span><span>${target[element]}</span></p>`;
}
div.innerHTML = html;
}
return ob;
}
const ob = observer({
a: 1,
b: 2,
});
Proxy应用-偷懒的构造函数
class User {}
function ConstructorProxy(Class, ...propNames) {
return new Proxy(Class, {
construct(target, argumentsList) {
// 通过原来的对象创建一个新的实例
const obj = Reflect.construct(target, argumentsList);
// 往实例加属性
propNames.forEach((name, index) => {
Reflect.set(obj, name, argumentsList[index]);
});
return obj;
},
});
}
const UserProxy = ConstructorProxy(User, "name", "age");
const obj = new UserProxy("test", 18);
console.log(obj);
Proxy应用-可以验证的函数参数
function sum(a, b) {
return a + b;
}
function validatorFunction(func, ...types) {
return new Proxy(func, {
apply(target, thisArgument, argumentsList) {
types.forEach((t, i) => {
const arg = argumentsList[i];
if (typeof arg != t) {
throw new Error(`第${i + 1}个参数${argumentsList[i]}不满足类型${t}`);
}
});
return Reflect.apply(target, thisArgument, argumentsList);
},
});
}
const sumProxy = validatorFunction(sum, "number", "number");
console.log(sumProxy("1", "2"));
新增的数组Api
静态方法
-
Array.of( ...args ) 使用指定的数组项创建一个新数组
- 消除
new Array带来的歧义,创建指定长度的数组还是要 new Array -
Array.of(1, 2, 3, 4); // [1,2,3,4]
- 消除
-
Array.from( args ) 通过给定的类数组 或 可迭代对象 创建一个新的数组
-
const divs = document.querySelectorAll("div"); Array.from(divs);
-
实例方法
-
find( callback ) 用于查找满足条件的第一个元素
- callback 返回true 表示满足要求,终止循环
- false就会继续找,直到结束
-
const arr = [{ name: 1 }, { name: 2 }, { name: 3 }]; console.log(arr.find((el) => el.name == 2));
-
findIndex(callback) 用于查找满足条件的第一个元素的下标
- 跟 find 类似,只不过返回的是下标
-
console.log(arr.findIndex((el) => el.name == 2));
-
fill ( data ) 用指定的数组填充满数组所有的内容
-
console.log(new Array(100).fill("abc"));
-
-
copyWithin ( target ,start ?, end ?) 在数组内部完成赋值
- target 表示从下标位置开始改变
- start 表示从下标位置开始复制( 数据来源 )
- end 表示数据来源结束位置
-
includes( data ) 判断数组中是否包含某个值,使用 Object.is 匹配
-
const arr = [1, 2, 3, 4, 5, 6, 7, 8]; console.log(arr.includes(5));
-
类型化数组
数字存储的前置知识
-
计算机必须使用固定的位数来存储数字,无论存储的数字是大是小,在内存中占用的空间是固定的
-
n 位的无符号整数能表示的数字是 2 ^ n 个,取值范围是 0 ~ 2^n - 1
-
n 位的有符号整数能表示的数字是 2 ^ n 个,取值范围是 -2 ^( n - 1 ) ~ 2 ^ (n - 1) - 1
-
浮点型表示法可以用于表示整数和小数,目前分为两种标准
- 32位浮点数, 又称为单精度浮点数,它用1位表示符号,8位表示阶码,23位表示尾数
- 64位浮点数,又称为双精度浮点数,它用1位表示符号,11位表示阶码,52位表示尾数
-
js中所有数字,均使用双精度浮点数保存