1.异步执行顺序问题
阅读下面代码,我们只考虑浏览器环境下的输出结果,写出它们结果打印的先后顺序,并分析出原因,小伙伴们,加油哦!
console.log("AAAA");
setTimeout(() => console.log("BBBB"), 1000);
const start = new Date();
while (new Date() - start < 3000) { }
console.log("CCCC");
setTimeout(() => console.log("DDDD"), 0);
new Promise((resolve, reject) => {
console.log("EEEE");
foo.bar(100);
})
.then(() => console.log("FFFF"))
.then(() => console.log("GGGG"))
.catch(() => console.log("HHHH"));
console.log("IIII");
答案:
浏览器下 输出结果的先后顺序是
AAAA
CCCC
EEEE
IIII
HHHH
BBBB
DDDD
答案解析:这道题考察重点是 js异步执行 宏任务 微任务。
一开始代码执行,输出AAAA
. 1
第二行代码开启一个计时器t1(一个称呼),这是一个异步任务且是宏任务,需要等到1秒后提交。
第四行是个while语句,需要等待3秒后才能执行下面的代码,这里有个问题,就是3秒后上一个计时器t1的提交时间已经过了,但是线程上的任务还没有执行结束,所以暂时不能打印结果,所以它排在宏任务的最前面了。
第五行又输出CCCC
第六行又开启一个计时器t2(称呼),它提交的时间是0秒(其实每个浏览器器有默认最小时间的,暂时忽略),但是之前的t1任务还没有执行,还在等待,所以t2就排在t1的后面。(t2排在t1后面的原因是while造成的)都还需要等待,因为线程上的任务还没执行完毕。
第七行new Promise
将执行promise函数,它参数是一个回调函数,这个回调函数内的代码是同步的,它的异步核心在于resolve和reject,同时这个异步任务在任务队列中属于微任务,是优先于宏任务执行的,(不管宏任务有多急,反正我是VIP)。所以先直接打印输出同步代码EEEE
。第九行中的代码是个不存在的对象,这个错误要抛给reject这个状态,也就是catch去处理,但是它是异步的且是微任务,只有等到线程上的任务执行完毕,立马执行它,不管宏任务(计时器,ajax等)等待多久了。
第十四行,这是线程上的最后一个任务,打印输出 IIII
我们先找出线程上的同步代码,将结果依次排列出来:AAAA CCCC EEEE IIII
然后我们再找出所有异步任务中的微任务 把结果打印出来 HHHH
最后我们再找出异步中的所有宏任务,这里t1排在前面t2排在后面(这个原因是while造成的),输出结果顺序是 BBBB DDDD
所以综上 结果是 AAAA CCCC EEEE IIII HHHH BBBB DDDD
2.异步执行顺序问题
阅读下面代码,我们只考虑浏览器环境下的输出结果,写出它们结果打印的先后顺序,并分析出原因,小伙伴们,加油哦!
async function async1() {
console.log("AAAA");
async2();
console.log("BBBB");
}
async function async2() {
console.log("CCCC");
}
console.log("DDDD");
setTimeout(function () {
console.log("FFFF");
}, 0);
async1();
new Promise(function (resolve) {
console.log("GGGG");
resolve();
}).then(function () {
console.log("HHHH");
});
console.log("IIII");
答案:
浏览器下 输出结果的先后顺序是
DDDD
AAAA
CCCC
BBBB
GGGG
IIII
HHHH
FFFF
答案解析:这道题考察重点是 js异步执行 宏任务 微任务.
这道题的坑就在于 async中如果没有await,那么它就是一个纯同步函数。
这道题的起始代码在第9行,输出DDDD
第10行计时器开启一个异步任务t1(一个称呼),这个任务且为宏任务。
第13行函数async1
执行,这个函数内没有await 所以它其实就是一个纯同步函数,打印输出AAAA
,
在async1
中执行async2
函数,因为async2
的内部也没有await,所以它也是个纯同步函数,打印输出CCCC
紧接着打印输出BBBB
。
第14行new Promise执行里面的代码也是同步的,所以打印输出GGGG
,resolve()调用的时候开启一个异步任务t2(一个称呼),且这个任务t2是微任务,它的执行交给then()中的第一个回调函数执行,且优先级高于宏任务(t1)执行。
第20行打印输出IIII
,此时线程上的同步任务全部执行结束。
在执行任务队列中的异步任务时,微任务优先于宏任务执行,所以先执行微任务 t2 打印输出 HHHH
,然后执行宏任务 t1 打印输出 FFFF
所以综上 结果输出是 DDDD AAAA CCCC BBBB GGGG IIII HHHH FFFF
3.微任务执行问题
async await
-
问题1
async function t1() { let a = await "lagou"; console.log(a); } t1()
问题解析
await
是一个表达式,如果后面不是一个promise对象,就直接返回对应的值。所以问题1可以理解为
async function t1() { let a = "lagou"; console.log(a);//lagou } t1()
-
问题2
async function t2() { let a = await new Promise((resolve) => {}); console.log(a);// } t2()
问题解析
await
后面如果跟一个promise对象,await将等待这个promise对象的resolve状态的值value,且将这个值返回给前面的变量,此时的promise对象的状态是一个pending状态,没有resolve状态值,所以什么也打印不了。 -
问题3
async function t3() { let a = await new Promise((resolve) => { resolve(); }); console.log(a);//undefined } t3()
await
后面如果跟一个promise对象,await将等待这个promise对象的resolve状态的值value,且将这个值返回给前面的变量,此时的promise对象的状态是一个resolve状态,但是它的状态值是undefined,所以打印出undefined。 -
问题4
async function t4() { let a = await new Promise((resolve) => { resolve("hello"); }); console.log(a);//hello}t4()
await
后面如果跟一个promise对象,await将等待这个promise对象的resolve状态的值,且将这个值返回给前面的变量,此时的promise对象的状态是一个resolve状态,它的状态值是hello,所以打印出hello。 -
问题5
async function t5() { let a = await new Promise((resolve) => { resolve("hello"); }).then(() => { return "lala"; }); console.log(a);//lala}t5()
await
后面如果跟一个promise对象,await将等待这个promise对象的resolve状态的值,且将这个值返回给前面的变量,此时的promise对象的状态是一个resolve状态,它的状态值是hello,紧接着后面又执行了一个then方法,then方法又会返回一个全新的promise对象,且这个then方法中的返回值会作为这个全新的promise中resolve的值,所以最终的结果是lala。 -
问题6
async function t6() { let a = await fn().then((res)=>{return res}) console.log(a);//undefined}async function fn(){ await new Promise((resolve)=>{ resolve("lagou") })}t6()
问题解析
async
函数执行返回一个promise
对象,且async
函数内部的返回值会当作这个promise对象resolve状态的值async function fn() { return "la";}var p = fn();console.log(p); //Promise {<resolved>: "la"} //__proto__: Promise //[[PromiseStatus]]: "resolved" //[[PromiseValue]]: "la"
首先考虑
fn()
执行返回一个promise对象,因为fn执行没有返回值,所以这个promise对象的状态resolve的值是undefined,且将这个undefined当作下一个then中回调函数的参数,所以打印的结果是undefined -
问题7
async function t7() { let a = await fn().then((res)=>{return res}) console.log(a);}async function fn(){ await new Promise((resolve)=>{ resolve("lagou") }) return "lala"}t7()
首先考虑
fn()
执行返回一个promise对象,因为fn()
执行有返回值lala,所以这个promise对象的状态resolve的值是lala,且将这个lala当作下一个then中回调函数的参数,所以打印的结果是lala。
注意细节
-
async函数执行的返回结果是一个promise对象,这个函数的返回值是这个promise状态值resolve的值
-
await后面如果不是一个promise对象,将直接返回这个值
-
await后面如果是一个promise对象,将会把这个promise的状态resolve的值返回出去。
以上没有考虑reject状态。
4.this指向问题
说出并解释下列代码的输出结果:
function Foo() {
getName = function () {
console.log(1);
};
return this;
}
Foo.getName = function () {
console.log(2);
};
Foo.prototype.getName = function () {
console.log(3);
};
var getName = function () {
console.log(4);
};
function getName() {
console.log(5);
}
Foo.getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
参考答案
1、 Foo.getName(); 调用Foo的静态方法,所以,打印2
2、 Foo().getName(); Foo()就是普通函数调用,返回的this是window,后面调用window.getName() 而window下的getName在Foo()中调用getName被重新赋值, 所以, 打印1
3、 getName(); 在执行过Foo().getName()的基础上,所以getName = function () { console.log(1) }, 所以, 打印1,[如果getName()放在Foo().getName()上执行打印结果为4]
4、 new Foo.getName(); 构造器私有属性的getName(), 所以, 打印2
5、 new Foo().getName(); 原型上的getName(),打印3
6、 new new Foo().getName() 首先new Foo()得到一个空对象{ } 第二步向空对象中添加一个属性getName,值为一个函数第三步new { }.getName() 等价于 var bar = new (new Foo().getName)(); console.log(bar)
先new Foo得到的实例对象上的getName方法,再将这个原型上getName方法当做构造函数继续new ,所以执行原型上的方法, 打印3
5.promise执行问题
谈一谈下列两种写法的区别
//第一种
promise.then((res) => {
console.log('then:', res);
})
.catch((err) => {
console.log('catch:', err);
})
// 第二种
promise.then((res) => {
console.log('then:', res);
}, (err) => { console.log('catch:', err); })
参考解析
第一种 catch 方法可以捕获到 catch 之前整条 promise 链路上所有抛出的异常。
第二种 then 方法的第二个参数捕获的异常依赖于上一个 Promise 对象的执行结果。
promise.then(successCb, faildCd) 接收两个函数作为参数,来处理上一个promise 对象的结果。then f 方法返回的是 promise 对象。第一种链式写法,使用catch,相当于给前面一个then方法返回的promise 注册回调,可以捕获到前面then没有被处理的异常。第二种是回调函数写法,仅为为上一个promise 注册异常回调。
如果是promise内部报错 reject 抛出错误后,then 的第二个参数就能捕获得到,如果then的第二个参数不存在,则catch方法会捕获到。
如果是then的第一个参数函数 resolve 中抛出了异常,即成功回调函数出现异常后,then的第二个参数reject 捕获捕获不到,catch方法可以捕获到。
6.promise请求并发问题
var urls = ['http://jsonplaceholder.typicode.com/posts/1', 'http://jsonplaceholder.typicode.com/posts/2', 'http://jsonplaceholder.typicode.com/posts/3', 'http://jsonplaceholder.typicode.com/posts/4', 'http://jsonplaceholder.typicode.com/posts/5', 'http://jsonplaceholder.typicode.com/posts/6', 'http://jsonplaceholder.typicode.com/posts/7', 'http://jsonplaceholder.typicode.com/posts/8', 'http://jsonplaceholder.typicode.com/posts/9', 'http://jsonplaceholder.typicode.com/posts/10']\
function loadDate(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.onload = function () {
resolve(xhr.responseText)
}
xhr.open('GET', url)
xhr.send()
})
}
在 urls
数组中存放了 10 个接口地址。同时还定义了一个 loadDate
函数,这个函数接受一个 url
参数,返回一个 Promise
对象,该 Promise
在接口调用成功时返回 resolve
,失败时返回 reject
。
要求:任意时刻,同时下载的链接数量不可以超过 3 个。
试写出一段代码实现这个需求,要求尽可能快速地将所有接口中的数据得到。
解题思路
按照题意我们可以这样做,首先并发请求 3 个 url
中的数据,当其中一条 url
请求得到数据后,立即发起对一条新 url
上数据的请求,我们要始终让并发数保持在 3 个,直到所有需要加载数据的 url
全部都完成请求并得到数据。
用 Promise 实现的思路就是,首先并发请求3个 url
,得到 3 个 Promise
,然后组成一个叫 promises
的数组。再不断的调用 Promise.race
来返回最快改变状态的 Promise
,然后从数组promises
中删掉这个 Promise
对象,再加入一个新的 Promise
,直到所有的 url
被取完,最后再使用 Promise.all
来处理一遍数组promises
中没有改变状态的 Promise
。
参考答案
var urls = ['http://jsonplaceholder.typicode.com/posts/1', 'http://jsonplaceholder.typicode.com/posts/2', 'http://jsonplaceholder.typicode.com/posts/3', 'http://jsonplaceholder.typicode.com/posts/4', 'http://jsonplaceholder.typicode.com/posts/5', 'http://jsonplaceholder.typicode.com/posts/6', 'http://jsonplaceholder.typicode.com/posts/7', 'http://jsonplaceholder.typicode.com/posts/8', 'http://jsonplaceholder.typicode.com/posts/9', 'http://jsonplaceholder.typicode.com/posts/10']
function loadDate(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.onload = function () {
resolve(xhr.responseText)
}
xhr.open('GET', url)
xhr.send()
})
}
function limitLoad(urls, handler, limit) {
// 对数组进行一个拷贝
const sequence = [].concat(urls)
let promises = [];
//实现并发请求达到最大值
promises = sequence.splice(0, limit).map((url, index) => {
// 这里返回的 index 是任务在数组 promises 的脚标
//用于在 Promise.race 后找到完成的任务脚标
return handler(url).then(() => {
return index
});
});
// 利用数组的 reduce 方法来以队列的形式执行
return sequence.reduce((last, url, currentIndex) => {
return last.then(() => {
// 返回最快改变状态的 Promise
return Promise.race(promises)
}).catch(err => {
// 这里的 catch 不仅用来捕获前面 then 方法抛出的错误
// 更重要的是防止中断整个链式调用
console.error(err)
}).then((res) => {
// 用新的 Promise 替换掉最快改变状态的 Promise
promises[res] = handler(sequence[currentIndex]).then(
() => { return res });
})
}, Promise.resolve()).then(() => { return Promise.all(promises) })
}
limitLoad(urls, loadDate, 3)
//因为 loadDate 函数也返回一个 Promise所以当 所有图片加载完成后可以继续链式调用
limitLoad(urls, loadDate, 3).then(() => {
console.log('所有url数据请求成功');
})
.catch(err => { console.error(err); })
7.跨域问题
- API跨域可以通过服务器上nginx反向代理
- 本地webpack dev server可以设置 proxy,
- new Image, 设src 的时候,图片需要设置Cors
cors需要后台配合设置HTTP响应头,如果请求不是简单请求(1. method:get,post,2. content-type:三种表单自带的content-type,3. 没有自定义的HTTP header),浏览器会先发送option预检请求,后端需要响应option请求,然后浏览器才会发送正式请求,cors通过白名单的形式允许指定的域发送请求
jsonp是浏览器会放过 img script标签引入资源的方式。所以可以通过后端返回一段执行js函数的脚本,将数据作为参数传入。然后在前端执行这段脚本。双方约定一个函数的名称。
联调的时候会需要跨域,线上前端站点域和后台接口不一致也需要跨域,开发时跨域可以通过代理服务器来转发请求,因为跨域本身是浏览器对请求的限制,常见的跨域处理还有JSONP和cors,jsonp是利用脚本资源请求本身就可以跨域的特性,通过与请求一起发送回调函数名,后台返回script脚本直接执行回调,但是由于资源请求是get类型,请求参数长度有限制,也不能进行post请求。cors需要后台配合设置HTTP响应头,如果请求不是简单请求(1. method:get,post,2. content-type:三种表单自带的content-type,3. 没有自定义的HTTP header),浏览器会先发送option预检请求,后端需要响应option请求,然后浏览器才会发送正式请求,cors通过白名单的形式允许指定的域发送请求
同源策略只是浏览器客户端的防护机制,当发现非同源HTTP请求时会拦截响应,但服务器依然处理了这个请求。
服务器端不拦截,所以在同源服务器下做代理,可以实现跨域。
我之前这么看的node中间层处理跨域。
8.Sourcemap问题
Sourcemap是什么?有什么作用?在生产环境怎么用?
Sourcemap
本质上是一个信息文件,里面储存着代码转换前后的对应位置信息。它记录了转换压缩后的代码所对应的转换前的源代码位置,是源代码和生产代码的映射。 Sourcemap
解决了在打包过程中,代码经过压缩,去空格以及 babel
编译转化后,由于代码之间差异性过大,造成无法debug
的问题,简单说 Sourcemap
构建了处理前以及处理后的代码之间的一座桥梁,方便定位生产环境中出现 bug
的位置。因为现在的前端开发都是模块化、组件化的方式,在上线前对 js 和 css 文件进行合并压缩容易造成混淆。如果对这样的线上代码进行调试,肯定不切实际,sourceMap
的作用就是能够让浏览器的调试面版将生成后的代码映射到源码文件当中,开发者可以在源码文件中 debug,这样就会让程序员调试轻松、简单很多。
Sourcemap
的种类有很多, 在生产环境下可以用process.env
判断一下。 webpack
中可以在devtool
中设置, 在开发环境中可以配置devtool
为cheap-module-source-map
,方便调试。生产环境下建议采用none
的方式,这样做不暴露源代码。或者是nosources-source-map
的方式,既可以定位源代码位置,又不暴露源代码
9.this拔高面试题
各位小伙伴,今天的面试时刻到啦!下面有两道面试题,大家思考之后给出相应的结果并说明理由(只考虑浏览器环境)
第一题:写出打印结果,并分析出原因
var length = 10;
function fn() {
console.log(this.length);
}
var obj = {
length: 5,
method: function (fn) {
fn();
arguments[0]();
}
};
obj.method(fn, 1);
解析:首先,我们在全局定义了一个变量length、一个对象obj和一个函数fn,length赋值为10。接下来是fn函数,输出this.length。对象obj中,obj.length是5,obj.method是一个函数。method函数里面的形参也是一个函数,这个函数里面调用了fn函数,arguments是一个伪数组,代表method函数实际接收到的参数列表,所以arguments[0] ()就代表了调用arguments里的第一项。obj.method(fn, 1)代表的就是调用obj当中的method函数,并且传递了两个参数,fn和1。
分析完了代码的含义,我们来看输出结果。method函数当中调用的fn函数是全局当中的函数,所以this指向的是window,this.length就是10。上面说了,arguments[0] ()代表的是调用arguments里面的第一项,也就是传参进来的fn,所以这个this指向的是arguments,method函数接收的参数是两个,所以arguments.length就是2。最后的输出结果就是 10 2
第二题:写出打印结果,并分析出原因
function a(xx) {
this.x = xx;
return this;
};
var x = a(5);
var y = a(6);
console.log(x.x);
console.log(y.x);
解析:首先,我们在全局定义了一个变量x、一个变量y和一个函数a,函数a当中的this.x等于接收到的参数,返回this,这里要注意,返回的不是this.x,而是this。接下来我们给x赋值,值为a(5),又给y进行赋值,值为a(6)。最后,我们输出x.x,y.x。
分析完代码的含义,我们来看输出结果。a函数传了一个参数5,那么this.x就被赋值为了5,函数a的this指向的是window,也就是window.x = 5。上面我们说过,这个函数返回的是this,也就是this指向的window,x = a(5)就相当于window.x = window,此时的x被赋值为了window。下面又执行了y = a(6),也就是说,x的值再次发生了改变,边为了6,y则被赋值为了window。console.log(x.x)就相当于console.log(6.x),输出的自然是undefined。console.log(y.x),输出的相当于是console.log(window.x),得到的值自然是6。最后输出的结果为 undefined 6