手写apply、call、bind函数

时间:2021-1-18 作者:admin

本文档已更新于 【前端橘子君】 【Github】

this指向

// this指向
function fn () {
  console.log(this)
}

var obj = {
  name: '张三'
}
obj.fn = fn;

obj.fn() // 输出:obj
fn() // 输出window

1、非严格模式下默认指向全局的window
2、this指向调用者

在上述代码中

  • obj.fn()中函数 fn 的调用者是 obj,所以最后输出obj对象
  • fn()可以看作是window.fn(),window 是函数 fn 的调用者,所以输出window对象。

详情可见 this指向问题

call、apply、bind的用法

function sum (a, b) {
  console.log(a + b, this);
}

var obj = {
  name: '橘子君'
}

sum(1, 2) // window
sum.call(obj, 1, 2) // obj
sum.apply(obj, [1, 2]) // obj
sum.bind(obj)(1, 2) // obj

可以看到,默认状况下,this指向的是window,而call、apply、bind函数改变了this的指向.

call的实现方式

sum.call(obj, 1, 2)

先看看参数,第一个参数是目标对象,即需要修改的this指向的参数,其余参数为sum函数的真实参数

这里需要解决两个问题

  • 1、怎么修改this指向
  • 2、参数长度可变问题

第二个问题很好解决,用ES6的解构就行
第一个问题也好解决,最开始的时候说过函数中的this指向被调用者,那么我们在call函数内部用obj.sum()就可以将this指向调用者obj

/**
 * @params {Object} ctx 即目标对象
 * @params {any} args 真实的参数
*/
Function.prototype._call = function (ctx, ...args) { // 用结构的方法解决可变参数的问题
  ctx.fn = this; // 修改this的指向
  return ctx.fn(...args); // 此时的this指向的就是ctx
}

function sum (a, b) {
  console.log(a + b, this);
}

var obj = {
  name: '橘子君'
}
sum._call(obj, 1, 2); // 输出:3 {name: "橘子君", fn: ƒ}

var obj2 = {
  name: '橘子君',
  fn (a, b) {
    console.log(a * b, this)
  }
}
sum._call(obj2, 1, 2) // 输出:3 {name: "橘子君", fn: ƒ}
obj2.sum(1, 2) // 输出:3 {name: "橘子君", fn: ƒ}

可以看到,这里实现了call的基本功能,但是这种写法又出现了两个问题

  • 目标对象ctx中新增了一个sum方法
  • 当目标对象中存在一个同名方法时(即上述代码中的fn),会覆盖对象自身的同名方法

第一个问题好解决,可以用delete方法进行删除

至于第二个问题,在ES6中新引入了一种数据类型:Symbol

Symbol(): 创造一个独一无二的值,但不是字符串,而是一个新的数据类型

var obj = {};
var n = obj[Symbol()] = 1
console.log(obj[Symbol()]) // 输出:undefined
console.log(n) // 输出: 1

因此我们可以用Symbol()来解决上述遗留的第二个问题

Function.prototype._call = function (ctx, ...args) { // 用结构的方法解决可变参数的问题
  ctx = ctx || window; // 默认为window
  let Sym = Symbol('fn'); // 创建临时属性
  ctx[Sym] = this; // 修改this的指向
  let result = ctx[Sym](...args);
  delete ctx[Sym]; // 删除临时属性
  return result;
}

function sum (a, b) {
  console.log(a + b, this);
}

var obj = {
  name: '橘子君'
}

sum._call(obj, 1, 2); // 输出:3 {name: "橘子君"}

到此为止,call的方法已经实现了。

apply的实现方式

apply和call的实现方法可以说是一样的,甚至说apply比call要简单,因为它的参数数量是固定的,只有2个,即目标对象和参数数组

call的实现方式中,因为我们调用的时候用了解构,所以我们只要针对call方法修改一行代码就行了。

Function.prototype._apply = function (ctx, args) { // 固定参数数量
  ctx = ctx || window;
  let Sym = Symbol('fn');
  ctx[Sym] = this;
  let result = ctx[Sym](...args);
  delete ctx[Sym];
  return result;
}

function sum (a, b) {
  console.log(a + b, this);
}

var obj = {
  name: '橘子君'
}

sum._apply(obj, [1, 2]); // 输出:3 {name: "橘子君"}

bind的实现方式

bind函数是一个高阶函数,因为它返回的不是一个具体数值,而是一个函数

var obj = {}
function sum(a, b) {
  console.log(a + b)
};
sum.bind(obj); // 输出:ƒ sum () {}
sum.bind(obj)(1, 2) // 输出:3

既然如此,我们先来看看怎么实现它。

Function.prototype._bind = function (ctx) {
  ctx = ctx || window;
  let Sym = Symbol('fn');
  ctx[Sym] = this;
  return function (...args) { // 相比call和apply,bind方法只是多了一层外壳而已。
    // 调用方法和删除临时属性放到了返回函数体中
    let result = ctx[Sym](...args);
    delete ctx[Sym];
    return result;
  }
}
var obj = {
  name: '橘子君'
}
function sum(a, b) {
  console.log(a + b)
  console.log(this);
};
sum._bind(obj)(1,2) // 输出:3 {name: '橘子君'}

相比apply和call方法,不同的是bind方法将sum函数的实际调用和删除临时属性放到了返回的函数体中,其他的基本都是一致的。


更多相关文档,请见:

线上地址 【前端橘子君】

GitHub仓库【前端橘子君】

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。