写点什么

高级前端常见手写面试题指南

  • 2022-12-14
    浙江
  • 本文字数:9957 字

    阅读完需:约 33 分钟

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.length    return {        // 自定义next方法        next: function() {            // 如果索引还没有超出集合长度,done为false            var 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事件数>=2 hanlder.push(wrapCallback(fn, once)); }}// 模拟实现removeListenerEventEmitter.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 和 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;}
复制代码

类数组转化为数组

类数组是具有 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 => DOMfunction 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); } // 普通DOM const 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 = {}    // 当前hash    this.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: 你打篮球的样子像kunkun
var 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)); // 12console.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)())  //15console.log(addCurry(1)(2)(3, 4, 5)())  //15console.log(addCurry(1)(2, 3, 4, 5)())  //15
复制代码


用户头像

还未添加个人签名 2022-07-31 加入

还未添加个人简介

评论

发布
暂无评论
高级前端常见手写面试题指南_JavaScript_helloworld1024fd_InfoQ写作社区