可能我们最常见的需要手写代码的地方就是面试了,往往很多时候,聊着聊着面试官突然就说让你实现某某方法。经常见到很多人抱怨,面试造火箭,真的很反感八股文。可是没有办法,面试官需要在短短一个小时或者两小时内判断你的水平,你总得展示点什么。最简单的判断方式之一就是看你实现一些方法,包括但不仅限于手写场景题或简单算法题,一些常用的工具方法,以及一些JS内置库函数。
除此之外,手写一些常见的方法能提升自己的代码掌控能力,测试自己JS基础是否扎实,理解底层的一些实现也更有利于自己写出高质量,少bug的代码,就比如知道forEach的实现原理,就不会在里面写return,更不会想用个变量去接收它的返回值。
我整理了一下自己面试中遇到,以及看各厂面经中出现高频的一些手写题,分为两篇:手写JS内置库函数,手写常用工具类函数,此为第二篇,以下是一些JS常用工具函数的简单实现
实现防抖函数
核心原理:事件触发后在指定时间内只执行一次,若在指定时间内再次触发,则重新计算时间。
- 声明一个定时器变量timer
- 返回一个新的函数,这是防抖的核心逻辑,利用闭包
- 在返回的新函数内部,每次调用时都需要清除之前的定时器(如果存在),这样可以保证只有在最后一次调用之后的
wait
时间内没有新的调用发生时,才执行fu
。
该函数有简单一点的,先等待再执行,也可以立即执行,实现会多一步。
js
// 延迟执行,先等待再执行
function debounce(fn, wait) {
let timer = null;
return function () {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(function () {
fn.apply(this, arguments);
}, wait)
}
}
// 立即执行
function debounce(fn, wait) {
let timer = null;
return function () {
timer && clearTimeout(timer);
let callNow = !timer;
timer = setTimeout(function () {
timer = null
}, wait)
callNow && fn.apply(this, arguments)
}
}
实现节流函数
核心原理:连续发生的事件在指定时间内只触发一次。
也是利用闭包特性,判断连续发生的事件间隔是否超过给定的频率,该函数有两种实现方式:
- 利用时间戳,计算两次事件触发间隔,大于给定频率则执行函数
- 使用定时器
js
// 使用时间戳
function throttle(fn, wait) {
let pre = 0;
return function () {
const cur = Date.now();
if (cur - pre > wait) {
fn.apply(this, arguments);
pre = cur
}
}
}
// 使用定时器
function throttle(fn, wait) {
let timer = null;
return function () {
if (!timer) {
timer = setTimeout(function () {
timer = null;
fn.apply(this, arguments)
}, wait)
}
}
}
实现深拷贝函数
用法:拷贝一个对象的属性值 如果遇到属性值为引用类型的时候,会新建一个引用类型并将对应的值复制给它,因此对象获得的一个新的引用类型而不是一个原有类型的引用。以下只是最简单的实现,没有考虑时间,函数,正则等特殊对象,也没有处理循环引用。
js
/** 深拷贝
*/
const deepClone = obj => {
if (!obj || typeof obj !== 'object') {
return obj
}
const temp = Array.isArray(obj) ? [] : {};
for (let k in obj) {
if (typeof obj[k] === 'object') {
temp[k] = deepClone(obj[k])
} else {
temp[k] = obj[k]
}
}
return temp
}
实现函数柯里化
函数柯里化是一种将使用多个参数的函数转换成一系列使用一个参数的函数的技术。
js
function curry(fn, ...args) {
return (..._args) => {
let newArgs = [...args, ..._args];
if (newArgs.length < fn.length) {
return curry.call(this, fn, ...newArgs)
} else {
return fn.apply(this, newArgs)
}
}
}
// test
function sum(a, b, c) {
return a + b + c;
}
console.log(curry(sum, 1, 2, 3)()); // 输出:6
console.log(curry(sum, 1)(2, 3)); // 输出:6
console.log(curry(sum, 1, 2)(3)); // 输出:6
实现数组扁平化
es6 有提供新的特性flat
方法,用于将多维数组拉平(扁平化),不影响原数组,返回新的数组。写该函数纯粹是为了面试或者测试下自身基础及对代码掌控能力,或者自行处理es5以下的兼容问题。
js
function flatten(arr) {
let result = [];
for (let i = 0; i < arr.length; i++) {
const cur = arr[i];
if (Array.isArray(cur)) {
result = result.concat(flatten(cur))
} else {
result.push(cur)
}
}
return result
}
将数字每千分位用逗号隔开
笔试部分非常常见的一道题目
js
function formatNumber(num) {
const numStr = num.toString();
let result = '';
let counter = 0;
for (let i = numStr.length - 1; i >= 0; i--) {
counter++;
// result = numStr.charAt(i) + result;
result = numStr[i] + result;
if (!(counter % 3) && i !== 0) {
result = ',' + result;
}
}
return result
}
数组转树
非常高频的笔试题
js
// [{ id: 1, pid: 0, name: 'body'},{}]
function arrToTree(arr) {
const map = {};
const tree = [];
arr.forEach(item => {
map[item.id] = item;
});
arr.forEach(item => {
if (item.pid !== 0 || !map[item.pid]) { //根节点根据具体情况而定
map[item.pid].children = item.children || [];
map[item.pid].children.push(map[item.id]);
} else {
tree.push(item)
}
})
return tree
}
// test
let source = [
{
id: 1,
pid: 0,
name: 'body',
},
{
id: 2,
pid: 1,
name: 'title',
},
{
id: 3,
pid: 2,
name: 'div',
},
{
id: 4,
pid: 0,
name: 'html',
},
{
id: 5,
pid: 4,
name: 'div',
},
{
id: 6,
pid: 5,
name: 'span',
},
{
id: 7,
pid: 5,
name: 'img',
},
]
// 转为
[{
id: 1,
pid: 0,
name: 'body',
children: [
{
id: 2,
pid: 1,
name: 'title',
children: [{ id: 3, pid: 2, name: 'div' }],
},
],
},
{
id: 4,
pid: 0,
name: 'html',
children: [
{
id: 5,
pid: 4,
name: 'div',
children: [{ id: 7, pid: 5, name: 'img' }],
},
],
}]
树转数组
非常常见的笔试题
js
// 简单实现
function treeToArr(tree) {
const result = [];
tree.forEach(item => {
result.push(item);
if (item.children) {
result.push(...treeToArr(item.children))
}
});
return result
}
// 完整实现
function treeToArr(tree) {
const result = [];
const addNode = (node) => {
const { id, pid, name } = node;
result.push({ id, pid, name });
if (node.children && node.children.length) {
node.children.forEach(child => addNode(child))
}
}
tree.forEach(rootNode => addNode(rootNode));
return result
}
//test
let source1 = [
{
id: 1,
pid: 0,
name: 'body',
children: [
{
id: 2,
pid: 1,
name: 'title',
children: [{ id: 3, pid: 2, name: 'div' }],
},
],
},
{
id: 4,
pid: 0,
name: 'html',
children: [
{
id: 5,
pid: 4,
name: 'div',
children: [{ id: 7, pid: 5, name: 'img' }],
},
],
},
]
// 转为
[
{ id: 1, pid: 0, name: 'body' },
{ id: 4, pid: 0, name: 'html' },
{ id: 2, pid: 1, name: 'title' },
{ id: 5, pid: 4, name: 'div' },
{ id: 3, pid: 2, name: 'div' },
{ id: 7, pid: 5, name: 'img' }
]
9. 获取给定范围内的随机数
该方法笔试经常碰到,也比较有实用性
js
const getRandom = (start, end) => {
const range = end - start,
random = Math.random();
return Math.round(random * range) + start
}
// 一行代码简写
// const getRandom = (start, end) => Math.round(Math.random() * (end - start)) + start;
console.log(getRandom(3, 10))