面试常考手撕源码题

284 阅读4分钟

手写防抖节流

  1. 防抖和节流区别=>防抖是 N 秒内函数只会被执行一次,如果 N 秒内再次被触发,则重新计算延迟时间(举个极端的例子 如果 window 滚动事件添加了防抖 2s 执行一次 如果你不停地滚动 永远不停下 那这个回调函数就永远无法执行)节流是规定一个单位时间,在这个单位时间内最多只能触发一次函数执行(还是滚动事件 如果你一直不停地滚动 那么2 秒就会执行一次回调)
  2. 防抖怎么保证 事件延迟执行 并且在规定时间内再次触发需要清除 这个很容易就想到了 setTimeout
  3. 节流怎么保证 在单位时间内触发了一次就不再生效了 可以用一个 flag 标志来控制
// 防抖
function debounce(fn, delay) {
	let timer;
	return function () {
		var args = arguments;
		if (timer) {
			clearTimeout(timer);
		}
		timer = setTimeout(() => {
			fn.apply(this, args);
		}, delay);
	};
}
// 节流
function throttle(fn, delay) {
	// let flag = true;
	// return () => {
	//   if (!flag) return;
	//   flag = false;
	//   timer = setTimeout(() => {
	//     fn();
	//     flag = true;
	//   }, delay);
	// }

	let startTime = new Date();
	return () => {
		let endTime = new Date();
		if (endTime - startTime >= delay) {
			fn();
			startTime = endTime;
		} else {
			return;
		}
	};
}

手写 EventEmitter(发布订阅模式--简单版)

  1. 什么是发布订阅模式=>发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知
  2. 怎么实现一对多=>既然一对多 肯定有一个事件调度中心用来调度事件 用户可以注册事件到事件中心 发布者可以发布事件到调度中心 用户也可以取消订阅或者只订阅一次
class EventEmitter {
	constructor() {
		this.events = {};
	}
	on(type, callback) {
		if (!this.events) this.events = Object.create(null);

		if (!this.events[type]) {
			this.events[type] = [callback];
		} else {
			this.events[type].push(callback);
		}
	}
	off(type, callback) {
		if (!this.events[type]) return;
		this.events[type] = this.events[type].filter((item) => item !== callback);
	}
	once(type, callback) {
		function fn() {
			callback();
			this.off(type, fn);
		}
		this.on(type, fn);
	}
	emit(type, ...rest) {
		this.events[type] && this.events[type].forEach((fn) => fn.apply(this, rest));
	}
}
  1. call 用法=>第一个参数 可以改变调用函数的 this 指向 第二个以及之后的参数为传入的函数的参数
  2. 怎么改变 this 指向呢=>对象的方法调用 那么方法内部的 this 就指向这个对象
  3. 怎么获取传入的不定参数呢 =>...args 剩余参数获取方法(rest)
Function.prototype.myCall = function (context, ...args) {
	context = context || window;
	let fn = Symbol();
	context[fn] = this;
	const result = context[fn](...args);
	delete context.fn;
	return result;
};
Function.prototype.myApply = function (context, args) {
	if (typeof this !== 'function') {
		throw new TypeError('Error');
	}
	context = context || window;
	let fn = Symbol();
	context[fn] = this;
	const result = context[fn](...args);
	delete context.fn;
	return result;
};
Function.prototype.myBind = function (context, ...args) {
	context = context || window;
	const _this = this;
	return function F() {
		if (this instanceof F) {
			return new _this(...args, ...arguments);
		}
		return _this.apply(context, args.concat(...arguments));
	};
};

手写 new 操作符

  1. new 用法=>从构造函数创造一个实例对象 构造函数的 this 指向为创造的实例函数 并且可以使用构造函数原型属性和方法
  2. 怎么实现 this 指向改变 =>call apply
  3. 怎么实现构造函数原型属性和方法的使用 =>原型链 原型继承
function myNew(fn, ...args) {
	let obj = {};
	obj.__proto__ = fn.prototype;
	// obj = Object.create(fn.prototype);
	let result = fn.call(obj, ...args);
	if ((result && typeof result === 'object') || typeof result === 'function') {
		return result;
	}
	return obj;
}

手写 instanceof

  1. instanceof 原理=>右侧对象的原型对象(prototype )是否在左侧对象的原型链上面
  2. 怎么遍历左侧对象全部的原型链是关键点=>while(true) 一直遍历 知道原型链的尽头 null 都没有相等就说明不存在 返回 false
function myInstanceof(left, right) {
	let leftProp = left.__proto__;
	let rightProp = right.__proto__;
	while (true) {
		if (leftProp === null) return false;
		if (leftProp === rightProp) {
			return true;
		} else {
			leftProp = leftProp.__proto__;
		}
	}
}

JavaScript 实现 reduce() 方法函数

Array.prototype.myReduce = function(fn, initialValue) {
	const array = this;
	let acc = initialValue || array[0];
	const startIndex = initialValue ? 0 : 1;

	for (let i = startIndex;i<array.length;i++) {
		const cur = array[i];
		acc = fn(acc, cur, i, array);
	}
	return acc;
}

JavaScript 实现 filter() 方法函数

Array.prototype.myFilter = function(fn) {
	let arr = [];
	let arr1 = Array.prototype.slice.call(this, 0, this.length);

	for(let i=0;i<arr1.length;i++) {
		if(fn(this[i]), i, this) {
			arr.push(arr1[i]);
		}
	}

	return arr;
}

深度优先

深度优先遍历——是指从某个顶点出发,首先访问这个顶点,然后找出刚访问 这个结点的第一个未被访问的邻结点,然后再以此邻结点为顶点,继续找它的 下一个顶点进行访问。重复此步骤,直至所有结点都被访问完为止。

// 深度优先遍历的递归写法
function deepTraversal(node) {
	let nodes = [];
	if (node !== null) {
		nodes.push(node);
		let childrens = node.children;
		for (let i=0;i<childrens.length;i++) {
			deepTraversal(childrens[i]);
		}
	}
	return nodes;
}

广度优先

广度优先遍历——是从某个顶点出发,首先访问这个顶点,然后找出刚访问这 个结点所有未被访问的邻结点,访问完后再访问这些结点中第一个邻结点的所 有结点,重复此方法,直到所有结点都被访问完为止。

function wideTraversal(node) {
	let nodes = [], i=0;
	if (node!==null) {
		nodes.push(node);
		wideTraversal(node.nextElementSibling);
		node += nodes[i++];
		wideTraversal(node.firstElementChild)
	}
	return nodes;
}

用 JavaScript 写一个函数,输入 int 型,返回整数逆序后的字符串。如:输入 整型 1234,返回字符串“4321”。要求必须使用递归函数调用,不能用全局变量, 输入函数必须只有一个参数传入,必须返回字符串。

function fun(num) {
	let num1 = num / 10;
	let num2 = num % 10;
	if (num1 < 1) {
		return num;
	} else {
		num1 = Math.floor(num1);
		return `${num2}${fun(num1)}`;
	}
}
var a = fun(12345);
console.log(a);
console.log(typeof a);