关于javascript:前端面试手写代码callapplybind

1 call、apply、bind 用法及比照

1.1 Function.prototype

三者都是Function原型上的办法,所有函数都能调用它们

Function.prototype.call
Function.prototype.apply
Function.prototype.bind

1.2 语法

fn代表一个函数

fn.call(thisArg, arg1, arg2, ...) // 接管参数列表
fn.apply(thisArg, argsArray) // apply 接管数组参数
fn.bind(thisArg, arg1, arg2, ...) // 接管参数列表

1.3 参数阐明

thisArg:在 fn 运行时应用的 this 值

arg1,arg2,...:参数列表,传给 fn 应用的

argsArray:数组或类数组对象(比方Arguments对象),传给 fn 应用的

1.4 返回值

callapply:同 fn 执行后的返回值

bind:返回一个原函数的拷贝,并领有指定的 this 值和初始参数。并且返回的函数能够传参。

const f = fn.bind(obj, arg1, arg2, ...)
f(a, b, c, ...)
// 调用 f 相当于调用 fn.call(obj, ...args)
// args是调用bind传入的参数加上调用f传入的参数列表
// 即arg1,arg2...a,b,c...

1.5 作用

三个办法的作用雷同:扭转函数运行时的this值,能够实现函数的重用

1.6 用法举例

function fn(a, b) {
    console.log(this.myName);
}

const obj = {
    myName: '蜜瓜'
}

fn(1, 2) 
// 输入:undefined 
// 因为此时this指向全局对象,全局对象上没有myName属性

fn.call(obj, 1, 2) 
fn.apply(obj, [1, 2])
// 输入:蜜瓜
// 此时this指向obj,所以能够读取到myName属性

const fn1 = fn.bind(obj, 1, 2)
fn1()
// 输入:蜜瓜
// 此时this指向obj,所以能够读取到myName属性

1.7 三个办法的比照

办法 性能 参数 是否立刻执行
apply 扭转函数运行时的this 数组
call 扭转函数运行时的this 参数列表
bind 扭转函数运行时的this 参数列表 否。返回一个函数
  1. applycall会立刻取得执行后果,而bind会返回一个曾经指定this和参数的函数,须要手动调用此函数才会取得执行后果
  2. applycall惟一的区别就是参数模式不同
  3. 只有apply的参数是数组,记忆办法:apply和数组array都是a结尾

2 实现call、apply、bind

2.1 实现call

2.1.1 易混同的变量指向

当初咱们来实现call办法,命名为myCall

咱们把它挂载到Function的原型上,让所有函数能调用这个办法

// 咱们用残余参数来接管参数列表
Function.prototype.myCall = function (thisArg, ...args) {
  console.log(this)
  console.log(thisArg)
}

首先要明确的是这个函数中thisthisArg别离指向什么

看看咱们是怎么调用的:

fn.myCall(obj, arg1, arg2, ...)

所以,myCall中的this指向fnthisArg指向obj(指标对象)

咱们的目标是让fn运行时的this(留神这个thisfn中的)指向thisArg指标对象

换句话说就是fn成为obj这个对象的办法来运行(外围思路)

2.1.2 简易版call

咱们根据上述外围思路能够写出一个简略版本的myCall

Function.prototype.myCall = function (thisArg, ...args) {
  // 给thisArg新增一个办法
  thisArg.f = this; // this就是fn
  // 运行这个办法,传入残余参数
  let result = thisArg.f(...args);
  // 因为call办法的返回值同fn
  return result;
};

call办法的基本功能就实现了,然而显然存在问题:

  1. 假使有多个函数同时调用这个办法,并且指标对象雷同,则存在指标对象的f属性被笼罩的可能
fn1.myCall(obj)
fn2.myCall(obj)
  1. 指标对象上会永远存在这个属性f

解决方案:

  1. ES6引入了一种新的原始数据类型Symbol,示意举世无双的值,最大的用法是用来定义对象的惟一属性名
  2. delete 操作符用于删除对象的某个属性

2.1.3 优化显著问题后的call

优化后的myCall

Function.prototype.myCall = function (thisArg, ...args) {
  // 生成惟一属性名,解决笼罩的问题
  const prop = Symbol()
  // 留神这里不能用.
  thisArg[prop] = this; 
  // 运行这个办法,传入残余参数,同样不能用.
  let result = thisArg[prop](...args);
  // 运行完删除属性
  delete thisArg[prop]
  // 因为call办法的返回值同fn
  return result;
};

至此myCall办法的性能就绝对残缺了,然而还有一些细节须要补充

2.1.4 补充细节后的call

如果咱们传入的thisArg(指标对象)是undefined或者null,咱们就将其替换为指向全局对象(MDN文档就是这么形容的)

// 残缺代码
Function.prototype.myCall = function (thisArg, ...args) {
  // 替换为全局对象:global或window
  thisArg = thisArg || global
  const prop = Symbol();
  thisArg[prop] = this;
  let result = thisArg[prop](...args);
  delete thisArg[prop];
  return result;
};

2.2 实现apply

applycall实现思路一样,只是传参模式不同

// 把残余参数改成接管一个数组
Function.prototype.myApply = function (thisArg, args) {
  thisArg = thisArg || global
  // 判断是否接管参数,若未接管参数,替换为[]
  args = args || []
  const prop = Symbol();
  thisArg[prop] = this;
  // 用...运算符开展传入
  let result = thisArg[prop](...args);
  delete thisArg[prop];
  return result;
};

2.3 实现bind

2.3.1 简易版bind

实现思路:bind会创立一个新的绑定函数,它包装了原函数对象,调用绑定函数会执行被包装的函数

后面曾经实现了callapply,咱们能够选用其中一个来绑定this,而后再封装一层函数,就能失去一个简易版的办法:

Function.prototype.myBind = function(thisArg, ...args) {
  // this指向的是fn
  const self = this
  // 返回绑定函数
  return function() {
    // 包装了原函数对象
    return self.apply(thisArg, args)
  }
}

2.3.2 留神点

  1. 留神apply的参数模式是数组,所以咱们传入的是args而非...args
  2. 为什么要在return前定义self来保留this

    因为咱们须要利用闭包将this(即fn)保存起来,使得myBind办法返回的函数在运行时的this值可能正确地指向fn

    具体解释如下:

// 如果不定义self
Function.prototype.myBind = function(thisArg, ...args) {
  return function() {
    return this.apply(thisArg, args)
  }
}
const f = fn.myBind(obj) // 返回一个函数
// 为了看得分明,写成上面这种模式
// 其中thisArg、args保留在内存中,这是因为造成了闭包
const f = function() {
  return this.apply(thisArg, args)
}
// 当初咱们调用f
// 会发现其this指向全局对象(window/global)
// 而非咱们冀望的fn
f()

2.3.3 让bind返回的函数(绑定函数)能够传参

后面说了bind返回的参数能够传参(见1.4),当初来对myBind进行改良:

Function.prototype.myBind = function(thisArg, ...args) {
  const self = this
  // 返回绑定函数,用残余参数接管参数
  return function(...innerArgs) {
    // 合并两次传入的参数
    const finalArgs = [...args, ...innerArgs]
    return self.apply(thisArg, finalArgs)
  }
}

2.3.4 “new + 绑定函数”存在什么问题

MDN:绑定函数也能够应用 new 运算符结构,它会体现为指标函数曾经被构建结束了似的。提供的 this 值会被疏忽,但前置参数仍会提供给模仿函数。

这是MDN文档中的形容,意思是绑定函数能够作为构造函数来创立实例,并且先前作为bind办法的第一个参数传入的指标对象thisArg生效,然而先前提供的参数仍然无效。

先来看咱们的myBind

绑定函数的外部:

// 绑定函数f
function(...innerArgs) {
  ...
  // 为了看得分明,这里间接将self写成了fn
  return fn.apply(thisArg, finalArgs)
}

new来创立f的实例:

const o = new f()

咱们都晓得(如果不晓得看这篇:手写实现new),new的过程中会执行构造函数的代码,即此处绑定函数f中的代码会被执行。

包含fn.apply(thisArg, finalArgs)这句代码,并且其中的thisArg依然无效,这就不合乎原生bind办法的形容了

2.3.5 绑定函数中怎么辨别是否应用了new

如何解决:用new创立绑定函数的实例时,让先前传入的thisArg生效

事实上对于绑定函数f来说,执行时的this值并不确定。

  1. 如果咱们间接执行f,那么绑定函数中的this指向全局对象
  2. 如果咱们用new来创立f的实例,那么f中的this指向新创建的实例。(这点如果不分明看这篇:手写实现new)
Function.prototype.myBind = function(thisArg, ...args) {
  const self = this
  return function(...innerArgs) {
    console.log(this) // 留神此处的this并不确定
    const finalArgs = [...args, ...innerArgs]
    return self.apply(thisArg, finalArgs)
  }
}
// 绑定函数
const f = fn.myBind(obj)
// 如果咱们间接执行f,那么绑定函数中的this指向全局对象
f()
// 如果咱们用new来创立f的实例,那么f中的this指向新创建的实例
const o = new f()

基于上述两种状况,咱们能够批改myBind返回的绑定函数,在函数内对this值进行判断,从而辨别是否应用了new运算符

myBind进行如下更改:

Function.prototype.myBind = function(thisArg, ...args) {
  const self = this
  const bound = function(...innerArgs) {
    const finalArgs = [...args, ...innerArgs]
    const isNew = this instanceof bound // 以此来判断是否应用了new
    if (isNew) {
      
    } 
    // 未应用new就跟原来一样返回
    return self.apply(thisArg, finalArgs)
  }
  return bound
}

2.3.6 补充完绑定函数外部操作

当初咱们须要晓得如果是new结构实例的状况应该进行哪些操作。

看看应用原生bind办法是什么后果:

const fn = function(a, b) {
  this.a = a
  this.b = b
}
const targetObj = {
  name: '蜜瓜'
}
// 绑定函数
const bound = fn.bind(targetObj, 1)
const o = new bound(2)
console.log(o); // fn { a: 1, b: 2 }
console.log(o.constructor); // [Function: fn]
console.log(o.__proto__ === fn.prototype); // true

能够看到,new bound()返回的是以fn为构造函数创立的实例。

依据这点能够补充完if (new) {}中的代码:

Function.prototype.myBind = function(thisArg, ...args) {
  const self = this
  const bound = function(...innerArgs) {
    const finalArgs = [...args, ...innerArgs]
    const isNew = this instanceof bound // 以此来判断是否应用了new
    if (isNew) {
      // 间接创立fn的实例
      return new self(...finalArgs)
    } 
    // 未应用new就跟原来一样返回
    return self.apply(thisArg, finalArgs)
  }
  return bound
}
const bound = fn.myBind(targetObj, 1)
const o = new bound(2)

这样,const o = new bound(2)相当于const o = new self(...finalArgs),因为构造函数如果显式返回一个对象,就会间接笼罩new过程中创立的对象(不晓得的话能够看看这篇:手写实现new)

2.3.7 残缺代码

Function.prototype.myBind = function(thisArg, ...args) {
  const self = this
  const bound = function(...innerArgs) {
    const finalArgs = [...args, ...innerArgs]
    const isNew = this instanceof bound
    if (isNew) {
      return new self(...finalArgs)
    } 
    return self.apply(thisArg, finalArgs)
  }
  return bound
}

事实上,这段代码仍存在和原生bind出入的中央,然而这里只是表白实现bind的一个整体思路,不用奢求完全一致

3 补充

  1. applycall办法还有一些细节咱们没有实现:如果这个函数(fn)处于非严格模式下,则指定为 nullundefined 时会主动替换为指向全局对象,原始值会被包装(比方1会被包装类Number包装成对象)。
  2. bind办法也是函数柯里化的一个利用,不相熟柯里化的能够看看这篇内容:JS函数柯里化

公众号【前端嘛】获取更多前端优质内容

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理