惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
B
Blog RSS Feed
宝玉的分享
宝玉的分享
腾讯CDC
博客园_首页
T
Tailwind CSS Blog
月光博客
月光博客
博客园 - 司徒正美
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
M
MIT News - Artificial intelligence
A
About on SuperTechFans
云风的 BLOG
云风的 BLOG
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
有赞技术团队
有赞技术团队
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
大猫的无限游戏
大猫的无限游戏
MongoDB | Blog
MongoDB | Blog
博客园 - 聂微东
V
Visual Studio Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
SecWiki News
SecWiki News
美团技术团队
P
Privacy International News Feed
H
Help Net Security
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Microsoft Security Blog
Microsoft Security Blog
Know Your Adversary
Know Your Adversary
Y
Y Combinator Blog
D
DataBreaches.Net
Project Zero
Project Zero
T
The Blog of Author Tim Ferriss
Cyberwarzone
Cyberwarzone
C
Cybersecurity and Infrastructure Security Agency CISA
C
Cisco Blogs
S
Schneier on Security
G
GRAHAM CLULEY
博客园 - 三生石上(FineUI控件)
Cisco Talos Blog
Cisco Talos Blog
小众软件
小众软件
Forbes - Security
Forbes - Security
D
Docker
T
Tenable Blog
S
Secure Thoughts
雷峰网
雷峰网
S
Security @ Cisco Blogs
T
The Exploit Database - CXSecurity.com
The Cloudflare Blog
博客园 - 【当耐特】
Spread Privacy
Spread Privacy
阮一峰的网络日志
阮一峰的网络日志

XPoet's Blog

Codex 常用命令 | XPoet's Blog Claude Code 常用命令 | XPoet's Blog AI 编程工程化:Plugin——AI 工具能力的产品化形态 | XPoet's Blog AI 编程工程化:MCP——给你的 AI 员工打通外部能力 | XPoet's Blog AI 编程工程化:Subagent——给你的 AI 员工打造协作助手 | XPoet's Blog AI 编程工程化:Hook——AI 每次操作前后的自动检查站 | XPoet's Blog AI 编程工程化:Skill——给你的 AI 员工装上技能包 | XPoet's Blog AI 编程工程化:Command——给你的 AI 员工编一套操作手册 | XPoet's Blog AI 编程工程化:Rule——给你的 AI 员工立规矩 | XPoet's Blog AI 编程工程化:AI 时代程序员的基本功 | XPoet's Blog 独立开发者的福音:Amazon DynamoDB 数据库 | XPoet's Blog 零基础入门 JavaScript 算法 | XPoet's Blog 微信公众号又双叒叕改名了 | XPoet's Blog 赞赏作者 Admire Author 💕 | XPoet's Blog JavaScript 的并发模型和事件循环 | XPoet's Blog JavaScript 的内存管理 | XPoet's Blog 从 0 开始手把手带你搭建一套企业级 Vue3.x 工程化项目 前端工程化入门 | XPoet's Blog 快速掌握 Vue 3.0 基础语法 | XPoet's Blog Vue 项目动态修改 DOM 的 CSS 伪类
常考的 JavaScript 手写题 | XPoet's Blog
XPoet · 2022-01-06 · via XPoet's Blog

现在的前端招聘圈,对前端程序员的能力要求真的是越来越高了(内卷真的太厉害了),出去找工作面试,哪怕是家小公司,大概率都会考一些 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))
console.log(myTypeOf('1'))
console.log(myTypeOf(true))
console.log(myTypeOf([]))
console.log(myTypeOf({}))
console.log(myTypeOf(/^/))
console.log(myTypeOf(new Date()))
console.log(myTypeOf(Math))
console.log(myTypeOf(() => {}))

手写数组去重

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


console.log(myUnique([1, 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) {

return new Promise(function(resolve, reject) {
const xhr = new XMLHttpRequest()


xhr.open('GET', url, true)


xhr.responseType = 'json'


xhr.setRequestHeader('Accept', 'application/json')


xhr.onreadystatechange = function() {
if (this.readyState !== 4) return


if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}


xhr.onerror = function() {
reject(new Error(this.statusText))
}


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




const throttle = (fn, delay = 1000) => {

let prevTime = 0


return function(...args) {

const nowTime = Date.now()


if (nowTime - prevTime > delay) {
prevTime = nowTime
fn.apply(this, args)
}
}
}


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


setInterval(testFn, 100)

手写函数防抖

定义:在事件被触发 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)


setInterval(testFn, 1000)


setInterval(testFn, 3000)

手写深浅拷贝

浅拷贝

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


const newObject = Array.isArray(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)

const arr1 = [1, 2, 3]
const arr2 = shallowCopy(arr1)
console.log(arr2)

深拷贝

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


const newObject = Array.isArray(object) ? [] : {}

for (const key in object) {
if (object.hasOwnProperty(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)

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

手写 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()
a.call(null)
a.call(undefined)
a.call(1)
a.call('')
a.call(true)
a.call(b)
a.call(c)

手写 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 = 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)

手写 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 = 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])

手写 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)()

函数柯里化

函数柯里化指的是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。例如: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)
_fn(1)(2)(3, 4, 5)
_fn(1, 2)(3, 4)(5)
_fn(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




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)
})
}
}


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 {


PENDING = 'pending'
FULFILLED = 'fulfilled'
REJECTED = 'rejected'


constructor(executor) {
this._status = this.PENDING
this._value = undefined
this._resolveQueue = []
this._rejectQueue = []


const resolve = value => {
const run = () => {

if (this._status === this.PENDING) {
this._status = this.FULFILLED
this._value = value


while (this._resolveQueue.length) {
const callback = this._resolveQueue.shift()
callback(value)
}
}
}

setTimeout(run)
}


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)
}


executor(resolve, reject)
}


then(onFulfilled, onRejected) {

typeof onFulfilled !== 'function' ? onFulfilled = value => value : null
typeof onRejected !== 'function' ? onRejected = error => error : null


return new MyPromise((resolve, reject) => {
const resolveFn = value => {
try {
const x = onFulfilled(value)

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)
})
}


static resolve(value) {
return value instanceof MyPromise ? value : new MyPromise(resolve => resolve(value))
}


static reject(error) {
return new MyPromise((resolve, reject) => reject(error))
}


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)
})
})
})
}


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] = [];
}
}
};
})();