【前端学习笔记-02】—JavaScript面试常见手撕-01

111 阅读7分钟

1. 手写防抖函数

函数防抖是抖是指在事件被触发 n 秒后再执⾏回调,如果在这 n 秒内事件⼜被触发,则重新计时。这可以使⽤在⼀些点击请求的事件上,避免因为⽤户的多次点击向后端发送多次请求。

场景:

  • 搜索框搜索输入,只需用户最后一次输入完,在发送请求。
  • 手机号、邮箱验证输入检测。
  • 窗口大小的resize,只需要窗口调整完整后再计算窗口大小。
function debounce(func, delay) {
    let timer = null
    return function() {
        const context = this
        const args = [...arguments]
        // 取消之前的定时器,重新计时
        clearTimeout(timer)
        // 设置定时器,使时间间隔指定事件后执行
        timer = setTimeout(function() {
          func.apply(context,args)
        },delay) 
    }
}

加入immediate,表示立即执行。

function debounce(func, delay) {
  let timer = null
  return function() {
    let context = this
    let args = [...arguments]
    if (timer) {
      clearTimeout(timer)
    }
    // 如果立即执行
    if (immediate) {
      // 第一次会立即执行 以后只有事件执行后才会再次触发
      let callNow = !timer
      timer = setTimeout(function() {
        timer = null
      },delay)
      if (callNow) {
        func.apply(context,args)
      } 
    }
    timer = setTimeout(function() {
      func.apply(context,args)
    }
    ,delay)
  }

2. 手写节流函数

n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时。

场景:

  • 滚动加载,加载更多或滚到底部监听
  • 搜索框,搜索联想功能
function throttle(fn, delay) {
    let timer = null;
    return function () {
        if (!timer) {
            timer = setTimeout(() => {
                fn.apply(this, arguments);
                timer = null;
            }, delay);
        }
    }
}

使用定时器写法,delay毫秒后第一次执行,第二次事件停止触发后依然会再一次执行, 可以将时间戳写法的特性与定时器写法的特性相结合,实现一个更加精确的节流。实现如下

function throttle(fn, delay) {
  let timer = null
  let startTime = Date.now()
  return function () {
    let currentTime = Date.now()
    let remaining = delay - (currentTime - startTime)
    if (remaining <= 0) {
      fn.apply(this, arguments)
      startTime = Date.now()
    } else {
      if (!timer) {
        timer = setTimeout(() => {
          fn.apply(this, arguments);
          timer = null;
        }, remaining);
      }
    }
  }
}

3. 手写bind

/**
 * call 、apply 、bind 作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this指向
 * bind()方法返回的是一个函数
 *
 * 实现bind 的步骤 可以分解成三个部分
 * 1.修改this指向
 * 2.动态传递参数
 * // 方式一:只在bind中传递函数参数
 * fn.bind(obj,1,2)()
 * // 方式二:在bind中传递函数参数,也在返回函数中传递参数
 * fn.bind(obj,1)(2)
 */
Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  // this指向当前对象
  var _this = this
  // 这里slice(1)是获取参数 因为第一个参数往往是obj,所以不考虑
  var args = [...arguments].slice(1)
  // 返回⼀个函数
  return function F() {
    // 因为返回了⼀个函数,我们可以 new F(),所以需要判断
    if (this instanceof F) {
      // 根据调用方式,传入不同绑定值
      return new _this(...args, ...arguments)
    }
    return _this.apply(context, args.concat(...arguments))
  }
}

4. 手写call

/**
 * 1.call()方法是函数原型对象的方法,调用者为函数对象
 * 可以将一个对象指定为第一个参数,此时这个对象就会成为函数执行时候的this,从第二个函数依次传入实参
 * call(obj, a, b);第一个参数表示函数中的this,a,b表示实参
 *
 *2. apply()可以将一个对象指定为第一个参数,此时这个对象将会成为函数执行时的this,
 * 实参需要封装到一个数组中,统一传递
 * apply(obj, [a, b]);第一个参数表示函数中的this,[a,b]数组表示实参数组
 *
 * 3.bind()一个参数表示函数中的this,a,b表示实参,需要单独用括号
 * bind(obj)(a,b)
 */

// 注意只有在函数执行时候才会存在this
//1.实现call()

function add(a, b) {
  // console.log(this)
  return a + b + this.c;
}
var c = 20;
const obj = {
  c: 20,
};
//改变this指向的函数,对象,剩余参数
function call(fn, obj, ...args) {
  //给obj临时绑定方法
  obj.temp = fn;
  //执行方法 这里就是解构参数 称为扩展运算符
  const result = obj.temp(...args);
  //删除方法
  delete obj.temp;
  return result;
}
console.log(call(add, obj, 20, 20));

//伪代码
Function.prototype.myCall = function (context, ...arr) {
  if (context === null || context === undefined) {
    // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
    context = window;
  } else {
    context = Object(context); // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
  }
  const specialPrototype = Symbol('特殊属性Symbol'); // 用于临时储存函数
  context[specialPrototype] = this; // 函数的this指向隐式绑定到context上
  let result = context[specialPrototype](...arr); // 通过隐式绑定执行函数并传递参数
  delete context[specialPrototype]; // 删除上下文对象的属性
  return result; // 返回函数执行结果
};

5. 手写apply

Function.prototype.apply = function(context = window, args) {
  if (typeof this !== 'function') {
    throw new TypeError('Type Error');
  }
  const fn = Symbol('fn');
  context[fn] = this;
  const res = context[fn](...args);
  delete context[fn];
  return res;
}

6. 手写instanceof

instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。

function instanceOf(left, right) {
  // 获取对象的原型
  // let proto = Object.getPrototypeOf(left)
  let leftValue = left.__proto__;
  let rightValue = right.prototype;
  while (true) {
    if (leftValue === null) return false;
    if (leftValue === rightValue) return true;
    leftValue = leftValue.__proto__;
  }
}
const arr = new Array();
console.log(instanceOf(arr, Array)); //true

7. 手写new

JavaScript中,new操作符用于创建一个给定构造函数的实例对象,主要过程:

  • 创建一个新的对象obj
  • 将对象与构建函数通过原型链连接起来
  • 将构建函数中的this绑定到新建的对象obj
  • 根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理
function mynew(Func, ...args) {
  // 1.创建一个新对象
  const obj = {};
  // 2.新对象原型指向构造函数原型对象
  obj.__proto__ = Func.prototype;
  // 3.将构建函数的this指向新对象
  let result = Func.apply(obj, args);
  // 4.根据返回值判断
  return result instanceof Object ? result : obj;
}

8. 函数柯理化

柯里化是什么:是指这样一个函数,它接收函数 A,并且能返回一个新的函数,这个新的函数能够处理函数 A 的剩余参数。

/**
 * 指的是将一个接受多个参数的函数 变为接收一个参数返回一个函数的固定形式
 *
 add(1,2,3)
 add(1)(2)(3)
 add(1)(2,3)(4)
 */
// 1.方法一
function add() {
  //将传入的不定参数转换为数组对象
  //在ES6以前,我们将一个伪数组转换为真正数组通常情况下都是使用[ ].slice.call()方法
  // es6方法是Array.from
  // 将slice方法指向arguments  arguments调用slice方法 复制一份数组
  // var _args = Array.prototype.slice.call(arguments)
  var args = [...arguments]
  var adder = function () {
    args.push(...arguments)
    return adder //返回的是函数
  }

  adder.toString = function () {
    return args.reduce(function (a, b) {
      return a + b
    }, 0)
  }
  return adder
}

let a = add(1, 2, 3).toString()
let b = add(1)(2)(3).toString()
let c = add(1)(2, 3)(4).toString()
console.log(a) // 6
console.log(a === 6)  // true
console.log(c) // 10

9. 扁平化数组

/**
 * 数组扁平化 并去除其中重复的数据 最终得到一个升序且不重复的数组
 *
 */
var arr = [
    [1,2,2],
    [3,4,5,5],
    [6,7,8,9, [11,12,[12,12,[13]]]],
    10
]
Array.prototype.flat = function () {
    const result = this.map( function (item) {
        // 判断元素是否是数组
        if (Array.isArray(item)) {
            //是的话进行递归
            return item.flat()
        }
        // 可以用数组包裹一下
        return[item]
    })
    // 将每一轮的递归调用的结果进行连接
    return [].concat(...result)
}
// console.log(arr.flat())


// 方法二:使用while循环
Array.prototype.flat2 = function () {
    //结果的初始值就是我们数组本身
    let result = this
    //寻找数组中有数组元素的值
    while(result.some(function (item) {
        return Array.isArray(item)
    })) {
        //如果存在item本身是一个数组的话 就进行扁平化 ...result
        result = [].concat(...result)
    }
    return result
}

// 去重  加排序
console.log([...new Set(arr.flat2())].sort( (a,b) => {
    return a - b
}))

10. 数组转树

扁平数据结构
let arr = [
  {id: 1, name: '部门1', pid: 0},
  {id: 2, name: '部门2', pid: 1},
  {id: 3, name: '部门3', pid: 1},
  {id: 4, name: '部门4', pid: 3},
  {id: 5, name: '部门5', pid: 4},
]
要求输出结果
[
  {
    "id": 1,
    "name": "部门1",
    "pid": 0,
    "children": [
      {
        "id": 2,
        "name": "部门2",
        "pid": 1,
        "children": []
      },
      {
        "id": 3,
        "name": "部门3",
        "pid": 1,
        "children": [
          // 结果 ,,,
        ]
      }
    ]
  }
]
let arr = [
  {id: 1, name: '部门1', pid: 0},
  {id: 2, name: '部门2', pid: 1},
  {id: 3, name: '部门3', pid: 1},
  {id: 4, name: '部门4', pid: 3},
  {id: 5, name: '部门5', pid: 4},
]
// function arraytoTree(items) {
//   const result = [];   // 存放结果集
//   const itemMap = {};  //
//   for (const item of items) {
//     const id = item.id;
//     const pid = item.pid;
//     if (!itemMap[id]) {   //{id: 1, name: '部门1', pid: 0, children: []}
//       itemMap[id] = {
//         children: [],
//       }
//     }
//     itemMap[id] = {
//       ...item,
//       children: itemMap[id]['children']
//     }
//     const treeItem =  itemMap[id];
//     if (pid === 0) {
//       result.push(treeItem);
//     } else {
//       if (!itemMap[pid]) {
//         itemMap[pid] = {
//           children: [],
//         }
//       }
//       itemMap[pid].children.push(treeItem)
//     }
//   }
//   return result;
// }
//
let res = arraytoTree(arr, 0)
console.log(res)

/**
 * 方法二
 */
function arraytoTree(arr) {
  const newArr = []
  const map = {}
  arr.forEach((item) => {
    if(!item.children) item.children = []
    map[item.id] = item
  })
  arr.forEach((item) => {
    if(map[item.pid]) {
      map[item.pid].children.push(item)
    }else{
      newArr.push(item)
    }
  });
  return newArr
}

11. 数组去重

const arr = [1, 1, '1', 17, true, true, false, false, 'true', 'a', {}, {}]; // => [1, '1', 17, true, false, 'true', 'a', {}, {}]
  • 方法一:利用Set
1const res1 = Array.from(new Set(arr));
  • 方法二:两层for循环+splice
const unique1 = arr => {
  let len = arr.length;
  for (let i = 0; i < len; i++) {
    for (let j = i + 1; j < len; j++) {
      if (arr[i] === arr[j]) {
        arr.splice(j, 1);
        // 每删除一个树,j--保证j的值经过自加后不变。同时,len--,减少循环次数提升性能
        len--;
        j--;
      }
    }
  }
  return arr;
}
  • 方法三:利用indexOf
const unique2 = arr => {
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (res.indexOf(arr[i]) === -1) res.push(arr[i]);
  }
  return res;
}

当然也可以用include、filter,思路大同小异。

  • 方法四:利用include
const unique3 = arr => {
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (!res.includes(arr[i])) res.push(arr[i]);
  }
  return res;
}
  • 方法五:利用filter
const unique4 = arr => {
  return arr.filter((item, index) => {
    return arr.indexOf(item) === index;
  });
}
  • 方法六:利用Map
const unique5 = arr => {
  const map = new Map();
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (!map.has(arr[i])) {
      map.set(arr[i], true)
      res.push(arr[i]);
    }
  }
  return res;
}