写点什么

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

  • 2023-02-07
    浙江
  • 本文字数:8254 字

    阅读完需:约 27 分钟

实现单例模式

核心要点: 用闭包和Proxy属性拦截


function proxy(func) {    let instance;    let handler = {        constructor(target, args) {            if(!instance) {                instance = Reflect.constructor(fun, args);            }            return instance;        }    }    return new Proxy(func, handler);}
复制代码

实现每隔一秒打印 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);}
复制代码

实现防抖函数(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 请求进行验证,验证一次就好

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

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

Array.prototype.map()

Array.prototype.map = function(callback, thisArg) {  if (this == undefined) {    throw new TypeError('this is null or not defined');  }  if (typeof callback !== 'function') {    throw new TypeError(callback + ' is not a function');  }  const res = [];  // 同理  const O = Object(this);  const len = O.length >>> 0;  for (let i = 0; i < len; i++) {    if (i in O) {      // 调用回调函数并传入新数组      res[i] = callback.call(thisArg, O[i], i, this);    }  }  return res;}
复制代码

实现数组元素求和

  • 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
复制代码

手写 new 操作符

在调用 new 的过程中会发生以上四件事情:


(1)首先创建了一个新的空对象


(2)设置原型,将对象的原型设置为函数的 prototype 对象。


(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)


(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。


function objectFactory() {  let newObject = null;  let constructor = Array.prototype.shift.call(arguments);  let result = null;  // 判断参数是否是一个函数  if (typeof constructor !== "function") {    console.error("type error");    return;  }  // 新建一个空对象,对象的原型为构造函数的 prototype 对象  newObject = Object.create(constructor.prototype);  // 将 this 指向新建对象,并执行函数  result = constructor.apply(newObject, arguments);  // 判断返回对象  let flag = result && (typeof result === "object" || typeof result === "function");  // 判断返回结果  return flag ? result : newObject;}// 使用方法objectFactory(构造函数, 初始化参数);
复制代码


参考 前端进阶面试题详细解答

实现类数组转化为数组

类数组转换为数组的方法有这样几种:


  • 通过 call 调用数组的 slice 方法来实现转换


Array.prototype.slice.call(arrayLike);
复制代码


  • 通过 call 调用数组的 splice 方法来实现转换


Array.prototype.splice.call(arrayLike, 0);
复制代码


  • 通过 apply 调用数组的 concat 方法来实现转换


Array.prototype.concat.apply([], arrayLike);
复制代码


  • 通过 Array.from 方法来实现转换


Array.from(arrayLike);
复制代码

手写 Object.create

思路:将传入的对象作为原型


function create(obj) {  function F() {}  F.prototype = obj  return new F()}
复制代码

查找字符串中出现最多的字符和个数

例: abbcccddddd -> 字符最多的是 d,出现了 5 次


let str = "abcabcabcbbccccc";let num = 0;let char = '';
// 使其按照一定的次序排列str = str.split('').sort().join('');// "aaabbbbbcccccccc"
// 定义正则表达式let re = /(\w)\1+/g;str.replace(re,($0,$1) => { if(num < $0.length){ num = $0.length; char = $1; }});console.log(`字符最多的是${char},出现了${num}次`);

复制代码

实现一个迷你版的 vue

入口


// js/vue.jsclass Vue {  constructor (options) {    // 1. 通过属性保存选项的数据    this.$options = options || {}    this.$data = options.data || {}    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el    // 2. 把data中的成员转换成getter和setter,注入到vue实例中    this._proxyData(this.$data)    // 3. 调用observer对象,监听数据的变化    new Observer(this.$data)    // 4. 调用compiler对象,解析指令和差值表达式    new Compiler(this)  }  _proxyData (data) {    // 遍历data中的所有属性    Object.keys(data).forEach(key => {      // 把data的属性注入到vue实例中      Object.defineProperty(this, key, {        enumerable: true,        configurable: true,        get () {          return data[key]        },        set (newValue) {          if (newValue === data[key]) {            return          }          data[key] = newValue        }      })    })  }}
复制代码


实现 Dep


class Dep {  constructor () {    // 存储所有的观察者    this.subs = []  }  // 添加观察者  addSub (sub) {    if (sub && sub.update) {      this.subs.push(sub)    }  }  // 发送通知  notify () {    this.subs.forEach(sub => {      sub.update()    })  }}
复制代码


实现 watcher


class Watcher {  constructor (vm, key, cb) {    this.vm = vm    // data中的属性名称    this.key = key    // 回调函数负责更新视图    this.cb = cb
// 把watcher对象记录到Dep类的静态属性target Dep.target = this // 触发get方法,在get方法中会调用addSub this.oldValue = vm[key] Dep.target = null } // 当数据发生变化的时候更新视图 update () { let newValue = this.vm[this.key] if (this.oldValue === newValue) { return } this.cb(newValue) }}
复制代码


实现 compiler


class Compiler {  constructor (vm) {    this.el = vm.$el    this.vm = vm    this.compile(this.el)  }  // 编译模板,处理文本节点和元素节点  compile (el) {    let childNodes = el.childNodes    Array.from(childNodes).forEach(node => {      // 处理文本节点      if (this.isTextNode(node)) {        this.compileText(node)      } else if (this.isElementNode(node)) {        // 处理元素节点        this.compileElement(node)      }
// 判断node节点,是否有子节点,如果有子节点,要递归调用compile if (node.childNodes && node.childNodes.length) { this.compile(node) } }) } // 编译元素节点,处理指令 compileElement (node) { // console.log(node.attributes) // 遍历所有的属性节点 Array.from(node.attributes).forEach(attr => { // 判断是否是指令 let attrName = attr.name if (this.isDirective(attrName)) { // v-text --> text attrName = attrName.substr(2) let key = attr.value this.update(node, key, attrName) } }) }
update (node, key, attrName) { let updateFn = this[attrName + 'Updater'] updateFn && updateFn.call(this, node, this.vm[key], key) }
// 处理 v-text 指令 textUpdater (node, value, key) { node.textContent = value new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } // v-model modelUpdater (node, value, key) { node.value = value new Watcher(this.vm, key, (newValue) => { node.value = newValue }) // 双向绑定 node.addEventListener('input', () => { this.vm[key] = node.value }) }
// 编译文本节点,处理差值表达式 compileText (node) { // console.dir(node) // {{ msg }} let reg = /\{\{(.+?)\}\}/ let value = node.textContent if (reg.test(value)) { let key = RegExp.$1.trim() node.textContent = value.replace(reg, this.vm[key])
// 创建watcher对象,当数据改变更新视图 new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } } // 判断元素属性是否是指令 isDirective (attrName) { return attrName.startsWith('v-') } // 判断节点是否是文本节点 isTextNode (node) { return node.nodeType === 3 } // 判断节点是否是元素节点 isElementNode (node) { return node.nodeType === 1 }}
复制代码


实现 Observer


class Observer {  constructor (data) {    this.walk(data)  }  walk (data) {    // 1. 判断data是否是对象    if (!data || typeof data !== 'object') {      return    }    // 2. 遍历data对象的所有属性    Object.keys(data).forEach(key => {      this.defineReactive(data, key, data[key])    })  }  defineReactive (obj, key, val) {    let that = this    // 负责收集依赖,并发送通知    let dep = new Dep()    // 如果val是对象,把val内部的属性转换成响应式数据    this.walk(val)    Object.defineProperty(obj, key, {      enumerable: true,      configurable: true,      get () {        // 收集依赖        Dep.target && dep.addSub(Dep.target)        return val      },      set (newValue) {        if (newValue === val) {          return        }        val = newValue        that.walk(newValue)        // 发送通知        dep.notify()      }    })  }}
复制代码


使用


<!DOCTYPE html><html lang="cn"><head>  <meta charset="UTF-8">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <meta http-equiv="X-UA-Compatible" content="ie=edge">  <title>Mini Vue</title></head><body>  <div id="app">    <h1>差值表达式</h1>    <h3>{{ msg }}</h3>    <h3>{{ count }}</h3>    <h1>v-text</h1>    <div v-text="msg"></div>    <h1>v-model</h1>    <input type="text" v-model="msg">    <input type="text" v-model="count">  </div>  <script src="./js/dep.js"></script>  <script src="./js/watcher.js"></script>  <script src="./js/compiler.js"></script>  <script src="./js/observer.js"></script>  <script src="./js/vue.js"></script>  <script>    let vm = new Vue({      el: '#app',      data: {        msg: 'Hello Vue',        count: 100,        person: { name: 'zs' }      }    })    console.log(vm.msg)    // vm.msg = { test: 'Hello' }    vm.test = 'abc'  </script></body></html>
复制代码

实现一个函数判断数据类型

function getType(obj) {   if (obj === null) return String(obj);   return typeof obj === 'object'    ? Object.prototype.toString.call(obj).replace('[object ', '').replace(']', '').toLowerCase()   : typeof obj;}
// 调用getType(null); // -> nullgetType(undefined); // -> undefinedgetType({}); // -> objectgetType([]); // -> arraygetType(123); // -> numbergetType(true); // -> booleangetType('123'); // -> stringgetType(/123/); // -> regexpgetType(new Date()); // -> date
复制代码

对象数组如何去重

根据每个对象的某一个具体属性来进行去重


const responseList = [  { id: 1, a: 1 },  { id: 2, a: 2 },  { id: 3, a: 3 },  { id: 1, a: 4 },];const result = responseList.reduce((acc, cur) => {    const ids = acc.map(item => item.id);    return ids.includes(cur.id) ? acc : [...acc, cur];}, []);console.log(result); // -> [ { id: 1, a: 1}, {id: 2, a: 2}, {id: 3, a: 3} ]
复制代码

Array.prototype.reduce()

Array.prototype.reduce = function(callback, initialValue) {  if (this == undefined) {    throw new TypeError('this is null or not defined');  }  if (typeof callback !== 'function') {    throw new TypeError(callbackfn + ' is not a function');  }  const O = Object(this);  const len = this.length >>> 0;  let accumulator = initialValue;  let k = 0;  // 如果第二个参数为undefined的情况下  // 则数组的第一个有效值作为累加器的初始值  if (accumulator === undefined) {    while (k < len && !(k in O)) {      k++;    }    // 如果超出数组界限还没有找到累加器的初始值,则TypeError    if (k >= len) {      throw new TypeError('Reduce of empty array with no initial value');    }    accumulator = O[k++];  }  while (k < len) {    if (k in O) {      accumulator = callback.call(undefined, accumulator, O[k], k, O);    }    k++;  }  return accumulator;}
复制代码

实现 findIndex 方法

var users = [  {id: 1, name: '张三'},  {id: 2, name: '张三'},  {id: 3, name: '张三'},  {id: 4, name: '张三'}]
Array.prototype.myFindIndex = function (callback) { // var callback = function (item, index) { return item.id === 4 } for (var i = 0; i < this.length; i++) { if (callback(this[i], i)) { // 这里返回 return i } }}
var ret = users.myFind(function (item, index) { return item.id === 2})
console.log(ret)
复制代码

手写节流函数

函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。


// 函数节流的实现;function throttle(fn, delay) {  let curTime = Date.now();
return function() { let context = this, args = arguments, nowTime = Date.now();
// 如果两次时间间隔超过了指定时间,则执行函数。 if (nowTime - curTime >= delay) { curTime = Date.now(); return fn.apply(context, args); } };}
复制代码

实现 apply 方法

思路: 利用this的上下文特性。apply其实就是改一下参数的问题


Function.prototype.myApply = function(context = window, args) {  // 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); // 这里和call传参不一样
// 清除定义的this 不删除会导致context属性越来越多 delete context[key];
// 返回结果 return result;}
复制代码


// 使用function f(a,b){ console.log(a,b) console.log(this.name)}let obj={ name:'张三'}f.myApply(obj,[1,2])  //arguments[1]
复制代码

模板引擎实现

let template = '我是{{name}},年龄{{age}},性别{{sex}}';let data = {  name: '姓名',  age: 18}render(template, data); // 我是姓名,年龄18,性别undefined

复制代码


function render(template, data) {  const reg = /\{\{(\w+)\}\}/; // 模板字符串正则  if (reg.test(template)) { // 判断模板里是否有模板字符串    const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段    template = template.replace(reg, data[name]); // 将第一个模板字符串渲染    return render(template, data); // 递归的渲染并返回渲染后的结构  }  return template; // 如果模板没有模板字符串直接返回}

复制代码

数组中的数据根据 key 去重

给定一个任意数组,实现一个通用函数,让数组中的数据根据 key 排重:


const dedup = (data, getKey = () => {} ) => {  // todo}let data = [  { id: 1, v: 1 },  { id: 2, v: 2 },  { id: 1, v: 1 },];
// 以 id 作为排重 key,执行函数得到结果// data = [// { id: 1, v: 1 },// { id: 2, v: 2 },// ];
复制代码


实现


const dedup = (data, getKey = () => { }) => {    const dateMap = data.reduce((pre, cur) => {        const key = getKey(cur)        if (!pre[key]) {            pre[key] = cur        }        return pre    }, {})    return Object.values(dateMap)}
复制代码


使用


let data = [    { id: 1, v: 1 },    { id: 2, v: 2 },    { id: 1, v: 1 },];console.log(dedup(data, (item) => item.id))
// 以 id 作为排重 key,执行函数得到结果// data = [// { id: 1, v: 1 },// { id: 2, v: 2 },// ];
复制代码

实现 instanceOf

// 模拟 instanceoffunction 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__;  }}
复制代码

图片懒加载

可以给 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);
复制代码


用户头像

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

还未添加个人简介

评论

发布
暂无评论
高级前端常考手写面试题(必备)_JavaScript_helloworld1024fd_InfoQ写作社区