一、数组去重
请封装一个方法将下面数组去重,写出你所能想到的实现方式,越多越好
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基础,万丈高楼平地起,所以基础对于前端开发来说还是非常重要的,希望读完本篇你能有所收获,在面试中对答如流,一帆风顺。