写点什么

前端高频手写面试题

  • 2022-10-25
    浙江
  • 本文字数:7561 字

    阅读完需:约 25 分钟

滚动加载

原理就是监听页面滚动事件,分析 clientHeightscrollTopscrollHeight 三者的属性关系。


window.addEventListener('scroll', function() {  const clientHeight = document.documentElement.clientHeight;  const scrollTop = document.documentElement.scrollTop;  const scrollHeight = document.documentElement.scrollHeight;  if (clientHeight + scrollTop >= scrollHeight) {    // 检测到滚动至页面底部,进行后续操作    // ...  }}, false);
复制代码

将虚拟 Dom 转化为真实 Dom

{  tag: 'DIV',  attrs:{  id:'app'  },  children: [    {      tag: 'SPAN',      children: [        { tag: 'A', children: [] }      ]    },    {      tag: 'SPAN',      children: [        { tag: 'A', children: [] },        { tag: 'A', children: [] }      ]    }  ]}
把上面虚拟Dom转化成下方真实Dom
<div id="app"> <span> <a></a> </span> <span> <a></a> <a></a> </span></div>
复制代码


实现


// 真正的渲染函数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) => dom.appendChild(_render(child)));  return dom;}
复制代码

实现 JSON.parse

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


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

实现字符串的 repeat 方法

输入字符串 s,以及其重复的次数,输出重复的结果,例如输入 abc,2,输出 abcabc。


function repeat(s, n) {    return (new Array(n + 1)).join(s);}
复制代码


递归:


function repeat(s, n) {    return (n > 0) ? s.concat(repeat(s, --n)) : "";}
复制代码

深克隆(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. 另外对于确保没有循环引用的对象,我们可以省去对循环引用的特殊处理,因为这很消耗时间


原理详解实现深克隆

实现防抖函数(debounce)

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


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


手写简化版:


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


适用场景:


  • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次

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


生存环境请用 lodash.debounce

实现数组的 push 方法

let arr = [];Array.prototype.push = function() {    for( let i = 0 ; i < arguments.length ; i++){        this[this.length] = arguments[i] ;    }    return this.length;}
复制代码

字符串查找

请使用最基本的遍历来实现判断字符串 a 是否被包含在字符串 b 中,并返回第一次出现的位置(找不到返回 -1)。


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

复制代码

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);}
复制代码


参考:前端手写面试题详细解答

打印出当前网页使用了多少种 HTML 元素

一行代码可以解决:


const fn = () => {  return [...new Set([...document.querySelectorAll('*')].map(el => el.tagName))].length;}
复制代码


值得注意的是:DOM 操作返回的是类数组,需要转换为数组之后才可以调用数组的方法。

实现防抖函数(debounce)

防抖函数原理:把触发非常频繁的事件合并成一次去执行 在指定时间内只执行一次回调函数,如果在指定的时间内又触发了该事件,则回调函数的执行时间会基于此刻重新开始计算



防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行


eg. 像百度搜索,就应该用防抖,当我连续不断输入时,不会发送请求;当我一段时间内不输入了,才会发送一次请求;如果小于这段时间继续输入的话,时间会重新计算,也不会发送请求。


手写简化版:


// func是用户传入需要防抖的函数// wait是等待时间const debounce = (func, wait = 50) => {  // 缓存一个定时器id  let timer = 0  // 这里返回的函数是每次用户实际调用的防抖函数  // 如果已经设定过定时器了就清空上一次的定时器  // 开始一个新的定时器,延迟执行用户传入的方法  return function(...args) {    if (timer) clearTimeout(timer)    timer = setTimeout(() => {      func.apply(this, args)    }, wait)  }}
复制代码


适用场景:


  • 文本输入的验证,连续输入文字后发送 AJAX 请求进行验证,验证一次就好

  • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次

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

Object.assign

Object.assign()方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象(请注意这个操作是浅拷贝)


Object.defineProperty(Object, 'assign', {  value: function(target, ...args) {    if (target == null) {      return new TypeError('Cannot convert undefined or null to object');    }
// 目标对象需要统一是引用数据类型,若不是会自动转换 const to = Object(target);
for (let i = 0; i < args.length; i++) { // 每一个源对象 const nextSource = args[i]; if (nextSource !== null) { // 使用for...in和hasOwnProperty双重判断,确保只拿到本身的属性、方法(不包含继承的) for (const nextKey in nextSource) { if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { to[nextKey] = nextSource[nextKey]; } } } } return to; }, // 不可枚举 enumerable: false, writable: true, configurable: true,})
复制代码

Object.is

Object.is解决的主要是这两个问题:


+0 === -0  // trueNaN === NaN // false
复制代码


const is= (x, y) => {  if (x === y) {    // +0和-0应该不相等    return x !== 0 || y !== 0 || 1/x === 1/y;  } else {    return x !== x && y !== y;  }}
复制代码

Promise.all

Promise.all是支持链式调用的,本质上就是返回了一个 Promise 实例,通过resolvereject来改变实例状态。


Promise.myAll = function(promiseArr) {  return new Promise((resolve, reject) => {    const ans = [];    let index = 0;    for (let i = 0; i < promiseArr.length; i++) {      promiseArr[i]      .then(res => {        ans[i] = res;        index++;        if (index === promiseArr.length) {          resolve(ans);        }      })      .catch(err => reject(err));    }  })}
复制代码

手写 apply 函数

apply 函数的实现步骤:


  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。

  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window 。

  3. 将函数作为上下文对象的一个属性。

  4. 判断参数值是否传入

  5. 使用上下文对象来调用这个方法,并保存返回结果。

  6. 删除刚才新增的属性

  7. 返回结果


// apply 函数实现Function.prototype.myApply = function(context) {  // 判断调用对象是否为函数  if (typeof this !== "function") {    throw new TypeError("Error");  }  let result = null;  // 判断 context 是否存在,如果未传入则为 window  context = context || window;  // 将函数设为对象的方法  context.fn = this;  // 调用方法  if (arguments[1]) {    result = context.fn(...arguments[1]);  } else {    result = context.fn();  }  // 将属性删除  delete context.fn;  return result;};
复制代码

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();  })}
复制代码

将数字每千分位用逗号隔开

数字有小数版本:


let format = n => {    let num = n.toString() // 转成字符串    let decimals = ''        // 判断是否有小数    num.indexOf('.') > -1 ? decimals = num.split('.')[1] : decimals    let len = num.length    if (len <= 3) {        return num    } else {        let temp = ''        let remainder = len % 3        decimals ? temp = '.' + decimals : temp        if (remainder > 0) { // 不是3的整数倍            return num.slice(0, remainder) + ',' + num.slice(remainder, len).match(/\d{3}/g).join(',') + temp        } else { // 是3的整数倍            return num.slice(0, len).match(/\d{3}/g).join(',') + temp         }    }}format(12323.33)  // '12,323.33'
复制代码


数字无小数版本:


let format = n => {    let num = n.toString()     let len = num.length    if (len <= 3) {        return num    } else {        let remainder = len % 3        if (remainder > 0) { // 不是3的整数倍            return num.slice(0, remainder) + ',' + num.slice(remainder, len).match(/\d{3}/g).join(',')         } else { // 是3的整数倍            return num.slice(0, len).match(/\d{3}/g).join(',')         }    }}format(1232323)  // '1,232,323'
复制代码

JSONP

script 标签不遵循同源协议,可以用来进行跨域请求,优点就是兼容性好但仅限于 GET 请求


const jsonp = ({ url, params, callbackName }) => {  const generateUrl = () => {    let dataSrc = '';    for (let key in params) {      if (Object.prototype.hasOwnProperty.call(params, key)) {        dataSrc += `${key}=${params[key]}&`;      }    }    dataSrc += `callback=${callbackName}`;    return `${url}?${dataSrc}`;  }  return new Promise((resolve, reject) => {    const scriptEle = document.createElement('script');    scriptEle.src = generateUrl();    document.body.appendChild(scriptEle);    window[callbackName] = data => {      resolve(data);      document.removeChild(scriptEle);    }  })}
复制代码

深拷贝

递归的完整版本(考虑到了 Symbol 属性):


const cloneDeep1 = (target, hash = new WeakMap()) => {  // 对于传入参数处理  if (typeof target !== 'object' || target === null) {    return target;  }  // 哈希表中存在直接返回  if (hash.has(target)) return hash.get(target);
const cloneTarget = Array.isArray(target) ? [] : {}; hash.set(target, cloneTarget);
// 针对Symbol属性 const symKeys = Object.getOwnPropertySymbols(target); if (symKeys.length) { symKeys.forEach(symKey => { if (typeof target[symKey] === 'object' && target[symKey] !== null) { cloneTarget[symKey] = cloneDeep1(target[symKey]); } else { cloneTarget[symKey] = target[symKey]; } }) }
for (const i in target) { if (Object.prototype.hasOwnProperty.call(target, i)) { cloneTarget[i] = typeof target[i] === 'object' && target[i] !== null ? cloneDeep1(target[i], hash) : target[i]; } } return cloneTarget;}
复制代码

实现每隔一秒打印 1,2,3,4

// 使用闭包实现for (var i = 0; i < 5; i++) {  (function(i) {    setTimeout(function() {      console.log(i);    }, i * 1000);  })(i);}// 使用 let 块级作用域for (let i = 0; i < 5; i++) {  setTimeout(function() {    console.log(i);  }, i * 1000);}
复制代码


用户头像

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

还未添加个人简介

评论

发布
暂无评论
前端高频手写面试题_JavaScript_helloworld1024fd_InfoQ写作社区