前端手写题总结
前端手写题是面试过程中常见的一种考察方式,用于评估候选人对前端技术的理解和编程能力。
Promise系列
1. Promise.all
Promise.all 是一个 JavaScript 函数,用于处理多个 Promise 对象。当你有多个异步操作(每个都返回一个 Promise)并且想要等待所有的异步操作都完成时,Promise.all 非常有用。它接收一个 Promise 数组作为输入,并返回一个新的 Promise。
- 如果数组中的所有 Promise 都成功解决(resolved),
Promise.all返回的 Promise 将解决为一个数组,包含每个 Promise 的结果。 - 如果任何一个 Promise 被拒绝(rejected),
Promise.all返回的 Promise 将立即被拒绝,并返回拒绝的原因。
Promise.myAll = (promises) => {
return new Promise((resolve, reject) => {
let idx = 0;// 计数器
const ans = [];// 存储每一个Promise的结果
promises.forEach((item, index) => {
Promise.resolve(item).then( // 包裹item,确保为Promise
(res) => {
ans[index] = res;// 将解决的结果存储再ans数组
idx++;// 增加计数器
if (idx === promises.length) { // 如果所有Promise全解决 则返回
resolve(ans);
}
},// 如果有任意一个Promise被拒绝 则返回err
(err) => reject(err)
);
});
});
};
// 示例
Promise.myAll([
Promise.resolve(1),
Promise.resolve(2),
Promise.reject(3),
4,
]).then(
(data) => {
// data:[1,2,3,4]
console.log("成功", data);
},
(reason) => {
// reason:reason2
console.log("失败", reason);
}
);
2. Promise.allSettled
Promise.allSettled 是一个 JavaScript 函数,用于处理多个 Promise 对象。它接收一个 Promise 数组作为输入,并返回一个新的 Promise,该 Promise 总是会被解决(resolved),不论数组中的 Promise 是被解决(resolved)还是被拒绝(rejected)。
与 Promise.all 不同的是,Promise.allSettled 不会在遇到任何被拒绝(rejected)的 Promise 时立即拒绝。相反,它等待所有的 Promise 都已经被解决或被拒绝,并返回一个对象数组,每个对象表示对应的 Promise 的结果,包括状态(status)和值(value)或拒绝原因(reason)。
这使得 Promise.allSettled 成为一个有用的工具,用于在需要等待多个异步操作完成并处理每个操作的结果时,无论这些操作是成功还是失败。
Promise.myAllSettled = function (promises) {
return Promise.all(promises.map(p => Promise.resolve(p).then(
value => ({ status: 'fulfilled', value }),
reason => ({ status: 'rejected', reason })
)));
}
Promise.myAllSettled([
Promise.resolve(1),
Promise.resolve(2),
new Promise((resolve, reject) =>
setTimeout(reject, 100, "bad")
),
4,
]).then(
(data) => {
// data:[1,2,3,4]
console.log("成功", data);
},
(reason) => {
// reason:reason2
console.log("失败", reason);
}
);
3. Primise.race
Promise.race 是 JavaScript 中的一个函数,它用于处理多个 Promise 对象。这个函数接收一个 Promise 数组作为输入,并返回一个新的 Promise。关键特性是,Promise.race 返回的 Promise 将会以第一个解决(resolved)或拒绝(rejected)的输入 Promise 的结果来解决或拒绝。
Promise.myRace=function(promise) {
return new Promise((resolve, reject) => {
for (const p of promise) {
Promise.resolve(p).then(resolve, reject);
}
});
}
Promise.myRace([
new Promise((resolve, reject) =>
setTimeout(reject, 100, "bad")
),
Promise.resolve(1),
Promise.resolve(2),
new Promise((resolve, reject) =>
setTimeout(reject, 100, "bad")
),
4,
]).then(
(data) => {
// 成功 1
console.log("成功", data);
},
(reason) => {
//
console.log("失败", reason);
}
);
事件委托
事件委托是一种常用的 JavaScript 事件处理模式,特别是在处理动态内容或大量元素的事件时。在事件委托中,而不是在每个目标元素上直接绑定事件处理器,我们在它们的共同祖先元素上绑定一个事件处理器,并利用事件冒泡的原理来管理事件。这种方式可以提高性能并减少内存消耗。
HTML
<ul id="myList">
<li>Item 1</li>
<li>Item 2</li>
<!-- 更多列表项 -->
</ul>
JS
// 获取父元素
const list = document.getElementById('myList');
// 事件委托函数
list.addEventListener('click', function(event) {
// 获取触发事件的目标元素
const target = event.target;
// 检查事件是否在 <li> 元素上触发
if (target.tagName === 'LI') {
// 在这里处理点击事件
console.log('Clicked on item:', target.textContent);
}
});
数组扁平化
数组扁平化是指将一个多层嵌套的数组转换为一个单层数组。
const arr = [1, 2, [3, 4, [5, [6, 7]]], [8, 9, 10], 11, 12];
const myFlaten = (arr) => {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr)
}
return arr;
}
const mayFlaten1 = (arr) => {
return arr.reduce((acc, cur) => {
return acc.concat(Array.isArray(cur) ? mayFlaten1(cur) : cur)
}, [])
}
console.log(myFlaten(arr)) // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
console.log(mayFlaten1(arr)) // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
手写lodash.get
const lodashGet = (object, path, defaultValue) => {
const pathArray = Array.isArray(path) ? path : path.replace(/\[(\w+)\]/g, '.$1').split('.')
result = object;
for (let i = 0; i < pathArray.length; i++) {
result = result[pathArray[i]];
if (result === null || result === undefined) {
return defaultValue
}
}
return result
}
console.log(lodashGet(obj, 'a.b.c', 'default')); // 输出: [1, 2, 3]
console.log(lodashGet(obj, 'a.b.c[0]', 'default')); // 输出: 1
console.log(lodashGet(obj, 'a.b.x', 'default')); // 输出: 'default'
数组去重
// 方法一:利用Set数据结构,Set中的元素都是唯一的,所以可以利用这个特性实现数组去重
const uniqueArray = (arr) => {
return [...new Set(arr)]
}
// 方法二: 利用indexOf方法,遍历数组,如果当前元素在数组中第一次出现的位置和最后一次出现的位置相同,则说明该元素是唯一的
const uniqueArray = (arr) => {
return arr.filter((item, index) => arr.indexOf(item) === index)
}
// 方法三:利用reduce方法,遍历数组,将每个元素与已经遍历过的元素进行比较,如果相同则跳过,不相同则添加到累加器结果数组中
const uniqueArray = (arr) => {
return arr.reduce((acc, cur) => {
if(!acc.includes(cur)) {
acc.push(cur)
}
return acc
}, [])
}
// 方法四:利用map的key的唯一性
const uniqueArray = (arr) => {
const map = new Map()
arr.forEach((item) => {
map.set(item, true)
})
return Array.from(map.keys())
}
const arr = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5]
console.log(uniqueArray(arr)) // [1, 2, 3, 4, 5]
防抖和节流
// 防抖
const debounce = (fn, delay, immediate) => {
let timer = null
let callNow = true
return (...args) => {
clearTimeout(timer)
if(immediate && callNow) {
fn(...args)
callNow = false
} else {
timer = setTimeout(() => {
fn(...args)
callNow = true
}, delay)
}
}
}
// 节流
const throttle = (fn, delay) => {
let preTime = 0
return (...args) => {
let nowTime = Date.now()
if(nowTime - preTime > delay) {
fn(...args)
preTime = nowTime
}
}
}
new操作符
思路:
-
创建一个全新的空对象,此对象的原型指向构造函数的原型
-
将this指向新对象
-
如果构造函数返回了一个对象,并且这个对象不是null(因为null也有对象的性质),则返回构造函数的返回值
function myNew(constructor, ...args) {
// 指定的对象作为新创建对象的原型(__proto__)
const newObj = Object.create(constructor.prototype); // 使用Object.create()建立原型链关系
const res = constructor.apply(newObj, args); // 应用构造函数
// 如果构造函数返回了一个对象并且该对象不是一个原始值(null除外),则返回构造函数的结果
// 否则返回我们刚创建的对象newObj
return res && (typeof res === 'object' || typeof res === 'function') ? res : newObj;
}
// 使用示例
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
};
// 使用 myNew 创建 Person 的实例
const person = myNew(Person, 'guang', 30);
console.log(person.name); // 输出 "guang"
console.log(person.age); // 输出 30
person.greet(); // 正常输出 "Hello, my name is guang and I'm 30 years old."
函数柯里化
函数柯里化是将一个多参数函数转换成一系列接受一个参数的函数的方法
柯里化函数的调用过程类似于洋葱的剥皮过程。每次调用柯里化函数,它都会剥去一层参数,直到所有参数都被提供才调用原函数。
function curry(fn) {
return function curried(...args) {
if(args.length >= fn.length) {
return fn.apply(this, args)
} else {
return function(...args2) {
return curried.apply(this, args.concat(...args2))
}
}
}
}
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
const add5 = curriedAdd(5);
console.log(add5(10, 15)); // 30
console.log(curriedAdd(5)(10)(20)); // 35
<!DOCTYPE html>
<html>
<head>
<style>
.red,
.yellow,
.green {
width: 100px;
height: 100px;
border-radius: 50%;
margin-top: 10px;
background-color: grey;
}
.red.on {
background-color: red;
}
.yellow.on {
background-color: yellow;
}
.green.on {
background-color: green;
}
</style>
</head>
<body>
<div class="red"></div>
<div class="yellow"></div>
<div class="green"></div>
<script>
let lights = Array.from(document.getElementsByTagName('div'));
let currentLight = 2;
function changeLight() {
// 关闭当前灯
lights[currentLight].classList.remove('on');
currentLight = (currentLight + 1) % lights.length;
// 打开下一个灯
lights[currentLight].classList.add('on');
if (currentLight === 0) {
setTimeout(changeLight, 5000);
} else if (currentLight === 1) {
setTimeout(changeLight, 3000);
} else {
setTimeout(changeLight, 1000);
}
}
changeLight();
</script>
</body>
</html>