面试官:你工作这么久,这个手写源码题你都写不出来???
我:呃... 平时没深入研究过,但是我知道这个的,我会用!😅
面试官: ...🤔🤔🤔
我: 心里想,废了...要挂了...
相信大家在面试时肯定遇到过面试官让你手写源码的情况,其实有时我们知道某个方法是什么,怎么用!但是让手写,我写不出来😰 其实写不出来还是因为没有彻底搞懂实现过程和原理。
面试中大部分手写题,都是考察编程思维 的,很明显我们的项目中出现类似多级嵌套的数组数据结构的情况不是很高;但是会有需要你使用递归、堆栈等思想 来写代码,使用其特性来解决业务中遇到的问题。
为了近一步搞懂常见的一些函数方法的实现过程及原理,在这里我借鉴了网上一些学习资料,汇总了一些高频的手写源码系列面试题,方便自己日后复习学习,谨以此篇记录自己的学习过程;如果有更优雅的实现方式或有误区和错误,欢迎大家在评论区指正!
如果文章对你有帮助,可以点赞、评论、收藏、转发互动支持哈 😀😀😀
点击链接 学习交流群(前端微信群) 加vx拉你进 前端学习交流群 让我们一起 好好学习(🐟🐟🐟)吧😎😎😎
这里手写的函数和js真正的函数的实现是不一样的;我们真正调用js上的一些如 apply、call、bind 等函数是会考虑很多边界情况的 !
我们这里只注重函数的核心实现思想、逻辑,对边界问题并不做过多的处理,手写这些函数只是为了辅助理解这些函数,以达到加深印象的目的!
手写new操作符
-
new的实现过程
- 创建了一个空的对象
- 将空对象的原型,指向于构造函数的原型(prototype)
- 执行构造函数,且将构造函数指向新的空对象(改变this指向)
- 对构造函数有返回值的处理判断(注意: 使用new就不要在构造函数中 return,通常情况下,构造函数是没有返回值的)
-
手写过程
jsfunction myNew (fn){ const obj = new Object obj.__proto__ = fn.prototype const args = [].slice.call(arguments, 1) const res = fn.apply(obj, args) return res instanceof Object ? res : obj } // 精简版 function _new(fn,...args){ const newObj = Object.create(fn.prototype); const value = fn.apply(newObj,args); return value instanceof Object ? value : newObj; }
-
测试
准备一个构造函数(通常情况下,构造函数是没有返回值的! )js// 构造函数,通常情况下,构造函数是没有返回值的! function Fn(name){ this.name = name } // 使用 const res = myNew(Fn,'啦啦啦') console.log(res) // {"name":"啦啦啦"}
手写Promise
Promise有三种状态:
pending
:等待中,是初始状态fulfilled
:成功状态rejected
:失败状态
Promise知识点:
-
执行了
resolve
,Promise状态会变成fulfilled
-
执行了
reject
,Promise状态会变成rejected
-
Promise只以
第一次为准
,第一次成功就永久
为fulfilled
,第一次失败就永远状态为rejected
-
Promise中有
throw
的话,就相当于执行了reject
简易版(函数实现)
js
function MyPromise (constructor) {
const self = this
self.status = 'pending' // 初始状态
self.promiseResult = undefined // 终值
function resolve (value) {
// 两个==="pending",保证了状态的改变是不可逆的
if (self.status === 'pending') {
self.promiseResult = value
self.status = 'fulfilled'
}
}
function reject (reason) {
// 两个==="pending",保证了状态的改变是不可逆的
if (self.status === 'pending') {
self.promiseResult = reason
self.status = 'rejected'
}
}
// 捕获构造异常
try {
constructor(resolve, reject)
} catch (e) {
reject(e)
}
}
// MyPromise的原型上定义链式调用的then方法:
MyPromise.prototype.then = function (onFullfilled, onRejected) {
const self = this
switch (self.status) {
case 'fulfilled':
onFullfilled(self.promiseResult)
break
case 'rejected':
onRejected(self.promiseResult)
break
default:
}
}
测试:
js
const p = new MyPromise((resolve, reject) => {
resolve('成功啦200')
// reject('错误')
})
p.then(
(res) => {
console.log(res)
},
(err) => {
console.log(err)
}
)
简易版(class实现)
js
class MyPromise2 {
promiseState = 'pending'
promiseResult = null
constructor(fun) {
this.initBind()
// 执行传进来的函数
fun(this.resovle, this.reject)
}
initBind(){
// 初始化this
this.resovle = this.resovle.bind(this)
this.reject = this.reject.bind(this)
}
resovle(val) {
if (this.promiseState !== 'pending') return
this.promiseState = 'fulfilled'
this.promiseResult = val
}
reject(val) {
if (this.promiseState !== 'pending') return
this.promiseState = 'rejected'
this.promiseResult = val
}
then(onFulfilled, onRejected){
if(this.promiseState === 'fulfilled'){
onFulfilled(this.promiseResult)
}else if(this.promiseState === 'rejected'){
onRejected(this.promiseResult)
}
}
}
const p = new MyPromise2((resolve, rejcet) => {
resolve('成功')
rejcet('失败')
// throw('失败')
})
console.log(p)
p.then((res)=>{
console.log(res)
}, (err)=>{
console.log(err)
})
详细版
移步参考 看了就会,手写Promise原理,最通俗易懂的版本!!! - 掘金 (juejin.cn)
手写 instanceof
思路: 接受两个参数,判断第二个参数是不是在第一个参数的原型链上
js
function myInstanceof(l, right) {
// 获得实例对象的原型 也就是 l.__proto__
let left = Object.getPrototypeOf(l)
// 获得构造函数的原型
let prototype = right.prototype
// 判断构造函数的原型 是不是 在实例的原型链上
while (true) {
// 原型链一层层向上找,都没找到 最终会为 null
if (left === null) return false
if (prototype === left) return true
// 没找到就把上一层拿过来,继续循环,再向上一层找
left = Object.getPrototypeOf(left)
}
}
// 测试
const arr1 = [1,2,3]
console.log(arr1 instanceof Array)
console.log(myInstanceof(arr1,Array))
手写call、apply、bind
call、apply、bind的区别
- 都可以改变
this
指向 - call 和 apply 会
立即执行
,bind 不会,而是返回一个函数 - call 和 bind 可以接收
多个参数
;apply
只能接受两个,第二个是数组
- bind 参数可以分多次传入
手写call
js
// 实现原理:把foo 添加到obj里->执行 foo 拿到返回值->再从 obj里删除foo->返回结果
Function.prototype.myCall = function(ctx, ...args){
ctx = ctx || window // 避免传入null问题,ctx就是传入的obj
// Symbol是因为他独一无二的,避免和obj里的属性重名
let fn = Symbol()
ctx[fn] = this // 获取需要执行的函数,myCall 函数是显示绑定this原则,所以这里的this指向的是调用myCall 函数,即下面的 foo函数
let res = ctx[fn](...args) // 执行foo函数拿到结果
delete ctx[fn] // 删除属性,释放内存
return res // 返回结果
}
手写apply(同call原理)
js
Function.prototype.myApply = function (ctx) {
ctx = ctx || window
console.log('ctx=', ctx)
let fn = Symbol()
ctx[fn] = this
let result
if (arguments[1]) { // 从arguments拿到参数
result = ctx[fn](...arguments[1]) // 执行foo且传参拿到返回值
} else {
result = ctx[fn]()
}
delete ctx[fn] // 删除属性,释放内存
return result
}
手写bind(存在闭包)
js
Function.prototype.myBind = function (context, ...args1) {
if (this === Function.prototype) {
throw new TypeError('Error')
}
const _this = this
return function F(...args2) {
// 判断是否是构造函数
if (this instanceof F) {
// 如果是构造函数,则以 foo(即_this)为构造器调用函数
return new _this(...args1, ...args2)
}
// 如果非构造函数,则用 apply 指向代码
return _this.apply(context, args1.concat(args2))
}
}
测试自定义 call,apply、bind
js
const obj = {
name: 'johnny'
}
function foo(age, hobby) {
console.log('foo函数打印:',this.name, age, hobby)
}
foo.myCall(obj, '8', 'sleep1'); // johnny 8 sleep1
foo.myApply(obj, [28, 'sleep2']); // johnny 28 sleep2
// myBind(返回一个可执行函数)
const bind1 = foo.myBind(obj, 40)
bind1('aaa') // johnny 40 aaa
手写防抖函数
防抖的思想:等待一段时间后执行最后一次操作(防抖就是在规定的时间内没有第二次的操作,才执行逻辑 。)
应用场景: 点击按钮,用户输入事件,词语联想等
js
function debounce(fn, wait) {
let timer = null
return function(){ //闭包
// 每一次点击判断有定时任务就先停止
if(timer) clearTimeout(timer)
// 没有定时任务就开启延迟任务
timer = setTimeout(() => {
// 使用 `call` 方法将 `this` 显式绑定到当前(闭包函数)上下文
fn.call(this,...arguments)
timer = null
},wait)
}
}
function sayDebounce() {
console.log("防抖成功!")
}
btn.addEventListener("click", debounce(sayDebounce,1000))
手写节流函数
节流的思想是:在一定时间间隔内执行一次操作,不管触发了多少次事件 应用场景:滚动事件、鼠标移动事件等需要限制触发频率
js
// 方案1 连续点击的话,每过 wait 秒执行一次
function throttle(fn, wait) {
let bool = true
return function () {
if (!bool) return
bool = false
setTimeout(() => {
// fn() // fn中this指向window
// 使用 `call` 方法将 `this` 显式绑定到当前(闭包函数)上下文。 fn 函数的参数是不定的,所以用arguments类数组去保存fn函数的参数。
fn.call(this, arguments)
// fn中this指向bool 下面同理
bool = true
}, wait)
}
}
// 方案2 连续点击的话,第一下点击会立即执行一次 然后每过 wait 秒执行一次
function throttle2(fn, wait) {
let date = Date.now()
return function () {
// 用当前时间 减去 上一次点击的时间 和 传进来的时间作对比
let now = Date.now()
//第二次点击时间 - 第一次 > delay
if (now - date > wait) {
fn.call(this, arguments)
date = now
}
}
}
function sayThrottle() {
console.log("节流成功!")
}
btn2.addEventListener("click", throttle(sayThrottle, 1000))
浅拷贝 (只拷贝引用的第一层数据)
- Object.assign()
- Array.prototype.concat
- Array.prototype.slice
- 手写实现
js
const clone = (target) => {
// 如果是引用类型
if (target && typeof target === "object") {
// 判断是数据还是对象,为其初始化一个数据
const cloneTarget = Array.isArray(target) ? [] : {};
// for in 可以遍历数组 对象
for (let key in target) {
if (target.hasOwnProperty(key)) {
cloneTarget[key] = target[key];
}
}
return cloneTarget;
} else {
// 基础类型 直接返回
return target;
}
};
手写深拷贝
- 递归方式实现深拷贝(不考虑循环引用问题)
js
function deepClone(obj) {
let cloneObj;
if (obj && typeof obj !== 'object') {
// 如果传入简单数据类型时,直接赋值
cloneObj = obj;
} else if (obj && typeof obj === 'object') {
// 判断是数据还是对象,为其初始化一个数据
cloneObj = Array.isArray(obj) ? [] : {};
for (let key in obj) {
// 判断对象是否存在key属性
if (obj.hasOwnProperty(key)) {
// 若当前元素类型还是对象时,递归调用deepClone实现拷贝
if (obj[key] && typeof obj[key] === 'object') {
cloneObj[key] = deepClone(obj[key]);
} else {
// 若当前元素类型为基本数据类型直接赋值
cloneObj[key] = obj[key];
}
}
}
}
return cloneObj;
}
// 测试用例
const obj1 = {
x: 1,
y: [5, 6, 7],
z: {
a: 0,
b: 1
}
}
const obj2 = deepClone(obj1)
obj1.x = 100
console.log(obj2)
console.log(obj1)
- 较完整版本
js
// Map 强引用,需要手动清除属性才能释放内存。
// WeakMap 弱引用,随时可能被垃圾回收,使内存及时释放,是解决循环引用的不二之选。
function cloneDeep(obj, map = new WeakMap()) {
if (obj === null || obj === undefined) return obj // 不进行拷贝
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj)
// 基础类型不需要深拷贝
if (typeof obj !== 'object' && typeof obj !== 'function') return obj
// 处理普通函数和箭头函数
if (typeof obj === 'function') return handleFunc(obj)
// 是对象的话就要进行深拷贝
if (map.get(obj)) return map.get(obj)
let cloneObj = new obj.constructor()
// 找到的是所属类原型上的 constructor,而原型上的 constructor 指向的是当前类本身。
map.set(obj, cloneObj)
if (getType(obj) === '[object Map]') {
obj.forEach((item, key) => {
cloneObj.set(cloneDeep(key, map), cloneDeep(item, map));
})
}
if (getType(obj) === '[object Set]') {
obj.forEach(item => {
cloneObj.add(cloneDeep(item, map));
})
}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key], map)
}
}
return cloneObj
}
// 获取更详细的数据类型
function getType(obj) {
return Object.prototype.toString.call(obj)
}
// 处理普通函数和箭头函数
function handleFunc(func) {
if(!func.prototype) return func // 箭头函数直接返回自身
const bodyReg = /(?<={)(.|\n)+(?=})/m
const paramReg = /(?<=\().+(?=\)\s+{)/
const funcString = func.toString()
// 分别匹配 函数参数 和 函数体
const param = paramReg.exec(funcString)
const body = bodyReg.exec(funcString)
if(!body) return null
if (param) {
const paramArr = param[0].split(',')
return new Function(...paramArr, body[0])
} else {
return new Function(body[0])
}
}
数组操作相关
数组扁平化
扁平化就是将多维数组变成一维数组,不存在数组的嵌套
- flat(es6 自带API)
flat(depth)depth
(可选) 指定要提取嵌套数组的结构深度,默认值为 1
使用Infinity
,可展开任意深度的嵌套数组
js
function flatten(params) {
return params.flat(Infinity)
}
- 使用 递归的思想 +concat 实现多层嵌套的数组
js
// 循环判断数组的每一项是否是数组
// 是数组就递归调用上面的扁平化一层的代码
// 不是数组,直接通过push添加到返回值数组
function flatten(arr) {
let result = [];
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i]));
} else {
result.push(arr[i])
}
}
return result
}
console.log(flatten(arr));
- 使用堆栈 stack 避免递归
递归循环都可通过维护一个堆结构来解决
如果不使用递归数组来实现扁平化,可以使用堆栈来解决
深度的控制比较低效,因为需要检查每一个值的深度
思路:
- 把数组通过一个栈来维护
- 当栈不为空的时候循环执行处理
- pop()将栈尾出栈
- 如果出栈的元素是数组,就将该元素解构后每一元素进行入栈操作
- 出栈的元素不是数组就push进返回值res
- 反转恢复原数组的顺序
js
// 使用数据结构栈的特性取代递归操作,减少时间复杂度
var arr1 = [1,2,3,[1,2,3,4, [2,3,4]]];
function flatten(arr) {
const stack = [...arr];
const res = [];
while (stack.length) {
// 使用 pop 从 stack 中取出并移除值
const next = stack.pop();
if (Array.isArray(next)) {
// 使用 push + 展开运算符 送回内层数组中的元素,不会改动原始输入
stack.push(...next);
} else {
res.push(next);
}
}
// 反转恢复原数组的顺序
return res.reverse();
}
flatten(arr1);// [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
数组去重
- from + set去重(es6 )
Array.from: 可将一个类数组对象或者可遍历对象转换成一个真正的数组
Array.Set: ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
js
function unique(arr) {
// set返回一个类数组
return Array.from(new Set(arr))
}
- set+扩展运算符去重
js
function unique(arr) {
// 利用Set去重后直接扩展运算符放入数组中返回
return [...new Set(arr)]
}
- forEach循环 + includes去重
js
function unique(arr){
let res = []
arr.forEach((item)=>{
if(!res.includes(item)){
res.push(item)
}
// 或者
// if(res.indexOf(item)===-1){
// res.push(item)
// }
})
return res
}
- 排序+循环去重
js
function unique( arr ){
const res = [];
arr = arr.sort();
for(let i=0;i<arr.length;i++){
// console.log('i-1=>',arr[i-1],arr[i])
if( arr[i] !== arr[i-1]){
res.push( arr[i] );
}
}
return res;
}
- filter+indexOf 去重
js
function unique(arr) {
return arr.filter((item, index, array) => {
// indexOf 如果要检索的字符串值没有出现,则该方法返回 -1。
// console.log(array.indexOf(item))
return array.indexOf(item) === index;
});
}
vue相关
双向数据绑定实现
js
const obj = {}
// 数据劫持
Object.defineProperty(obj, 'text', {
// 这里监听obj下的 text属性
configurable: true, // 可配置
enumerable: true, // 可枚举
get() {
// 获取数据就直接拿
console.log('get获取数据',box.innerHTML)
return box.innerHTML
},
set(newVal) {
// 修改数据就重新赋值
console.log('set数据更新')
input.value = newVal
box.innerHTML = newVal
}
})
// 输入监听
input.addEventListener('keyup', function(e) {
obj.text = e.target.value
})
// 获取数据
btn.addEventListener("click", function(e){
console.log('点击获取到的数据为:',obj.text)
})
设计模式相关
发布订阅者模式
- 简单版本1
js
class EventHub {
map = {} // 相当于在 constructor 中写 this.map = {}
// 添加订阅
on(name, fn) {
this.map[name] = this.map[name] || []
this.map[name].push(fn)
}
// 取消订阅
off(name, fn) {
const q = this.map[name]
if (!q) return
const index = q.indexOf(fn)
if (index < 0) return
q.splice(index, 1)
}
// 触发订阅
emit(name, data) {
const q = this.map[name]
if (!q) return
q.map(fn => fn.call(undefined, data))
}
}
// 测试
const person1 = new EventHub()
// 订阅
person1.on('a',handlerA)
person1.on('a',handlerB)
// 触发
// person1.emit('a')
console.log('person1===', person1)
person1.off('a',handlerA)
// console.log('person1==1=', person1)
- 版本2
js
class Observer {
constructor() {
this.message = {} // 消息队列
}
/**
* `$on` 向消息队列添加内容(添加订阅)
* @param {*} type 事件名 (事件类型)
* @param {*} callback 回调函数
*/
$on(type, callback) {
// 判断有没有这个属性(事件类型)
if (!this.message[type]) {
// 如果没有这个属性,就初始化一个空的数组
this.message[type] = [];
}
// 如果有这个属性,就往他的后面push一个新的callback
this.message[type].push(callback);
}
/**
* $off 删除消息队列里的内容(取消订阅)
* @param {*} type 事件名 (事件类型)
* @param {*} callback 回调函数
*/
$off(type, callback) {
// 判断是否有订阅,即消息队列里是否有type这个类型的事件,没有的话就直接return
if (!this.message[type]) return;
// 判断是否有callback这个参数
if (!callback) {
// 如果没有callback,就删掉整个事件
this.message[type] = undefined;
return;
}
// 如果有callback,就仅仅删掉callback这个消息(过滤掉这个消息方法)
this.message[type] = this.message[type].filter((item) => item !== callback);
}
/**
* $emit 触发消息队列里的内容
* @param {*} type 事件名 (事件类型)
*/
$emit(type) {
// 判断是否有订阅
if (!this.message[type]) return;
// 如果有订阅,就对这个`type`事件做一个轮询 (for循环)
this.message[type].forEach(item => {
// 挨个执行每一个消息的回调函数callback
item()
});
}
}
// ----------------测试
function handlerA() {
console.log('buy handlerA');
}
function handlerB() {
console.log('buy handlerB');
}
function handlerC() {
console.log('buy handlerC');
}
// 使用构造函数创建一个实例
const person2 = new Observer();
// 订阅
person2.$on('buy', handlerA);
person2.$on('buy', handlerB);
person2.$on('buy', handlerC);
console.log('person2 :>> ', person2);
// 触发 buy 事件
person2.$emit('buy')