Promise 是异步编程的一种解决方案,简单说就是一个保存着某个未来才会结束的事件(通常是一个异步操作)结果的容器。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
它有三种状态,pending(进行中)、fulfilled(已成功)、reject(已失败)。注:resolved是指完成状态,结果可能包含fulfilled和rejected
使用Promise的语法来解决回调地狱的问题,使代码拥有可读性和可维护性。
“回调函数”:把一个函数当作参数传递,传递的是函数的定义并不会立即执行,而是在将来特定的时机再去调用,这个函数就叫做回调函数。
“回调地狱”:把函数作为参数层层嵌套请求,这样层层嵌套,人们称之为回调地狱,代码阅读性非常差。
var sayhello = function (order, callback) {
setTimeout(function () {
console.log(order);
callback();
}, 1000);
}
sayhello("first", function () {
sayhello("second", function () {
sayhello("third", function () {
console.log("end");
});
});
});
使用promise改造
ar sayhello = function (order) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log(order);
//在异步操作执行完后执行 resolve() 函数
resolve();
}, 1000);
});
}
sayhello("first").then(function () {
//仍然返回一个 Promise 对象
return sayhello("second");
}).then(function () {
return sayhello("third");
}).then(function () {
console.log('end');
}).catch(function (err) {
console.log(err);
})
从表面上看,Promise只是能够简化层层回调的写法,而实质上Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback 函数要简单、灵活的多。
通过Promise这种方式很好的解决了回调地狱问题,使得异步过程同步化,让代码的整体逻辑与大脑的思维逻辑一致,减少出错率。
promise
.finally(() => {
// 语句
});
// 等同于
promise
.then(
result => {
// 语句
return result;
},
error => {
// 语句
throw error;
}
);
实现:
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
上面代码中,不管前面的 Promise 是fulfilled还是rejected,都会执行回调函数callback。
(详见手写promise)[https://developer.aliyun.com/article/613412]
(结合阮一峰的promise和class介绍一起看理解会更深刻一些)[https://es6.ruanyifeng.com/#docs/promise]
class Promise {
// constructor为构造方法,通过new命令生成对象实例时,自动调用该方法
// this是实例对象
constructor(executor) {
// 初始状态
this.state = 'pending'
// 成功的返回值
this.value = undefined
// 失败原因
this.reason = undefined
// 成功存放的数组
this.onResolvedCallbacks = []
// 失败存放的数组
this.onRejectCallbacks = []
const resolve = value => {
// 调用resolved后,状态需要变更
if (this.state === 'pending') {
this.state = 'fulfilled'
this.value = value
// 一旦执行resolve,调用成功数组的函数
this.onResolvedCallbacks.forEach(fn => fn())
}
}
const reject = reason => {
// 调用resolved后,状态变更为失败
if (this.state === 'pending') {
this.state = 'rejected'
this.reason = reason
this.onRejectCallbacks.forEach(fn => fn())
}
}
try {
executor(resolve, reject)
} catch (err) {
// 如果执行错误,直接把错误返回
reject(err)
}
}
// then方法的第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数,它们都是可选的。
then (onFulfilled, onRejected) {
// 如果onFulfilled不是函数,直接返回value
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
onRejected = typeof onRejected === 'function' ? onRejected : err => {throw err}
// 如果状态为成功,执行onFulfilled,传入成功值
let promise2 = new Promise((resolve, reject)=> {
// onFulfilled和onReject只能异步调用
if (this.state === 'fulfilled') {
setTimeout(()=> {
try{
let x = onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch(err) {
reject(err)
}
},0)
} else if (this.state === 'rejected') {
setTimeout(()=> {
try{
let x = onFulfilled(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch(err) {
reject(err)
}
},0)
} else if (this.state === 'pending') {
this.onResolvedCallbacks.push(()=> {
setTimeout(()=> {
try{
let x = onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch(err) {
reject(err)
}
},0)
})
this.onRejectCallbacks.push(()=> {
setTimeout(()=> {
try{
let x = onFulfilled(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch(err) {
reject(err)
}
},0)
})
}
})
return promise2
}
// Promise.prototype.catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。
catch() {
}
}
function resolvePromise(promise2, x, resolve, reject) {
// x不能等于promise2,会循环饮用报错
if (x === promise2) {
return reject(new TypeError('Chaining cycle detected for promise'))
}
// 防止重复调用
let called
// 如果为普通类型直接resolve
if (x != null && (typeof x === 'object' || typeof x === 'function')) {
try {
let then = x.then
// then是函数,默认为promise
if (typeof then === 'function') {
then.call(x,y => {
if (called) return
called = true
// resolve的结果依旧是promise 那就继续解析
resolvePromise(promise2, y, resolve, reject);
}, err => {
if (called) return
called = true
reject(err)
})
} else {
resolve(x)
}
} catch (e) {
if (called) return
called = true
reject(e)
}
} else {
resolve(x)
}
}
// resolve、catch、reject、race、all方法不在promise/A+规范中,均为ES6实现的方法
Promise.resolve = function (val) {
return new Promise((resolve, reject)=> {
resolve(val)
})
}
Promise.reject = function (val) {
return new Promise((resolve, reject)=>{
reject(val)
})
}
// 第一个完成的promise函数状态传递给Promise.race的结果
Promise.race = function (promises = []) {
return new Promise((resolve, reject) => {
promises.forEach(fn => {
// 如果传入的参数为promise
if (fn && typeof fn === 'function') {
// 谁先执行完到then,使用第一个执行完成的结果
fn.then(resolve, reject) // 这里不理解可以看看前面then的实现
} else {
Promise.resolve(fn).then(resolve, reject)
}
})
})
}
Promise.all = function (promises = []) {
const promiseRes = []
// 主要需要实现两个功能,1、所有promise的状态都变成fulfilled才会变成fulfilled,其中要一个被rejected,状态就会变成reject;
// 2、返回的参数是按照传入的顺序而不是完成的时间先后
return new Promise((resolve, reject) => {
promises.forEach((fn, index) => {
// 可以加入传入参数不为promise的判断处理,见race方法的实现
fn.then(res => {
promiseRes.splice(index, 0, res)
if (promiseRes.length = promises.length) {
resolve(promiseRes)
}
}, err => {
reject(err)
})
})
})
}
理解了上面的手写promise,就可以轻易回答这个问题
异步队列:
promise 的本质是回调函数,then 方法的本质是依赖收集,它把 fulfilled 状态要执行的回调函数放在一个队列, rejected 状态要执行的回调函数放在另一个队列。待 promise 从 pending 变为 fulfilled/rejected 状态后,把相应队列的所有函数,执行一遍。
链式调用:
then方法返回的是一个新的Promise实例。所以可以采用链式写法,将返回结果作为参数,传入第二个回调函数
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
async 函数是 Generator 函数的语法糖,是一种异步编程解决方案。
async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
异步函数的语法结构更像是标准的同步函数,发明了async和await的初衷就是让异步代码的语法结构跟同步代码类似。相对promise,async的实现最简洁,最符合语义
应用场景:await能解决的问题,promise其实也可以,但是在一些简单的异步场景,await会更加简洁,更具语义化;另外promise提供了很多方法,比如all,race这些能满足更多场景的使用
有三种状态,pengding、fulfilled、rejected。
优点:
(1)解决回调地狱问题 (2)更好地进行错误捕获 (3)提高代码可读性
缺点:
(1)无法取消Promise,一旦新建它就会立即执行,无法中途取消。
(2)如果不设置回调函数,promise内部抛出的错误,不会反应到外部。
(3)当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
generator:
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。ES6 诞生以前,异步编程的方法大概有四种,回调函数、事件监听、发布/订阅、Promise 对象。Generator 函数将 JavaScript 异步编程带入了一个全新的阶段。
语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态
遍历器对象的next方法的运行逻辑如下。
(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。
for…of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。
async/await:
async 其实就是 Generator 函数的语法糖。将 Generator 函数的星号(*)替换成async,将yield替换成await
async函数对 Generator 函数的改进,体现在以下四点。
1)内置执行器。Generator 函数的执行必须靠执行器,await会自动执行
2) 更好的语义。比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
3)更广的适用性。await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)
4)返回值是 Promise。async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了
详见
事件循环分为浏览器事件循环和node.js事件循环
浏览器的事件循环分为同步任务和异步任务;所有同步任务都在主线程上执行,形成一个函数调用栈(执行栈),而异步则先放到任务队列(task queue)里,任务队列又分为宏任务(macro-task)与微任务(micro-task)。下面的整个执行过程就是事件循环
宏任务大概包括::script(整块代码)、setTimeout、setInterval、I/O、UI交互事件、setImmediate(node环境)
微任务大概包括::new promise().then(回调)、MutationObserver(html5新特新)、Object.observe(已废弃)、process.nextTick(node环境)
若同时存在promise和nextTick,则先执行nextTick
执行过程
JS 引擎去执行 JS 代码的时候会从上至下按顺序执行,先把同步任务放入执行栈中立即执行,微任务放入微任务队列,宏任务放在宏任务队列。当执行栈被清空,然后去执行所有的微任务,当所有微任务执行完毕之后。再次从宏任务开始循环执行,直到执行完毕,然后再执行所有的微任务,就这样一直循环下去。如果在执行微队列任务的过程中,又产生了微任务,那么会加入整个队列的队尾,也会在当前的周期中执行。其实浏览器执行Js代码的完整顺序应该是:同步任务 ——> 异步微任务 ——> DOM渲染页面 ——>异步宏任务
process.nextTick(),效率最高,消费资源小,但会阻塞CPU的后续调用; process.nextTick()方法可以在当前"执行栈"的尾部–>下一次Event Loop(主线程读取"任务队列")之前–>触发process指定的回调函数。也就是说,它指定的任务总是发生在所有异步任务之前,当前主线程的末尾。(nextTick虽然也会异步执行,但是不会给其他io事件执行的任何机会)
setTimeout(),精确度不高,可能有延迟执行的情况发生,且因为动用了红黑树,所以消耗资源大; setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。
setImmediate(),消耗的资源小,也不会造成阻塞,但效率也是最低的。setImmediate()是将事件插入到事件队列尾部,主线程和事件队列的函数执行完成之后立即执行setImmediate指定的回调函数,和setTimeout(fn,0)的效果差不多,但是当他们同时在同一个事件循环中时,执行顺序是不定的。
(参考)[https://blog.csdn.net/qq_42033567/article/details/108129645]
(部分题目)[https://blog.csdn.net/IT_studied/article/details/124758936]
1、
console.log('1')
setTimeout(() => {
console.log('4')
setTimeout(() => {
console.log('7')
}, 0)
Promise.resolve()
.then(() => {
console.log('6')
})
console.log('5')
}, 0)
Promise.resolve()
.then(() => {
console.log('3')
})
console.log('2')
输出 1,2,3,4,5,6,7
2、
console.log(1);
setTimeout(()=>console.log(2));
new Promise((resolve, reject)=>{
Promise.resolve(3).then((result)=>{
console.log(result);
});
resolve();
console.log(4);
}).then((result)=>{
console.log(result);
}, (error)=>{
console.log(error);
});
console.log(5);
输出:1 4 5 3 undefined 2
3、
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 1000)
})
const promise2 = promise1.then(() => {
throw new Error('error!!!')
})
console.log('promise1', promise1)
console.log('promise2', promise2)
setTimeout(() => {
console.log('promise1', promise1)
console.log('promise2', promise2)
}, 2000)
输出:
promise1 Promise { }
promise2 Promise { }
(node:50928) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: error!!!
(node:50928) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
promise1 Promise { 'success' }
promise2 Promise {
Error: error!!!
at promise.then (...)
at }
4、
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('once')
resolve('success')
}, 1000)
})
const start = Date.now()
promise.then((res) => {
console.log(res, Date.now() - start)
})
promise.then((res) => {
console.log(res, Date.now() - start)
})
输出: (答案不唯一,promise forEach消耗的时间会有差异)
once
success 1002
success 1002
5、
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
输出:1(.then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透。)
6、
const promise = new Promise((resolve, reject) => {
console.log(1);
setTimeout(() => {
console.log("timerStart");
resolve("success");
console.log("timerEnd");
}, 0);
console.log(2);
});
promise.then((res) => {
console.log(res);
});
console.log(4);
输出:1、2、4、timerStart、timerEnd、success
1、ES6 语法用过哪些,都有哪些常用的特性
function Parent(name) {
this.name = name || 'parent';
}
function Child(name, age) {
Parent.call(this, name); //继承实例属性,第一次调用Parent()
this.age = age;
}
Child.prototype = new Parent(); //继承原型,第二次调用Parent()
Child.prototype.constructor = Child;//修正构造函数为自己本身
寄生组合式继承:所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
function extend(subClass, superClass) {
var prototype = Object(superClass.prototype); // 创建对象,创建父类原型的一个副本
prototype.constructor = subClass; // 增强对象,弥补因重写原型而失去的默认的constructor 属性
subClass.prototype = prototype; // 指定对象,将新创建的对象赋值给子类的原型
}
function Parent(name) {
this.name = 'allen';
}
function Child(name) {
Parent.call(this, name); //继承第一步,继承实例属性,调用Parent()
}
extend(Child, Parent); //继承第二步,不会调用Parent()
3、箭头函数与普通函数的区别
1、js的数据类型都有哪些 ,有什么区别,数据类型常用的判断方式都有哪些,为什么基本数据类型存到栈但是引用数据类型存到堆
基础数据类型:Number、String、Boolean、Null、Undefined、Symbol
引用数据类型:Object、Array、Function
常用判断方式:
2、闭包
闭包就是能够读取其他函数内部变量的函数。可以理解成“定义在一个函数内部的函数“。当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
特点:
3、原型链讲一下
当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__(隐式原型)属性所指向的那个对象(可以理解为父对象)里找,如果父对象也不存在这个属性,则继续往父对象的__proto__属性所指向的那个对象(可以理解为爷爷对象)里找,这种通过__proto__属性来连接对象直到null的一条链即为我们所谓的原型链。
原型链详解
4、esmodule和commonjs区别是什么,还接触过其他的模块化方案么
6、设计模式
前端比较常见的是单例模式、观察者模式、代理模式。
单例模式:指保证一个类仅有一个实例,并提供一个访问它的全局访问点。常见是用于命名空间;Vuex、Redux也采纳了单例模式,两者都用一个全局的惟一Store来存储所有状态。
7、process.env.NODE_ENV是什么?说一下 Process ,以及 Require 原理?
在node中,有全局变量process表示的是当前的node进程。
process.env包含着关于系统环境的信息,但是process.env中并不存在NODE_ENV。
NODE_ENV是一个用户自定义的变量,在webpack中它的用途是判断生产环境或开发环境。
8、Object.create(null)和直接创建一个{}有什么区别
Object.create() 方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型。
Object.create(null) 创建一个空对象,此对象无原型方法。
{} 其实是new Object(),具有原型方法。
10、异步加载js的方式都有哪些
defer,始终在页面渲染后(dom树生成后)再执行js
async,js加载完后立即执行,可能会阻塞渲染
按需加载
11、判断一个对象是否是循环引用对象
循环引用是指对象的地址和源的地址相同,它只会发生在Object等引用类型的数据中
// 循环引用
const a = {};
a.b = a
实现一个方法判断是否是循环引用对象,具体思路是遍历对象的值是否存在与源的地址相同的情况
function cycle(obj, parent) {
//表示调用的父级数组
var parentArr = parent || [obj];
for (var i in obj) {
if (typeof obj[i] === "object") {
//判断是否有循环引用
parentArr.forEach((pObj) => {
if (pObj === obj[i]) {
obj[i] = "[cycle]"
}
});
cycle(obj[i], [...parentArr, obj[i]])
}
}
return obj;
}
12、跨域,img标签为什么没有跨域问题
浏览器要求,在解析Ajax请求时,要求浏览器的路径与Ajax的请求的路径必须满足三个要求,则满足同源策略,可以访问服务器
协议、域名、端口号都相同才为同源,否则会跨域
解决跨域的方法:
this.x = 9; // 在浏览器中,this 指向全局的 "window" 对象
var module = {
x: 81,
getX: function() { return this.x; }
};
module.getX(); // 81
var retrieveX = module.getX;
retrieveX();
// 返回 9 - 因为函数是在全局作用域中调用的
// 创建一个新函数,把 'this' 绑定到 module 对象
var boundGetX = retrieveX.bind(module);
boundGetX(); // 81
1、call、bind、apply使用目的上都是一样的,都是将一个构造方法当作一个普通方法调用;均传递是一个this域对象;均可传递参数。
2、不同点: call、apply直接调用,bind返回一个新函数需手动调用;apply的参数为数组形式,call、bind为单个参数
手写call:(与apply的不同仅为处理参数时, let args = arguments[1] )
Function.prototype.myCall = function(context) {
// 判断是否是undefined和null
if (typeof context === 'undefined' || context === null) {
context = window
}
// call一个参数是其改变指向的对象,后续参数作为实参传递给调用者。所以这里用[...arguments].slice(1)获取到了传递给调用者的函数参数。
let args = [...arguments].slice(1)
context.fn = this // 将调用的函数设置为参数context的方法
let result = context.fn(...args) // 调用函数
delete context.fn // 移除属性
return result // 返回结果
}
手写bind:
Function.prototype.myBind = function(context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
let _this = this
let args = [...arguments].slice(1)
return function F() {
// 判断是否被当做构造函数使用
if (this instanceof F) {
return _this.apply(this, args.concat([...arguments]))
}
return _this.apply(context, args.concat([...arguments]))
}
}
15、Map
Map 数据结构类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
包含以下属性和操作方法:
17、手写instanceof
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上object instanceof constructor;object为 某个实例对象、constructor 为某个构造函数
instanceof可以用于判断复杂数据类型
function myInstanceOf(object, constructor) {
if (obj === null || type obj !== 'object' || typeof constructor !== 'function') return false
let pointer = object._proto_
while (pointer !== null){
if (pointer === constructor.prototype) return true
else pointer = pointer._proto_
}
}
18、0.1 + 0.2 为什么不等于 0.3
因为在 0.1+0.2 的计算过程中发生了两次精度丢失。第一次是在 0.1 和 0.2 转成双精度二进制浮点数时,由于二进制浮点数的小数位只能存储52位,导致小数点后第53位的数要进行为1则进1为0则舍去的操作,从而造成一次精度丢失。第二次在 0.1 和 0.2 转成二进制浮点数后,二进制浮点数相加的过程中,小数位相加导致小数位多出了一位,又要让第53位的数进行为1则进1为0则舍去的操作,又造成一次精度丢失。最终导致 0.1+0.2 不等于0.3 。
19、js 垃圾回收机制
各大浏览器通常采用的垃圾回收有两种方法:标记清除、引用计数
20、和=的区别
1、对于 string、number 等基础类型,== 和 === 是有区别的
a)不同类型间比较,== 之比较 “转化成同一类型后的值” 看 “值” 是否相等,=== 如果类型不同,其结果就是不等。
b)同类型比较,直接进行 “值” 比较,两者结果一样。
2、对于 Array,Object 等高级类型,== 和 === 是没有区别的
进行 “指针地址” 比较
3、基础类型与高级类型,== 和 === 是有区别的
a)对于 ,将高级转化为基础类型,进行 “值” 比较
b)因为类型不同,= 结果为 false