一、前端面试题
1.1. 函数节流和防抖
函数节流:一个函数执行一次后,只有大于设定的执行周期后才会执行第二次。
function control(fn, delay) {
// 记录上一次函数触发的时间
let lastTime = 0;
// 闭包实现lastTime不会被重新赋值
return function () {
// 记录当前函数触发的时间
let nowTime = Date.now();
if (nowTime - lastTime > delay) {
fn();
// 同步时间
lastTime = nowTime;
}
};
}
document.onscroll = control(function () {
console.log("事件触发了" + Date.now());
}, 300);
防抖函数:一个需要频繁触发的函数,在规定时间内,只让最后一次生效,前面的不生效。
function debounce(fn, delay) {
// 记录上一次的延时器
let timer = null;
return function () {
// 清除上一次延时器
clearTimeout(timer);
// 设置延时器
timer = setTimeout(function () {
fn.apply(this);
}, delay);
};
}
document.getElementById("btn").onclick = debounce(function () {
console.log("触发了点击事件" + Date.now(), 1000);
});
1.2. 跨域和解决方法
同源策略:浏览器的一种安全策略,协议名、域名、端口号必须完全一致。
跨域:违背同源策略就好产生跨域。
解决跨域:Jsonp、CORS(跨域资源共享,Cross-Origin Resource Sharing)定义一种跨域访问的机制.
/* JSONP
1.jsonp方法 只能get
2.动态创建<script>标签,然后利用<script>的src不受同源策略约束来跨域获取数据。
3.JSONP 由两部分组成:回调函数和数据。
*/
// 1. 动态的创建一个 script 标签
let script = document.createElement("script");
// 2. 设置 script 的 src,设置回调函数
script.src = "http://localhost:3000/testAJAX?callback=abc";
function abc(data) {
alert(data.name);
};
// 3. 将 script 添加到 body 中
document.body.appendChild(script);
// 4. 服务器中路由的处理
router.get("/testAJAX" , function (req , res) {
console.log("收到请求");
let callback = req.query.callback;
let obj = { name:"孙悟空", age:18 }
res.send(callback+"("+JSON.stringify(obj)+")");
// res.end(`callback(${JSON.stringify(obj)})`)
});
/*
CORS,在服务器设置请求头
*/
// 引入express模块
const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => {
// JSON格式 JSON.stringify(data)
res.send('Hello World!')
})
// 如果参数值中有中文,比如张三丰,则用encodeURI对中文进行编码 data = "username="+encodeURI("张三丰")+"&password=2016";
// 设置响应头,允许跨域
response.setHeader('Access-Control-Allow-Origin','*');
// 允许任何类型的请求头
response.setHeader('Access-Control-Allow-Headers','*')
// 设置请求方法
response.setHeader('Access-Control-Allow-Method','*')
// 监听端口启动服务
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
1.3. node js 的事件轮询机制
node js 的事件轮询机制借助 libuv 库实现的,概括事件轮询机制,包括6个阶段:
- timers 定时器阶段,计时和执行到点的定时器回调函数
- pending callbacks,某些系统操作(例如TCP错误类型)的回调函数
- idle,prepare,准备工作
- poll 轮询阶段(轮询队列)
- 如果轮询队列不为空,依次同步取出轮询队列中第一个回调函数执行,直到轮询队列为空或者达到系统最大的限制
- 如果轮询队列为空
- 如果之前设置过setImmediate函数,直接进入下一个 check 阶段
- 如果之前没有设置过setImmediate函数,在当前 poll 阶段等待,直到轮询队列添加回调函数,就去第一个情况执行,如果定时器到店了,也会去下一个阶段
- check 查阶段,执行setImmediate设置的回调函数
- close callbacks 关闭阶段,执行 close 事件回调函数
process.nextTick 能在任意阶段优先执行。
setTimeout(function () {
console.log("setTimeout()");
}, 0);
setImmediate(function () {
console.log("setImmediate()");
});
process.nextTick(function () {
console.log("process.nextTick()");
});
// 执行结果
process.nextTick()
setTimeout()
setImmediate()
1.4. 从一个url地址到最终页面渲染完成,发生了什么?
-
DNS解析:将域名地址解析为 ip地址
- 浏览器DNS缓存
- 系统DNS缓存
- 路由器DNS缓存
- 网络运营商DNS缓存
- 递归搜索: blog.baidu.com
- .com域名下查找DNS解析
- .baidu域名下查找DNS解析
- blog域名下查找DNS解析
- 出错了
-
TCP 连接:TCP 三次握手
- 第一次握手,由浏览器发起,告诉服务器我要发送请求了
- 第二次握手,由服务器发起,告诉浏览器我准备接受了,你赶紧发送吧
- 第三次握手,由浏览器发送,告诉服务器,我马上发了,准备接受吧
-
发送请求
- 请求报文:HTTP协议的通信内容
-
接受响应
- 响应报文
-
渲染页面
- 遇见HTML标记,浏览器调用HTML解析器解析成Token 并构建DOM树
- 遇见style/link 标记,浏览器调用css 解析器,处理css 标记并构建CSSOM树
- 遇见script 标记,调用JavaScript 解析器,处理script 代码(绑定事件,修改DOM树/CSSOM树)
- 将DOM树和CSSOM树合并成一个渲染树
- 根据渲染树来计算布局,计算每个节点的几何信息(布局)
- 将各个节点颜色绘制到屏幕上(渲染)
注意:
这五个步骤不一定按照顺序执行,如果dom树或cssom树被修改了,可能会执行多次布局和渲染,
往往在实际页面中,这些步骤都会执行多次
-
断开连接,TCP四次挥手
- 第一次挥手,由浏览器发起的,发送给服务器,我东西发送完了(请求报文),你准备关闭吧
- 第二次挥手,由服务器发起的,告诉浏览器,我东西接受完了(请求报文),我准备关闭了,你也准备吧
- 第三次挥手,由服务器发起,告诉浏览器,我东西发送完了(响应报文),你准备关闭吧
- 第四次挥手,由浏览器发起,告诉服务器,我东西接受完了,我准备关闭了(响应报文),你也准备吧
1.5. 闭包
什么是闭包(Closure):
- 密闭的容器,类似于set、map容器,存储数据的
- 闭包是一个对象,存放数据的格式: key:value
闭包形成条件:
- 函数嵌套
- 内部函数引用外部函数的局部变量
闭包的优点:延长外部函数局部变量的生命周期
闭包的缺点:容易造成内存泄露
注意点:
- 合理使用闭包
- 用完闭包要及时清除(销毁)
function fun(){
var count = 1;
return function fun2(){
count++;
console.log(count);
}
}
var fun2 = fun();
fun2(); // 2
fun2(); // 3
1.6. 变量提升和执行上下文
变量提升:
- 所有的声明都会提升到作用域的最顶上去。
- 同一个变量只会声明一次,其他的会被忽略掉或者覆盖掉。
- 函数声明的优先级高于变量声明的优先级,并且函数声明和函数定义的部分一起被提升。
console.log(v1);
var v1 = 100;
function foo() {
console.log(v1);
var v1 = 200;
console.log(v1);
}
foo();
console.log(v1);
// undefined undefined 200 100
函数提升:具名函数的声明有两种方式:1. 函数声明式 2. 函数字面量式
- 函数 变量形式声明 和普通变量一样 提升的 只是一个没有值的变量。
- 函数声明式的提升现象和变量提升略有不同,函数声明式会提升到作用域最前边,并且将声明内容一起提升到最上边。
//函数声明式
function bar () {}
//变量形式声明;
var foo = function () {}
执行上下文:代码执行的环境,代码正式执行之前会进入到执行环境
工作:
-
创建变量对象:
- 变量
- 函数及函数的参数
- 全局:window
- 局部:抽象的但是确实存在
-
确认this的指向
- 全局:this --> window
- 局部: this --> 调用其他对象
-
创建作用域链
- 父级作用域链 + 当前的变量对象
-
扩展:
-
ECObj = { 变量对象: {变量,函数,函数的形参} scopeChain: 父级作用域链 + 当前的变量对象, this: {window || 调用其它的对象} }
-
1.7. 宏任务和微任务
宏任务macrotask,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。
浏览器为了能够使得 JS 内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:
(macro)task->渲染->(macro)task->...
宏任务包含:
script(整体代码)
setTimeout
setInterval
I/O
UI交互事件
postMessage
MessageChannel
setImmediate(Node.js 环境)
微任务microtask,可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。
所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。
微任务包含:
Promise.then
Object.observe
MutaionObserver
process.nextTick(Node.js 环境)
运行机制
在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
- 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
console.log('start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
new Promise((resolve) => {
console.log('promise')
resolve()
})
.then(() => {
console.log('then1')
})
.then(() => {
console.log('then2')
})
console.log('end')
/*
start
promise
end
then1
then2
setTimeout
*/