20道手撕JS面试题--上篇

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);

最近想把面试笔记分享出来,如果对你有帮助,点赞加关注呀!👍有空会全部整理分享出来

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

万希&

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值