金三银四:20道前端手写面试题

news2024/9/29 18:13:06

文章目录

    • 一、前言
    • 二、题目
      • 1. 防抖节流
        • 解读
      • 2.一个正则题
      • 3. 不使用a标签,如何实现a标签的功能
      • 4. 不使用循环API 来删除数组中指定位置的元素(如:删除第三位) 写越多越好
      • 5. 深拷贝
        • 解读
      • 6. 手写call bind apply
        • call 解读
        • apply 解读
      • 7. 手写实现继承
      • 8. 手写 new 操作符
      • 9. js执行机制 说出结果并说出why
      • 10. 如何拦截全局Promise reject,但并没有设定 reject处理器 时候的错误
      • 11. 手写实现sleep
      • 12. 实现add(1)(2) =3
      • 13. 两个数组中完全独立的数据
      • 14. 判断完全平方数
      • 15. 函数执行 说出结果并说出why
      • 16. 原型调用面试题 说出结果并说出 why
      • 17. 数组分组改成减法运算
      • 18. 手写数组的 flat
      • 19. 数组转为tree
      • 20. 合并数组并排序去重
      • 21、Function.prototype使用
    • 三、结语

一、前言

话说跳槽好时节,金三银四,金九银十。 我在上周也成功跳槽并入职了新公司,跳槽的路途坎坷,遇到的面试也有奇奇怪怪的。

在上一篇文章中,有答应大家整理出部分手写面试题给大家。这不, 现在就来了!

本文的所有题目,大部分是我自己去面试的时候遇到的。如有错误或者更好的答案,欢迎大家评论区留言~

二、题目

1. 防抖节流

这也是一个经典题目了,首先要知道什么是防抖,什么是节流。

  • 防抖: 在一段时间内,事件只会最后触发一次。

  • 节流: 事件,按照一段时间的间隔来进行触发。

    实在不懂的话,可以去这个大佬的Demo地址玩玩防抖节流DEMO

// 防抖
function debounce(fn) {
 let timeout = null; 
 return function () {
   // 如果事件再次触发就清除定时器,重新计时
   clearTimeout(timeout);
   timeout = setTimeout(() => {
     fn.apply(this, arguments);
   }, 500);
 };
}

// 节流
function throttle(fn) {
 let flag = null; // 通过闭包保存一个标记
 return function () {
   if (flag) return; // 当定时器没有执行的时候标记永远是null
   flag = setTimeout(() => {
     fn.apply(this, arguments);
      // 最后在setTimeout执行完毕后再把标记设置为null(关键)
      // 表示可以执行下一次循环了。
     flag = null;
   }, 500);
 };
}  

这道题主要还是考查对 防抖 节流 的理解吧,千万别记反了!

解读

这段代码包含了两个函数:debouncethrottle,它们都是用来控制另一个函数的执行频率的。

  1. debounce函数:
    debounce函数的作用是防止一个函数在短时间内被频繁调用。它通过设置一个定时器来实现,如果在设定的时间间隔内再次触发事件,则清除之前的定时器并重新设置一个新的定时器。只有在最后一次触发事件后的一段时间内没有新的触发时,才会执行原函数。
function debounce(fn) {
 let timeout = null; // 定义一个变量来保存定时器的引用
 return function () {
   // 如果事件再次触发就清除定时器,重新计时
   clearTimeout(timeout);
   timeout = setTimeout(() => {
     fn.apply(this, arguments); // 在延迟后执行原函数,并保持this指向和参数不变
   }, 500); // 延迟时间设置为500毫秒
 };
}
  1. throttle函数:
    throttle函数的作用是确保一个函数在单位时间内只被调用一次,即使在这段时间内多次触发事件。它通过设置一个标记来实现,如果标记为真(非null),则不执行函数;如果标记为假(null),则执行函数并设置标记为真,然后在一段时间后将标记重置为假。
function throttle(fn) {
 let flag = null; // 通过闭包保存一个标记
 return function () {
   if (flag) return; // 当定时器没有执行的时候标记永远是null,此时允许执行
   flag = setTimeout(() => {
     fn.apply(this, arguments); // 执行原函数,并保持this指向和参数不变
     flag = null; // 最后在setTimeout执行完毕后再把标记设置为null,表示可以执行下一次循环了
   }, 500); // 延迟时间设置为500毫秒
 };
}

这两个函数通常用于优化性能,例如在处理DOM事件监听器、窗口大小调整或用户输入等场景中,避免因为频繁的事件触发导致的性能问题。

2.一个正则题

要求写出 区号+8位数字,或者区号+特殊号码: 10010/110,中间用短横线隔开的正则验证。 区号就是三位数字开头。

例如 010-12345678

 let reg = /^\d{3}-(\d{8}|10010|110)/g

这个比较简单,熟悉正则的基本用法就可以做出来了。

3. 不使用a标签,如何实现a标签的功能

 // 通过 window.open 和 location.href 方法其实就可以实现。 
 // 分别对应了a标签的 blank 和 self 属性

4. 不使用循环API 来删除数组中指定位置的元素(如:删除第三位) 写越多越好

这个题的意思就是,不能循环的API(如 for filter之类的)。


var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

// 方法一 : splice 操作数组 会改变原数组 
arr.splice(2, 1)


// 方法二 : slice 截取选中元素 返回新数组 不改变原数组
arr.slice(0, 2).concat(arr.slice(3,))

// 方法三 delete数组中的元素 再把这个元素给剔除掉
delete arr[2]
arr.join(" ").replaceAll(/\s{1,2}/g," ").split(" ")

5. 深拷贝

深拷贝和浅拷贝的区别就在于

  • 浅拷贝: 对于复杂数据类型,浅拷贝只是把引用地址赋值给了新的对象,改变这个新对象的值,原对象的值也会一起改变
  • 深拷贝: 对于复杂数据类型,拷贝后地址引用都是新的,改变拷贝后新对象的值,不会影响原对象的值

所以关键点就在于对复杂数据类型的处理,这里我写了两种写法,第二中比第一种有部分性能提升

const isObj = (val) => typeof val === "object" && val !== null;

// 写法1
function deepClone(obj) {
    // 通过 instanceof 去判断你要拷贝的变量它是否是数组(如果不是数组则对象)。

    // 1. 准备你想返回的变量(新地址)。
    const newObj = obj instanceof Array ? [] : {}; // 核心代码。

    // 2. 做拷贝;简单数据类型只需要赋值,如果遇到复杂数据类型就再次进入进行深拷贝,直到所找到的数据为简单数据类型为止。
    for (const key in obj) {
        const item = obj[key];
        newObj[key] = isObj(item) ? deepClone(item) : item;
    }

    // 3. 返回拷贝的变量。
    return newObj;
}




// 写法2 利用es6新特性 WeakMap弱引用 性能更好 并且支持 Symbol
function deepClone2(obj, wMap = new WeakMap()) {
  if (isObj(obj)) {
    // 判断是对象还是数组
    let target = Array.isArray(obj) ? [] : {};

    // 如果存在这个就直接返回
    if (wMap.has(obj)) {
      return wMap.get(obj);
    }

    wMap.set(obj, target);

    // 遍历对象
    Reflect.ownKeys(obj).forEach((item) => {
      // 拿到数据后判断是复杂数据还是简单数据 如果是复杂数据类型就继续递归调用
      target[item] = isObj(obj[item]) ? deepClone2(obj[item], wMap) : obj[item];
    });

    return target;
  } else {
    return obj;
  }
}

这道题主要是的方案就是,递归加数据类型的判断

如是复杂数据类型,就递归的再次调用你这个拷贝方法 直到是简单数据类型后可以进行直接赋值

解读

这段代码包含了两个函数,deepClonedeepClone2,它们都用于深度克隆一个对象或数组,即创建一个新的对象或数组,其内容是原对象或数组的深拷贝。

  1. isObj 函数:这是一个箭头函数,用于检查传入的值 val 是否是一个非空对象(即类型为 “object” 且不为 null)。这个函数在 deepClonedeepClone2 中被用来检测是否需要进行深拷贝。

  2. deepClone 函数:这是第一个实现深度克隆的方法。它首先判断传入的对象 obj 是否是数组,如果是数组则创建一个空数组 newObj,否则创建一个空对象 newObj。然后,它遍历 obj 的所有属性,对于每个属性值,如果它是一个对象或数组,递归调用 deepClone 函数进行深拷贝;如果不是对象或数组,直接将值赋给 newObj 的相应属性。最后返回 newObj

  3. deepClone2 函数:这是第二个实现深度克隆的方法,它使用了 WeakMap 来优化性能并支持 Symbol 类型的键。WeakMap 允许垃圾回收器回收其键所对应的对象,因为它不会阻止这些对象被垃圾回收。函数首先检查传入的对象 obj 是否是一个对象,如果不是,直接返回 obj。如果是对象,它会检查 WeakMap 中是否已经存在该对象的克隆,如果存在,直接返回克隆对象。如果不存在,它会创建一个新的空对象或数组 target,并将其与原始对象关联存储在 WeakMap 中。然后,使用 Reflect.ownKeys(obj) 获取对象的所有自有属性(包括符号和字符串键),并对每个属性进行遍历。对于每个属性值,如果它是一个对象或数组,递归调用 deepClone2 函数进行深拷贝;如果不是对象或数组,直接将值赋给 target 的相应属性。最后返回 target

总结来说,这两个函数都是用于深度克隆对象的,但 deepClone2 使用了 WeakMap 来提高性能并处理更广泛的键类型。

6. 手写call bind apply

call bind apply的作用都是可以进行修改this指向

  • call 和 apply的区别在于参数传递的不同
  • bind 区别在于最后会返回一个函数。

    // call
    Function.prototype.MyCall = function (context) {
      if (typeof this !== "function") {
        throw new Error('type error')
      }
      if (context === null || context === undefined) {
        // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
        context = window
      } else {
        // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
        context = Object(context)
      }

      // 使用Symbol 来确定唯一
      const fnSym = Symbol()

      //模拟对象的this指向
      context[fnSym] = this

      // 获取参数
      const args = [...arguments].slice(1)

      //绑定参数 并执行函数
      const result = context[fnSym](...args) 

      //清除定义的this
      delete context[fnSym]

      // 返回结果 
      return result
    } 
    
    
    // call 如果能明白的话 apply其实就是改一下参数的问题
    // apply
    Function.prototype.MyApply = function (context) {
      if (typeof this !== "function") {
        throw new Error('type error')
      }

      if (context === null || context === undefined) {
        // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
        context = window
      } else {
        // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
        context = Object(context) 
      }


      // 使用Symbol 来确定唯一
      const fnSym = Symbol()
      //模拟对象的this指向
      context[fnSym] = this

      // 获取参数
      const args = [...arguments][1]

      //绑定参数 并执行函数 由于apply 传入的是一个数组 所以需要解构
      const result = arguments.length > 1 ? context[fnSym](...args) : context[fnSym]()

      //清除定义的this
      delete context[fnSym]

      // 返回结果  //清除定义的this
      return result
    }
    
    
    
    // bind
    Function.prototype.MyBind = function (context) {
      if (typeof this !== "function") {
        throw new Error('type error')
      }

      if (context === null || context === undefined) {
        // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
        context = window
      } else {
        // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
        context = Object(context) 
      }

      //模拟对象的this指向
      const self = this

      // 获取参数
      const args = [...arguments].slice(1)
        
      // 最后返回一个函数 并绑定 this 要考虑到使用new去调用,并且bind是可以传参的
      return function Fn(...newFnArgs) {
        if (this instanceof Fn) {
            return new self(...args, ...newFnArgs)
        }
            return self.apply(context, [...args, ...newFnArgs])
        }
    }
call 解读

这段代码定义了一个名为 MyCall 的方法,它被添加到了 JavaScript 的 Function.prototype 上。这意味着所有的函数都可以使用这个方法来模拟调用函数的行为,即使这个函数不是作为对象的方法被调用。下面是对代码块中每一部分的解释:

  1. Function.prototype.MyCall = function (context) { ... }:
    这行代码向 Function 原型添加了一个名为 MyCall 的方法。这个方法接受一个参数 context,它将用作函数调用时的 this 值。

  2. if (typeof this !== "function") { throw new Error('type error') }:
    这行代码检查当前的 this(即调用 MyCall 方法的函数)是否是一个函数。如果不是,抛出一个类型错误。

  3. if (context === null || context === undefined) { context = window }:
    如果传入的 contextnullundefined,则将 context 设置为全局对象(在浏览器环境中通常是 window)。

  4. else { context = Object(context) }:
    如果 context 不是 nullundefined,则将其转换为一个对象。这样做是为了确保 context 可以作为一个对象来使用,特别是当 context 是一个原始值(如数字、字符串或布尔值)时。

  5. const fnSym = Symbol():
    创建一个唯一的符号 fnSym,用作将要绑定到 context 上的函数的键。

  6. context[fnSym] = this:
    将当前函数(this)赋值给 context 对象的 fnSym 属性。这样,context 对象就有了一个与当前函数相同的属性,可以用来模拟函数调用的上下文。

  7. const args = [...arguments].slice(1):
    使用扩展运算符 ...arguments 对象获取除了第一个参数(即 context)之外的所有参数,并将它们存储在 args 数组中。

  8. const result = context[fnSym](...args):
    通过 context[fnSym] 调用函数,并传入之前收集的参数 args。这将模拟函数调用,并执行该函数。

  9. delete context[fnSym]:
    函数调用完成后,从 context 对象中删除之前添加的 fnSym 属性,以保持 context 对象的整洁。

  10. return result:
    返回函数调用的结果。

总的来说,这段代码实现了一个类似于 Function.prototype.call 的功能,允许你在任何对象上调用任何函数,就像它是那个对象的方法一样。这在需要改变函数执行上下文的场景中非常有用,例如事件处理程序或者构造函数中。

apply 解读

这段代码定义了一个名为 MyApply 的方法,它被添加到了 Function.prototype 上,这意味着所有的函数都可以使用这个方法。MyApply 方法模拟了原生 JavaScript 中的 apply 方法的行为,允许你指定一个上下文对象(context)并在这个上下文中调用当前函数。

以下是对代码块的逐行解释:

  1. Function.prototype.MyApply = function (context) {: 这行代码开始定义一个新的方法 MyApply,并将其添加到 Function.prototype 上,使其成为所有函数的一个属性。

  2. if (typeof this !== "function") {: 这行代码检查当前的 this 是否是一个函数。如果不是,抛出一个类型错误异常。

  3. throw new Error('type error'): 如果 this 不是一个函数,则抛出一个错误。

  4. if (context === null || context === undefined) {: 这行代码检查传入的 context 参数是否为 nullundefined

  5. context = window: 如果 contextnullundefined,则将 context 设置为全局对象(在浏览器环境中通常是 window)。

  6. } else {: 如果 context 不是 nullundefined,则执行下面的代码。

  7. context = Object(context): 将 context 转换为一个对象。如果 context 是一个原始值(如数字、字符串或布尔值),则将其转换为对应的包装对象(如 NumberStringBoolean 的实例)。

  8. const fnSym = Symbol(): 创建一个唯一的符号,用作将要绑定到 context 的属性名。

  9. context[fnSym] = this: 将当前函数(this)赋值给 context 对象的一个新属性,属性名为 fnSym

  10. const args = [...arguments][1]: 获取传递给 MyApply 的第二个参数及之后的所有参数,这些参数将被用作调用函数时的参数。

  11. const result = arguments.length > 1 ? context[fnSym](...args) : context[fnSym](): 根据是否有额外的参数来决定如何调用函数。如果有额外的参数,则使用扩展运算符 ...args 将这些参数传递给函数;如果没有额外的参数,则直接调用函数。

  12. delete context[fnSym]: 在函数调用完成后,从 context 对象中删除之前添加的属性。

  13. return result: 返回函数调用的结果。

总结来说,MyApply 方法允许你将一个函数作为某个对象的方法来调用,并且可以传递参数数组。这与原生的 Function.prototype.apply 方法类似,但有一些差异,例如它不直接接受参数数组,而是通过解构剩余参数(...arguments)来获取参数数组。此外,它还处理了 contextnullundefined 的情况,在这种情况下,它会默认使用全局对象作为上下文。

7. 手写实现继承

这里我就只实现两种方法了,ES6之前的寄生组合式继承 和 ES6之后的class继承方式。

    /**
    * es6之前  寄生组合继承 
    */
    {
      function Parent(name) {
        this.name = name
        this.arr = [1, 2, 3]
      }

      Parent.prototype.say = () => {
        console.log('Hi');
      }

      function Child(name, age) {
        Parent.call(this, name)
        this.age = age
      }

      //  核心代码 通过Object.create创建新对象 子类 和 父类就会隔离
      // Object.create:创建一个新对象,使用现有的对象来提供新创建的对象的__proto__ 
      Child.prototype = Object.create(Parent.prototype)
      Child.prototype.constructor = Child
    }
    
    
    
    /**
    *   es6继承 使用关键字class
    */
     {
      class Parent {
        constructor(name) {
          this.name = name
          this.arr = [1, 2, 3]
        }
      }
      class Child extends Parent {
        constructor(name, age) {
          super(name)
          this.age = age
        }
      }
    }

补充一个小知识, ES6的Class继承在通过 Babel 进行转换成ES5代码的时候 使用的就是 寄生组合式继承。

继承的方法有很多,记住上面这两种基本就可以了!

8. 手写 new 操作符

首先我们要知道 new一个对象的时候他发生了什么。

其实就是在内部生成了一个对象,然后把你的属性这些附加到这个对象上,最后再返回这个对象。

function myNew(fn, ...args) {
  // 基于原型链 创建一个新对象
  let newObj = Object.create(fn.prototype)

  // 添加属性到新对象上 并获取obj函数的结果
  let res = fn.call(newObj, ...args)

  // 如果执行结果有返回值并且是一个对象, 返回执行的结果, 否则, 返回新创建的对象
  return res && typeof res === 'object' ? res : newObj;
}

9. js执行机制 说出结果并说出why

这道题考察的是,js的任务执行流程,对宏任务和微任务的理解

console.log("start");

setTimeout(() => {
  console.log("setTimeout1");
}, 0);

(async function foo() {
  console.log("async 1");

  await asyncFunction();

  console.log("async2");

})().then(console.log("foo.then"));

async function asyncFunction() {
  console.log("asyncFunction");

  setTimeout(() => {
    console.log("setTimeout2");
  }, 0);

  new Promise((res) => {
    console.log("promise1");

    res("promise2");
  }).then(console.log);
}

console.log("end");

提示:

  1. script标签算一个宏任务所以最开始就执行了
  2. async await 在await之后的 Promise 都会被放到微任务队列中去

开始执行

  • 最开始碰到 console.log(“start”); 直接执行并打印出 start
  • 往下走,遇到一个 setTimeout1 就放到宏任务队列
  • 碰到立即执行函数 foo, 打印出 async 1
  • 遇到 await 堵塞队列,先 执行await的函数
  • 执行 asyncFunction 函数, 打印出 asyncFunction
  • 遇到第二个 setTimeout2, 放到宏任务队列
  • new Promise 立即执行,打印出 promise1
  • 执行到 res(“promise2”) 函数调用,就是Promise.then。放到微任务队列
  • asyncFunction函数就执行完毕, 把后面的打印 async2 会放到微任务队列
  • 然后打印出立即执行函数的then方法 foo.then
  • 最后执行打印 end
  • 开始执行微任务的队列 打印出第一个 promise2
  • 然后打印第二个 async2
  • 微任务执行完毕,执行宏任务 打印第一个 setTimeout1
  • 执行第二个宏任务 打印 setTimeout2
  • 就此,函数执行完毕 image.png

画工不好,能理解到意思就行😭。 看看你们的想法和答案是否和这个流程一致

10. 如何拦截全局Promise reject,但并没有设定 reject处理器 时候的错误

这道题我是没写出来,最开始想着 trycatch 但这个并不是全局的。

后续查了资料才发现 是用一个window上面的方法

// 使用Try catch 只能拦截try语句块里面的
try {
  new Promise((resolve, reject) => {
    reject("WTF 123");
  });
} catch (e) {
  console.log("e", e);
  throw e;
}

// 使用 unhandledrejection 来拦截全局错误  (这个是对的)
window.addEventListener("unhandledrejection", (event) => {
  event && event.preventDefault();
  console.log("event", event);
});

11. 手写实现sleep

这个我只通过了一种方法实现,就是刚刚我们在上面js执行流程中我有提过。 await 会有异步堵塞的意思

还有一个方法是我在网上找到的方法,通过完全堵塞进程的方法来实现 这个有点吊

    // 使用 promise 配合await的异步方法来实现 sleep
    {
      (async () => {
        console.log('start');
        await sleep(3000)
        console.log('end');

        function sleep(timer) {
          return new Promise(res => {
            setTimeout(() => {
              res()
            }, timer);
          })
        }
      })();
    }

    // 方法二 这是完全堵塞进程来达到sleep
    {
      (async () => {
        console.log('start');
        await sleep(3000)
        console.log('end');

        function sleep(delay) {
          let t = Date.now();
          while (Date.now() - t <= delay) {
            continue;
          }
        };
      })()
    }

12. 实现add(1)(2) =3

光这个的话,可以通过闭包的方式实现了

我给这个加了一个难度,如何才能实现一直调用

    // 题意的答案
   const add = (num1) => (num2)=> num2 + num1;
   
   
   // 我自己整了一个加强版 可以无限链式调用 add(1)(2)(3)(4)(5)....
   function add(x) {
      // 存储和
      let sum = x;
       
      // 函数调用会相加,然后每次都会返回这个函数本身
      let tmp = function (y) {
        sum = sum + y;
        return tmp;
      };
      
      // 对象的toString必须是一个方法 在方法中返回了这个和
      tmp.toString = () => sum
      return tmp;
   }
   
   alert(add(1)(2)(3)(4)(5))

无限链式调用实现的关键在于 对象的 toString 方法: 每个对象都有一个 toString() 方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用

也就是我在调用很多次后,他们的结果会存在add函数中的sum变量上,当我alert的时候 add会自动调用 toString方法 打印出 sum, 也就是最终的结果

13. 两个数组中完全独立的数据

就是找到仅在两个数组中出现过一次的数据

var a = [1, 2, 4], b = [1, 3, 8, 4]
const newArr = a.concat(b).filter((item, _, arr) => {
  return arr.indexOf(item) === arr.lastIndexOf(item)
})

最终出来的结果是 [2,3,8], 原理其实很简单: 合并两个数组,然后查找数组的第一个出现的索引最后一个出现的索引是否一致就可以判断是否是独立的数据了。

14. 判断完全平方数

就是判断一个数字能不能被开平方, 比如9的开平方是3 是对的。 5没法开平方就是错的。

var fn = function (num) {
  return num ** 0.5 % 1 == 0
};

原理就是,开平方后判断是否是正整数就行了

15. 函数执行 说出结果并说出why

function Foo() {
  getName = function () {
    console.log(1);
  };
  return this;
}

Foo.getName = function () {
  console.log(2);
}

Foo.prototype.getName = function () {
  console.log(3);
}

var getName = function () { 
  console.log(4);
}

function getName() {
  console.log(5)
}

Foo.getName();

getName();

Foo().getName()

getName();

new Foo.getName(); 

new Foo().getName()

new new Foo().getName()

这道题其实就是看你对作用域的关系的理解吧

执行结果:

  • 执行 Foo.getName(), 执行Foo函数对象上的的静态方法。打印出 2

  • 执行 getName(), 就是执行的getName变量的函数。打印 4

    • 为什么这里是 执行的 变量getName,而不是函数getName呢。这得归功于js的预编译
    • js在执行之前进行预编译,会进行 函数提升变量提升
    • 所以函数和变量都进行提升了,但是函数声明的优先级最高,会被提升至当前作用域最顶端
    • 当在执行到后面的时候会导致getName被重新赋值,就会把执行结果为 4 的这个函数赋值给变量
  • 执行 Foo().getName()调用Foo执行后返回值上的getName方法。 Foo函数执行了,里面会给外面的getName函数重新赋值,并返回了this。 也就是执行了this.getName。所以打印出了 1

  • 执行 getName(), 由于上一步,函数被重新赋值。所以这次的结果和上次的结果是一样的,还是为1

  • 执行 new Foo.getName(), 这个 new 其实就是new了Foo上面的静态方法getName 所以是2。 当然如果你们在这个函数里面打印this的话,会发现指向的是一个新对象 也就是new出来的一个新对象

    • 可以把 Foo.getName()看成一个整体,因为这里 . 的优先级比 new 高
  • 执行 new Foo().getName(),这里函数执行 new Foo() 会返回一个对象,然后调用这个对象原型上的getName方法, 所以结果是 3

  • 执行 new new Foo().getName(), 这个和上一次的结果是一样,上一个函数调用后并咩有返回值,所以在进行new的时候也没有意义了。 最终结果也是3

16. 原型调用面试题 说出结果并说出 why

function Foo() {
  Foo.a = function () {
    console.log(1);
  };
  this.a = function () {
    console.log(2);
  };
}

Foo.prototype.a = function () {
  console.log(4);
};

Function.prototype.a = function () {
  console.log(3);
};


Foo.a();

let obj = new Foo();
obj.a();
Foo.a();

执行结果:

  • 执行Foo.a()Foo本身目前并没有a这个值,就会通过 __proto__ 进行查找,但是 image.png, 所以输出是 3

  • new 实例化了 Foo 生成对象 obj,然后调用 obj.a(),但是在Foo函数内部给这个obj对象附上了a函数。 所以结果是2。 如果在内部没有给这个对象赋值a的话,就会去到原型链查找a函数,就会打印4.

  • 执行Foo.a(), 在上一步中Foo函数执行,内部给Foo本身赋值函数a,所以这次就打印1

17. 数组分组改成减法运算

这个题的意思就是 [5, [[4, 3], 2, 1]] 变成 (5 - ((4 - 3) - 2 - 1)) 并执行。 且不能使用eval()

方法一: 既然不能用 eval, 那我们就用new Function吧🤭

方法二: 当然方法一有点违背了题意,所以还有第二种方法

var newArr = [5, [[4, 3], 2, 1]]

    // 1. 取巧
    // 转为字符串
    let newStringArr = `${JSON.stringify(newArr)}`
    // 循环修改括号和减号
    let fn = newStringArr.split("").map((el) => {
      switch (el) {
        case "[":
          return '('
        case "]":
          return ')'
        case ",":
          return '-'
        default:
          return el
      }
    }).join("")
    // 最终通过new Function 调用可以了!
    new Function("return " + fn)()
    
    
    // 2. 方法二 
    function run(arr) {
      return arr.reduce((pre, cur) => {
        let first = Array.isArray(pre) ? run(pre) : pre
        let last = Array.isArray(cur) ? run(cur) : cur
        return first - last
      })
    }
    run(newArr)
  • 方法一的原理就很简单,转成字符串循环修改括号和减号在进行拼接。最终通过 new Function 调用就可以了

  • 方法二的意思就是通过 reduce 进行一个递归调用 的意思。 如果左边不是数组就可以减去右边的,但如果右边是数组的话,就要把右边的数组先进行减法运算。也是就减法括号运算的的优先级.

18. 手写数组的 flat

    const flat = function (arr, deep = 1) {
      // 声明一个新数组
      let result = []
      
      arr.forEach(item => {
        if (Array.isArray(item) && deep > 0) {
          // 层级递减
          // deep--  来自评论区的大佬指正:deep - 1
          // 使用concat链接数组  
          result = result.concat(flat(item, deep - 1))
        } else {
          result.push(item)
        }
      })
      return result
    }
  • 原理就是,先在内部生成一个新数组,遍历原来的数组

  • 当原数组内 存在数组并且层级deep大于等于1时进行递归, 如果不满足这个条件就可以直接push数据到新数组

  • 递归同时要先把层级减少, 然后通过 concat 链接递归出来的数组

  • 最终返回这个数组就可以了

19. 数组转为tree

最顶层的parent 为 -1 ,其余的 parent都是为 上一层节点的id

    let arr = [
      { id: 0, name: '1', parent: -1, childNode: [] },
      { id: 1, name: '1', parent: 0, childNode: [] },
      { id: 99, name: '1-1', parent: 1, childNode: [] },
      { id: 111, name: '1-1-1', parent: 99, childNode: [] },
      { id: 66, name: '1-1-2', parent: 99, childNode: [] },
      { id: 1121, name: '1-1-2-1', parent: 112, childNode: [] },
      { id: 12, name: '1-2', parent: 1, childNode: [] },
      { id: 2, name: '2', parent: 0, childNode: [] },
      { id: 21, name: '2-1', parent: 2, childNode: [] },
      { id: 22, name: '2-2', parent: 2, childNode: [] },
      { id: 221, name: '2-2-1', parent: 22, childNode: [] },
      { id: 3, name: '3', parent: 0, childNode: [] },
      { id: 31, name: '3-1', parent: 3, childNode: [] },
      { id: 32, name: '3-2', parent: 3, childNode: [] }
    ]

    function arrToTree(arr, parentId) {
       // 判断是否是顶层节点,如果是就返回。不是的话就判断是不是自己要找的子节点
      const filterArr = arr.filter(item => {
        return parentId === undefined ? item.parent === -1 : item.parent === parentId
      })
       
      // 进行递归调用把子节点加到父节点的 childNode里面去
      filterArr.map(item => {
        item.childNode = arrToTree(arr, item.id)
        return item
      })
       
      return filterArr
    }
    
    arrToTree(arr)
  • 这道题也是利用递归来进行的,在最开始会进行是否是顶层节点的判断

  • 如果是就直接返回,如果不是则判断是不是自己要添加到父节点的子节点

  • 然后再一层一层把节点加入进去

  • 最后返回这个对象

20. 合并数组并排序去重

题意就是, 我有两个数组,把他们两个合并。然后并去重去重的逻辑是哪儿边的重复次数更多,我就留下哪儿边的。

比如下面的数组中,一边有两个数字5另一半有三个数字5 。则我需要留下三个数字5去掉两个数字5。 循环往复,最后得到的结果在进行排序。

  • 数组一: [1, 100, 0, 5, 1, 5]

  • 数组二: [2, 5, 5, 5, 1, 3]

  • 最终的结果: [0, 1, 1, 2, 3, 5, 5, 5, 100]

  // 判断出现次数最多的次数
    function maxNum(item, arr) {
      let num = 0;
      arr.forEach(val => {
        item === val && num++
      })

      return num
    }

    function fn(arr1, arr2) {
      // 使用Map数据类型来记录次数
      let obj = new Map();

      // 合并数组并找出最多的次数, 并以键值对存放到Map数据类型
      [...arr1, ...arr2].forEach(item => {
        let hasNum = obj.get(item)
        let num = 1
        if (hasNum) {
          num = hasNum + 1
        }
        obj.set(item, num)
      })

      // 存放合并并去重之后的数组
      let arr = []
      // 遍历Map数据类型 然后把次数最多的直接push到新数组
      for (const key of obj.keys()) {
        if (obj.get(key) > 1) {
          for (let index = 0; index < Math.max(maxNum(key, arr1), maxNum(key, arr2)); index++) {
            arr.push(key)
          }
        } else {
          arr.push(key)
        }
      }

    // 最后进行排序
      return arr.sort((a, b) => a - b)
    }
  • 这个题的思路其实就是,我先把两个数组合并起来

  • 并以键值对的方式存放到Map数据类型, 键就是数据,而值就是这个数据出现的次数

  • 生成一个新数组,用来存放合并之后的数组

  • 遍历这个Map数据类型, 如果这个数据出现的次数大于一,那么就去寻找两个数组中谁出现的次数更多,把出现次数更多的这个数据,循环push到新数组中。 如果出现次数等于一,那就直接push到新数组中即可。

  • 最后再把数组进行排序,然后返回新数组就可。

21、Function.prototype使用

Function.prototype 是 JavaScript 中的一个属性,它允许你向所有函数对象添加新的属性和方法。通过修改 Function.prototype,你可以为所有的函数实例添加自定义的行为或功能。

例如,如果你想给所有的函数添加一个名为 myCustomMethod 的方法,你可以这样做:

Function.prototype.myCustomMethod = function() {
    console.log('这是一个自定义方法');
};

// 现在所有的函数都可以调用这个方法
function exampleFunction() {
    console.log('这是一个示例函数');
}

exampleFunction.myCustomMethod(); // 输出: "这是一个自定义方法"

需要注意的是,直接修改 Function.prototype 可能会导致与其他库或框架的冲突,因此在实际开发中要谨慎使用。

三、结语

最后希望大家能理解这些题并知晓why,有些题,并不是单单只是用来考你。而是变相的让你理解这其中深藏的意义。

以上的题目,我的答案并非就是最优答案。若你有更好的解决方法,请不要吝啬,大胆的敲到评论区! 我会把你的解决方法补充到文章里面。

如果文中内容对你有帮助, 记得三连~ 🎉🎉🎉 如文中有错误,也欢迎大家指正修改!


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2177502.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Chrome无法拖入加载.crx扩展文件(以IDM为例)

问题原因&#xff1a;新版本的Chrome浏览器已不支持加载.crx文件 解决办法&#xff1a;将.crx文件压缩为.zip文件&#xff0c;解压缩后再加载到Chrome中 以IDM的.crx文件作为示例&#xff1b; IDM的.crx文件位于C:\Program Files (x86)\Internet Download Manager; 将IDMGCE…

计算机毕业设计 C语言学习辅导网站的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

记一次实战中对Ruoyi系统的渗透

前言 最近碰到比较多Ruoyi的站&#xff0c;ruoyi的话漏洞还是比较多的&#xff0c;这里就分享一下自己渗透的一些案例吧&#xff0c;方便大家参考学习 首先声明 文章中涉及的敏感信息均已做打码处理&#xff0c;文章仅做经验分享用途&#xff0c;切勿当真&#xff0c;未授权…

解决:使用layui.treeTable.updateNode,更新表格数据后,done里面的事件丢失问题

1. 背景 在给树形表格添加行点击事件&#xff0c;并且只更新当前行数据。 treeTable.updateNode("SpeProjListId", result.LAY_DATA_INDEX, result);更新数据后&#xff0c;点击事件失效。 1. 给字段绑定事件&#xff1a; class"link_a link_style" , {…

基于Spring3.0实现AOP的小案例

前言 AOP&#xff08;Aspect Oriented Programming&#xff09;即面向切面编程&#xff0c;是一种通过预编译方式和运行期间动态代理实现程序功能统一维护的技术。针对功能增强的描述&#xff0c;可以理解为&#xff1a;“AOP允许在不修改源代码的情况下&#xff0c;通过定义切…

Java异步编程:从入门到精通

在现代编程实践中&#xff0c;异步编程已成为提升程序性能和用户体验的关键技术。Java&#xff0c;作为一种成熟且广泛使用的编程语言&#xff0c;提供了多种实现异步编程的方法。本文将带你从异步编程的基础知识入手&#xff0c;逐步深入到Java中的异步编程实践。 异步编程简介…

SQL常用数据过滤 - EXISTS运算符

SQL查询中的EXISTS运算符用于检查查询子句是否存在满足特定条件的记录&#xff0c;如果有一条或者多条记录存在&#xff0c;则返回True&#xff0c;否则返回False。 语法结构 SELECT column_name(s)FROM table_nameWHERE EXISTS(SELECT column_name FROM table_name WHERE co…

基于两分支卷积和 Transformer 的轻量级多尺度特征融合超分辨率网络 !

当前的单图像超分辨率&#xff08;SISR&#xff09;算法有两种主要的深度学习模型&#xff0c;一种是基于卷积神经网络&#xff08;CNN&#xff09;的模型&#xff0c;另一种是基于Transformer的模型。前者利用不同卷积核大小的卷积层堆叠来设计模型&#xff0c;使得模型能够更…

SOLID原则:现代软件架构的永恒基石

关注TechLead&#xff0c;复旦博士&#xff0c;分享云服务领域全维度开发技术。拥有10年互联网服务架构、AI产品研发经验、团队管理经验&#xff0c;复旦机器人智能实验室成员&#xff0c;国家级大学生赛事评审专家&#xff0c;发表多篇SCI核心期刊学术论文&#xff0c;阿里云认…

面积开运算bwareaopen

一个非常有用的二值图像形态学后处理算法&#xff0c;建立在连通分量分析的基础之上。 bwareaopen 从二值图像中删除小对象 语法 BW2 bwareaopen(BW,P) BW2 bwareaopen(BW,P,conn) 说明 BW2 bwareaopen(BW,P) 从二值图像 BW 中删除少于 P 个像素的所有连通分量&#x…

docker简介、安装、基础知识

基础知识 Docker简介&#xff1a; 1.Docker是一种用于构建、发布及运行应用程序的开源项目&#xff0c;他通过容器化技术简化了应用程序的部署和管理 2.Docker是一个开源的应用容器引擎&#xff0c;基于go语言开发&#xff0c;为应用打包、部署平台&#xff0c;而非单纯的虚…

计算机毕业设计 办公用品管理系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

C++(Qt)软件调试---内存调试器Dr.Memory(21)

C(Qt)软件调试—内存调试器Dr. Memory&#xff08;21&#xff09; 文章目录 C(Qt)软件调试---内存调试器Dr. Memory&#xff08;21&#xff09;[toc]1、概述&#x1f41c;2、安装Dr.Memory&#x1fab2;3、命令行使用Dr.Memory&#x1f997;4、Qt Creator集成使用Dr.Memory&…

预计OpenAI在新一轮融资后估值可达1500亿美元!Hugging Face平台托管模型数量破100万|AI日报

文章推荐 HuggingChat macOS版正式发布&#xff01;文章内附体验地址&#xff01;我国打造糖尿病专用AI模型&#xff5c;AI日报 今日热点 OpenAI在新一轮融资后估值可能达到1500亿美元 知情人士表示&#xff0c;Thrive Capital将在OpenAI目前的65亿美元融资轮中投资超过10亿…

如何高效管理知识产权全链条?

为了有效保护企业的创新成果&#xff0c;确保技术创意的顺利转化&#xff0c;以及高效管理知识产权案件&#xff0c;建立一套完善的知识产权管理体系至关重要。对于企业而言&#xff0c;如何有效地管理知识产权的各个环节&#xff0c;从研发项目到技术创意&#xff0c;再到提案…

排序(交换排序:快排)

快速排序&#xff1a; 写快排的注意事项 1.单趟排序hoare 2.不写优化只说优化就行 理想的情况下&#xff1a;每次排序都是二分&#xff0c;直到二分到最后&#xff0c;那就相当于递归高度次(logN)&#xff0c;每一层单趟排都是O(N)&#xff0c;时间复杂度O(NlogN) 空间复杂度就…

PHP程离禁用一段IP的写法示例

PHP程离禁用一段IP的写法示例 。 在PHP中&#xff0c;如果你想禁用一段IP地址的访问&#xff0c;你可以使用$_SERVER[REMOTE_ADDR]来获取访问者的IP地址&#xff0c;然后通过判断IP地址是否在你想要禁用的范围内来决定是否拒绝服务。 以下是一个简单的例子&#xff0c;展示了…

net Core aspx视图引擎 razor视图引擎

视图引擎 》》定义&#xff0c;什么是视图引擎 视图引擎就是&#xff0c;将服务器端模板转换为HTML标记&#xff0c;并在控制器的操作方法触发时在web浏览器中呈现 现在都推荐 Razor视图引擎了&#xff08;也是默认视图引擎&#xff09;&#xff0c;aspx引擎不推荐了。 ASPX …

AI新掌舵:智享AI直播系统:直播界的新浪潮还是真人主播的终结者?

AI新掌舵&#xff1a;智享AI直播系统&#xff1a;直播界的新浪潮还是真人主播的终结者&#xff1f; 在数字化浪潮的汹涌澎湃中&#xff0c;人工智能&#xff08;AI&#xff09;以其前所未有的速度渗透至各行各业&#xff0c;其中&#xff0c;直播领域正经历着一场前所未有的变革…

javascript:冻结对象

1 作用 冻结一个对象&#xff0c;使对象不可扩展。 2 特性 对象的属性不可再被新增、删除对象的属性的值不可再被修改对象的属性的描述符中任意配置项都不可被重新定义 3 代码示例 3.1 冻结对象 Object.freeze() 代码如下&#xff1a; use strict let initialData {a: 1…