前端需要掌握的20个手写功能—new、call、apply、bind

333 阅读7分钟

前言

该系列是笔者整理的前端需要掌握的手写功能集合,这些功能的手撕需要具备一定的前端基础功底,在面试中也会高频的出现。笔者会将每个手写功能单独呈现为一篇,尽可能整理的细致,同时也不会让文章篇幅太长,内容太过杂乱,该篇为JavaScript内置方法call、apply、bind的重写及new的简易实现

方法的重写

在重写之前我们要先确定一个思路,在重写任何方法之前我们首先要明明确,原方法是如何调用的?有什么参数?返回什么?具备什么功能?,带着这些疑问去重写方法将让你的思路更加的清晰

bind

首先我们看下bind的一般调用示例

function fn (num1, num2) {
  console.log(this) // obj
  console.log(num1, num2) // 1,2
}
const obj = {}
const fn2 = fn.bind(obj, 1, 2) //第一个参数是绑定的this,之后就是传递给执行函数的参数
fn2()

我们能看到bind是通过函数调用的,因此这个bind存在于Function的原型上,为了避免和原方法冲突,我们这里重写的方法叫做myBind,bind的参数第一个是需要绑定this,从第二参数开始后续的参数列表会传递给返回的函数作为默认参数,该方法的返回值是一个函数,根据现有的条件我们可以搭建出这个函数的雏形

Function.prototype.myBind = function () {
  const fn = this, // 当前函数
    _this = arguments[0], // 第一个参数,也就是绑定的this
    args = Array.prototype.slice.call(arguments, 1) // 传递给返回函数的默认参数列表
    return function () {
    // 绑定this的具体实现
  }
}

现在我们需要考虑这个函数的功能了,返回一个函数,返回函数的this绑定为传入的第一个参数,返回函数的默认参数是调用bind时从第二个参数开始的参数列表,这里我们用apply来实现

Function.prototype.myBind = function () {
  const fn = this, // 当前函数
    _this = arguments[0], // 第一个参数,也就是绑定的this
    args = Array.prototype.slice.call(arguments, 1) // 之后的参数列表
  return function () {
    // 通过bind绑定this,并将默认参数和当前参数合并后传入
    fn.apply(_this, args.concat(Array.prototype.slice.call(arguments)))
  }
}

以上我们就实现了bind方法的重写,同时根据重写我们应该能明白为什么bind的返回函数无法再被重新绑定this

function fn (num1, num2) {
  console.log(this) 
  console.log(num1, num2) 
}
const obj = {}
fn.bind(obj, 1, 2).call(window) // obj 1 2

通过上面的例子我们能看到bind的返回函数即使调用call了this也没有改为window,原因在于

Function.prototype.myBind = function () {
  const fn = this, 
    _this = arguments[0], 
    args = Array.prototype.slice.call(arguments, 1)
  // 注意我们return的函数内部的执行this的绑定是通过apply绑定的_this
  return function () {
    // 即使在这里改变了 this 实际上绑定的还是_this变量,这里函数this根本没有使用到
    fn.apply(_this, args.concat(Array.prototype.slice.call(arguments)))
  }
}

call、apply

现在我们看下call和apply的实现,call的传参方式和bind相同,但是功能不一样,call是直接执行调用的函数,并将其this改为参入的第一个参数

要改变this指向,除了显式模式下使用call、apply、bind之外,还有一种方法就是隐式绑定模式,通过将函数改变为对象方法的调用就可以完成,以下是具体实现

 Function.prototype.myCall = function () {
  const fn = this, //当前函数
    _this = arguments[0] || window // 第一个参数,也就是绑定的this
  args = Array.prototype.slice.call(arguments, 1); // 之后的参数列表

  //重点是这里 我们将当前函数 改变为传入对象的属性
  _this.fn = fn;
  let result;
  result = _this.fn(...args)
  //别忘了删除这个属性,否则外部传入的this会增加fn属性 因为js对于对象类型的参数是按引传递的
  delete _this.fn
  return result
}

知道了call怎么写apply就简单了,逻辑相同不一样的是仅仅是apply是将参数列表直接以数组的形式传入了,我们只需要修改原函数获取参数的代码就可以了

  // - call的获取参数
  args = Array.prototype.slice.call(arguments, 1); // 之后的参数列表
  
  // + apply的获取参数
  args = arguments[1] || []

new

在重写new前我们先简单了解new的几个特性

  1. 函数通过new关键字调用时,这个函数会被当做构造函数执行 其实在JavaScript中没有明确构造函数和一般函数的区别,默认情况下我们通常将构造函数的首字母大写以示区分,但实际上所有的函数都可以通过new关键字调用

  2. 构造函数区别于一般函数执行 当函数被作为构造函数执行时,函数内部会发生一些变化,或者说被添加一些功能,首先构造函数会在执行时生成一个对象,这个对象和构造函数的原型相关联,然后这个对象会被传递给this,最终构造函数返回的实例对象就是这个this

  3. 构造函数的返回值可由用户自己决定 简单一点说就是如果你在构造函数中手动去return值,如果这个值是对象类型,那么该值就会作为构造函数的返回值,如果不是对象类型,那么就会默认返回this

new的过程涉及到原型链的问题,这里不对其做细节讨论,只做简单的演示,本文重点关注的是this的实现

// 构造函数
function Person (name, age) {
  /**
   *  1. 当函数通过new调用时 函数内部会创建一个对象 这个对象和函数的原型关联起来 并且赋值给 this,过程示意如下
   *  默认创建了一个对象:const context = {} 
   *  对象的和函数的原型相关联:context.__proto__ = Person.prototype
   *  对象赋值给了this: const this = context
   *  注意:以上只是为了方便理解做的简易伪代码展示
   */

  // 此时这里就已经有一个this了 因此我们可以通过this. 的方式修改其属性
  this.name = name
  this.age = age

  /**
   * 最后如果我们没有主动返回值,那么就会默认返回this
   * 如果我们主动调用return,就分为两种情况了
   * 一:主动返回值是基础数据类型 返回值还是this
   * 二:主动返回值是对象类型 返回值是主动返回的对象类型的值
   */
}

const person1 = new Person('jack', 12) // {name:'jack',age:12} Person / 发现是new调用,函数就会当做构造函数执行
const person2 = Person('jack', 12) // undefined / 非new调用就是一般函数执行

接下来我们就可以实现new了,因为我们没法通过关键字调用实现的方法,我们就通过函数调用的方式实现,我们方法叫做myNew,第一个参数是构造函数,之后的参数是传递给构造函数的参数列表

function myNew (constructor, ...args) {
  // 创建实例对象 这里用Object.create可以让实例对象的原型为constructor.prototype.也就是之前说的和构造函数原型对象创建联系
  var instance = Object.create(constructor.prototype);
  // 通过bind将实例对象传入构造函数中 这一步主要是初始化实例对象的属性 如:构造函数中有 this.name = name 这种
  var result = constructor.apply(instance, args);
  // 如果函数主动return了对象类型的数据,就返回主动return的值,否则返回实例对象
  if (result && (typeof result === 'function' || typeof result === 'object')) {
    return result;
  }
  return instance;
}

结语

以上就是new、call、apply、bind的重写,笔者将其总结在一起,主要是他们都涉及到对于this绑定的理解,如果对this绑定不熟悉,可以翻看我之前的JavaScript中this指向解析,如果想看其他手写方法,也可以通过下方链接进行查看。

扎实的基础是技术成长的基石,与各位同学共勉。

上一篇手写文章地址:前端需要掌握的20个手写功能—防抖节流