深入理解call,apply,bind相同点及区别

25 阅读5分钟

1.call,apply,bind相同点和区别总结

相同点 : 作用一致,修改函数this指向

不同点 :

传参方式不同 :

call是按照顺序传参 sum.call(obj,1,2,3),

apply是数组/伪数组传参sum.apply(obj,[1,2,3]),

bind按照顺序传参sum1=sum.bind(obj,1,2,3)(),返回一个新的函数sum1(4,5)可以后续补充参数

执行机制不同 :

call和apply会立即执行函数,而bind不会立即执行而是得到修改this的新函数

柯里化特性

bind 支持 “预传部分参数 + 后续补充参数” 的柯里化特性

call apply “立即绑定上下文(this)并调用函数”,参数必须在调用时一次性传全(或传部分,但后续无法补充)

2.call,apply,bind底层原理

2.1 call

①.绑定this

// 绑定this
Function.prototype.call2 = function(context) {
    // 首先要获取调用call2的函数,用this可以获取,这里是bar
    context.fn = this;//foo.fn=bar
    context.fn();//执行bar(),通过foo调用
    delete context.fn;//垃圾回收
}

// 测试一下
 let foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call2(foo); // 1

根据前面发的执行上下文和this,我们可以知道

当执行 bar.call2(foo) 时,发生了这几件事:

  1. call2 函数内部的 this 指向 bar(因为是 bar 调用了 call2);
  2. context 指向传入的 foo 对象;
  3. context.fn = this → 相当于 foo.fn = bar(给 foo 临时加了一个 fn 属性,值为 bar 函数);
  4. context.fn() → 相当于 foo.fn()(通过 foo 对象调用 bar 函数);
  5. 因为函数是通过 foo 对象调用的,所以 bar 内部的 this 指向 foo,因此 this.value 就是 foo.value(1);
  6. 最后 delete context.fn → 删除 foo 上的临时属性 fn,恢复 foo 的原始状态(避免污染)。

②.传参

Function.prototype.call2 = function(context,...arg) {
    context.fn = this;
    context.fn(...arg)
    delete context.fn;
}

// 测试一下
var foo = {
    value: 1
};

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

bar.call2(foo, 'aria', 28); 
// aria
// 28
// 1

③.this传入null

JavaScriptcall(null) 的核心是:

  • 非严格模式:this 绑定全局对象(window/global);
  • 严格模式:thisnull
  • 箭头函数:this 不受影响(继承外层作用域)。

最实用的场景是调用不依赖 this 的函数并传递参数,明确表示 “无需改变 this 指向”,语义更清晰。若函数依赖 this,则需传入具体的绑定对象(而非 null)。

Function.prototype.call2 = function (context, ...args) {
    // 如果 context 是 null 或 undefined,globalThis 就是正确的全局对象
    // 在浏览器里,globalThis 就是 window
    // 在 Node.js 里,globalThis 就是 global
    const ctx = context ?? globalThis; 

    ctx.fn = this;
    const result = ctx.fn(...args);
    delete ctx.fn;
    return result;
//原生 call 的逻辑是:执行目标函数后,会将函数的返回值作为自己的返回值返回。
};

相关知识点

globalThis

globalThis 是 ECMAScript 2020 (ES11) 引入的一个标准内置对象,它提供了一种统一的方式来访问全局对象,无论代码运行在什么环境中。

  • 在浏览器环境中globalThis 指向 window 对象。
  • 在 Node.js 环境中globalThis 指向 global 对象。
  • 在 Web Workers 环境中globalThis 指向 worker 全局作用域。
  • 在严格模式下:即使在模块或函数中,globalThis 依然指向全局对象,这与 this 的行为不同。
|| 和?? 区别

|| 逻辑或运算符

  • 判断标准:只要左侧是「假值(falsy)」,就返回右侧值;否则返回左侧值。
  • JavaScript 中的假值undefinednullfalse0-0NaN、空字符串 ""

?? 空值合并运算符

  • 判断标准:只有左侧是「空值(nullish)」,才返回右侧值;否则返回左侧值。
  • JavaScript 中的空值:仅 undefinednull(这是它与 || 的核心区别)。

2.2 apply

apply 的实现跟 call 类似,只是入参不一样,apply为数组,call为参数

Function.prototype.call2 = function (context, args) {
    
    const ctx = context ?? globalThis; 

    ctx.fn = this;
    const result = ctx.fn(...args);
    delete ctx.fn;
    return result;
};

2.3 bind

bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

①.this绑定 上下文

关于指定 this 的指向,我们可以使用 call 或者 apply 实现

Function.prototype.bind2 = function (context) {
    var self = this;

    return function () {
        return self.apply(context);
    }

}

②.传参 柯里化(预传部分参数 + 后续补充参数)

Function.prototype.bind2 = function (context, ...args) { 
// 剩余参数提取预传参数
  const self = this;
  // 返回的函数也用剩余参数提取“补充参数”
  return function (...bindArgs) { 
    // 合并预传参数和补充参数,传给apply(第二个参数必须是数组)
    return self.apply(context, [...args, ...bindArgs]); 
  }
}

③bind返回的函数作为构造函数(预传部分参数 + 后续补充参数)

bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效。

a.原始的bind函数

通过把" bind 返回的函数 "作为构造函数,实现柯里化, 预传部分参数 + 后续补充参数

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

// 创建一个绑定了预存参数的构造函数
const DiscountedProduct = Product.bind(null, 'Discount Item');

// 使用 new 创建实例,只需要提供价格
const product1 = new DiscountedProduct(9.99);
console.log(product1); // 输出: Product { name: 'Discount Item', price: 9.99 }
b.模拟bind,判断是否用作构造函数,并改变原型指向
Function.prototype.bind2 = function (context,...args) {

  //边界校验,确保调用者是函数,call,apply,bind都需要确保调用者是函数,否则报错 
  if (typeof this !== 'function') {
  throw new TypeError(`${this} is not a function`);
}

  var self = this;

  var fBound = function (...bindArgs) {
    //this instanceof fBound 判断fBound是否在原型链上,是不是被当做构造函数,
    //是构造函数就this,不是构造函数就绑定传入的context
    // [...args,...bindArgs] 合并:bind2 时的预传参数 + 新函数执行时的参数
    return self.apply(this instanceof fBound ? this : context, [...args,...bindArgs]);
  }
  
  //继承原型
  fBound.prototype = Object.create(this.prototype);
  //修复 fBound.prototype.constructor 指向,防止constructor被修改过
  fBound.prototype.constructor = this

  //返回函数  
  return fBound;
}

最后

这是JavaScript系列第7篇,将持续更新。

小伙伴如果喜欢我的分享,可以动动您发财的手关注下我,我会持续更新的!!!
您对我的关注、点赞和收藏,是对我最大的支持!欢迎关注、评论、讨论和指正!