现在的前端招聘圈,对前端程序员的能力要求真的是越来越高了(内卷真的太厉害了),出去找工作面试,哪怕是家小公司,大概率都会考一些 JS 手写题,你不会但别人会,自然就被卷死了。
其实,对一个前端工程师来说,JS 本就重中之重,我们要掌握的可不仅仅是这些手写题。
当然,你不会这些手写题也不能代表你的 JS 编程能力不好,我觉得那是因为你不熟悉而已。
现在就跟着作者来一步一步学习,彻底搞懂这些常考的 JS 手写题,无论是在业务开发还是求职面试中都很实用。
截止目前,市面上超过 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 中,call
、apply
和 bind
是 Function
对象自带的三个方法,这三个方法的主要作用是改变函数中的 this
指向。
共同点:
apply
、 call
、bind
三者都是用来改变函数的 this
对象指向。apply
、 call
、bind
三者第一个参数都是 this
要指向的对象,也就是想指定的上下文。(函数的每次调用都会拥有一个特殊值——本次调用的上下文(context),这就是 this
关键字的值。)apply
、 call
、bind
三者都可以利用后续参数传参。
区别:
bind
是返回对应函数,便于稍后调用。apply
、call
则是立即调用。
call()
和 apply()
的作用是一样的,都是用于改变 this
的指向,区别在于 call
接受多个参数,而 apply
接受的是一个数组。
第一个参数的取值有以下 4 种情况:
- 不传,或者传
null
、undefined
,函数中的 this
指向 window
对象。 - 传递另一个函数的函数名,函数中的
this
指向这个函数的引用。 - 传递字符串、数值或布尔类型等基础类型,函数中的
this
指向其对应的包装对象,如 String
、Number
、Boolean
。 - 传递一个对象,函数中的
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 函数的实现步骤:
- 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用
call
等方式调用的情况。 - 判断传入上下文对象是否存在,如果不存在,则设置为
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
| 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
- 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用
call
等方式调用的情况。 - 判断传入上下文对象是否存在,如果不存在,则设置为
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 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
- 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用
call
等方式调用的情况。 - 保存当前函数的引用,获取其余传入参数值。
- 创建一个函数返回。
- 函数内部使用
apply
来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this
给 apply
调用,其余情况都传入指定的上下文对象。
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] = []; } } }; })();
|