前言
在面试的时候,经常有面试官问候选人:“你可以实现一个XXX嘛?”。如果候选人没有写过的话,可能会多多少少有些发懵。随着前端的发展,前端的门槛变得越来越高,很多面试官开始注重候选人的coding能力。如果没有写好的话,可能会给面试官留下不太好的印象。下面是一些常考的基础手写代码总结。
new
new
用构造函数创建实例对象,为实例对象添加this属性和方法。
new
在调用过程中实现了以下几个步骤:
- 创建一个新的对象
- 链接到原型,将该对象 obj 的原型链指向构造函数的原型 prototype
- 绑定this,让this变量指向这个新创建的对象
- 返回新对象
function createNew() {
// 创建一个空的对象
var obj = new Object();
// 获得构造函数,arguments中去除第一个参数
var Con = [].shift.call(arguments);
// 链接到原型,obj 可以访问到构造函数原型中的属性
obj.__proto__ = Con.prototype;
// 绑定 this 实现继承,obj 可以访问到构造函数中的属性
var res = Con.apply(obj, arguments);
// 优先返回构造函数返回的对象,判断下返回的值是不是一个对象,如果是对象则返回这个对象,不然返回新创建的 obj对象。
return res instanceof Object ? res : obj;
};
instanceof
instanceof
用于判断对象的具体类型。它依靠原型链向上查找,遍历左侧变量的原型链,查看右侧变量的 prototype是否在左边的原型链上。如果查找失败,返回false。
function creatInstanceof(left, right) {
//如果不是object或者为null,直接返回false
if (typeof(left)!== 'object' || left === null) return false;
// 取左表达式的__proto__值
let left = left.__proto__;
while (true) {
if (left === null || left === undefined)
return false;
//判断右表达式的 prototype 值是否和左表达式的__proto__值相等
if (right.prototype === left)
return true;
//往下走
left = left.__proto__;
}
}
节流
节流
主要用来稀释函数的执行次数,当持续触发事件时,让函数在特定的时间内只执行一次。例如:假设我们设定延时时间为1000ms,有一个函数A一直在被调用,我们使用节流
,就可以让它1000ms执行一次,而不是一直在执行。
function throttle(fn, delay = 1000) {
// 定义开始时间
var startTime = 0;
return function () {
//获取当前时间
var nowTime = Date.now();
//如果当前时间减去开始时间大于约定的延时时间,则执行
if (nowTime - startTime > delay) {
//执行函数,同时改变this当前指向
fn.call(this);
//更新开始时间的值
startTime = nowTime;
}
}
}
//应用:滑动鼠标输出12345(如果一直滑动的话,会1000ms输出一次)
document.onmousemove = throttle(() =>{
console.log('12345');
},1000)
防抖
防抖
是函数在特定的时间内不被调用后再执行。例如:假设我们设定延时时间为1000ms,有一个按钮,点击这个按钮生成随机数。我们使用防抖
,点击按钮之后的1000ms内,按钮没有被再次点击,才会生成随机数,如果在1000ms内按钮被再次点击,那么它会重新计时,不会生成随机数。(搜索框的联想功能也会应用到防抖
思想)
const debounce = function(fn,delay){
//定义一个timer
var timer = null;
return function(){
//如果函数执行了,那么清除定时器
clearTimeout(timer)
//应用定时器计时,超过延时时间,执行函数
timer = setTimeout(() =>{
//执行函数,同时改变this指向
fn.call(this);
},delay)
}
}
//举例中的应用:点击按钮输出12345。
obtn.onclick = debounce(function(){
console.log('12345');
},1000)
深拷贝
- 浅拷贝:复制对象的第一层,指向被复制的内存地址,如果原地址发生改变,那么浅拷贝出来的对象也会相应的改变。
- 深拷贝:在计算机中开辟一块新的内存地址用于存放复制的对象。复制后的两个对象对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。
针对像Object, Array 这样的复杂对象的。简单来说,浅复制只复制一层对象的属性,而深复制则递归复制了所有层级。
简单写法:
let newObj1 = JSON.parse(JSON.stringfy(obj));
该方法有一些局限性:
- 会忽略 undefined
- 会忽略 symbol
- 不能序列化函数
- 不能解决循环引用的对象
递归写法(简版):
const deepClone = function (obj) {
// 判断是否为对象,不是的话返回
if (typeof obj !== 'object') return;
// 判断是数组还是对象
let newObj = obj instanceof Array ? [] : {};
//循环这个对象
for (var key in obj) {
//判断对象自身属性是否有这个key
if (obj.hasOwnProperty(key)) {
//如果obj[key]为对象的话,递归。否则直接赋值即可。
newObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
}
}
return newObj;
}
call
call
用来改变this的指向。使用:fn.call(obj, arg1, arg2, ...)
Function.prototype.myCall = function (context) {
// 判断如果不是函数的话,报出错误
if (typeof this !== 'function') {
throw new TypeError('Error');
}
// context为null或undefined的话会被全局对象代替
context = context || window;
// 获取调用call的函数(就是this)把它作为context的一个属性
context.fn = this;
// 获取剩余参数
const args = [...arguments].slice(1);
// 执行
const result = context.fn(...args);
// 执行后删除即可
delete context.fn;
// 返回结果
return result;
}
apply
apply
用来改变this的指向,和call的区别是传递的参数格式不同,apply
接受数组作为参数。
使用:fn.apply(obj, [arg1, arg2, ...])
Function.prototype.myApply = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error');
}
context = context || window;
context.fn = this;
let result;
// 参数处理和 call 有区别,其余基本一致
if (arguments[1]) {
result = context.fn(...arguments[1]);
} else {
result = context.fn();
}
delete context.fn;
return result;
}
注:上面实现的call和apply,当我们被问到的时候,通常可以直接这么写。假如写完之后,面试官问你,如果context里面原来就有fn属性,你该怎么办? 由于我们最后一步delete context.fn;
将我们建的fn删除了,如果它原来存在的话,那么也就被我们删除啦。我们可以考虑复制一个新的context2,而不是直接改context,或者先保留旧的context.fn的值,执行完逻辑之后再将它还原,这样的话就可以解决这个问题了。(有把握的话也可以直接说哦~)
bind
bind
用来改变this指向,和call、apply的区别是bind
是绑定,执行需要再次调用。
使用:fn.bind(obj, arg1, arg2, ...)()
bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN )
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error');
}
const _this = this;
// 获取当前的参数
const args = [...arguments].slice(1);
// 返回一个函数
return function F() {
// 因为返回了一个函数,我们可以 new F(),所以需要判断
if (this instanceof F) {
return new _this(...args, ...arguments);
}
// 利用apply改变this指向,同时拼接返回的这个函数中的参数
return _this.apply(context, args.concat(...arguments));
}
}
Promise.all
Promise.all
方法接受一个数组,当数组中所有的promise请求成功之后,会走到.then的方法里面。如果中间哪一个promise失败了,那么promise.all
就会直接走到.catch的方法里面。
使用:Promise.all([p1,p2,p3,···]).then(function(){}).catch(function(){})
Promise.prototype.all = function (promiseAry = []) {
// 定义一个index,用于计数。
let index = 0;
// 定义空数组,用于保存结果
let result = [];
// 返回新的Promise
return new Promise((resolve, reject) => {
// 循环传入的promise数组
for (let i = 0; i < promiseAry.length; i++) {
// 执行成功走到then
promiseAry[i].then(val => {
// 成功,index+1
index++;
// 存入对应结果
result[i] = val;
// 当所有函数都正确执行了,resolve输出所有返回结果
if (index === promiseAry.length) {
resolve(result)
}
}, reject)
}
})
}
Promise.race
Promise.race
方法接受一个数组,只要请求最快的promise请求成功,那么就会直接走到then的方法里面,即使后面有请求失败的promise不用管。同理,只要请求最快的promise请求失败。那么promise.race就直接走到catch啦。
使用:Promise.all([p1,p2,p3,···]).then(function(){}).catch(function(){})
Promise.prototype.race = function (promiseAry) {
// 返回一个promise
return new Promise((resolve, reject) => {
// 如果数组长度为0,直接返回
if (promiseAry.length === 0) {
return;
} else {
// 循环数组
for (let i = 0; i < promiseAry.length; i++) {
// 最快的那个成功的话走到then,失败走到后面
promiseAry[i].then(val => {
// resolve输出对应结果
resolve(result);
return;
}, reject)
}
}
})
}
Promise.finally
Promise.finally
不管 Promise 对象最后状态如何,都会执行finally方法指定的回调函数。
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
// onFulfilled
value => P.resolve(callback()).then(() => value),
// onRejected
reason => P.resolve(callback()).then(() => {
throw reason
})
);
};
函数柯里化
柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。(来自于百度百科)
简单点来说就是把一个多参数的函数(fn)作为参数,传递到柯里化的这个函数中(curry),运行后能够返回一个新的函数。这个新的函数能够继续处理这个函数(fn)的剩余参数。
function curry(fn, args) {
// 获取函数需要的参数长度
let length = fn.length;
args = args || [];
return function () {
let subArgs = args.slice(0);
// 拼接得到现有的所有参数
subArgs = subArgs.concat(Array.prototype.slice.call(arguments))
// 判断参数的长度是否已经满足函数所需参数的长度
if (subArgs.length >= length) {
// 如果满足,执行函数
return fn.apply(this, subArgs);
} else {
// 如果不满足,递归返回科里化的函数,等待参数的传入
return curry.call(this, fn, subArgs);
}
};
}
//应用:
function add(a, b, c) {
return a + b + c;
}
var _add = curry(add)
console.log(_add(1, 2, 3)) //6
console.log(_add(1)(2)(3)) //6
console.log(_add(1, 2)(3)) //6
深度优先遍历
深度优先遍历算法(DFS)
,是从根节点开始,沿着树的深度遍历树的节点。简单来说,首先以一个未被访问过的顶点作为起始顶点,沿当前顶点的边开始走未访问过的顶点。当没有未访问过的顶点时,则回到上一个顶点,继续重复以上过程,直至所有的顶点都被访问,算法中止。
递归写法
let deepTraversal = (node) => {
// 定义空数组,用于存储节点
let nodes = [];
// 当节点不为空时
if (node !== null) {
// 将当前节点push进数组中
nodes.push(node);
// 取出当前节点的孩子节点
let children = node.children;
// 循环所有的孩子节点
if (children) {
for (let i = 0; i < children.length; i++) {
// 递归调用并将结果进行拼接
nodes = nodes.concat(deepTraversal(children[i]));
}
}
}
// 返回结果
return nodes
}
非递归写法
let deepTraversal = function (node) {
// 定义保存结果数组nodes,以及辅助数组stack(栈)
let stack = [];
let nodes = [];
if (node) {
// 推入当前处理的node
stack.push(node);
while (stack.length) {
// 将最后一个弹出
let item = stack.pop();
// 取出他的孩子节点
let children = item.children;
// 将这个节点push进结果数组
nodes.push(item);
// 将孩子节点倒过来push进辅助栈中。例如当前节点有两个孩子,children1和children2
// 那么stack里面为[children2,children1],这样pop()的时候children1会先弹出,
// 进而children1会先被push进nodes,先遍历children1的孩子节点(以此类推)
if (children) {
for (let i = children.length - 1; i >= 0; i--) {
stack.push(children[i]);
}
}
}
}
// 返回结果数组
return nodes;
}
广度优先遍历
广度优先遍历算法(BFS)
,是从根节点开始,沿着树的宽度遍历树的节点(一层一层的遍历)。如果所有节点均被访问,则算法中止。
let widthTraversal = (node) => {
// 定义保存结果数组nodes,以及辅助数组queue(队列)
let nodes = [];
let queue = [];
if (node) {
// 将节点push进队列中
queue.push(node);
// 当队列长度不为0时循环
while (queue.length) {
// 将值从头部弹出
let item = queue.shift();
// 取出当前节点的孩子节点
let children = item.children;
// 将当前节点push进结果数组
nodes.push(item);
// 将孩子节点顺次push进辅助队列中。例如当前节点有两个孩子,children1和children2
// 那么queue里面为[children1,children2],这样shift()的时候children1会先弹出,
// 进而children1会先被push进nodes,children1的孩子节点会顺次push进queue中 [child2,child1-1](以此类推)
if (children) {
for (let i = 0; i < children.length; i++) {
queue.push(children[i]);
}
}
}
}
return nodes;
}
发布订阅模式
发布订阅模式
是比较常问的设计模式之一,实现一般分为两步,一是注册也就是添加订阅(添加监听事件及对应的方法),二是进行激活也就是发布消息(根据传入的事件类型触发相应的方法)。
let event = {
// 存放订阅事件
childrenList: {},
//订阅函数
listen(key, fn) {
//如果chilidrenlist里这个缓存不存在,就先将它创建为空,为后续做准备
if (!this.childrenList[key]) {
this.childrenList[key] = [];
}
// 判断传进来的是否是一个函数,若是就加到childrenList[key]下的数组中等待执行
if (typeof fn == 'function') {
this.childrenList[key].push(fn);
}
},
// 发布函数
touch(key) {
// 取出对应key中的函数
let fns = this.childrenList[key];
// 判断如果不存在直接返回
if (!fns && fns.length === 0) {
return false;
}
// 循环出每一个函数,进行执行
fns.forEach(fn => {
fn.apply(this, [arguments]);
});
},
// 删除订阅函数
remove(key, fn) {
// 取出该类型对应的消息集合
var fns = this.childrenList[key];
if (!fns) {
return false;
}
// 如果没有传入具体的fn,就表示需要取消所有订阅
if (!fn) {
fns && (fns.length = 0);
} else {
// 将函数循环取出
for (var i = 0; i < fns.length; i++) {
if (fn === fns[i]) {
// 删除订阅者的这个回掉
fns.splice(i, 1);
}
}
}
}
}
// 应用:
event.listen('zs', arguments => {
console.log(`${arguments[0]},${arguments[1]}`)
})
event.listen('lisi', arguments => {
console.log(`${arguments[0]},${arguments[1]}`)
})
event.touch('zs', '收到啦面试邀请');
event.touch('lisi', '接到啦offer');
实现数组的扁平化
数组的扁平化主要是将多维数组转化为一维数组。平常的写法可以直接调用数组的Api:var newArr = arr.flat(Infinity);
。下面用原生简单实现:
const getFlat = function (arr) {
// 循环数组,当发现里面元素包含数组时
while (arr.some(item => Array.isArray(item))) {
// 用扩展运算符取出元素,concat进行拼接
arr = [].concat(...arr);
}
return arr;
}
//应用
let arr = [1, 2, [3, 4, 5, [6, 7], 8], [9, [10]]];
console.log(getFlat(arr)) //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
二分查找
二分法查找
,也称折半查找,是一种在有序数组中查找特定元素的搜索算法。
实现思路:先取出中间值,如果和目标值相等则直接返回索引。否则比较目标值和中间值的大小,进而在数组大于或小于中间值的那一半区域查找,重复上述操作。
非递归写法
function search(arr, key) {
// 初始化low和height
var low = 0;
var height = arr.length - 1;
var mid;
while (low <= height) {
// (小索引+大索引)除以2,向下取整找到中间值
mid = Math.floor((low + height) / 2);
// 如果当前索引为中间值的数值刚好和这个目标值想等
if (arr[mid] == key) {
// 返回目标元素的索引值
return mid;
} else if (arr[mid] < key) {//如果当前索引为中间值的数值小于这个目标值
// 将中间值+1赋值给low
low = mid + 1;
} else {// 如果当前索引为中间值的数值大于这个目标值
// 将中间值-1赋值给height
height = mid - 1;
}
}
// 没有查到,返回-1
return -1;
}
递归写法
function search(arr, low, height, key) {
// 递归出口(没找到返回-1)
if (low > height) {
return -1;
}
// (小索引+大索引)除以2,向下取整找到中间值
var mid = Math.floor((low + height) / 2);
// 如果当前索引为中间值的数值刚好和这个目标值想等
if (arr[mid] == key) {
// 返回目标元素的索引值
return mid;
} else if (arr[mid] < key) { // 如果当前索引为中间值的数值小于这个目标值
// 将中间值+1赋值给low
low = mid + 1;
// 递归调用
return search(arr, low, height, key);
} else { // 如果当前索引为中间值的数值大于这个目标值
// 将中间值-1赋值给height
height = mid - 1;
// 递归调用
return search(arr, low, height, key);
}
}
总结
上边写啦一些常见的基础代码,写给即将站在面试场上的你,希望能够对同在前端路上不断前行的你有所帮助!
如有问题,还望辛苦指正,谢谢~