前端面试题集

本文记录前端面试中的一些常见题型,如果有幸让你看到了这篇文章,欢迎探讨学习!

编程题

实现 call 函数

call 函数的作用,可以改变调用者函数内部的 this 指向,立即执行,第二个参数为 rest 类型参数。如果没有指定第一个参数,则 this 指向 window。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Function.prototype.myCall = function(con) {
// 获取执行环境,如果传入的值为 非真值 则将 window 作为 上下文环境
let context = con || window,
_fn = Symbol("_fn");

// 谁调 myCall 这里的 this 就指的是谁。将调用者挂载到当前指定的 上下文执行环境 上
context[_fn] = this;

// 取出传入 myCall 中 除了第一个参数外的其他参数
let arg = [...arguments].slice(1);

let res = context[_fn](...arg);

// 执行完后,删除 fn
delete context[_fn];

// 返回执行结果
return res;
};

function a(num) {
console.log(this.foo + num);
}

let foo = 4;
let obj = { foo: 5 };

a.myCall(obj, 1);

a.call(obj, 1);

我们在使用 _fn 作为临时属性时,使用的是 Symbol 类型,这是为了避免与上下文环境中的其他属性重复而发生冲突。

实现 apply 函数

apply 函数的作用,可以改变调用者函数内部的 this 指向,立即执行,第二个参数为数组类型。如果没有指定第一个参数,则 this 指向 window。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Function.prototype.myApply = function(con, arr = []) {
let context = con || window,
_fn = Symbol("_fn");

context[_fn] = this;

let res = context[_fn](...arr);

delete context[_fn];

return res;
};

function a(num1, num2, num3) {
console.log(this.foo + num1 + num2 + num3);
}

var foo = 2;
var obj = { foo: 4 };

a.myApply(obj, [1, 2, 3]);

a.apply(obj, [1, 2, 3]);

实现 bind 函数

bind 函数的作用,返回新的函数,该函数内的 this 指向 bind 函数的第一个参数对象,第二个参数为传入新函数内的 rest 参数。如果没有指定第一个参数,则 this 指向 window。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Function.prototype.myBind = function(con) {
if (typeof this !== "function") throw new Error("error");

var context = con || window;

var _this = this;

var args = [...arguments].slice(1);

return function Fn() {
// 因为返回了一个函数,我们可以 new Fn(),所以需要判断
if (this instanceof Fn) {
return new _this(...args, ...arguments);
}
return _this.apply(context, [...args, ...arguments]);
};
};

var foo = 1;
var obj = { foo: 2 };

function a(num1, num2) {
console.log(this.foo + num1 + num2);
}

// 参数是累计的,多出的参数不会被使用
var fn1 = a.bind(obj, 2, 2);
fn1(3, 5);

var fn2 = a.myBind(obj, 2, 2);
fn2(3, 5);

对多维数组进行降维(扁平化)

实现的效果类似这样:

1
let arr = [[1, [2]], [3], [4]];

扁平化后,如下所示:

1
let flatArr = [1, 2, 3, 4];

方式 1:

1
2
3
let arr1 = [1, [2, [4, [5]]], 3].flat(Infinity);

console.log("方式1", arr1);

方式 2:

1
2
3
4
5
6
7
8
9
const flattenDeep = arr => {
return Array.isArray(arr)
? arr.reduce((a, b) => [...a, ...flattenDeep(b)], [])
: [arr];
};

let arr2 = flattenDeep([1, [[2], [3, [4]], 5]]);

console.log("方式2", arr2);

方式 3:

使用 generater 函数 和 for ...of 循环 或 使用 扩展运算符 ... 也行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function* iterArr(arr) {
if (Array.isArray(arr)) {
for (let i = 0; i < arr.length; i++) {
yield* iterArr(arr[i]);
}
} else {
yield arr;
}
}

let newArr = [],
arr3 = [1, [[2], [3, [4]], 5]];

for (let v of iterArr(arr3)) {
newArr.push(v);
}

console.log("方式3", newArr);
console.log("方式3(扩展运算符)", [...iterArr(arr3)]);

实现支持注册、分发和解绑的事件类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 数组置空的方法:
// arr = []; arr.length = 0; arr.splice(0, arr.length)

class Event {
constructor() {
this._cache = {};
}

// 注册事件:如果不存在此种type,创建相关数组
on(type, callback) {
this._cache[type] = this._cache[type] || [];
let fns = this._cache[type];
if (fns.indexOf(callback) === -1) {
fns.push(callback);
}
return this;
}

// 触发事件:对于一个type中的所有事件函数,均进行触发
trigger(type, ...data) {
let fns = this._cache[type];
if (Array.isArray(fns)) {
fns.forEach(fn => {
fn(...data);
});
}
return this;
}

// 删除事件:删除事件类型对应的array
off(type, callback) {
let fns = this._cache[type];
// 检查是否存在type的事件绑定
if (Array.isArray(fns)) {
if (callback) {
// 卸载指定的回调函数
let index = fns.indexOf(callback);
if (index !== -1) {
fns.splice(index, 1);
}
} else {
// 全部清空
fns = [];
}
}
return this;
}
}

// 以下是测试函数

const event = new Event();
event
.on("test", a => {
console.log(a);
})
.trigger("test", "hello");

实现斐波那契数列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* fib() {
// 两个变量,第 3 个值为 prev + curr ...
let [prev, curr] = [1, 1];

for (;;) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}

for (let v of fib()) {
if (v > 55) break;
console.log(v);
}

利用 Generater 函数 和 for ...of 循环可以很巧妙的实现,注意,generator 函数在被执行后,里面的代码并不会立即执行,需要依靠遍历器驱动 yield 执行。

所以,for(;;){} 并不会有什么性能问题。

实现防抖和节流函数

  1. 防抖

功能:触发高频事件后 interval 毫秒内函数只会执行一次,如果 interval 毫秒 内高频事件再次被触发,则重新计算时间。

会清除定时器,对于高频率触发的动作,会限制其频率降低。

1
2
3
4
5
6
7
8
9
10
11
function debounce(fn, interval = 300) {
let timer = null;
return function() {
// 每次执行该函数时,就清除已注册的定时器程序
clearTimeout(timer);
// 生成新的定时器程序,如果用户没有在指定延时时间内再次出发该函数,则该函数会被执行
timer = setTimeout(() => {
fn.apply(this, arguments);
}, interval);
};
}
  1. 节流

功能:高频事件触发,但在 interval 毫秒内只会执行一次,所以节流会稀释函数的执行频率。

不会清除定时器,对于高频率触发的动作,会减少其触发次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
function throttle(fn, interval = 1000) {
let canRun = true;

return function() {
if (!canRun) return;
canRun = false;

setTimeout(() => {
fn.apply(this, arguments);
canRun = true;
}, interval);
};
}

实现对字符串进行金额格式化的功能函数

显示类似如下的字符串格式转换功能。

1
2
let num = 52545455454;
num.toLocaleString(); // 52,545,455,454

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function toPriceRight(str, gap, type) {
return String(str)
.split("")
.reduce((init, v, i) => {
if (i % gap == gap - 1) {
init += type + v;
} else {
init += v;
}
return init;
}, "");
}

console.log(toPriceRight("52545455454", 3, ",")); // 52,545,455,454

对指定数组生成树形结构数据

题目:对于如下数据,pid 表示 父节点的值,如果没有 pid 则表示该对象对象为根节点,请将其格式化为树形数据结构,要求可以达到无限深度。

1
2
3
4
5
6
7
8
9
10
11
let origin = [
{ id: 1 },
{ id: 2 },
{ id: 3 },
{ id: 11, pid: 1 },
{ id: 12, pid: 1 },
{ id: 111, pid: 11 },
{ id: 112, pid: 11 },
{ id: 21, pid: 2 },
{ id: 31, pid: 3 }
];

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function tree(arr, ori) {
if (arr.length === 0) arr = ori.filter(v => !("pid" in v));

arr.forEach(v => {
let childs = ori.filter(item => item.pid === v.id);

if (childs.length > 0) {
v.children = tree(childs, ori);
}
});

return arr;
}

console.log(tree([], origin));

实现倒计时功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const endtime = +new Date("2019/12/15 0:0:0"); //定义结束时间日期

function daojishi() {
let nowtime = +new Date(), //获取当前的时间日期
leftime = (endtime - nowtime) / 1000, //获取剩余的秒数
d = parseInt(leftime / 60 / 60 / 24), //获取剩余天数
h = parseInt((leftime / 60 / 60) % 60), //获取剩余小时数
m = parseInt((leftime / 60) % 60), //获取剩余分钟数
s = parseInt(leftime % 60); //获取剩余秒数

console.log(`距离结束还有:${d}${h} 小时 ${m} 分钟 ${s}秒`);

if (nowtime <= endtime) setTimeout(daojishi, 1000); //递归循环刷新时间
}

daojishi();

给定一个数组,对里面所有的奇数求和

1
2
3
4
5
6
7
8
let arr = ["1", "2", "3", 6, 4, -99, -101];

let res = arr.reduce((init, v) => {
if (v % 2 !== 0) init += Number.parseFloat(v);
return init;
}, 0);

console.log(res); // -196

函数柯里化

题目:完成 bindLeft 实现函数参数的部分绑定功能。

1
2
3
4
function bindLeft() {
// 完成这里的代码
// 绑定参数个数以传进来的为准
}

使用方法如下:

1
2
3
let fn1 = (a, b, c, d) => a - b * c + d;
let fn2 = bindLeft(fn1, 1, 2); // 绑定参数 a = 1, b = 2
console.log(fn2(3, 4)); // 1 - 2 * 3 + 4 输出 -1

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let fn1 = (a, b, c, d) => a - b * c + d;

function bindLeft(fn) {
let args = [...arguments].slice(1);

return function() {
return fn.apply(this, [...args, ...arguments]);
};
}

let fn2 = bindLeft(fn1, 1, 2);

console.log(fn2(3, 4));

console.log(1 - 2 * 3 + 4);

使用冒泡排序对数组中所有正整数排序(非正整数位置保持不变)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
let arr = [11, -1, 6, 5, -4, -7, 9, 8];

function bubbleSort(arr) {
let len = arr.length;
let ori = arr.reduce((init, v, i) => {
if (v < 0) {
init.push({ value: v, index: i });
}
return init;
}, []);

for (let i = 0; i < len; i++) {
for (let j = 0; j < len - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}

let index = arr.findIndex(v => v >= 0),
right = arr.slice(index);

ori.forEach(v => {
right.splice(v.index, 0, v.value);
});

return right;
}

console.log(bubbleSort(arr)); // [5, -1, 6, 8, -4, -7, 9, 11]

已知一个对象 obj ,在不知道第一个属性键名的情况下,如何获取第一个属性的值

1
2
3
let obj = { a: 1, b: 2 };

console.log(Object.values(obj)[0]);

将数组去除重复项并按降序排列

1
2
3
4
5
6
7
8
9
10
11
12
let arr = [2, 0, 1, 8, 0, 2, 1, 5];

let res = arr
.sort((a, b) => b - a)
.reduce((init, v) => {
if (init.length === 0 || init[init.length - 1] !== v) {
init.push(v);
}
return init;
}, []);

console.log(res); // [8, 5, 2, 1, 0]

看题作答

  1. 请写出下面程序的打印结果
1
2
3
4
5
6
7
8
9
10
var p = new Promise((resolve, reject) => {
console.log("a");
resolve();
});

setTimeout(() => console.log("d"), 0);

p.then(() => console.log("c"));

console.log("b");

对于以上这类型的题,看到 PromsiesetTimeout 这样的字眼,就知道肯定考察的是与 js 的运行机制 事件循环 相关的。

ok,我们捋一下上面代码的执行过程:

首先,new Promise() 构造函数被执行,console.log('a') 被率先执行。打印出 a

然后,遇到 setTimeout() 函数,其被执行后,回调函数被推入 宏任务 队列中,等待下一轮事件循环时执行。

紧接着,p.then() 被执行,其中的回调函数被推入 微任务 队列中,等到这一轮事件循环的执行栈为空时,再清空 微任务 队列。

接下来,同步代码 console.log('b') 被执行。打印出 b

此时,执行栈空,清空 微任务 队列,console.log('c') 被压入执行栈中执行。打印出 c

最后,开始第二轮事件循环,console.log('d') 出队,压入执行栈中执行,打印出 d。程序结束。

所以最终的打印结果为:

1
// a b c d
  1. 请写出下面程序的打印结果
1
2
3
4
5
6
7
8
9
10
var x = 0;
function test() {
console.log(this.x);
}
var o = {};
o.x = 1;
o.m = test;

o.m.apply(); // 0
test(); // 0

ok!定眼一看,这是一道有关 this 问题的题目。所以,你的脑中应该迅速回忆起 this 相关的知识点。

该题涉及到 3this 指向情况:

  • 普通函数中的 this

  • 对象方法中的 this

  • apply 改变过的 this

首先,代码中,在全局定义了 3 个变量:变量xtest函数、对象o

第一条执行语句,使用 apply 的方式调用了 o.m() 方法。这里只要使用了 apply 函数,没有指定 thisArg 的话,那么 o.m() 方法中的 this 就指向 window。所以打印出 0

第二条执行语句,test() 被直接调用,其中的 this 指向全局 window,打印出 0。