高级前端常见手写面试题指南
Function.prototype.call
于call
唯一不同的是,call()
方法接受的是一个参数列表
Function.prototype.call = function(context = window, ...args) {if (typeof this !== 'function') {throw new TypeError('Type Error');}const fn = Symbol('fn');context[fn] = this;const res = context[fn](...args);delete context[fn];return res;
}
实现迭代器生成函数
我们说迭代器对象全凭迭代器生成函数帮我们生成。在ES6
中,实现一个迭代器生成函数并不是什么难事儿,因为ES6早帮我们考虑好了全套的解决方案,内置了贴心的 生成器 (Generator
)供我们使用:
// 编写一个迭代器生成函数
function *iteratorGenerator() {yield '1号选手'yield '2号选手'yield '3号选手'
}const iterator = iteratorGenerator()iterator.next()
iterator.next()
iterator.next()
丢进控制台,不负众望:
写一个生成器函数并没有什么难度,但在面试的过程中,面试官往往对生成器这种语法糖背后的实现逻辑更感兴趣。下面我们要做的,不仅仅是写一个迭代器对象,而是用ES5
去写一个能够生成迭代器对象的迭代器生成函数(解析在注释里):
// 定义生成器函数,入参是任意集合
function iteratorGenerator(list) {// idx记录当前访问的索引var idx = 0// len记录传入集合的长度var len = list.lengthreturn {// 自定义next方法next: function() {// 如果索引还没有超出集合长度,done为falsevar done = idx >= len// 如果done为false,则可以继续取值var value = !done ? list[idx++] : undefined// 将当前值与遍历是否完毕(done)返回return {done: done,value: value}}}
}var iterator = iteratorGenerator(['1号选手', '2号选手', '3号选手'])
iterator.next()
iterator.next()
iterator.next()
此处为了记录每次遍历的位置,我们实现了一个闭包,借助自由变量来做我们的迭代过程中的“游标”。
运行一下我们自定义的迭代器,结果符合预期:
手写防抖函数
函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。
// 函数防抖的实现
function debounce(fn, wait) {let timer = null;return function() {let context = this,args = arguments;// 如果此时存在定时器的话,则取消之前的定时器重新记时if (timer) {clearTimeout(timer);timer = null;}// 设置定时器,使事件间隔指定事件后执行timer = setTimeout(() => {fn.apply(context, args);}, wait);};
}
图片懒加载
可以给img标签统一自定义属性data-src='default.png'
,当检测到图片出现在窗口之后再补充src属性,此时才会进行图片资源加载。
function lazyload() {const imgs = document.getElementsByTagName('img');const len = imgs.length;// 视口的高度const viewHeight = document.documentElement.clientHeight;// 滚动条高度const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop;for (let i = 0; i < len; i++) {const offsetHeight = imgs[i].offsetTop;if (offsetHeight < viewHeight + scrollHeight) {const src = imgs[i].dataset.src;imgs[i].src = src;}}
}// 可以使用节流优化一下
window.addEventListener('scroll', lazyload);
实现防抖函数(debounce)
防抖函数原理:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
那么与节流函数的区别直接看这个动画实现即可。
手写简化版:
// 防抖函数
const debounce = (fn, delay) => {let timer = null;return (...args) => {clearTimeout(timer);timer = setTimeout(() => {fn.apply(this, args);}, delay);};
};
适用场景:
- 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
- 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似
生存环境请用lodash.debounce
event模块
实现node中回调函数的机制,node中回调函数其实是内部使用了观察者模式。
观察者模式:定义了对象间一种一对多的依赖关系,当目标对象Subject发生改变时,所有依赖它的对象Observer都会得到通知。
function EventEmitter() {this.events = new Map();
}// 需要实现的一些方法:
// addListener、removeListener、once、removeAllListeners、emit// 模拟实现addlistener方法
const wrapCallback = (fn, once = false) => ({ callback: fn, once });
EventEmitter.prototype.addListener = function(type, fn, once = false) {const hanlder = this.events.get(type);if (!hanlder) {// 没有type绑定事件this.events.set(type, wrapCallback(fn, once));} else if (hanlder && typeof hanlder.callback === 'function') {// 目前type事件只有一个回调this.events.set(type, [hanlder, wrapCallback(fn, once)]);} else {// 目前type事件数>=2hanlder.push(wrapCallback(fn, once));}
}
// 模拟实现removeListener
EventEmitter.prototype.removeListener = function(type, listener) {const hanlder = this.events.get(type);if (!hanlder) return;if (!Array.isArray(this.events)) {if (hanlder.callback === listener.callback) this.events.delete(type);else return;}for (let i = 0; i < hanlder.length; i++) {const item = hanlder[i];if (item.callback === listener.callback) {hanlder.splice(i, 1);i--;if (hanlder.length === 1) {this.events.set(type, hanlder[0]);}}}
}
// 模拟实现once方法
EventEmitter.prototype.once = function(type, listener) {this.addListener(type, listener, true);
}
// 模拟实现emit方法
EventEmitter.prototype.emit = function(type, ...args) {const hanlder = this.events.get(type);if (!hanlder) return;if (Array.isArray(hanlder)) {hanlder.forEach(item => {item.callback.apply(this, args);if (item.once) {this.removeListener(type, item);}})} else {hanlder.callback.apply(this, args);if (hanlder.once) {this.events.delete(type);}}return true;
}
EventEmitter.prototype.removeAllListeners = function(type) {const hanlder = this.events.get(type);if (!hanlder) return;this.events.delete(type);
}
参考 前端进阶面试题详细解答
实现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
使用Promise封装AJAX请求
// promise 封装实现:
function getJSON(url) {// 创建一个 promise 对象let promise = new Promise(function(resolve, reject) {let xhr = new XMLHttpRequest();// 新建一个 http 请求xhr.open("GET", url, true);// 设置状态的监听函数xhr.onreadystatechange = function() {if (this.readyState !== 4) return;// 当请求成功或失败时,改变 promise 的状态if (this.status === 200) {resolve(this.response);} else {reject(new Error(this.statusText));}};// 设置错误监听函数xhr.onerror = function() {reject(new Error(this.statusText));};// 设置响应的数据类型xhr.responseType = "json";// 设置请求头信息xhr.setRequestHeader("Accept", "application/json");// 发送 http 请求xhr.send(null);});return promise;
}
解析 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 和 valueval = 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;
}
类数组转化为数组
类数组是具有length属性,但不具有数组原型上的方法。常见的类数组有arguments、DOM操作方法返回的结果。
方法一:Array.from
Array.from(document.querySelectorAll('div'))
方法二:Array.prototype.slice.call()
Array.prototype.slice.call(document.querySelectorAll('div'))
方法三:扩展运算符
[...document.querySelectorAll('div')]
方法四:利用concat
Array.prototype.concat.apply([], document.querySelectorAll('div'));
AJAX
const getJSON = function(url) {return new Promise((resolve, reject) => {const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp');xhr.open('GET', url, false);xhr.setRequestHeader('Accept', 'application/json');xhr.onreadystatechange = function() {if (xhr.readyState !== 4) return;if (xhr.status === 200 || xhr.status === 304) {resolve(xhr.responseText);} else {reject(new Error(xhr.responseText));}}xhr.send();})
}
字符串解析问题
var a = {b: 123,c: '456',e: '789',
}
var str=`a{a.b}aa{a.c}aa {a.d}aaaa`;
// => 'a123aa456aa {a.d}aaaa'
实现函数使得将str字符串中的{}
内的变量替换,如果属性不存在保持原样(比如{a.d}
)
类似于模版字符串,但有一点出入,实际上原理大差不差
const fn1 = (str, obj) => {let res = '';// 标志位,标志前面是否有{let flag = false;let start;for (let i = 0; i < str.length; i++) {if (str[i] === '{') {flag = true;start = i + 1;continue;}if (!flag) res += str[i];else {if (str[i] === '}') {flag = false;res += match(str.slice(start, i), obj);}}}return res;
}
// 对象匹配操作
const match = (str, obj) => {const keys = str.split('.').slice(1);let index = 0;let o = obj;while (index < keys.length) {const key = keys[index];if (!o[key]) {return `{${str}}`;} else {o = o[key];}index++;}return o;
}
将VirtualDom转化为真实DOM结构
这是当前SPA应用的核心概念之一
// vnode结构:
// {// tag,
// attrs,
// children,
// }//Virtual DOM => DOM
function render(vnode, container) {container.appendChild(_render(vnode));
}
function _render(vnode) {// 如果是数字类型转化为字符串if (typeof vnode === 'number') {vnode = String(vnode);}// 字符串类型直接就是文本节点if (typeof vnode === 'string') {return document.createTextNode(vnode);}// 普通DOMconst dom = document.createElement(vnode.tag);if (vnode.attrs) {// 遍历属性Object.keys(vnode.attrs).forEach(key => {const value = vnode.attrs[key];dom.setAttribute(key, value);})}// 子数组进行递归操作vnode.children.forEach(child => render(child, dom));return dom;
}
打印出当前网页使用了多少种HTML元素
一行代码可以解决:
const fn = () => {return [...new Set([...document.querySelectorAll('*')].map(el => el.tagName))].length;
}
值得注意的是:DOM操作返回的是类数组,需要转换为数组之后才可以调用数组的方法。
原型继承
这里只写寄生组合继承了,中间还有几个演变过来的继承但都有一些缺陷
function Parent() {this.name = 'parent';
}
function Child() {Parent.call(this);this.type = 'children';
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
实现简单路由
// hash路由
class Route{constructor(){// 路由存储对象this.routes = {}// 当前hashthis.currentHash = ''// 绑定this,避免监听时this指向改变this.freshRoute = this.freshRoute.bind(this)// 监听window.addEventListener('load', this.freshRoute, false)window.addEventListener('hashchange', this.freshRoute, false)}// 存储storeRoute (path, cb) {this.routes[path] = cb || function () {}}// 更新freshRoute () {this.currentHash = location.hash.slice(1) || '/'this.routes[this.currentHash]()}
}
实现数组元素求和
- arr=[1,2,3,4,5,6,7,8,9,10],求和
let arr=[1,2,3,4,5,6,7,8,9,10]
let sum = arr.reduce( (total,i) => total += i,0);
console.log(sum);
- arr=[1,2,3,[[4,5],6],7,8,9],求和
var = arr=[1,2,3,[[4,5],6],7,8,9]
let arr= arr.toString().split(',').reduce( (total,i) => total += Number(i),0);
console.log(arr);
递归实现:
let arr = [1, 2, 3, 4, 5, 6] function add(arr) {if (arr.length == 1) return arr[0] return arr[0] + add(arr.slice(1))
}
console.log(add(arr)) // 21
实现类的继承
类的继承在几年前是重点内容,有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: 你打篮球的样子像kunkunvar child = new Child('cxk', 'father');
child.say() // father好,我是练习时长两年半的cxk
封装异步的fetch,使用async await方式来使用
(async () => {class HttpRequestUtil {async get(url) {const res = await fetch(url);const data = await res.json();return data;}async post(url, data) {const res = await fetch(url, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(data)});const result = await res.json();return result;}async put(url, data) {const res = await fetch(url, {method: 'PUT',headers: {'Content-Type': 'application/json'},data: JSON.stringify(data)});const result = await res.json();return result;}async delete(url, data) {const res = await fetch(url, {method: 'DELETE',headers: {'Content-Type': 'application/json'},data: JSON.stringify(data)});const result = await res.json();return result;}}const httpRequestUtil = new HttpRequestUtil();const res = await httpRequestUtil.get('http://golderbrother.cn/');console.log(res);
})();
实现 add(1)(2)(3)
函数柯里化概念: 柯里化(Currying)是把接受多个参数的函数转变为接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。
1)粗暴版
function add (a) {return function (b) {return function (c) {return a + b + c;}
}
}
console.log(add(1)(2)(3)); // 6
2)柯里化解决方案
- 参数长度固定
var add = function (m) {var temp = function (n) {return add(m + n);}temp.toString = function () {return m;}return temp;
};
console.log(add(3)(4)(5)); // 12
console.log(add(3)(6)(9)(25)); // 43
对于add(3)(4)(5),其执行过程如下:
先执行add(3),此时m=3,并且返回temp函数;
执行temp(4),这个函数内执行add(m+n),n是此次传进来的数值4,m值还是上一步中的3,所以add(m+n)=add(3+4)=add(7),此时m=7,并且返回temp函数
执行temp(5),这个函数内执行add(m+n),n是此次传进来的数值5,m值还是上一步中的7,所以add(m+n)=add(7+5)=add(12),此时m=12,并且返回temp函数
由于后面没有传入参数,等于返回的temp函数不被执行而是打印,了解JS的朋友都知道对象的toString是修改对象转换字符串的方法,因此代码中temp函数的toString函数return m值,而m值是最后一步执行函数时的值m=12,所以返回值是12。
- 参数长度不固定
function add (...args) {//求和return args.reduce((a, b) => a + b)
}
function currying (fn) {let args = []return function temp (...newArgs) {if (newArgs.length) {args = [...args,...newArgs]return temp} else {let val = fn.apply(this, args)args = [] //保证再次调用时清空return val}}
}
let addCurry = currying(add)
console.log(addCurry(1)(2)(3)(4, 5)()) //15
console.log(addCurry(1)(2)(3, 4, 5)()) //15
console.log(addCurry(1)(2, 3, 4, 5)()) //15
高级前端常见手写面试题指南相关推荐
- 前端常见手写面试题集锦
实现迭代器生成函数 我们说迭代器对象全凭迭代器生成函数帮我们生成.在ES6中,实现一个迭代器生成函数并不是什么难事儿,因为ES6早帮我们考虑好了全套的解决方案,内置了贴心的 生成器 (Generato ...
- 前端常见手写面试题合集
实现一个函数判断数据类型 function getType(obj) {if (obj === null) return String(obj);return typeof obj === 'obje ...
- 前端工程师常考手写面试题指南
实现 add(1)(2)(3) 函数柯里化概念: 柯里化(Currying)是把接受多个参数的函数转变为接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术. 1)粗暴版 funct ...
- 前端高频手写面试题总结
实现字符串的repeat方法 输入字符串s,以及其重复的次数,输出重复的结果,例如输入abc,2,输出abcabc. function repeat(s, n) {return (new Array( ...
- 【2022前端面试】CSS手写面试题汇总(加紧收藏)
[2022前端面试]CSS手写面试题汇总(加紧收藏) 更新时间:2022年3月3日 把答案一起写上,但是希望大家在看之前思考一下,如果有好的建议,跪求改正! 本文致力于建设前端面试题库,欢迎兄弟们投稿 ...
- 前端常考手写面试题汇总
实现一个函数判断数据类型 function getType(obj) {if (obj === null) return String(obj);return typeof obj === 'obje ...
- 高级前端必会手写面试题及答案
循环打印红黄绿 下面来看一道比较典型的问题,通过这个问题来对比几种异步编程方法:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次:如何让三个灯不断交替重复亮灯? 三个亮灯函数: functi ...
- 高级前端常考手写面试题(必备)
实现单例模式 核心要点: 用闭包和Proxy属性拦截 function proxy(func) {let instance;let handler = {constructor(target, arg ...
- 社招前端必会手写面试题集锦
查找字符串中出现最多的字符和个数 例: abbcccddddd -> 字符最多的是d,出现了5次 let str = "abcabcabcbbccccc"; let num ...
最新文章
- 第十八章 MySQL Workbench5.2使用(待续)
- 服务器内存使用率高找不到是哪个进程,内存占用率高,但是找不到内存消耗大的程序...
- 1.4建立网站的基本流程
- SQL 养成一个好习惯是一笔财富
- 根据netmask快速判断是否在一个网域
- MFC画图(画线、画矩形、画刷画笔的使用)
- 值得学习的100个网站推广方法。新站推广必备
- js开源框架最新版下载
- fat32 linux 打包工具_UbuntuLinux默认安装图形化的压缩工具是File-Roller文件打包器.ppt...
- 说话心理学 个人笔记
- 特征检测和特征提取算子
- 计算机电脑的时区怎么弄,电脑时区怎么设置,教你win10中国时区UTC设置教程
- “Deep Freeze冰点还原”解冻操作
- 浏览器及Windows常用快捷键汇总
- 视频号怎么赚钱?视频号有什么功能?
- 如何用divi主题做独立站?
- 51单片机c语言相位计,相位计.doc
- vulnhub刷题记录(The Planets: Earth)
- 使用synchronized实现Lock接口的lock和unlock方法
- unity弹幕功能实现
热门文章
- PTA 最大子序列和
- 介词短语做定语,修饰宾语
- 你知道Hello World程序的由来吗?
- Clang Builtin函数格式说明
- 如何用Mac给移动硬盘分区
- linux 符号执行,基于程序切片的符号执行,Symbolic execution based on program slicing,音标,读音,翻译,英文例句,英语词典...
- 关于linuxC语言中创建进程,利用execlp函数执行.c程序问题
- 用图片打造的网站,原来并不是不能优化的
- 云平台下的运维体系搭建
- Linux、ubuntu系统下查看显卡型号、显卡信息详解