背景
去年年底12月份第一次被裁。今年5月份找到新的工作,最近又被裁了,还卡着我还有几天就满的六个月试用期。说我试用期没满,不给赔偿。嗐,一言难尽~
又得重新找工作了,所以搜集了一波比较高频的js手写代码题,记录一下。
下面所有代码都已经上传github仓库:github.com/ryan6015/fr…
并都带有测试用例。
-
模拟new操作符
首先要知道new操作符有哪些操作:
- 创建一个空对象
- 把这个对象的原型指向函数的prototype对象
- 将函数作用于指向这个对象并执行构造函数,
- 返回对象
/**
* 实现一个函数模拟new操作符
*
* 首先想清楚在new的过程中,有哪些操作
* 1. 创建一个空对象
* 2. 对象的原型指向函数的prototype对象
* 3. 执行构造函数,
* 4. 返回对象(构造函数一般不会返回东西,但是也有可能,如果返回的是对象的话,那么就返回这个对象)
*
* @param {*} fn 构造函数
* @param {...any} args 参数
*/
function _new(fn, ...args) {
if (typeof fn !== "function") {
throw new TypeError("fn must be a function");
}
const obj = {};
// 对象的原型指向函数的prototype对象
Object.setPrototypeOf(obj, fn.prototype)
// 执行构造函数,将this指向obj
const res = fn.apply(obj, args);
// 如果构造函数返回的是一个对象,那么就返回这个对象,否则返回obj
return typeof res === "object" ? res : obj;
}
-
模拟call函数实现
call函数会改变函数中this的指向,并且同时接收多个参数,执行函数。
我们在模拟实现中要改变this的指向,可以把函数放在那个对象上运行。
/**
* 实现一个函数,模拟call方法实现
*
* call函数改变了this的指向,并且执行了函数
* 想要改变函数中this的指向,我们可以把函数放到某个对象中,然后在对象中执行函数
*
* @param {funtion} fn 执行函数
* @param {object} context 执行函数的上下文
* @param {...any} args 参数
*/
function _call(fn, context, ...args) {
if (typeof fn !== "function") {
throw new TypeError("fn must be a function");
}
// 如果context为null,那么就指向window
// 这里jest运行在node环境上,所以没有window,用global代替
// 如果context是一个基本类型,那么就把它转换成对象类型
const ctx = context ? Object(context) : global;
// 把函数放到对象中, 这里会有重名的风险,简单写下,重要的是是原理
// 所以忽略这个问题,在实际开发中,不要这样做
ctx._fn = fn;
// 执行函数
const result = ctx._fn(...args);
// 删除函数
delete ctx._fn;
// 返回结果
return result;
}
-
模拟apply函数实现
apply和call的差别就是接收参数形式的不同。
apply函数接收一个数组,call接收多个参数。
/**
* 实现一个函数,模拟apply方法实现
*
* apply和call的区别就在于,接受的参数apply接受的是一个数组,call接受的是多个参数
*
* @param {funtion} fn 执行函数
* @param {object} context 执行函数的上下文
* @param {any[]} args 参数
*/
function _apply(fn, context, args) {
if (typeof fn !== "function") {
throw new TypeError("fn must be a function");
}
// 如果context为null,那么就指向window
// 这里jest运行在node环境上,所以没有window,用global代替
// 如果context是一个基本类型,那么就把它转换成对象类型
const ctx = context ? Object(context) : global;
// 把函数放到对象中, 这里会有重名的风险,简单写下,重要的是是原理
// 所以忽略这个问题,在实际开发中,不要这样做
ctx._fn = fn;
// 执行函数
const result = ctx._fn(...args);
// 删除函数
delete ctx._fn;
// 返回结果
return result;
}
-
模拟bind函数实现
bind函数相对于call和apply会复杂一些。先看看bind函数原本的功能: MDN-bind
- 返回一个新的函数,绑定函数的this指向,
- 同时接收不定量的参数,这些参数会在函数运行前放在前面传给函数
- 要注意的是:直接new的方式调用bind函数,那么this指向的是新创建的对象,而不是context
- 如果context为空,指向全局变量
/**
* 模拟bind函数实现
*
* 首先想想bind函数内部做了啥,
* 1. 返回一个新的函数,绑定函数的this指向
* 2. 同时接收不定量的参数,这些参数会在函数运行前放在前面传给函数
* 3. 要注意的是:直接new的方式调用bind函数,那么this指向的是新创建的对象,
* 而不是context
* 4. 如果context为空,指向全局变量
*
* @param {function} fn 执行函数
* @param {object} context 执行函数的上下文
* @param {...any} args 参数
*/
function _bind(fn, context, ...args) {
if (typeof fn !== "function") {
throw new TypeError("fn must be a function");
}
const boundFunction = function (...argLists) {
let ctx = context ? Object(context) : global;
// 这里判断this是否是func的实例(是否使用new),如果是,那么就返回this,
if (this instanceof boundFunction) {
return new fn(args.concat(argLists));
} else {
// 要加上之前bind时传入的参数,有点像是函数柯里化
return fn.apply(ctx, args.concat(argLists));
}
};
// 将函数的原型指向fn的原型,这样在使用new时,也可以继承fn的原型
boundFunction.prototype = Object.create(fn.prototype);
boundFunction.prototype.constructor = boundFunction;
return boundFunction;
}
-
实现一个深度克隆函数
深度克隆的话,平时比较简单的情况可能都会使用JSON.parse(JSON.stringify(obj))
,这个可以实现深拷贝,但是存在诸多限制。
-
对一些特殊类型的值无法处理(可以查看下面控制台打印截图)
- 会忽略函数类型,
- 正则对象会变成空对象,
- 日期对象会变成字符串,
- 会忽略symbol类型,
- set,map类型会变成空对象
-
循环引用会报错。
下面用另种一种方式兼容更多的类型
function deepClone(obj) {
// 处理特殊类型, undefined的typeof是undefined
if (typeof obj !== "object" || obj === null) {
return obj;
}
let res;
if (Array.isArray(obj)) {
res = [];
for (let i = 0; i < obj.length; i++) {
res[i] = deepClone(obj[i]);
}
} else {
res = {};
Object.keys(obj).forEach((key) => {
res[key] = deepClone(obj[key]);
});
}
return res;
}
上面这种方式,可以兼容更多的类型,还是没法处理循环引用的情况。如果要解决循环引用的问题,可以这么写:
function deepCloneWithMap(obj, map = new WeakMap()) {
if (typeof obj !== "object" || obj === null) {
return obj;
}
if (map.has(obj)) {
return map.get(obj);
}
let res;
if (Array.isArray(obj)) {
res = [];
} else {
res = {};
}
// map的设置必须在循环之前,否则会无限递归
map.set(obj, res);
if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
res[i] = deepCloneWithMap(obj[i], map);
}
} else {
Object.keys(obj).forEach((key) => {
res[key] = deepCloneWithMap(obj[key], map);
});
}
return res;
}
用map保存已经拷贝过得对象,如果对象再次拷贝的话,那么就直接取出来,避免无限循环。
循环引用的这个贴一个测试用例:
test("测试循环引用 deepCloneWithMap", () => {
const originalObj = {
name: "John",
age: 30,
hobbies: [
"reading",
{
book: "abc",
self: this,
},
],
address: {
city: "New York",
street: "Main St",
},
};
// 制造循环引用
originalObj.self = originalObj;
const clonedOriginalObj = deepCloneWithMap(originalObj);
expect(clonedOriginalObj).toEqual(originalObj);
});
-
实现防抖函数
防抖的核心思想是在事件被频繁触发的过程中,只让函数在最后一次触发事件后的一段特定延迟时间之后执行,如果在延迟时间内事件又被触发了,那么就重新计时延迟时间,直到延迟时间内没有新的触发,函数才会执行。简单来说,就是多次触发同一个事件,只执行最后一次触发对应的操作。
/**
* 防抖
* @param {*} fn 函数
* @param {*} delay 延迟时间
*/
function debounce(fn, delay) {
let timer = null;
return function (...args) {
if (timer) {
clearTimeout(timer);
}
const context = this;
timer = setTimeout(() => {
timer = null;
fn.apply(context, args);
}, delay);
};
}
-
实现节流函数
节流的目的是限制函数在一定时间内只能被触发一次,无论这段时间内事件被触发了多少次,函数都只会执行一次,然后在规定的时间间隔过去之后,才可以再次执行。它就像是水龙头限流一样,按照固定的时间间隔来 “放水”(执行函数)。
/**
* 节流
* @param {*} fn 函数
* @param {*} delay 延迟时间
*/
function throttle(fn, delay) {
let timer = null;
// 这里设置成0,首次调用会立即执行
// 如果希望首次调用也要满足延时时间,就设置成Date.now()
let lastTime = 0;
return function (...args) {
const now = Date.now();
const remaining = delay - (now - lastTime);
const context = this;
// 空闲,但是还没到时间,重新设置定时器
if (!timer && remaining > 0) {
timer = setTimeout(() => {
fn.apply(context, args);
timer = null;
lastTime = now;
}, remaining);
} else if (!timer && remaining <= 0) {
// 空闲,并且时间到了
fn.apply(context, args);
lastTime = now;
}
};
}
上面所有代码都已经上传github仓库:github.com/ryan6015/fr…
上面代码如果存在问题的话,欢迎在评论区指出。
下一篇文章: Promise静态方法实现--JS手写代码题(二)