编程技术文章分享与教程

网站首页 > 技术文章 正文

高频JavaScript手写面试题及答案 前端面试笔试题手写代码

hmc789 2024-11-14 16:31:19 技术文章 2 ℃

实现防抖函数(debounce)

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

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

手写简化版:

// 防抖函数
const debounce = (fn, delay) => {
 let timer = null;
 return (...args) => {
 clearTimeout(timer);
 timer = setTimeout(() => {
 fn.apply(this, args);
 }, delay);
 };
};

适用场景:

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

生存环境请用lodash.debounce

我自己是一名从事了多年开发的web前端老程序员,目前辞职在做自己的web前端私人定制课程,今年年初我花了一个月整理了一份最适合2019年学习的web前端学习干货,各种框架都有整理,送给每一位前端小伙伴,想要获取的可以关注我的头条号并在后台私信我:前端,即可免费获取。

实现节流函数(throttle)

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

// 手写简化版

// 节流函数
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)

简单版:

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

局限性:

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

面试版:

/**
 * deep clone
 * @param {[type]} parent object 需要进行克隆的对象
 * @return {[type]} 深克隆后的对象
 */
const clone = parent => {
 // 判断类型
 const isType = (obj, type) => {
 if (typeof obj !== "object") return false;
 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) return null;
 if (typeof parent !== "object") return parent;

 let child, proto;

 if (isType(parent, "Array")) {
 // 对数组做特殊处理
 child = [];
 } else if (isType(parent, "RegExp")) {
 // 对正则对象做特殊处理
 child = new RegExp(parent.source, getRegExp(parent));
 if (parent.lastIndex) child.lastIndex = parent.lastIndex;
 } else if (isType(parent, "Date")) {
 // 对Date对象做特殊处理
 child = new Date(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中各个模块的基石,又是前端组件通信的依赖手段之一,同时涉及了订阅-发布设计模式,是非常重要的基础。

简单版:

class EventEmeitter {
 constructor() {
 this._events = this._events || new Map(); // 储存事件/回调键值对
 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);
 }
 return true;
};

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

面试版:

class EventEmeitter {
 constructor() {
 this._events = this._events || new Map(); // 储存事件/回调键值对
 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);
 }
 return true;
};

// 监听名为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);
 }
 }

 return true;
};

// 监听名为type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
 const handler = this._events.get(type); // 获取对应事件名称的函数清单
 if (!handler) {
 this._events.set(type, fn);
 } else if (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 {
 return this;
 }
 }
};

实现instanceOf

// 模拟 instanceof
function instance_of(L, R) {
 //L 表示左表达式,R 表示右表达式
 var O = R.prototype; // 取 R 的显示原型
 L = L.__proto__; // 取 L 的隐式原型
 while (true) {
 if (L === null) return false;
 if (O === L)
 // 这里重点:当 O 严格等于 L 时,返回 true
 return true;
 L = L.__proto__;
 }
}

模拟new

new操作符做了这些事:

  • 它创建了一个全新的对象
  • 它会被执行[[Prototype]](也就是proto)链接
  • 它使this指向新创建的对象
  • 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上
  • 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用将返回该对象引用
// objectFactory(name, 'cxk', '18')
function objectFactory() {
 const obj = new Object();
 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
// 模拟 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很相似,不多赘述

// 模拟 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失效,但是传入的参数依然有效
// mdn的实现
if (!Function.prototype.bind) {
 Function.prototype.bind = function(oThis) {
 if (typeof this !== 'function') {
 // closest thing possible to the ECMAScript 5
 // internal IsCallable function
 throw new TypeError('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(this instanceof 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

// 模拟 Object.create

function create(proto) {
 function F() {}
 F.prototype = proto;

 return new F();
}

实现类的继承

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

function Parent(name) {
 this.parent = name
}
Parent.prototype.say = function() {
 console.log(`${this.parent}: 你打篮球的样子像kunkun`)
}
function Child(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: 你打篮球的样子像kunkun

var child = new Child('cxk', 'father');
child.say() // father好,我是练习时长两年半的cxk

实现JSON.parse

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

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

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

实现Promise

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

var PromisePolyfill = (function () {
 // 和reject不同的是resolve需要尝试展开thenable对象
 function tryToResolve (value) {
 if (this === value) {
 // 主要是防止下面这种情况
 // let y = new Promise(res => setTimeout(res(y)))
 throw TypeError('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 = false
 if (typeof then === 'function') {
 // 是thenable 那么尝试展开
 // 并且在该thenable状态改变之前this对象的状态不变
 then.bind(value)(
 // onFullfilled
 function (value2) {
 if (thenAlreadyCalledOrThrow) return
 thenAlreadyCalledOrThrow = true
 tryToResolve.bind(this, value2)()
 }.bind(this),

 // onRejected
 function (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)()
 }
 }

 function resolveOrReject (status, data) {
 if (this.status !== 'pending') return
 this.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]()
 }
 }
 }

 function Promise (executor) {
 if (!(this instanceof Promise)) {
 throw Error('Promise can not be called without new !')
 }

 if (typeof executor !== 'function') {
 // 非标准 但与Chrome谷歌保持一致
 throw TypeError('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
 // 会被调用, 然而我们想要调用的是onRejected
 if (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
 // 并以此为依据解析这个新的Promise
 var 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 {
 // pending
 return new Promise(function (resolve, reject) {
 this.resolveList.push(executor.bind(this, resolve, reject))
 this.rejectList.push(executor.bind(this, resolve, reject))
 }.bind(this))
 }
 }

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

 // for prmise A+ test
 if (typeof module !== 'undefined') {
 module.exports = Promise
 }

 return Promise
})()

PromisePolyfill.all = function (promises) {
 return new Promise((resolve, reject) => {
 const result = []
 let cnt = 0
 for (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) {
 return new Promise((resolve, reject) => {
 for (let i = 0; i < promises.length; ++i) {
 promises[i].then(resolve, reject)
 }
 })
}

解析 URL Params 为对象

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
}
*/
function parseParam(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;
}

模板引擎实现

let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let data = {
 name: '姓名',
 age: 18
}
render(template, data); // 我是姓名,年龄18,性别undefined
function render(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; // 如果模板没有模板字符串直接返回
}

转化为驼峰命名

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次

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

a='34';b='1234567'; // 返回 2
a='35';b='1234567'; // 返回 -1
a='355';b='12354355'; // 返回 5
isContain(a,b);
function isContain(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;
}

实现千位分隔符

// 保留三位小数
parseToMoney(1234.56); // return '1,234.56'
parseToMoney(123456789); // return '123,456,789'
parseToMoney(1087654.321); // return '1,087,654.321'
function parseToMoney(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 : '');
}

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

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

判断是否是电话号码

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

验证是否是邮箱

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

验证是否是身份证

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

原文链接:https://mp.weixin.qq.com/s/CY-igdCB_U-DtA4E2BKhhA
作者:程序员面试官

Tags:

标签列表
最新留言