乐趣区

关于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 函数柯里化

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

退出移动版