前端面试题总结-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
const obj = {
name: 'Alan',
age: 22
};
const cloneObj = obj;
obj.age = 18;
console.log(cloneObj.age); // 18
通过上面的例子我们知道,cloneObj 和 obj 是指向同一个地址的,任何一方修改都会影响到对方,那如何创建一个独立的 cloneObj?,这就要使用深拷贝和浅拷贝了。
深拷贝和浅拷贝的区别:
根据拷贝的层级进行区分,浅拷贝只进行一层拷贝,深拷贝进行多层拷贝。
浅拷贝
- 方法1
- 方法2
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 shallowClone1(source) {
return Object.assign({}, source);
}
const shallowObj = shallowClone1(obj);
obj.a = 10;
console.log(shallowObj.a); // 1
obj.b.b1 = 6;
console.log(shallowObj.b.b1); // 6
深拷贝
- 方法1
- 方法2
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 obj = { a: 1, b: { b1: 1, b2: 2 }, c: 0 };
function deepClone(source) {
return JSON.parse(JSON.stringify(source));
}
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
- 指向调用它的那个对象,函数运行时获得的。
- 箭头函数的 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 组件可以获取组件内部状态)