引子
本篇是面试中常见的手写代码集合。这些手写代码的题目,在一定程度上能体现候选人的代码功底和编程经验。熟练理解并写出这些题目,不仅可以应对手写代码本身,在回答相关的问题时,也能有更好的表现。
参数的获取和操作
参数的获取和操作有很多写法,我们新旧共赏,后面的代码我都会采取定义参数的方式
// 获取第一个参数
Array.prototype.slice(arguments, 0, 1);
// 获取除了第一个参数之外的参数
Array.prototype.slice(arguments, 1);
// 直接把 arguments 转化成数组再操作
Array.from(arguments)
// 定义函数参数
function (first, ...others) {}
数组去重
// ES6 新特性
function unique(arr) {
return Array.from(new Set(arr));
}
// 利用 indexOf 返回第一个匹配索引
function unique(arr) {
return arr.filter((item, index) => {
return arr.indexOf(item) === index;
});
}
数组乱序
// 为每个元素关联一个随机数,对随机数排序
function shuffle(arr) {
return arr
.map((item) => ({
value: item,
random: Math.random(),
}))
.sort((a, b) => a.random - b.random)
.map((item) => item.value);
}
// 遍历,将当前元素与随机位置元素交换
function shuffle(arr) {
const len = arr.length;
for (let i = 0; i < len; i++) {
const j = Math.floor(Math.random() * len);
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
数组扁平化
// 递归
function flatten(arr) {
return arr.reduce((prev, next) => {
return prev.concat(Array.isArray(next) ? flatten(next) : next);
}, []);
}
call、apply、bind
这三个函数方法很类似,参数都是 this 的指向和传递给原函数的参数。call 和 apply 只是入参形式不同,其他都一样;bind 返回新的函数。
Function.prototype._call = function (context, ...args) {
// 顺便秀一下 Symbol 和 Reflect
const fn = Symbol("fn");
context[fn] = this;
const result = context[fn](...args);
Reflect.deleteProperty(context, fn);
return result;
};
Function.prototype._apply = function (context, args) {
context.fn = this;
const result = context.fn(...args);
delete context.fn;
return result;
};
Function.prototype._bind = function (context, ...args) {
const self = this;
return function (...otherArgs) {
const finalArgs = [...args, ...otherArgs];
return self.apply(context, finalArgs);
};
};
函数柯里化、函数组合
这两个方法能把函数式编程的精神体现出来,一个把提供参数的时机打散,一个把多个函数链式组合
function curry(fn) {
return function judge(...args) {
if (args.length === fn.length) {
return fn(...args);
} else {
return function (...otherArgs) {
return judge(...args, ...otherArgs);
};
}
};
}
function compose(...fns) {
return fns.reduce(
(a, b) =>
(...args) =>
a(b(...args))
);
}
防抖、节流
防抖和节流都是限制调用频率的策略。防抖就像蓄力攻击,蓄力完成才能攻击,重新蓄力得重置时间;节流就像技能 CD,技能放完了得等 CD 才能再放。
function debounce(fn, wait) {
let timer = null;
return function () {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, arguments);
}, wait);
};
}
function throttle(fn, wait) {
let last = 0;
return function () {
const now = new Date();
if (now - last > wait) {
fn.apply(this, arguments);
last = now;
}
};
}
原型链和继承
function _new(constructor, ...args) {
const obj = new Object();
obj.__proto__ = constructor.prototype;
constructor.apply(obj, ...args);
return obj;
}
function _instanceOf(obj, constructor) {
let proto = obj.__proto__;
while (proto) {
if (proto === constructor.prototype) {
return true;
} else {
proto = proto.__proto__;
}
}
return false;
}
// 组合继承
function Father(firstname) {
this.firstname = firstname;
}
function Son(firstname, secondname) {
Father.call(this, firstname); // 1.call
this.secondname = secondname;
}
Son.prototype = Object.create(Father.prototype); // 2.chain
深拷贝
function deepClone(obj, cache = new WeakMap()) {
// 基本数据类型直接返回
if (!isObject(obj)) {
return obj;
}
// 缓存解决循环引用
if (cache.get(obj)) return cache.get(obj);
const type = getAbsoluteType(obj);
// 高级对象类型特殊处理举例
if (type === "date") return new Date(obj);
if (type === "regexp") return new RegExp(obj);
// {} []
let cloneObj;
if (type === "object") {
cloneObj = {};
} else if (type === "array") {
cloneObj = [];
} else {
return `do not support ${type}`;
}
for (let key in obj) {
const value = obj[key];
cloneObj[key] = deepClone(value);
}
cache.set(obj, cloneObj);
return cloneObj;
}
// 两个辅助函数也很有必要记一下
// 辅助函数1:判断是对象类型还是基本数据类型
function isObject(obj) {
const type = typeof obj;
return (type === "object" || type === "function") && obj !== null;
}
// 辅助函数2:获取精确类型
function getAbsoluteType(obj) {
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}
JSON.stringify
// 思路和深拷贝类似,{}、[]、其他高级对象、各种基本数据类型
// 难点:字符串的拼接
function stringify(obj) {
const type = getAbsoluteType(obj);
// {} []
const isObject = type === "object";
const isArray = type === "array";
if (isObject || isArray) {
let json = [];
for (let key in obj) {
let val = obj[key];
let strVal = stringify(val);
json.push((isArray ? "" : `"${key}":`) + strVal);
}
return (isArray ? "[" : "{") + json + (isArray ? "]" : "}");
}
if (type === "string") return `"${obj}"`;
// 尝试处理不同的对象类型
if (type === "function") return "null";
if (type === "date") return obj.toJSON();
// 简单降级
return String(obj);
}
function getAbsoluteType(obj) {
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}
// 先把单元测试写出来是一个很好的方式
// 一边跑一边完善代码
const origin = [
{
string: "Jay",
number: 40,
bool: true,
time: new Date(),
null: null,
undefined: undefined,
func: () => {},
},
() => {},
];
console.log(stringify(origin));
console.log(JSON.stringify(origin));
Promise 与并发
完整版的 Promise 太长也太复杂,复杂的点在于如何实现链式调用,面试不太可能写完整版,但是你应该知道原理。另外 Promise 的几个并发方法也能考。
// 简单版 Promise
class MyPromise {
constructor(executor) {
this.status = "pendding";
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.status === "pendding") {
this.status = "fulfilled";
this.value = value;
this.onResolvedCallbacks.forEach((fn) => fn(value));
}
};
const reject = (reason) => {
if (this.status === "pendding") {
this.status = "rejected";
this.reason = reason;
this.onRejectedCallbacks.forEach((fn) => fn(reason));
}
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFulfilled, onRejected) {
this.onResolvedCallbacks.push(onFulfilled);
this.onRejectedCallbacks.push(onRejected);
}
}
// 并发方法以最常考的 all 举例
// 另外三个 allSettled any race 写法思路都是一样的
function all(tasks) {
return new Promise((resolve, reject) => {
let resultArr = [];
let count = 0;
function collect(res, index) {
resultArr[index] = res;
count++;
if (count === tasks.length) {
resolve(resultArr);
}
}
tasks.forEach((task, index) => {
task.then((res) => collect(res, index), reject);
});
});
}
// 增加一点难度,allSettled + 限制最大并发数
function allSettled(tasks, max) {
const len = tasks.length;
let resultArr = new Array(len);
let count = 0;
// 开始时立即执行最大并发数量的任务
while (count < max) {
next();
}
function next() {
const current = count++;
// 判断是否还有剩余任务
if (current >= len) {
// 判断是否所有任务都已经结束
if (!resultArr.includes(undefined)) {
resolve(resultArr);
}
return;
}
const task = tasks[current];
task.then(
(res) => {
resultArr[current] = res;
// 每结束一个任务,执行下一个任务
if (current < len) next();
},
(err) => {
resultArr[current] = err;
if (current < len) next();
}
);
}
}
发布订阅
class EventBus {
constructor() {
this.callbackMap = {};
}
on(name, fn) {
if (!this.callbackMap[name]) this.callbackMap[name] = [];
this.callbackMap[name].push(fn);
}
off(name, fn) {
const cbs = this.callbackMap[name] || [];
this.callbackMap[name] = cbs.filter((cb) => cb !== fn);
}
once(name, fn) {
const one = (...args) => {
fn(...args);
this.off(name, one);
};
this.on(name, one);
}
emit(name, ...args) {
const cbs = this.callbackMap[name] || [];
cbs.forEach((cb) => cb(...args));
}
}