共计 14251 个字符,预计需要花费 36 分钟才能阅读完成。
JavaScript 笔试部分
点击关注本公众号获取文档最新更新, 并可以领取配套于本指南的 《前端面试手册》 以及 最标准的简历模板.
实现防抖函数(debounce)
防抖函数原理:在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时。
那么与节流函数的区别直接看这个动画实现即可。
手写简化版:
// 防抖函数
const debounce = (fn, delay) => {
let timer = null;
return (...args) => {clearTimeout(timer);
timer = setTimeout(() => {fn.apply(this, args);
}, delay);
};
};
适用场景:
- 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
- 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似
生存环境请用 lodash.debounce
实现节流函数(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));
局限性:
- 他无法实现对函数、RegExp 等特殊对象的克隆
- 会抛弃对象的 constructor, 所有的构造函数会指向 Object
- 对象有循环引用, 会报错
面试版:
/**
* 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);
};
局限性:
- 一些特殊情况没有处理: 例如 Buffer 对象、Promise、Set、Map
- 另外对于确保没有循环引用的对象,我们可以省去对循环引用的特殊处理,因为这很消耗时间
原理详解实现深克隆
实现 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;}
}
};
实现具体过程和思路见实现 event
实现 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);
}
公众号
想要实时关注笔者最新的文章和最新的文档更新请关注公众号 程序员面试官, 后续的文章会优先在公众号更新.
简历模板: 关注公众号回复「模板」获取
《前端面试手册》: 配套于本指南的突击手册, 关注公众号回复「fed」获取