常考的 JavaScript 手写题
XPoet 自由程序猿

现在的前端招聘圈,对前端程序员的能力要求真的是越来越高了(内卷真的太厉害了),出去找工作面试,哪怕是家小公司,大概率都会考一些 JS 手写题,你不会但别人会,自然就被卷死了。

其实,对一个前端工程师来说,JS 本就重中之重,我们要掌握的可不仅仅是这些手写题。

当然,你不会这些手写题也不能代表你的 JS 编程能力不好,我觉得那是因为你不熟悉而已。

现在就跟着作者来一步一步学习,彻底搞懂这些常考的 JS 手写题,无论是在业务开发还是求职面试中都很实用。

image

截止目前,市面上超过 98% 的浏览器支持 ES6,ES6 (ECMAScript 2015) 都已经是 2015 年的标准了,所以这些手写题里面能使用 ES6 实现的均采用 ES6 实现。以下的每一道手写题,都经过作者反复检验,也配备了测试代码,小伙伴放心大胆学起来。

如发现错误或者有更好的实现方式,欢迎大家指正。

代码仓库:https://github.com/XPoet/handwriting-js

手写类型判断

1
2
3
4
5
6
7
8
9
10
11
12
const myTypeOf = (data) => Object.prototype.toString.call(data).slice(8, -1).toLowerCase()

// 测试
console.log(myTypeOf(1)) //--> number
console.log(myTypeOf('1')) //--> string
console.log(myTypeOf(true)) //--> boolean
console.log(myTypeOf([])) //--> array
console.log(myTypeOf({})) //--> object
console.log(myTypeOf(/^/)) //--> regexp
console.log(myTypeOf(new Date())) //--> date
console.log(myTypeOf(Math)) //--> math
console.log(myTypeOf(() => {})) //--> function

手写数组去重

1
2
3
4
const myUnique = array => [...new Set(array)]

// 测试
console.log(myUnique([1, 1, 2, 3])) //--> [1, 2, 3]

手写 Ajax 的 GET 方法

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
function myAjaxGet(url) {
// 创建一个 Promise 对象
return new Promise(function(resolve, reject) {
const xhr = new XMLHttpRequest()

// 新建一个 http 请求
xhr.open('GET', url, true)

// 设置响应的数据类型
xhr.responseType = 'json'

// 设置请求头信息
xhr.setRequestHeader('Accept', 'application/json')

// 设置状态的监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return

// 当请求成功或失败时,改变 Promise 的状态
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}

// 设置错误监听函数
xhr.onerror = function() {
reject(new Error(this.statusText))
}

// 发送 http 请求
xhr.send(null)
})
}

// 测试
myAjaxGet('https://api.github.com/users/XPoet').then(res => {
console.log('res: ', res) //--> {...}
})

手写函数节流

定义:规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。

使用场景:窗口 resize、scroll、输入框 input、频繁点击等

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
/**
* @param fn 需要执行的函数
* @param delay 间隔时间
*/
const throttle = (fn, delay = 1000) => {
// 上一次执行 fn 的时间
let prevTime = 0

// 将 throttle 处理结果当作函数返回
return function(...args) {
// 获取当前时间,转换成时间戳,单位毫秒 ms
const nowTime = Date.now()
// 将当前时间和上一次执行函数的时间进行对比
// 大于间隔时间就把 prevTime 设置为当前时间并执行函数 fn
if (nowTime - prevTime > delay) {
prevTime = nowTime
fn.apply(this, args)
}
}
}

// 测试
const testFn = throttle(() => {
console.log('函数节流测试 - fn 执行了')
}, 1000)

// 定时器每 100 毫秒执行一次 testFn 函数,但是只有间隔时间差大于 1000 毫秒时才会执行 fn
setInterval(testFn, 100) //--> 函数节流测试 - fn 执行了

手写函数防抖

定义:在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。
使用场景:搜索框输入搜索、点击提交等

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 debounce(fn, wait) {
let timer = null

return function(...args) {
const ctx = this

// 如果此时存在定时器的话,则取消之前的定时器重新记时
if (timer) {
clearTimeout(timer)
timer = null
}

// 设置定时器,使事件间隔指定事件后执行
timer = setTimeout(() => {
fn.apply(ctx, args)
}, wait)
}
}

// 测试
const testFn = debounce(() => {
console.log('函数防抖测试 - fn 执行了')
}, 2000)

// 定时器每 1000 毫秒执行一次 testFn 函数,等待时间未大于 2000 毫秒,所以 fn 永远不会执行
setInterval(testFn, 1000)

// 定时器每 3000 毫秒执行一次 testFn 函数,等待时间大于 2000 毫秒,所以 fn 会隔 3000 执行一次
setInterval(testFn, 3000) //--> 函数防抖测试 - 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
function shallowCopy(object) {
// 只拷贝对象类型的数据
if (!object || typeof object !== 'object') return

// object 如果是数组类型就新建一个空数组,否则新建空对象
const newObject = Array.isArray(object) ? [] : {}

// 遍历 object,进行属性拷贝
for (const key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] = object[key]
}
}

return newObject
}

// 测试
const obj1 = { x: 1, y: 2, z: 3 }
const obj2 = shallowCopy(obj1)
console.log(obj2) //--> { x: 1, y: 2, z: 3 }

const arr1 = [1, 2, 3]
const arr2 = shallowCopy(arr1)
console.log(arr2) //--> [1, 2, 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
function deepCopy(object) {
// 只拷贝对象类型的数据
if (!object || typeof object !== 'object') return

// object 如果是数组类型就新建一个空数组,否则新建空对象
const newObject = Array.isArray(object) ? [] : {}

for (const key in object) {
if (object.hasOwnProperty(key)) {
// object[key] 如果是对象类型,则使用递归继续遍历拷贝属性
newObject[key] = typeof object[key] === 'object' ? deepCopy(object[key]) : object[key]
}
}

return newObject
}

// 测试
const obj1 = { x: 1, y: { z: 3 } }
const obj2 = deepCopy(obj1)
console.log(obj2) //--> { x: 1, y: { z: 3 } }

const arr1 = [1, [2, 3]]
const arr2 = deepCopy(arr1)
console.log(arr2) //--> [1, [2, 3]]

手写 call、apply 和 bind 函数

在 JavaScript 中,callapplybindFunction 对象自带的三个方法,这三个方法的主要作用是改变函数中的 this 指向。

共同点:

  • applycallbind 三者都是用来改变函数的 this 对象指向。
  • applycallbind 三者第一个参数都是 this 要指向的对象,也就是想指定的上下文。(函数的每次调用都会拥有一个特殊值——本次调用的上下文(context),这就是 this 关键字的值。)
  • applycallbind 三者都可以利用后续参数传参。

区别:

  • bind 是返回对应函数,便于稍后调用。
  • applycall 则是立即调用。

call()apply() 的作用是一样的,都是用于改变 this 的指向,区别在于 call 接受多个参数,而 apply 接受的是一个数组。

第一个参数的取值有以下 4 种情况:

  1. 不传,或者传 nullundefined,函数中的 this 指向 window 对象。
  2. 传递另一个函数的函数名,函数中的 this 指向这个函数的引用。
  3. 传递字符串、数值或布尔类型等基础类型,函数中的 this 指向其对应的包装对象,如 StringNumberBoolean
  4. 传递一个对象,函数中的 this 指向这个对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function a() {
console.log(this)
}

function b() {}

const c = { x: 1 }

a.call() //--> window
a.call(null) //--> window
a.call(undefined) //window
a.call(1) //--> Number {1}
a.call('') //--> String {''}
a.call(true) //--> Boolean {true}
a.call(b) //--> function b(){}
a.call(c) //--> {x: 1}

手写 call

call 函数的实现步骤:

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window
  3. 将函数作为上下文对象的一个属性。
  4. 使用上下文对象来调用这个方法,并保存返回结果。
  5. 删除刚才新增的属性。
  6. 返回结果。
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.prototype.myCall = function(ctx, ...args) {
// 判断调用对象
if (typeof this !== 'function') {
throw new TypeError('Type Error')
}

// 判断 ctx 是否传入,如果未传入则设置为 window
ctx = ctx || window

// 将调用函数设为对象的方法
ctx.fn = this

// 调用函数
const result = ctx.fn(...args)

// 将属性删除
delete ctx.fn

return result
}


// 测试
const obj = {
test(a, b, c) {
console.log(this, a, b)
}
}
obj.test.myCall(obj, 4, 5) //--> {test: ƒ, fn: ƒ} 4 5

手写 apply

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window
  3. 将函数作为上下文对象的一个属性。
  4. 判断参数值是否传入。
  5. 使用上下文对象来调用这个方法,并保存返回结果。
  6. 删除刚才新增的属性。
  7. 返回结果。
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
Function.prototype.myApply = function(ctx) {
// 判断调用对象是否为函数
if (typeof this !== 'function') {
throw new TypeError('Type Error')
}

let result = null

// 判断 ctx 是否存在,如果未传入则为 window
ctx = ctx || window

// 将函数设为对象的方法
ctx.fn = this

// 调用方法
if (arguments[1]) {
result = ctx.fn(...arguments[1])
} else {
result = ctx.fn()
}

// 将属性删除
delete ctx.fn

return result
}

// 测试
const obj = {
test(a, b, c) {
console.log(this, a, b, c)
}
}
obj.test.myApply(obj, [4, 5, 6]) //--> {test: ƒ, fn: ƒ} 4 5 6

手写 bind

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  2. 保存当前函数的引用,获取其余传入参数值。
  3. 创建一个函数返回。
  4. 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 thisapply 调用,其余情况都传入指定的上下文对象。
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.myBind = function(ctx, ...args) {
// 判断调用对象是否为函数
if (typeof this !== 'function') {
throw new TypeError('Type Error')
}

const fn = this

return function Fn() {
// 根据调用方式,传入不同绑定值
return fn.apply(
this instanceof Fn ? this : ctx, args.concat(...arguments)
)
}
}

// 测试
const obj = {
test(a, b, c) {
console.log(this, a + b)
}
}
obj.test.myBind(obj, 4, 5)() //--> {test: ƒ} 9

函数柯里化

函数柯里化指的是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。例如:add(1, 2, 3, 4, 5) 转换成 add(1)(2)(3)(4)(5)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function curry(fn, ...args) {
return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args)
}

// 测试
// 普通函数
function fn(a, b, c, d, e) {
console.log(a, b, c, d, e)
}

// 生成的柯里化函数
const _fn = curry(fn)

_fn(1, 2, 3, 4, 5) //--> 1 2 3 4 5
_fn(1)(2)(3, 4, 5) //--> 1 2 3 4 5
_fn(1, 2)(3, 4)(5) //--> 1 2 3 4 5
_fn(1)(2)(3)(4)(5) //--> 1 2 3 4 5

看起来柯里化好像是把简答的问题复杂化了,但是复杂化的同时,我们在使用函数时拥有了更加多的自由度。而这里对于函数参数的自由处理,正是柯里化的核心所在。柯里化本质上是降低通用性,提高适用性。

手写 EventBus

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
/**
* 手写 EventBus
*/

class EventBus {
constructor() {
// 存储事件及其对应的回调函数
this.events = {}
}

// 订阅事件
subscribe(eventName, callback) {
// 如果事件不存在,则创建一个新的事件数组
if (!this.events[eventName]) {
this.events[eventName] = []
}
// 将回调函数添加到事件数组中
this.events[eventName].push(callback)
}

// 取消订阅事件
unsubscribe(eventName, callback) {
// 如果事件不存在,则直接返回
if (!this.events[eventName]) {
return
}
// 从事件数组中移除指定的回调函数
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback)
}

// 发布事件
next(eventName, data) {
// 如果事件不存在,则直接返回
if (!this.events[eventName]) {
return
}
// 遍历事件数组,依次执行回调函数
this.events[eventName].forEach(callback => {
callback(data)
})
}
}

// 创建一个新的 EventBus 实例
const eventBus = new EventBus()

// 定义事件处理函数
const handler1 = data => {
console.log('Handler 1:', data)
}

const handler2 = data => {
console.log('Handler 2:', data)
}

// 订阅事件
eventBus.subscribe('event1', handler1)
eventBus.subscribe('event1', handler2)

// 发布事件
eventBus.next('event1', 'Hello, EventBus!')

// 取消订阅事件
eventBus.unsubscribe('event1', handler2)

手写 Promise

Promise 是异步编程的一种解决方案,比传统的解决方案回调函数和事件更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 Promise 对象。

所谓 Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

下面我们用 ES6 语法来手写一个 Promise:

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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
class MyPromise {

// Promise/A+ 规范规定的三种状态
PENDING = 'pending' // 等待状态
FULFILLED = 'fulfilled'// 成功状态
REJECTED = 'rejected' // 失败状态

// 构造函数接收一个执行回调
constructor(executor) {
this._status = this.PENDING // Promise 初始状态
this._value = undefined // then 回调的值
this._resolveQueue = [] // resolve 时触发的成功队列
this._rejectQueue = [] // reject 时触发的失败队列

// 使用箭头函数固定 this(resolve 函数在 executor 中触发,不然找不到 this)
const resolve = value => {
const run = () => {
// Promise/A+ 规范规定的 Promise 状态只能从 pending 触发,变成 fulfilled
if (this._status === this.PENDING) {
this._status = this.FULFILLED // 更改状态
this._value = value // 储存当前值,用于 then 回调

// 执行 resolve 回调
while (this._resolveQueue.length) {
const callback = this._resolveQueue.shift()
callback(value)
}
}
}
// 把 resolve 执行回调的操作封装成一个函数,放进 setTimeout 里,以实现 Promise 异步调用的特性(规范上是微任务,这里是宏任务)
setTimeout(run)
}

// 同 resolve
const reject = value => {
const run = () => {
if (this._status === this.PENDING) {
this._status = this.REJECTED
this._value = value

while (this._rejectQueue.length) {
const callback = this._rejectQueue.shift()
callback(value)
}
}
}
setTimeout(run)
}

// new Promise() 时立即执行 executor,并传入 resolve 和 reject
executor(resolve, reject)
}

// then 方法,接收一个成功的回调和一个失败的回调
then(onFulfilled, onRejected) {
// 根据规范,如果 then 的参数不是 function,则忽略它,让值继续往下传递,链式调用继续往下执行
typeof onFulfilled !== 'function' ? onFulfilled = value => value : null
typeof onRejected !== 'function' ? onRejected = error => error : null

// then 返回一个新的 Promise
return new MyPromise((resolve, reject) => {
const resolveFn = value => {
try {
const x = onFulfilled(value)
// 分类讨论返回值,如果是 Promise,那么等待 Promise 状态变更,否则直接 resolve
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}

const rejectFn = error => {
try {
const x = onRejected(error)
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}

switch (this._status) {
case this.PENDING:
this._resolveQueue.push(resolveFn)
this._rejectQueue.push(rejectFn)
break
case this.FULFILLED:
resolveFn(this._value)
break
case this.REJECTED:
rejectFn(this._value)
break
}
})
}

catch(onRejected) {
return this.then(null, onRejected)
}

finally(callback) {
return this.then(value => MyPromise.resolve(callback()).then(() => value), error => {
MyPromise.resolve(callback()).then(() => error)
})
}

// 静态 resolve 方法
static resolve(value) {
return value instanceof MyPromise ? value : new MyPromise(resolve => resolve(value))
}

// 静态 reject 方法
static reject(error) {
return new MyPromise((resolve, reject) => reject(error))
}

// 静态 all 方法
static all(promiseArr) {
let count = 0
let result = []
return new MyPromise((resolve, reject) => {
if (!promiseArr.length) {
return resolve(result)
}
promiseArr.forEach((p, i) => {
MyPromise.resolve(p).then(value => {
count++
result[i] = value
if (count === promiseArr.length) {
resolve(result)
}
}, error => {
reject(error)
})
})
})
}

// 静态 race 方法
static race(promiseArr) {
return new MyPromise((resolve, reject) => {
promiseArr.forEach(p => {
MyPromise.resolve(p).then(value => {
resolve(value)
}, error => {
reject(error)
})
})
})
}
}

// 测试
function fn() {
return new MyPromise((resolve, reject) => {
if (Math.random() > 0.5) {
setTimeout(() => {
resolve(`resolve ***`)
}, 500)
} else {
setTimeout(() => {
reject(`reject ***`)
}, 500)
}
})
}

fn().then(res => {
console.log('resolve value: ', res)
}).catch(err => {
console.log('reject value: ', err)
})

手写 JSONP

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
function jsonp(url, params, callback) {
// 判断是否含有参数
let queryString = url.indexOf('?') === '-1' ? '?' : '&';

// 添加参数
for (var k in params) {
if (params.hasOwnProperty(k)) {
queryString += k + '=' + params[k] + '&';
}
}

// 处理回调函数名
let random = Math.random().toString().replace('.', ''),
callbackName = 'myJsonp' + random;

// 添加回调函数
queryString += 'callback=' + callbackName;

// 构建请求
let scriptNode = document.createElement('script');
scriptNode.src = url + queryString;

window[callbackName] = function () {
// 调用回调函数
callback(...arguments);

// 删除这个引入的脚本
document.getElementsByTagName('head')[0].removeChild(scriptNode);
};

// 发起请求
document.getElementsByTagName('head')[0].appendChild(scriptNode);
}

手写观察者模式

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
var events = (function () {
var topics = {};

return {
// 注册监听函数
subscribe: function (topic, handler) {
if (!topics.hasOwnProperty(topic)) {
topics[topic] = [];
}
topics[topic].push(handler);
},

// 发布事件,触发观察者回调事件
publish: function (topic, info) {
if (topics.hasOwnProperty(topic)) {
topics[topic].forEach(function (handler) {
handler(info);
});
}
},

// 移除主题的一个观察者的回调事件
remove: function (topic, handler) {
if (!topics.hasOwnProperty(topic)) return;

var handlerIndex = -1;
topics[topic].forEach(function (item, index) {
if (item === handler) {
handlerIndex = index;
}
});

if (handlerIndex >= 0) {
topics[topic].splice(handlerIndex, 1);
}
},

// 移除主题的所有观察者的回调事件
removeAll: function (topic) {
if (topics.hasOwnProperty(topic)) {
topics[topic] = [];
}
}
};
})();
 Comments
Comment plugin failed to load
Loading comment plugin