深入学习this 与 call、apply、bind 方法(完整版)

207 阅读14分钟

this 与 call、apply、bind 方法

一、前置知识

1、对 this 的理解

  • this是执行上下文中的一个属性。
  • 它指向最后一次调用这个方法的对象。

实际开发过程中,this的指向可以通过4中调用模式来判断:

1、函数调用模式

当一个函数不是某个对象的属性时,直接作为函数来调用时,this指向全局对象。

2、方法调用模式

如果一个函数作为一个对象的方法来调用时,this指向这个新创建的对象。

3、构造器调用模式

如果一个函数用new调用时,函数执行前会新创建一个对象,this指向这个新创建的对象。

4、apply、bind、call调用模式

    这三个方法都是可以显示的指定调用函数的this指向。

    apply方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。

    call方法接收的参数,第一个是 this 的绑定的对象,后面剩余参数是传入函数指向的参数。

    也就是说,在调用 call() 方法时,传递给函数的参数biu逐个列举出来。

    bind方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this指向除了使用 new 时会被改变,其他情况都不会改变。

2、函数 this 的绑定规则

1、默认绑定

当函数调用类型为独立函数调用时,函数 this 为默认绑定,指向全局变量;在严格模式下,this 绑定到 undefined。

function foo() {
  console.log(this.a);
}

foo(); //等价于window.foo()
2、隐式绑定

当函数的调用位置有上下文对象时,或者说函数在被调用时被某个对象拥有或者包含时,隐式绑定规则就会把函数调用中的 this 绑定到这个上下文对象。

如下:foo 在调用时 this 便被隐式绑定到了 obj 上了

function foo2() {
  console.log(this.a);
}

const obj = {
  a: 2,
  foo,
};

obj.foo();
3、显示绑定

使用 call、apply、bind 显示的绑定函数调用时的 this 指向。注意无论是 call、bind、apply 方法,都是被另外一个方法调用

function showName(arg1, arg2) {
  console.log(arg1, arg2, this.name);
}

const user = {
  name: "jack",
};

showName.call(user, "hello", "world");
4、new

当使用 new 调用函数时,会发生 this 的指向绑定,但此时发生的 this 绑定与函数本身无关。

二、call、apply、bind介绍

1、call()、apply()、bind()介绍

1、call()方法 [ Function.prototype.call() ]
语法
function call(thisArg, arg1, arg2, ...)
参数

thisArg

可选的。在 function 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。

arg1, arg2, ...

指定的参数列表。

返回值

使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined

描述

call() 允许为不同的对象分配和调用属于一个对象的函数/方法。

call() 提供新的 this 值给当前调用的函数/方法。你可以使用 call 来实现继承:写一个方法,然后让另外一个新的对象来继承它(而不是在新对象中再写一次这个方法)。

案例demo
<script>
  function sayHello(arg1, arg2) {
    console.log(
      `我是${this.name}, 今年${this.age}岁.接受传过来的值arg1: ${arg1},arg2: ${arg2}`
    );
  }

  let user = {
    name: "jack",
    age: 20,
  };

  sayHello.call(user, "参数1", "参数2");
</script>
使用场景

1、使用 call 方法调用父构造函数

    在一个子构造函数中,你可以通过调用父构造函数的 call 方法来实现继承,类似于 Java 中的写法。下例中,使用 Food 和 Toy 构造函数创建的对象实例都会拥有在 Product 构造函数中添加的 name 属性和 price 属性,但 category 属性是在各自的构造函数中定义的。

<script>
  function Product(name, price) {
    this.name = name;
    this.price = price;
  }

  function Food(name, price) {
    Product.call(this, name, price);
    this.category = "food";
  }

  function Toy(name, price) {
    Product.call(this, name, price);
    this.category = "toy";
  }

  let cheese = new Food("feta", 5);
  let fun = new Toy("feta", 5);

  console.log("cheese", cheese);
  console.log("fun", fun);
</script>

2、使用 call 方法调用匿名函数

    在下例中的 for 循环体内,我们创建了一个匿名函数,然后通过调用该函数的 call 方法,将每个数组元素作为指定的 this 值执行了那个匿名函数。这个匿名函数的主要目的是给每个数组元素对象添加一个 print 方法,这个 print 方法可以打印出各元素在数组中的正确索引号。当然,这里不是必须得让数组元素作为 this 值传入那个匿名函数(普通参数就可以),目的是为了演示 call 的用法。

<script>
  let animal = [
    { species: "Lion", name: "King" },
    { species: "Whale", name: "Fail" },
  ];

  for (let i = 0; i < animal.length; i++) {
    (function (i) {
      this.print = function () {
        console.log("#" + i + " " + this.species + ": " + this.name);
      };
      this.print();
    }.call(animal[i], i));
  }
</script>

3、使用 call 方法调用函数并指定上下文的 'this'

在下面的例子中,当调用 greet 方法的时候,该方法的this值会绑定到 obj 对象。

<script>
  function greet() {
    let reply = [
      this.animal,
      "typically sleep between",
      this.sleepDuration,
    ].join(" ");
    console.log(reply);
  }

  var obj = {
    animal: "cats",
    sleepDuration: "12 and 16 hours",
  };

  greet.call(obj); // cats typically sleep between 12 and 16 hours
</script>

4、使用 call 方法调用函数并且不指定第一个参数(argument)

    在下面的例子中,我们调用了 display 方法,但并没有传递它的第一个参数。如果没有传递第一个参数,this 的值将会被绑定为全局对象。

var sData = 'Wisen';

function display() {
  console.log('sData value is %s ', this.sData);
}

display.call();  // sData value is Wisen

备注: 在严格模式下,this 的值将会是 undefined.如下:

'use strict'

var sData = 'Wisen';

function display() {
    console.log(this.sData);
}

display.call(); // Cannot read the property of 'sData' of undefined
2、apply()方法 [ Function.prototype.apply() ]
语法
apply(thisArg)
apply(thisArg, argsArray)
参数

thisArg

在 func 函数运行时使用的 this 值。请注意,this 可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。

argsArray 可选

一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为 null 或 undefined,则表示不需要传入任何参数。从 ECMAScript 5 开始可以使用类数组对象。

返回值

调用有指定 this 值和参数的函数的结果。

描述

备注: 虽然这个函数的语法与 call() 几乎相同,但根本区别在于,call() 接受一个参数列表,而 apply() 接受一个参数的单数组

备注: 当第一个参数为 null 或 undefined 时,可以使用数组展开语法实现类似的结果。

    在调用一个存在的函数时,你可以为其指定一个 this 对象。this 指当前对象,也就是正在调用这个函数的对象。使用 apply,你可以只写一次这个方法然后在另一个对象中继承它,而不用在新对象中重复写该方法。

    apply 与 call() 非常相似,不同之处在于提供参数的方式。apply 使用参数数组而不是一组参数列表。apply 可以使用数组字面量(array literal),如 fun.apply(this, ['eat', 'bananas']),或数组对象,如 fun.apply(this, new Array('eat', 'bananas'))

    你也可以使用 arguments 对象作为 argsArray 参数。arguments 是一个函数的局部变量。它可以被用作被调用对象的所有未指定的参数。这样,你在使用 apply 函数的时候就不需要知道被调用对象的所有参数。你可以使用 arguments 来把所有的参数传递给被调用对象。被调用对象接下来就负责处理这些参数。

    从 ECMAScript 第 5 版开始,可以使用任何种类的类数组对象,就是说只要有一个 length 属性和 (0..length-1) 范围的整数属性。例如现在可以使用 NodeList 或一个自己定义的类似 {'length': 2, '0': 'eat', '1': 'bananas'} 形式的对象。

备注: 许多较旧的浏览器,包括 Chrome <17 以及 Internet Explorer <9 不接受类数组对象。如果传入类数组对象,它们会抛出异常。

案例demo
<script>
  function sayHello(arg1, arg2) {
    console.log(
      `我是${this.name}, 今年${this.age}岁.接受传过来的值arg1: ${arg1},arg2: ${arg2}`
    );
  }

  let user = {
    name: "jack",
    age: 20,
  };

  sayHello.apply(user, ["参数1", "参数2"]); // 我是jack, 今年20岁.接受传过来的值arg1: 参数1,arg2: 参数2
</script>
使用场景

1、使用apply将数组各项添加到另一个数组

    我们可以使用 push 将元素追加到数组中。由于 push 接受可变数量的参数,所以也可以一次追加多个元素。

    但是,如果 push 的参数是数组,它会将该数组作为单个元素添加,而不是将这个数组内的每个元素添加进去,因此我们最终会得到一个数组内的数组。如果不想这样呢?concat 符合我们的需求,但它并不是将元素添加到现有数组,而是创建并返回一个新数组。然而我们需要将元素追加到现有数组......那么怎么做好?难道要写一个循环吗?别当然不是!

<script>
  const arr = ["a", "b"];
  const elements = [1, 2, 3];

  // 如果使用concat会创建一个新数组
  //   let result = arr.concat(elements);
  //   console.log(result);

  // 如果使用apply则在原数组上添加元素
  arr.push.apply(arr, elements);
  console.log(arr);
</script>

2、使用apply和内置函数

    对于一些需要写循环以遍历数组各项的需求,我们可以用 apply 完成以避免循环。

    下面是示例,我们将用 Math.max/Math.min 求得数组中的最大/小值。

<script>
  const numbers = [-99, 5, 99, 2, 3, 7];

  // 使用 Math.min/Math.max 以及 apply 函数时的代码
  let maxValue = Math.max.apply(null, numbers);
  let minValue = Math.min.apply(null, numbers);

  //   最大值是:99,最小值是:-99
  console.log(`最大值是:${maxValue},最小值是:${minValue}`);

  // 对比:简单循环算法
  max = Infinity;
  min = -Infinity;

  for (let i of numbers) {
    if (i > max) {
      max = i;
    }
    if (i < min) {
      min = i;
    }
  }
  //   最大值是:99,最小值是:-99
  console.log(`最大值是:${maxValue},最小值是:${minValue}`);
</script>

    注意:如果按上面方式调用 apply,有超出 JavaScript 引擎参数长度上限的风险。一个方法传入过多参数(比如一万个)时的后果在不同 JavaScript 引擎中表现不同。(JavaScriptCore 引擎中有被硬编码的参数个数上限:65536)。

    这是因为此限制(实际上也是任何用到超大栈空间的行为的自然表现)是不明确的。一些引擎会抛出异常,更糟糕的是其他引擎会直接限制传入到方法的参数个数,导致参数丢失。比如:假设某个引擎的方法参数上限为 4(实际上限当然要高得多),这种情况下,上面的代码执行后,真正被传递到 apply的参数为 5, 6, 2, 3 ,而不是完整的数组。

    如果你的参数数组可能非常大,那么推荐使用下面这种混合策略:将数组切块后循环传入目标方法:

<script>
  function minOfArray(arr) {
    let min = Infinity;
    let QUANTUM = 32768;

    for (let i = 0, len = arr.length; i < len; i += QUANTUM) {
      const submin = Math.min.apply(
        null,
        arr.slice(i, Math.min(i + QUANTUM, len))
      );
      min = Math.min(submin, min);
    }

    return min;
  }

  let min = minOfArray([99, 6, -99, 3, 7]);
  console.log(min);
</script>

3、使用apply来链接构造器

    你可以使用 apply 来链接一个对象构造器,类似于 Java。在接下来的例子中我们会创建一个全局 Global_Objects/Function 对象的 construct 方法,来使你能够在构造器中使用一个类数组对象而非参数列表。

<script>
  Function.prototype.construct = function (aArgs) {
    let oNew = Object.create(this.prototype);
    this.apply(oNew, aArgs);
    return oNew;
  };

  function MyConstructor() {
    for (let nProp = 0; nProp < arguments.length; nProp++) {
      this["property" + nProp] = arguments[nProp];
    }
  }

  let myArray = [4, "Hello World!", false];
  let myConstructor = MyConstructor.construct(myArray);

  console.log(myConstructor.property1); // 'Hello world!'
  console.log(myConstructor instanceof MyConstructor); // 'true'
  console.log(myConstructor.constructor); // 'MyConstructor'
</script>

备注: 这个非原生的 Function.construct 方法无法和一些原生构造器(例如 Date)一起使用。在这种情况下你必须使用 Function.prototype.bind 方法。例如,想象有如下一个数组要用在 Date 构造器中:[2012, 11, 4];这时你需要这样写:new (Function.prototype.bind.apply(Date, [null].concat([2012, 11, 4])))() ——无论如何这不是最好的实现方式并且也许不该用在任何生产环境中。

3、bind()方法 [ Function.prototype.bind() ]
语法
function.bind(thisArg[, arg1[, arg2[, ...]]])
参数

thisArg

    调用绑定函数时作为 this 参数传递给目标函数的值。如果使用new运算符构造绑定函数,则忽略该值。当使用 bind 在 setTimeout 中创建一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 object。如果 bind 函数的参数列表为空,或者thisArgnullundefined,执行作用域的 this 将被视为新函数的 thisArg

arg1, arg2, ...

    当目标函数被调用时,被预置入绑定函数的参数列表中的参数。

返回值

返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。

描述

    bind() 函数会创建一个新的绑定函数bound function,BF)。绑定函数是一个 exotic function object(怪异函数对象,ECMAScript 2015 中的术语),它包装了原函数对象。调用绑定函数通常会导致执行包装函数。 绑定函数具有以下内部属性:

  • [[BoundTargetFunction]] - 包装的函数对象
  • [[BoundThis]] - 在调用包装函数时始终作为 this 值传递的值。
  • [[BoundArguments]] - 列表,在对包装函数做任何调用都会优先用列表元素填充参数列表。
  • [[Call]] - 执行与此对象关联的代码。通过函数调用表达式调用。内部方法的参数是一个this值和一个包含通过调用表达式传递给函数的参数的列表。

    当调用绑定函数时,它调用 [[BoundTargetFunction]] 上的内部方法 [[Call]] ,就像这样 Call(boundThisargs) 。其中,boundThis 是 [[BoundThis]]args 是 [[BoundArguments]] 加上通过函数调用传入的参数列表。

    绑定函数也可以使用 new 运算符构造,它会表现为目标函数已经被构建完毕了似的。提供的 this 值会被忽略,但前置参数仍会提供给模拟函数。

案例demo
<script>
  function sayHello(arg1, arg2) {
    console.log(
      `我是${this.name}, 今年${this.age}岁.接受传过来的值arg1: ${arg1},arg2: ${arg2}`
    );
  }

  let user = {
    name: "jack",
    age: 20,
  };

  let fn = sayHello.bind(user, "参数1", "参数2");
  fn();
</script>
使用场景

1、创建绑定函数

    bind() 最简单的用法是创建一个函数,不论怎么调用,这个函数都有同样的 this 值。JavaScript 新手经常犯的一个错误是将一个方法从对象中拿出来,然后再调用,期望方法中的 this 是原来的对象(比如在回调中传入这个方法)。如果不做特殊处理的话,一般会丢失原来的对象。基于这个函数,用原始的对象创建一个绑定函数,巧妙地解决了这个问题:

<script>
  this.x = 9;

  var module = {
    x: 81,
    getX() {
      return this.x;
    },
  };

  module.getX(); // 81

  var retrieveX = module.getX;
  retrieveX(); // 返回 9。因为这句话等价于 window.retrieveX(),this的指向取决于最后被调用的对象

  var boundGetX = retrieveX.bind(module);
  boundGetX(); // 虽然等价于window.retrieveX.bind(module); 但是此时的this指向被bind方法改变成了 module.结果成81
</script>

2、偏函数

    bind() 的另一个最简单的用法是使一个函数拥有预设的初始参数。只要将这些参数(如果有的话)作为 bind() 的参数写在 this 后面。当绑定函数被调用时,这些参数会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们后面。

<script>
  function list() {
    return Array.prototype.slice.call(arguments);
  }

  function addArguments(arg1, arg2) {
    return arg1 + arg2;
  }

  let listRes = list(1, 2, 3); //[1,2,3]
  let result = addArguments(1, 2); // 3

  console.log(listRes, result);

  // 创建一个函数 它拥有预设参数列表
  var leadingThirtysevenList = list.bind(null, 37); // 切记 bind返回的是一个方法
  //   console.log("leadingThirtysevenList:", leadingThirtysevenList()); // [37]

  // 创建一个函数,它拥有预设的第一个参数
  var addThirtySeven = addArguments.bind(null, 37);
  //   console.log("addThirtySeven", addThirtySeven()); // NaN

  var list2 = leadingThirtysevenList(); // [37]

  var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]

  var result2 = addThirtySeven(5); // 37 + 5 = 42

  var result3 = addThirtySeven(5, 10);
  // 37 + 5 = 42,第二个参数被忽略
</script>

3、配合setTimeOut

    在默认情况下,使用 window.setTimeout() 时,this 关键字会指向 window(或 global)对象。当类的方法中需要 this 指向类的实例时,你可能需要显式地把 this 绑定到回调函数,就不会丢失该实例的引用。

function LateBloomer() {
  this.petalCount = Math.ceil(Math.random() * 12) + 1;
}

// 在 1 秒钟后声明 bloom
LateBloomer.prototype.bloom = function() {
  window.setTimeout(this.declare.bind(this), 1000);
};

LateBloomer.prototype.declare = function() {
  console.log('I am a beautiful flower with ' +
    this.petalCount + ' petals!');
};

var flower = new LateBloomer();
flower.bloom();  // 一秒钟后,调用 'declare' 方法

4、作为构造函数使用的绑定函数

警告: 这部分演示了 JavaScript 的能力并且记录了 bind() 的超前用法。以下展示的方法并不是最佳的解决方案,且可能不应该用在任何生产环境中。

    绑定函数自动适应于使用 new 操作符去构造一个由目标函数创建的新实例。当一个绑定函数是用来构建一个值的,原来提供的 this 就会被忽略。不过提供的参数列表仍然会插入到构造函数调用时的参数列表之前。

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function() {
  return this.x + ',' + this.y;
};

var p = new Point(1, 2);
p.toString(); // '1,2'

var emptyObj = {};
var YAxisPoint = Point.bind(emptyObj, 0/*x*/);

// 本页下方的 polyfill 不支持运行这行代码,
// 但使用原生的 bind 方法运行是没问题的:

var YAxisPoint = Point.bind(null, 0/*x*/);

/*(译注:polyfill 的 bind 方法中,如果把 bind 的第一个参数加上,
即对新绑定的 this 执行 Object(this),包装为对象,
因为 Object(null) 是 {},所以也可以支持)*/

var axisPoint = new YAxisPoint(5);
axisPoint.toString(); // '0,5'

axisPoint instanceof Point; // true
axisPoint instanceof YAxisPoint; // true
new YAxisPoint(17, 42) instanceof Point; // true

    请注意,你不需要做特别的处理就可以创建一个和 new 操作符一起使用的绑定函数。也就是说,你不需要做特别处理就可以创建一个可以被直接调用的绑定函数,即使你更希望绑定函数是用 new 操作符来调用。

// ...接着上面的代码继续的话,
// 这个例子可以直接在你的 JavaScript 控制台运行

// 仍然能作为一个普通函数来调用
//(即使通常来说这个不是被期望发生的)
YAxisPoint(13);

emptyObj.x + ',' + emptyObj.y;   //  '0,13'

    如果你希望一个绑定函数要么只能用 new 操作符,要么只能直接调用,那你必须在目标函数上显式规定这个限制。

5、快捷调用

    在你想要为一个需要特定的 this 值的函数创建一个捷径(shortcut)的时候,bind() 也很好用。

    你可以用 Array.prototype.slice 来将一个类似于数组的对象(array-like object)转换成一个真正的数组,就拿它来举例子吧。你可以简单地这样写:

var slice = Array.prototype.slice;

// ...

slice.apply(arguments);

    用 bind()可以使这个过程变得简单。在下面这段代码里面,slice 是 Function.prototype 的 apply() 方法的绑定函数,并且将 Array.prototype 的 slice() 方法作为 this 的值。这意味着我们压根儿用不着上面那个 apply()调用了。

// 与前一段代码的 "slice" 效果相同
var unboundSlice = Array.prototype.slice;
var slice = Function.prototype.apply.bind(unboundSlice);

// ...

slice(arguments);

2、call() 和 apply() 的区别

    它们的作用一模一样,区别仅在于传入参数的形式不同。

  • apply 接受两个参数,第一个参数制定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数。
  • call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数。

三、实现call

1、call方法

call的实现步骤:

    1、判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用call等方式调用的情况

    2、判断传入上下文是否存在,如果不存在,则设置为window

    3、处理传入的参数,截取第一个参数后的所有参数

    4、将函数作为上下文对象的一个属性

    5、使用上下文对象来调用这个对象,并保存返回结果

    6、删除刚才新增的属性

    7、返回结果

<script>
  Function.prototype.myCall = function (context) {
    // 判断调用对象
    if (typeof this != "function") {
      new Error("type error");
    }

    // 获取参数
    let args = [...arguments].slice(1);
    result = null;

    // 判断context是否传入,如果未传入则设置为window
    context = context || window;
    // 将调用函数设为对象的方法
    context.fn = this;
    // 调用函数
    result = context.fn(...args);
    // 将属性删除
    delete context.fn;
    return result;
  };
</script>
<script>
  Function.prototype.myCall2 = function (context, ...args) {
    // 判断传入对象是否存在,因为是可选参数
    if (!context || context == null) {
      context = window;
    }

    // 创造唯一的key值 作为我们构造的context内部方法名
    let fn = Symbol();
    context[fn] = this;

    return context[fn](...args);
  };
</script>
<script>
  Function.prototype.myCall = function () {
    let args = Array.prototype.slice.call(arguments);

    let context = args.shift(); // 传入对象(弹出第一个参数)
    context.fn = this;

    console.log(args);

    let result = context.fn(...args);
    delete context.fn;
    return result;
  };

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

  fn.myCall({ name: "jack" }, 20, 30);
</script>

2、apply方法

apply函数的实现步骤:

    1、判断调用对象是否为函数,即使是定义在函数的原型上,但是可能出现使用call方法的情况

    2、判断传入上下文对象是否存在,如果存在,则设置为window

    3、将函数作为上下文对象的一个属性

    4、判断参数值是否传入

    5、使用上下文对象来调用这个方法,并保存返回结果

    6、删除刚才新增的属性

    7、返回结果

<script>
  Function.prototype.myApply = function (context) {
    if (typeof this !== "function") {
      throw new TypeError("Type error");
    }

    if (!context || context == null) {
      context = window;
    }

    let result = null;
    // 将函数设为对象的方法
    context.fn = this;
    // 调用方法
    if (arguments[1]) {
      result = context.fn(...arguments[1]);
    } else {
      result = context.fn();
    }
    // 删除方法
    delete context.fn;
    return result;
  };
</script>
<script>
  Function.prototype.myApply = function () {
    let args = Array.prototype.slice.call(arguments);

    let context = args.shift();

    context.fn = this;

    let result = context.fn();
    delete context.fn;
    return result;
  };

  function fun(a, b) {
    console.log(a, b, this.name);
  }

  let user = {
    name: "jack",
  };

  fun.myApply(user, [20, 30]);
</script>

3、bind方法

bind函数的实现步骤:

    1、判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。

    2、保存当前函数的引用,获取其余传入参数

    3、创建一个函数返回

    4、函数内部使用apply来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的this给apply调用,其余情况都传入指定的上下文对象

<script>
  Function.prototype.myBind = function (context) {
    if (typeof this !== "function") {
      throw new TypeError("Type Error");
    }

    if (!context || context == null) {
      context = window;
    }

    let args = [...arguments].slice(1);

    fn = this;

    return function Fn() {
      return fn.apply(
        this instanceof Fn ? this : context,
        args.concat(...arguments)
      );
    };
  };

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

  let user = {
    name: "jack",
    age: 20,
  };

  let res = sayHello.myBind(user, "rose", 30);
  res();
</script>