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),其执行过程如下:

  1. 先执行add(3),此时m=3,并且返回temp函数;

  2. 执行temp(4),这个函数内执行add(m+n),n是此次传进来的数值4,m值还是上一步中的3,所以add(m+n)=add(3+4)=add(7),此时m=7,并且返回temp函数

  3. 执行temp(5),这个函数内执行add(m+n),n是此次传进来的数值5,m值还是上一步中的7,所以add(m+n)=add(7+5)=add(12),此时m=12,并且返回temp函数

  4. 由于后面没有传入参数,等于返回的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

高级前端常见手写面试题指南相关推荐

  1. 前端常见手写面试题集锦

    实现迭代器生成函数 我们说迭代器对象全凭迭代器生成函数帮我们生成.在ES6中,实现一个迭代器生成函数并不是什么难事儿,因为ES6早帮我们考虑好了全套的解决方案,内置了贴心的 生成器 (Generato ...

  2. 前端常见手写面试题合集

    实现一个函数判断数据类型 function getType(obj) {if (obj === null) return String(obj);return typeof obj === 'obje ...

  3. 前端工程师常考手写面试题指南

    实现 add(1)(2)(3) 函数柯里化概念: 柯里化(Currying)是把接受多个参数的函数转变为接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术. 1)粗暴版 funct ...

  4. 前端高频手写面试题总结

    实现字符串的repeat方法 输入字符串s,以及其重复的次数,输出重复的结果,例如输入abc,2,输出abcabc. function repeat(s, n) {return (new Array( ...

  5. 【2022前端面试】CSS手写面试题汇总(加紧收藏)

    [2022前端面试]CSS手写面试题汇总(加紧收藏) 更新时间:2022年3月3日 把答案一起写上,但是希望大家在看之前思考一下,如果有好的建议,跪求改正! 本文致力于建设前端面试题库,欢迎兄弟们投稿 ...

  6. 前端常考手写面试题汇总

    实现一个函数判断数据类型 function getType(obj) {if (obj === null) return String(obj);return typeof obj === 'obje ...

  7. 高级前端必会手写面试题及答案

    循环打印红黄绿 下面来看一道比较典型的问题,通过这个问题来对比几种异步编程方法:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次:如何让三个灯不断交替重复亮灯? 三个亮灯函数: functi ...

  8. 高级前端常考手写面试题(必备)

    实现单例模式 核心要点: 用闭包和Proxy属性拦截 function proxy(func) {let instance;let handler = {constructor(target, arg ...

  9. 社招前端必会手写面试题集锦

    查找字符串中出现最多的字符和个数 例: abbcccddddd -> 字符最多的是d,出现了5次 let str = "abcabcabcbbccccc"; let num ...

最新文章

  1. 第十八章 MySQL Workbench5.2使用(待续)
  2. 服务器内存使用率高找不到是哪个进程,内存占用率高,但是找不到内存消耗大的程序...
  3. 1.4建立网站的基本流程
  4. SQL 养成一个好习惯是一笔财富
  5. 根据netmask快速判断是否在一个网域
  6. MFC画图(画线、画矩形、画刷画笔的使用)
  7. 值得学习的100个网站推广方法。新站推广必备
  8. js开源框架最新版下载
  9. fat32 linux 打包工具_UbuntuLinux默认安装图形化的压缩工具是File-Roller文件打包器.ppt...
  10. 说话心理学 个人笔记
  11. 特征检测和特征提取算子
  12. 计算机电脑的时区怎么弄,电脑时区怎么设置,教你win10中国时区UTC设置教程
  13. “Deep Freeze冰点还原”解冻操作
  14. 浏览器及Windows常用快捷键汇总
  15. 视频号怎么赚钱?视频号有什么功能?
  16. 如何用divi主题做独立站?
  17. 51单片机c语言相位计,相位计.doc
  18. vulnhub刷题记录(The Planets: Earth)
  19. 使用synchronized实现Lock接口的lock和unlock方法
  20. unity弹幕功能实现

热门文章

  1. PTA 最大子序列和
  2. 介词短语做定语,修饰宾语
  3. 你知道Hello World程序的由来吗?
  4. Clang Builtin函数格式说明
  5. 如何用Mac给移动硬盘分区
  6. linux 符号执行,基于程序切片的符号执行,Symbolic execution based on program slicing,音标,读音,翻译,英文例句,英语词典...
  7. 关于linuxC语言中创建进程,利用execlp函数执行.c程序问题
  8. 用图片打造的网站,原来并不是不能优化的
  9. 云平台下的运维体系搭建
  10. Linux、ubuntu系统下查看显卡型号、显卡信息详解