前端面试题汇总

1,170 阅读7分钟
  • 🏆🏆🏆 小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
  • 📣📣 欢迎大家多多点赞,关注!!!

前言

  • 最近几天收集了不少关于前端的面试题以及一些基础算法,后续会持续更新...

浏览器解析渲染页面

这个图就是Webkit解析渲染页面的过程。

  • 解析HTML形成DOM树
  • 解析CSS形成CSSOM 树
  • 合并DOM树和CSSOM树形成渲染树
  • 浏览器开始渲染并绘制页面

这个过程涉及两个比较重要的概念回流重绘,DOM结点都是以盒模型形式存在,需要浏览器去计算位置和宽度等,这个过程就是回流。等到页面的宽高,大小,颜色等属性确定下来后,浏览器开始绘制内容,这个过程叫做重绘。浏览器刚打开页面一定要经过这两个过程的,但是这个过程非常非常非常消耗性能,所以我们应该尽量减少页面的回流和重绘

性能优化之回流重绘

回流

当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。

会导致回流的操作:

  • 页面首次渲染
  • 浏览器窗口大小发生改变
  • 元素尺寸或位置发生改变
  • 元素内容变化(文字数量或图片大小等等)
  • 元素字体大小变化
  • 添加或者删除可见的DOM元素
  • 激活CSS伪类(例如::hover)
  • 查询某些属性或调用某些方法

一些常用且会导致回流的属性和方法:

  • clientWidth、clientHeight、clientTop、clientLeft
  • offsetWidth、offsetHeight、offsetTop、offsetLeft
  • scrollWidth、scrollHeight、scrollTop、scrollLeft
  • scrollIntoView()、scrollIntoViewIfNeeded()
  • getComputedStyle()
  • getBoundingClientRect()
  • scrollTo()

重绘

当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

优化

CSS
  • 避免使用table布局。
  • 尽可能在DOM树的最末端改变class。
  • 避免设置多层内联样式。
  • 将动画效果应用到position属性为absolute或fixed的元素上。
  • 避免使用CSS表达式(例如:calc())。
JavaScript
  • 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
  • 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
  • 也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

JS的解析

JS的解析是由浏览器的JS引擎完成的。由于JavaScript是单进程运行,也就是说一个时间只能干一件事,干这件事情时其他事情都有排队,但是有些人物比较耗时(例如IO操作),所以将任务分为同步任务异步任务,所有的同步任务放在主线程上执行,形成执行栈,而异步任务等待,当执行栈被清空时才去看看异步任务有没有东西要搞,有再提取到主线程执行,这样往复循环(冤冤相报何时了,阿弥陀佛),就形成了Event Loop事件循环,下面来看看大人物

举出闭包实际场景运用的例子

常见的防抖节流

//防抖
// 设置延时器,短时间高频率触发只有最后一次触发成功
function debounce (fn, delay= 300) {
	let timer;
  	return function() {
    	const args = arguments;
      if(timer) {
        clearTimeout(timer);
      }
      timer = setTimeout( () => {
      	fn.apply(this, args);
      },delay);
    }
}


// 节流
// 设置状态锁,短时间高频率触发只有第一次触发成功
function throttle(fn, delay) {
	let flag = true;
  return () => {
  	if(!flag) return;
    flag = false;
    timer = setTimeout( ()=> {
    	fn();
      flag = true;
    },delay)
  }
}

vue Diff算法原理总结

  1. 首先判断是否是真实节点,oldVnode是真实节点,代表是初次渲染,直接用新的dom替换el
  2. 如果是虚拟dom,就是更新过程,采用diff算法。首先去比较新老节点的tag,如果不一致,就用新的节点替换老的节点
  1. 如果旧节点是一个文本节点,直接替换老的节点的内容就ok
  2. 如果标签tag一致,并且不是文本节点,首先尽心属性替换,然后比对子元素 为了节点复用,所以直接把旧的虚拟dom对应真实dom赋值给新的虚拟dom的el属性
  1. **新老节点都有子节点,采用双指针方式进行比对,同节点判断tag和key完全相同为同一个节点 进行节点复用
    1. 头和头相等比较
    2. 尾和尾相等比较
      1. samenode同节点的时候传入两个新老子节点patch(oldChild, newChild)
    1. 乱序情况--上面都不符合 先遍历旧的子节点数组形成key值映射的map对象,然后根据新子节点数组循环 按照key值和位置关系移动以及新增节点 最后删除多余的旧子节点 如果移动旧节点同样需要patch(oldChild, newchild)
  1. 新的有老的没有子节点,直接将子元素虚拟节点转化为真实节点插入
  2. 新的没有老的有,直接清空el的innnerhtml

手写Promise

class MyPromise {
  constructor(fn) {
	this.state = 'pending';
	this.successFun = [];
	this.failFun =  [];

	let resolve = val => {
  	if(this.state !== 'pending') return;
    
    this.state = 'success';
    
    setTimeout(()=> {
    	this.successFun.forEach(item => item.call(this, val))
    })
  };
	let reject = err => {
  	  if(this.state !== 'pending') return;
    	this.state = 'fail';
    setTimeout(()=> {
    	this.failFun.forEach(item => item.call(this, val))
    })
  };
  // 调用函数
  try {
    fn(resolve, reject);
  } catch (error) {
    reject(error);
  }
  }
	then(resolveCallback,rejectCallback) {
		resolveCallback = typeOf resolveCallback !== 'function' ? v => v : resolveCallback;
    rejectCallback = typeOf rejectCallback !== 'function' ? v => v : rejectCallback;
    
    //保持链式调用 继续返回Promise
     return new Mypromise((resolve,reject) => {
     		//将回调注册到successfun事件集合里面
       	this.successFun.push((val)=> {
        	try {
          	let x = resolveCallback(val);
            // 如果回调函数结果是普通值 那么就resolve出去给下一个then链式调用  如果是一个promise对象(代表又是一个异步) 那么调用x的then方法 将resolve和reject传进去 等到x内部的异步 执行完毕的时候(状态完成)就会自动执行传入的resolve 这样就控制了链式调用的顺序
            x instanceof MyPromise ? x.then(resolve, reject): resolve(x);
          }
          catch(err) {
          	reject(err)
          }
        })
     })
    this.failFun.push((val) => {
        try {
          //    执行回调函数
          let x = rejectCallback(val);
          x instanceof Mypromise ? x.then(resolve, reject) : reject(x);
        } catch (error) {
          reject(error);
        }
      });
  }
}

手写all和race

static all(promiseArr) {
	let result = [];
  let conunt = 0;
  return new Mypromise((resolve, reject) => {
  	for (let i = 0; i < promiseArr.length;i++) {
      Promise.resolve(promiseArr[i].then(
      	res => {
        	// 不能直接push,控制顺序一一对应
          result[i] = res;
          if(count === promiseArr.length) {
            resolve(result);
          }
        },
        err => {
          reject(err);
        }
      ) 
    }
  })
}

//race
 static race(promiseArr) {
 	 return new MyPromise((resolve,reject) => {
   	for (let i = 0; i < promiseArr.length; i++) {
    	Promise.resolve(promiseArr[i].then(
      	res => {
        	resolve(res)
        },
        err => {
          resject(err)
        }
      ))
    }
   })
 }

寄生组合继承

function Parent (name) {
	this.name = name;
  this.say = () => {
  	console.log(1111);
  }
}

Parent.prototype.play = () = {
	console.log(222)
}

function Child(name) {
	Parent.call(this);
  this.name = name;
}

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

归并排序

function mergeSort(arr) {
	var len = arr.length;
  if(len < 2) {
  	return arr
  }
  var middle = Marg.floor(len/2);
  var left = arr.slice(0, middle);
  var right = arr.slice(midddle);
  
  return  merge(mergeSort(left), mergeSort(right))
}

function merge (left, right) {
  var result = []
  while(left.length > 0 && right.length > 0) {
  	if(left[0] < right[0]) {
      result.push(left.shift())
    } else {
      result.push(right.shift())
    }
  }
  while(left.length) result.push(left.shift())
  while(right.length) result.push(right.shift())
  return result
}

冒泡排序

function bubbleSort(arr) {
	var len = arr.length;
  for (let i = 0; i < len; i++ ) {
  	for(let j = i + 1; j< len; j++) {
    	if(arr[i] > arr[j]) {
      	var temp = arr[i];
        arr[i] = arr[j]
        arr[j] = temp
      }
    }
  }
  return arr
}

快速排序

function quickSort(arr) {
	let len = arr.length;
  if(len <= 1) return arr;
  
  let privotIndex = Math.floor(len/2);
  let privot = arr.splice(prvotIndex, 1)[0];
  let left = [];
  let right = [];
  for (let i = 0; i < len; i++){
  	if(arr[i] < privot) {
    	left.push(arr[i])
    } else {
    	right.push(arr[i])
    }
  }
  return quickSort(left).concat([privot],quickSort(right));
}

找到一个数组中第一个没出现的最小正整数

// o(n^2)
function firstMissingPos(nums) {
	let i = 0;
  let res = 1;
  while(i < nums.length) {
  	if(num[i] == res){
    	res++;
      i=0;
    } else {
    	i++;
    }
  }
  return res
}



// o(n)
	function firstMissingPos(nums) {
  	let set = new Set();
    for(let i = 0; i < nums.length; i++) {
    	set.add(nums[i])
    }
    for(let i = 1; i < nums.length + 1; i++) {
    	if(!set.has(i)) {
      	return i;
      }
    }
  }

Proxy与Object.defineProperty的优劣对比?

Proxy的优势如下:

  • Proxy可以直接监听对象而非属性
  • Proxy可以直接监听数组的变化
  • Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的
  • Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改
  • Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利

Object.defineProperty的优势如下:

  • 兼容性好,支持IE9

compose

function compose(...fn) {
	if(!fn.length) return v=> v;
  if(fn.length === 1 ) return fn[0];
  return fn.reduce(
  	(pre, cur) => {
    	...args => pre(cur(...args));
    }
  )
}

数组扁平化

//递归
function flatter(arr) {
	if(!arr.length) return;
  return arr.reduce(
  	(pre, cur) => 
    Array.isArray(cur) ? [...pre, flatter(cur)] : [...pre, cur];
  )
}


//迭代
function flatter(arr) {
  if (!arr.length) return;
  while (arr.some((item) => Array.isArray(item))) {
    arr = [].concat(...arr);
  }
  return arr;
}

通过时间分片方式解决简单dom结构大数据量渲染

let ul = document.createElement('ul');
let total = 100000;
let once = 20;
let index = 0;
let page = total/once;


function loop(curTotal, curIndex) {
	if(curTotal <= 0) {
  	return false
  }
  //每页多少条
  let pageCount = Math.min(curTotal, once);
  
  window.requestAnimationFrame(()=>{
  	for(let i = 0; i < pageCount; i++) {
    	let li = document.createElement('li')l;
      li.innerText = curindex+i;
      ul.appenChild(li)
    }
    loop(curTotal - pageCount, curindex +  pageCount)
  })  
}

loop(total, index);

一维数组&树形结构互转

//转树
const arrayToTree = (arr, pid) => {
  return arr.reduce((res, current) => {
    if (current['pid'] === pid) {
      current.children = arrayToTree(arr, current['id']);
      return res.concat(current);
    }
    return res;
  }, []);
};
console.log(arrayToTree(list, 0))

//转数组
let list = [];
    function getList(treeData){
        treeData.map((item,index)=>{
            let { node_id, name, parent_id} = item
            list.push({ node_id, name, parent_id})
            if(item.children.length>0){
                getList(item.children)
            }
        })
  }

a = [1, 2, 3, 4, 5, 6] 右移动 n 位, n >=0 , 求移动后的数组

function moveElement(arr, n) {
  if(Math.abs(n)>arr.length) n = n%arr.length
  return arr.slice(-n).concat(arr.slice(0,-n))
}

// moveElement(arr, 9)
// moveElement(arr, 0)
// moveElement(arr, -9)

取数组交集

// 两个数组的交集
const interSection = (arr1, arr2) => {
	const s = new Set(arr2);
  return [... new Set(arr1)].filter(x => s.has(x));
}

//多个数组的交集

const interSection = (arr) => {
	return arr.reduce((per,cur) => per.filter(item => cur.includs(item)))
}