# js 进阶面试题
# 1、实现图片懒加载的思路
判断图片所在位置是否在可视区域内,图片移到可视区域内进行加载,提供三种判断方法:
- offsetTop < clientHeight + scrollTop
- element.getBoundingClientRect().top < clientHeight
- IntersectionObserver
方案一:clientHeight、scrollTop 和 offsetTop
首先给图片一个占位资源:
<img src="default.jpg" data-src="http://www.xxx.com/target.jpg" />
接着,通过监听 scroll 事件来判断图片是否到达视口:
let img = document.getElementsByTagName("img");
let num = img.length;
let count = 0;//计数器,从第一张图片开始计
lazyload();//首次加载别忘了显示图片
window.addEventListener('scroll', lazyload);
function lazyload() {
let viewHeight = document.documentElement.clientHeight;//视口高度
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;//滚动条卷去的高度
for(let i = count; i <num; i++) {
// 元素现在已经出现在视口中
if(img[i].offsetTop < scrollHeight + viewHeight) {
if(img[i].getAttribute("src") !== "default.jpg") continue;
img[i].src = img[i].getAttribute("data-src");
count ++;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
当然,最好对 scroll 事件做节流处理,以免频繁触发:
// throttle函数我们上节已经实现
window.addEventListener('scroll', throttle(lazyload, 200));
2
方案二:getBoundingClientRect
现在我们用另外一种方式来判断图片是否出现在了当前视口, 即 DOM 元素的 getBoundingClientRect API。
上述的 lazyload 函数改成下面这样:
function lazyload() {
for(let i = count; i <num; i++) {
// 元素现在已经出现在视口中
if(img[i].getBoundingClientRect().top < document.documentElement.clientHeight) {
if(img[i].getAttribute("src") !== "default.jpg") continue;
img[i].src = img[i].getAttribute("data-src");
count ++;
}
}
}
2
3
4
5
6
7
8
9
10
方案三: IntersectionObserver
这是浏览器内置的一个API,实现了监听window的scroll事件、判断是否在视口中以及节流三大功能。
let img = document.getElementsByTagName("img");
const observer = new IntersectionObserver(changes => {
//changes 是被观察的元素集合
for(let i = 0, len = changes.length; i < len; i++) {
let change = changes[i];
// 通过这个属性判断是否在视口中
if(change.isIntersecting) {
const imgElement = change.target;
imgElement.src = imgElement.getAttribute("data-src");
observer.unobserve(imgElement);
}
}
})
Array.from(img).forEach(item => observer.observe(item));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这样就很方便地实现了图片懒加载,当然这个IntersectionObserver也可以用作其他资源的预加载,功能非常强大。
# 2、表单可以跨域吗?
表单提交是可以进行跨域的,不受浏览器的同源策略限制,估计是历史遗留原因,也有可能是表单提交的结果 js 是拿不到的,所以不限制问题也不大。但是存在一个问题,就是 csrf 攻击,具体不展开了,因为可以自动带上 cookie 造成攻击成功,而 cookie 的新属性 SameSite 就能用来限制这种情况。
# 3、为什么typeof可以检测类型,有没有更好的方法
typeof 一般被用于判断一个变量的类型,我们可以利用 typeof 来判断number, string, object, boolean, function, undefined, symbol 这七种类型,这种判断能帮助我们搞定一些问题,js 在底层存储变量的时候会在变量的机器码的低位 1-3 位存储其类型信息(000:对象,010:浮点数,100:字符串,110:布尔,1:整数),但是 null 所有机器码均为 0,直接被当做了对象来看待。
那么有没有更好的办法区分类型呢,一般使用Object.prototype.toString.call()。
# 4、什么是事件委托,它有什么好处?
事件委托是利用事件冒泡机制处理指定一个事件处理程序,来管理某一类型的所有事件 利用冒泡的原理,将事件加到父级身上,触发执行效果,这样只在内存中开辟一块空间,既节省资源又减少 DOM 操作,提高性能 动态绑定事件,列表新增元素不用进行重新绑定了。
# 5、使用js如何改变url,并且页面不刷新?
改变URL的目的是让 js 拿到不同的参数,进行不同的页面渲染,其实就是 vue-router 的原理 最简单的就是改变 hash,改变 hash 是并不会刷新页面的,也会改变URL,也能监听 hashchange 事件进行页面的渲染 还有一种就是使用 HTML5 的 history.pushState() 方法,该方法也可以改变 url 然后不刷新页面。
# 6、以下代码输出结果?
console.log('script start');
const dog = new Promise(function(resolve) {
console.log('dog1');
resolve();
console.log("promiseResolve")
}).then(function() {
console.log('dog2');
return "dog"
}).then(console.log("dog end")); // 注意这里没有回调
const cat = new Promise(function(resolve) {
console.log('cat1');
resolve();
setTimeout(() => {
console.log('setTimeout1')
})
}).then(function() {
console.log('cat2');
return "cat"
})
setTimeout(function() {
console.log('setTimeout2');
}, 0)
console.log("before promise.race")
Promise.race([dog, cat])
.then((one, two) => {
console.log("one", one)
console.log("two", two)
})
.catch(err => {
console.error('err', err);
})
console.log('script end');
/*
script start
dog1
promiseResolve
dog end
cat1
before promise.race
script end
dog2
cat2
one cat
two undefined
setTimeout1
setTimeout2
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 7、以下代码输出结果?
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
new Promise(function(resolve) {
console.log('promise1');
resolve();
console.log("promiseResolve")
}).then(function() {
setTimeout(function() {
console.log('setTimeout1');
})
console.log('promise2');
});
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout2');
}, 0)
async1();
process.nextTick(() => {
console.log("nextTick");
})
new Promise(function(resolve) {
console.log('promise3');
resolve();
setTimeout(() => {
console.log('setTimeout3')
})
}).then(function() {
console.log('promise4');
})
.then(() => console.log('promise5'))
.then(() => console.log('promise6'))
console.log('script end');
/*
script start
async1 start
promise1
promiseResolve
promise3
nextTick // nextTick有自己的队列,优先于其它微任务先执行
promise2
script end
async1 end
promise4
promise5
promise6
setTimeout2
setTimeout3
setTimeout1
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 8、有N个请求,每次发送一个,如果有一个失败了,就不再执行后面的请求并返回直到上一个请求的所有结果,否则返回所有结果
// 我这里用异步宏任务模拟请求
const result = []
const arr = [1,2,3,4,5,6,7]
function call(data){
return new Promise((res, rej) => {
setTimeout(() => {
if(data === "5"){
rej(data)
} else {
res(data)
}
}, 1000)
})
}
function foo(){
return call(arr.splice(0, 1).join(""))
.then((response) => {
console.log("response", response)
result.push(response)
if(arr.length){
return foo()
} else {
console.log("result", result)
return result
}
})
.catch(err => {
console.log("result", result)
return result
})
}
foo()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 9、写一段柯里化函数实现下面的功能
在开始之前,我们首先需要搞清楚函数柯里化的概念。
函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
function add(a, b, c){
return a+b+c
}
curry(add)(1)(2)(3)
curry(add, 1)(2)(3)
curry(add, 1, 2)(3)
curry(add, 1, 2, 3)
2
3
4
5
6
7
答案:
function curry(fn, ...args) {
var length = fn.length;
var args = args || [];
return function(){
newArgs = args.concat(Array.prototype.slice.call(arguments));
if (newArgs.length < length) {
return curry.call(this,fn,...newArgs);
}else{
return fn.apply(this,newArgs);
}
}
}
console.log(curry(add, 1, 2, 3))
2
3
4
5
6
7
8
9
10
11
12
13
const curry = (fn, ...args) => {
args.length < fn.length
// 参数长度不足时,重新柯里化该函数,等待接收新参数
? (...arguments) => curry(fn, ...args, ...arguments)
// 参数长度满足时,执行函数
: fn(...args)
}
2
3
4
5
6
7
函数柯里化的主要作用:
- 参数复用
- 提前返回 – 返回接受余下的参数且返回结果的新函数
- 延迟执行 – 返回新函数,等待执行
# 10、实现一个方法,用于比较两个版本号(version1、version2)
- 如果version1 > version2,返回1;如果version1 < version2,返回-1,其他情况返回0
- 版本号规则
x.y.z
,xyz
均为大于等于0的整数,至少有x位 - 示例:
- compareVersion('0.1', '1.1.1'); // 返回-1
- compareVersion('13.37', '1.2 '); // 返回1
- compareVersion('1.1', '1.1.0'); // 返回0 */
function compareVersion(version1, version2){
const version1Arr = version1.split(".")
const version2Arr = version2.split(".")
const length = Math.max(version1Arr.length, version2Arr.length)
for(let i=0; i < length; i++){
if(i<(length-1)){
if(Number(version1Arr[i]) > Number(version2Arr[i])){
return 1
} else if(Number(version1Arr[i]) < Number(version2Arr[i])){
return -1
}
} else {
const firstThird = version1Arr[length-1] || 0
const SecondThird = version2Arr[length-1] || 0
if(firstThird > SecondThird){
return 1
} else if(firstThird < SecondThird){
return -1
} else {
return 0
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 11、有一个对象X,内容如下,请写程序找出所有d的值
const X = {
y: {
c: {
d: 1
}
},
z: {
e: {
d: 2
}
},
q: {
f: {
d: 3
}
},
o: {
m: {
g: {
h: {
d: 4
}
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 答案
const resultArr = []
function lookup(obj){
for(let i in obj){
if(Object.prototype.toString.call(obj[i]) === '[object Object]'){
lookup(obj[i])
} else if(i === "d"){
resultArr.push(obj.d)
}
}
}
lookup(X)
console.log("resultArr", resultArr)
2
3
4
5
6
7
8
9
10
11
12
13
# 12、实现以下功能,已知有 N 个 url,每次最多可并行请求 M,尽快完成对 N 个 url 的请求并保存所有返回结果到数组中。(每次发M个请求,每完成一个请求就发送一个剩下的请求)
const urls = ["1", '2', '3', '4', '5', '6', '7']
const results = []
const promises = []
function getData(url) {
return fetch(url)
.then((response) => {
results.push(response)
if(urls.length) {
return getData(urls.splice(0, 1).join(""))
}
})
}
urls.splice(0, M).forEach(item => {
promises.push(getData(item))
})
Promise.all(promises)
.then(() => {
console.log('results', results)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 13、怎么计算组件在视口内出现了几次?IntersectionObserver 怎么使用的?怎么知道一个 DOM 节点出现在视口内?
要了解某个元素是否进入了"视口"(viewport),即用户能不能看到它,传统的实现方法是,监听到 scroll 事件后,调用目标元素的getBoundingClientRect()方法,得到它对应于视口左上角的坐标,再判断是否在视口之内。然后声明一个全局变量,每出现一次就加一,就可以得出在视口出现了几次。这种方法的缺点是,由于 scroll 事件密集发生,计算量很大,容易造成性能问题。
于是便有了 IntersectionObserver API
IntersectionObserver
var io = new IntersectionObserver(callback, option);
上面代码中,IntersectionObserver 是浏览器原生提供的构造函数,接受两个参数:callback 是可见性变化时的回调函数,option 是配置对象(该参数可选)。
构造函数的返回值是一个观察器实例。实例的 observe 方法可以指定观察哪个 DOM 节点。
// 开始观察
io.observe(document.getElementById("example"));
// 停止观察
io.unobserve(element);
// 关闭观察器
io.disconnect();
2
3
4
5
6
7
8
上面代码中,observe的参数是一个 DOM 节点对象。如果要观察多个节点,就要多次调用这个方法。
io.observe(elementA);
io.observe(elementB);
2
Callback 参数
目标元素的可见性变化时,就会调用观察器的回调函数callback。 callback一般会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。
var io = new IntersectionObserver((entries) => {
console.log(entries);
});
2
3
callback 函数的参数(entries)是一个数组,每个成员都是一个 IntersectionObserverEntry 对象。如果同时有两个被观察的对象的可见性发生变化,entries 数组就会有两个成员。
IntersectionObserverEntry 对象提供目标元素的信息,一共有六个属性。
{
time: 3893.92,
rootBounds: ClientRect {
bottom: 920,
height: 1024,
left: 0,
right: 1024,
top: 0,
width: 920
},
boundingClientRect: ClientRect {
// ...
},
intersectionRect: ClientRect {
// ...
},
intersectionRatio: 0.54,
target: element
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
每个属性的含义如下:
- time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
- target:被观察的目标元素,是一个 DOM 节点对象
- rootBounds:根元素的矩形区域的信息,getBoundingClientRect() 方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回 null
- boundingClientRect:目标元素的矩形区域的信息
- intersectionRect:目标元素与视口(或根元素)的交叉区域的信息
- intersectionRatio:目标元素的可见比例,即intersectionRect占boundingClientRect的比例,完全可见时为1,完全不可见时小于等于0
Option 对象
IntersectionObserver构造函数的第二个参数是一个配置对象。它可以设置以下属性:
- threshold 属性
- root 属性、rootMargin 属性
# 14、浏览器为什么要阻止跨域请求?如何解决跨域?每次跨域请求都需要到达服务端吗?
什么是跨域
跨域是针对浏览器的“同源策略”提出的说法。之所以有“同源策略”这种模式是基于网络安全方面的考虑。所谓的同源策略关注三点:
协议 (http:www.baidu.com & https.www.baidu.com http 协议不同,跨域) 域名 (https://www.aliyun.com & https://developer.aliyun.com 域名不同,跨域) 端口 (http://localhost:8080 & http://localhost:8000 端口号不同,跨域)
哪些网络资源涉及到跨域
“同源策略”对于跨域网络资源的设定非常的清晰。这些场景涉及到跨域禁止操作:
- 无法获取非同源网页的 cookie、localstorage 和 indexedDB。
- 无法访问非同源网页的 DOM (iframe)。
- 无法向非同源地址发送 AJAX 请求 或 fetch 请求(可以发送,但浏览器拒绝接受响应)。
为什么要阻止跨域呢?上文我们说过是基于安全策略:比如一个恶意网站的页面通过 iframe 嵌入了银行的登录页面(二者不同源),如果没有同源限制,恶意网页上的 javascript 脚本就可以在用户登录银行的时候获取用户名和密码。
如何解决跨域
针对跨越问题我们该如何解决,主流的方案有以下:
- 通过 jsonp 跨域
- document.domain + iframe 跨域
- location.hash + iframe
- window.name + iframe 跨域
- postMessage 跨域
- 跨域资源共享(CORS)
- nginx 代理跨域
- nodejs 中间件代理跨域
- WebSocket 协议跨域
关于跨域需要明确的问题
跨域并非浏览器限制了发起跨站请求,而是跨站请求可以正常发起,但是返回结果被浏览器拦截了。
每次需求都会发出,服务器端也会做出响应,只是浏览器端在接受响应的时候会基于同源策略进行拦截。
注意:有些浏览器不允许从 HTTPS 的域跨域访问 HTTP,比如 Chrome 和 Firefox,这些浏览器在请求还未发出的时候就会拦截请求,这是一个特例。
# 15、JSONP 的原理是什么?
尽管浏览器有同源策略,但是 <script>
标签的 src 属性不会被同源策略所约束,可以获取任意服务器上的脚本并执行。jsonp 通过插入 script 标签的方式来实现跨域,参数只能通过 url 传入,仅能支持 get 请求。
实现原理:
- Step1: 创建 callback 方法
- Step2: 插入 script 标签
- Step3: 后台接受到请求,解析前端传过去的 callback 方法,返回该方法的调用,并且数据作为参数传入该方法Step4: 前端执行服务端返回的方法调用
jsonp 实现:
function jsonp ({url, params, callback}) {
return new Promise((resolve, reject) => {
// 创建 script 标签
let script = document.createElement('script')
// 将函数挂在 window 上
window[callback] = function (data) {
resolve(data)
// 代码执行后,删除 script 标签
document.body.removeChild(script)
}
// 回调函数加在请求地址上
params = {...params, callback} // wb=b&callback=show
let arrs = []
for (let key in params) {
array.push(`${key}=${params[key]}`)
}
script.src = `${url}?${arrs.join('&')}`
document.body.appendChild(script)
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
使用
function show(data) {
console.log(data)
}
jsonp({
url: 'http://localhost:3000/show',
params:{
// code 参数
},
callback: 'show'
}).then(data => {
console.log(data)
})
2
3
4
5
6
7
8
9
10
11
12
# 16、深拷贝实现
深拷贝最简单的实现是: JSON.parse(JSON.stringify(obj))
JSON.parse(JSON.stringify(obj))
是最简单的实现方式,但是有一些缺陷:
- 对象的属性值是函数时,无法拷贝。
- 原型链上的属性无法拷贝
- 不能正确的处理 Date 类型的数据
- 不能处理 RegExp
- 会忽略 symbol
- 会忽略 undefined
实现一个 deepClone 函数:
- 如果是基本数据类型,直接返回
- 如果是 RegExp 或者 Date 类型,返回对应类型
- 如果是复杂数据类型,递归。
- 考虑循环引用的问题
// 递归拷贝
function deepClone(obj, hash = new WeakMap()) {
if (obj instanceof RegExp) return new RegExp(obj)
if (obj instanceof Date) return new Date(obj)
if (obj === null || typeof obj !== 'object') {
// 如果不是复杂数据类型,直接返回
return obj
}
if(hash.has(obj)) {
// 如果已经处理过相同的对象,直接获取(解决循环引用)
return hash.get(obj)
}
/**
* 如果 obj 是数组,那么 obj.constructor 是 [Function: Array]
* 如果 obj 是对象,那么 obj.constructor 是 [Function: Object]
*/
let t = new obj.constructor()
hash.set(obj, t)
for (let ikey in obj) {
// 递归
if (obj.hasOwnProperty(key)) { // 是否是自身的属性
t[key] = deepClone(obj[key], hash)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 17、深拷贝怎么解决循环引用问题
看个例子:
function deepCopy(obj){
const res = Array.isArray(obj) ? [] : {};
for(let key in obj){
if(typeof obj[key] === 'object'){
res[key] = deepCopy(obj[key]);
}else{
res[key] = obj[key];
}
}
return res
}
var obj = {
a:1,
b:2,
c:[1,2,3],
d:{aa:1,bb:2},
};
obj.e = obj;
console.log('obj',obj); // 不会报错
const objCopy = deepCopy(obj);
console.log(objCopy); //Uncaught RangeError: Maximum call stack size exceeded
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
从例子可以看到,当存在循环引用的时候,deepCopy会报错,栈溢出。
obj对象存在循环引用时,打印它时是不会栈溢出
深拷贝obj时,才会导致栈溢出
循环应用问题解决
即:目标对象存在循环应用时报错处理
大家都知道,对象的 key 是不能是对象的。
{{a:1}:2}
// Uncaught SyntaxError: Unexpected token ':'
2
参考解决方式一:使用weekmap:
解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系 这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型, 我们可以选择 WeakMap 这种数据结构:
- 检查 WeakMap 中有无克隆过的对象
- 有,直接返回
- 没有,将当前对象作为key,克隆对象作为value进行存储
- 继续克隆
function isObject(obj) {
return (typeof obj === 'object' || typeof obj === 'function') && obj !== null
}
function cloneDeep(source, hash = new WeakMap()) {
if (!isObject(source)) return source;
if (hash.has(source)) return hash.get(source); // 新增代码,查哈希表
var target = Array.isArray(source) ? [] : {};
hash.set(source, target); // 新增代码,哈希表设值
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (isObject(source[key])) {
target[key] = cloneDeep(source[key], hash); // 新增代码,传入哈希表
} else {
target[key] = source[key];
}
}
}
return target;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
参考解决方式二:
可以用 Set,发现相同的对象直接赋值,也可用 Map:
const o = { a: 1, b: 2 };
o.c = o;
function isPrimitive(val) {
return Object(val) !== val;
}
const set = new Set();
function clone(obj) {
const copied = {};
for (const [key, value] of Object.entries(obj)) {
if (isPrimitive(value)) {
copied[key] = value;
} else {
if (set.has(value)) {
copied[key] = { ...value };
} else {
set.add(value);
copied[key] = clone(value);
}
}
}
return copied;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 18、 编写一个程序将数组扁平化去并除其中重复部分数据,最终得到一个升序且不重复的数组
Array.from(new Set(arr.flat(Infinity))).sort((a,b)=>{ return a-b})
# 19、描述一下 Promise:
Promise 是一个对象,它代表了一个异步操作的最终完成或者失败。由于它的 then 方法和 catch、finally 方法会返回一个新的 Promise 所以可以允许我们链式调用,解决了传统的回调地狱问题。
关于 then 以及 catch 方法:
- Promise的状态一经改变就不能再改变。
- .then和.catch都会返回一个新的Promise。
- catch不管被连接到哪里,都能捕获上层未捕捉过的错误。
- 在Promise中,返回任意一个非 promise 的值都会被包裹成 promise 对象,例如return 2会被包装为return Promise.resolve(2)。
- Promise 的 .then 或者 .catch 可以被调用多次, 但如果Promise内部的状态一经改变,并且有了一个值,那么后续每次调用.then或者.catch的时候都会直接拿到该值。
- .then 或者 .catch 中 return 一个 error 对象并不会抛出错误,所以不会被后续的 .catch 捕获。
- .then 或 .catch 返回的值不能是 promise 本身,否则会造成死循环。
- .then 或者 .catch 的参数期望是函数,传入非函数则会发生值透传。
- .then方法是能接收两个参数的,第一个是处理成功的函数,第二个是处
- 失败的函数,再某些时候你可以认为catch是.then第二个参数的简便写法。
- .finally方法也是返回一个Promise,他在Promise结束的时候,无论结果为resolved还是rejected,都会执行里面的回调函数。
finally方法:
- .finally()方法不管Promise对象最后的状态如何都会执行
- .finally()方法的回调函数不接受任何的参数,也就是说你在.finally()函数中是没法知道Promise最终的状态是resolved还是rejected的
- 它最终返回的默认会是一个上一次的Promise对象值,不过如果抛出的是一个异常则返回异常的Promise对象。
最后可以说一下all以及race方法:
- Promise.all()的作用是接收一组异步任务,然后并行执行异步任务,并且在所有异步操作执行完后才执行回调。
- .race()的作用也是接收一组异步任务,然后并行执行异步任务,只保留取第一个执行完成的异步操作的结果,其他的方法仍在执行,不过执行结果会被抛弃。
- Promise.all().then()结果中数组的顺序和Promise.all()接收到的数组顺序一致。
- all和race传入的数组中如果有会抛出异常的异步任务,那么只有最先抛出的错误会被捕获,并且是被then的第二个参数或者后面的catch捕获;但并不会影响数组中其它的异步任务的执行。
# 20、手动实现以下 promiseAll和allSeleted
Promise.all = function(promises) {
const values = []
let count = 0
return new Promise((resolve, reject) => {
promises.forEach((promise, index) => {
Promise.resolve(promise).then(res => {
count++
values[index] = res
if (count === promises.length) {
resolve(values)
}
}, err => {
reject(err)
})
})
})
}
Promise.allSeleted = function(promises) {
let count = 0
let result = []
return new Promise((resolve, reject) => {
promises.forEach((promise, index) => {
Promise.resolve(promise).then(res => {
result[index] = {
value: res,
reason: null,
}
}, err => {
result[index] = {
value: null,
reason: err,
}
}).finally(() => {
count++
if (count === promises.length) {
resolve(result)
}
})
})
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 21、Promise.allSettled 了解吗?手写 Promise.allSettled
- Promise.allSettled() 方法接受一组 Promise 实例作为参数,返回一个新的 Promise 实例。
- 只有等到所有这些参数实例都返回结果,不管是 fulfilled 还是 rejected ,包装实例才会结束。
- 返回的新 Promise 实例,一旦结束,状态总是 fulfilled ,不会变成 rejected 。
- 新的 Promise 实例给监听函数传递一个数组 results 。该数组的每个成员都是一个对象,对应传入 Promise.allSettled的 Promise 实例。每个对象都有 status 属性,对应着 fulfilled 和 rejected 。 fulfilled 时,对象有 value 属性, rejected 时有 reason 属性,对应两种状态的返回值。
- 有时候我们不关心异步操作的结果,只关心这些操作有没有结束时,这个方法会比较有用。
手写看上题
# 如何实现大文件上传?
- 前端上传大文件时使用 Blob.prototype.slice 将文件切片,并发上传多个切片,最后发送一个合并的请求通知服务端合并切片
- 服务端接收切片并存储,收到合并请求后使用流将切片合并到最终文件
- 原生 XMLHttpRequest 的 upload.onprogress 对切片上传进度的监听
- 使用 Vue 计算属性根据每个切片的进度算出整个文件的上传进度
断点续传
- 使用 spark-md5 根据文件内容算出文件 hash
- 通过 hash (根据文件内容生成)可以判断服务端是否已经上传该文件,从而直接提示用户上传成功(秒传)
- 通过 XMLHttpRequest 的 abort 方法暂停切片的上传
- 上传前服务端返回已经上传的切片名,前端跳过这些切片的上传
# 表单可以跨域么?
在普通的 HTML 表单中,直接提交表单数据是不支持跨域的。这是由于浏览器的同源策略所限制的安全机制。同源策略要求网页只能与同一源(协议、域名和端口号相同)的资源进行交互,以防止恶意网站获取用户的敏感信息。
当表单的目标地址(action)指向不同的域名时,浏览器会阻止直接提交表单,而是会产生一个跨域请求的警告或错误。这种情况下,你需要采取其他方法来处理跨域表单提交。
以下是几种常见的跨域表单提交的解决方案:
- 代理:可以通过在同一域名下设置代理服务器,将跨域请求转发到目标域名上。例如,你可以在服务器端设置一个代理脚本,将表单数据发送到目标地址,并将响应返回给客户端。
- JSONP:如果目标域名支持 JSONP(JSON with Padding),你可以通过创建一个包含表单数据的动态
<script>
标签,并将目标地址作为脚本的src
属性值,从而实现跨域请求。 - CORS:如果目标域名允许跨域请求,并在响应中设置了适当的 CORS(跨域资源共享)头部,那么浏览器可以发送跨域请求,并将表单数据提交到目标域名。
需要注意的是,以上解决方案都需要在目标域名的服务器端进行相应的配置或支持。具体的实施方法取决于你的项目需求和后端技术栈。
# 观察者和订阅-发布的区别,各⾃⽤在哪⾥?
观察者模式(Observer Pattern)是一种对象间的一对多依赖关系,其中一个对象(称为主题或可观察对象)维护一个对象列表(称为观察者),并在状态发生变化时通知所有观察者。观察者模式中的主题和观察者之间是直接耦合的。主题提供注册、注销和通知观察者的方法,而观察者定义了接收通知并执行相应操作的方法。观察者模式常用于事件处理、GUI 界面、消息系统等场景。
订阅-发布模式(Publish-Subscribe Pattern)也是一种对象间的一对多依赖关系,但通过引入一个中间件(通常称为消息代理或事件总线)来解耦发布者和订阅者。发布者(或称为生产者)将消息发布到中间件,而订阅者(或称为消费者)通过订阅感兴趣的消息类型来接收消息。订阅-发布模式中,发布者和订阅者之间并不直接依赖,它们通过中间件进行通信。这种解耦使得发布者和订阅者可以独立地进行扩展和修改。订阅-发布模式常用于事件驱动架构、消息队列、异步编程等场景。
总结一下它们的区别:
- 耦合性:观察者模式中的主题和观察者之间是直接耦合的,而订阅-发布模式中的发布者和订阅者通过中间件解耦。
- 扩展性:观察者模式中,主题和观察者之间的关系是固定的,难以动态添加或移除观察者。而在订阅-发布模式中,发布者和订阅者可以独立地进行扩展和修改。
- 灵活性:订阅-发布模式相对于观察者模式更加灵活,因为它引入了中间件,可以支持多个发布者和多个订阅者之间的通信。
根据具体的需求和场景,可以选择使用观察者模式或订阅-发布模式。如果需要简单的一对多通信关系,并且不需要动态添加或移除观察者,观察者模式是一个合适的选择。如果需要更灵活的消息传递机制,支持多个发布者和多个订阅者之间的通信,并且希望发布者和订阅者之间解耦,那么订阅-发布模式更适合。
# 简单介绍下 AST抽象语法树
抽象语法树(Abstract Syntax Tree,AST)是源代码的结构化表示,它以树状的形式表示代码的语法结构。AST 是编译器和相关工具中常用的数据结构,用于分析、转换和生成代码。
AST 将源代码解析为一系列的节点,每个节点代表代码中的一个语法结构,例如函数、变量声明、表达式等。每个节点都包含与其对应的语法元素相关的信息,例如标识符、操作符、参数等。
AST 的构建过程通常包括以下步骤:
- 词法分析(Lexical Analysis):将源代码分解为一个个的词法单元(tokens),例如关键字、标识符、操作符等。
- 语法分析(Syntax Analysis):根据词法单元构建抽象语法树。在这个阶段,解析器会根据语法规则将词法单元组织成语法结构,并生成对应的 AST 节点。
- 语义分析(Semantic Analysis):对 AST 进行进一步的分析,检查语法的正确性、类型推断等。在这个阶段,可以进行符号表的构建、类型检查、作用域分析等操作。
AST 的应用非常广泛,包括但不限于以下方面:
- 编译器:编译器使用 AST 进行代码的分析、优化和生成。通过对 AST 进行遍历和转换,可以进行代码优化、死代码消除、内联函数等操作。
- 代码静态分析:静态分析工具使用 AST 分析代码的结构和潜在问题。例如,代码风格检查工具可以通过分析 AST 来检查代码是否符合指定的代码风格规范。
- 代码转换和重构:通过对 AST 进行修改和重构,可以实现代码的转换和重构。例如,将 ES5 代码转换为 ES6 代码,或者进行自动化的代码重构操作。
- 代码生成:将 AST 转换为目标代码。编译器将 AST 转换为中间代码或目标代码,用于执行或部署应用程序。
# 如何将 AST 转换为中间代码或目标代码?
将抽象语法树(AST)转换为中间代码或目标代码通常涉及以下步骤:
- AST 遍历:遍历 AST 是将其转换为中间代码或目标代码的关键步骤。通过遍历 AST,可以访问和操作每个节点,根据需要进行转换和修改。
- 转换规则定义:定义转换规则,将 AST 中的节点映射到中间代码或目标代码的表示形式。这些规则可以根据编程语言、目标平台或转换的具体需求而不同。
- 转换操作:根据转换规则,对 AST 的节点进行相应的转换操作。这可能涉及节点的替换、修改、添加或删除等操作。
- 代码生成:根据转换后的 AST,生成中间代码或目标代码的表示形式。这可能包括生成新的代码字符串、构建新的代码结构或生成字节码等。
具体的转换过程和工具取决于你使用的编程语言和转换目标。下面是一些常见的工具和技术,用于将 AST 转换为中间代码或目标代码:
- Babel:Babel 是一个广泛使用的 JavaScript 编译器,它可以将 ES6+ 代码转换为向后兼容的 JavaScript 代码。Babel 使用 AST 进行代码转换。你可以使用 Babel 插件来定义转换规则,并通过 Babel 将 AST 转换为目标代码。
- TypeScript Compiler API:TypeScript 提供了一个编译器 API,可以使用它来分析和转换 TypeScript 代码。TypeScript 编译器使用 AST 进行代码分析和转换。你可以使用 TypeScript 编译器 API 来访问和修改 AST,并将其转换为中间代码或目标代码。
- LLVM:LLVM 是一个开源的编译器基础设施,它提供了强大的工具和库,用于生成高质量的中间代码。LLVM 使用自己的中间表示(LLVM IR)来表示代码,并提供了丰富的工具链和库来进行代码转换和优化。
- 自定义转换工具:如果你有特定的需求或目标,你也可以编写自己的转换工具。这通常涉及编写代码来遍历 AST、定义转换规则和生成目标代码。你可以使用编程语言的解析器和语法分析器来构建 AST,然后根据需求进行转换和代码生成。
无论使用哪种工具或技术,将 AST 转换为中间代码或目标代码需要对语言和目标平台有一定的了解,并具备对 AST 进行遍历和操作的能力。这通常需要对编译原理和相关工具有一定的了解和经验。
# js 定时器为什么是不精确的?
JavaScript中的定时器(setTimeout
和setInterval
)也可能不是完全精确的,这是因为JavaScript是单线程的,并且在执行JavaScript代码时,可能会发生以下情况导致定时器不精确:
- 事件循环机制:
- JavaScript使用事件循环机制来处理异步任务。当定时器到期时,会将定时器的回调函数放入任务队列中,等待执行。但是,只有在主线程上的执行栈为空时,才会从任务队列中取出任务执行。因此,如果在定时器到期时主线程正在执行其他任务,定时器的回调函数可能会延迟执行。
- 高优先级任务:
- 如果在定时器到期前有其他高优先级的任务需要执行,例如用户交互事件或网络请求,JavaScript引擎会优先处理这些任务。这可能会导致定时器的回调函数被延迟执行。
- 定时器的最小延迟:
- 根据HTML5规范,
setTimeout
和setInterval
的最小延迟时间是4毫秒。这意味着无论传递给定时器的延迟值是多少,实际上定时器最早也会在4毫秒后执行。这种最小延迟的存在可能导致定时器的不精确性。
- 根据HTML5规范,
- 页面不可见性:
- 当页面处于不可见状态(例如,用户切换到其他标签或最小化浏览器窗口),浏览器可能会降低对定时器的处理优先级,以节省资源。这可能导致定时器的回调函数被延迟执行或甚至暂停执行。
要提高定时器的精确性,可以考虑以下方法:
- 使用
requestAnimationFrame
代替setTimeout
或setInterval
,因为requestAnimationFrame
会在浏览器的下一帧绘制之前执行,通常可以获得更好的时间精度。 - 使用
Web Workers
在后台线程中处理定时任务,以避免主线程的阻塞。 - 注意避免在定时器回调函数中执行耗时的操作,以确保定时器的回调函数能够及时执行。
尽管如此,仍然无法保证定时器的绝对精确性,因为它受到浏览器和操作系统等因素的限制。