JS基础
数据类型-介绍
原始数据类型:
- boolean
- null
- undefined
- number
- string
- symbol
- bigint
引用数据类型:
对象Object(包含普通对象-Object,数组对象-Array,正则对象-RegExp,日期对象-Date,数学函数-Math,函数对象-Function)
symbol
返回symbol类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的symbol注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:"new Symbol()"。
每个从Symbol()返回的symbol值都是唯一的。一个symbol值能作为对象属性的标识符;这是该数据类型仅有的目的
bigInt
什么是BigInt?
BigInt是一种新的数据类型,用于当整数值大于Number数据类型支持的范围时。这种数据类型允许我们安全地对
大整数执行算术操作,表示高分辨率的时间戳,使用大整数id,等等,而不需要使用库。
应用-大整数id
axios 为了方便我们使用数据,它会在内部使用
JSON.parse()把后端返回的数据转为 JavaScript 对象。但是,超出安全整数范围的 id 无法精确表示。可以通过
json-bigint插件在axios中的transformResponse来自定义响应数据的格式
import axios from 'axios'
import jsonBig from 'json-bigint'
var json = '{ "value" : 9223372036854775807, "v2": 123 }'
console.log(jsonBig.parse(json))
const request = axios.create({
baseURL: 'http://ttapi.research.itcast.cn/', // 接口基础路径
// transformResponse 允许自定义原始的响应数据(字符串)
transformResponse: [function (data) {
try {
// 如果转换成功则返回转换的数据结果
return jsonBig.parse(data)
} catch (err) {
// 如果转换失败,则包装为统一数据格式并返回
return {
data
}
}
}]
})
export default request
数据类型-判断
typeof 判断原始数据类型
对于原始类型来说,除了null都可以调用typeof显示正确的类型。
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null() // 'object'
但对于引用数据类型,除了函数之外,都会显示"object"。
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
instanceof 判断基本数据类型
instanceof的原理是基于原型链的查询,只要处于原型链中,判断永远为true
const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true
var str1 = 'hello world'
str1 instanceof String // false
var str2 = new String('hello world')
str2 instanceof String // true
手动实现一下instanceof的功能?
核心: 原型链的向上查找。
Object.getPrototypeOf() 方法返回指定对象的原型(内部[[Prototype]]属性的值)
const prototype1 = {}
const object1 = Object.create(prototype1)
console.log(Object.getPrototypeOf(object1) === prototype1)
// 判断left是不是right数据类型
function myInstanceof(left, right) {
// 先判断是不是 简单数据 类型 和 null,直接返回false
if (typeof left !== 'object' || left === null) return false
// 拿到left的对象 原型
let proto = Object.getPrototypeOf(left)
// 往原型链上找
while (true) {
// a.表示一直没找到
if (proto === null) return false
// b.找到了,返回true
if (proto === right.prototype) return true
// c.如果没找到,再往上一层找
proto = Object.getPrototypeOf(proto)
}
}
const left = new Date()
console.log(myInstanceof(left, Date)) //true
Object.is和===的区别?
Object在严格等于的基础上修复了一些特殊情况下的失误,具体来说就是+0和-0,NaN和NaN 。 源码如下:
function is(x, y) {
// +0与-0的比较,+0===-0结果为true,是不对的
if (x === y) {
//运行到1/x === 1/y的时候x和y都为0,但是1/+0 = +Infinity, 1/-0 = -Infinity, 是不一样的
return x !== 0 || y !== 0 || 1 / x === 1 / y;
} else {
//NaN===NaN是false,这是不对的,我们在这里做一个拦截,x !== x,那么一定是 NaN, y 同理
//两个都是NaN的时候返回true
return x !== x && y !== y;
}
数据类型-转换
JS中,类型转换只有三种:
- 转换成数字
- 转换成布尔值
- 转换成字符串
隐式转换
注意:
数组转换为Number,首先会被转为原始类型,也就是ToPrimitive,然后在根据转换后的原始类型按照上面的规则处理
1.1 ToString
这里所说的
ToString可不是对象的toString方法,而是指其他类型的值转换为字符串类型的操作。
这里我们讨论null、undefined、布尔型、数字、数组、普通对象转换为字符串的规则。
- null:转为
"null" - undefined:转为
"undefined" - 布尔类型:
true和false分别被转为"true"和"false" - 数字类型:转为数字的字符串形式,如
10转为"10",1e21转为"1e+21" - 数组:转为字符串是将所有元素按照","连接起来,相当于调用数组的
Array.prototype.join()方法,如[1, 2, 3]转为"1,2,3",空数组[]转为空字符串,数组中的null或undefined,会被当做空字符串处理 - 普通对象:转为字符串相当于直接使用
Object.prototype.toString(),返回"[object Object]"
String(null) // 'null'
String(undefined) // 'undefined'
String(true) // 'true'
String(10) // '10'
String(1e21) // '1e+21'
String([1,2,3]) // '1,2,3'
String([]) // ''
String([null]) // ''
String([1, undefined, 3]) // '1,,3'
String({}) // '[object Objecr]'
对象的toString方法,满足ToString操作的规则。
注意:上面所说的规则是在默认的情况下,如果修改默认的
toString()方法,会导致不同的结果
1.2 ToNumber
ToNumber指其他类型转换为数字类型的操作。
- null: 转为
0 - undefined:转为
NaN - 字符串:如果是纯数字形式,则转为对应的数字,空字符转为
0, 否则一律按转换失败处理,转为NaN - 布尔型:
true和false被转为1和0 - 数组:数组首先会被转为原始类型,也就是
ToPrimitive,然后在根据转换后的原始类型按照上面的规则处理,关于ToPrimitive,会在下文中讲到 - 对象:同数组的处理
Number(null) // 0
Number(undefined) // NaN
Number('10') // 10
Number('10a') // NaN
Number('') // 0
Number(true) // 1
Number(false) // 0
Number([]) // 0
Number(['1']) // 1
Number({}) // NaN
1.3 ToBoolean
ToBoolean指其他类型转换为布尔类型的操作。
js中的假值只有false、null、undefined、空字符、0和NaN,其它值转为布尔型都为true。
Boolean(null) // false
Boolean(undefined) // false
Boolean('') // flase
Boolean(NaN) // flase
Boolean(0) // flase
Boolean([]) // true
Boolean({}) // true
Boolean(Infinity) // true
ToPrimitive
ToPrimitive指对象类型类型(如:对象、数组)转换为原始类型的操作。
对象转原始类型,会调用内置的[ToPrimitive]函数,对于该函数而言,其逻辑如下:
注意:Date对象会优先尝试toString()方法来实现转换
var obj = {
value: 3,
valueOf() {
return 4;
},
toString() {
return '5'
},
[Symbol.toPrimitive]() {
return 6
}
}
console.log(obj + 1); // 输出7
// [].valueOf()为数组本身,[].toString()为'', Number('')为0
Number([]) // 0
// ['10'].valueOf()为数组本身,['10'].toString()为'10', Number('10')为10
Number(['10']) //10
// obj1.valueOf()为100
const obj1 = {
valueOf() {
return 100
},
toString() {
return 101
}
}
Number(obj1) // 100
// obj1.toString()为{},不为原始数据类型,抛出异常
const obj3 = {
toString() {
return {}
}
}
Number(obj3) // TypeError
== 和 ===有什么区别?
===叫做严格相等,是指:左右两边不仅值要相等,类型也要相等,例如'1'===1的结果是false,因为一边是string,另一边是number。
==不像===那样严格,对于一般情况,只要值相等,就返回true,但==还涉及一些类型转换,它的转换规则如下:
-
两边的类型是否相同,相同的话就比较值的大小,例如1==2,返回false
-
判断的是否是null和undefined,是的话就返回true
-
判断的类型是否是String和Number,是的话,把String类型转换成Number,再进行比较
-
判断其中一方是否是Boolean,是的话就把Boolean转换成Number,再进行比较
-
判断Boolean和String,都会就将其转换成Number,再进行比较
-
如果其中一方为Object,且另一方为String、Number或者Symbol,会将Object转换成字符串,再进行比较
console.log({a: 1} == true);//false
console.log({a: 1} == "[object Object]");//true
null、undefined和其他类型的比较
null和undefined宽松相等的结果为true,这一点大家都知道
其次,null和undefined都是假值,那么
null == false // false
undefined == false // false
为什么呢? 首先,false转为0,然后呢? 没有然后了,ECMAScript规范中规定null和undefined之间互相宽松相等(==),并且也与其自身相等,但和其他所有的值都不宽松相等(==)。
如何让if(a == 1 && a == 2)条件成立?
let a = {
value: 1,
valueOf: function () {
return this.value++
}
}
console.log(a == 1 && a == 2)
生成器Generator
迭代器Iterator
JS核心
闭包
闭包是指有权访问另外一个函数作用域中的变量的函数
当一个函数被创建并传递或从另一个函数返回时,它会携带一个背包。背包中是函数声明时作用域内的所有变量。
闭包产生的原因?
在ES5中只存在两种作用域————全局作用域和函数作用域,当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链,值得注意的是,每一个子函数都会拷贝上级的作用域,形成一个作用域的链条。
闭包产生的本质就是,当前环境中存在指向父级作用域的引用
如何形成闭包
// 方式一
function f1() {
var a = 2
function f2() {
console.log(a);//2
}
return f2;
}
var x = f1();
x();
// 方式一
// 外面的变量`f3存在着父级作用域的引用`,因此产生了闭包
var f3;
function f1() {
var a = 2
f3 = function() {
console.log(a);
}
}
f1();
f3();
闭包的实际应用
- 返回一个函数。刚刚已经举例。
- 作为函数参数传递
var a = 1;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
// 这就是闭包
fn();
}
// 输出2,而不是1
foo();
- 在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。
以下的闭包保存的仅仅是window和当前作用域。
// 定时器
setTimeout(function timeHandler(){
console.log('111');
},100)
// 事件监听
$('#app').click(function(){
console.log('DOM Listener');
})
- IIFE(立即执行函数表达式)创建闭包, 保存了
全局作用域window和当前函数的作用域,因此可以全局的变量。
var a = 2;
(function IIFE(){
// 输出2
console.log(a);
})();
防抖节流
<input type="text" id="ipt" />
<div id="msg"></div>
<div>
<img src="" data-src="../pic/samuel-scrimshaw-361584-unsplash.jpg" alt="" />
</div>
... 很多个img
<div>
<img src="" data-src="../pic/samuel-scrimshaw-361584-unsplash.jpg" alt="" />
</div>
<script>
let ipt = document.querySelector('#ipt')
let msg = document.querySelector('#msg')
let images = document.querySelectorAll('img')
// 防抖
ipt.addEventListener('keyup', debounce(fn1, 500))
function fn1(e) {
msg.innerHTML = e.target.value
}
function debounce(callback, delay) {
let timer = null
return function () {
let context = this
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
callback.call(context, ...arguments)
}, delay)
}
}
window.addEventListener('scroll', throttle(lazyLoad, 1000))
// 首屏加载一次
lazyLoad()
// 懒加载
function lazyLoad() {
let viewHeight = document.documentElement.clientHeight //视口高度
let scrollTop = document.documentElement.scrollTop //滚动条卷去的高度
for (let i = 0; i < images.length; i++) {
// 如果图片出现在视口中
if (images[i].offsetTop < viewHeight + scrollTop) {
images[i].src = images[i].getAttribute('data-src')
}
}
}
// 节流
function throttle(callback, interval) {
let flag = true
return function () {
let context = this
if (!flag) return
flag = false
setTimeout(() => {
callback.call(context, ...arguments)
flag = true
}, interval)
}
}
// 防抖+节流
function deThrottle(callback, delay) {
let last = 0,
timer = null
return function () {
let context = this
let now = +new Date()
if (now - last < delay) {
clearTimeout(timer)
timer = setTimeout(() => {
callback.call(context, ...arguments)
last = now
}, delay)
} else {
// 这个时候表示时间到了,必须给响应
callback.call(context, ...arguments)
last = now
}
}
}
</script>
原型链
原型对象和构造函数有何关系?
在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会天生自带一个prototype属性,这个属性指向函数的原型对象。
当函数经过new调用时,这个函数就成为了构造函数,返回一个全新的实例对象,这个实例对象有一个__proto__属性,指向构造函数的原型对象。
能不能描述一下原型链?
每一个实例对象又有一个__proto__属性,指向的构造函数的原型对象,构造函数的原型对象也是一个对象,也有__proto__属性,这样一层一层往上找就形成了原型链。
JavaScript对象通过__proto__ 指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条, 即原型链。
成员的查找机制
- 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
- 如果没有就查找它的原型(也就是 __proto__指向的 prototype 原型对象)。
- 如果还没有就查找原型对象的原型(Object的原型对象)。
... 依此类推一直找到 Object 为止(null)。
// Object是内置对象,只能添加方法,是不能覆盖对象的
Object.prototype.sing1 = function() {
console.log('我会拍戏');
}
Object.prototype.uname1 = '你好啊';
function Star(uname, age) {
this.uname = uname;
this.age = age;
}
Star.prototype.sing = function() {
console.log('我会唱歌');
}
var ldh = new Star('刘德华', 18);
console.log(ldh.uname1);
console.log(ldh.__proto__);
// 1. 只要是对象就有__proto__ 原型, 指向原型对象
console.log(Star.prototype);
// 2.我们Star原型对象里面的__proto__原型指向的是 Object.prototype
console.log(Object.prototype.__proto__);
// 3. 我们Object.prototype原型对象里面的__proto__原型 指向为 null
console.log(Star.prototype.__proto__);
注意:
- 对象的
Object.prototype.hasOwnProperty()来检查对象自身中是否含有该属性 - 使用 in 检查对象中是否含有某个属性时,如果对象中没有但是原型链中有,也会返回 true
Object.getPrototypeOf()方法返回指定对象的原型(内部[[Prototype]]属性的值)
ES6 类继承
// 父类有加法方法
class Father {
constructor(x, y) {
this.x = x;
this.y = y;
}
sum() {
console.log(this.x + this.y);
}
}
// 子类继承父类加法方法 同时 扩展减法方法
class Son extends Father {
constructor(x, y, z) {
// 利用super 调用父类的构造函数
// super 必须在子类this之前调用
super(x, y);
this.z = z;
}
say() {
// console.log('我是儿子');
console.log(super.say() + '的儿子');
// super.say() 就是调用父类中的普通函数 say()
}
subtract() {
console.log(this.x - this.y - this.z);
}
}
var son = new Son(5, 3, 9);
son.subtract(); // -7
son.sum(); // 8
JS ES5实现继承
寄生组合继承
function Parent () {
this.name = 'parent5';
this.play = [1, 2, 3];
}
function Child() {
Parent.call(this);
this.type = 'child';
}
// 注意:子构造函数 的 原型对象上 的 方法 通过继承之后 会直接被移除掉
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
面向对象的设计一定是好的设计吗?
不一定。从继承的角度说,这一设计是存在巨大隐患的。
继承的最大问题在于:无法决定继承哪些属性,所有属性都得继承。
假如现在有不同品牌的车,每辆车都有drive、music、addOil这三个方法。
现在可以实现车的功能,并且以此去扩展不同的车。
但是问题来了,新能源汽车也是车,但是它并不需要addOil(加油)。
如果让新能源汽车的类继承Car的话,也是有问题的。也就是说加油这个方法,我现在是不需要的,但是由于继承的原因,也给到子类了。
这时,如果再创建一个父类,把加油的方法给去掉,也是有问题的,一方面父类是无法描述所有子类的细节情况的,为了不同的子类特性去增加不同的父类,代码势必会大量重复,另一方面一旦子类有所变动,父类也要进行相应的更新,代码的耦合性太高,维护性不好。
那如何来解决继承的诸多问题呢?
用组合,这也是当今编程语法发展的趋势,比如golang完全采用的是面向组合的设计方式。
顾名思义,面向组合就是先设计一系列零件,然后将这些零件进行拼装,来形成不同的实例或者类。
function drive(){
console.log("wuwuwu!");
}
function music(){
console.log("lalala!")
}
function addOil(){
console.log("哦哟!")
}
let car = compose(drive, music, addOil);
let newEnergyCar = compose(drive, music);
代码干净,复用性也很好。这就是面向组合的设计方式。
面向过程与面向对象对比
| 面向过程 | 面向对象 | |
|---|---|---|
| 优点 | 性能比面向对象高,适合跟硬件联系很紧密的东西,例如单片机就采用的面向过程编程。 | 易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护 |
| 缺点 | 不易维护、不易复用、不易扩展 | 性能比面向过程低 |
-
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了;
-
面向对象是把构成问题事务分解成各个对象,然后由对象之间分工与合作。建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
V8引擎的内存机制
基本数据类型用栈存储,引用数据类型用堆存储。
注意:
闭包变量是存在堆内存中的。
特点总结
-
栈:
- 存储基础数据类型
- 按值访问
- 存储的值大小固定
- 由系统自动分配内存空间
- 空间小,运行效率高
- 先进后出,后进先出
- 栈中的DOM,ajax,setTimeout会依次进入到队列中,当栈中代码执行完毕后,再将队列中的事件放到执行栈中依次执行。
- 微任务和宏任务
-
堆:
- 存储引用数据类型
- 按引用访问
- 存储的值大小不定,可动态调整
- 主要用来存放对象
- 空间大,但是运行效率相对较低
- 无序存储,可根据引用直接获取
为什么不全部用栈来保存呢?
-
首先,对于系统栈来说,它的功能除了保存变量之外,还有
创建并切换函数执行上下文的功能 -
如果采用栈来存储相对基本类型更加复杂的对象数据,那么切换上下文的开销将变得巨大!
不过堆内存虽然空间大,能存放大量的数据,但与此同时垃圾内存的回收会带来更大的开销
V8引擎如何进行垃圾内存的回收
强引用和弱引用
WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。
弱引用: 在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。
我们默认创建一个对象:const obj = {},就默认创建了一个强引用的对象,我们只有手动将obj = null,它才会被垃圾回收机制进行回收,如果是弱引用对象,垃圾回收机制会自动帮我们回收。
举个例子:
如果我们使用
Map的话,那么对象间是存在强引用关系的:
const myMap = new Map()
let my = {
name: "jean",
sex: "女"
}
myMap.set(my, 'info');
my=null
当执行my = null时会解除my对原数据的引用,如果是强引用关系则引用计数为 1 ,不会被垃圾回收机制清除。
再来看
WeakMap:
const myMap = new WeakMap()
let my = {
name: "jean",
sex: "女"
}
myMap.set(my, 'info');
my=null
而myMap实例对象对my所引用对象是弱引用关系,该数据的引用计数为 0 ,程序垃圾回收机制在执行时会将引用对象回收。
V8 中执行一段JS代码的整个过程
- 首先通过词法分析和语法分析生成
AST - 将 AST 转换为字节码
- 由解释器逐行执行字节码,遇到热点代码启动编译器进行编译,生成对应的机器码, 以优化执行效率
事件循环机制
浏览器的事件循环
JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。整个执行过程,我们称为事件循环过程。一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。 说说事件循环机制
栈:先进后出-----------------队列:先进先出
-
一开始整段脚本作为第一个宏任务执行
-
执行过程中同步代码放入
执行栈中直接执行,宏任务进入宏任务队列,微任务进入微任务队列 -
当前宏任务执行完出栈,检查微任务队列,如果有则依次执行,直到微任务队列为空
-
执行浏览器 UI 线程的渲染工作
-
检查是否有Web worker任务,有则执行
-
执行队首新的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空
微任务macro-task和宏任务micro-task
微任务和宏任务都是属于
队列,而不是放在栈中在每一个宏任务中定义一个微任务队列,当该宏任务执行完成,会检查其中的微任务队列,如果为空则直接执行下一个宏任务,如果不为空,则
依次执行微任务,执行完成才去执行下一个宏任务。
为什么要有微任务?
按照官方的设想,任务之间是不平等的,有些任务对用户体验影响大,就应该优先执行,而有些任务属于背景任务(比如定时器),晚点执行没有什么问题,所以设计了这种优先级队列的方式
利用微任务解决了两大痛点:
-
- 采用异步回调替代同步回调解决了浪费 CPU 性能的问题。
-
- 放到当前宏任务最后执行,解决了回调执行的实时性问题。
执行机制:
执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。
macro-task大概包括:
- script(整体代码)
- setTimeout
- setInterval
- setImmediate
- I/O
- UI render
micro-task大概包括:
- process.nextTick
- Promise
- Async/Await(实际就是promise)
- MutationObserver(html5新特性)
async/await
async就是promise的一种语法包装(所谓语法糖)
await其实是异步的,跟then差不多(从语法上来说,await其实就是promise的then)
-
如果await 后面直接跟的为一个
变量或者是同步任务-
如await 1或await console.log(222)
-
这种情况的话相当于直接把await后面的代码注册为一个微任务,可以简单理解为promise.then(await下面的代码)
-
(async ()=>{
console.log(111);
await console.log(222);
console.log(333);
})()
// 相当于
(async ()=>{
console.log(111);
new Promise(resolve => {
console.log(222)
resolve()
}).then(()=>{
console.log(333)
})
})()
结果:111-222-333
再举个完整🌰
console.log('aaa');
setTimeout(()=>console.log('t1'), 0);
(async ()=>{
console.log(111);
await console.log(222);
console.log(333);
setTimeout(()=>console.log('t2'), 0);
})().then(()=>{
console.log(444);
});
console.log('bbb');
aaa
111
222
bbb
333
444
t1
t2
-
第1步、毫无悬念
aaa,过 -
第2步、
t1会放入任务队列等待 -
第3步、虽然
async是异步操作,但async函数本身(也就是111所在的()=>{}),其实依然是同步执行的,除非有await出现,这个下面会说,所以,这里111会直接同步执行,而不是放到队列里等待 -
第4步、
222这里很重要了,首先,console.log自己是同步的,所以立即就会执行,我们能直接看到222,但是await本身就是then,所以接下来的console.log(333);和setTimeout(()=>console.log('t2'), 0);无法直接执行,就塞到微任务队列里等待了 -
第5步、
bbb毫无疑问,而且当前任务完成,优先执行微任务队列,也就是console.log(333)开始的那里 -
第6步、执行
333,然后定时器t2会加入任务队列等待(此时的任务队列里有t1和t2两个了),并且async完成,所以console.log(444)进入微任务队列等待 -
第7步、优先执行微任务,也就是
444,此时所有微任务都完成了 -
第8步、执行剩下的普通任务队列,这时
t1和t2才会出来
-
如果await后面跟的是一个
异步函数的调用,如下-
此时执行完awit并
不先把await后面的代码注册到微任务队列中去 -
而是执行完await之后,直接跳出async1函数,执行其他代码。
-
然后遇到promise的时候,把promise.then注册为微任务。
-
其他代码执行完毕后,需要回到async1函数去执行剩下的代码,然后把await后面的代码注册到微任务队列当中,注意此时微任务队列中是有之前注册的微任务的。
-
所以这种情况会先执行async1函数之外的微任务(promise1,promise2),然后才执行async1内注册的微任务(async1 end).
-
可以理解为,这种情况下,await 后面的代码会在本轮循环的最后被执行.
-
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
return Promise.resolve().then(()=>{
console.log('async2 end1')
})
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
输出结果:
// script start => async2 end => Promise => script end => async2 end1 => promise1 => promise2 => async1 end => setTimeout
谈谈你对JS中this的理解
隐式绑定
- 全局上下文
- 直接调用函数
- 对象.方法的形式调用
- DOM事件绑定(特殊)
- new构造函数绑定
- 箭头函数
显示绑定
call/apply/bind
箭头函数和普通函数的区别
箭头函数的this指向规则:
-
箭头函数没有
prototype(原型),所以箭头函数本身没有this -
箭头函数的this指向定义时所在的外层第一个普通函数,跟使用位置没有关系。
-
不能直接修改箭头函数的this指向
- 但是被继承的普通函数的this指向改变,箭头函数的this指向会跟着改变
-
箭头函数外层没有普通函数,严格模式和非严格模式下它的this都会指向
window(全局对象)
箭头函数的arguments
-
如果箭头函数的this指向
window(全局对象)使用arguments会报错,未声明arguments。PS:如果你声明了一个全局变量为
arguments,那就不会报错了,但是你为什么要这么做呢?
let b = () => {
console.log(arguments);
};
b(1, 2, 3, 4); // Uncaught ReferenceError: arguments is not defined
- 箭头函数的this如果指向普通函数,它的
argumens继承于该普通函数。
rest参数(...扩展运算符)获取函数的多余参数
这是ES6的API,用于获取函数不定数量的参数数组,这个API是用来替代arguments的
let a = (first, ...abc) => {
console.log(first, abc); // 1 [2, 3, 4]
};
a(1, 2, 3, 4);
rest参数的用法相对于arguments的优点:
-
箭头函数和普通函数都可以使用。
-
更加灵活,接收参数的数量完全自定义。
-
可读性更好
参数都是在函数括号中定义的,不会突然出现一个
arguments,以前刚见到的时候,真的好奇怪了! -
rest是一个真正的数组,可以使用数组的API。
注意:
- rest必须是函数的最后一位参数:
- 函数的length属性,不包括 rest 参数
let a = (first, ...rest, three) => {
console.log(first, rest,three); // 报错:Rest parameter must be last formal parameter
};
a(1, 2, 3, 4);
(function(...a) {}).length // 0
(function(a, ...b) {}).length // 1
使用new调用箭头函数会报错
无论箭头函数的thsi指向哪里,使用new调用箭头函数都会报错,因为箭头函数没有constructor
let a = () => {};
let b = new a(); // a is not a constructor
箭头函数不支持new.target
new.target是ES6新引入的属性,普通函数如果通过new调用,new.target会返回该函数的引用。此属性主要:用于确定构造函数
是否为new调用的。
-
箭头函数的this指向全局对象,在箭头函数中使用箭头函数会报错
let a = () => { console.log(new.target); // 报错:new.target 不允许在这里使用 }; a(); -
箭头函数的this指向普通函数,它的new.target就是指向该普通函数的引用。
new bb(); function bb() { let a = () => { console.log(new.target); // 指向函数bb:function bb(){...} }; a(); }
JS深入
函数的arguments为什么不是数组?如何转化成数组?
因为arguments本身并不能调用数组方法,它是一个另外一种对象类型,只不过属性从0开始排,依次为0,1,2...最后还有callee和length属性。我们也把这样的对象称为类数组。
常见的类数组还有:
-
- 用getElementsByTagName/ClassName()获得的HTMLCollection
-
- 用querySelector获得的nodeList
1. Array.prototype.slice.call()
function sum(a, b) {
let args = Array.prototype.slice.call(arguments);
}
sum(1, 2);//3
复制代码
2. Array.from()
从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。
function sum(a, b) {
let args = Array.from(arguments);
}
sum(1, 2);//3
这种方法也可以用来转换Set和Map哦!
扩展:数组去重合并
function combine(){
let arr = [].concat.apply([], arguments); //没有去重复的新数组
return Array.from(new Set(arr));
}
var m = [1, 2, 2], n = [2,3,3];
console.log(combine(m,n));
3. ES6展开运算符
function sum(a, b) {
let args = [...arguments];
}
sum(1, 2);//3
4. 利用concat+apply
function sum(a, b) {
let args = Array.prototype.concat.apply([], arguments);//apply方法会把第二个参数展开
console.log(args.reduce((sum, cur) => sum + cur));//args可以调用数组原生的方法啦
}
sum(1, 2);//3
forEach
forEach实现原理
// forEach的实现原理
Array.prototype.baseForEach = function (callback) {
for (let i = 0; i < this.length; i++) {
callback(this[i], i, this)
}
}
如何中断forEach循环?
官方推荐方法(替换方法):用every和some替代forEach函数。every在碰到return false的时候,中止循环。some在碰到return true的时候,中止循环
- 使用try监视代码块,在需要中断的地方抛出异常。
let arr = [0, 1, 'stop', 3, 4]
try {
arr.forEach((item, index) => {
if (item === 'stop') {
throw new Error('中断')
}
console.log(index) // 输出 0 1 后面不输出
})
} catch (err) {
console.log(err.message) // 中断
}
- 手写forEach中断循环
// 手写实现退出循环
Array.prototype.myForEach = function (fn) {
for (let i = 0; i < this.length; i++) {
let ret = fn(this[i], i, this)
// 如果没有return,或return后面没有值,结果都是undefined,不会中断
// 让return false,或return null,就会中断循环
if (ret !== undefined && (ret === null || ret === false)) break
}
}
当 forEach 遇上了 async 与 await
现象:
keys的每一项都是age。理由只有一个:forEach循环没有等待await的执行。原因: 从上面的baseForEach实现原理,可以看到
myForEach的参数callback是一个异步函数,但是在内部进行调用的时候并没有使用await关键字来等待异步执行的结果,而是直接进行循坏,所以当然不会拿到结果。
let arr = ['jean', '17']
let keys = ['name', 'age']
let i = 0
let obj = {}
arr.forEach(async (item, index) => {
i++
const value = await Promise.resolve(item)
obj[keys[i]] = value
console.log(obj)
})
// {age: "jean"}
// {age: "17"}
解决方案
- 改造我们的
myForEach函数
- 让
callback函数不要立即执行就好,而是等待异步任务的状态改变后执行
Array.prototype.myForEach = async function(callback, thisArg) {
for (let i = 0; i < this.length; i ++) {
await callback(this[i], i, this)
}
}
// 用myForEach来循环
arr.myForEach(async (item, index) => {
i++
const value = await Promise.resolve(item)
obj[keys[i]] = value
console.log(obj)
})
// {name: "jean"}
// {name: "jean", age: "17"}
- 普通的
for循环、for ... in ...、for ... of ...,这些循环结构都会在本身得异步函数作用域中执行,可以在其内部直接添加await关键字获取到值。
JS判断数组中是否包含某个值
1.Array.prototype.indexOf
此方法判断数组中是否存在某个值,如果存在,则返回数组元素的下标,否则返回-1。
注意:只能判断普通数据类型!!因为普通数据类型,匹配值;复杂数据类型,匹配地址
# 基本的数组
let a = [1,2,3,4,5]
//判断a当中有没有2
//方式1.通过indexOf
let i = a.indexOf(2)
console.log(i!==-1)//索引
# 复杂数组(对象数组)
let a = [{ name: 1 }, { name: 2 }, { name: 3 }]
//判断a当中有没有name为2
//方式1.通过indexOf,不可以用
let i = a.indexOf({name: 2})//普通数据类型,匹配值;复杂数据类型,匹配地址
console.log(i)//-1,
2.Array.prototype.includes(searcElement[,fromIndex])
此方法判断数组中是否存在某个值,如果存在返回true,否则返回false
注意:只能判断普通数据类型!!因为普通数据类型,匹配值;复杂数据类型,匹配地址
# 基本的数组
//方式2-通过inclueds.
let i = a.includes(2)
console.log(i)// true
# 复杂数组(对象数组)
//方式2-通过inclueds,,不可以用
let i = a.includes({name: 2})//普通数据类型,匹配值;复杂数据类型,匹配地址
console.log(i)// false
3.Array.prototype.find(callback[,thisArg])
返回数组中满足条件的第一个元素的值,如果没有,返回undefined
# 基本的数组
//方式3-find
let i = a.find(t=>t===2)
console.log(i) //2
# 复杂数组(对象数组)
//方式3-find-可以
let i = a.find(t=>t.name===2)
console.log(i) //{name: 2}
4.Array.prototype.findeIndex(callback[,thisArg])
返回数组中满足条件的第一个元素的下标,如果没有找到,返回
-1]
# 基本的数组
//方式4-findIndex
let i = a.findIndex(t=>t === 2)
console.log(i)//索引
# 复杂数组(对象数组)
//方式4-findIndex-可以
let i = a.findIndex(t=>t.name === 2)
console.log(i)//索引
5.Array.prototype.some(callback[,thisArg])
此方法判断数组中是否存在某个值,如果存在返回true,否则返回false
# 基本的数组
//方式5-some:是否有一个满足...条件
let i = a.some(t=>t === 2)
console.log(i)//true
# 复杂数组(对象数组)
//方式5-some:是否有一个满足...条件,true
let i = a.some(t => t.name === 2)
console.log(i) //true
JS中flat---数组扁平化
JS数组的高阶函数
Set 集合
数组去重
- 集合是由一组无序且唯一(即不能重复)的项组成的,可以想象成集合是一个既没有重复元素,也没有顺序概念的数组
- ES6提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值
- Set 本身是一个构造函数,用来生成 Set 数据结构
- 这里说的Set其实就是我们所要讲到的集合,先来看下基础用法
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i); // 2 3 5 4
}
// 去除数组的重复成员
let array = [1,2,1,4,5,3];
[...new Set(array)] // [1, 2, 4, 5, 3]
Set实例的属性和方法
-
Set的属性:
- size:返回集合所包含元素的数量
-
Set的方法:
-
操作方法
- add(value):向集合添加一个新的项
- delete(value):从集合中移除一个值
- has(value):如果值在集合中存在,返回true,否则false
- clear(): 移除集合里所有的项
-
遍历方法
- keys():返回一个包含集合中所有键的数组
- values():返回一个包含集合中所有值的数组
- entries:返回一个包含集合中所有键值对的数组(感觉没什么用就不实现了)
- forEach():用于对集合成员执行某种操作,没有返回值
-
Map 字典
数据存储 集合又和字典有什么区别呢:
- 共同点:集合、字典可以存储不重复的值
- 不同点:集合是以
[值,值]的形式存储元素,字典是以[键,值]的形式存储
所以这一下让我们明白了,Map其实的主要用途也是用于存储数据的,相比于Object只提供 '字符串—值的对应,Map提供了“值—值”的对应。也就是说如果你需要“键值对”的数据结构,Map比Object更合适
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
Map的属性和方法
属性:
- size:返回字典所包含的元素个数
操作方法:
- set(key, val): 向字典中添加新元素
- get(key):通过键值查找特定的数值并返回
- has(key):如果键存在字典中返回true,否则false
- delete(key): 通过键值从字典中移除对应的数据
- clear():将这个字典中的所有元素删除
遍历方法:
- keys():将字典中包含的所有键名以数组形式返回
- values():将字典中包含的所有数值以数组形式返回
- forEach():遍历字典的所有成员
JS手动实现
模拟实现JS的new操作符
new 做了什么?
(1)创建一个新的对象
(2)将构造函数的作用域赋给新的对象(因此this就指向了这个新对象)
(3)执行构造函数中的代码(为这个新对象添加属性)
(4)绑定原型
(5)返回新对象
如何实现new
-
创建了一个全新的对象。
-
这个对象会被执行
[[Prototype]](也就是__proto__)链接。 -
生成的新对象会绑定到函数调用的
this。 -
通过
new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。 -
如果函数没有返回对象类型
Object(包含Functoin,Array,Date,RegExg,Error),那么new表达式中的函数调用会自动返回这个新的对象。
/**
* 模拟实现 new 操作符
* @param {Function} ctor [构造函数]
* @return {Object|Function|Regex|Date|Error} [返回结果]
* **/
function newOperator(ctor) {
// 0.判断ctor是否是个函数
if (typeof ctor !== 'function') {
throw new TypeError('newOperator function the first param must be a function')
}
// 1.创建了一个全新的对象。2. 4.被[[Prototype]]链接到这个函数的prototype对象上。
const newObj = Object.create(ctor.prototype)
// 2.拿到传入的参数 args
const [first, ...args] = arguments
// 3.生成的新对象会绑定到函数调用的`this`。
const ctorReturnResult = ctor.apply(newObj, args)
// 5.判断 是否返回对象类型Object|Function,
// 先判断return结果是不是 Object|Function
const isObject = typeof ctorReturnResult === 'object' && ctorReturnResult !== null
const isFunction = typeof ctorReturnResult === 'function'
if (isObject || isFunction) {
// 5.1如果是会返回 这个return的结果
return ctorReturnResult
}
// 5.2如果不是,就直接返回newObj这个对象
return newObj
}
function Student(name) {
this.name = name
// return {}
}
var student = newOperator(Student, 'jean')
console.log(student)
手写call
// !!注意:不能使用let,因为let声明的变量不会挂载到window上
var name = '这是window下的name'
Function.prototype.myCall = function (context) {
// this指的就是getName这个函数 ---- ƒ () {return this.name}
// 1. 先判断 调用的函数 是不是 个function
if (typeof this !== 'function') {
throw new TypeError('Error')
}
// 2. 再判断有没有传入 context
context = context || window
// const result = this() 此时调用这个函数 的是window
// 3. 假设 context的fn属性 指向了 该函数
context.fn = this
// 4. 实现传参 拿掉取出 第一个参数【this的指向--context】 的后面传的参数
const args = [...arguments].splice(1)
// arguments --- [0: {name: "jean", fn: ƒ}, 1: 1, 2: 2, 3: 3]
// args --- [1,2,3]
// 5. 此时调用这个函数的就是 context了
const result = context.fn(...args)
// 6. 记得 执行函数完后 ,要把先前添加的fn属性delete掉
delete context.fn
return result
}
let obj = {
getName: function () {
console.log([...arguments]) // [1, 2, 3]
return this.name + [...arguments] // jean1,2,3
}
}
let obj2 = {
name: 'jean'
}
console.log(obj.getName.myCall(obj2, 1, 2, 3))
手写apply
<script>
// !!注意:不能使用let,因为let声明的变量不会挂载到window上
var name = '这是window下的name'
Function.prototype.myApply = function (context) {
// 先判断是不是个函数
if (typeof this !== 'function') {
throw new TypeError('Error')
}
// 再判断有没有 传入this需要指向的对象
context = context || window
// 给context临时添加一个新的fn属性,指向this
context.fn = this
// 处理传入的参数, 拿到传入的第二个参数 即为数组
const args = arguments[1]
// args--- [1, 2, 3]
let result
// 判断有没有传入数组 形参
if (args) {
result = context.fn(...args)
} else {
result = context.fn()
}
delete context.fn
return result
}
let obj = {
getName: function () {
console.log(this.name + [...arguments])
}
}
let obj2 = {
name: 'jean'
}
obj.getName.myApply(obj2, [1, 2, 3])
</script>
手写bind
- 对于普通函数,绑定this指向【如下述代码】
- 对于构造函数,要保证原函数的原型对象上的属性不能丢失 参考链接
<script>
var name = 'window下的name'
Function.prototype.myBind = function (context) {
// 1. 先判断this是不是个函数
if (typeof this !== 'function') {
throw new TypeError('Error')
}
// 2. 判断有没有传入 this的指向
context = context || window
// 保存this的值,它代表调用 bind 的函数
const _this = this
// 3. 拿到参数 删除第一个
const args = [...arguments].slice(1)
// 返回的是一个函数
return function () {
// 再调用apply
return _this.apply(context, args.concat(...arguments))
}
}
let obj = {
getName: function () {
console.log(this.name + [...arguments])
}
}
let obj2 = {
name: 'jean'
}
const fn = obj.getName.myBind(obj2)
fn()
</script>
赋值|浅拷贝|深拷贝的区别
针对简单数据类型
拷贝的是值,是深拷贝
针对引用数据类型
-
当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
-
浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。
-
深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。
实现浅拷贝
创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
1.手动实现
function shallowClone(target) {
// 1.1先判断是否为引用类型数据
if (typeof target === 'object' || target != null) {
// 2.1判断是数组 还是 对象, 初始化数据
const cloneTarget = Array.isArray(target) ? [] : {}
for (let key in target) {
// 2.2判断 key 是否是 target 的自身属性,而不是object的继承属性
if (target.hasOwnProperty(key)) {
cloneTarget[key] = target[key]
}
}
return cloneTarget
} else {
// 1.2 如果是简单数据类型或为null,直接赋值即可
return target
}
}
2.Array.prototype.slice()
let arr = [1, 3, {
username: ' jean'
}];
let arr3 = arr.slice();
arr3[2].username = 'ximi'
console.log(arr); // [ 1, 3, { username: 'ximi' } ]
3.Array.prototype.concat()
let arr = [1, 3, {
username: 'kobe'
}];
let arr2 = arr.concat();
4.Object.assign()
Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
let obj1 = { person: {name: "jean", age: 41},sports:'basketball' };
let obj2 = Object.assign({}, obj1);
obj2.person.name = "ximi";
obj2.sports = 'football'
console.log(obj1); // { person: { name: 'ximi', age: 41 }, sports: 'basketball' }
```-开运算符...
展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()的功能相同。
```js
let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}
6.函数库lodash的_.clone方法
该函数库也有提供_.clone用来做 Shallow Copy,利用这个库也实现深拷贝。
终端下载lodash
npm i lodash
在node.js 或 vue-cli环境下,引入插件并使用
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.clone(obj1);
console.log(obj1.b.f === obj2.b.f);// true
实现深拷贝
将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象
1.JSON.parse(JSON.stringify(obj))
注意:
- 会忽略undefined Symbol
- 不能序列化函数
- 不能解决循环引用的对象
- 不能正确处理 new Date()
- 不能处理正则
2.jQuery.extend()
3.lodash.cloneDeep()
4.如何写出一个惊艳面试官的深拷贝?
function deepClone(target, map = new WeakMap()) {
if (typeof target === 'object' && target !== null) {
let cloneTarget = Array.isArray(target) ? [] : {}
// 判断是否有循环引用
if (map.get(target)) {
return map.get(target)
}
map.set(target, cloneTarget)
for (let key in target) {
if (target.hasOwnProperty(key)) {
cloneTarget[key] = deepClone(target[key], map)
}
}
return cloneTarget
} else {
return target
}
}
强引用与弱引用
什么是弱引用呢?
在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。
我们默认创建一个对象:const obj = {},就默认创建了一个强引用的对象,我们只有手动将obj = null,它才会被垃圾回收机制进行回收,如果是弱引用对象,垃圾回收机制会自动帮我们回收。
如果我们使用Map的话,那么对象间是存在强引用关系的:
let obj = { name : 'ConardLi'}
const target = new Map();
target.set(obj,'code秘密花园');
obj = null;
虽然我们手动将obj,进行释放,然是target依然对obj存在强引用关系,所以这部分内存依然无法被释放。
再来看WeakMap
WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。
let obj = { name : 'ConardLi'}
const target = new WeakMap();
target.set(obj,'code秘密花园');
obj = null;
如果是WeakMap的话,target和obj存在的就是弱引用关系,当下一次垃圾回收机制执行时,这块内存就会被释放掉。
设想一下,如果我们要拷贝的对象非常庞大时,使用Map会对内存造成非常大的额外消耗,而且我们需要手动清除Map的属性才能释放这块内存,而WeakMap会帮我们巧妙化解这个问题。
手写promise
// MyPromise.js
// 先定义三个常量表示状态
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
// 新建 MyPromise 类
class MyPromise {
constructor(executor) {
// executor 是一个执行器,进入会立即执行
// 并传入resolve和reject方法
try {
executor(this.resolve, this.reject)
} catch (error) {
this.reject(error)
}
}
// 储存状态的变量,初始值是 pending
status = PENDING
// 成功之后的值
value = null
// 失败之后的原因
reason = null
// 存储成功回调函数
onFulfilledCallbacks = []
// 存储失败回调函数
onRejectedCallbacks = []
// 更改成功后的状态
resolve = value => {
// 只有状态是等待,才执行状态修改
if (this.status === PENDING) {
// 状态修改为成功
this.status = FULFILLED
// 保存成功之后的值
this.value = value
// resolve里面将所有成功的回调拿出来执行
while (this.onFulfilledCallbacks.length) {
// Array.shift() 取出数组第一个元素,然后()调用,shift不是纯函数,取出后,数组将失去该元素,直到数组为空
this.onFulfilledCallbacks.shift()(value)
}
}
}
// 更改失败后的状态
reject = reason => {
// 只有状态是等待,才执行状态修改
if (this.status === PENDING) {
// 状态成功为失败
this.status = REJECTED
// 保存失败后的原因
this.reason = reason
// resolve里面将所有失败的回调拿出来执行
while (this.onRejectedCallbacks.length) {
this.onRejectedCallbacks.shift()(reason)
}
}
}
then(onFulfilled, onRejected) {
// 默认值处理
const realOnFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
const realOnRejected =
typeof onRejected === 'function'
? onRejected
: reason => {
throw reason
}
// 为了链式调用这里直接创建一个 MyPromise,并在后面 return 出去
const promise2 = new MyPromise((resolve, reject) => {
const callbackMicrotask = callback => {
// 创建一个微任务等待 promise2 完成初始化
queueMicrotask(() => {
try {
const value = callback === realOnFulfilled ? this.value : this.reason
// 获取成功回调函数的执行结果
const x = callback(value)
// 集中处理
// 如果相等了,说明return的是自己,抛出类型错误并返回
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
}
// 判断x是不是 MyPromise 实例对象
if (x instanceof MyPromise) {
// 执行 x,调用 then 方法,目的是将其状态变为 fulfilled 或者 rejected
// x.then(value => resolve(value), reason => reject(reason))
// 简化之后
x.then(resolve, reject)
} else {
// 普通值
resolve(x)
}
} catch (error) {
reject(error)
}
})
}
// 判断状态
if (this.status === FULFILLED) {
callbackMicrotask(realOnFulfilled)
} else if (this.status === REJECTED) {
callbackMicrotask(realOnRejected)
} else if (this.status === PENDING) {
// 等待
// 因为不知道后面状态的变化情况,所以将成功回调和失败回调存储起来
// 等到执行成功失败函数的时候再传递
this.onFulfilledCallbacks.push(function onFulfilledCallback() {
callbackMicrotask(realOnFulfilled)
})
this.onRejectedCallbacks.push(function onRejectedCallback() {
callbackMicrotask(realOnRejected)
})
}
})
return promise2
}
catch(onRejected) {
return this.then(undefined, onRejected)
}
// resolve 静态方法 Promise.resolve()
static resolve(parameter) {
// 如果传入 MyPromise 就直接返回
if (parameter instanceof MyPromise) {
return parameter
}
// 转成常规方式
return new MyPromise(resolve => {
resolve(parameter)
})
}
// reject 静态方法
static reject(reason) {
return new MyPromise((resolve, reject) => {
reject(reason)
})
}
// Promise函数对象的 all 方法,接受一个promise类型的数组
// 返回一个新的Promise对象
static all(promises) {
// 保证返回的值得结果的顺序和传进来的时候一致
// 只有全部都成功长才返回成功
const values = new Array(promises.length) //保存所有的value
let successCount = 0
return new MyPromise((resolve, reject) => {
promises.forEach((p, index) => {
// 由于p有可能不是一个Promise
MyPromise.resolve(p).then(
value => {
successCount++
values[index] = value
// 拿到了全部的成功结果
if (successCount === promises.length) {
resolve(values)
}
},
// 如果失败,状态就会发生变化,就不会再往后执行了
reason => {
reject(reason)
}
)
})
})
}
static race(promises) {
return new MyPromise((resolve, reject) => {
promises.forEach(p => {
// 由于p有可能不是一个Promise
MyPromise.resolve(p).then(
value => {
resolve(value)
},
reason => {
reject(reason)
}
)
})
})
}
}