跳到主要内容

前端面试题总结-2020

记录了自己疫情期间复习总结的一些面试题+面试遇到的一些问题,文章将会持续更新。

JS

原型及继承

红宝书中的图

组合继承

function Person(name) {
this.name = name;
this.features = ['eyes'];
}

function Student(name, id) {
Person.call(this, name); //继承属性相当于super(this)
this.id = id;
}

Student.prototype = new Person();

Student.prototype.sayHello = function () {
console.log('hello');
};

const s1 = new Student('Alan', '001');
const s2 = new Student('Bob', '002');

console.log(s1 instanceof Person); // true
// 引用类型在各实例中不会相互影响
s1.features.push('hand');
console.log(s2.features); // ["eyes"]

更多继承方式及其优缺点可以查看红宝书第六章(讲得非常“干”)

深拷贝和浅拷贝

在 JS 中,变量分为了基本类型和引用类型。对基本类型进行赋值时是对值进行拷贝的,而对引用类型进行赋值则是对地址进行拷贝。

let a = 1;
let b = a;
console.log(a); // 1
a++;
console.log(a); // 2
console.log(b); // 1

通过上面的例子我们知道,cloneObj 和 obj 是指向同一个地址的,任何一方修改都会影响到对方,那如何创建一个独立的 cloneObj?,这就要使用深拷贝和浅拷贝了。

深拷贝和浅拷贝的区别:

根据拷贝的层级进行区分,浅拷贝只进行一层拷贝,深拷贝进行多层拷贝。

浅拷贝

const obj = { a: 1, b: { b1: 1, b2: 2 }, c: 0 };
function shallowClone(source) {
const result = {};
for (const key in source) {
if (source.hasOwnProperty(key)) {
result[key] = source[key];
}
}
return result;
}
const shallowObj = shallowClone(obj);
obj.a = 10;
console.log(shallowObj.a); // 1
obj.b.b1 = 6;
console.log(shallowObj.b.b1); // 6

深拷贝

const obj = { a: 1, b: { b1: 1, b2: 2 }, c: 0 };
function deepClone(source) {
const result = {};
for (const key in source) {
if (source.hasOwnProperty(key)) {
if (typeof source[key] === 'object') {
result[key] = deepClone(source[key]);
} else {
result[key] = source[key];
}
}
}
return result;
}
const deepObj = deepClone(obj);
obj.a = 10;
console.log(deepObj.a); // 1
obj.b.b1 = 6;
console.log(deepObj.b.b1); // 1

数组去重

// 笨方法
const arr = [1, 1, 2, 5, 2, 6, 8];
let newArr = [];
for (let i = 0; i < arr.length; i++) {
if (newArr.includes(arr[i])) {
continue;
}
newArr.push(arr[i]);
}

数组填充

new Array(10).fill(1);

Array.from({ length: 10 }, item => 1);

for in 和 for of

  • for in 会遍历数组中的可枚举属性,包括原型。可以遍历对象,遍历的是 key 值
  • for of 只是遍历数组的元素或者可以迭代的对象,不包括原型。遍历的是 value 值。
Array.prototype.testMethod = function () {
console.log('testMethod');
};
const mArr = [1, 2, 3, 7];
const mObject = {
name: 'Alan',
age: 1
};
for (const key in mArr) {
console.log(mArr[key]);
// 1 2 3 7
/* ƒ () {
console.log('testMethod');
} */
}
// 解决方案
for (const key in mArr) {
if (mArr.hasOwnProperty(key)) {
console.log(mArr[key]);
// 1 2 3 7
}
}
for (const key in mObject) {
console.log(key);
// name
// age
}
try {
for (const iterator of mObject) {
console.log(iterator);
}
} catch (error) {
console.log(error);
//mObject is not iterable
}

事件委托

事件委托利用了事件冒泡,只指定了一个事件处理程序,就可以管理某一类型的所有事件。

例如,click 事件会一直冒泡到 document 层次。例如下面例子中我们无需对所有 li 元素添加 onclick 事件,只需使用事件冒泡的特性来实现事件委托。

<body>
<ul id="myList">
<li id="sayName">Name</li>
<li id="sayHello">Hello</li>
<li id="sayAge">Age</li>
</ul>
<script>
const myList = document.getElementById('myList');
myList.addEventListener('click', function (e) {
const target = e.target;
if (target.id === 'sayName') {
alert('Alan');
} else if (target.id === 'sayHello') {
alert('Hello');
} else {
alert('sayAge');
}
});
</script>
</body>

实现滑动动画

<div id="myDiv"></div>

<script>
const myDiv = document.getElementById('myDiv');
const time = Date.now(); //时间戳

const transition = setInterval(() => {
const timeLength = Date.now() - time;
const step = (5000 - timeLength) / 1000;
console.log(step);

if (timeLength > 5000) {
clearInterval(transition);
// 5s后结束
}
myDiv.style.left = myDiv.offsetLeft + step + 'px';
}, 50);
</script>

数组扁平化

// Array.prototype.flat([depth])
let arr = [1, 2, 7, [2, [2, 3], 6]];

console.log(arr.flat(Infinity));

eventLoop

JS 是单线程的,那为什么不是多线程的呢,设想一个场景,一个线程修改了 body 的 background 为 red,另一个线程修改了 body 的 background 为 green。那最终浏览器就不知道 background 到底为什么。由此可以看到多线程会为浏览器的 DOM 操作带来很多同步问题。参考资料

JS 的任务可以分为同步任务和异步任务。

1.同步任务优先在主线程上执行,会形成一个执行栈。

2.异步任务会被放入任务队列中,当执行栈清空时会读取任务队列中的任务丢进执行栈中。

1、2 两步反复执行形成了 eventLoop。

如果将任务细分的话还可以分成宏任务和微任务

  • macro-task(宏任务):包括整体代码 script,setTimeout,setInterval
  • micro-task(微任务):Promise.then(),process.nextTick

优先级:process.nextTick>Promise.then()

当执行栈中没有任务时,微任务总是优先于宏任务执行

详细查看文章这一次,彻底弄懂 JavaScript 执行机制

console.log('script start');

async function async1() {
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2 end');
return Promise.resolve().then(() => {
console.log('async2 end1');
});
}
async1();

setTimeout(function () {
console.log('setTimeout');
}, 0);

new Promise(resolve => {
console.log('Promise');
resolve();
})
.then(function () {
console.log('promise1');
})
.then(function () {
console.log('promise2');
});

console.log('script end');

// script start => async2 end => Promise => script end => async2 end1 => promise1 => promise2 => async1 end => setTimeout

this

https://juejin.cn/post/6844903496253177863

  • 指向调用它的那个对象,函数运行时获得的。
  • 箭头函数的 this 指向取决于定义时最近一层的非箭头函数的 this 值。取决于外部的上下文。

特殊例子

var name = 'windowsName';
var a = {
name: null,
fn: function () {
console.log(this.name); // windowsName
}
};

var f = a.fn;
f();

闭包

关键词: 内存泄漏

  • 当前函数的执行上下文中的内容被该上下文以外的内容占用,导致当前上下文无法释放。
  • 闭包是指有权访问另一个函数作用域中的变量的函数。
  • 创建闭包的常见方式,就是在一个函数内部创建另一个函数

相关文章

求数组最大值

const arr = [1, 2, 1, 4, 2, 10];
console.log(Math.max.apply(null, arr));
const arr = [1, 2, 1, 4, 2, 10];
console.log(arr.sort((a, b) => a - b)[arr.length - 1]);
const arr = [1, 2, 1, 4, 2, 10];
console.log(Math.max(...arr));

Promise

// new Promise(executor),当new Promise被创建,executor自动执行
let promise = new Promise(function (resolve, reject) {
resolve('finished');
// reject(new Error);
});
// Promise.then(f1,f2) f1在resolve后运行(参数为resolve结果),f2在reject后运行(参数为reject错误)
promise.then(
result => console.log(result), //finished
error => console.log(error) //输出错误
);
  • promise.all()同时执行多个 promise,只要有一个 promise 被 reject,那么将不再执行
  • promise.allSettled()和 promise.all()类似,只是会等所有 promise 执行完。
  • promise.race()返回最快执行完的 promise 结果。
  • 通过 window.addEventListener('unhandledrejection', event => alert(event.reason)) 来捕获未处理的 rejection。
  • thenable 对象(具有可调用的 then 方法的对象)

实现一个 sleep

function sleep(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}

(async () => {
console.log('2s后输出内容...');
await sleep(2000);
console.log('666');
})();

防抖和节流

节流

函数在一定时间内只执行一次,比如点击按钮后回去服务器获取数据,使用节流可以防止短时间内请求多次,减少服务器的压力

防抖

在一定时间后才执行(触发多次只会执行一次)。应用场景:input 搜索框在 wait 秒后再发送请求

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
body {
height: 4000px;
width: 100px;
background: rgb(241, 165, 165);
}
</style>
</head>
<body>
<button id="fetchBtn">fetch data</button>
<span>searchBar</span><input id="inputBar" />
<script>
// 节流
function throttle(func, wait) {
let last = 0;
return function (...args) {
let now = new Date();
if (now - last > wait) {
last = now;
func.apply(this, args);
}
};
}

// 防抖
function debounce(func, wait) {
let timer = 0;
return function (...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, wait);
};
}

document.getElementById('fetchBtn').addEventListener(
'click',
throttle(function (numb) {
console.log('fetching');
}, 1000)
);
document.getElementById('inputBar').addEventListener(
'input',
debounce(function (numb) {
console.log('searching');
}, 1000)
);
</script>
</body>
</html>

XMLHttpRequest

常用方法:

  • open(method, url, [async, user, password])初始化,async(false/true)控制同步/异步
  • send([body])发送请求
  • abort()中止请求
  • setRequestHeader(name, value)设置请求头

常用属性:

status: 404/200...

statusText: Not Found/OK...

responseType: 响应格式

readyState: 状态

withCredentials: 跨域设置

UNSENT = 0; // 初始状态
OPENED = 1; // open 被调用
HEADERS_RECEIVED = 2; // 接收到 response header
LOADING = 3; // 响应正在被加载(接收到一个数据包)
DONE = 4; // 请求完成

常用监听事件:

  • onload
  • onerror
  • onprogress

JS 题目

const a = ['1', '2', '3'].map(parseInt);
// 数组a中的'1'转化为10进制。
console.log(a); // [1, NaN, NaN]
// map的三个参数(item,index,array)
/* parseInt(string, radix)
当radix等于0或者undefined或者没有指定时,如果string以'0x'或者''0X'开头,则radix=16
以'0'开头,根据实际情况radix=10/8。 */
const users = [
{
name: 'Alan',
age: 19
},
{
name: 'Bob',
age: 25
}
];
const userList = users.sort((a, b) => b.age - a.age);
// 根据年龄进行排序,注意:sort会改变原来的数组
function isSameLetter(a, b) {
a = a.toString().toLowerCase();
b = b.toString().toLowerCase();
return a.split('').sort().join('') === b.split('').sort().join('');
}
console.log(isSameLetter('176as', 'a17s6'));
//判断两者是否是由相同的字母组成,顺序可以不一样

前端框架相关

React

  • React 生命周期
  • useRef
    • 操作 DOM
    • 保存不需要在 JSX 中渲染的变量
    • 保存示例
    • 使用 forwardRef 进行 ref 透传
    • 使用 useImperativeHandle 约束向外暴露的东西,且可以解决函数组件 ref 无法获取示例的问题。
    • ref 要使用最新的 state 时,使用 flushSync
  • cloneElement 可以劫持 children element 混入 props
  • HOC
    • 注解
    • 复用逻辑
    • 正向属性代理(强化 props)
    • 反向属性代理(通过 extends 组件可以获取组件内部状态)

单页应用优缺点

优点:

  • 基于 ajax 加载数据,无需刷新页面,用户体验好
  • 前后端分离,后端代码可以应用到多端
  • 减轻服务器压力

缺点:

  • 不利于 SEO,可以使用 SSR 解决
  • 首屏加载速度慢
  • 不支持浏览器前进后退功能