22道高频JS手写笔试题

前端面试与进阶指南

20W字囊括上百个前端面试题的项目开源了

实现防抖函数(debounce)

防抖函数原理:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

那么与节流函数的区别直接看这个动画实现即可。

手写简化版:

1
2
3
4
5
6
7
8
9
// 防抖函数const debounce = (fn, delay) => {
let timer = null;
return(...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
};

适用场景:

  • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
  • 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似

生存环境请用lodash.debounce

实现节流函数(throttle)

防抖函数原理:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 手写简化版

// 节流函数const throttle = (fn, delay = 500) => {
let flag = true;
return(...args) => {
if (!flag) return;
flag = false;
setTimeout(() => {
fn.apply(this, args);
flag = true;
}, delay);
};
};

适用场景:

  • 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
  • 缩放场景:监控浏览器resize
  • 动画场景:避免短时间内多次触发动画引起性能问题

深克隆(deepclone)

简单版:

1
const newObj = JSON.parse(JSON.stringify(oldObj));

局限性:

  1. 他无法实现对函数 、RegExp等特殊对象的克隆
  2. 会抛弃对象的constructor,所有的构造函数会指向Object
  3. 对象有循环引用,会报错

面试版:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
* deep clone
* @param {[type]} parent object 需要进行克隆的对象
* @return {[type]} 深克隆后的对象
*/const clone = parent => {
// 判断类型const isType = (obj, type) => {
if (typeof obj !== "object") returnfalse;
const typeString = Object.prototype.toString.call(obj);
let flag;
switch (type) {
case"Array":
flag = typeString === "[object Array]";
break;
case"Date":
flag = typeString === "[object Date]";
break;
case"RegExp":
flag = typeString === "[object RegExp]";
break;
default:
flag = false;
}
return flag;
};

// 处理正则const getRegExp = re => {
var flags = "";
if (re.global) flags += "g";
if (re.ignoreCase) flags += "i";
if (re.multiline) flags += "m";
return flags;
};
// 维护两个储存循环引用的数组const parents = [];
const children = [];

const _clone = parent => {
if (parent === null) returnnull;
if (typeof parent !== "object") return parent;

let child, proto;

if (isType(parent, "Array")) {
// 对数组做特殊处理
child = [];
} elseif (isType(parent, "RegExp")) {
// 对正则对象做特殊处理
child = newRegExp(parent.source, getRegExp(parent));
if (parent.lastIndex) child.lastIndex = parent.lastIndex;
} elseif (isType(parent, "Date")) {
// 对Date对象做特殊处理
child = newDate(parent.getTime());
} else {
// 处理对象原型
proto = Object.getPrototypeOf(parent);
// 利用Object.create切断原型链
child = Object.create(proto);
}

// 处理循环引用const index = parents.indexOf(parent);

if (index != -1) {
// 如果父数组存在本对象,说明之前已经被引用过,直接返回此对象return children[index];
}
parents.push(parent);
children.push(child);

for (let i in parent) {
// 递归
child[i] = _clone(parent[i]);
}

return child;
};
return _clone(parent);
};

局限性:

  1. 一些特殊情况没有处理: 例如Buffer对象、Promise、Set、Map
  2. 另外对于确保没有循环引用的对象,我们可以省去对循环引用的特殊处理,因为这很消耗时间

原理详解实现深克隆

实现Event(event bus)

event bus既是node中各个模块的基石,又是前端组件通信的依赖手段之一,同时涉及了订阅-发布设计模式,是非常重要的基础。

简单版:

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
classEventEmeitter{
constructor() {
this._events = this._events || newMap(); // 储存事件/回调键值对this._maxListeners = this._maxListeners || 10; // 设立监听上限
}
}


// 触发名为type的事件
EventEmeitter.prototype.emit = function(type, ...args) {
let handler;
// 从储存事件键值对的this._events中获取对应事件回调函数
handler = this._events.get(type);
if (args.length > 0) {
handler.apply(this, args);
} else {
handler.call(this);
}
returntrue;
};

// 监听名为type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
// 将type事件以及对应的fn函数放入this._events中储存if (!this._events.get(type)) {
this._events.set(type, fn);
}
};

面试版:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
classEventEmeitter{
constructor() {
this._events = this._events || newMap(); // 储存事件/回调键值对this._maxListeners = this._maxListeners || 10; // 设立监听上限
}
}

// 触发名为type的事件
EventEmeitter.prototype.emit = function(type, ...args) {
let handler;
// 从储存事件键值对的this._events中获取对应事件回调函数
handler = this._events.get(type);
if (args.length > 0) {
handler.apply(this, args);
} else {
handler.call(this);
}
returntrue;
};

// 监听名为type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
// 将type事件以及对应的fn函数放入this._events中储存if (!this._events.get(type)) {
this._events.set(type, fn);
}
};

// 触发名为type的事件
EventEmeitter.prototype.emit = function(type, ...args) {
let handler;
handler = this._events.get(type);
if (Array.isArray(handler)) {
// 如果是一个数组说明有多个监听者,需要依次此触发里面的函数for (let i = 0; i < handler.length; i++) {
if (args.length > 0) {
handler[i].apply(this, args);
} else {
handler[i].call(this);
}
}
} else {
// 单个函数的情况我们直接触发即可if (args.length > 0) {
handler.apply(this, args);
} else {
handler.call(this);
}
}

returntrue;
};

// 监听名为type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
const handler = this._events.get(type); // 获取对应事件名称的函数清单if (!handler) {
this._events.set(type, fn);
} elseif (handler && typeof handler === "function") {
// 如果handler是函数说明只有一个监听者this._events.set(type, [handler, fn]); // 多个监听者我们需要用数组储存
} else {
handler.push(fn); // 已经有多个监听者,那么直接往数组里push函数即可
}
};

EventEmeitter.prototype.removeListener = function(type, fn) {
const handler = this._events.get(type); // 获取对应事件名称的函数清单// 如果是函数,说明只被监听了一次if (handler && typeof handler === "function") {
this._events.delete(type, fn);
} else {
let postion;
// 如果handler是数组,说明被监听多次要找到对应的函数for (let i = 0; i < handler.length; i++) {
if (handler[i] === fn) {
postion = i;
} else {
postion = -1;
}
}
// 如果找到匹配的函数,从数组中清除if (postion !== -1) {
// 找到数组对应的位置,直接清除此回调
handler.splice(postion, 1);
// 如果清除后只有一个函数,那么取消数组,以函数形式保存if (handler.length === 1) {
this._events.set(type, handler[0]);
}
} else {
returnthis;
}
}
};

实现具体过程和思路见实现event

实现instanceOf

1
2
3
4
5
6
7
8
9
10
11
// 模拟 instanceof 
function instance_of(L, R) {
//L 表示左表达式,R 表示右表达式
var O = R.prototype; // 取 R 的显示原型
L = L.__proto__; // 取 L 的隐式原型while (true) {
if (L === null) returnfalse;
if (O === L)
// 这里重点:当 O 严格等于 L 时,返回 truereturntrue;
L = L.__proto__;
}
}

模拟new

new操作符做了这些事:

  • 它创建了一个全新的对象
  • 它会被执行[[Prototype]](也就是proto)链接
  • 它使this指向新创建的对象
  • 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上
  • 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用将返回该对象引用
1
2
3
4
5
6
7
8
9
10
11
// objectFactory(name, 'cxk', '18')
function objectFactory() {
const obj = newObject();
const Constructor = [].shift.call(arguments);

obj.__proto__ = Constructor.prototype;

const ret = Constructor.apply(obj, arguments);

return typeof ret === "object" ? ret : obj;
}

实现一个call

call做了什么:

  • 将函数设为对象的属性
  • 执行&删除这个函数
  • 指定this到函数并传入给定参数执行函数
  • 如果不传入参数,默认指向为 window
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 模拟 call bar.mycall(null);
//实现一个call方法:
Function.prototype.myCall = function(context) {
//此处没有考虑context非object情况
context.fn = this;
let args = [];
for (let i = 1, len = arguments.length; i < len; i++) {
args.push(arguments[i]);
}
context.fn(...args);
let result = context.fn(...args);
delete context.fn;
return result;
};

具体实现参考JavaScript深入之call和apply的模拟实现

实现apply方法

apply原理与call很相似,不多赘述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 模拟 apply
Function.prototype.myapply = function(context, arr) {
var context = Object(context) || window;
context.fn = this;

var result;
if (!arr) {
result = context.fn();
} else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push("arr[" + i + "]");
}
result = eval("context.fn(" + args + ")");
}

delete context.fn;
return result;
};

实现bind

实现bind要做什么

  • 返回一个函数,绑定this,传递预置参数
  • bind返回的函数可以作为构造函数使用。故作为构造函数时应使得this失效,但是传入的参数依然有效
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
// mdn的实现
if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
if (typeofthis !== 'function') {
// closest thing possible to the ECMAScript 5// internal IsCallable functionthrownewTypeError('Function.prototype.bind - what is trying to be bound is not callable');
}

var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function() {},
fBound = function() {
// this instanceof fBound === true时,说明返回的fBound被当做new的构造函数调用return fToBind.apply(thisinstanceof fBound
? this
: oThis,
// 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的
aArgs.concat(Array.prototype.slice.call(arguments)));
};

// 维护原型关系if (this.prototype) {
// Function.prototype doesn't have a prototype property
fNOP.prototype = this.prototype;
}
// 下行的代码使fBound.prototype是fNOP的实例,因此// 返回的fBound若作为new的构造函数,new生成的新对象作为this传入fBound,新对象的__proto__就是fNOP的实例
fBound.prototype = new fNOP();

return fBound;
};
}

详解请移步JavaScript深入之bind的模拟实现 #12

模拟Object.create

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的proto

1
2
3
4
5
6
7
// 模拟 Object.create
function create(proto) {
functionF() {}
F.prototype = proto;

return new F();
}

实现类的继承

类的继承在几年前是重点内容,有n种继承方式各有优劣,es6普及后越来越不重要,那么多种写法有点『回字有四样写法』的意思,如果还想深入理解的去看红宝书即可,我们目前只实现一种最理想的继承方式。

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
function Parent(name) {
this.parent = name
}
Parent.prototype.say = function() {
console.log(`${this.parent}: 你打篮球的样子像kunkun`)
}
functionChild(name, parent) {
// 将父类的构造函数绑定在子类上
Parent.call(this, parent)
this.child = name
}

/**
1. 这一步不用Child.prototype =Parent.prototype的原因是怕共享内存,修改父类原型对象就会影响子类
2. 不用Child.prototype = new Parent()的原因是会调用2次父类的构造方法(另一次是call),会存在一份多余的父类实例属性
3. Object.create是创建了父类原型的副本,与父类原型完全隔离
*/

Child.prototype = Object.create(Parent.prototype);
Child.prototype.say = function() {
console.log(`${this.parent}好,我是练习时长两年半的${this.child}`);
}

// 注意记得把子类的构造指向子类本身
Child.prototype.constructor = Child;

var parent = new Parent('father');
parent.say() // father: 你打篮球的样子像kunkunvar child = new Child('cxk', 'father');
child.say() // father好,我是练习时长两年半的cxk

实现JSON.parse

1
2
var json = '{"name":"cxk", "age":25}';
var obj = eval("(" + json + ")");

此方法属于黑魔法,极易容易被xss攻击,还有一种new Function大同小异。

简单的教程看这个半小时实现一个 JSON 解析器

实现Promise

我很早之前实现过一版,而且注释很多,但是居然找不到了,这是在网络上找了一版带注释的,目测没有大问题,具体过程可以看这篇史上最易读懂的 Promise/A+ 完全实现

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
var PromisePolyfill = (function () {
// 和reject不同的是resolve需要尝试展开thenable对象functiontryToResolve (value) {
if (this === value) {
// 主要是防止下面这种情况// let y = new Promise(res => setTimeout(res(y)))throwTypeError('Chaining cycle detected for promise!')
}

// 根据规范2.32以及2.33 对对象或者函数尝试展开// 保证S6之前的 polyfill 也能和ES6的原生promise混用if (value !== null &&
(typeof value === 'object' || typeof value === 'function')) {
try {
// 这里记录这次then的值同时要被try包裹// 主要原因是 then 可能是一个getter, 也也就是说// 1. value.then可能报错// 2. value.then可能产生副作用(例如多次执行可能结果不同)var then = value.then

// 另一方面, 由于无法保证 then 确实会像预期的那样只调用一个onFullfilled / onRejected// 所以增加了一个flag来防止resolveOrReject被多次调用var thenAlreadyCalledOrThrow = falseif (typeof then === 'function') {
// 是thenable 那么尝试展开// 并且在该thenable状态改变之前this对象的状态不变
then.bind(value)(
// onFullfilledfunction (value2) {
if (thenAlreadyCalledOrThrow) return
thenAlreadyCalledOrThrow = true
tryToResolve.bind(this, value2)()
}.bind(this),

// onRejectedfunction (reason2) {
if (thenAlreadyCalledOrThrow) return
thenAlreadyCalledOrThrow = true
resolveOrReject.bind(this, 'rejected', reason2)()
}.bind(this)
)
} else {
// 拥有then 但是then不是一个函数 所以也不是thenable
resolveOrReject.bind(this, 'resolved', value)()
}
} catch (e) {
if (thenAlreadyCalledOrThrow) return
thenAlreadyCalledOrThrow = true
resolveOrReject.bind(this, 'rejected', e)()
}
} else {
// 基本类型 直接返回
resolveOrReject.bind(this, 'resolved', value)()
}
}

functionresolveOrReject (status, data) {
if (this.status !== 'pending') returnthis.status = status
this.data = data
if (status === 'resolved') {
for (var i = 0; i < this.resolveList.length; ++i) {
this.resolveList[i]()
}
} else {
for (i = 0; i < this.rejectList.length; ++i) {
this.rejectList[i]()
}
}
}

functionPromise (executor) {
if (!(thisinstanceofPromise)) {
throwError('Promise can not be called without new !')
}

if (typeof executor !== 'function') {
// 非标准 但与Chrome谷歌保持一致throwTypeError('Promise resolver ' + executor + ' is not a function')
}

this.status = 'pending'this.resolveList = []
this.rejectList = []

try {
executor(tryToResolve.bind(this), resolveOrReject.bind(this, 'rejected'))
} catch (e) {
resolveOrReject.bind(this, 'rejected', e)()
}
}

Promise.prototype.then = function (onFullfilled, onRejected) {
// 返回值穿透以及错误穿透, 注意错误穿透用的是throw而不是return,否则的话// 这个then返回的promise状态将变成resolved即接下来的then中的onFullfilled// 会被调用, 然而我们想要调用的是onRejectedif (typeof onFullfilled !== 'function') {
onFullfilled = function (data) {
return data
}
}
if (typeof onRejected !== 'function') {
onRejected = function (reason) {
throw reason
}
}

var executor = function (resolve, reject) {
setTimeout(function () {
try {
// 拿到对应的handle函数处理this.data// 并以此为依据解析这个新的Promisevar value = this.status === 'resolved'
? onFullfilled(this.data)
: onRejected(this.data)
resolve(value)
} catch (e) {
reject(e)
}
}.bind(this))
}

// then 接受两个函数返回一个新的Promise// then 自身的执行永远异步与onFullfilled/onRejected的执行if (this.status !== 'pending') {
return new Promise(executor.bind(this))
} else {
// pendingreturnnewPromise(function (resolve, reject) {
this.resolveList.push(executor.bind(this, resolve, reject))
this.rejectList.push(executor.bind(this, resolve, reject))
}.bind(this))
}
}

// for prmise A+ testPromise.deferred = Promise.defer = function () {
var dfd = {}
dfd.promise = newPromise(function (resolve, reject) {
dfd.resolve = resolve
dfd.reject = reject
})
return dfd
}

// for prmise A+ testif (typeofmodule !== 'undefined') {
module.exports = Promise
}

returnPromise
})()

PromisePolyfill.all = function (promises) {
returnnewPromise((resolve, reject) => {
const result = []
let cnt = 0for (let i = 0; i < promises.length; ++i) {
promises[i].then(value => {
cnt++
result[i] = value
if (cnt === promises.length) resolve(result)
}, reject)
}
})
}

PromisePolyfill.race = function (promises) {
returnnewPromise((resolve, reject) => {
for (let i = 0; i < promises.length; ++i) {
promises[i].then(resolve, reject)
}
})
}

解析 URL Params 为对象

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
let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
parseParam(url)
/* 结果
{ user: 'anonymous',
id: [ 123, 456 ], // 重复出现的 key 要组装成数组,能被转成数字的就转成数字类型
city: '北京', // 中文需解码
enabled: true, // 未指定值得 key 约定为 true
}
*/

functionparseParam(url) {
const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来const paramsArr = paramsStr.split('&'); // 将字符串以 & 分割后存到数组中let paramsObj = {};
// 将 params 存到对象中
paramsArr.forEach(param => {
if (/=/.test(param)) { // 处理有 value 的参数let [key, val] = param.split('='); // 分割 key 和 value
val = decodeURIComponent(val); // 解码
val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则添加一个值
paramsObj[key] = [].concat(paramsObj[key], val);
} else { // 如果对象没有这个 key,创建 key 并设置值
paramsObj[key] = val;
}
} else { // 处理没有 value 的参数
paramsObj[param] = true;
}
})

return paramsObj;
}

模板引擎实现

1
2
3
4
5
6
7
8
9
10
11
12
13
let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let data = {
name: '姓名',
age: 18
}
render(template, data); // 我是姓名,年龄18,性别undefined

functionrender(template, data) {
const reg = /\{\{(\w+)\}\}/; // 模板字符串正则if (reg.test(template)) { // 判断模板里是否有模板字符串const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段
template = template.replace(reg, data[name]); // 将第一个模板字符串渲染return render(template, data); // 递归的渲染并返回渲染后的结构
}
return template; // 如果模板没有模板字符串直接返回
}

转化为驼峰命名

1
2
3
4
5
6
7
var s1 = "get-element-by-id"// 转化为 getElementById

var f = function(s) {
return s.replace(/-\w/g, function(x) {
return x.slice(1).toUpperCase();
})
}

查找字符串中出现最多的字符和个数

例: abbcccddddd -> 字符最多的是d,出现了5次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let str = "abcabcabcbbccccc";
let num = 0;
let char = '';

// 使其按照一定的次序排列
str = str.split('').sort().join('');
// "aaabbbbbcccccccc"// 定义正则表达式let re = /(\w)\1+/g;
str.replace(re,($0,$1) => {
if(num < $0.length){
num = $0.length;
char = $1;
}
});
console.log(`字符最多的是${char},出现了${num}次`);

字符串查找

请使用最基本的遍历来实现判断字符串 a 是否被包含在字符串 b 中,并返回第一次出现的位置(找不到返回 -1)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
a='34';b='1234567'; // 返回 2
a='35';b='1234567'; // 返回 -1
a='355';b='12354355'; // 返回 5
isContain(a,b);



functionisContain(a, b) {
for (let i in b) {
if (a[0] === b[i]) {
let tmp = true;
for (let j in a) {
if (a[j] !== b[~~i + ~~j]) {
tmp = false;
}
}
if (tmp) {
return i;
}
}
}
return-1;
}

实现千位分隔符

1
2
3
4
5
6
7
8
9
10
11
// 保留三位小数
parseToMoney(1234.56); // return '1,234.56'
parseToMoney(123456789); // return '123,456,789'
parseToMoney(1087654.321); // return '1,087,654.321'

functionparseToMoney(num) {
num = parseFloat(num.toFixed(3));
let [integer, decimal] = String.prototype.split.call(num, '.');
integer = integer.replace(/\d(?=(\d{3})+$)/g, '$&,');
return integer + '.' + (decimal ? decimal : '');
}

正则表达式(运用了正则的前向声明和反前向声明):

1
2
3
4
5
function parseToMoney(str){
// 仅仅对位置进行匹配
let re = /(?=(?!\b)(\d{3})+$)/g;
return str.replace(re,',');
}

判断是否是电话号码

1
2
3
4
function isPhone(tel) {
var regx = /^1[34578]\d{9}$/;
return regx.test(tel);
}

验证是否是邮箱

1
2
3
4
function isEmail(email) {
var regx = /^([a-zA-Z0-9_\-])+@([a-zA-Z0-9_\-])+(\.[a-zA-Z0-9_\-])+$/;
return regx.test(email);
}

验证是否是身份证

1
2
3
4
5
    function isCardNo(number) {
var regx = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
return regx.test(number);
}
`