call apply bind
题目描述:手写 call apply bind 实现
实现代码如下:
Function.prototype.myCall = function (context, ...args) {
if (!context || context === null) {
context = window;
}
// 创造唯一的key值 作为我们构造的context内部方法名
let fn = Symbol();
context[fn] = this; //this指向调用call的函数
// 执行函数并返回结果 相当于把自身作为传入的context的方法进行调用了
return context[fn](...args);
};
// apply原理一致 只是第二个参数是传入的数组
Function.prototype.myApply = function (context, args) {
if (!context || context === null) {
context = window;
}
// 创造唯一的key值 作为我们构造的context内部方法名
let fn = Symbol();
context[fn] = this;
// 执行函数并返回结果
return context[fn](...args);
};
//bind实现要复杂一点 因为他考虑的情况比较多 还要涉及到参数合并(类似函数柯里化)
Function.prototype.myBind = function (context, ...args) {
if (!context || context === null) {
context = window;
}
// 创造唯一的key值 作为我们构造的context内部方法名
let fn = Symbol();
context[fn] = this;
let _this = this;
// bind情况要复杂一点
const result = function (...innerArgs) {
// 第一种情况 :若是将 bind 绑定之后的函数当作构造函数,通过 new 操作符使用,则不绑定传入的 this,而是将 this 指向实例化出来的对象
// 此时由于new操作符作用 this指向result实例对象 而result又继承自传入的_this 根据原型链知识可得出以下结论
// this.__proto__ === result.prototype //this instanceof result =>true
// this.__proto__.__proto__ === result.prototype.__proto__ === _this.prototype; //this instanceof _this =>true
if (this instanceof _this === true) {
// 此时this指向指向result的实例 这时候不需要改变this指向
this[fn] = _this;
this[fn](...[...args, ...innerArgs]); //这里使用es6的方法让bind支持参数合并
} else {
// 如果只是作为普通函数调用 那就很简单了 直接改变this指向为传入的context
context[fn](...[...args, ...innerArgs]);
}
};
// 如果绑定的是构造函数 那么需要继承构造函数原型属性和方法
// 实现继承的方式: 使用Object.create
result.prototype = Object.create(this.prototype);
return result;
};
//用法如下
// function Person(name, age) {
// console.log(name); //'我是参数传进来的name'
// console.log(age); //'我是参数传进来的age'
// console.log(this); //构造函数this指向实例对象
// }
// // 构造函数原型的方法
// Person.prototype.say = function() {
// console.log(123);
// }
// let obj = {
// objName: '我是obj传进来的name',
// objAge: '我是obj传进来的age'
// }
// // 普通函数
// function normalFun(name, age) {
// console.log(name); //'我是参数传进来的name'
// console.log(age); //'我是参数传进来的age'
// console.log(this); //普通函数this指向绑定bind的第一个参数 也就是例子中的obj
// console.log(this.objName); //'我是obj传进来的name'
// console.log(this.objAge); //'我是obj传进来的age'
// }
// 先测试作为构造函数调用
// let bindFun = Person.myBind(obj, '我是参数传进来的name')
// let a = new bindFun('我是参数传进来的age')
// a.say() //123
// 再测试作为普通函数调用
// let bindFun = normalFun.myBind(obj, '我是参数传进来的name')
// bindFun('我是参数传进来的age')
instanceof
题目描述:手写 instanceof 操作符实现
实现代码如下:
function myInstanceof(left, right) {
while (true) {
if (left === null) {
return false;
}
if (left.__proto__ === right.prototype) {
return true;
}
left = left.__proto__;
}
}
防抖
function debounce(func, wait, immediate) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
let callNow = !timeout;
timeout = setTimeout(function () {
timeout = null;
}, wait);
if (callNow) func.apply(context, args);
} else {
timeout = setTimeout(function () {
func.apply(context, args);
}, wait);
}
};
}
如何写出一个惊艳面试官的深拷贝?
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8],
f: { f: { f: { f: { f: { f: { f: { f: { f: { f: { f: { f: {} } } } } } } } } } } },
};
target.target = target;
console.time();
const result = clone1(target);
console.timeEnd();
console.time();
const result2 = clone2(target);
console.timeEnd();
function clone(target, map = new WeakMap()) {
if (typeof target === 'object') {
const isArray = Array.isArray(target);
let cloneTarget = isArray ? [] : {};
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
const keys = isArray ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone2(target[key], map);
});
return cloneTarget;
} else {
return target;
}
}
列表转成树
题目: 输入一组列表如下,转化成树形结构
[
{ id: 1, title: "child1", parentId: 0 },
{ id: 2, title: "child2", parentId: 0 },
{ id: 3, title: "child1_1", parentId: 1 },
{ id: 4, title: "child1_2", parentId: 1 },
{ id: 5, title: "child2_1", parentId: 2 }
]
function listToTree(data) {
// 使用对象重新存储数据, 空间换时间
let map = {};
// treeData存储最后结果
let treeData = [];
// 遍历原始数据data,存到map中,id为key,值为数据
for (let i = 0; i < data.length; i++) {
map[data[i].id] = data[i];
}
// 遍历对象
for (let i in map) {
// 根据 parentId 找到的是父节点
if (map[i].parentId) {
if (!map[map[i].parentId].children) {
map[map[i].parentId].children = [];
}
// 将子节点放到父节点的 children中
map[map[i].parentId].children.push(map[i]);
} else {
// parentId 找不到对应值,说明是根结点,直接插到根数组中
treeData.push(map[i]);
}
}
return treeData;
}
深度优先遍历
题目: 对树进行遍历,从第一个节点开始,遍历其子节点,直到它的所有子节点都被遍历完毕,然后再遍历它的兄弟节点
输入: 上文第11题生成的tree
输出: [1, 3, 4, 2, 5]
// 递归版本
function deepTree(tree, arr = []) {
if (!tree || !tree.length) return arr;
tree.forEach(data => {
arr.push(data.id);
// 遍历子树
data.children && deepTree(data.children, arr);
});
return arr;
}
// 非递归版本
function deepTree(tree) {
if (!tree || !tree.length) return;
let arr = [];
let stack = [];
//先将第一层节点放入栈
for (let i = 0, len = tree.length; i < len; i++) {
stack.push(tree[i]);
}
let node;
while (stack.length) {
// 获取当前第一个节点
node = stack.shift();
arr.push(node.id);
//如果该节点有子节点,继续添加进入栈顶
if (node.children && node.children.length) {
stack = node.children.concat(stack);
}
}
return arr;
}
广度优先遍历
题目: 以横向的维度对树进行遍历,从第一个节点开始,依次遍历其所有的兄弟节点,再遍历第一个节点的子节点,一层层向下遍历
输入: 上文第11题生成的tree
输出: [1, 2, 3, 4, 5]
function rangeTree(tree) {
if (!tree || !tree.length) return;
let arr = [];
let node, list = [...tree];
// 取出当前节点
while ((node = list.shift())) {
arr.push(node.id);
node.children && list.push(...node.children);
}
return arr;
}
查找二叉树的路径
题目: 查找二叉树和为某一值的路径
输入: 二叉树结构如下,找到和为 11 的所有路径 输出: [[5, 3, 2, 1], [5, 6]]
/**
* 利用回溯算法,找到和为某一值的路径
* @param {object} node - 二叉树
* @param {number} num - 目标值
* @param {array} stack - 栈
* @param {number} sum - 当前路径的和
* @param {array} result - 存储所有的结果
* */
function findPath(node, num, stack = [], sum = 0, result = []) {
stack.push(node.data);
sum += node.data;
// 找到所有的节点路径(包含叶子节点和子节点的所有情况之和)
if (sum === num) {
// if (!node.left && !node.right && sum === num) { // 找到所有的叶子节点路径
result.push(stack.slice());
}
if (node.left) {
findPath(node.left, num, stack, sum, result);
}
if (node.right) {
findPath(node.right, num, stack, sum, result);
}
// 回溯算法:不符合要求,退回来,换一条路再试
// 叶子节点直接pop;子节点中的所有的节点递归完成后再pop
stack.pop();
return result;
}
数组扁平化
题目描述:实现一个方法使多维数组变成一维数组
最常见的递归版本如下:
function flatter(arr) {
if (!arr.length) return;
return arr.reduce(
(pre, cur) =>
Array.isArray(cur) ? [...pre, ...flatter(cur)] : [...pre, cur],
[]
);
}
// console.log(flatter([1, 2, [1, [2, 3, [4, 5, [6]]]]]));
带并发的异步调度器 Scheduler
JS 实现一个带并发限制的异度调度器 Scheduler,保证同时运行的任务最多有两个。完善下面代码中的 Scheduler 类,使得以下程序能正确输出。
class Scheduler {
add(promiseMaker) {}
}
const timeout = (time) =>
new Promise((resolve) => {
setTimeout(resolve, time);
});
const scheduler = new Scheduler();
const addTask = (time, order) => {
scheduler.add(() => timeout(time).then(() => console.log(order)));
};
addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");
// output:2 3 1 4
// 一开始,1,2两个任务进入队列。
// 500ms 时,2完成,输出2,任务3入队。
// 800ms 时,3完成,输出3,任务4入队。
// 1000ms 时,1完成,输出1。
根据题目,我们只需要操作 Scheduler 类就行:
class Scheduler {
constructor() {
this.waitTasks = []; // 待执行的任务队列
this.excutingTasks = []; // 正在执行的任务队列
this.maxExcutingNum = 2; // 允许同时运行的任务数量
}
add(promiseMaker) {
if (this.excutingTasks.length < this.maxExcutingNum) {
this.run(promiseMaker);
} else {
this.waitTasks.push(promiseMaker);
}
}
run(promiseMaker) {
const len = this.excutingTasks.push(promiseMaker);
const index = len - 1;
promiseMaker().then(() => {
this.excutingTasks.splice(index, 1);
if (this.waitTasks.length > 0) {
this.run(this.waitTasks.shift());
}
});
}
}