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基础,万丈高楼平地起,所以基础对于前端开发来说还是非常重要的,希望读完本篇你能有所收获,在面试中对答如流,一帆风顺。

相关推荐
brzhang22 分钟前
我写了个脚本,让AI每天自动看完热榜、写稿、配乐,还用我的声音读出来
前端·后端·架构
Mintopia1 小时前
Three.js 3D 图表与数据可视化:在数字宇宙中绘制数据星河
前端·javascript·three.js
JohnYan1 小时前
Bun技术评估 - 11 Websocket
javascript·后端·bun
全干engineer1 小时前
Web3-Web3.js核心操作:Metamask、合约调用、事件订阅全指南
开发语言·javascript·web3·区块链·智能合约
Leyla1 小时前
你不知道的 parseInt 方法
javascript·面试
米花丶1 小时前
前端 Service Worker最佳实践(上):高效的预缓存与运行时缓存方案
前端
困困的果果头2 小时前
【vue + element】el-table支持多层级合并列
前端·javascript·vue.js·elementui
GISer_Jing2 小时前
React前端与React Native移动端开发须知差异
前端·react native·react.js
EndingCoder2 小时前
React Native 与后端协同开发指南
javascript·react native·react.js