自整理前端手撕代码题
前言
本文适合在找前端实习或应届生入职的同学看
本篇文章为个人整理的前端面试中较高几率出现的手撕代码题,有些我的确在面试中遇到过,其中一些写出来了,一些因为少接触吃过亏,其他的是我暂时没遇见但是同学面经有出现,现在开一篇文章记录一下吧,帮自己也希望能帮到读者。
本文会根据个人所遇所见不时更新,如有错误或建议,欢迎私信或留言!
一、JS
1. 扁平化数组
描述:将多层嵌套数组降低层级,比如[ 1, 2, [3, [4, [5]]] ]扁平到[ 1, 2, 3, 4, 5 ]或者[ 1, 2, 3, [4,[5]] ]等等
emm很经典的一道代码题,我在面试中遇过,这道题我会介绍2种写法,分别是递归与concat函数写法。
递归写法(复杂一点)
// 原数组
const arr = [ 1, 2, [3, [4, [5] ] ] ];
// 自定义深度
const deep = 2;
// 结果数组
let result= [];
// 思想: 提取原数组每一项元素,然后判断元素为数字或数组,数字则推入暂存数组,数组则递归,判断推入
const myFlat = (arrInput, curDeep) => {
// 退出
if(curDeep==0){
return arrInput;
}
// 存放本层结果
let tempArr = [];
for(let i=0;i<arrInput.length;i++){
let item = arrInput[i];
if(Array.isArray(item)){
// 向下递归,结果层层返回
let returnArr = myFlat(item,curDeep-1)
tempArr = tempArr.concat(returnArr);
} else {
tempArr.push(item);
}
}
return tempArr;
};
result = myFlat(arr, deep);
console.log('DEEP: ',deep);
console.log('RESULT: ',JSON.stringify(result));
concat方法(推荐,内有我写的注释)
function useConcatToFlatten(arr, curDeep){
for(let i=0;i<curDeep;i++){
// 1. concat是合并返回一个新的数组
// 2. ...arr的作用等同于切割每一项元素成为独立参数,以逗号分开,控制台输出时不显示逗号
// 3. [].concat(...arr) = [].concat(n1,n2,n3...)
// 4. concat又可以合并嵌套数组,去掉每一项元素的一层嵌套,如果是[].concat(1, [2]) -> [[], 1, [2]] -> [1, 2]
// 5. arr = [].concat(...arr) 这种写法可以更新覆盖掉arr原本的结果,再走下一步运算。
arr = [].concat(...arr);
// console.log(...arr);// 这里可以自行查看
}
return arr;
}
let res2 = useConcatToFlatten([ 1, 2, [3, [4, [5] ] ] ], 2);
console.log('JSON_RESULT2: ',JSON.stringify(res2));
结果展示
2. 检测页面用了哪些标签
描述:获取页面所用标签,比如应含有<html><head><meta><style><body>等标签
同样,这里可以我所知道的有2种方法:从<html>开始宽度优先遍历,和通配符筛选。
关键知识:
- Node.childElementCount可以返回某节点子节点个数,Node.children为子节点集。
- document.getElementsByTagName(‘*’)可以获取所有标签
BFS遍历 从<html>标签开始
// 检测用了哪些标签...
let html = document.querySelector('html');
// console.log(html.nodeType) // type=1 代表是是元素节点
let resArr = [];
// 要 一个队列 + 一个集合去重 有点BFS的意思
// 只需要nodeName.toLowerCase()作为答案
let set = new Set();
// shift() 前 pop()后 并且还会返回那个元素哦
let queue = [];
queue.push(html);
// BFS
while(queue.length !== 0){
// Array.shift返回第一个元素并弹出
let nowNode = queue.shift();
let nowNodeName = nowNode.nodeName;
if(!set.has(nowNodeName)){
resArr.push(nowNodeName.toLowerCase());
set.add(nowNodeName);
}
// 然后是把子元素推入队列
let childCount = nowNode.childElementCount;
for(let i=0;i<childCount;i++){
let cur = nowNode.children[i];
queue.push(cur);
}
}
console.log(resArr);
通配符号并去重
// ByTagName'*' 是匹配全部 去重即可
console.log('-----------------');
let resOfTagName = document.getElementsByTagName('*');
console.log(resOfTagName)
let nodeArr = [];
for(let i=0;i<resOfTagName.length;i++){
nodeArr.push(resOfTagName[i].nodeName);
}
let resArray = [];
// 排序,然后通过和后者对比去重 当然这里也可以建立Set
nodeArr.sort();
for(let i=0;i<nodeArr.length;i++){
if(nodeArr[i]==nodeArr[i+1]){
continue;
} else {
resArray.push(nodeArr[i]);
}
}
console.log(resArray);
结果展示
3. 写一个函数判断传值是否为对象
描述:如题,注意JS分基本类型和引用类型,引用类型中Function和Array不算是对象
这题我给的解法不一定是最好的,这道题我面试中遇到过,可惜当初卡住了没能做到完美。
我的做法是:
1.先用typeof可以筛选null以外的基本数据类型+function,留下的null,array,object在typeof下都被分为object。
2. 因为null是基本数据类型,所以此时可以用 === 判断变量是否和null等,进而筛选。
3. 此时剩下的array和object,可以用Array.isArray()判断是否为数组。
4. 以上步骤到最后都通过才是Object的对象,才是true,不然其他算false。
// 判断对象
const t1 = "Ouch!";
const t2 = 114514;
const t3 = null;
const t4 = [1, 2, 3, 4];
const t5 = {name: 'Ouch', age: 21};
function myJudge(obj){
// 结果变量
let isObj = false;
// 筛选剩下null array object
if(typeof obj == "object"){
// 筛选null
if(obj !== null){
// 筛选array
if(!Array.isArray(obj)){
isObj = true;
}
}
}
// 输出
console.log(isObj);
}
myJudge(t1);
myJudge(t2);
myJudge(t3);
myJudge(t4);
myJudge(t5);
输出结果如右侧控制台
4. setTimeout+Promise问题
描述:写一个函数,利用Promise在2秒后返回一个值
唔,我在面试遇到过,大意为↑描述,当时一紧张忘了一些细节,比如setTimeout其实可以接受三个和更多数量的参数,前2个必须的是代码或者函数名用以在指定延迟时间执行,和指定的延迟时间,后面的是传值给第一个参数的代码或函数。
那么Promise呢,Promise最常见的用法就是在函数里return new Promise((resolve, reject) => {…}),
而最后结束是调用参数中的resolve或者reject函数,并且resolve和reject可以带参数回调,举例↓
回到题目,2秒后返回某值,肯定要用到setTimeout,剩下的问题就是谁包裹谁,完整的一个Promise操作是new Promise + resolve/reject + then回调,这里setTimeout只能包裹到resolve/reject才能有回调走完,看看代码↓
复盘又懊悔一遍当初没写出来,慢慢从鶸变强吧:(
5. 手写Promise.all方法
描述:实现Promise.all功能
Promise.all详细介绍MDN写的很清楚啦,我的长话短说就是接受一个Promise数组,然后最终合成一个Promise实例返回,后续then回调,其中数组里如果全为resolve则最后结果是resolve带回来的参数的数组,如果有一个reject,那么只能返回reject所带的参数。
其实关键要点是,我怎么知道某个Promise里写的是resolve还是reject呢?
我的一个思路是,不管你写什么,resolve/reject都得运行,反正最后都得走then或者catch结束,根据这个思路,怎么跑一个Promise实例呢,有个方法是Promise.resolve(),长话短说就是参数如果是数字或者是Promise中调用的resolve,那么Promise.resolve()回调走then(),如果输入的参数是Promise调用reject,那么Promise.resolve()回调走catch()。这时候如果是合规的走then塞进一个数组里,满了之后调resolve跳出,否则有一个错就终止并用reject跳出
这里的一个坑是,分清Promise.resolve() 和 Promise((resolve, reject) => {...}) 同样叫resolve,一个是方法(函数),一个是参数(函数)。当然这里解法如果有更好的欢迎留言或私信告知我呀
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
// resolve('RESOLVE2');
reject('REJECT2');
});
const promise4 = new Promise((resolve, reject) => {
resolve('OK2');
});
function myPromiseAll(args){
return new Promise((resolve, reject) => {
if(Array.isArray(args)){
const resultArray = [];
for(let item of args){
Promise.resolve(item)
.then(v => {
resultArray.push(v);
if(resultArray.length === args.length){
resolve(resultArray);
}
}).catch(err => {
reject(err)
})
}
} else {
resolve(args);
}
})
}
myPromiseAll([promise1, promise2, promise3, promise4])
.then(values => console.log(values))
.catch(err => console.log(err))
在promise3中注释resolve(‘RESOLVE2’),结果就是REJECT2,因为promise3走reject,如果注释掉reject(‘REJECT2’),那么结果就是[3, 42, “RESOLVE2”, “OK2”]
6. 手写new
描述:如题,写一个函数实现new
首先,new做了什么?
1.新建一个对象
2.建立原型链
3.执行构造函数方法,
4.绑定this值
5.返回这个新对象
那么根据这个思路…
function myNew(Class, ...rest){
// 创建新对象以及建立原型链 obj.__proto__ = Class.protype
const newObj = Object.create(Class.prototype);
// 执行构造函数方法以及绑定this值,apply结果为指定 this 值和参数的函数的结果。
const result = Class.apply(newObj, rest);
// 最后返回一个对象
return result;
}
const obj = myNew(Array, 1, 2, 3);
console.log(obj);
结果为控制台输出[1, 2, 3]
7. 手写instanceof
描述:如题,写一个函数实现instanceof
还是先看看instanceof的原理吧家人们↓
instanceof
长话短说版:instanceof判断某类是不是出现在某实例的原型链上,原型链展开说又得纷纷扰扰,这里就不多说啦,记得是obj.__proto__ = Class.prototype就行。
那么既然是链,自然想到链表,是不是也能像链表一样从起点到终点找呢,当然可以
class People{
constructor(name) {
this.name = name;
}
}
class Student extends People{
constructor(name,id) {
super(name);
this.id = id;
}
}
let s1 = new Student('Ouch!', 123);
console.log(s1);
// 目标 instanceof Student People Object是对的
console.log(s1 instanceof Student);
console.log(s1 instanceof People);
console.log(s1 instanceof Object);
console.log(s1 instanceof Function);
console.log(s1 instanceof Number);
console.log('MY TURN');
const myInstanceof = (obj, askClass) => {
if(askClass === obj.constructor){
return true;
}
let askPrototype = askClass.prototype;
let objProto = obj.__proto__;
while(true){
if(objProto === null){
return false;
}
if(objProto === askPrototype){
return true;
}
objProto = objProto.__proto__;
}
return false;
}
console.log(myInstanceof(s1, Student));
console.log(myInstanceof(s1, People));
console.log(myInstanceof(s1, Object));
console.log(myInstanceof(s1, Function));
console.log(myInstanceof(s1, Number));
结果把原生instanceof和手写的对比,完全一致
二、CSS
1. 做一个扇形图形
描述:利用css在网页实现一个扇形图形
扇形是啥,是不是可以看成一个等腰三角形+一个半圆?
三角形怎么来?可以用border实现,当内容为空时,放大border宽度可以看见四个等腰三角形,保留一边即可。然后用border-radius: 50%实现圆边↓
当然这个也有第二种办法,是一个新支持的css属性,已经有很详细的解答啦,请看 clip-path
2. 表格点击效果
描述:点击某表格中某一单元格,使得其同列同行凸显
↓实现这种效果↓
挺难的如果是第一次接触,我也不太记得在哪里记录到我的硬盘上了=.=
当初写的注释还蛮详细的,读者可以先自己实现再看看我的代码?给一个提示,如果某个单元格调用了this.cellIndex,那么会返回[0, n-1]中自身的下标,本行共n个格。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title></title>
<style type="text/css">
table.game {
font-size: 14px;
border-collapse: collapse;
width: 100%;
table-layout: fixed;
}
table.game td {
border: 1px solid #e1e1e1;
padding: 0;
height: 30px;
text-align: center;
}
table.game td.current{
background: #1890ff;
}
table.game td.wrap{
background: #f5f5f5;
}
</style>
</head>
<body>
<div id="jsContainer">
<table class="game">
<tbody>
<tr><td></td><td></td><td></td><td></td><td class="wrap"></td><td></td><td></td><td></td><td></td></tr>
<tr><td></td><td></td><td></td><td></td><td class="wrap"></td><td></td><td></td><td></td><td></td></tr>
<tr><td></td><td></td><td></td><td></td><td class="wrap"></td><td></td><td></td><td></td><td></td></tr>
<tr><td></td><td></td><td></td><td></td><td class="wrap"></td><td></td><td></td><td></td><td></td></tr>
<tr><td class="wrap"></td><td class="wrap"></td><td class="wrap"></td><td class="wrap"></td><td class="current"></td><td class="wrap"></td><td class="wrap"></td><td class="wrap"></td><td class="wrap"></td></tr>
<tr><td></td><td></td><td></td><td></td><td class="wrap"></td><td></td><td></td><td></td><td></td></tr>
<tr><td></td><td></td><td></td><td></td><td class="wrap"></td><td></td><td></td><td></td><td></td></tr>
<tr><td></td><td></td><td></td><td></td><td class="wrap"></td><td></td><td></td><td></td><td></td></tr>
<tr><td></td><td></td><td></td><td></td><td class="wrap"></td><td></td><td></td><td></td><td></td></tr>
</tbody>
</table>
</div>
<script type="text/javascript">
function bind() {
var tr = document.querySelectorAll('tr')
var td =document.querySelectorAll('td')
// i为遍历全部td的下标
for(var i=0;i<td.length;i++){
// 使用 addEventListener 绑定事件
td[i].addEventListener('click',function(){
// 先清掉所有其他td的className
for(var i =0;i<td.length;i++){
td[i].className =""
}
// childNodes指的是返回当前元素子节点的所有类型节点,其中连空格和换行符都会默认文本节点,
// childern指的是返回当前元素的所有元素节点,比如<p>,<td>
var trC = this.parentNode.children
// 所以这里是同行下,所有<td>都会上类选择器
for(var i =0;i<trC.length;i++){
trC[i].className="wrap"
}
// 同列 遍历所有行下的 子元素 的指定下标
// cellIndex 属性可返回一行的单元格集合中单元格的位置。 0 - n-1
for(var i = 0;i<tr.length;i++){
tr[i].children[this.cellIndex].className="wrap"
}
// 最后置为current就行
this.className="current"
})
}
}
bind();
</script>
</body>
</html>
3. 无限滚动和懒加载
描述:当滚动条快到达页面或容器底部时,插入内容,再次上拉仍能插入内容,实现无限滚动。同时这也是网页优化的一项,可以理解为懒加载的原理。
唔,这部分挺有趣的,当初是看到腾讯21年还是20年招前端实习生出的一道笔试题,然后我就记录下来啦。
关键知识:scrollHeight:滚动内容的全高度,scrollTop已滚动的距离容器顶端的距离,clientHeight容器高度。当下滚的时候,scrollTop会增加而scrollHeight不会,后者只看滚动容器所有元素的高度,所以scrollTop + clientHeight - scollHeight 会逐渐减少,最终scrollTop+clientHeight可以近似于scrollHeight,所以只要绑定滚动事件,加入判断scrollTop + clientHeight - scollHeight的值小于某指定数值,比如30(px),100(px)时添加内容即可,这样scrollHeight会增长使得滚动继续(好像干巴巴地解释得不太好,读者可以把代码拷到本地html文件浏览器打开看看)
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title></title>
<style type="text/css">
.box {
height: 500px;
width: 300px;
overflow: scroll;
padding-top: 20px;
padding-bottom: 20px;
background-color: cadetblue;
color: white;
margin-top: 48px;
border-left: 7px solid salmon;
border-top: 18px solid darkgoldenrod;
}
.box > div {
height: 50px;
line-height: 50px;
font-size: 20px;
}
</style>
</head>
<body>
<div class="box" id="test">
<div>鸟语花香</div>
<div>鸟语花香</div>
<div>鸟语花香</div>
<div>鸟语花香</div>
<div>鸟语花香</div>
<div>鸟语花香</div>
<div>鸟语花香</div>
<div>鸟语花香</div>
<div>鸟语花香</div>
<div>鸟语花香</div>
<div>鸟语花香</div>
<div>鸟语花香</div>
</div>
<script async="async" type="text/javascript">
const test = document.querySelector('#test');
// 绑定滚动事件
test.addEventListener('scroll', ()=>{
// 现在容器内的的滚动高度
let nowScrollHeight = test.scrollHeight;
// 现在滚动条滚动上距离
let nowScrollTop = test.scrollTop;
// 现在滚动容器的外观高度
const clientHeight = test.clientHeight;
// 距离底部小于内100px就添加
if( (nowScrollTop+clientHeight) + 100 >= nowScrollHeight){
test.appendChild(test.lastElementChild.cloneNode(true));
}
console.log(test.querySelectorAll('div').length);
});
</script>
</body>
</html>