10道js经典面试题助你找到好工作

一、数组去重

请封装一个方法将下面数组去重,写出你所能想到的实现方式,越多越好

js 复制代码
const arr = [1, 2, 3, 3, 2, 5, 6, 5, 5, '1' , '2']; 

1. 使用Set(ES6最简单的方式)

js 复制代码
function unique(arr) {
  return [...new Set(arr)];
}

2. 使用filter和indexOf

js 复制代码
function unique(arr) {
  return arr.filter((item, index) => arr.indexOf(item) === index);
}

这个实现方式可能有的同学一下不太想得明白,其实原理很简单,因为indexOf获取到的始终是数组的第一个成立的元素的下标,如果当前元素不是第一个了arr.indexOf(item) === index) 肯定返回false, filter过滤掉了该元素。自然就达到了去重的目的

3. 使用reduce和includes

js 复制代码
function unique(arr) {
  return arr.reduce((prev, current) => {
    return prev.includes(current) ? prev : [...prev, current];
  }, []);
}

4. 使用forEach和includes

js 复制代码
function unique(arr) {
  const result = [];
  arr.forEach((item) => {
    if (!result.includes(item)) {
      result.push(item);
    }
  });
  return result;
}

5. 使用对象键值对

js 复制代码
function unique(arr) {
  const obj = {};
  return arr.filter((item) => {
    console.log(obj.hasOwnProperty(typeof item + item));
    return obj.hasOwnProperty(typeof item + item)
      ? false
      : (obj[typeof item + item] = true);
  });
}

这里需要注意的是在添加属性和判断属性时使用了typeof item + item,而不直接使用item为什么要这样呢,这是为了不让字符串的数字值和数字类型的值相同的被过滤掉。也就是说使用直接item 的结果会是这样的:

而使用typeof item + item 的结果是这样的

6. 排序后相邻比较

js 复制代码
function unique(arr) {
  arr.sort();
  const result = [arr[0]];
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] !== arr[i - 1]) {
      result.push(arr[i]);
    }
  }
  return result;
}

7. 使用Map(ES6)

js 复制代码
function unique(arr) {
  const map = new Map();
  return arr.filter(item => !map.has(item) && map.set(item, true));
}

8. 双重循环(最原始的方法)

js 复制代码
function unique(arr) {
  for (let i = 0; i < arr.length; i++) {
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[i] === arr[j]) {
        arr.splice(j, 1);
        j--;
      }
    }
  }
  return arr;
}

9. 使用findIndex(ES6)

js 复制代码
function unique(arr) {
  const result = [];
  arr.forEach((item) => {
    if (result.findIndex((v) => v === item) === -1) {
      result.push(item);
    }
  });
  return result;
}

二、闭包和作用域

有如下一段代码:

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>js基础</title>
  </head>
  <body>
    <ul class="list">
      <li>1</li>
      <li>2</li>
      <li>3</li>
      <li>4</li>
      <li>5</li>
    </ul>
    <script>
      const oList = document.getElementsByClassName("list")[0];
      const oItemList = oList.getElementsByTagName("li");
      for (var i = 0; i < oItemList.length; i++) {
        console.log(oItemList[i]);
        oItemList[i].onclick = function () {
          console.log(i);
        };
      }
    </script>
  </body>
</html>

请问当点击第三个li标签在控制台会打印多少

这道题主要考察对js 作用域的理解,由于var 声明的变量不存在块级作用域,只受函数作用域限制,所以答案必然是5,因为点击的时候循环已经完成了,所以i是循环结束的时候的值,值是5

面试官接着问,如果想要达到理想的结果,点击第一个打印0, 点击第二个打印1,该怎么改进呢?

第一种方式: 利用立即执行函数形成作用域

js 复制代码
const oList = document.getElementsByClassName("list")[0];
const oItemList = oList.getElementsByTagName("li");
for (var i = 0; i < oItemList.length; i++) {
  (function (i) {
    oItemList[i].onclick = function () {
      console.log(i);
    };
  })(i);
}

第二种方式: 将var声明的变量改成let 声明

js 复制代码
const oList = document.getElementsByClassName("list")[0];
const oItemList = oList.getElementsByTagName("li");
for (let i = 0; i < oItemList.length; i++) {
  oItemList[i].onclick = function () {
    console.log(i);
  };
}

三、类型转换和运算优先级

请写出下列代码会在控制台打印出的结果

js 复制代码
console.log(typeof null);
console.log(typeof (1 - "1"));
console.log(typeof ("1" - "1"));
console.log(typeof a);
console.log(typeof typeof a);

console.log(+"1");
console.log(+"abc");
console.log(+"123abc");
console.log(parseInt("123abc"));
console.log(parseFloat("123.45abc"));
console.log(Number("123abc"));

console.log([] === []);
console.log(0 === -0);
console.log(NaN === NaN);

console.log("b" + "a" + +"a" + "a");
console.log(!!"false" == !!"true");
console.log(1 < 2 < 3);
console.log(3 > 2 > 1);

答案如下:

js 复制代码
console.log(typeof null); // object
console.log(typeof (1 - "1")); // number
console.log(typeof ("1" - "1")); // number
console.log(typeof a); // undefined
console.log(typeof typeof a); // string

console.log(+"1"); // 1
console.log(+"abc"); // NaN
console.log(+"123abc"); // NaN
console.log(parseInt("123abc")); // 123
console.log(parseFloat("123.45abc")); // 123.45
console.log(Number("123abc")); // NaN

console.log([] === []); // false
console.log(0 === -0); // true
console.log(NaN === NaN); // false

console.log("b" + "a" + +"a" + "a"); // baNaNa
console.log(!!"false" == !!"true"); //  true
console.log(1 < 2 < 3); // true
console.log(3 > 2 > 1); // false

四、事件冒泡和事件捕获

有以下一段代码, 先点击按钮一,再点击按钮二,再点击按钮三, 请问控制台会打印出什么结果

js 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>js基础</title>
  </head>
  <body>
    <button id="btn1">按钮一</button>
    <button id="btn2">按钮二</button>
    <button id="btn3">按钮三</button>
    <script>
      const oBtn1 = document.getElementById("btn1");
      const oBtn2 = document.getElementById("btn2");
      const oBtn3 = document.getElementById("btn3");
      oBtn1.addEventListener(
        "click",
        function () {
          console.log(1);
        },
        false
      );
      oBtn1.addEventListener(
        "click",
        function () {
          console.log(2);
        },
        true
      );

      oBtn2.addEventListener(
        "click",
        function () {
          console.log(3);
        },
        true
      );
      oBtn2.addEventListener(
        "click",
        function () {
          console.log(4);
        },
        false
      );

      oBtn3.onclick = function () {
        console.log(5);
      };
      oBtn3.addEventListener(
        "click",
        function () {
          console.log(6);
        },
        false
      );
      oBtn3.addEventListener(
        "click",
        function () {
          console.log(7);
        },
        true
      );
    </script>
  </body>
</html>

答案: 2 1 3 4 7 5 6

解析: 本题主要考察 捕获先执行还是冒泡先执行,当还有onclick时,又是谁先执行。他们执行的先后顺序是这样的:先执行捕获,onclick和冒泡谁放在前面谁就先执行,所以最终答案是2 1 3 4 7 5 6

五、函数参数,简单数据类型,引用数据类型

有如下一段代码,请写出控制台打印出的结果

js 复制代码
var a = 1;
function test1(a) {
  console.log(a);
  a = 2;
  console.log(a);
}
test1(a);
console.log(a);

var b = {
  a: 1,
};
function test2(b) {
  console.log(b.a);
b.a = 2;
  console.log(b.a);
}
test2(b);
console.log(b.a);

答案: 1 2 1 1 2 2

解析: 函数的形参其实相当于在内部声明的一个新变量,所以对于基础数据类型,重新赋值不会影响外部的变量。对于引用类型,虽然也是相当于新声明了一个变量,但是它和外界的变量指向是同一个地址,所以当在函数内部修改形参的属性这里的a属性时,函数的外部也会受影响这里指b变量的a属性。

六、函数的特性

有如下一段代码, 请写出在控制台打印的结果

js 复制代码
function Foo() {
  getName = function () {
    console.log(1);
  };
}
Foo.prototype.getName = function () {
  console.log(2);
};
Foo.getName = function () {
  console.log(3);
};

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

getName();
new Foo.getName();
new Foo();
getName();

答案: 4 3 1

解析:

  • 执行第一个getName时,由于js 执行的时候会先进行预编译,预编译阶段变量会提升,预编译结束的时候 getName 结果是function 的getName,预编译结束后代码开始执行,执行给var 变量赋值,所以这时候getName变成了var getName, 所以执行第一个getName的时候是var getName, 所以控制台会打印4。
  • 执行new Foo.getName() 时, 会执行Foo.getName 所以,打印3
  • new Foo 执行,由于Foo函数内部 没有getName, 所以这里getName会指向外函数外部的getName,所以外部的getName值会被覆盖
  • 最后再执行getName时,getName已经被Foo中的getName重新赋值了,最后执行getName 打印的结果必然是1

七、函数防抖和函数节流

请封装一个防抖函数和一个节流函数

首先要完成这个题,就需要知道什么是函数防抖,和函数节流

函数防抖(Debounce): 在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时 应用场景:

  • 搜索框输入关键词(等待用户停止输入后再发送请求)

  • 窗口大小调整(等待调整结束后再计算布局)

  • 表单验证(用户输入完成后才验证)

函数节流(Throttle): 规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

应用场景:

  • 滚动事件(每隔一段时间计算位置信息)

  • 鼠标移动事件(控制处理频率)

  • 按钮频繁点击(防止重复提交)

防抖函数封装:

js 复制代码
/**
 *
 * @param {要执行的函数} fn
 * @param {防抖的时间(延时执行时间)} time
 * @param {第一次是否立即执行} triggerNow
 * @returns
 */
function debuonce(fn, time, triggerNow) {
  var t = null;
  var debuonced = function () {
    var _self = this;
    var args = arguments;

    if (t) {
      clearTimeout(t);
    }
    if (triggerNow) {
      var exec = !t;
      t = setTimeout(function () {
        t = null;
      }, time);
      if (exec) {
        fn.apply(_self, args);
      }
    } else {
      t = setTimeout(function () {
        fn.apply(_self, args);
      }, time);
    }
    debuonced.remove = function () {
      clearTimeout(t);
      t = null;
    };
  };

  return debuonced;
}

节流函数封装:

js 复制代码
/**
 *
 * @param {要执行的函数} fn
 * @param {延迟执行的时间} delay
 * @returns
 */
function throttle(fn, delay) {
  var t = null,
    begin = new Date().getTime();

  return function () {
    var _self = this,
      args = arguments,
      cur = new Date().getTime();
    clearTimeout(t);

    if (cur - begin >= delay) {
      fn.apply(_self, args);
      begin = cur;
    } else {
      t = setTimeout(function () {
        fn.apply(_self, args);
      }, delay);
    }
  };
}

八、解构赋值

有如下一段代码, 请在不声明第三个变量的情况下将a变量和b变量的值进行交换

js 复制代码
let a = 1;
let b = 2;

答案:

js 复制代码
[b, a] = [a, b];

九、迭代器生成器

有如下一段代码, 请问如何才能使得obj 可以使用for of 进行遍历

js 复制代码
const obj = {
  a: 1,
  b: 2,
  c: 3,
};

分析: 这道题主要考察对迭代器的掌握,一个能被for of 遍历的数据结构到底具备什么特点,一个能被for of遍历的数据结构需要在原型上实现[Symbol.iterator] 方法,而Symbol.iterator 方法中重要的就是会返回next方法,next方法会返回每次迭代的值,和done属性,没有迭代完成时done为false,迭代完成时done 为true,当前迭代值为undefined。根据这些特点就可以来完成上面的需求。实现代码如下:

js 复制代码
Object.prototype[Symbol.iterator] = function () {
  const o = this;
  const keys = Object.keys(o);
  let step = 0;

  return {
    next() {
      if (step < keys.length) {
        const key = keys[step++];
        const value = o[key];

        return {
          value: { [key]: value },
          done: false,
        };
      } else {
        return {
          value: undefined,
          done: true,
        };
      }
    },
  };
};

十、数据扁平化

有如下一段代码,请写出在控制台打印的结果

js 复制代码
const arr1 = [1, 2, [3, [4, [5]]]];

const arr2 = arr1.flat(false);
console.log(arr2);

const arr3 = arr1.flat("3");
console.log(arr3);

const arr4 = arr1.flat(-3);
console.log(arr4);

const arr5 = [1, , , [2], 3, 4];
const arr6 = arr1.flat();
console.log(arr6);

这题目主要考察面试者的学习能力,在学习的时候有没有进行深度思考。答案如下:

js 复制代码
const arr1 = [1, 2, [3, [4, [5]]]];

const arr2 = arr1.flat(false);
console.log(arr2); // [1, 2, 3,[4, [5]]]

const arr3 = arr1.flat("3");
console.log(arr3); // [1, 2, 3, 4, 5]

const arr4 = arr1.flat(-3);
console.log(arr4); // [1, 2, [3, [4, [5]]]]

const arr5 = [1, , , [2], 3, 4];
const arr6 = arr1.flat();
console.log(arr6); // [1, 2, 3, 4]

十一、总结

本篇主要分享了10个景点面试题,主要是考察js基础,万丈高楼平地起,所以基础对于前端开发来说还是非常重要的,希望读完本篇你能有所收获,在面试中对答如流,一帆风顺。

相关推荐
江上月5132 分钟前
JMeter中级指南:从数据提取到断言校验全流程掌握
java·前端·数据库
代码猎人3 分钟前
forEach和map方法有哪些区别
前端
恋猫de小郭4 分钟前
Google DeepMind :RAG 已死,无限上下文是伪命题?RLM 如何用“代码思维”终结 AI 的记忆焦虑
前端·flutter·ai编程
byzh_rc12 分钟前
[微机原理与系统设计-从入门到入土] 微型计算机基础
开发语言·javascript·ecmascript
m0_4711996313 分钟前
【小程序】订单数据缓存 以及针对海量库存数据的 懒加载+数据分片 的具体实现方式
前端·vue.js·小程序
编程大师哥14 分钟前
Java web
java·开发语言·前端
A小码哥15 分钟前
Vibe Coding 提示词优化的四个实战策略
前端
Murrays16 分钟前
【React】01 初识 React
前端·javascript·react.js
大喜xi19 分钟前
ReactNative 使用百分比宽度时,aspectRatio 在某些情况下无法正确推断出高度,导致图片高度为 0,从而无法显示
前端
helloCat19 分钟前
你的前端代码应该怎么写
前端·javascript·架构