乐趣区

关于前端:社招前端一面经典手写面试题集锦

对象数组列表转成树形构造(解决菜单)

[
    {
        id: 1,
        text: '节点 1',
        parentId: 0 // 这里用 0 示意为顶级节点
    },
    {
        id: 2,
        text: '节点 1_1',
        parentId: 1 // 通过这个字段来确定子父级
    }
    ...
]

转成
[
    {
        id: 1,
        text: '节点 1',
        parentId: 0,
        children: [
            {
                id:2,
                text: '节点 1_1',
                parentId:1
            }
        ]
    }
]

实现代码如下:

function listToTree(data) {let temp = {};
  let treeData = [];
  for (let i = 0; i < data.length; i++) {temp[data[i].id] = data[i];
  }
  for (let i in temp) {if (+temp[i].parentId != 0) {if (!temp[temp[i].parentId].children) {temp[temp[i].parentId].children = [];}
      temp[temp[i].parentId].children.push(temp[i]);
    } else {treeData.push(temp[i]);
    }
  }
  return treeData;
}

前端手写面试题具体解答

实现一个 padStart()或 padEnd()的 polyfil

String.prototype.padStartString.prototype.padEndES8 中新增的办法,容许将空字符串或其余字符串增加到原始字符串的结尾或结尾。咱们先看下应用语法:

String.padStart(targetLength,[padString])

用法:

'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'

// 1. 若是输出的指标长度小于字符串本来的长度则返回字符串自身
'xxx'.padStart(2, 's') // 'xxx'

// 2. 第二个参数的默认值为 " ",长度是为 1 的
// 3. 而此参数可能是个不确定长度的字符串,若是要填充的内容达到了指标长度,则将不要的局部截取
'xxx'.padStart(5, 'sss') // ssxxx

// 4. 可用来解决日期、金额格式化问题
'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"

polyfill 实现:

String.prototype.myPadStart = function (targetLen, padString = " ") {if (!targetLen) {throw new Error('请输出须要填充到的长度');
  }
  let originStr = String(this); // 获取到调用的字符串, 因为 this 本来是 String{},所以须要用 String 转为字符串
  let originLen = originStr.length; // 调用的字符串本来的长度
  if (originLen >= targetLen) return originStr; // 若是 本来 > 指标 则返回本来字符串
  let diffNum = targetLen - originLen; // 10 - 6 // 差值
  for (let i = 0; i < diffNum; i++) { // 要增加几个成员
    for (let j = 0; j < padString.length; j++) { // 输出的 padString 的长度可能不为 1
      if (originStr.length === targetLen) break; // 判断每一次增加之后是否到了指标长度
      originStr = `${padString[j]}${originStr}`;
    }
    if (originStr.length === targetLen) break;
  }
  return originStr;
}
console.log('xxx'.myPadStart(16))
console.log('xxx'.padStart(16))

还是比较简单的,而 padEnd 的实现和它一样,只须要把第二层 for 循环里的 ${padString[j]}${orignStr} 换下地位就能够了。

实现字符串翻转

在字符串的原型链上增加一个办法,实现字符串翻转:

String.prototype._reverse = function(a){return a.split("").reverse().join("");
}
var obj = new String();
var res = obj._reverse ('hello');
console.log(res);    // olleh

须要留神的是,必须通过实例化对象之后再去调用定义的办法,不然找不到该办法。

实现 instanceOf

// 模仿 instanceof
function instance_of(L, R) {
  //L 示意左表达式,R 示意右表达式
  var O = R.prototype; // 取 R 的显示原型
  L = L.__proto__; // 取 L 的隐式原型
  while (true) {if (L === null) return false;
    if (O === L)
      // 这里重点:当 O 严格等于 L 时,返回 true
      return true;
    L = L.__proto__;
  }
}

实现观察者模式

观察者模式(基于公布订阅模式)有观察者,也有被观察者

观察者须要放到被观察者中,被观察者的状态变动须要告诉观察者 我变动了 外部也是基于公布订阅模式,收集观察者,状态变动后要被动告诉观察者

class Subject { // 被观察者 学生
  constructor(name) {
    this.state = 'happy'
    this.observers = []; // 存储所有的观察者}
  // 收集所有的观察者
  attach(o){ // Subject. prototype. attch
    this.observers.push(o)
  }
  // 更新被观察者 状态的办法
  setState(newState) {
    this.state = newState; // 更新状态
    // this 指被观察者 学生
    this.observers.forEach(o => o.update(this)) // 告诉观察者 更新它们的状态
  }
}

class Observer{ // 观察者 父母和老师
  constructor(name) {this.name = name}
  update(student) {console.log('以后' + this.name + '被告诉了', '以后学生的状态是' + student.state)
  }
}

let student = new Subject('学生'); 

let parent = new Observer('父母'); 
let teacher = new Observer('老师'); 

// 被观察者存储观察者的前提,须要先接收观察者
student. attach(parent); 
student. attach(teacher); 
student. setState('被欺侮了');

实现 some 办法

Array.prototype.mySome=function(callback, context = window){
             var len = this.length,
                 flag=false,
           i = 0;

             for(;i < len; i++){if(callback.apply(context, [this[i], i , this])){
                    flag=true;
                    break;
                } 
             }
             return flag;
        }

        // var flag=arr.mySome((v,index,arr)=>v.num>=10,obj)
        // console.log(flag);

实现一下 hash 路由

根底的 html 代码:

<html>
  <style>
    html, body {
      margin: 0;
      height: 100%;
    }
    ul {
      list-style: none;
      margin: 0;
      padding: 0;
      display: flex;
      justify-content: center;
    }
    .box {
      width: 100%;
      height: 100%;
      background-color: red;
    }
  </style>
  <body>
  <ul>
    <li>
      <a href="#red"> 红色 </a>
    </li>
    <li>
      <a href="#green"> 绿色 </a>
    </li>
    <li>
      <a href="#purple"> 紫色 </a>
    </li>
  </ul>
  </body>
</html>

简略实现:

<script>
  const box = document.getElementsByClassName('box')[0];
  const hash = location.hash
  window.onhashchange = function (e) {const color = hash.slice(1)
    box.style.background = color
  }
</script>

封装成一个 class:

<script>
  const box = document.getElementsByClassName('box')[0];
  const hash = location.hash
  class HashRouter {constructor (hashStr, cb) {
      this.hashStr = hashStr
      this.cb = cb
      this.watchHash()
      this.watch = this.watchHash.bind(this)
      window.addEventListener('hashchange', this.watch)
    }
    watchHash () {let hash = window.location.hash.slice(1)
      this.hashStr = hash
      this.cb(hash)
    }
  }
  new HashRouter('red', (color) => {box.style.background = color})
</script>

版本号排序的办法

题目形容: 有一组版本号如下 ['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5']。当初须要对其进行排序,排序的后果为 ['4.3.5','4.3.4.5','2.3.3','0.302.1','0.1.1']

arr.sort((a, b) => {
  let i = 0;
  const arr1 = a.split(".");
  const arr2 = b.split(".");

  while (true) {const s1 = arr1[i];
    const s2 = arr2[i];
    i++;
    if (s1 === undefined || s2 === undefined) {return arr2.length - arr1.length;}

    if (s1 === s2) continue;

    return s2 - s1;
  }
});
console.log(arr);

手写深度比拟 isEqual

思路:深度比拟两个对象,就是要深度比拟对象的每一个元素。=> 递归

  • 递归退出条件:

    • 被比拟的是两个值类型变量,间接用“===”判断
    • 被比拟的两个变量之一为null,直接判断另一个元素是否也为null
  • 提前结束递推:

    • 两个变量 keys 数量不同
    • 传入的两个参数是同一个变量
  • 递推工作:深度比拟每一个key
function isEqual(obj1, obj2){
    // 其中一个为值类型或 null
    if(!isObject(obj1) || !isObject(obj2)){return obj1 === obj2;}

    // 判断是否两个参数是同一个变量
    if(obj1 === obj2){return true;}

    // 判断 keys 数是否相等
    const obj1Keys = Object.keys(obj1);
    const obj2Keys = Object.keys(obj2);
    if(obj1Keys.length !== obj2Keys.length){return false;}

    // 深度比拟每一个 key
    for(let key in obj1){if(!isEqual(obj1[key], obj2[key])){return false;}
    }

    return true;
}

实现 Array.isArray 办法

Array.myIsArray = function(o) {return Object.prototype.toString.call(Object(o)) === '[object Array]';
};

console.log(Array.myIsArray([])); // true

手写防抖函数

函数防抖是指在事件被触发 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);
  };
}

设计一个办法提取对象中所有 value 大于 2 的键值对并返回最新的对象

实现:

var obj = {a: 1, b: 3, c: 4}
foo(obj) // {b: 3, c: 4}

办法有很多种,这里提供一种比拟简洁的写法,用到了 ES10Object.fromEntries()

var obj = {a: 1, b: 3, c: 4}
function foo (obj) {
  return Object.fromEntries(Object.entries(obj).filter(([key, value]) => value > 2)
  )
}
var obj2 = foo(obj) // {b: 3, c: 4}
console.log(obj2)
// ES8 中 Object.entries()的作用:var obj = {a: 1, b: 2}
var entries = Object.entries(obj); // [['a', 1], ['b', 2]]
// ES10 中 Object.fromEntries()的作用:Object.fromEntries(entries); // {a: 1, b: 2}

实现 call 办法

call 做了什么:

  • 将函数设为对象的属性
  • 执行和删除这个函数
  • 指定 this 到函数并传入给定参数执行函数
  • 如果不传入参数,默认指向为 window
// 模仿 call bar.mycall(null);
// 实现一个 call 办法:// 原理:利用 context.xxx = self obj.xx = func-->obj.xx()
Function.prototype.myCall = function(context = window, ...args) {if (typeof this !== "function") {throw new Error('type error')
  }
  // this-->func  context--> obj  args--> 传递过去的参数

  // 在 context 上加一个惟一值不影响 context 上的属性
  let key = Symbol('key')
  context[key] = this; // context 为调用的上下文,this 此处为函数,将这个函数作为 context 的办法
  // let args = [...arguments].slice(1)   // 第一个参数为 obj 所以删除, 伪数组转为数组

  // 绑定参数 并执行函数
  let result = context[key](...args);
  // 革除定义的 this 不删除会导致 context 属性越来越多
  delete context[key];

  // 返回后果 
  return result;
};
// 用法:f.call(obj,arg1)
function f(a,b){console.log(a+b)
 console.log(this.name)
}
let obj={name:1}
f.myCall(obj,1,2) // 否则 this 指向 window

查找文章中呈现频率最高的单词

function findMostWord(article) {
  // 合法性判断
  if (!article) return;
  // 参数解决
  article = article.trim().toLowerCase();
  let wordList = article.match(/[a-z]+/g),
    visited = [],
    maxNum = 0,
    maxWord = "";
  article = "" + wordList.join("  ") +" ";
  // 遍历判断单词呈现次数
  wordList.forEach(function(item) {if (visited.indexOf(item) < 0) {
      // 退出 visited 
      visited.push(item);
      let word = new RegExp("" + item +" ","g"),
        num = article.match(word).length;
      if (num > maxNum) {
        maxNum = num;
        maxWord = item;
      }
    }
  });
  return maxWord + " " + maxNum;
}

原生实现

function ajax() {let xhr = new XMLHttpRequest() // 实例化,以调用办法
  xhr.open('get', 'https://www.google.com')  // 参数 2,url。参数三:异步
  xhr.onreadystatechange = () => {  // 每当 readyState 属性扭转时,就会调用该函数。if (xhr.readyState === 4) {  //XMLHttpRequest 代理以后所处状态。if (xhr.status >= 200 && xhr.status < 300) {  //200-300 申请胜利
        let string = request.responseText
        //JSON.parse() 办法用来解析 JSON 字符串,结构由字符串形容的 JavaScript 值或对象
        let object = JSON.parse(string)
      }
    }
  }
  request.send() // 用于理论收回 HTTP 申请。不带参数为 GET 申请}

字符串查找

请应用最根本的遍从来实现判断字符串 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;
}

实现日期格式化函数

输出:

dateFormat(new Date('2020-12-01'), 'yyyy/MM/dd') // 2020/12/01
dateFormat(new Date('2020-04-01'), 'yyyy/MM/dd') // 2020/04/01
dateFormat(new Date('2020-04-01'), 'yyyy 年 MM 月 dd 日') // 2020 年 04 月 01 日
const dateFormat = (dateInput, format)=>{var day = dateInput.getDate() 
    var month = dateInput.getMonth() + 1  
    var year = dateInput.getFullYear()   
    format = format.replace(/yyyy/, year)
    format = format.replace(/MM/,month)
    format = format.replace(/dd/,day)
    return format
}

实现 Promise

var PromisePolyfill = (function () {
  // 和 reject 不同的是 resolve 须要尝试开展 thenable 对象
  function tryToResolve (value) {if (this === value) {
    // 次要是避免上面这种状况
    // let y = new Promise(res => setTimeout(res(y)))
      throw TypeError('Chaining cycle detected for promise!')
    }

    // 依据标准 2.32 以及 2.33 对对象或者函数尝试开展
    // 保障 S6 之前的 polyfill 也能和 ES6 的原生 promise 混用
    if (value !== null &&
      (typeof value === 'object' || typeof value === 'function')) {
      try {
      // 这里记录这次 then 的值同时要被 try 包裹
      // 次要起因是 then 可能是一个 getter, 也也就是说
      //   1. value.then 可能报错
      //   2. value.then 可能产生副作用(例如屡次执行可能后果不同)
        var then = value.then

        // 另一方面, 因为无奈保障 then 的确会像预期的那样只调用一个 onFullfilled / onRejected
        // 所以减少了一个 flag 来避免 resolveOrReject 被屡次调用
        var thenAlreadyCalledOrThrow = false
        if (typeof then === 'function') {
        // 是 thenable 那么尝试开展
        // 并且在该 thenable 状态扭转之前 this 对象的状态不变
          then.bind(value)(
          // onFullfilled
            function (value2) {if (thenAlreadyCalledOrThrow) return
              thenAlreadyCalledOrThrow = true
              tryToResolve.bind(this, value2)()}.bind(this),

            // onRejected
            function (reason2) {if (thenAlreadyCalledOrThrow) return
              thenAlreadyCalledOrThrow = true
              resolveOrReject.bind(this, 'rejected', reason2)()}.bind(this)
          )
        } else {
        // 领有 then 然而 then 不是一个函数 所以也不是 thenable
          resolveOrReject.bind(this, 'resolved', value)()}
      } catch (e) {if (thenAlreadyCalledOrThrow) return
        thenAlreadyCalledOrThrow = true
        resolveOrReject.bind(this, 'rejected', e)()}
    } else {
    // 根本类型 间接返回
      resolveOrReject.bind(this, 'resolved', value)()}
  }

  function resolveOrReject (status, data) {if (this.status !== 'pending') return
    this.status = status
    this.data = data
    if (status === 'resolved') {for (var i = 0; i < this.resolveList.length; ++i) {this.resolveList[i]()}
    } else {for (i = 0; i < this.rejectList.length; ++i) {this.rejectList[i]()}
    }
  }

  function Promise (executor) {if (!(this instanceof Promise)) {throw Error('Promise can not be called without new !')
    }

    if (typeof executor !== 'function') {
    // 非标准 但与 Chrome 谷歌保持一致
      throw TypeError('Promise resolver' + executor + 'is not a function')
    }

    this.status = 'pending'
    this.resolveList = []
    this.rejectList = []

    try {executor(tryToResolve.bind(this), resolveOrReject.bind(this, 'rejected'))
    } catch (e) {resolveOrReject.bind(this, 'rejected', e)()}
  }

  Promise.prototype.then = function (onFullfilled, onRejected) {
  // 返回值穿透以及谬误穿透, 留神谬误穿透用的是 throw 而不是 return,否则的话
  // 这个 then 返回的 promise 状态将变成 resolved 即接下来的 then 中的 onFullfilled
  // 会被调用, 然而咱们想要调用的是 onRejected
    if (typeof onFullfilled !== 'function') {onFullfilled = function (data) {return data}
    }
    if (typeof onRejected !== 'function') {onRejected = function (reason) {throw reason}
    }

    var executor = function (resolve, reject) {setTimeout(function () {
        try {
        // 拿到对应的 handle 函数解决 this.data
        // 并以此为根据解析这个新的 Promise
          var value = this.status === 'resolved'
            ? onFullfilled(this.data)
            : onRejected(this.data)
          resolve(value)
        } catch (e) {reject(e)
        }
      }.bind(this))
    }

    // then 承受两个函数返回一个新的 Promise
    // then 本身的执行永远异步与 onFullfilled/onRejected 的执行
    if (this.status !== 'pending') {return new Promise(executor.bind(this))
    } else {
    // pending
      return new Promise(function (resolve, reject) {this.resolveList.push(executor.bind(this, resolve, reject))
        this.rejectList.push(executor.bind(this, resolve, reject))
      }.bind(this))
    }
  }

  // for prmise A+ test
  Promise.deferred = Promise.defer = function () {var dfd = {}
    dfd.promise = new Promise(function (resolve, reject) {
      dfd.resolve = resolve
      dfd.reject = reject
    })
    return dfd
  }

  // for prmise A+ test
  if (typeof module !== 'undefined') {module.exports = Promise}

  return Promise
})()

PromisePolyfill.all = function (promises) {return new Promise((resolve, reject) => {const result = []
    let cnt = 0
    for (let i = 0; i < promises.length; ++i) {promises[i].then(value => {
        cnt++
        result[i] = value
        if (cnt === promises.length) resolve(result)
      }, reject)
    }
  })
}

PromisePolyfill.race = function (promises) {return new Promise((resolve, reject) => {for (let i = 0; i < promises.length; ++i) {promises[i].then(resolve, reject)
    }
  })
}

图片懒加载

能够给 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);

基于 Promise.all 实现 Ajax 的串行和并行

基于 Promise.all 实现 Ajax 的串行和并行

  • 串行:申请是异步的,须要期待上一个申请胜利,能力执行下一个申请
  • 并行:同时发送多个申请「HTTP申请能够同时进行,然而 JS 的操作都是一步步的来的,因为 JS 是单线程」, 期待所有申请都胜利,咱们再去做什么事件?
Promise.all([axios.get('/user/list'),
    axios.get('/user/list'),
    axios.get('/user/list')
]).then(results => {console.log(results);
}).catch(reason => {});

Promise.all 并发限度及 async-pool 的利用

并发限度指的是,每个时刻并发执行的 promise 数量是固定的,最终的执行后果还是放弃与原来的

const delay = function delay(interval) {return new Promise((resolve, reject) => {setTimeout(() => {// if (interval === 1003) reject('xxx');
            resolve(interval);
        }, interval);
    });
};
let tasks = [() => {return delay(1000);
}, () => {return delay(1003);
}, () => {return delay(1005);
}, () => {return delay(1002);
}, () => {return delay(1004);
}, () => {return delay(1006);
}];

/* Promise.all(tasks.map(task => task())).then(results => {console.log(results);
}); */

let results = [];
asyncPool(2, tasks, (task, next) => {task().then(result => {results.push(result);
        next();});
}, () => {console.log(results);
});
const delay = function delay(interval) {return new Promise((resolve, reject) => {setTimeout(() => {resolve(interval);
        }, interval);
    });
};
let tasks = [() => {return delay(1000);
}, () => {return delay(1003);
}, () => {return delay(1005);
}, () => {return delay(1002);
}, () => {return delay(1004);
}, () => {return delay(1006);
}];

JS 实现 Ajax 并发申请管制的两大解决方案

tasks:数组,数组蕴含很多办法,每一个办法执行就是发送一个申请「基于 Promise 治理」

function createRequest(tasks, pool) {
    pool = pool || 5;
    let results = [],
        together = new Array(pool).fill(null),
        index = 0;
    together = together.map(() => {return new Promise((resolve, reject) => {const run = function run() {if (index >= tasks.length) {resolve();
                    return;
                };
                let old_index = index,
                    task = tasks[index++];
                task().then(result => {results[old_index] = result;
                    run();}).catch(reason => {reject(reason);
                });
            };
            run();});
    });
    return Promise.all(together).then(() => results);
} 

/* createRequest(tasks, 2).then(results => {
    // 都胜利,整体才是胜利,按顺序存储后果
    console.log('胜利 -->', results);
}).catch(reason => {
    // 只有有也给失败,整体就是失败
    console.log('失败 -->', reason);
}); */
function createRequest(tasks, pool, callback) {if (typeof pool === "function") {
        callback = pool;
        pool = 5;
    }
    if (typeof pool !== "number") pool = 5;
    if (typeof callback !== "function") callback = function () {};
    //------
    class TaskQueue {
        running = 0;
        queue = [];
        results = [];
        pushTask(task) {
            let self = this;
            self.queue.push(task);
            self.next();}
        next() {
            let self = this;
            while (self.running < pool && self.queue.length) {
                self.running++;
                let task = self.queue.shift();
                task().then(result => {self.results.push(result);
                }).finally(() => {
                    self.running--;
                    self.next();});
            }
            if (self.running === 0) callback(self.results);
        }
    }
    let TQ = new TaskQueue;
    tasks.forEach(task => TQ.pushTask(task));
}
createRequest(tasks, 2, results => {console.log(results);
});
退出移动版