【面试官系列】10个必会JavaScript高频手写题,思路和相关知识点都给你备好了,不会看不懂了

 

前言        

        金三银四来了,万物复苏,又到了换工作的季节。想要找到一个好工作,手写题是必不可少的一部分。本文我给大家总结了几个经常考到,又对自己js掌握有提升的手写题。
       我给大家整理了实现思路和大家可能陌生的知识点。
       另外,本期博客参与了【新星计划】,还请大家三连支持一下🌟🌟🌟感谢感谢💓💓💓

目录

1. instanceOf

2. 实现promise

3. 实现发布订阅(EventEmitter)

4. 实现new方法

5. 实现一个call  /  apply函数

6. 实现一个bind函数

7. 深拷贝、浅拷贝

8. 防抖、节流

9. 实现一个双向数据绑定

10. rem基础设置


1. instanceOf

思路:

1. 首先instanceOf只能判断非null并且typeof是对象的数据
2.判断对象的原型是否和构造函数的原型对象相等,如果相等返回true,否则继续深入原型链进行比对,直至最底层。

相关知识点:

Object.getPrototypeOf(obj) 
返回指定对象的原型(内部[[Prototype]]属性的值)。
obj:要返回其原型的对象。
返回值:给定对象的原型。如果没有继承属性,则返回 null 。

实现:

递归实现(方式1)

```javascript
/**
 * 
 * @param {*} obj 实例对象
 * @param {*} func 构造函数
 * @returns true false
 */
const myInstanceOf = (obj,func) => {
    if(obj === null || typeof(obj) !== 'object')return false
    let proto = Object.getPrototypeOf(obj)
    if(proto === func.prototype){
      return true
    }else if(proto === null){
      return false
    }else{
      return myInstanceOf(proto,func)
    }
}

// 测试
let Fn = function () { }
let p1 = new Fn()

console.log(myInstanceOf ({}, Object)) // true
console.log(myInstanceOf (p1, Fn)) // true
console.log(myInstanceOf ({}, Fn)) // false
console.log(myInstanceOf (null, Fn)) // false
console.log(myInstanceOf (1, Fn)) // false
```

遍历实现(方式2)

```javascript
/**
 * 
 * @param {*} obj 实例对象
 * @param {*} func 构造函数
 * @returns true false
 */
const instanceOf3 = (obj, func) => {
  if (obj === null || typeof obj !== 'object') {
    return false
  }

  let proto = obj
  // 因为一定会有结束的时候(最顶层Object),所以不会是死循环
  while (true) {
    if (proto === null) {
      return false
    } else if (proto === func.prototype) {
      return true
    } else {
      proto = Object.getPrototypeOf(proto)
    }
  }
}

// 测试
let Fn = function () { }
let p1 = new Fn()

console.log(instanceOf3({}, Object)) // true
console.log(instanceOf3(p1, Fn)) // true
console.log(instanceOf3({}, Fn)) // false
console.log(instanceOf3(null, Fn)) // false
console.log(instanceOf3(1, Fn)) // false
```

2. 实现promise

思路:

1. promise()接受一个参数,它是传入的执行函数executor 。这个执行函数是一个匿名函数,并且它有两个参数: resolve 和 reject,分别是成功的回调和失败的回调;

2. promise实例化时,会立即执行executor这个传入的回调函数,并且将promise内部定义的resolve和reject方法当参数传递过去;

3. promise内部定义的resolve方法有一个参数value,可以接受成功后返回的值,reject方法有一个参数reson,可以接受失败的理由;

4. promise有三个状态,分为 pending, resolved, rejected。在执行传入的executor函数时,可以调用传入resolve和reject回调方法修改promise的状态;

5. 实现.then:修改promise的原型对象,为它加上.then方法,在.then方法内部执行传入的回调函数;

6. 然后为了实现异步.then ,我们可以通过判断promise的状态,将传入的回调函数存储在实例的对象(回调函数数组)里面。当状态是 resolved 的时候,resolve 函数把里面的 callback 拿出来逐次执行。

实现:

用函数实现(方式1)

        function MyPromise(executor){
            let self = this; //这个 this 指的是 new 出来的 promise 实例
            this.status = 'pending'; // 实例中的 pending 状态
            this.resolveCallback = [];
            this.rejectCallback = [];
            function resolve(value){ 
            // 你不可以直接在这个函数里使用 this,请思考这个函数里的 this 是谁?
             函数的作用域里面有专属于自己的 this,它的值取决于函数在哪里执行。
                if(self.status==='pending'){
                    console.log(this,'resolve')
                    self.value=value;
                    self.status='resolved';
                    self.resolveCallback.forEach(fn=>{
                        fn();
                    })
                }
            }
            function reject(reason){
                if(self.status==='pending'){
                    console.log('reject')
                    self.value=reason;
                    self.status = 'rejected';
                    self.rejectCallback.forEach(fn=>{
                        fn();
                    })
                }
            }
            executor(resolve,reject);
        }

        MyPromise.prototype.then = function(onFulfilled,onRejected){
            let self = this
            if(self.status==='pending'){
                console.log('then pending')
                self.resolveCallback.push(()=>onFulfilled(self.value));
                self.rejectCallback.push(()=>onRejected(self.reason));
            }
            if(self.status==='resolved'){
                onFulfilled(self.value)
            }
            if(self.status==='rejected'){
                onRejected(self.reason)
            }
        }

console.log('start');
var p = new MyPromise(function(resolve,reject){
     setTimeout(()=>{
        resolve(5)
     },0)
}).then(value=>{
     console.log('then',value);
});
console.log('end');

//start   then pending   end   then5

用类实现(方式2)

```javascript
class MyPromise {
  constructor (exe) {
    // 最后的值,Promise .then或者.catch接收的值
    this.value = undefined
    // 状态:三种状态 pending success failure
    this.status = 'pending'
    // 成功的函数队列
    this.successQueue = []
    // 失败的函数队列
    this.failureQueue = []
    const resolve = (value) => {
      const doResolve = () => {
        // 将缓存的函数队列挨个执行,并且将状态和值设置好
        if (this.status === 'pending') {
          this.status = 'success'
          this.value = value
  
          while (this.successQueue.length) {
            const cb = this.successQueue.shift()
  
            cb && cb(this.value)
          }
        }
      }

      setTimeout(doResolve, 0)
    }

    const reject = (value) => {
      // 基本同resolve
      const doReject = () => {
        if (this.status === 'pending') {
          this.status = 'failure'
          this.value = value
  
          while (this.failureQueue.length) {
            const cb = this.failureQueue.shift()
  
            cb && cb(this.value)
          }
        }
      }

      setTimeout(doReject, 0)
    }

    exe(resolve, reject)
  }
  
  then (success = (value) => value, failure = (value) => value) {
    // .then返回的是一个新的Promise
    return new MyPromise((resolve, reject) => {
      // 包装回到函数
      const successFn = (value) => {
        try {
          const result = success(value)
          // 如果结果值是一个Promise,那么需要将这个Promise的值继续往下传递,否则直接resolve即可
          result instanceof MyPromise ? result.then(resolve, reject) : resolve(result)
        } catch (err) {
          reject(err)
        }
      }
      // 基本筒成功回调函数的封装
      const failureFn = (value) => {
        try {
          const result = failure(value)
          
          result instanceof MyPromise ? result.then(resolve, reject) : resolve(result)
        } catch (err) {
          reject(err)
        }
      }
      // 如果Promise的状态还未结束,则将成功和失败的函数缓存到队列里
      if (this.status === 'pending') {
        this.successQueue.push(successFn)
        this.failureQueue.push(failureFn)
        // 如果已经成功结束,直接执行成功回调 
      } else if (this.status === 'success') {
        success(this.value)
      } else {
        // 如果已经失败,直接执行失败回调
        failure(this.value)
      }
    })
  }
  // 其他函数就不一一实现了
  catch () {

  }
} 
// 以下举个例子,验证一下以上实现的结果
const pro = new MyPromise((resolve, reject) => {
  setTimeout(resolve, 1000)
  setTimeout(reject, 2000)
})

pro
  .then(() => {
    console.log('2_1')
    const newPro = new MyPromise((resolve, reject) => {
      console.log('2_2')
      setTimeout(reject, 2000)
    })
    console.log('2_3')
    return newPro
  })
  .then(
    () => {
      console.log('2_4')
    },
    () => {
      console.log('2_5')
    }
  )
  
pro
  .then(
    data => {
      console.log('3_1')
      throw new Error()
    },
    data => {
      console.log('3_2')
    }
  )
  .then(
    () => {
      console.log('3_3')
    },
    e => {
      console.log('3_4')
    }
  )
// 2_1
// 2_2
// 2_3
// 3_1
// 3_4
// 2_5
```

3. 实现发布订阅(EventEmitter)

发布订阅相信大家一定不会陌生,实际工作也经常会遇到,比如Vue的EventBus, $on, $emit等。

思路:

1. 首先要判断emit注册的是不是函数,如果不是就报错;

2. on函数其实就是声明一个自定义名称的函数,由于可能注册同名的函数,所以用数组将这些函数存储起来,和函数名形成一对多的关系;

3. 存储可以用一个对象eventsmap(啥名都可以),这样key是函数名,value是所有同名的函数;

4. emit是利用对象的动态属性语法调用eventsmap里的函数数组(遍历执行)。

5. 其他的off,once等方法以此类推

相关知识点:

1. 对象可以用[]来获取动态属性,例如

let a = { test: '测试'  };

let b = 'test'

console.log(a[b])     //输出  '测试'

实现:

class EventEmitter {
  constructor() {
    // eventsMap 用来存储事件和监听函数之间的关系
    this.eventsMap= {}
  }
  // eventName 代表事件的名称
  on(eventName, handler) {
    // hanlder 必须是一个函数,如果不是直接报错
    if(!(handler instanceof Function)) {
      throw new Error("哥 你错了 请传一个函数")
    }
    // 判断 eventName 事件对应的队列是否存在
    if(!this.eventsMap[eventName]) {
      // 若不存在,新建该队列
      this.eventsMap[eventName] = []
    }
    // 若存在,直接往队列里推入 handler
      this.eventsMap[eventName].push(handler)
    }
  emit(eventName, ...argu) {
    if(this.eventsMap[eventName]) {
      this.eventsMap[eventName].forEach(fn => fn(...argu))
    }
  }
  off(eventName, handler) {
    if(this.eventsMap[eventName]) {
      this.eventsMap[eventName] = this.eventsMap[eventName].filter(fn => handler!== fn && fn.l !== handler);
      // 也可以按照如下的方式删除一个监听函数
      // this.eventMap[type].splice(this.eventMap[type].indexOf(handler)>>>0,1)
      // 这里的 >>> 注意
      // 如果传入一个不存在的函数给 off 方法,indexOf 找不到会返回 -1 ,
      // 再调用 splice 就会将队列中最后一个函数删除掉了。
      // 使用无符号右移,-1 无符号右移的结果为 4294967295,这个数足够大,不会对原队列造成影响。
    }
  }
  once(eventName, callback) {
    const _once = () => {
      callback();
      this.off(eventName, _once)
    }
    _once.l = callback;
    this.on(eventName, _once)
  }
}


//测试代码
// 实例化 EventEmitter
const myEvent = new EventEmitter();

// 编写一个简单的 handler
const testHandler = function (params) {
  console.log(`test 事件被触发了,testHandler 接收到的入参是${params}`);
};

// 监听 test 事件
myEvent.on("test", testHandler);

// 在触发 test 事件的同时,传入希望 testHandler 感知的参数
myEvent.emit("test", "newState");

4. 实现new方法

思路:

1. 首先我们new实例化对象时,肯定不能把属性写死,所以传入参数也不能写死,我们可以用arguments类数组获取传入的参数,然后第一项是构造函数,后面的是向构造函数传入的属性;

2. 在我们的函数内部要实现:

  1. 创建一个空对象 
  2. 将空对象的原型指向构造函数的原型对象
  3. 修改this指向并传入参数
  4. 返回新对象

相关知识点:

1. arguments是一个对应于传递给函数的参数的类数组对象。arguments对象是所有(非箭头)函数中都可用的局部变量。你可以使用arguments对象在函数中引用函数的参数。

注意:因为它是类数组,所以不能直接用数组的push,shift等方法,要先转换下,具体转换在下面代码里有

2. apply,和call一样,都可以修改this指向,并且在修改指向的同时调用函数,唯一的区别是,传参方式不同,aplly需要提供一。

实现:

/**
 * 
 * @param {*} Constructor 构造函数
 * @param ...args 构造对象的属性
 * @returns 返回原型是构造函数的的原型对象的对象
 */

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

    // 2.接下来就是要想如何得到其中这个构造函数和其他的参数

    // 由于arguments是类数组,没有直接的方法可以供其使用,我们可以有以下两种方法:
    // (1) Array.from(arguments).shift(); //转换成数组 使用数组的方法shift将第一项弹出
    // (2)[].shift().call(arguments); // 通过call() 让arguments能够借用shift方法

    const Constructor = [].shift.call(arguments);
    const args = arguments;

    // 3.接下来的想法 给obj这个新生对象的原型指向它的构造函数的原型 
    obj.__proto__ = Constructor.prototype

     
    // 4.给构造函数传入属性,注意:构造函数的this属性
    // 参数传进Constructor对obj的属性赋值,this要指向obj对象
    // 在Coustructor内部手动指定函数执行时的this 使用call、apply实现
    Constructor.call(obj,...args);
    return obj;
}


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

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

5. 实现一个call  /  apply函数

思路:

将要改变this指向的方法挂到目标this上执行并返回

相关知识点:

实现:

实现call()

/**
 * 
 * @param {*} ctx 函数执行上下文this
 * @param  {...any} args 参数列表
 * @returns 函数执行的结果
 */
 
Function.prototype.myCall = function (ctx, ...args) {
  // 简单处理未传ctx上下文,或者传的是null和undefined等场景
  if (!ctx) {
    ctx = typeof window !== 'undefined' ? window : global
  }
  // 暴力处理 ctx有可能传非对象
  ctx = Object(ctx)
  // 用Symbol生成唯一的key
  const fnName = Symbol()
  // 这里的this,即要调用的函数
  ctx[ fnName ] = this
  // 将args展开,并且调用fnName函数,此时fnName函数内部的this也就是ctx了
  const result = ctx[ fnName ](...args)
  // 用完之后,将fnName从上下文ctx中删除
  delete ctx[ fnName ]

  return result
}

// 测试
let fn = function (name, sex) {
  console.log(this, name, sex)
}

fn.myCall('', '前端阿彬')
// window 前端阿彬 boy
fn.myCall({ name: '前端阿彬', sex: 'boy' }, '前端阿彬')
// { name: '前端阿彬', sex: 'boy' } 前端阿彬 boy

实现apply()

/**
 * 
 * @param {*} ctx 函数执行上下文this
 * @param {*} args  参数列表
 * @returns 函数执行的结果
 */
// 唯一的区别在这里,不需要...args变成数组 
Function.prototype.myApply = function (ctx, args) {
  if (!ctx) {
    ctx = typeof window !== 'undefined' ? window : global
  }

  ctx = Object(ctx)

  const fnName = Symbol()

  ctx[ fnName ] = this
  // 将args参数数组,展开为多个参数,供函数调用
  const result = ctx[ fnName ](...args)

  delete ctx[ fnName ]

  return result
}

// 测试
let fn = function (name, sex) {
  console.log(this, name, sex)
}

fn.myApply('', ['前端胖头鱼', 'boy'])
// window 前端胖头鱼 boy
fn.myApply({ name: '前端胖头鱼', sex: 'boy' }, ['前端胖头鱼', 'boy'])
// { name: '前端胖头鱼', sex: 'boy' } 前端胖头鱼 boy

6. 实现一个bind函数

思路:

类似call,但返回的是函数,它与call,apply最大的区别就是不会立马调用。

相关知识点:

实现:

/**
 * 
 * @param {*} 调用绑定函数时作为this 传递给目标函数
 * @param {*} args  参数列表
 * @returns 返回一个原函数的拷贝,并拥有指定的this值和初始参数。
 */
Function.prototype.mybind = function (context) {
  if (!ctx) {
    ctx = typeof window !== 'undefined' ? window : global
  }
  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))
    }
  }
}

7. 深拷贝、浅拷贝

实现:浅拷贝

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

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

实现:深拷贝

// 1. JOSN.stringify()/JSON.parse()  
// 缺点:拷贝对象包含 正则表达式,函数,或者undefined等值会失败
let obj = {a: 1, b: {x: 3}}
JSON.parse(JSON.stringify(obj))

// 2. 递归拷贝
function deepClone(obj) {
  let copy = obj instanceof Array ? [] : {}
  for (let i in obj) {
    if (obj.hasOwnProperty(i)) {
      copy[i] = typeof obj[i] === 'object' ? deepClone(obj[i]) : obj[i]
    }
  }
  return copy
}

8. 防抖、节流

防抖:

思路:

1. 在规定时间内未触发第二次,则执行

2. 将事件保存在定时器里,并且利用闭包形成私有变量

3. 在规定时间内再次触发会先清除定时器后再重设定时器

实现:

function debounce (fn, delay) {
  // 利用闭包保存定时器
  let timer = null
  return function () {
    let context = this
    let arg = arguments
    // 在规定时间内再次触发会先清除定时器后再重设定时器
    clearTimeout(timer)
    timer = setTimeout(function () {
      fn.apply(context, arg)
    }, delay)
  }
}

function fn () {
  console.log('防抖')
}
addEventListener('scroll', debounce(fn, 1000))

节流:

思路:

1. 当持续触发事件时,保证一定时间段内只调用一次(或两次)事件处理函数。节流通俗解释就比如我们水龙头放水,阀门一打开,水哗哗的往下流,秉着勤俭节约的优良传统美德,我们要把水龙头关小点,最好是如我们心意按照一定规律在某个时间间隔内一滴一滴的往下滴。

2. 可以利用时间戳对比上次触发时间和这次触发时间,也可以利用定时器。

实现:

// 基础版1:时间戳(第一次触发会执行,但不排除不执行的可能,请思考一下哦)
function throttle(fn, delay) {
  var prev = Date.now()
  return function(...args) {
    var dist = Date.now() - prev
    if (dist >= delay) {
      fn.apply(this, args)
      prev = Date.now()
    }
  }
}

// 基础版2:定时器(最后一次也会执行)
function throttle(fn, delay) {
  var timer = null
  return function(...args) {
    var that = this
    if(!timer) {
      timer = setTimeout(function() {
        fn.apply(this, args)
        timer = null
      }, delay)
    }
  }
}

// 进阶版:开始执行、结束执行
function throttle(fn, delay) {
  var timer = null
  var prev = Date.now()
  return function(...args) {
    var that = this
    var remaining = delay - (Date.now() - prev)  // 剩余时间
    if (remaining <= 0) {  // 第 1 次触发
      fn.apply(that, args)
      prev = Date.now()
    } else { // 第 1 次之后触发
      timer && clearTimeout(timer)
      timer = setTimeout(function() {
        fn.apply(that, args)
      }, remaining)
    }
  }
}

function fn () {
  console.log('节流')
}
addEventListener('scroll', throttle(fn, 1000))

9. 实现一个双向数据绑定

相关知识:

1. Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象;

备注:应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用。

2. 利用Object.defineProperty() 方法改写Object对象的get和set方法,实现双向数据绑定

实现:

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
})

10. rem基础设置

相关知识:

1. document.documentElement 属性以一个元素对象返回一个文档的文档元素。

2. getBoundingClientRect方法返回元素的大小及其相对于视口的位置,该方法没有参数

3. 使用rem为元素设定字体大小时,相对的是HTML根元素。

rem和em的区别:

em以当前元素font-size为基准

rem以html font-size为基准

实现:

// 提前执行,初始化 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)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端阿彬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值