阅读12分钟
在面试中,常常会问到一些“手写XXX”的面试题,如果我们只是停留在熟练使用这些 API,问到这种问题想必总是束手无策的。其实想要手写 API 的实现也并不难,更多的是需要我们训练自己通过使用方式来推倒实现的能力,千万不要死记硬背。最近我也在强化自己手写 API 的能力,并汇总了面试中高频的手写 API 面试题,希望对大家有所帮助~
相关文章:
「面试必会」中高级前端必会的手写面试题(一)
一、使用ES5实现类的继承
1. 构造函数继承
在子类的构造函数中执行父类的构造函数。并为其绑定子类的
this
,让父类的构造函数把成员的属性和方法都挂在
子类的this
这样能避免实例之间共享一个原型实例,又能向父类构造函数传参。
function Parent(){
this.name = 'Cherry';
Parent.prototype.getName = function() {
return this.name;
function Child(){
Parent.call(this);
this.type = 'child';
const child = new Child();
console.log(child);
console.log(child.getName());
这么看使用构造函数继承的缺点已经很明显了:继承不到父类原型上的属性和方法,那么引出下面的方法。
2. 原型链继承
让子类的原型指向父类的实例,当子类实例找不到对用的属性和方法时,就会沿着原型链向上找,也就是去父类的实例上找,从而实现对父类属性和方法的继承。
function Parent() {
this.name = 'Cherry';
this.play = [1, 2, 3];
Parent.prototype.getName = function() {
return this.name;
function Child() {
this.type = 'child';
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const child = new Child();
console.log(child);
console.log(child.getName());
看似没有问题,父类的方法和属性都能够访问,但实际上有一个潜在的问题:
const child1 = new Child();
const child2 = new Child();
child1.play.push(4);
console.log(child1.play, child2.play);
在上面这个例子中,虽然我只改变了child1
的play
属性,但是child2
的play
属性也跟着变了。原因是因为两个实例引用的是同一个原型对象。
由此我们可以发现,使用原型链继承有以下两个缺点:
由于所有Child实例原型都指向同一个Parent实例, 因此对某个Child实例的父类引用类型变量修改会影响所有的Child实例
在创建子类实例时无法向父类构造传参, 即没有实现super()
的功能
3. 组合式继承
既然原型链继承和构造函数继承各有互补的优缺点,那么我们为什么不组合起来使用呢,所以就有了综合二者的组合式继承。
function Parent () {
this.name = 'Cherry';
this.play = [1, 2, 3];
Parent.prototype.getName = function() {
return this.name;
function Child() {
Parent.call(this);
this.type = 'child';
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const child1 = new Child();
const child2 = new Child();
console.log(child1);
console.log(child1.getName());
child1.play.push(4);
console.log(child1.play, child2.play);
我们通过控制台的输出结果可以看到,之前的问题都得到了解决。但是这里又增加了一个新问题,那就是Parent的构造函数会多执行了一次Child.prototype = new Parent();
虽然这并不影响父类的继承,但子类创建实例时,原型中会存在两份相同的属性和方法,这并不优雅。那么如何解决这个问题?
4. 寄生式组合继承
为了解决构造函数被执行两次的问题, 我们将指向父类实例
改为指向父类原型
, 减去一次构造函数的执行。
function Parent () {
this.name = 'Cherry';
this.play = [1, 2, 3];
Parent.prototype.getName = function() {
return this.name
function Child() {
Parent.call(this);
this.type = 'child';
Child.prototype = Parent.prototype;
Child.prototype.constructor = Child;
const child1 = new Child();
const child2 = new Child();
console.log(child1);
console.log(child1.getName());
child1.play.push(4);
console.log(child1.play, child2.play);
但这种方式存在一个问题,由于子类原型和父类原型指向同一个对象,我们对子类原型的操作会影响到父类原型,例如给Child.prototype
增加一个getName()
方法,那么会使Parent.prototype
上也增加或被覆盖一个getName()
方法,为了解决这个问题,我们会给Parent.prototype
做一个浅拷贝。
function Parent () {
this.name = 'Cherry';
this.play = [1, 2, 3];
Parent.prototype.getName = function() {
return this.name
function Child() {
Parent.call(this);
this.type = 'child';
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
const child1 = new Child();
const child2 = new Child();
console.log(child1);
console.log(child1.getName());
child1.play.push(4);
console.log(child1.play, child2.play);
到这里我们就完成了ES5环境下的继承的实现,这种继承方式称为寄生组合式继承
,是目前最成熟的继承方式,babel对ES6继承的转化也是使用了寄生组合式继承。
二、实现数组扁平化
将一个多维数组变为一维数组:
[1, [2, 3, [4, 5]]] ------> [1, 2, 3, 4, 5]
1. ES6的flat()
let arr = [1, [2, 3, [4, 5]]];
arr.flat(Infinity);
2. 序列化后正则
let arr = [1, [2, 3, [4, 5]]];
let str = JSON.stringify(arr).replace(/(\[|\])/g, '');
str = '[' + str + ']';
JSON.parse(str);
3. 递归处理
对于树状结构的数据,最直接的处理方式就是递归
let arr = [1, [2, 3, [4, 5]]];
function flat(arr) {
let result = [];
for (const item of arr) {
item instanceof Array ? result = result.concat(flat(item)) : result.push(item)
return result;
flat(arr);
4. reduce
遍历数组每一项,若值为数组则递归遍历,否则直接累加。
let arr = [1, [2, 3, [4, 5]]];
function flat(arr) {
return arr.reduce((prev, current) => {
return prev.concat(current instanceof Array ? flat(current) : current)
}, [])
flat(arr);
5. 迭代+扩展运算符
es6的扩展运算符能将二维数组变为一维
let arr = [1, [1,2], [1,2,3,[4,4,4]]]
while (arr.some(Array.isArray)) {
arr = [].concat(...arr);
console.log(arr);
三、实现数组去重
1. 使用 filter 方法
filter 方法可以过滤掉不符合条件的元素,并返回一个新数组,任何不符合条件的数组都将不在过滤后的数组中。
let arr = ["banana", "apple", "orange", "lemon", "apple", "lemon"];
function removeDuplicates(data) {
return data.filter((value, index) => data.indexOf(value) === index);
console.log(removeDuplicates(arr));
我们还可以通过简单的调整,使用filter方法从数据中检索出重复值
let arr = ["banana", "apple", "orange", "lemon", "apple", "lemon"];
function removeDuplicates(data) {
return data.filter((value, index) => data.indexOf(value) !== index);
console.log(removeDuplicates(arr));
2.使用 ES6 的 Set
Set 是 ES6 中的新对象类型,用于创建唯一key的集合。
let arr = ["banana", "apple", "orange", "lemon", "apple", "lemon"];
function removeDuplicates(data) {
return [...new Set(data)];
console.log(removeDuplicates(arr));
3. 使用 forEach 方法
forEach 方法可以遍历数组中的元素,如果该元素不在数组中,就将该元素push到数组中。
let arr = ["banana", "apple", "orange", "lemon", "apple", "lemon"];
function removeDuplicates(data) {
let unique = [];
data.forEach(element => {
if (!unique.includes(element)) {
unique.push(element);
return unique;
console.log(removeDuplicates(arr));
4.使用 reduce 方法
let arr = ["banana", "apple", "orange", "lemon", "apple", "lemon"];
function removeDuplicates(data) {
let unique = data.reduce(function (a, b) {
if (a.indexOf(b) < 0) a.push(b);
return a;
}, []);
return unique;
console.log(removeDuplicates(arr));
let arr = ["banana", "apple", "orange", "lemon", "apple", "lemon"];
function removeDuplicates(data) {
return data.reduce((acc, curr) => acc.includes(curr) ? acc : [...acc, curr], []);
console.log(removeDuplicates(arr));
5.在数组原型上添加去重方法
let arr = ["banana", "apple", "orange", "lemon", "apple", "lemon"];
Array.prototype.unique = function () {
let unique = [];
for (let i = 0; i < this.length; i++) {
const current = this[i];
if (unique.indexOf(current) < 0) unique.push(current);
return unique;
console.log(arr.unique());
6. Array.from + ES6 Set
let arr = ["banana", "apple", "orange", "lemon", "apple", "lemon"];
function removeDuplicates(data) {
return Array.from(new Set(arr))
console.log(removeDuplicates(arr));
7.从对象数组中删除重复的对象
有时,我们需要通过属性的名称从对象数据中删除重复的对象,我们可以使用 Map 来实现:
let users = [
{ id: 1, name: 'susan', age: 25 },
{ id: 2, name: 'cherry', age: 28 },
{ id: 3, name: 'cindy', age: 27 },
{ id: 2, name: 'cherry', age: 28 },
{ id: 1, name: 'susan', age: 25 },
function uniqueByKey(data, key) {
return [
...new Map(
data.map(x => [key(x), x])
).values()
console.log(uniqueByKey(users, item => item.id));
或者用reduce实现:
let users = [
{ id: 1, name: 'susan', age: 25 },
{ id: 2, name: 'cherry', age: 28 },
{ id: 3, name: 'cindy', age: 27 },
{ id: 2, name: 'cherry', age: 28 },
{ id: 1, name: 'susan', age: 25 },
function uniqueByKey(data, key) {
const object = {};
data = data.reduce((prev, next) => {
object[next[key]]
: (object[next[key]] = true && prev.push(next));
return prev;
}, []);
return data;
console.log(uniqueByKey(users, "id"));
四、实现数组的取交集,并集,差集
1. 取交集
Array.prototype.includes
let a = [1, 2, 3];
let b = [2, 4, 5];
let intersection = a.filter(v => b.includes(v));
console.log(intersection);
Array.from
let a = [1, 2, 3];
let b = [2, 4, 5];
let aSet = new Set(a);
let bSet = new Set(b);
let intersection = Array.from(new Set(a.filter(v => bSet.has(v))));
console.log(intersection);
Array.prototype.indexOf
let a = [1, 2, 3];
let b = [2, 4, 5];
let intersection = a.filter((v) => b.indexOf(v) > -1);
console.log(intersection);
2. 取并集
Array.prototype.includes
let a = [1, 2, 3];
let b = [2, 4, 5];
let union = a.concat(b.filter(v => !a.includes(v)));
console.log(union);
Array.from
let a = [1, 2, 3];
let b = [2, 4, 5];
let aSet = new Set(a);
let bSet = new Set(b);
let union = Array.from(new Set(a.concat(b)));
console.log(union);
Array.prototype.indexOf
let a = [1, 2, 3];
let b = [2, 4, 5];
let union = a.concat(b.filter((v) => a.indexOf(v) === -1));
console.log(union);
3. 取差集
Array.prototype.includes
let a = [1, 2, 3];
let b = [2, 4, 5];
let difference = a.concat(b).filter(v => !a.includes(v) || !b.includes(v));
console.log(difference);
Array.from
let a = [1, 2, 3];
let b = [2, 4, 5];
let aSet = new Set(a);
let bSet = new Set(b);
let difference = Array.from(new Set(a.concat(b).filter(v => !aSet.has(v) || !bSet.has(v))));
console.log(difference);
Array.prototype.indexOf
let a = [1, 2, 3];
let b = [2, 4, 5];
let difference = a.filter((v) => b.indexOf(v) === -1).concat(b.filter((v) => a.indexOf(v) === -1));
console.log(difference);
五、实现发布订阅模式
发布订阅模式 一共分为两个部分:on
、emit
。发布和订阅之间没有依赖关系,发布者告诉第三方(事件频道)发生了改变,第三方再通知订阅者发生了改变。
on:就是把一些函数维护到数组中
emit:让数组中的方法依次执行
let fs = require("fs");
let event = {
arr: [],
on(fn) {
this.arr.push(fn);
emit() {
this.arr.forEach(fn => fn());
event.on(function () {
console.log("读取了一个");
event.on(function () {
if (Object.keys(school).length === 2) {
console.log("读取完毕");
let school = {};
fs.readFile('./name.txt', 'utf8', function (err, data) {
school.name = data;
event.emit();
fs.readFile('./age.txt', 'utf8', function (err, data) {
school.age = data;
event.emit();
六、实现观察者模式
观察者模式 是基于发布订阅模式的,分为观察者
和被观察者
两部分,需要被观察者先收集观察者,当被观察者的状态改变时通知观察者。观察者和被观察者之间存在关系,被观察者数据发生变化时直接通知观察者改变。
比如:现在有一家之口,爸爸、妈妈和小宝宝,爸爸妈妈告诉小宝宝你有任何状态变化都要通知我们,当小宝宝饿了的时候,就会通知爸爸妈妈过来处理。这里的小宝宝就是被观察者Subject
,爸爸妈妈就是观察者Observer
,小宝宝维护了一个观察者队列,当自己有任何状态改变的时候都直接通知队列中的观察者。
class Subject {
constructor(name) {
this.name = name;
this.state = "开心的";
this.observer = [];
attach(o) {
this.observer.push(o);
setState(newState) {
this.state = newState;
this.observer.forEach(o => o.update(this));
class Observer {
constructor(name) {
this.name = name;
update(baby) {
console.log("当前"+this.name+"被通知了,当前小宝宝的状态是:"+baby.state);
let baby = new Subject("小宝宝");
let father = new Observer("爸爸");
let mother = new Observer("妈妈");
baby.attach(father);
baby.attach(mother);
baby.setState("我饿了");
所以,我用下图表示这两个模式最重要的区别:
七、实现单例模式
单例模式是创建型设计模式的一种。确保全局中有且仅有一个对象实例,并提供一个访问它的全局访问点,如线程池、全局缓存、window 对象等。
1.常规实现:
function CreateSingleton (name) {
this.name = name;
this.getName();
CreateSingleton.prototype.getName = function() {
console.log(this.name)
var Singleton = (function(){
var instance;
return function (name) {
if(!instance) {
instance = new CreateSingleton(name);
return instance;
})();
var a = new Singleton('a');
var b = new Singleton('b');
console.log(a === b);
2. 用闭包和Proxy属性拦截实现
const singletonify = (className) => {
return new Proxy(className.prototype.constructor, {
instance: null,
construct: (target, argumentsList) => {
if (!this.instance)
this.instance = new target(...argumentsList);
return this.instance;
class MyClass {
constructor(msg) {
this.msg = msg;
printMsg() {
console.log(this.msg);
MySingletonClass = singletonify(MyClass);
const myObj = new MySingletonClass('first');
myObj.printMsg();
const myObj2 = new MySingletonClass('second');
myObj2.printMsg();
八、实现Promise
重点难点,面试高频考点,具体请参考我的另一篇文章:
面试官:“你能手写一个 Promise 吗”
九、实现深拷贝
也是面试高频考点,具体请参考我的另一篇文章:
Javascript经典面试之深拷贝VS浅拷贝
十、手写字符串转二进制
function charToBinary(text) {
let code = "";
for (let i of text) {
let number = i.charCodeAt().toString(2);
for (let a = 0; a <= 8 - number.length; a++) {
number = 0 + number;
code += number;
return code;
十一、手写二进制转Base64
function binaryTobase64(code) {
let base64Code = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let res = '';
if (code.length % 24 === 8) {
code += '0000';
res += '=='
if (code.length % 24 === 16) {
code += '00';
res += '='
let encode = '';
for (let i = 0; i < code.length; i += 6) {
let item = code.slice(i, i + 6);
encode += base64Code[parseInt(item, 2)];
return encode + res;
十二、手写字符转Base64
function base64encode(text) {
let base64Code = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
let res = '';
let i = 0;
while (i < text.length) {
let char1, char2, char3, enc1, enc2, enc3, enc4;
char1 = text.charCodeAt(i++);
char2 = text.charCodeAt(i++);
char3 = text.charCodeAt(i++);
enc1 = char1 >> 2;
if (isNaN(char2)) {
enc2 = ((char1 & 3) << 4) | (0 >> 4);
enc3 = enc4 = 64;
} else if (isNaN(char3)) {
enc2 = ((char1 & 3) << 4) | (char2 >> 4);
enc3 = ((char2 & 15) << 2) | (0 >> 6);
enc4 = 64;
} else {
enc2 = ((char1 & 3) << 4) | (char2 >> 4);
enc3 = ((char2 & 15) << 2) | (char3 >> 6);
enc4 = char3 & 63;
res += base64Code.charAt(enc1) + base64Code.charAt(enc2) + base64Code.charAt(enc3) + base64Code.charAt(enc4)
return res;
let encodedData = window.btoa("this is a example");
console.log(encodedData);
let decodeData = window.atob(encodedData);
console.log(decodeData);
十三、实现一个可以拖拽的DIV
有一个DIV层,设定position属性为absolute或fixed,通过更改其left,top来更改层的相对位置。
在DIV层上绑定mousedown事件,设置一个拖动开始的标志为true,拖动结束的标志为false,本例为isMouseDown。
拖动时的细节优化,如:
鼠标于DIV的相对位置
拖动时防止文字被选中
限定DIV的移动范围,拖动到边界处的处理
当鼠标移出窗口时失去焦点的处理
当鼠标移动到iframe上的处理
let injectedHTML = document.createElement("DIV");
injectedHTML.innerHTML = '<dragBox id="dragBox" class="drag-box">\
<dragBoxBar id="dragBoxBar" class="no-select"></dragBoxBar>\
<injectedBox id="injectedBox">CONTENT</injectedBox>\
</dragBox>';
document.body.appendChild(injectedHTML);
let isMouseDown,
initX,
initY,
height = injectedBox.offsetHeight,
width = injectedBox.offsetWidth,
dragBoxBar = document.getElementById('dragBoxBar');
dragBoxBar.addEventListener('mousedown', function(e) {
isMouseDown = true;
document.body.classList.add('no-select');
injectedBox.classList.add('pointer-events');
initX = e.offsetX;
initY = e.offsetY;
dragBox.style.opacity = 0.5;
dragBoxBar.addEventListener('mouseup', function(e) {
mouseupHandler();
document.addEventListener('mousemove', function(e) {
if (isMouseDown) {
let cx = e.clientX - initX,
cy = e.clientY - initY;
if (cx < 0) {
cx = 0;
if (cy < 0) {
cy = 0;
if (window.innerWidth - e.clientX + initX < width + 16) {
cx = window.innerWidth - width;
if (e.clientY > window.innerHeight - height - dragBoxBar.offsetHeight + initY) {
cy = window.innerHeight - dragBoxBar.offsetHeight - height;
dragBox.style.left = cx + 'px';
dragBox.style.top = cy + 'px';
document.addEventListener('mouseup', function(e) {
if (e.clientY > window.innerWidth || e.clientY < 0 || e.clientX < 0 || e.clientX > window.innerHeight) {
mouseupHandler();
function mouseupHandler() {
isMouseDown = false;
document.body.classList.remove('no-select');
injectedBox.classList.remove('pointer-events');
dragBox.style.opacity = 1;
margin: 0;
padding: 0;
border: none
body,
html {
height: 100%;
width: 100%;
.drag-box {
user-select: none;
background: #f0f0f0;
z-index: 2147483647;
position: fixed;
left: 0;
top: 0;
width: 200px;
#dragBoxBar {
align-items: center;
display: flex;
justify-content: space-between;
background: #ccc;
width: 100%;
height: 40px;
cursor: move;
user-select: none;
.no-select {
user-select: none;
.pointer-events {
pointer-events: none;
.no-border {
border: none;
#injectedBox {
height: 160px;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
background: #eee;
十四、实现一个批量请求函数 multiRequest(urls, maxNum)
function loadImg(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = function() {
console.log(url, "加载完成");
resolve(img);
img.onerror = function() {
reject(new Error('Error at:' + url));
img.src = url;
function multiRequest(urls, maxNum) {
const firstMaxNum = urls.splice(0, maxNum);
let promises = firstMaxNum.map((url, index)=>{
return loadImg(url).then(()=>{
return index
return urls.reduce((res, cur)=>{
return res.then(()=>{
return Promise.race(promises)
}).then((idx)=>{
promises[idx] = loadImg(cur).then(()=>{
return idx
}, Promise.resolve()).then(()=>{
return Promise.all(promises)
multiRequest(urls, 4).then(()=>{
console.log('finish')
十五、实现一个 sleep 函数
思路:比如 sleep(1000) 意味着等待1000毫秒,可从 Promise、Generator、Async/Await 等角度实现。
const sleep = time => {
return new Promise(resolve => setTimeout(resolve,time))
sleep(1000).then(()=>{
console.log(1)
function* sleepGenerator(time) {
yield new Promise(function(resolve,reject){
setTimeout(resolve,time);
sleepGenerator(1000).next().value.then(()=>{console.log(1)})
function sleep(time) {
return new Promise(resolve => setTimeout(resolve,time))
async function output() {
let out = await sleep(1000);
console.log(1);
return out;
output();
function sleep(callback,time) {
if(typeof callback === 'function')
setTimeout(callback,time)
function output(){
console.log(1);
sleep(output,1000);
十六、模拟实现一个 localStorage
'use strict'
const valuesMap = new Map()
class LocalStorage {
getItem (key) {
const stringKey = String(key)
if (valuesMap.has(key)) {
return String(valuesMap.get(stringKey))
return null
setItem (key, val) {
valuesMap.set(String(key), String(val))
removeItem (key) {
valuesMap.delete(key)
clear () {
valuesMap.clear()
key (i) {
if (arguments.length === 0) {
throw new TypeError("Failed to execute 'key' on 'Storage': 1 argument required, but only 0 present.")
let arr = Array.from(valuesMap.keys())
return arr[i]
get length () {
return valuesMap.size
const instance = new LocalStorage()
global.localStorage = new Proxy(instance, {
set: function (obj, prop, value) {
if (LocalStorage.prototype.hasOwnProperty(prop)) {
instance[prop] = value
} else {
instance.setItem(prop, value)
return true
get: function (target, name) {
if (LocalStorage.prototype.hasOwnProperty(name)) {
return instance[name]
if (valuesMap.has(name)) {
return instance.getItem(name)
2万字 | 前端基础拾遗90问
高级前端面试题汇总
7 ways to remove duplicates from an array in JavaScript
How can I implement a singleton in JavaScript
作者齐小神,前端程序媛一枚。
有点文艺,喜欢摄影。 虽然现在朝九晚五,埋头苦学, 但梦想是做女侠,扶贫济穷,仗剑走天涯。 希望有一天能改完 BUG 去实现自己的梦想。
公众号:大前端Space,不定时更新,欢迎来玩~