2021年的几次面试让我死磕了17道JS手写题!

Posted 前端纸飞机

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2021年的几次面试让我死磕了17道JS手写题!相关的知识,希望对你有一定的参考价值。

 1、浅拷贝、深拷贝的实现

浅拷贝

// 1. ...实现
let copy1 = {...{x:1}}

// 2. Object.assign实现
let copy2 = Object.assign({}, {x:1})

深拷贝

javascript深拷贝和浅拷贝以及实现方法(推荐)_纸飞机博客-CSDN博客_js浅拷贝和深拷贝的区别深拷贝和浅拷贝的区别?浅拷贝: 将原对象或原数组的引用直接赋给新对象,新数组,新对象/数组只是原对象的一个引用。深拷贝: 创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”。为什么要用深拷贝?我们希望在改变新的数组(对象)的时候,不改变原数组(对象)一般是针对Array和Object类型数据的复制对象深拷贝方法1.JSON的方式实现function deepClone2(obj) { let _obj = JSON.sthttps://blog.csdn.net/qq_32442973/article/details/118584594

2、手写防抖节流函数 

javascript的防抖和节流深入理解_纸飞机博客-CSDN博客基本概念函数防抖(debounce):触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间。函数节流(throttle):高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率。函数防抖(debounce)与 函数节流(throttle)都是为了限制函数的执行频次,以优化函数触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象。函数防抖(debounce)实现方式:每次触发事件时设置一个延迟调用方法,并且取消之前的延时调用方法https://blog.csdn.net/qq_32442973/article/details/118739927

3、instanceof (考察对原型链的理解) 

instanceof作用:判断一个实例是否是其父类或者祖先类型的实例

instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype查找失败,返回 false。

let myInstanceof = (target,origin) => {
     while(target) {
         if(target.__proto__===origin.prototype) {
            return true
         }
         target = target.__proto__
     }
     return false
}
let a = [1,2,3]
console.log(myInstanceof(a,Array));  // true
console.log(myInstanceof(a,Object));  // true

4、实现数组的map方法

Array.prototype.newMap = function(fn) {
   var newArr = [];
   for(var i = 0; i<this.length; i++){
     newArr.push(fn(this[i],i,this))
   }
   return newArr;
}

5、实现 new 方法

function createNew() {
    let obj = {}  // 1.创建一个空对象

    let constructor = [].shift.call(arguments) 
    // let [constructor,...args] = [...arguments]  

    obj.__proto__ = constructor.prototype  // 2.链接到原型

    let result = constructor.apply(obj, arguments)  // 3.绑定this值,为实例添加方法和属性
    // let result = constructor.apply(obj, args)   

    return typeof result === 'object' ? result : obj  // 4.返回新对象
}

function People(name,age) {
    this.name = name
    this.age = age
}

let peo = createNew(People,'Bob',22)
console.log(peo.name)
console.log(peo.age)

6、实现call&apply&bind

call 

Function.prototype.myCall = function (context = window) {
  // 函数的方法,所以写在Fuction原型对象上
  if (typeof this !== "function") {
    // 这里if其实没必要,会自动抛出错误
    throw new Error("不是函数");
  }
  const obj = context || window; //这里可用ES6方法,为参数添加默认值,js严格模式全局作用域this为undefined
  obj.fn = this; //this为调用的上下文,this此处为函数,将这个函数作为obj的方法
  const arg = [...arguments].slice(1); //第一个为obj所以删除,伪数组转为数组
  res = obj.fn(...arg);
  delete obj.fn; // 不删除会导致context属性越来越多
  return res;
};

apply(arguments[this, [参数1,参数2.....] ])

Function.prototype.myApply = function (context) {
  // 箭头函数从不具有参数对象!!!!!这里不能写成箭头函数
  let obj = context || window;
  obj.fn = this;
  const arg = arguments[1] || []; //若有参数,得到的是数组
  let res = obj.fn(...arg);
  delete obj.fn;
  return res;
};
function f(a, b) {
  console.log(a, b);
  console.log(this.name);
}
let obj = {
  name: "张三",
};
f.myApply(obj, [1, 2]); //arguments[1]

bind

// 思路:类似call,但返回的是函数
Function.prototype.mybind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  let _this = this
  let arg = [...arguments].slice(1)
  return function F() {
    // 处理函数使用new的情况
    if (this instanceof F) {
      return new _this(...arg, ...arguments)
    } else {
      return _this.apply(context, arg.concat(...arguments))
    }
  }
}

更多实现:bind方法的实现

7、手动实现promise

// Promise/A+ 规范规定的三种状态
const STATUS = {
 PENDING: 'pending',
 FULFILLED: 'fulfilled',
 REJECTED: 'rejected'
}

class MyPromise {
 // 构造函数接收一个执行回调
 constructor(executor) {
     this._status = STATUS.PENDING // Promise初始状态
     this._value = undefined // then回调的值
     this._resolveQueue = [] // resolve时触发的成功队列
     this._rejectQueue = [] // reject时触发的失败队列
    
 // 使用箭头函数固定this(resolve函数在executor中触发,不然找不到this)
 const resolve = value => {
     const run = () => {
         // Promise/A+ 规范规定的Promise状态只能从pending触发,变成fulfilled
         if (this._status === STATUS.PENDING) {
             this._status = STATUS.FULFILLED // 更改状态
             this._value = value // 储存当前值,用于then回调
            
             // 执行resolve回调
             while (this._resolveQueue.length) {
                 const callback = this._resolveQueue.shift()
                 callback(value)
             }
         }
     }
     //把resolve执行回调的操作封装成一个函数,放进setTimeout里,以实现promise异步调用的特性(规范上是微任务,这里是宏任务)
     setTimeout(run)
 }

 // 同 resolve
 const reject = value => {
     const run = () => {
         if (this._status === STATUS.PENDING) {
         this._status = STATUS.REJECTED
         this._value = value
        
         while (this._rejectQueue.length) {
             const callback = this._rejectQueue.shift()
             callback(value)
         }
     }
 }
     setTimeout(run)
 }

     // new Promise()时立即执行executor,并传入resolve和reject
     executor(resolve, reject)
 }

 // then方法,接收一个成功的回调和一个失败的回调
 function then(onFulfilled, onRejected) {
  // 根据规范,如果then的参数不是function,则忽略它, 让值继续往下传递,链式调用继续往下执行
  typeof onFulfilled !== 'function' ? onFulfilled = value => value : null
  typeof onRejected !== 'function' ? onRejected = error => error : null

  // then 返回一个新的promise
  return new MyPromise((resolve, reject) => {
    const resolveFn = value => {
      try {
        const x = onFulfilled(value)
        // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve
        x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
      } catch (error) {
        reject(error)
      }
    }
  }
}

  const rejectFn = error => {
      try {
        const x = onRejected(error)
        x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
      } catch (error) {
        reject(error)
      }
    }

    switch (this._status) {
      case STATUS.PENDING:
        this._resolveQueue.push(resolveFn)
        this._rejectQueue.push(rejectFn)
        break;
      case STATUS.FULFILLED:
        resolveFn(this._value)
        break;
      case STATUS.REJECTED:
        rejectFn(this._value)
        break;
    }
 })
 }
 catch (rejectFn) {
  return this.then(undefined, rejectFn)
}
// promise.finally方法
finally(callback) {
  return this.then(value => MyPromise.resolve(callback()).then(() => value), error => {
    MyPromise.resolve(callback()).then(() => error)
  })
}

 // 静态resolve方法
 static resolve(value) {
      return value instanceof MyPromise ? value : new MyPromise(resolve => resolve(value))
  }

 // 静态reject方法
 static reject(error) {
      return new MyPromise((resolve, reject) => reject(error))
    }

 // 静态all方法
 static all(promiseArr) {
      let count = 0
      let result = []
      return new MyPromise((resolve, reject) =>       {
        if (!promiseArr.length) {
          return resolve(result)
        }
        promiseArr.forEach((p, i) => {
          MyPromise.resolve(p).then(value => {
            count++
            result[i] = value
            if (count === promiseArr.length) {
              resolve(result)
            }
          }, error => {
            reject(error)
          })
        })
      })
    }

 // 静态race方法
 static race(promiseArr) {
      return new MyPromise((resolve, reject) => {
        promiseArr.forEach(p => {
          MyPromise.resolve(p).then(value => {
            resolve(value)
          }, error => {
            reject(error)
          })
        })
      })
    }
}

8、手写原生AJAX

步骤

  1. 创建 XMLHttpRequest 实例

  2. 发出 HTTP 请求

  3. 服务器返回 XML 格式的字符串

  4. JS 解析 XML,并更新局部页面

    不过随着历史进程的推进,XML 已经被淘汰,取而代之的是 JSON。

了解了属性和方法之后,根据 AJAX 的步骤,手写最简单的 GET 请求。

myButton.addEventListener("click", function () {
  ajax();
});

function ajax() {
  let xhr = new XMLHttpRequest(); //实例化,以调用方法
  xhr.open("get", "https://www.baidu.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请求
}

基于promise实现:

function ajax(url) {
  const p = new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest();
    xhr.open("get", url);
    xhr.onreadystatechange = () => {
      if (xhr.readyState == 4) {
        if (xhr.status >= 200 && xhr.status <= 300) {
          resolve(JSON.parse(xhr.responseText));
        } else {
          reject("请求出错");
        }
      }
    };
    xhr.send(); //发送hppt请求
  });
  return p;
}
let url = "/data.json";
ajax(url)
  .then((res) => console.log(res))
  .catch((reason) => console.log(reason));

9、柯里化函数的实现

柯里化函数的定义:将多参数的函数转换成单参数的形式
柯里化函数实现的原理:利用闭包原理在执行可以形成一个不销毁的作用域,然后把需要预先处理的内容都储存在这个不销毁的作用域中,并且返回一个最少参数函数。

问法有很多,比较全面的可看该文:JS函数柯里化

10、实现一个双向数据绑定

let obj = {}
let input = document.getElementById('input')
let span = document.getElementById('span')
// 数据劫持
Object.defineProperty(obj, 'text', {
  configurable: true,
  enumerable: true,
  get() {
    console.log('获取数据了')
  },
  set(newVal) {
    console.log('数据更新了')
    input.value = newVal
    span.innerhtml = newVal
  }
})
// 输入监听
input.addEventListener('keyup', function(e) {
  obj.text = e.target.value
})

11、rem 基本设置

// 提前执行,初始化 resize 事件不会执行
setRem()
// 原始配置
function setRem () {
  let doc = document.documentElement
  let width = doc.getBoundingClientRect().width
  let rem = width / 75
  doc.style.fontSize = rem + 'px'
}
// 监听窗口变化
addEventListener("resize", setRem)

12、手写发布订阅

发布订阅模式的发布和订阅都由一个调度中心来处理

发布订阅模式是完全解耦的,因为调度中心中存的直接就是逻辑处理函数

要点:都要实现添加/删除/派发更新三个事件

class Event {
  // 首先定义一个事件容器,用来装事件数组(因为订阅者可以是多个)
  #handlers = {}

  // 事件添加方法,参数有事件名和事件方法
  addEventListener(type, handler) {
    // 首先判断handlers内有没有type事件容器,没有则创建一个新数组容器
    if (!(type in this.#handlers)) {
      this.#handlers[type] = []
    }
    // 将事件存入
    this.#handlers[type].push(handler)
  }

  // 触发事件两个参数(事件名,参数)
  dispatchEvent(type, ...params) {
    // 若没有注册该事件则抛出错误
    if (!(type in this.#handlers)) {
      return new Error('未注册该事件')
    }
    // 便利触发
    this.#handlers[type].forEach(handler => {
      handler(...params)
    })
  }

  // 事件移除参数(事件名,删除的事件,若无第二个参数则删除该事件的订阅和发布)
  removeEventListener(type, handler) {
    // 无效事件抛出
    if (!(type in this.#handlers)) {
      return new Error('无效事件')
    }
    if (!handler) {
      // 直接移除事件
      delete this.#handlers[type]
    } else {
      const idx = this.#handlers[type].findIndex(ele => ele === handler)
      // 抛出异常事件
      if (idx === -1) {
        return new Error('无该绑定事件')
      }
      // 移除事件
      this.#handlers[type].splice(idx, 1)
      if (this.#handlers[type].length === 0) {
        delete this.#handlers[type]
      }
    }
  }
}

13、数组去重的实现

该题比较灵活,答题时可根据题目选取最便捷的写法。

JS数组去重的实现

14、实现数组拍平

参考:

15、实现斐波那契数列

参考:js实现斐波那契数列的几种方式

16、实现图片懒加载

与普通的图片懒加载不同,如下这个多做了 2 个精心处理:

  • 图片全部加载完成后移除事件监听;
  • 加载完的图片,从 imgList 移除;
let imgList = [...document.querySelectorAll('img')]
let length = imgList.length

const imgLazyLoad = function() {
    let count = 0
    return (function() {
        let deleteIndexList = []
        imgList.forEach((img, index) => {
            let rect = img.getBoundingClientRect()
            if (rect.top < window.innerHeight) {
                img.src = img.dataset.src
                deleteIndexList.push(index)
                count++
                if (count === length) {
                    document.removeEventListener('scroll', imgLazyLoad)
                }
            }
        })
        imgList = imgList.filter((img, index) => !deleteIndexList.includes(index))
    })()
}

// 这里最好加上防抖处理
document.addEventListener('scroll', imgLazyLoad)

17、实现一个JSONP

JSONP 核心原理:script 标签不受同源策略约束,所以可以用来进行跨域请求,优点是兼容性好,但是只能用于 GET 请求

const jsonp = ({ url, params, callbackName }) => {
    const generateUrl = () => {
        let dataSrc = ''
        for (let key in params) {
            if (params.hasOwnProperty(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)
        }
    })
}

推荐文章:

大前端基本知识点及面试重灾区学习目录_纸飞机博客-CSDN博客

2021前端面试js题目总结,不妨看看有没有属于你的那道题_纸飞机博客-CSDN博客

一起谈一谈js中的宏任务和微任务!_纸飞机博客-CSDN博客

以上是关于2021年的几次面试让我死磕了17道JS手写题!的主要内容,如果未能解决你的问题,请参考以下文章

在简历上写了“精通自动化测试,阿里面试官跟我死磕后就给我发了高薪 offer

那些在一个公司死磕了5-10年的人,最后都怎么样了?

阿里面试官竟然和我死磕 Maven

死磕这50道MySQL面试题,阿里面试官直呼内行(上)

Java继承中的几道面试题

js基础面试题131-160道题目