跳到主要内容

前端面试题总结-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

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>

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();

闭包

关键词: 内存泄漏

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

相关文章

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, immediate) {
let timer = 0;
return function (...args) {
if (timer) clearTimeout(timer);

if (immediate) {
if (!timer) {
func.apply(this, args);
}

timer = setTimeout(() => {
timer = null;
}, wait);
} else {
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>

JS 题目

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 组件可以获取组件内部状态)