乐趣区

关于javascript:记录面试前端遇到的手写题

滚动加载

原理就是监听页面滚动事件,剖析 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'; // 返回 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;
}

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));
  }
}
// 模仿实现 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);
}

参考:前端手写面试题具体解答

打印出以后网页应用了多少种 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  // true
NaN === 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);
}
退出移动版