20道手撕JS面试题—上篇
1.防抖
😍定义: 事件触发n秒后,如果n秒内不再次触发,执行事件,否则重新计时
💥应用场景:
1.窗口大小变化,调整样式
window.addEventListener(‘resize’, debounce(handleResize, 200));
2. 搜索框加入防抖函数,输入后1000毫秒搜索 。debounce(fetchSelectData, 300);
3.表单验证,输入1000毫秒后验证。debounce(validator, 1000);
🪶JS代码
// 非立即执行
function debounce(event, time) {
let timer = null;
// 返回一个闭包函数,用闭包保存timer确保其不会销毁,重复点击会清理上一次的定时器
return function (...args) {
//n秒内再次调用,重新计时
clearTimeout(timer);
timer = setTimeout(() => {
//this指向闭包调用者,而不是指向window
event.apply(this, args);
}, time);
};
}
// 立即执行,使用flag标签来表示是否是立即执行
function debounce1(event, time, flag) {
let timer = null;
return function (...args) {
//n秒内再次调用,重新计时
clearTimeout(timer);
//立即执行
if (!timer && flag) event.apply(this, args);
timer = setTimeout(() => {
//this指向闭包调用者,而不是指向window
//目的:解决参数个数不确定和将参数传回防抖函数
event.apply(this, args);
}, time);
};
2.节流
😍定义:间隔一段时间执行一次
💥应用:
1.滚动鼠标滚轮监听滚动条位置
2. 防止按钮多次点击
3.输入框事件
🪶JS代码:
// 时间戳实现:第一次事件肯定触发,最后一次若不瞒住要求,不会触发
//首次Date.now()-pre一定大于time
function throttle(event, time) {
let pre = 0;
return function (...arg) {
if (Date.now() - pre > time) {
pre = Date.now();
event.apply(this, args);
}
};
}
//定时器实现:第一次事件不会立即执行,time秒后执行,最后一次触发会再执行一次
function throttle1(event, time) {
let timer = null;
return function (...arg) {
//定时器为空
if (!timer) {
//开启定时器
timer = setTimeout(() => {
event.apply(this.args);
//函数执行后,重置定时器
timer = null;
}, time);
}
};
}
// 时间戳&定时器:第一次会马上执行,最后一次也会执行
function throttle2(event, time) {
let pre = 0;
let timer = null;
return function (...args) {
//时间间隔瞒足time,立即执行
if (Date.now() - pre > time) {
clearTimeout(timer);
timer = null;
pre = Date.now();
event.apply(this, args);
}
//否则,使用定时器来执行最后一次event
else if (!timer) {
timer = setTimeout(() => {
event.apply(this, args);
timer = null;
}, time);
}
};
}
3.函数柯里化
😍定义:把接受多个参数的函数变换成一个接受单个参数的函数,并且返回结果
🪶JS代码1
function curryingadd() {
let args = Array.from(arguments);
let innerAdd = function () {
let argsinner = Array.from(arguments); // 如果此时内部没有参数了,也就是 f(...arg)() 就直接返回值即可
if (argsinner.length == 0) {
return args.reduce((prev, cur) => {
return prev + cur;
});
} else {
// 这里使用了闭包 因为args是外部定义的 所以递归时可以累计存储变量
args.push(...arguments);
return innerAdd;
}
};
//当函数调用是curryingadd(2)(1, 3, 4)而不是curryingadd(2)(1, 3, 4)(),会通过innerAdd.toString 输出我们需要的结果
innerAdd.toString = function () {
return args.reduce((prev, curr) => {
return prev + curr;
}, 0);
};
return innerAdd;
}
😊适用场景1:
let res = curryingadd(2)(1, 3, 4)(2, 3)(3)(4, 6)(7, 98);
let res1 = curryingadd(2)(1, 3, 4)(2, 3)(3)(4, 6)(7, 98)();
console.log(parseInt(res)); // 133
console.log(parseInt(res1)); // 133
🪶JS代码2
//一行代码搞定
const curry = (fn, ...args) =>args.length >= fn.length?fn(...args): (...arg1) => curry(fn, ...args, ...arg1);
//解释原理:
//判断当前传入函数的参数个数 (args.length) 是否大于等于原函数所需参数个数 (fn.length) ,
//如果是,则执行当前函数;如果是小于,则返回一个函数
const curry = (fn, ...args) =>
// 函数的参数个数可以直接通过函数数的.length属性来访问
args.length >= fn.length // 这个判断很关键!!!
? // 传入的参数大于等于原始函数fn的参数个数,则直接执行该函数
fn(...args)
: /**
* 传入的参数小于原始函数fn的参数个数时
* 则继续对当前函数进行柯里化,返回一个接受所有参数(当前参数和剩余参数) 的函数
*/
(..._args) => curry(fn, ...args, ..._args);
😊适用场景2:
function add1(x, y, z) {
return x + y + z;
}
console.log(curry(add1)(1, 2)(3));//6
4.浅拷贝
😊定义:创建新对象,如果属性是基本类型,拷贝的是基本类型的值、拷贝前后互不影响。 如果属性是引用类型,拷贝的是内存地址。拷贝前后相互影响
🪶JS代码
//for循环遍历
let obj = {
name: "codershanshan",
hobby: {
outdoor: "basketball",
indoor: "watch mv",
},
};
function shallowclone(obj) {
let newobj = {};
for (let i in obj) {
newobj[i] = obj[i];
}
return newobj;
}
除了for循环遍历,还有其他实现浅拷贝的方法,只是需要调用库或者已经封装好的方法。
// 方法1:Object.assign()
var obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
var obj2 = Object.assign({}, obj1);
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }
console.log(obj2)
//方法2:函数库lodash的_.clone方法
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.clone(obj1);
console.log(obj1.b.f === obj2.b.f);// true
// 方法3:展开运算符...
let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}
obj1.address.x = 200;
obj1.name = 'wade'
console.log('obj2',obj2) // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }
// 方法4:Array.prototype.concat()
arr = [1, 3, {
username: 'kobe'
}];
let arr2 = arr.concat();
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]
5.深拷贝
😊定义:重新开辟内存给对象,无论属性什么类型,拷贝前后互不影响
🪶JS代码
简单版
function deepclone(target) {
//若是引用类型,创建对象,遍历deepclone方法
if (typeof target == "object") {
let cloneTarget = {};
for (const key in target) {
cloneTarget[key] = deepclone(target[key]);
}
return cloneTarget;
}
//若是基本类型,直接返回
else {
return target;
}
}
升级版:解决循环遍历问题
function clone(target, map = new WeakMap()) {
//WeakMap弱引用,键必须是对象,值任意。方便垃圾回收
if (obj === null || obj == undefined) return obj; // 如果是null或者undefined我就不进行拷贝操作
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (typeof target === "object") {
//解决数组类型
let cloneTarget = Array.isArray(target) ? [] : {};
//解决循环遍历,map存储前对象和拷贝对象的对应关系
// map中有克隆对象,直接返回
if (map.get(target)) {
return map.get(target);
}
// map中无克隆对象,key-value存储
map.set(target, cloneTarget);
for (const key in target) {
cloneTarget[key] = clone(target[key], map);
}
return cloneTarget;
} else {
return target;
}
}
6.扁平化
😊定义:多维数组转化为一维数组
🪶JS代码
// 方法一:递归实现
const a = [1, [2, [3, [4, 5]]]];
const flatten = (arr) => {
let result = [];
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
//concat方法进行拼接
result = result.concat(flatten(arr[i]));
// console.log(result)
} else {
result.push(arr[i]);
}
}
return result;
};
// console.log(flatten(a))
// 方法二:reduce函数
const flatten1 = (arr) => {
return arr.reduce((prev, next) => {
return prev.concat(Array.isArray(next) ? flatten(next) : next);
}, []);
};
// console.log(flatten1(a))
// 方法三:扩展运算符
const flatten2 = (arr) => {
//some方法把数组中仍然是组数的项过滤出来
while (arr.some((item) => Array.isArray(item))) {
arr = [].concat(...arr);
// console.log(arr);
}
return arr;
};
// console.log(flatten2(a));
// 方法四:split&toString
const flatten3 = (arr) => {
return arr.toString().split(",");
};
// console.log(flatten3(a));
👍传入参数,决定扁平阶数方法
Array.prototype._flat = function (n) {
let result = [];
let num = n;
for (let item of this) {
// 如果是数组
if (Array.isArray(item)) {
n--;
// 没有扁平化的空间 直接推入
if (n < 0) {
result.push(item);
}
// 继续扁平化 并将n传入 决定item这一个数组中的扁平化
else {
result.push(...item._flat(n));
}
}
// 不是数组直接推入
else {
result.push(item);
}
// 每次循环 重置n 为传入的参数 因为每一项都需要扁平化 需要进行判断
n = num;
}
return result;
};
let arr = [1, 2, [3, 4], [5, 6, [7, 8]]];
let res = arr._flat(1);
// console.log(res); // [ 1, 2, 3, 4, 5, 6, [ 7, 8 ] ]
7.排序
✌️冒泡排序
思想:遍历len-1次,每次找到最大数放到末尾。比较是按照相邻数组比较
时间:n^2 空间:1
🪶JS代码
function bubblesort(arr) {
for (let i = 0; i < arr.length - 1; i++) {
for (let j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
let temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return arr;
}
console.log(bubblesort([10, 2, 4, 5, 60, 2])); //[ 2, 2, 4, 5, 10, 60 ]
✌️选择排序
思想:遍历len-1次,每次找到最小数放到开头。比较是按照相邻数组比较
时间:n^2 空间:1
🪶JS代码
function selectsort(arr) {
let min, temp;
for (let i = 0; i < arr.length - 1; i++) {
min = i;
for (let j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min]) {
min = j;
}
}
temp = arr[i];
arr[i] = arr[min];
arr[min] = temp;
}
return arr;
}
console.log(selectsort([3, 2, 4, 5, 8, 0, 1]));
✌️插入排序
思想:假设前n个数已经排序,取出下一个元素,在以排序的元素中找到合适位置插入。
时间:n^2 空间:1
🪶JS代码
function insertsort(arr) {
let len = arr.length;
for (let i = 0; i < len; i++) {
let key = arr[i];
let j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
return arr;
}
console.log(insertsort([3, 2, 4, 5, 8, 0]));
✌️快速排序
思想:找到一个基准数,将小于基准数放到left数组,大于基准数放到right数组。然后concat拼接。left,right数组进入quicksort递归遍历,直到找到出口。
时间:nlogn 空间:nlogn
🪶JS代码
function quicksort(arr) {
if (arr.length < 2) return arr; //出口
let left = [];
let right = [];
let mid = arr.splice(Math.floor(arr.length / 2), 1);
for (let i = 0; i < arr.length; i++) {
if (arr[i] < mid) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return quicksort(left).concat(mid, quicksort(right));
}
console.log(quicksort([3, 2, 4, 5, 15, 0]));
8.call
😊定义:改变this指向,后面参数分开传递
🪶JS代码:
Function.prototype.myCall = function (context = window, ...args) {
//context 为可选参数,如果不传的话默认上下文为 window
context = context || window;
// 为context 创建一个 Symbol(保证不会重名)属性,避免与原函数的其他属性名冲突
const fn = Symbol();
// 改变当前函数的this指向
context[fn] = this;
//接收其他参数
const result = context[fn](...args);
//调用函数后即删除该Symbol属性
delete context[fn];
return result;
};
var people = {
sex: "man",
age: 27,
};
function sayPeople(a, b) {
console.log(this.sex, this.age, a, b);
}
sayPeople.call();
sayPeople.call(people);
sayPeople.call(people, 1, 2);
9.apply
😊定义:改变this指向,参数放在一个数组中传递
🪶JS代码:
Function.prototype.myApply = function (context = window, args) {
context = context || window;
const fn = Symbol();
context[fn] = this;
let result;
//如果其他参数是数组,接收参数
if (Array.isArray(args)) {
result = context[fn](...args);
}
//如果其他参数是不是数组,不接收,运行时报错
else {
result = context[fn]();
}
delete context[fn];
return result;
};
var people = {
sex: "man",
age: 27,
};
function sayPeople(a, b) {
console.log(this.sex, this.age, a, b);
}
sayPeople.apply();
sayPeople.apply(people);
sayPeople.apply(people, [1, 2]);
10.bind
😊定义:改变this指向,参数放在一个数组中传递,不能直接调用,bind返回的是一个函数.
🪶JS代码:
Function.prototype.myBind = function (context, ...args1) {
//因为函数里面返回函数, 防止this丢失,提前保存this
const _this = this;
return function F(...args2) {
// 判断是否用于构造函数
if (this instanceof F) {
console.log(1);
// 是new出来的话 就让当前的这个实例作为this
return _this.apply(this, args1.concat(args2));
}
//如果不是,使用apply,将context和处理好的参数传入
return _this.apply(context, args1.concat(args2));
};
};
var people = {
sex: "man",
age: 27,
};
function sayPeople(a, b) {
console.log(this.sex, this.age, a, b);
}
sayPeople.bind()();
sayPeople.bind(people)();
sayPeople.bind(people, [1, 2])();
var s = sayPeople.bind(people, [1, 2]);
s(3);
最近想把面试笔记分享出来,如果对你有帮助,点赞加关注呀!👍有空会全部整理分享出来