本文整理了一下常见的手写面试题,将持续更新,如有需求可评论联系~
1、手写深拷贝
/* 手写深拷贝 */
function deepCopy1(obj) {
return JSON.parse(JSON.stringify(obj));
}
function deepCopy2(obj) {
//null 也是 object,但是null 没有 constructor
if (typeof obj === "object" && obj !== null) {
var result = obj.constructor === Array ? [] : {};
for (const key in obj) {
result[key] =
typeof obj[key] === "object" ? deepCopy2(obj[key]) : obj[key];
}
return result;
} else {
var result = obj;
return result;
}
}
const obj1 = {
name: "姓名",
str: [1, 2, 3, 4, 5],
attr: {
type: "name",
},
strUndefined: undefined,
strNull: null,
};
const obj2 = obj1;
const obj3 = deepCopy1(obj1);
const obj4 = deepCopy2(obj1);
console.log(obj2, obj2 === obj1);
//第一种实现深拷贝的方式,会把属性值为undefined的属性从对象去除掉
console.log(obj3, obj3 === obj1);
console.log(obj4, obj4 === obj1);
2、手写ajax
function sendAjax(obj) {
//get方式传入的时候,将data内容进行拼接
function splitStr(data) {
let str = "";
for (const key in data) {
str += key + "=" + data[key];
}
return str;
}
//原生Ajax实现步骤分析
// 一、声明XMLHttpRequest
var xhr = new XMLHttpRequest();
//二、初始化HTTP请求参数,只初始化并不会发送
if (obj.method.toUpperCase() === "GET") {
//get方法
xhr.open(
obj.method,
obj.url + "?" + splitStr(obj.data), //路径拼接
typeof obj.async === "boolean" ? obj.async : true
);
// 三、发送请求
xhr.send();
} else if (obj.method.toUpperCase() === "POST") {
//post方法
xhr.open(
obj.method,
obj.url,
typeof obj.async === "boolean" ? obj.async : true
);
xhr.setRequestHeader("content-type", "application/json"); //设置请求头,以表单方式提交
xhr.send(obj.data); //发送请求
}
//四、监听发送
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
//success回调
obj.success(xhr.responseText);
} else if (xhr.readyState === 4 && xhr.status !== 200) {
obj.error();
}
};
}
//调用方法
sendAjax({
method: "post",
data: {
name: "姓名",
age: 18,
age2: 19,
},
url: "http://127.0.0.1:5500/",
async: false,
success: function (result) {
console.log(result, "发送成功了");
},
error: () => {
console.log("发生了错误");
},
});
3、手写代码,监听数组变化,并返回数组长度
var arr = [];
//Object.getOwnPropertyNames方法返回一个数组,成员是参数对象自身的全部属性的属性名,不管该属性是否可遍历。
//Object.keys的行为不同,Object.keys只返回对象自身的可遍历属性的全部属性名。
console.log(Object.getOwnPropertyNames(Array.prototype), "arr");
Object.keys([]); // []
Object.getOwnPropertyNames([]); // [ 'length' ]
Object.keys(Object.prototype); // []
Object.getOwnPropertyNames(Object.prototype);
// ['hasOwnProperty',
// 'valueOf',
// 'constructor',
// 'toLocaleString',
// 'isPrototypeOf',
// 'propertyIsEnumerable',
// 'toString']
//Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
const arrayMethods = Object.create(Array.prototype);
console.log(arrayMethods, "arrayMethods");
var a = { m: 1 };
var b = Object.create(a);
console.log(b.__proto__ === a, b, b.m); // true {} 1
// 获取Array的原型,并创建一个新的对象指向这个原型
// const arrayMethods = Object.create(Array.prototype)
//手写代码,监测数组变化,并返回数组长度
// 创建一个新的原型,这就是改造之后的数组原型
const ArrayProto = [];
// 重新构建Array原型里面的虽有方法
Object.getOwnPropertyNames(Array.prototype).forEach((method) => {
if (typeof Array.prototype[method] === "function") {
ArrayProto[method] = function () {
console.log("监听到数组触发了" + method + "事件", arguments);
let len = this.length;
let result = Array.prototype[method].apply(this, arguments);
console.log(len, this.length);
if (len !== this.length) {
return this.length;
}
return result;
};
}
});
let list = [1, 2, 3];
//将数组的原型链指向重新构造的原型
list.__proto__ = ArrayProto;
// 执行push事件
console.log(list.push(1), list.pop(2), list.slice(1), list.unshift(1));
4、手写防抖和节流
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<input type="text" />
</body>
</html>
<script>
function debounce(fn, delay) {
// 1. 创建一个变量,记录上一次定时器
let timer = null;
// 2. 触发事件时执行函数
return function (...args) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
console.log(this);
//可以通过显示绑定apply,来获取事件的调用者,如果不需要直接fn(args)
fn.apply(this, args);
// fn();
timer = null;
}, delay);
};
}
function throttle(fn, interval) {
let startTime = 0;
return function (...args) {
const nowTime = new Date().getTime();
const waitTime = interval - (nowTime - startTime);
if (waitTime <= 0) {
fn.apply(this, args);
startTime = nowTime;
}
};
}
const inputDOM = document.querySelector("input");
let counter = 1;
function searchChange() {
console.log(`第${counter++}次请求`, this.value, this);
}
inputDOM.oninput = debounce(searchChange, 500);
</script>
5、手写实现发布/订阅模式
class Subject {
//定义被观察者
constructor() {
this.observers = [];
}
addObserver(observer) {
//订阅
this.observers.push(observer);
}
removerObserver(observer) {
//取消订阅
let index = this.observers.indexOf(observer);
if (index !== -1) {
this.observers.splice(index, 1);
}
}
notify() {
//通知
this.observers.forEach((observer) => {
observer.update();
});
}
}
class Observer {
// 定义观察者
update() {
console.log("subject更新了");
}
subscribeTo(subject) {
subject.addObserver(this);
}
}
let subject = new Subject(); //被观察者
let observer1 = new Observer(); //观察者
observer1.subscribeTo(subject); //观察者进行订阅
let observer2 = new Observer(); //观察者
observer2.subscribeTo(subject); //观察者进行订阅
subject.notify();
6、数组去重
/* 数组去重 */
// 数组去重比较常用的方法,我理解是有三种方法,元素、索引、es6的set。基本上都是基于此三种来实现
//另外,如果元素是数组或对象,是不能被去重的,因为对象和数组是引用类型,
// 1、es6的set方法
var arr = [
true,
9,
null,
true,
2,
5,
"我",
"我",
2,
[1],
undefined,
null,
undefined,
{ name: "姓名" },
{ name: "姓名" },
[1],
];
console.log(arr);
function noRepeat(arr) {
var newArr = [...new Set(arr)]; //利用了Set结构不能接收重复数据的特点
return newArr;
}
var arr2 = noRepeat(arr);
console.log(arr2);
//2、filter方法去重
function noRepeat2(arr) {
var newArr = arr.filter(function (item, index) {
return arr.indexOf(item) === index; // 因为indexOf 只能查找到第一个
});
}
var arr3 = noRepeat(arr);
console.log(arr3, "arr3");
//3、使用includes
function noRepeat3(arr) {
let newArr = [];
for (i = 0; i < arr.length; i++) {
if (!newArr.includes(arr[i])) {
newArr.push(arr[i]);
}
}
return newArr;
}
console.log(noRepeat3(arr), "includes");
7、手写call、bind、apply
/* 手写apply、call、bind */
/* 1、apply,call,bind都是可以改变this的指向
2、apply,call会执行调用的函数,bind返回一个新函数。
3、apply第二个参数要求是数组,call,bind则没有限制数据类型,它会把剩余的参数一起传给函数,
bind还会把新函数调用时传入的参数一起合并,传给新函数。
4、他们都是绑定在Function的prototype上。
*/
// 1、apply
Function.prototype.apply = function (context, args) {
// 不传默认是全局,window
context = context || window;
// args不传时默认是空数组,防止下面用spread操作符时报错
args = args ? args : [];
// 把this存到context.fn,这里的this是调用的函数
console.log(this, "this");
context.fn = this;
// 执行调用的函数,this指向context,参数用spread操作符扩展
const res = context.fn(...args);
// 删除,不污染context
delete context.fn;
// 返回res
return res;
};
function fn(a, b, c) {
console.log(this.name);
console.log(a, b, c);
}
let obj = { name: "码上游" };
fn.apply(obj, [1, 2, 3]); //改变fn的this指向,让其指向obj,最后再调用fn函数
// 输出 码上游 1 2 3
//2、call
Function.prototype.call = function (context, ...args) {
// 不传默认是全局,window
context = context || window;
// args不传时默认是空数组,防止下面用spread操作符时报错
args = args ? args : [];
// 把this存到context.fn,这里的this是调用的函数
context.fn = this;
// 执行调用的函数,this指向context,参数用spread操作符扩展
const res = context.fn(...args);
// 删除,不污染context
delete context.fn;
// 返回res
return res;
};
//3、bind
/* instanceof 是用来判断左侧对象是否是右侧构造函数的实例化对象,
或则说左侧对象能否通过其隐式原型 **[[proto]]**在原型链上一层层向上查找到右侧函数的原型对象,
即函数原型对象出现在实例对象的原型链上就返回 true。
通俗的理解: 右侧是不是左侧的爸爸、爷爷、祖宗,只要左侧对象继承自右侧函数就为 true */
Function.prototype.bind = function (context, ...args) {
// 不传默认是全局,window
context = context || window;
// 把this存到fn,这里的this是调用的函数
let fn = this;
return function newFn(...fnArgs) {
let res;
// 要考虑新函数是不是会当作构造函数
if (this instanceof newFn) {
// 如果是构造函数则调用new 并且合并参数args,fnArgs
res = new fn(...args, ...fnArgs);
} else {
// 当作普通函数调用 也可以用上面定义的_call
res = fn.call(context, ...args, ...fnArgs);
}
return res;
};
};
8、手写promise
//手写Promise
function MyPromise(executor) {
this.status = "pending";
this.result = undefined;
this.cb = [];
var _this = this;
function resolve(res) {
if (_this.status !== "pending") {
return;
}
_this.status = "fulfilled";
_this.result = res;
_this.cb.forEach((item) => {
item.successCB && item.successCB(_this.result);
});
}
function reject(res) {
if (_this.status !== "pending") {
return;
}
_this.status = "rejected";
_this.result = res;
_this.cb.forEach((item) => {
item.failCB && item.failCB(_this.result);
});
}
executor(resolve, reject);
}
MyPromise.prototype.then = function (successCB, failCB) {
if (!successCB) {
successCB = (value) => {
return value;
};
}
if (!failCB) {
failCB = (error) => {
return error;
};
}
return new MyPromise((resolve, reject) => {
if (this.status === "fulfilled") {
var result = successCB && successCB(this.result);
if (result instanceof MyPromise) {
//如果result还是一个promise
result.then(
(res) => {
resolve(res);
},
(err) => {
reject(err);
}
);
} else {
resolve(result);
}
}
if (this.status === "failed") {
var result = failCB && failCB(this.result);
if (result instanceof MyPromise) {
//如果result还是一个promise
result.then(
(res) => {
resolve(res);
},
(err) => {
reject(err);
}
);
} else {
reject(result);
}
}
if (this.status === "pending") {
//收集回调
this.cb.push({
successCB: () => {
var result = successCB && successCB(this.result);
if (result instanceof MyPromise) {
//如果result还是一个promise
result.then(
(res) => {
resolve(res);
},
(err) => {
reject(err);
}
);
} else {
resolve(result);
}
},
failCB: () => {
var result = failCB && failCB(this.result);
if (result instanceof MyPromise) {
//如果result还是一个promise
result.then(
(res) => {
resolve(res);
},
(err) => {
reject(err);
}
);
} else {
reject(result);
}
},
});
}
});
};
MyPromise.prototype.catch = function (failCB) {
this.then(undefined, failCB);
};
//测试调用
new MyPromise((resolve, reject) => {
console.log("五秒后执行-MyPromise");
setTimeout(() => {
// resolve("MyPromise返回的数据是成功的");
reject("MyPromise返回的数据是失败的");
}, 5000);
})
.then(
(res) => {
console.log(res, "成功");
},
(err) => {
console.log(err, "失败");
}
)
.catch((err) => {
console.log("MyPromise的catch的错误", err);
});
new Promise((resolve, reject) => {
console.log("五秒后执行-Promise");
setTimeout(() => {
// resolve("Promise返回的数据是成功的");
reject("Promise返回的数据是失败的");
}, 5000);
})
.then(
(res) => {
console.log(res, "成功");
},
//如果没有下面的reject函数,那就会执行catch函数,获取到失败信息
(err) => {
console.log(err, "失败");
}
)
.catch((err) => {
console.log("Promise的catch的错误", err);
});