Skip to content

面试集合

image-20211229234032345

正如大圣老师的面试学习法,让我收获颇丰。确实是给了自己耻辱感,让知道了自己水平大概在什么段位,确实是该学习了!

一轮-骄兵必败

耻辱的我熬夜写下了这篇文章。😒😒😒😒😒😒

一个前端大牛朋友,找他准备了一些面试题给我做了一次模拟面试,本以为应该不会太难,所以我压根没有做一点的复习。反而觉得这就是个模拟不用太在意,不会难到哪里去我应该大部分都会。

正所谓骄兵必败!总结下来还是因为自己确实没有花时间去准备,导致自己看待面试的态度出现了问题。狠狠的打了自己的脸。

害~该学习了!

工作中遇到的难点,如何解决?

我:项目中遇到的难点其实也有蛮多的,只不过大部分的问题都是可以通过查看文档来进行解决的,之前有遇到过小程序处理并发请求的问题,后面是通过调研,使用一个库,实现将请求挂起,逐个进行请求。

第一个问题真的太重要了,虽然现在写的就那么几十个字,但是我的表达....一言难尽,过程中咿咿呀呀的含糊不清的讲了好久。以致于后面的问题让我感觉可能不会过,状态太差。所以第一个问题一定要回答好!。

说出 ES6 新增的数组的方法?

我:按说这种题目应该是送分题的,可是没有错,我居然翻车了,我说的是mapreduceconcatfilter

面试官:没有一个是 ES6 的

我.......

正确答案:

实例方法:findfindIndexincludesoncesome

原型方法:Array.from()Array.of()Array.entries(),Array.keys()Array.values()

总结:看到答案的我吐血了,这些方法真的每一个我都会用也都能说出来分别的不同点,只是让我说一个个说出来我卡壳了....对不起看了红宝书和犀牛书。

从性能考虑,数组插入元素从头插入性能高还是从尾插入性能高

听到这题的时候我是一脸懵逼的,真的这是啥题,我真的不知道啊

我:数组插入元素应该是从头插入性能更低吧(回答的非常的不自信,含糊其辞,就算蒙对了面试官肯定也不会有什么好感的)

正确答案:

在数组的起始位置插入和删除元素的性能是更低的,因为在内存中,数组是一块连续的区域。

  • 插入数据时,待插入位置的的元素和它后面的所有元素都需要向后搬移
  • 删除数据时,待删除位置后面的所有元素都需要向前搬移

所以这题考察的还是基本常识,要知道数组在计算机内存中存储的方式。

对象深拷贝方法?

我:通过类似于{...obj}解构,只答出了这一个,如果是数组我可能还能憋出一个Array.from()

正确答案:

ES6 结构、Object.assign({} ,obj1)Object 原型方法、JSON.parse(JSON.stringify(obj))JSON 方法

闭包是什么,你认为的闭包最大的作用是什么?

我:因为 JS 采用的是词法作用域,也就是说只有一个函数或者变量的作用域取决于你在书写代码之后,而不是函数在哪里执行,当函数执行的作用域和函数书写时的作用域不在同一个地方的时候闭包就产生了。

我以为我说到这里就可以了,面试官继续问你觉得闭包最大的作用是什么?

我:...沉默了蛮久的,我说在我的想法中 JS 闭包最大作用是成就异步特性,因为所有的回调函数本质上都是闭包。

正确答案:

闭包的作用:

  • 变量长期驻扎在内存当中(一般函数执行完毕,变量和参数会被销毁)
  • 避免全局变量的污染

Vue 响应式原理

我是看过源码的,并且也是写过整个迷你响应式代码的,我的这个问题回答的跟个屎一样。

我:Vue3 是通过 Proxy 这个代理机制实现响应式,每当对象触发[[GET]][[PUT]]操作的时候都可以对数据进行拦截进行数据的更改

面试官:能说说具体[[GET]][[PUT]]Vue3 是怎么做的吗

我:Vue3 主要是通过代码,来实现拦截,当[[GET]]的时候会触发一个依赖收集的过程,这个收集的时候就会触发一个个的副作用更新函数,当通过[[PUT]]修改的时候就会找到一个依赖收集关系,去触发的依赖的一个个副作用函数。

我的回答问题的能力真的很差,感觉有点像是挤牙膏一样,按道理这个应该是我一口气说下来的,谁都知道是通过Proxy,但是应该要自己把所有的东西都说出来,而不是需要别人提醒你返回推敲。

除了 Vue 的这种观察监听的设计模式,你还知道什么设计模式

之前刷掘金有专门刷到一篇 JS 涉及模式文章,里面讲了很多,但是还是老毛病看了就忘。

我:涉及模式我知道有蛮多的,但是除了 Vue 的这个我目前能说出来的可能只有一个 单例模式

面试官:那你说下单例模式是如何实现的,你在什么时候有用到过

我:在写自己毕设的时候那时候用 node 搭建后台服务器 API 时候,增删改查接口都涉及到连接数据库,那么连接数据库这个操作就需要使用单例模式,因为连接数据库只需要一次。实现单例模式可以简单的使用一个类,然后写一个类的静态方法,通过静态方法和静态属性实现单例

面试官:除了用类还有没有用其他更简单的方法

我:除了用类我可能还会用函数吧,因为类本质上是函数的语法糖。

正确答案:

js 设计模式有

  • 单例模式
  • 策略模式
  • 代理模式
  • 中介者模式
  • 装饰者模式

除了单例和策略代理有听过,别的都没有听过,好像有专本一本书写 js 设计模式的,有空一定要看下

实现单例模式的方法:

查了一下居然有 6 种:

  • instanceof

    js
    function User() {
    	if (!(this instanceof User)) {
    		return
    	}
    	if (!User._instance) {
    		this.name = '无名'
    		User._instance = this
    	}
    	return User._instance
    }
    
    const u1 = new User()
    const u2 = new User()
    
    console.log(u1 === u2) // true
    function User() {
    	if (!(this instanceof User)) {
    		return
    	}
    	if (!User._instance) {
    		this.name = '无名'
    		User._instance = this
    	}
    	return User._instance
    }
    
    const u1 = new User()
    const u2 = new User()
    
    console.log(u1 === u2) // true
  • 在函数上直接添加方法属性调用生成实例

    js
    function User() {
    	this.name = '无名'
    }
    User.getInstance = function () {
    	if (!User._instance) {
    		User._instance = new User()
    	}
    	return User._instance
    }
    
    const u1 = User.getInstance()
    const u2 = User.getInstance()
    
    console.log(u1 === u2)
    function User() {
    	this.name = '无名'
    }
    User.getInstance = function () {
    	if (!User._instance) {
    		User._instance = new User()
    	}
    	return User._instance
    }
    
    const u1 = User.getInstance()
    const u2 = User.getInstance()
    
    console.log(u1 === u2)
  • 使用闭包,改进方式 2

    js
    function User() {
    	this.name = '无名'
    }
    User.getInstance = (function () {
    	var instance
    	return function () {
    		if (!instance) {
    			instance = new User()
    		}
    		return instance
    	}
    })()
    
    const u1 = User.getInstance()
    const u2 = User.getInstance()
    
    console.log(u1 === u2)
    function User() {
    	this.name = '无名'
    }
    User.getInstance = (function () {
    	var instance
    	return function () {
    		if (!instance) {
    			instance = new User()
    		}
    		return instance
    	}
    })()
    
    const u1 = User.getInstance()
    const u2 = User.getInstance()
    
    console.log(u1 === u2)
  • 使用包装对象结合闭包的形式实现

    js
    const User = (function () {
    	function _user() {
    		this.name = 'xm'
    	}
    	return function () {
    		if (!_user.instance) {
    			_user.instance = new _user()
    		}
    		return _user.instance
    	}
    })()
    
    const u1 = new User()
    const u2 = new User()
    
    console.log(u1 === u2) // true
    const User = (function () {
    	function _user() {
    		this.name = 'xm'
    	}
    	return function () {
    		if (!_user.instance) {
    			_user.instance = new _user()
    		}
    		return _user.instance
    	}
    })()
    
    const u1 = new User()
    const u2 = new User()
    
    console.log(u1 === u2) // true
  • 在构造函数中利用 new.target 判断是否使用 new 关键字

    js
    class User {
    	constructor() {
    		if (new.target !== User) {
    			return
    		}
    		if (!User._instance) {
    			this.name = 'xm'
    			User._instance = this
    		}
    		return User._instance
    	}
    }
    
    const u1 = new User()
    const u2 = new User()
    console.log(u1 === u2)
    class User {
    	constructor() {
    		if (new.target !== User) {
    			return
    		}
    		if (!User._instance) {
    			this.name = 'xm'
    			User._instance = this
    		}
    		return User._instance
    	}
    }
    
    const u1 = new User()
    const u2 = new User()
    console.log(u1 === u2)
  • 使用 static 静态方法

    js
    class User {
    	constructor() {
    		this.name = 'xm'
    	}
    	static getInstance() {
    		if (!User._instance) {
    			User._instance = new User()
    		}
    		return User._instance
    	}
    }
    
    const u1 = User.getInstance()
    const u2 = User.getInstance()
    
    console.log(u1 === u2)
    class User {
    	constructor() {
    		this.name = 'xm'
    	}
    	static getInstance() {
    		if (!User._instance) {
    			User._instance = new User()
    		}
    		return User._instance
    	}
    }
    
    const u1 = User.getInstance()
    const u2 = User.getInstance()
    
    console.log(u1 === u2)

数组和链表的区别

我听到这个问题想的是,天哪这个问题我应该要怎么回答,好难,不会答。

我:链表的数据结构是存储值和下一个值的指针,而数组是维护一个索引下标,硬要说区别的话就是遍历的区别,链表没有办法从中间取值,要找到一个值就必须先获取这个值的前一个值。

正确答案:

  • 在内存中,数组是一块连续的区域
  • 在数组起始位置处,插入数据和删除数据效率低。
  • 查找速度快,时间复杂度为 O(1)
  • 链表在内存中,元素的空间可以在任意地方,空间是分散的,不需要连续
  • 查找数据时效率低,时间复杂度为 O(N):因为链表的空间是分散的,所以不具有随机访问性,如要需要访问某个位置的数据,需要从第一个数据开始找起,依次往后遍历,直到找到待查询的位置,故可能在查找某个元素时,时间复杂度达到 O(N)
  • 任意位置插入元素和删除元素效率较高,时间复杂度为 O(1),因为只需要改变指针的指向即可
  • 链表的空间是从堆中分配的,数组的空间是从栈分配的

插入排序、选择排序、冒泡排序的时间复杂度

我:这三个排序我都会写,但是具体的孰优孰劣我一时没有办法说出来,只知道它们都是二层循环,冒泡排序的时间复杂度应该是最高的吧,每次都需要亮亮比对

我这个答的就非常的不好,首先是回答问题不自信,然后我确实也没有去刻意背这个,不过算法我是真的都写过这三种排序

冒泡排序

冒泡算法是最基础的一个排序算法

两层循环,每次两两进行比对。

js
// 冒泡排序
function bubbleSort(arr) {
	let temp
	for (let i = 0; i < arr.length; i++) {
		for (let j = i + 1; j < arr.length; j++) {
			if (arr[j] < arr[i]) {
				temp = arr[i]
				arr[i] = arr[j]
				arr[j] = temp
			}
		}
	}
	return arr
}
// 冒泡排序
function bubbleSort(arr) {
	let temp
	for (let i = 0; i < arr.length; i++) {
		for (let j = i + 1; j < arr.length; j++) {
			if (arr[j] < arr[i]) {
				temp = arr[i]
				arr[i] = arr[j]
				arr[j] = temp
			}
		}
	}
	return arr
}

选择排序

第一轮 从数组第 0 个元素开始遍历 在遍历的过程中如果找到最小的值 就将这个最小的值 和数组的第 0 个值进行替换

第二轮 从数组第 1 个元素开始遍历 在遍历的过程中如果找到最小的值 就将这个最小的值 和数组的第 1 个值进行替换

....

最终数组循环遍历结束之后 数组也就拍好了

需要使用两层循环 时间复杂度为 ON2

选择排序适用于数组量小的情况下 好处是操作的一直都是同一个数组 时间换空间的方式

js
function selectSort(arr) {
	let minIndex
	for (let i = 0; i < arr.length; i++) {
		minIndex = i
		for (let j = i + 1; j < arr.length; j++) {
			if (arr[j] < arr[minIndex]) {
				minIndex = j
			}
		}
		temp = arr[i]
		arr[i] = arr[minIndex]
		arr[minIndex] = temp
	}

	return arr
}
function selectSort(arr) {
	let minIndex
	for (let i = 0; i < arr.length; i++) {
		minIndex = i
		for (let j = i + 1; j < arr.length; j++) {
			if (arr[j] < arr[minIndex]) {
				minIndex = j
			}
		}
		temp = arr[i]
		arr[i] = arr[minIndex]
		arr[minIndex] = temp
	}

	return arr
}

插入排序

这个排序的场景是十分的像我们打扑克的时候的场景,

抽一张牌时(current) 会一个个的和前面的牌进行比较

直到找打比这个牌小的地方(preIndex) 插入进去 后面的全部往后挪动一位

这个排序也比较好理解 时间复杂度也是 ON2

js
function insertSort(arr) {
	let length = arr.length
	let preIndex, current
	for (let i = 1; i < length; i++) {
		preIndex = i - 1 // 记录前一个数的索引
		current = arr[i] // 记录比较的这个值
		while (preIndex >= 0 && arr[preIndex] > current) {
			// 在这个循环下 如果前一个数比当前这个数大 则后一个将前一个数覆盖
			arr[preIndex + 1] = arr[preIndex]
			preIndex--
		}
		// 当循环结束 说明找到了需要替换的位置 将这个位置的值替换成最初存的那个current即可
		arr[preIndex + 1] = current
	}
	return arr
}
function insertSort(arr) {
	let length = arr.length
	let preIndex, current
	for (let i = 1; i < length; i++) {
		preIndex = i - 1 // 记录前一个数的索引
		current = arr[i] // 记录比较的这个值
		while (preIndex >= 0 && arr[preIndex] > current) {
			// 在这个循环下 如果前一个数比当前这个数大 则后一个将前一个数覆盖
			arr[preIndex + 1] = arr[preIndex]
			preIndex--
		}
		// 当循环结束 说明找到了需要替换的位置 将这个位置的值替换成最初存的那个current即可
		arr[preIndex + 1] = current
	}
	return arr
}

总结:

  • 三者的时间复杂度都是 O(n2)

  • 冒牌排序的效率最低

  • 当数组量小的时候,应该优先使用选择排序

二叉树左右子树呼唤

我:不好意思这个我没有写过这个,不过这个问题应该也是和二叉树的遍历差不多可以用递归的方式进行解决的。

我觉得这个问题我不会应该坦诚的说我不会,但是我又间接的告诉面试官我会二叉树的遍历,我不知道这样的回答问题的模式是否是不好的,有点不懂装懂的感觉。

不用递归,使用循环的方式完成二叉树的先序遍历

可能因为我说了递归解决二叉树的遍历,所以面试官特意问我通过循环遍历......,万马奔腾。

我:面试官不好意思,循环的方式我没有写过,我直接解决二叉树遍历问题都是通过递归的方式来进行解决的。

递归二叉树先序遍历

js
var preorderTraversal = function (root) {
	let res = []
	const inOrder = root => {
		if (!root) {
			return
		}
		res.push(root.val) // 先存
		inOrder(root.left) // 进入左节点
		inOrder(root.right) // 进入右节点
	}
	inOrder(root)
	return res
}
var preorderTraversal = function (root) {
	let res = []
	const inOrder = root => {
		if (!root) {
			return
		}
		res.push(root.val) // 先存
		inOrder(root.left) // 进入左节点
		inOrder(root.right) // 进入右节点
	}
	inOrder(root)
	return res
}

循环二叉树先序遍历

这个我确实不会,有空再刷刷题吧研究一下。

总结

这一场面试是让我感觉备受耻辱,问题就没有几个算是答上来的,就算过了就这个面试情况也失去和 HR 讨价还价的资本了。除了算法的问题其他的问题问的真的不难,我居然还是翻车了,亏自己看书看了很多遍还是不能应付下来。也是跟自己自负有点关系了,程序员不能太气盛,骄兵必败。

  • 磨练自己的嘴皮子,软技能真的太重要了,表达能力真的是差到不行!
  • 认真对待面试,就算是模拟,也应该跟真的面试一样,即使准备和复习基础知识。
  • 算法题还是得继续坚持下去,这回问算法不算一点都没有答出来,但是总体还是差!

二轮-其实我都不会

简单的寒暄,直接进入主题 😒😒😒

0 == null 答案是 true 还是 false (两个等号)

我:应该不等吧(含糊的回答) 面试官:到底相等还是不相等? 我:应该相等,因为他们都会转成 false 进行比较。

正确答案

其实二者不相等。

很显然这题是面试官特意挖坑,在说题目的时候特意跟我说两个等号,就想着能快速的说出来,我还是入坑了......

0.1+0.2 会等于 0.3 吗

我:不等于,会等于 0.300000000...04。

面试官:为什么?

我:呃...因为 Number 类型底层其实是用二进制进行表示的,所以会有精度问题

面试官:那怎么解决?

我:不好意思我没有试过,如果我处理我会手动封装一个add方法,然后单独判断 0.1+0.2 的情况返回 0.3

正确答案

Number 类型本质上是二进制浮点数,表示整数的时候一点问题都没有,而表示小数的时候就会有精度的问题。如:

  • 二进制表示整数采用的是: 除 2 取余法
  • 二进制表示小数采用的是: 乘 2 取整法
js
// toString(2)表示转成2进制  toString(10)表示转成10进制
Number(0.2).toString(2) // 将0.2转为二进制 '0.001100110011001100110011001100110011001100110011001101'
// toString(2)表示转成2进制  toString(10)表示转成10进制
Number(0.2).toString(2) // 将0.2转为二进制 '0.001100110011001100110011001100110011001100110011001101'

为什么是这个数字呢?其实使用的是 乘 2 取整法

0.2 * 2 = 0.4 取整数部分0 小数部分0.4
0.4 * 2 = 0.8 取整数部分0 小数部分0.8
0.8 * 2 = 1.6 取整数部分1 小数部分0.6
0.6 * 2 = 1.2 取整数部分1 小数部分0.2
0.2 * 2 = 0.4 取整数部分0 小数部分0.4
0.4 * 2 = 0.8 取整数部分0 小数部分0.8
0.8 * 2 = 1.6 取整数部分1 小数部分0.6
.....
0.0011001.... 无限循环
所以本质上取小数时有时候是取得不是精确数是近似数,近似数相加就会出现不精准的时候。
0.2 * 2 = 0.4 取整数部分0 小数部分0.4
0.4 * 2 = 0.8 取整数部分0 小数部分0.8
0.8 * 2 = 1.6 取整数部分1 小数部分0.6
0.6 * 2 = 1.2 取整数部分1 小数部分0.2
0.2 * 2 = 0.4 取整数部分0 小数部分0.4
0.4 * 2 = 0.8 取整数部分0 小数部分0.8
0.8 * 2 = 1.6 取整数部分1 小数部分0.6
.....
0.0011001.... 无限循环
所以本质上取小数时有时候是取得不是精确数是近似数,近似数相加就会出现不精准的时候。

一个函数先执行bind()后执行call()最终 this 指向的是bind绑定的对象还是call绑定的对象

在还没有听到具体题目前,听他说函数 call、apply、bind 我以为我稳了,这个太简单了我都会,结果没想到并没有单独的问我,而是组合起来问我......

我:这个我没有试过,不过应该是call()应该call()是后面执行的。

面试官:是bind(),你下去了以后可以试验一下

之后我看了 《JavaScript 忍者秘籍》 发现其中在函数中有一段话,一旦一个函数通过bind()确定了 this 指向之后就无法再进行修改了。所以这个考的真的蛮....细节的,真的会有人先 bind 再 call 这样用嘛。

一个函数,连续的bind()了三次,那最终 this 指向的是最初的那个还是最后的那个?

我:应该是最初的那个。

面试官:为什么?

我:因为上一题我答错了,感觉bind()应该是效果更强的,不能被再次覆盖。

面试官:是的,建议下去之后手动的实验一下。

一旦一个函数通过bind()确定了 this 指向之后就无法再进行修改了。

给你 10 秒钟,看看你一口气能说出多少数组的方法

我:map、filter、forEach、some、find、findIndex、reduce、sort、concat、includes、keys、values、entrys

面试官:还有吗?

我:没有了。

面试官:少了,还有 push、pop、reserve、indexOf 等等,这题要考的其实是看你日常编码时的积累,写的多你就能临时说的多。

我又傻了,居然连数组最基本的 push、pop 之类的都没有说出来。可能会让面试官觉得我是个背题的,连最经常用的都不知道。

你知道的 js 类型转换有什么方法?比如String()

我:Number、Boolean、!!、+、!

好像有的并不止这些,还有toString()

一个构造函数在 new 的过程中都做了什么?

我:先是创建一个空对象,然后将构造函数中 this 有绑定的东西都绑定到这个空对象上,并将这个空对象的[[proto]]指向构造函数的prototype

其实这题我答的不好,标准答案应该是:

  • 先创建一个空对象
  • 将这个空对象作为 this 传入构造函数中
  • 将绑定之后的结果作为new 操作符的结果进行返回

你知道对象的construcotr属性吗,代表的是什么?

我:对象的constructor一般情况下代表的是这个对象是由哪个构造函数所实例化出来的,但是这个属性是不可靠的,是可以被显示的修改的。

demo:

js
function User() {}
function CopyUser() {}

let jimmy = new User()
jimmy.constructor = CopyUser
console.log(jimmy.constructor) // CopyUser
function User() {}
function CopyUser() {}

let jimmy = new User()
jimmy.constructor = CopyUser
console.log(jimmy.constructor) // CopyUser

看!通过以上的例子,一个对象的constructor居然能被我们手动的给修改!所以对象的constructor是不可靠的,并不能代码这个对象一定是根据这个构造函数所实例化出来的。

setTimeOut(()=>{},0)能立即执行吗

我:不能,因为解析器在解析代码时,遇到setTimeOut会将执行的内容放入任务队列,等到主线程的代码执行结束之后才会轮询异步队列,所以就算时写 0 也不会立刻执行。

如何让 script 标签不阻塞文档的解析

我:可以通过让 script 标签加上asyncdefer属性,可以让 script 标签异步的进行加载。

面试官:那二者有什么区别?

我:asyncdefer都是后台下载,区别在于,当async script后台下载好之后会立即停止文档的解析,转而去解析这个 script,而defer是会在整个文档解析完成之后了,再去解析这个defer script

这题考的就是async scriptdefer script 异步脚本的知识。

现在流行的 <script type="module"></script> 默认情况是加 defer 这个属性的

preload 和 prefetch 资源提示符作用

这两个资源提示符都是用于 link 标签的,同样也是异步的,在解析 html 时遇到这个也不是阻塞文档的解析,他会预先去加载我们将来要用的资源(图片、js、css 资源等其他资源),之后如果 image 标签用到这个资源的时候就可以直接取缓存,而不需要去再次触发网络请求。

html
<link rel="preload" href="/css/about.css" />

<link rel="prefetch" href="xxx.jpg" />
<link rel="preload" href="/css/about.css" />

<link rel="prefetch" href="xxx.jpg" />

preload 和 prefetch 二者的区别在于,preload 浏览器会认为可能是立马就需要用的资源,所以会以一个更高的优先级去先拿资源,而 preload 则是会选择在空闲的时间去获取数据。

有什么办法能让 setTimeOut 立刻执行

我:不好意思我没有处理过,不会,如果有这种需求通过手写回调函数?

面试官:这题其实想考你对 js 的理解,js 为什么有异步队列呢?因为是单线程,不异步处理会阻塞,所以这题思路应该是往线程的方向处理,js 单线程那变成多线程不就可以了,现在不是有webWorker新 API 可以再开一个线程吗,所以是想考你有没有熟悉一些新的 API

我傻了,webWorker 其实看书啥的有看到过,也知道主要是处理什么问题的,但是确实是自己没有用过,所以很难答的出来,也不知道面试官其实要考我的是这个。

结束扎心时刻

面试官:好了今天就问到这里吧,我大概知道了,你的 JS 基础还是相对薄弱的,我这都还没有问你框架源码呢。

我:不好意思我有点紧张,有些问题没有答好。

害!我这一年看了 红宝书、犀牛书、小黄书,结果居然被说 js 还是太薄弱了.....,雷霆一击一般,不过总结下来我也知道了我的薄弱项,看书喜欢跳过,遇到看不懂的也是直接跳过,这个习惯其实不好,所以下定决心,这回的 《JavaScript》忍者秘籍 我一定耐心的看下去,尽量不跳过。

不过确实,全问 JS 我都没有答好,还没问框架源码和算法呢,惊了。

还有一点是现在面试公司基本都问设计模式,所以安排了一本 **《前端架构-从入门到微前端》**这本也是 程序员鱼皮 推荐的一本架构和设计模式的书籍,忍者书看完就看。

最后,我以为我必挂,感谢领导,结果居然过了......

这次感觉还行

看你项目做了挺多,说说你是怎么优化你们项目的,比如网站白屏时间应该如何处理?

我:网站包比较大吧,可能会发生这种情况,可以上一些 cdn、把一些图片资源放在 oss 上。

这个问题感觉无数次的在一些博客论坛上看到,但是就是不知道怎么回答,感觉第一个问题就没答好

你在项目开发中有遇到过什么问题吗,是如果解决的?

我:细说了我处理列表页-详情页返回时保存状态的问题和处理方案,以及微信小程序开发小游戏时遇到的问题和解决方案

感觉这回的不错,之前面试挺怕被问到项目的,有了这两个项目经验之后感觉暂时可以应付这种问题了。

看你用 react 开发挺多,知道 react.memo 吗,为什么使用它可以实现性能优化

我:react.memo是用于优化组件的,由于 react 的渲染机制,每次状态改变都会触发重新渲染,而组件的是否渲染无非就是组件的 props 或者 context 发生改变,使用memo就是改变每次比对的条件判断方式。

使用memo之前,{} 和 {} 相比较是否相等,正常情况下是不等,所以会更新

使用memo之后,{} 和 {}相比较是否相等,就是相等的,所以不会触发更新

总结下来就是 深比较浅比较 的区别

运气比较好,几个星期前刚好看了 B 站一个叫 卡颂 的 up 主分享这个。

看你实现过一个叫useFetch的 hooks,这是干什么的,如何实现的

我:在开发后台是我们需要处理大量的列表页面,涉及到很多的筛选项,页数等等过滤信息,使用useFetch我们能够更加轻松的发送网络请求,和传统思路比我们只需要修改每次筛选项的参数即可,就会自动帮助我们触发网络请求,提升效率。

如果实现其实就是封装一个函数,这个函数我们依赖了一些 react 本身提供的一些 hooks,像 useEffect,等等

因为自己实现过公司 hooks 的代码,所以敢写,hooks

对框架源码是否有了解,能否说下 vue 在渲染的过程中都做了哪些事情

我:

  • 通过createApp 方法会创建一个 app 实例,实例上有mount()挂载方法,根据createApp方法传入的配置项创建一个vnode。将这个vnodemount()传递的 DOM 根节点一起传入 render渲染函数进行页面的渲染
  • 因为是首次挂载,所以 render 函数会将 这个vnode,渲染到宿主 DOM 中,这里会执行到mountComponent()挂载组件函数中,这个函数做了以下几件事情:
    • 先创建一个组件实例instance
    • 给组件的实例instance加工一下,绑定propsctx、添加组件的render渲染函数。
    • 根据组件中的inMounted值判断组件是否挂载过,如果挂载过就执行更新操作,如果未挂载过就执行初次渲染操作,执行结束之后将isMounted设置为true.
    • instance.update接入reactivity响应式系统,所以会长存,发生一些响应式值变化时会自动触发componentUpdateFn()方法。

比较碰巧也是几天刚看了 Vue 相关知识,这里推荐一个开源项目:mini-vue,也推荐一个我正在跟着 mini-vue 这个项目从头写的学习源码的仓库:vue-mini

vue 的 mixin 方法知道吗,说说它和 compositionAPI 的区别

我:对比mixin,组合基本可以实现mixin 的所有能实现的内容,可以更加的方便,还可以避免一些不好的事情:

  • 来历不明的属性

    如果 mixin 使用多了,我们可能还需要判断一下一些属性是从哪里 mixin 来的,比较复杂

  • 命名空间的重复

    如果 mixin 的属性名和原本自身的属性名重复,也是会出现问题的。

刷文档是有用的,最近学英语就在看 vue3 文档,刚好前两天晚上看过了

那么说说 compositionAPI 和 React Hooks 的区别

我:不好意思面试官,这个我不太清楚,只知道 compositionAPI 本质上基于自身的响应式系统的。

这个确实不会......

说说 css 中重绘和重排的区别

我:这是个性能优化的小点,如果使用不当或多或少会造成一些性能问题,重绘的影响大于重拍,比如我们如果想隐藏某个东西,如果条件允许我们可以设置透明度为 0,而不是直接卸载掉这个元素,因为卸载掉会导致页面的其他元素也跟着一起动起来,造成不必要的浏览器渲染压力,浪费资源等等。

感觉说的不够细节,但是应该够用了

你知道闭包的概念吗,有什么用,你平常用在哪里

我:闭包能够使我们变量长存不会被 JS 的垃圾回收机制所清理掉,此外还能避免一些变量的全局污染。日常开发中我好像用到闭包的点还挺少的,不过如果是需要封装一些带状态的函数的情况下就可以使用闭包

这个是一个老题了,上次面试没答出来,这回总算是答出来了

箭头函数能否被实例化,为什么?

我:不能实例化,因为箭头函数没有自己的 this

这个做完接着问了 call 和 apply 的用法,这个属于基础题了,正常答出来了。

说说宏任务和微任务的区别

我:日常使用的像定时器,事件监听这类都是宏任务,而 Promise 这种属于微任务,他们一起配合组成 JS 的事件轮询机制。整个执行过程大致是这样,先处理主线程,过程中可能会随时向宏任务队列和微任务队列添加任务,接着会检查宏任务队列,一旦宏任务队列处理完一个队列之后会理解去轮询微任务队列,直到微任务队列全部出队之后会再返回轮询宏任务队列(过程可能会继续向微任务队列中添加任务),不断的循环这个过程。

没啥好说的,JS 小黄书上写的非常的详细。

防抖和节流是干什么的,如果是你自己写一个防抖和节流你会怎么写

我:防抖是一段时间内的操作只以最后一次为有效。最佳使用场景就是搜索框的联想搜索;节流是一段时间的操作只以第一次有效,使用场景比如菜单的页面优惠券的小 logo(正常情况下是开启的,如果滑动了就关闭);如果是自己写一个防抖和节流也是挺容易的,只要我们通过设置定时器就可以实现。

这个问题也是经常被问到,正常答出来了。防抖节流手写代码

平常开发中都会使用到哪些库

我:日常使用的比较多的有像lodash这种比较经典的库,还有使用classNames这个库,react 中写类能够写的比较优雅,还有就是像最近新在用的一个原子化 css 库tailwindCss也是非常的给力

你英语水平怎么样

我:过了四级了

面试官笑了

最后的的谈话

最后和面试官进入聊天环节,大致的知道了一下我的情况,面试官说我答的总体上很不错。坐等情况吧~

忆之获

感觉日常写的一点小 demo 还是挺有用的,这回面试也确实是裸面,没用做什么准备,刚好日常在学和在做的东西还是很有用的,比如看文档!很多人就是不看文档!

image-20220608234840632

还有一个是自己平常优化方面的处理确实挺少,这个日后也是一个学习的点。

理论常见问题

前端工程化理解

资源来于:我对前端工程化的理解

前端工程化的主要目的是为了提高效率和降低成本,即提高开发过程中的开发效率,减少不必要的重复工作时间

如何做前端工程化?

  • 模块化

    • js 模块化
    • css 模块化
    • 静态资源模块化
  • 组件化

    业务单独抽离出组件,方便日后开发

  • 规范化

    • 目录结构规范
    • 接口规范
    • 文档规范
    • git 规范
    • ...
  • 自动化

    • CICD
    • 自动化测试

前端性能优化工作

主要体现在三个方面:

  • 网络优化(对加载时所消耗的网络资源优化)
  • 代码优化(资源加载完后,脚本解释执行的速度)
  • 框架优化(选择性能较好的框架)

tree shaking

如果使用 ES 模块化项目,可以利用它清除一下我们项目中无用的代码

ts
import _ from 'lodash' // 会将整个 lodash 打入项目 bundle 中
import _isEmpty from 'lodash/isEmpty' // 只打包 isEmpty 会极大的缩小包的体积
import _ from 'lodash' // 会将整个 lodash 打入项目 bundle 中
import _isEmpty from 'lodash/isEmpty' // 只打包 isEmpty 会极大的缩小包的体积

选择工具拓展包时也需要注意,优先使用es类型的包,如lodash-es的优先级应该高于lodash

按需加载

组件之类的按需加载

gzip

服务端配置 gzip 压缩后可大大缩减资源大小。可在 response header 中可以查看。

图片压缩

最常见的一个方式

使用精灵图

减少图片加载

CDN

很多静态资源都可以放在 CDN 上,加快访问的速度。

以前买火车票大家都只能去火车站买,后来我们买火车票就可以在楼下的火车票代售点买了。

懒加载

只展示视图需要的资源,别的用到了再加载,是一种非常好的优化网页性能的方式。

iconfont 字体图标

轻量级、矢量图、不占用图片资源请求。

webWorker

多一个线程,处理一些非常消耗性能的逻辑。

缓存

浏览器缓存、CDN、反向代理、本地缓存、分布式缓存、数据库缓存

http 301 302 304 的区别

300 系列的都是重定向。

  • 301 永久重定向,表示请求的资源分配了新的 url,以后应使用新 url
  • 临时重定向,请求的资源临时分配了新的 url(response 中 location 所指的地址),本次请求暂时使用新 url

服务器返回 302 时,也会返回 location,浏览器再次请求 location 中指定的地址,也就是浏览器请求了 2 次

  • 304:自从上次请求过后,网页未被修改过。客户端发送请求,有缓存则返回 304,客户端使用缓存资源

为什么小程序里拿不到 dom 相关的 api

在小程序中,渲染层和逻辑层是分开的,分别运行在不同的线程中,逻辑层运行在 JSCore 中,并没有一个完整浏览器对象,因而缺少相关的 DOM API 和 BOM API。