前端面试知识点

目录

防抖和节流

防抖:只要输入频率持续高于阈值,函数就不触发,当最后一次触发频率低于阈值时执行,将多次频繁执行合并为一次,函数固定时间执行次数与输入频率有关

function debounce(fn) {
  let timeout = null; // 创建一个标记用来存放定时器的返回值
  return function () {
    clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉
    timeout = setTimeout(() => { // 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
      fn.apply(this, arguments);
    }, 500);
  };
}
function sayHi() {
  console.log('防抖成功');
}
var inp = document.getElementById('inp');
inp.addEventListener('input', debounce(sayHi)); // 防抖

节流:输入频率持续高于阈值时,函数转换为低频率执行,即将函数由高频执行降为低频,函数固定时间执行次数与输入频率无关

function throttle(fn) {
  let canRun = true; // 通过闭包保存一个标记
  return function () {
    if (!canRun) return; // 在函数开头判断标记是否为true,不为true则return
    canRun = false; // 立即设置为false
    setTimeout(() => { // 将外部传入的函数的执行放在setTimeout中
      fn.apply(this, arguments);
      // 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。当定时器没有执行的时候标记永远是false,在开头被return掉
      canRun = true;
    }, 500);
  };
}
function sayHi(e) {
  console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi));

参考
什么是防抖和节流?有什么区别?如何实现?
前端面试查漏补缺–(一) 防抖和节流

Set、Map、WeakSet 和 WeakMap

1.Set为不含重复值的类数组结构
2.WeakSet为存储对象的类数组结构;且垃圾回收机制不考虑WeakSet成员
3.Map为Value-Value的Hash结构,键名不限于字符串
4.WeakMap键值只能为对象;且垃圾回收机制不考虑WeakMap键值

DFS和BFS

数据:

let data = {
	a: {
		c: {
			g: {
				m: {
					r: true,
					s: true
				}
			},
			h: {
				n: {
					t: true
				},
				o: true
			}
		},
		d:{
			i: true
		}
	},
	b: {
		e: {
			j: true,
			k: {
				p: true,
				q: true
			}
		},
		f: {
			l: true
		}
	}
}

深度遍历:

const depthFirstTraversing = (data)=>{
	let traversingRes = [];
	for(let key in data){
		// console.log(key);
		traversingRes.push(key);
		if(data[key] !== true){
			traversingRes = traversingRes.concat(depthFirstTraversing(data[key]))
		}
	}
	return traversingRes;
}
console.log(depthFirstTraversing(data));

广度遍历

const breadthFirstTraversing = (data)=>{
	let traversingRes = [];
	let leveNodes = [];
	for(let key in data){
		leveNodes.push({key, data: data[key]});
	}
	while(leveNodes.length > 0){
		let item = leveNodes.shift();
		traversingRes.push(item.key);
		for(let childKey in item.data){
			leveNodes.push({key: childKey, data: item.data[childKey]})
		};
	}
	return traversingRes;
}
console.log(breadthFirstTraversing(data))

利用DFS和BFS深拷贝对象

data同上题
利用DFS深拷贝对象

const deepCopyThroughDFS = (data)=>{
	let copyRes = {};
	for(let key in data){
		// console.log(key);
		if(data[key] !== true){
			copyRes[key] = deepCopyThroughDFS(data[key]);
		}else{
			copyRes[key] = true;
		}
	}
	return copyRes;
}

利用BFS深拷贝对象

const deepCopyThroughBFS = (data)=>{
	let copyRes = {};
	let leveNodes = [];
	for(let key in data){
		leveNodes.push({key, data: data[key], parentData: copyRes});
	}
	while(leveNodes.length > 0){
		let item = leveNodes.shift();
		if(item.data !== true){
			item.parentData[item.key] = {};
			for(let childKey in item.data){
				leveNodes.push({key: childKey, data: item.data[childKey], parentData: item.parentData[item.key]})
			};
		}else{
			item.parentData[item.key] = true;
		}
		
	}
	return copyRes;
}

class

es6 class的实例方法,实例属性,原型链方法,静态方法(static)支持实现
es6 class的原型链属性,静态属性不支持实现,暂时用es5方式实现

JS基础之异步

参考
JS 基础之异步

event loop 和 任务队列

js执行机制:

  1. js为单线程语言,主线程任务队列取出任务放入执行栈执行;工作线程将执行完毕的异步任务不断放入任务队列;主线程重复从任务队列取出任务执行(生产-消费模式)
  2. js任务分为宏任务(macrotasks)微任务(microtasks),macrotasks和microtasks分别放入macroqueuemicroqueue
  3. macrotasks,每次执行栈执行的代码就是一个宏任务,主要包含:script(整体代码)、setTimeout、setInterval、XHR/ajax callback、I/O、UI交互事件(DOM API event callback)、postMessage、MessageChannel、setImmediate(Node.js 环境)
  4. microtasks,当前 task 执行结束后立即执行的任务,主要包含:Promise.then、async/await(每个await表达式之后的代码)、MutaionObserver、process.nextTick(Node.js 环境)
  5. event loop:
    5.1.主线程从macroqueue取出一个macrotask
    5.2.将macrotask里的同步代码放入执行栈执行
    5.3.将macrotask里的microtask放入microqueue
    5.4.将macrotask里的macrotask放入macroqueue
    5.5.执行栈对当前macrotask的同步代码执行完毕之后,从microqueue取出源自当前macrotaskmicrotask(5.3.)放入执行栈执行
    5.6.microtask执行完毕后当前event loop结束;主线程从macroqueue取出一个macrotask,重复5.1.开始新的event loop
  6. 每个event loop执行一个macrotask后会清空当前macrotask产生的microtasks,然后进入下个event loop

参考:
js event loop 事件循环
浏览器的Tasks、microtasks、 queues 和 schedules
setTimeout、Promise、Async/Await 的区别

浏览器和Node事件循环的区别

1.Nodejs v11以下:
执行完第一个阶段的所有任务(包含macrotask与对应产生的microtasks),再执行完nextTick macrotasks队列里面的内容, 然后执行完微任务队列的内容
2.Nodejs v11及以上:
执行完每一个macrotask,再执行macrotask对应产生的microtasks,如此循环

参考:
浏览器和Node 事件循环的区别

Promise原理

  1. 在 then 函数执行时用一个属性this.callback保存回调函数 cb数组,然后在 resolve 执行时再将其执行
  2. 添加一个属性 isResolved,用来记录是否调用过 resolve 函数,避免多次执行resolve
  3. then返回Promise对象可支持链式调用

Promise 源码:实现一个简单的 Promise
Promise原理讲解 && 实现一个Promise对象 (遵循Promise/A+规范)
面试精选之Promise

Generator函数

以下generator函数代码输出什么?

function* demo() {
  console.log('Hello ' + (yield 'a')); // OK
  console.log('Hello ' + (yield 'b')); // OK
}
let de = demo()
let a = de.next('a')
console.log(a)
let b = de.next('b')
console.log(b)
let c = de.next('c')
console.log(c)
let d = de.next('d')
console.log(d)
  1. 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值
  2. 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式

参考:
yield 表达式

数组扁平化去重

已知如下数组:
var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];
编写一个程序将数组扁平化去并除其中重复部分数据,最终得到一个升序且不重复的数组

var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];
const flattenData  = (data)=>{
	var newData = [];
	data.forEach((item)=>{
		if(typeof item === 'object'){
			newData = newData.concat(flattenData(item))
		}else{
			newData.push(item)
		}
	})
	return newData;
}
const sortAndDeduplication = (data)=>{
	const sort = (a,b)=>{
		return a - b;
	}

	var newData = [];
	newData = data.sort(sort).reduce((acc, cVlue)=>{
		if(cVlue !== acc[acc.length - 1]){
			acc.push(cVlue);
		}
		return acc
	}, [])
	return newData;
}
const flatAndSortAndDeduplication = (data)=>{
	// let flatData = flattenData(data);
	let sortAndDeduplicatedData = sortAndDeduplication(flattenData(data));
	return sortAndDeduplicatedData
}
console.log(flatAndSortAndDeduplication(arr))

实现一个new

const _new = function(){
	const [klass, ...args] = arguments;
	var newInstance = {};
	klass.apply(newInstance, args);
	newInstance.__proto__ = klass.prototype
	return newInstance;
}
let classA = function(a, b){
	this.a = a;
	this.b = b;
}
classA.prototype = {
	geta: function(){
		console.log(this.a)
	},
	setb: function(b){
		this.b = b
	}
}
// 实例化classA
let aa = _new(classA, 1, 2)

http协议

1.请求报文格式

请求行(request line)、请求头部(header)、空行和请求数据
在这里插入图片描述

例:

在这里插入图片描述

2.响应报文格式
在这里插入图片描述

例:
在这里插入图片描述

3.状态码

分类分类描述
1**信息,服务器收到请求,需要请求者继续执行操作
2**成功,操作被成功接收并处理
3**重定向,需要进一步的操作以完成请求
4**客户端错误,请求包含语法错误或无法完成请求
5**服务器错误,服务器在处理请求的过程中发生了错误

参考:

一张图理清TCP/IP参考模型、OSI模型及HTTP/HTTPS的对应关系
HTTP和HTTPS协议,看一篇就够了
《图解HTTP与HTTPS》的干货1.2w字

介绍HTTPS握手过程

在这里插入图片描述
在这里插入图片描述

参考:
图文理清HTTPS及SSL/TLS加解密核心运行机制
SSL/TLS协议运行机制的概述
HTTP和HTTPS协议,看一篇就够了
介绍 HTTPS 握手过程
《图解HTTP与HTTPS》的干货1.2w字
《大前端进阶 安全》系列 HTTPS详解
一文看懂HTTPS、证书机构(CA)、证书、数字签名、私钥、公钥

HTTPS 中间人攻击和验证证书的合法性

中间人攻击
在这里插入图片描述
证书认证

CA颁发服务端证书
在这里插入图片描述
客户端验证证书
在这里插入图片描述

图文理清HTTPS及SSL/TLS加解密核心运行机制
大前端进阶 安全》系列 HTTPS详解
《图解HTTP与HTTPS》的干货1.2w字
一文看懂HTTPS、证书机构(CA)、证书、数字签名、私钥、公钥
HTTPS 握手过程中,客户端如何验证证书的合法性

谈谈你对TCP三次握手和四次挥手的理解

1. 三次握手

在这里插入图片描述
刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态。 进行三次握手:

  • 第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN©。此时客户端处于 SYN_SEND 状态。

    首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。
  • 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态。

    在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。
  • 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。

    确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。

发送第一个SYN的一端将执行主动打开(active open),接收这个SYN并发回下一个SYN的另一端执行被动打开(passive open)。

在socket编程中,客户端执行connect()时,将触发三次握手。

1.1. 为什么需要三次握手?

  • 建立TCP链接是为了保证稳定有序的收发数据,那么就要保证双发的发送和接收功能都是OK的。 三次握手三次是客户端和服务端都让 “对方” 知道自己发送和接收能力都OK的最小次数。 第一次:client—>server (服务端确认了客户端的发送功能) 第二次:setver—>client (客户端确认了服务端的发送功能和接收功能) 第三次:client—>server (服务端确认了客户端的接收功能); TCP连接建立。 两次无法保证,四次会多余。
  • 确认ISN

1.2. 三次握手过程中可以携带数据吗?
第三次握手的时候,是可以携带数据的。但是,第一次、第二次握手不可以携带数据。防止建立链接时遭受携带大量数据的攻击

1.3. SYN Flood攻击
一些恶意的人根据超时机制制造了SYN Flood攻击——给服务器发了一个SYN后,就下线了,于是服务器需要默认等63s才会断开连接,这样,攻击者就可以把服务器的syn连接的队列耗尽,让正常的连接请求不能处理。常见的防御 SYN 攻击的方法有如下几种:

  • 缩短超时(SYN Timeout)时间
  • 增加最大半连接数
  • 过滤网关防护
  • SYN cookies技术

2. 四次挥手
在这里插入图片描述
TCP 的连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),客户端或服务器均可主动发起挥手动作。

刚开始双方都处于 ESTABLISHED 状态,假如是客户端先发起关闭请求。四次挥手的过程如下:

  • 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。
  • 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。
  • 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。
  • 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
    即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。

收到一个FIN只意味着在这一方向上没有数据流动。客户端执行主动关闭并进入TIME_WAIT是正常的,服务端通常执行被动关闭,不会进入TIME_WAIT状态。

在socket编程中,任何一方执行close()操作即可产生挥手操作。

2.1. 挥手为什么需要四次
因为TCP是全双工的,所以,发送方和接收方都需要Fin和Ack。挥手的时候,A说我要断开了,B还没发完最后的数据,因此需要先回应一下A,我收到你的断开的请求了,但是你要等我把最后的内容给你,所以这里分开了2步: (1)回应A; (2)发送自己的最后一个数据

2.2. 等待2MSL的意义

  • TIME_WAIT确保有足够的时间让对端收到了ACK,如果被动关闭的那方没有收到Ack,就会触发被动端重发Fin,一来一去正好2个MSL
  • 防止“已失效的连接请求报文段”出现在本连接中。 客户端在发送完最后一个ACK报文段后,再经过2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段

3. TCP重传机制?

TCP要保证所有的数据包都可以到达,所以,必需要有重传机制。

注意,接收端给发送端的Ack确认只会确认最后一个连续的包,比如,发送端发了1,2,3,4,5一共五份数据,接收端收到了1,2,于是回ack 3,然后收到了4(注意此时3没收到),此时的TCP会怎么办?我们要知道,因为正如前面所说的,SeqNum和Ack是以字节数为单位,所以ack的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了。

遗留:为何不能跳着确认?

4. 快速重传机制
TCP引入了一种叫Fast Retransmit 的算法,不以时间驱动,而以数据驱动重传。也就是说,如果,包没有连续到达,就ack最后那个可能被丢了的包,如果发送方连续收到3次相同的ack,就重传。Fast Retransmit的好处是不用等timeout了再重传。

比如:如果发送方发出了1,2,3,4,5份数据,第一份先到送了,于是就ack回2,结果2因为某些原因没收到,3到达了,于是还是ack回2,后面的4和5都到了,但是还是ack回2,因为2还是没有收到,于是发送端收到了三个ack=2的确认,知道了2还没有到,于是就马上重转2。然后,接收端收到了2,此时因为3,4,5都收到了,于是ack回6。示意图如下:

在这里插入图片描述
参考:
面试官,不要再问我三次握手和四次挥手
TCP 的那些事儿(上)
作为前端的你了解多少tcp的内容
谈谈你对 TCP 三次握手和四次挥手的理解
TCP 请求头

npm install机制

关键点:扁平化安装,若存在多个不兼容版本则需嵌套安装
参考:
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/22
详解npm的模块安装机制

数据类型

原始类型:boolean、undefined、string、number、symbol
对象类型:null ,Array、function、Object

判断数组方法

1.Object.prototype.toString.call([]) // “[object Array]”
借用Object的toString方法,基本可以判断所有的数据类型
2.[] instanceof Array // true
判断[]原型链上是否能找到Array类型,仅对对象类型的数据有效,对原始类型无效
3.Array.isArray([]) // true
判断是否为数组,与2方法比较,该方法可以判断出iframe数组
4.typeof [] // “object”
注意:该方法不能判断对象类型,对象类型输出均为object,只能判断原始类型

Repaint&Reflow

参考:
介绍下重绘和回流(Repaint & Reflow),以及如何进行优化

观察者模式和发布订阅模式的区别

观察者模式和发布订阅模式的区别
参考:
观察者模式和发布订阅模式的区别
介绍下观察者模式和订阅-发布模式的区别,各自适用于什么场景

Redux 和 Vuex 的设计思想

参考:
Vuex、Flux、Redux、Redux-saga、Dva、MobX

前端中的模块化开发

参考:
前端中的模块化开发

this指向

参考:
箭头函数

const 和 let 的作用域

1.const和let会创造block作用域,只要在同一个block内都可以拿到变量
2.顶部var变量挂在window下
示例:
如下创造了2个Block,分别对应变量e和f;1个Script(也可以理解为顶级Block),对应变量a和b,剩下的c和d作为全局变量
const和let的作用域

xss与csrf

1.xss:跨站脚本攻击,分为存储型 XSS,反射型 XSS和DOM 型 XSS。向被攻击网站注入恶意代码并执行
2.csrf:跨站请求伪造,利用用户在被攻击网站已经保存的信息,在第三方网站向被攻击网站发送请求,冒充用户执行操作
3.预防
xss:过滤用户输入;转义html
csrf:同源检测;添加csrf token
4.
遭受xss攻击的网站,cookie和token均可以被劫持
遭受csrf攻击的网站,cookie可以被劫持,token只要不放在cookie就不会被劫持,或者只要不直接使用cookie token作为键值,被劫持的token也为无效token

参考
前端安全系列(一):如何防止XSS攻击?
前端安全系列之二:如何防止CSRF攻击?

react原理

  • 渲染机制及虚拟dom
    react
  • 更新机制
/**
 * 更新
 * @param {*} newState 新状态
 */
ReactClass.prototype.setState = function(newState) {
  // 拿到ReactCompositeComponent的实例
  // 在装载的时候保存
  // 代码:this._reactInternalInstance = this
  this._reactInternalInstance.receiveComponent(null, newState);
};
  • 文本节点的 receiveComponent
    文本节点的更新比较简单,拿到新的文本进行比较,不同则直接替换整个节点
  • 自定义元素的 receiveComponent
  1. 合并 state
  2. 更新 state
  3. 然后看业务代码中是否实现生命周期方法 shouldComponentUpdate 有则调用,如果返回值为 false 则停止往下执行
  4. 然后是生命周期方法 componentWillUpdate
  5. 然后通过拿到新 state 的 instance 调用 render 方法拿到新的 element 和之旧的 element 进行比较
  6. 如果要更新就继续调用对应的 component 类对应的 receiveComponent 就好啦,其实就是直接当甩手掌柜,事情直接丢给手下去办了。当然还有种情况是,两次生成的 element 差别太大,就不是一个类型的,那好办直接重新生成一份新的代码重新渲染一次就 ok 了
  • 基本元素的 receiveComponent

    基础元素的更新包括两方面

  • 属性的更新,包括对特殊属性比如事件的处理

  • 子节点的更新
    子节点的更新比较复杂,是提升效率的关键,所以需要处理以下问题:

  • diff - 拿新的子节点树跟以前老的子节点树对比,找出他们之间的差别。

  • patch - 所有差别找出后,再一次性的去更新。

React 源码分析
React 源码剖析系列 - 不可思议的 react diff
从Preact了解一个类React的框架是怎么实现的(一): 元素创建
从Preact了解一个类React的框架是怎么实现的(二): 元素diff
从Preact了解一个类React的框架是怎么实现的(三): 组件
ARV 渲染实现比较总结

对react setState异步更新的理解

在这里插入图片描述

  1. 合成事件中,异步
  2. 生命周期函数中,异步
  3. 原生事件中,同步
  4. setTimeout中,同步
  5. 批量更新,合并相同的key

参考:
你真的理解setState吗?

diff算法

参考
简单版react原理分析(虚拟dom,节点渲染,数据更新,diff算法等)
React 源码剖析系列 - 不可思议的 react diff
详解vue的diff算法
VueDiff算法的简单分析和一些个人思考
解析vue2.0的diff算法
传统diff、react优化diff、vue优化diff
写 React / Vue 项目时为什么要在列表组件中写 key,其作用是什么?
协调

以vue为例,如何实现一个mvvm(基于defineProperty)

一个mvvm包含:
1.编译器(compiler,基础模板引擎)
提取指令,data等,渲染真实dom
2.数据劫持(observer,利用defineProperty)
核心方法

defineReactive(obj,key,value){
    // 在获取某个值的适合 想弹个框
    let that = this;
    Object.defineProperty(obj,key,{
        enumerable:true,
        configurable:true,
        get(){ // 当取值时调用的方法
            return value;
        },
        set(newValue){ // 当给data属性中设置值的适合 更改获取的属性的值
            if(newValue!=value){
                // 这里的this不是实例 
                that.observe(newValue);// 如果是设置的是对象继续劫持
                value = newValue;
            }
        }
    });
}

3.view观察者(watcher)
watcher在拥有data挂载的模板编译阶段实例化一次,确定回调更新函数,使view成为model更新的订阅者;watcher在内部的构造函数内将自身与dep关联并加入dep.subs队列
4.model发布主体(dep)
数据observer化时,在get方法内将关联watcher加入队列;在set方法发布订阅
5.数据代理(proxy)
将vm.data.foo代理到vm.foo
6.监听dom事件
在编译阶段查看是否涉及数据绑定事件监听,如v-model等,并设置回调函数更新vm.data
7.diff更新view(watcher执行订阅更新之后)
mvvm
在这里插入图片描述

以上参考:
https://juejin.im/post/5af8eb55f265da0b814ba766
一种简化版MVVM的实现

实现一个mvvm(基于Proxy)

参照vue2.x(上例)的响应式设计模式,将数据劫持部分的Obejct.defineProperty替换为Proxy即可,其他部分,如compile,watcher,dep,事件监听等基本保持不变,简单实现代码如下:

class Watcher{
	constructor(cb){
		this.cb = cb;
	}
	update(){
		this.cb()
	}
}
class Dep{
	constructor(){
		this.subs = [];
	}
	publish(){
		this.subs.forEach((item)=>{
			item.update && item.update();
		})
	}
}
class MVVM{
	constructor(data){
		let that = this;
		this.dep = new Dep();
		this.data = new Proxy(data,{
			get(obj, key, prox){
				that.dep.target && that.dep.subs.push(that.dep.target);
				return obj[key]
			},
			set(obj, key, value, prox){
				obj[key] = value;
				that.dep.publish();
				return true;
			}
		})
		this.compile();
	}
	compile(){
		
		let divWatcher = new Watcher(()=>{
			this.compileUtils().div();
		})
		this.dep.target = divWatcher;
		this.compileUtils().div();
		this.dep.target = null;
		
		let inputWatcher = new Watcher(()=>{
			this.compileUtils().input();
		})
		this.dep.target = inputWatcher;
		this.compileUtils().input();
		this.compileUtils().addListener();
		this.dep.target = null;
	}
	compileUtils(){
		let that = this;
		return {
			div(){
				document.getElementById('foo').innerHTML = that.data.foo;
			},
			input(){
				document.getElementById('bar').value = that.data.bar;
			},
			addListener(){
				document.getElementById('bar').addEventListener('input', function(){
					that.data.bar = this.value;
				})
			}
		}
	}
}
let mvvm = new MVVM({foo: 'foo233', bar: 'bar233'})

通过mvvm.data.foo或者mvvm.data.bar可以操作数据,可以观察到view做出了反应;在输入框改变输入值,也可以通过mvvm.data观察到数据被触发改变

vue的事件处理机制

  1. 普通html元素和在组件上挂了.native修饰符的事件。最终EventTarget.addEventListener()挂载事件
    普通元素在compile阶段被解析为ast语法树,包含该元素监听的事件,事件在render阶段由add$1通过addEventListener挂载到真实元素上
  2. 组件上的,vue组件实例上的自定义事件(不包括.native)会调用原型上的$on,$emit(包括一些其他api $off,$once等等)
    组件在编译阶段data.on被替换为.native事件,data.on与普通hml元素做一致处理;自定义事件被缓存为listener,利用实例$on方法放入观察者队列,子组件调用$emit方法可发布订阅

参考:
vue源码解析-事件机制
Compile - 源码版 之 generate 拼接绑定的事件
vue 在 v-for 时给每项元素绑定事件需要用事件代理吗?为什么?

数组的方法:forEach,map, filter,some, reduce

1.reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。

arr.reduce(callback(accumulator, currentValue[, index[, sourceArray]])[, initialValue])

2.filter() 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素

var newArray = arr.filter(callback(element[, index[, array]])[, thisArg])

3.some() 方法测试是否至少有一个元素可以通过被提供的函数方法。该方法返回一个Boolean类型的值。

arr.some(callback(element[, index[, array]])[, thisArg])

4.forEach() 方法对数组的每个元素执行一次提供的函数。没有办法中止或者跳出 forEach() 循环,除了抛出一个异常.

arr.forEach(callback(element[, index[, array]])[, thisArg]);

5.map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。

var new_array = arr.map(function callback(currentValue[, index[, array]]) {
 // Return element for new_array 
}[, thisArg])

virtual dom vs mvvm vs 原生dom

参考:
网上都说操作真实 DOM 慢,但测试结果却比 React 更快,为什么
Virtual DOM 真的比操作原生 DOM 快吗?谈谈你的想法

IIFE下函数名为不可变量

以下代码输出b函数

var b = 10;
(function b(){
    b = 20;
    console.log(b); 
})();

因为iife的函数表达式函数名为不可变量,在 strict 模式下会报错,非 strict 模式下静默失败
参考:
js的作用域问题?
下面的代码打印什么内容,为什么?

IIFE下的变量提升

var a = 10;
(function () {
    console.log(a)
    a = 5
    console.log(window.a)
    var a = 20;
    console.log(a)
})()

以上代码输出:
undefined -> 10 -> 20,因为IIFE作用域内a在后面声明时被提升了
如果去掉 var a = 20,则打印10->5->5,因为IIFE内部取到的是外部定义的变量a

浏览器的缓存机制

1.强制缓存
强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control。Cache-Control优先级高于Expires,Expires是http1.0的产物,Cache-Control是http1.1的产物
2.协商缓存
协商缓存可以在response header中添加 Last-Modified和Etag实现。Etag优先级高于 Last-Modified,Etag精度较好,Last-Modified性能较好
3.强缓存优先级高于协商缓存
4.用户行为对浏览器缓存的影响

  • 地址栏输入url,检查是否有disk cache 可用
  • 刷新页面,先检查memory cache,再检查disk cache
  • 强制刷新 (Ctrl + F5),发送的请求头部均带有 Cache-control: no-cache,不使用缓存
    cache
    参考:
    深入理解浏览器的缓存机制

BFC(Block Formatting Contexts

BFC 就是块级格式上下文,是页面盒模型布局中的一种 CSS 渲染模式,相当于一个独立的容器,里面的元素和外部的元素相互不影响
1.触发BFC的条件

  • html 根元素
  • float 浮动
  • 绝对定位
  • overflow 不为 visible
  • display 为表格布局或者弹性布局
    2.BFC的作用
  • 清除浮动
  • 防止同一 BFC 容器中的相邻元素间的外边距重叠问题
  • 实现文字环绕或左图右文的布局
    参考:
    介绍下 BFC 及其应用
    https://github.com/glitchboyl/blog/issues/6

vue如何限制子组件不能直接修改props?

参考:
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/60

响应式原理:Object.defineProperty VS Proxy

1.Object.defineProperty不能检测数组下标变动,而Proxy可以代理整个数据
2.Object.defineProperty代理对象时需要遍历对象的每个属性(嵌套对象需要递归遍历),Proxy可以直接代理整个对象
参考:
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/90

div水平垂直居中

1.定宽高:

  • 绝对定位+margin auto
  • 绝对定位+transform

2.不定宽高:

  • 绝对定位+transform

优先级与对象赋值

var a = {n: 1};
var b = a;
a.x = a = {n: 2};

console.log(a.x) 	
console.log(b.x)

以上代码输出:
undefined->{n:2}

array like

var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)

以上代码输出:(push时array like对象length +1)

Object(4) [empty × 2, 1, 2, splice: ƒ, push: ƒ]

判断类数组的方法

  • 存在且是对象
  • 对象上的splice 属性是函数类型
  • 对象上有 length 属性且为正整数

为什么数据埋点时通常使用1x1 像素的透明 gif 图片?

  • 能够完成整个 HTTP 请求+响应(尽管不需要响应内容)
  • 触发 GET 请求之后不需要获取和处理数据、服务器也不需要发送数据
  • 跨域友好
  • 执行过程无阻塞
  • 相比 XMLHttpRequest 对象发送 GET 请求,性能上更好
  • GIF的最低合法体积最小(最小的BMP文件需要74个字节,PNG需要67个字节,而合法的GIF,只需要43个字节)
    参考:
    https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/87

实现LazyMan类

实现以下类:

LazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(10).eat('junk food');
// Hi I am Tony
// 等待了5秒...
// I am eating lunch
// I am eating dinner
// 等待了10秒...
// I am eating junk food

关键点:

  • 类原型方法返回实例
  • 维护任务队列
  • 利用setTimeout 0,实现队列任务全部添加完之后再执行任务
    代码:
/* 
LazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(10).eat('junk food');
// Hi I am Tony
// 等待了5秒...
// I am eating lunch
// I am eating dinner
// 等待了10秒...
// I am eating junk food
*/
/* 
实例属性 name
实例队列 queue
原型方法 eat, sleep, sleepFirst, doSth
sleep, sleepFirst 延迟x秒执行
sleepFirst 优先入队列
doSth 执行队列方法
 */
class LazyManClass {
  constructor(name) {
    this.name = name;
    this.queue = [];
    this.sayName();
    setTimeout(() => {
      this.doSth();
    }, 0);
  }
  sayName() {
    console.log(`Hi I am ${this.name}`);
  }
  eat(food) {
    this.queue.push({ action: "doEat", sth: food });
    return this;
  }
  doEat(food) {
    console.log(`I am eating ${food}`);
  }
  sleep(time) {
    this.queue.push({ action: "doSleep", sth: time });
    return this;
  }
  sleepFirst(time) {
    this.queue.unshift({ action: "doSleep", sth: time });
    return this;
  }
  doSleep(time) {
    setTimeout(() => {
      console.log(`${time} sencond later...`);
      this.doSth();
    }, time * 1000);
  }
  doSth() {
    while (this.queue.length) {
      const { action, sth } = this.queue.shift();
      this[action](sth);
      if (action === "doSleep") {
        break;
      }
    }
  }
}
const LazyMan = function (name) {
  return new LazyManClass(name);
};

export default LazyMan;

参考:
要求设计 LazyMan 类,实现以下功能

箭头函数与普通函数的区别

箭头函数没有自己的this,没有arguments,没有prototype,因此不能作为构造函数使用

如何实现token加密

  • 需要一个secret(随机数)
  • 后端利用secret和加密算法(如:HMAC-SHA256)对payload(如账号密码)生成一个字符串(token),返回前端
  • 前端每次request在header中带上token
  • 后端用同样的算法解密
    参考:
    https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/106

如何设计实现无缝轮播

1.关键点:

  • 元素处理:轮播周期结束后,将第一个li元素clone到尾部,然后移除第一个元素
  • 利用requestAnimationFrame实现动画效果,在动画循环中通过时间判断位移,在轮播周期内重置开始时间
    2.代码实现
const carousel = function(id, speed=1000){
	this.speed = speed;
	this.root = document.getElementById(id); 
	this.startTime = new Date();
	this.req = null;
	this.begin();
}
carousel.prototype = {
	begin(){
		this.render();
		let timer = setInterval(()=>{
			// cancelAnimationFrame(this.req);
			// clearInterval(timer);
			let lists = this.root.getElementsByTagName('li');
			let length = lists.length; 
			let first = lists[0];
			this.root.appendChild(first.cloneNode(true));
			this.root.removeChild(first);
			this.root.style.left = '0px';
			this.startTime = new Date();
			// this.render();
		}, this.speed)
	},
	render(){
		this.root.style.left = -(new Date() - this.startTime)/this.speed*100 + 'px'
		this.req = requestAnimationFrame(this.render.bind(this));
	}
}
let carouselIns = new carousel("carousel")

requestAnimationFrame

1.概念:
告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
2.兼容:

window.requestAnimFrame = (function() {
  return (
    window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    window.oRequestAnimationFrame ||
    window.msRequestAnimationFrame ||
    function(callback, element) {
      window.setTimeout(callback, 1000 / 60)
    }
  )
})()

3.优势:

  • 浏览器可以优化并行的动画动作,更合理的重新排列动作序列,并把能够合并的动作放在一个渲染周期内完成,从而呈现出更流畅的动画效果
  • 一旦页面不处于浏览器的当前标签,就会自动停止刷新。这就节省了CPU、GPU和电力

ES6 代码转成 ES5 代码的实现思路

  1. 解析:解析代码字符串,生成 AST;
  2. 转换:按一定的规则转换、修改 AST;
  3. 生成:将修改后的 AST 转换成普通代码。

HMR原理

关键点

  1. webpack watch文件更新
  2. webpack-dev-server通过sockjs与浏览器建立长连接
  3. HMR runtime接收到更新信息后通过ajax获取文件,并对比更新
    HMR
    参考:
    Webpack HMR 原理解析
    介绍下 webpack 热更新原理,是如何做到在不刷新浏览器的前提下更新页面

BFC、IFC、GFC、FFC

参考:
介绍下 BFC、IFC、GFC 和 FFC

input处理中文输入

使用compositionstart + compositionend判断,如过程不想触发input事件则加boolean值判断

currying与累加器

// 实现一个函数
add(1); 			// 1
add(1)(2);  	// 3
add(1)(2)(3)// 6
add(1)(2, 3); // 6
add(1, 2)(3); // 6
add(1, 2, 3); // 6

currying版本

注意:该版本只实现了固定长度的参数调用

关键点:

  1. 判断待传入参数大于等于函数执行所需的参数长度,再触发执行
  2. 利用递归接收所有参数,传入fn.bind(this, ...args)保存所有参数
const originAdd = (x,y,z)=>{
	return x+y+z
}
const currying = function(fn, length){
	length = length || fn.length
	return function(...args){
		return args.length >= length ? fn.apply(this, args) : currying(fn.bind(this, ...args), length - args.length)
	}
}
const add = currying(originAdd)

累加器版本

注意:该版本使用了toString方法,该方案在新版本的Chrome中似乎不起作用,待验证

关键点

  1. 维护闭包变量存储变量值
  2. 利用打印函数值调用toString方法,重写toString将所有参数累加
function add() {
    // 第一次执行时,定义一个数组专门用来存储所有的参数
    var _args = Array.prototype.slice.call(arguments);

    // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
    var _adder = function() {
        _args.push(...arguments);
        return _adder;
    };

    // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
    _adder.toString = function () {
        return _args.reduce(function (a, b) {
            return a + b;
        });
    }
    return _adder;
}

参考:
深入高阶函数应用之柯里化
请实现一个 add 函数,满足以下功能
详解JS函数柯里化

react-router 里的 Link 标签和 a 标签有什么区别

  • link由react-router代理了a的默认事件,用event.preventDefault()阻止默认事件
  • link点击事件查找是否有to或者replace,使用history替换
  • 其他渲染行为

HTML5 history 和 hash模式

list转tree

以下数据结构中,id 代表部门编号,name 是部门名称,parentId 是父部门编号,为 0 代表一级部门,现在要求实现一个 convert 方法,把原始 list 转换成树形结构,parentId 为多少就挂载在该 id 的属性 children 数组下,结构如下:

// 原始 list 如下
let list =[
    {id:1,name:'部门A',parentId:0},
    {id:2,name:'部门B',parentId:0},
    {id:3,name:'部门C',parentId:1},
    {id:4,name:'部门D',parentId:1},
    {id:5,name:'部门E',parentId:2},
    {id:6,name:'部门F',parentId:3},
    {id:7,name:'部门G',parentId:2},
    {id:8,name:'部门H',parentId:4}
];
const result = convert(list, ...);

实现如下:

const convert = (list)=>{
	let rs = [], listMap = {}, parent;
	for(let i=0;i<list.length;i++){
		let temp = list[i];
		listMap[temp.id] = temp;
	}
	list.forEach((temp)=>{
		if(temp.parentId === 0){
			rs.push(temp);
		}else{
			parent = listMap[temp.parentId];
			!parent.children && (parent.children = []);
			parent.children.push(temp)
		}
	})
	return rs;
}
let list = [
    {id:3,name:'部门C',parentId:1},
    {id:4,name:'部门D',parentId:1},
    {id:5,name:'部门E',parentId:2},
    {id:6,name:'部门F',parentId:3},
    {id:7,name:'部门G',parentId:2},
    {id:8,name:'部门H',parentId:4},
    {id:1,name:'部门A',parentId:0},
    {id:2,name:'部门B',parentId:0},
];
const result = convert(list);
console.log(result)

实现Promise相关类方法

Promise._all = function (promiseList) {
  return new Promise((resolve, reject) => {
    const totalCount = promiseList.length;
    let count = 0;
    let rs = [];
    for (const [index, promiseItem] of promiseList.entries()) {
      promiseItem.then((r) => {
        count++;
        rs[index] = r;
        if (count === totalCount) {
          resolve(rs);
        }
      }).catch(err=>{
        reject(err)
      })
    }
  });
};
Promise._race = function (promiseList) {
  return new Promise((resolve, reject) => {
    for (const promiseItem of promiseList) {
      promiseItem.then((r) => {
        resolve(r);
      }).catch((err) => {
        reject(err);
      });
    }
  });
};
Promise._any = function (promiseList) {
  return new Promise((resolve, reject) => {
    const totalCount = promiseList.length;
    let count = 0;
    for (const promiseItem of promiseList) {
      promiseItem.then((r) => {
        resolve(r);
      }).catch(err=>{
        count++
        if(count === totalCount){
          reject('AggregateError: All promises were rejected')
        }
      })
    }
  });
};
Promise._allSettled = function (promiseList) {
  return new Promise((resolve) => {
    const totalCount = promiseList.length;
    let count = 0;
    let rs = [];
    for (const [index, promiseItem] of promiseList.entries()) {
      promiseItem
        .then((r) => {
          count++;
          rs[index] = { status: "fulfilled", value: r };
          if (count === totalCount) {
            resolve(rs);
          }
        })
        .catch((err) => {
          count++;
          rs[index] = { status: "rejected", reason: err };
          if (count === totalCount) {
            resolve(rs);
          }
        });
    }
  });
};

// Promise.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 })
  );
};

时间复杂度为O(log(m+n))的取中位数算法

返回整数逆序后的字符串

解法一
关键点
转字符串,二分,前后部分交换,递归

const reverseOrder = (num)=>{
	let numStr = num.toString();
	let length = numStr.length;
	if(length == 1){
		return numStr;
	}else{		 
		return length%2 === 0 ? 
		reverseOrder(numStr.slice(length/2,length)) + reverseOrder(numStr.slice(0,length/2)) :
		reverseOrder(numStr.slice((length+1)/2,length)) + numStr[(length-1)/2] + reverseOrder(numStr.slice(0,(length-1)/2))
	}
}
console.log(reverseOrder(12345678))

解法二

function fun(num){
    let num1 = num / 10;
    let num2 = num % 10;
    if(num1<1){
        return num;
    }else{
        num1 = Math.floor(num1)
        return `${num2}${fun(num1)}`
    }
}

参考链接

vuex数据无法刷新

两个维度:
1.在生命周期钩子函数内将store.state.foo赋值给data,data无法刷新
解决:将store.state.foo绑定到计算属性
2.vuex无法持久化:
结合local/session storage,在set数据时写入缓存;在store.js读取缓存

vue mpa

react项目优化

动态加载,treeshaking,exclude/include,state合并及合理管理,使用key,shouldComponentUpdate,PureComponent,事件非内联,虚拟列表
参考:
https://juejin.cn/post/6908895801116721160

前端工程化

  1. 技术选型
  2. 统一规范
  3. 测试
  4. 部署
  5. 监控
  6. 性能优化
  7. 重构

参考:
入门前端工程化

重排(reflow)和重绘(repaint)

页面生成的过程:

1 .HTML 被 HTML 解析器解析成 DOM 树;
2. CSS 被 CSS 解析器解析成 CSSOM 树;
3. 结合 DOM 树和 CSSOM 树,生成一棵渲染树(Render Tree),这一过程称为 Attachment;
4. 生成布局(flow),浏览器在屏幕上“画”出渲染树中的所有节点;
5. 将布局绘制(paint)在屏幕上,显示出整个页面。

第四步和第五步是最耗时的部分,这两步合起来,就是我们通常所说的渲染。

重排(reflow):
当DOM的变化影响了元素的几何信息(元素的的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。

重排也叫回流,简单的说就是重新生成布局,重新排列元素。

重绘(Repaints):
当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘。

重排优化建议:

  1. 减少重排范围
  2. 减少重排次数
    2.1.样式集中改变
    2.2.分离读写操作
    2.3.将 DOM 离线
    2.4.使用 absolute 或 fixed 脱离文档流
    2.5.优化动画

参考:
重排(reflow)和重绘(repaint)

堆和栈

参考:
深入理解js数据类型与堆栈内存

React Hooks 原理

参考:
React Hooks 原理

使用CSS实现各种形状

参考:
用 css 画三角形、梯形、扇形、箭头和椭圆几种基本形状
CSS 实现半圆环的两种方式

<script>标签,async和defer属性

<script>标签用于引入js脚本文件,默认为同步加载和按顺序执行,会阻塞HTML解析,asyncdefer可以优化<script>的加载和执行行为

在这里插入图片描述
参考:
<script>标签,async和defer属性

css link 和 @import

  • link:浏览器会派发一个新的http线程加载资源文件,同时GUI渲染线程会继续向下渲染
  • @import:GUI渲染线程会暂停止渲染,去服务器加载资源,资源文件没有返回前不会继续渲染
    参考:
    关于css @import url()总结
    你真的理解@import和link引入样式的区别吗

浏览器输入URL之后发生了什么?

  1. 解析URL
  2. DNS域名解析
    2.1. 递归查询
    2.2. 迭代查询
    2.3. DNS负载均衡
  3. TCP/IP协议
    3.1. 三次握手
    3.2.四次挥手
  4. 发送HTTP请求
    请求行+请求头+请求内容
  5. 浏览器缓存
    4.1. 强缓存
    4.2. 协商缓存
    4.3. 储存位置
  6. HTTPS
  7. 服务器处理请求并返回HTTP报文
    响应行+响应头+响应内容
  8. 浏览器解析渲染页面

参考:
从输入URL开始建立前端知识体系
史上最详细的经典面试题 从输入URL到看到页面发生了什么?

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值