14道高频手写JS面试题及答案,巩固你的JS基础

目录

1. 手写深拷贝

2. 防抖函数

3. 节流函数

4. 模拟 instanceof

5. 全局通用的数据类型判断方法

6. 手写 call 函数

7. 手写 apply 函数

8. bind方法

9. 模拟 new 

10. 类数组转化为数组的方法

11. 组合继承

12. 原型式继承

13. 实现 Object.create()

14. 数组去重


1. 手写深拷贝

function deepClone(startObj,endObj) {
    let obj = endObj || {}
    for (let i in startObj) {
        if (typeof startObj[i] === 'object') {
            startObj[i].constructor === Array ? obj[i] = [] : obj[i] = {}
            deepClone(startObj[i],obj[i])
        } else {
            obj[i] = startObj[i]
        }
    }
    return obj
}

值得注意的一点是,在递归调用的时候,需要把当前处理的 obj[i] 给传回去,否则的话 每次递归obj都会被赋值为空对象,就会对已经克隆好的数据产生影响。 

我们验证一下深拷贝是否实现:

const person = {
    name: 'zyj',
    age: 20,
    sister: {
        name: 'duoduo',
        age: 13,
        mother: {
            name: 'lili',
            age:45
        }
    }
}
const newPerson = deepClone(person)
newPerson.sister.mother.age = 50
console.log(newPerson)
// {
//     name: 'zyj',
//     age: 20,
//     sister: { name: 'duoduo', age: 13, mother: { name: 'lili', age: 50 } }
// }
console.log(person)
// {
//     name: 'zyj',
//     age: 20,
//     sister: { name: 'duoduo', age: 13, mother: { name: 'lili', age: 45 } }
// }

2. 防抖函数

单位时间内,频繁触发一个事件,以最后一次触发为准。

function debounce(fn,delay) {
        let timer = null
        return function() {
            clearTimeout(timer)
            timer = setTimeout(() => {
                fn.call(this)
            }, delay);
        }
    }

我们看一下调用流程:

<body>
    <input type="text">
    <script>
        const input = document.querySelector('input')
        input.addEventListener('input',debounce(function() {
            console.log(111);
        },1000))
        function debounce(fn,delay) {
            let timer = null
            return function() {
                clearTimeout(timer)
                timer = setTimeout(() => {
                    fn.call(this)
                }, delay);
            }
        }
    </script>
</body>

可能有些同学对 fn.call(this) 不太明白,在 debounce 中我们把匿名函数作为参数传进来,因为匿名函数的执行环境具有全局性,所以它的 this 一般指向 window ,所以要改变一下 this 指向,让它指向调用者 input 。

3. 节流函数

单位时间内,频繁触发一个事件,只会触发一次。

function throttle(fn,delay) {
            return function () {
                if (fn.t) return;//每次触发事件时,如果当前有等待执行的延时函数,则直接return
                fn.t = setTimeout(() => { 
                    fn.call(this);//确保执行函数中this指向事件源,而不是window 
                    fn.t = null//执行完后设置 fn.t 为空,这样就能再次开启新的定时器
                }, delay);
            };
        }

调用流程:

<script>
        //节流throttle代码:
        function throttle(fn,delay) {
            return function () {
                if (fn.t) return;//每次触发事件时,如果当前有等待执行的延时函数,则直接return
                fn.t = setTimeout(() => { 
                    fn.call(this);//确保执行函数中this指向事件源,而不是window 
                    fn.t = null//执行完后设置 fn.t 为空,这样就能再次开启新的定时器
                }, delay);
            };
        }
        window.addEventListener('resize', throttle(function() {
            console.log(11);
        },1000));
    </script>

只有当调整浏览器视口大小时才会输出,且每隔一秒输出一次

4. 模拟 instanceof

// 模拟 instanceof
function myInstance(L, R) {
    //L 表示左表达式,R 表示右表达式
    let RP = R.prototype; // 取 R 的显示原型
    let LP = L.__proto__; // 取 L 的隐式原型
    while (true) {
      if (LP === null) return false;
      if (RP === LP)
        // 这里重点:当 O 严格等于 L 时,返回 true
        return true;
      LP = LP.__proto__;
    }
}
function person(name) {
    this.name = name
}
const zyj = new person('库里')

console.log(myInstance(zyj,person)); // true

5. 全局通用的数据类型判断方法

function getType(obj){
  let type  = typeof obj;
  if (type !== "object") {    // 先进行typeof判断,如果是基础数据类型,直接返回
    return type;
  }
  // 对于typeof返回结果是object的,再进行如下的判断,正则返回结果
  return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1');  // 注意正则中间有个空格
}

6. 手写 call 函数

Function.prototype.myCall = function (context) {
      // 先判断调用myCall是不是一个函数
      // 这里的this就是调用myCall的
      if (typeof this !== 'function') {
        throw new TypeError("Not a Function")
      }
 
      // 不传参数默认为window
      context = context || window
 
      // 保存this
      context.fn = this
 
      // 保存参数
      let args = Array.from(arguments).slice(1)
      //Array.from 把伪数组对象转为数组,然后调用 slice 方法,去掉第一个参数
 
      // 调用函数
      let result = context.fn(...args)
 
      delete context.fn
 
      return result
 
    }
 

7. 手写 apply 函数

Function.prototype.myApply = function (context) {
      // 判断this是不是函数
      if (typeof this !== "function") {
        throw new TypeError("Not a Function")
      }
 
      let result
 
      // 默认是window
      context = context || window
 
      // 保存this
      context.fn = this
 
      // 是否传参
      if (arguments[1]) {
        result = context.fn(...arguments[1])
      } else {
        result = context.fn()
      }
      delete context.fn
 
      return result
    }
 
 

8. bind方法

在实现手写bind方法的过程中,看了许多篇文章,答案给的都很统一,准确,但是不知其所以然,所以我们就好好剖析一下bind方法的实现过程。

我们先看一下bind函数做了什么:

bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

读到这里我们就发现,他和 apply , call 是不是很像,所以这里指定 this 功能,就可以借助 apply 去实现:

Function.prototype.myBind = function (context) {
    // 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 man
    const self = this;
    // 获取 myBind 函数从第二个参数到最后一个参数(第一个参数是 context)
    // 这里产生了闭包
    const args = Array.from(arguments).slice(1)
    return function () {
        // 这个时候的 arguments 是指 myBind 返回的函数传入的参数
        const bindArgs = Array.from(arguments)
        // 合并
        return self.apply(context, args.concat(bindArgs));
    };
};

大家对这段代码应该都能看懂,实现原理和手写 call , apply 都很像,因为 bind 可以通过返回的函数传参,所以在 return 里面获取的 bindArgs 就是这个意思,然后最后通过 concat 把原来的参数和后来传进来的参数进行数组合并。

我们来看一下结果:

const person = {
    name: 'zyj'
}

function man(age) {
    console.log(this.name);
    console.log(age)
}

const test = man.myBind(person)
test(18)//zyj 18

现在重点来了,bind 区别于 call 和 apply 的地方在于它可以返回一个函数,然后把这个函数当作构造函数通过 new 操作符来创建对象。

我们来试一下:

const person = {
    name: 'zyj'
}

function man(age) {
    console.log(this.name);
    console.log(age)
}

const test = man.myBind(person)
const newTest = new test(18) // zyj 18

这是用的我们上面写的 myBind 函数是这个结果,那原生 bind 呢?

const person = {
    name: 'zyj'
}

function man(age) {
    console.log(this.name);
    console.log(age)
}

const test = man.bind(person)
const newTest = new test(18) // undefined 18

由上述代码可见,使用原生 bind 生成绑定函数后,通过 new 操作符调用该函数时,this.name 是一个 undefined,这其实很好理解,因为我们 new 了一个新的实例,那么构造函数里的 this 肯定指向的就是实例,而我们的代码逻辑中指向的始终都是 context ,也就是传进去的参数。

所以现在我们要加个判断逻辑:

Function.prototype.myBind = function (context) {
    // 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 man
    const self = this;
    // 获取 myBind 函数从第二个参数到最后一个参数(第一个参数是 context)
    // 这里产生了闭包
    const args = Array.from(arguments).slice(1)
    const theBind = function () {
        const bindArgs = Array.from(arguments);
    
        // 当绑定函数作为构造函数时,其内部的 this 应该指向实例,此时需要更改绑定函数的 this 为实例
        // 当作为普通函数时,将绑定函数的 this 指向 context 即可
        // this instanceof fBound 的 this 就是绑定函数的调用者
        return self.apply(
          this instanceof theBind ? this : context,
          args.concat(bindArgs)
        );
      };
      return theBind;
};

现在这个效果我们也实现了,那我们的 myBind 函数就和其他的原生 bind 一样了吗?来看下面的代码:

const person = {
    name: 'zyj'
}
function man(age) {
    console.log(this.name);
    console.log(age)
}
man.prototype.sayHi = function() {
    console.log('hello')
}
const test = man.myBind(person)
const newTest = new test(18) // undefined 18
newTest.sayHi()

如果 newTest 是我们 new 出来的 man 实例,那根据原型链的知识,定义在man的原型对象上的方法肯定会被继承下来,所以我们通过 newTest.sayHi 调用能正常输出 hello 么?

该版代码的改进思路在于,将返回的绑定函数的原型对象的 __proto__ 属性,修改为原函数的原型对象。便可满足原有的继承关系。

Function.prototype.myBind = function (context) {
    // 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 man
    const self = this;
    // 获取 myBind 函数从第二个参数到最后一个参数(第一个参数是 context)
    // 这里产生了闭包
    const args = Array.from(arguments).slice(1);
    const theBind = function () {
        const bindArgs = Array.from(arguments);
    
        // 当绑定函数作为构造函数时,其内部的 this 应该指向实例,此时需要更改绑定函数的 this 为实例
        // 当作为普通函数时,将绑定函数的 this 指向 context 即可
        // this instanceof fBound 的 this 就是绑定函数的调用者
        return self.apply(
          this instanceof theBind ? this : context,
          args.concat(bindArgs)
        );
      };
      theBind.prototype = Object.create(self.prototype)
      return theBind;
};

9. 模拟 new 

// 手写一个new
    function myNew(fn, ...args) {
      // 创建一个空对象
      let obj = {}
      // 使空对象的隐式原型指向原函数的显式原型
      obj.__proto__ = fn.prototype
      // this指向obj
      let result = fn.apply(obj, args)
      // 返回
      return result instanceof Object ? result : obj
    }

有很多小伙伴不明白为什么要判断 result 是不是 Object  的实例,我们首先得了解,在JavaScript中构造函数可以有返回值也可以没有。

1. 没有返回值的情况返回实例化的对象

function Person(name, age){
    this.name = name
    this.age = age
}
console.log(Person());  //undefined
console.log(new Person('zyj',20));//Person { name: 'zyj', age: 20 }

2. 如果存在返回值则检查其返回值是否为引用类型,如果为非引用类型,如(string,number,boolean,null,undefined),上述几种类型的情况与没有返回值的情况相同,实际返回实例化的对象

function Person(name, age){
    this.name = name
    this.age = age
    return 'lalala'
}
console.log(Person());  //lalala
console.log(new Person('zyj',20));//Person { name: 'zyj', age: 20 }

3. 如果存在返回值是引用类型,则实际返回该引用类型

function Person(name, age){
    this.name = name
    this.age = age
    return {
        name: 'curry',
        ahe: 34
    }
}
console.log(Person());  //{ name: 'curry', ahe: 34 }
console.log(new Person('zyj',20));//{ name: 'curry', ahe: 34 }

10. 类数组转化为数组的方法

const arrayLike=document.querySelectorAll('div')

// 1.扩展运算符
[...arrayLike]
// 2.Array.from
Array.from(arrayLike)
// 3.Array.prototype.slice
Array.prototype.slice.call(arrayLike)
// 4.Array.apply
Array.apply(null, arrayLike)
// 5.Array.prototype.concat
Array.prototype.concat.apply([], arrayLike)

11. 组合继承

function father (name) {
  this.name = name
  this.age = 18
}

father.prototype.getName = function(){}  // 方法定义在父类原型上(公共区域)

function child () {
  // 继承父类属性,可传入参数
  father.call(this,'Tom')
  // 将会生成如下属性:
  // name:'tom'
  // age: 18
}
child.prototype = new father() // 重写原型对象
child.prototype.constructor = child

这里的原型链关系应该是这样的:

该方式也叫做伪经典继承。其核心思路是:重写子类的原型对象为父类实例,并通过盗用构造函数继承父类实例的属性。

12. 原型式继承

基本思路是,对传入的对象做了一次浅复制,并赋值给一个空函数 F (临时类型)的原型对象,并返回一个通过 F 生成的实例。这个实例的 __proto__ 自然而然地指向了传入的对象,可以理解为一个挂钩🧷的过程。

function object(o) { 
    function F() {} 
    F.prototype = o; 
    return new F(); 
  }
  
  let father = function() {}
  father.prototype.getName = function() {
      console.log('zyj')
  }
  
  let son = object(father)
  let daughter = object(father)
  son.prototype.getName()  // zyj

大概是这么个过程:

ECMAScript 5 中,通过增加 Object.create() 方法将原型式继承的概念规范化,即替代了上述自定义的 object() 函数。所以对于 Object.create() 的手写实现,核心思路与上述的自定义函数类似,只是添加了部分参数校验的环节。

let son = Object.create(father)  // 等同于上述代码

13. 实现 Object.create()

Object.myCreate = function(proto, propertyObject) {
    // 参数校验
    if (typeof proto !== 'object' && typeof proto !== 'function') {
        throw new TypeError('Object prototype may only be an Object or null.')
    // 不能传一个 null 值给实例作为属性
    if (propertyObject == null) {
        new TypeError('Cannot convert undefined or null to object')
    }
    // 原型式继承的思想:用一个空函数(即忽略掉原有构造函数的初始化代码)创建一个干净的实例    
    function F() {}
    F.prototype = proto // 确定后续的继承关系
    const obj = new F()
    
    // 如果有传入第二个参数,将其设为 obj 的属性
    if (propertyObject != undefined) {
        Object.defineProperties(obj, propertyObject)
    }
        
    // 即 Object.create(null)  创建一个没有原型对象的对象
    if (proto === null) {
        obj.__proto__ = null
    }
    return obj
}

14. 数组去重

ES5实现:

function unique(arr) {
    var res = arr.filter(function(item, index, array) {
        return array.indexOf(item) === index
    })
    return res
}

ES6实现:

var unique = arr => [...new Set(arr)]

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

YinJie…

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

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

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

打赏作者

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

抵扣说明:

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

余额充值