前端面试题

176 阅读7分钟

一、前端面试题


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个阶段:

  1. timers 定时器阶段,计时和执行到点的定时器回调函数
  2. pending callbacks,某些系统操作(例如TCP错误类型)的回调函数
  3. idle,prepare,准备工作
  4. poll 轮询阶段(轮询队列)
    1. 如果轮询队列不为空,依次同步取出轮询队列中第一个回调函数执行,直到轮询队列为空或者达到系统最大的限制
    2. 如果轮询队列为空
      • 如果之前设置过setImmediate函数,直接进入下一个 check 阶段
      • 如果之前没有设置过setImmediate函数,在当前 poll 阶段等待,直到轮询队列添加回调函数,就去第一个情况执行,如果定时器到店了,也会去下一个阶段
  5. check 查阶段,执行setImmediate设置的回调函数
  6. 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地址到最终页面渲染完成,发生了什么?

  1. DNS解析:将域名地址解析为 ip地址

    • 浏览器DNS缓存
    • 系统DNS缓存
    • 路由器DNS缓存
    • 网络运营商DNS缓存
    • 递归搜索: blog.baidu.com
      • .com域名下查找DNS解析
      • .baidu域名下查找DNS解析
      • blog域名下查找DNS解析
      • 出错了
  2. TCP 连接:TCP 三次握手

    1. 第一次握手,由浏览器发起,告诉服务器我要发送请求了
    2. 第二次握手,由服务器发起,告诉浏览器我准备接受了,你赶紧发送吧
    3. 第三次握手,由浏览器发送,告诉服务器,我马上发了,准备接受吧
  3. 发送请求

    • 请求报文:HTTP协议的通信内容
  4. 接受响应

    • 响应报文
  5. 渲染页面

    • 遇见HTML标记,浏览器调用HTML解析器解析成Token 并构建DOM树
    • 遇见style/link 标记,浏览器调用css 解析器,处理css 标记并构建CSSOM树
    • 遇见script 标记,调用JavaScript 解析器,处理script 代码(绑定事件,修改DOM树/CSSOM树)
    • 将DOM树和CSSOM树合并成一个渲染树
    • 根据渲染树来计算布局,计算每个节点的几何信息(布局)
    • 将各个节点颜色绘制到屏幕上(渲染)

    注意:

    这五个步骤不一定按照顺序执行,如果dom树或cssom树被修改了,可能会执行多次布局和渲染,

    往往在实际页面中,这些步骤都会执行多次

  6. 断开连接,TCP四次挥手

    1. 第一次挥手,由浏览器发起的,发送给服务器,我东西发送完了(请求报文),你准备关闭吧
    2. 第二次挥手,由服务器发起的,告诉浏览器,我东西接受完了(请求报文),我准备关闭了,你也准备吧
    3. 第三次挥手,由服务器发起,告诉浏览器,我东西发送完了(响应报文),你准备关闭吧
    4. 第四次挥手,由浏览器发起,告诉服务器,我东西接受完了,我准备关闭了(响应报文),你也准备吧

1.5. 闭包

什么是闭包(Closure):

  1. 密闭的容器,类似于set、map容器,存储数据的
  2. 闭包是一个对象,存放数据的格式: key:value

闭包形成条件:

  1. 函数嵌套
  2. 内部函数引用外部函数的局部变量

闭包的优点:延长外部函数局部变量的生命周期

闭包的缺点:容易造成内存泄露

注意点:

  1. 合理使用闭包
  2. 用完闭包要及时清除(销毁)
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 () {}

执行上下文:代码执行的环境,代码正式执行之前会进入到执行环境

工作:

  1. 创建变量对象:

    • 变量
    • 函数及函数的参数
    • 全局:window
    • 局部:抽象的但是确实存在
  2. 确认this的指向

    • 全局:this --> window
    • 局部: this --> 调用其他对象
  3. 创建作用域链

    • 父级作用域链 + 当前的变量对象
  4. 扩展:

    • 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
*/