一文搞懂call,apply,bind三者区别、实现原理以及应用场景

126 阅读4分钟

call,apply,bind三者区别以及实现原理

作用

在执行函数的时候,如果需要保证函数内部this不被污染或者说需要使函数内部this指向到指定对象的时候,都会按情况分别使用到callapplybind方法来实现需求

bindcallapply的区别

  • bind不会立即调用,他会返回一个函数,函数内部的this指向与bind执行时的第一个参数,而传入bind的第二个及以后的参数作为原函数的参数来调用原函数。
  • callapply直接返回函数的返回值。

callapply的区别

  • callapply第一个参数,都是设置函数内this要指向的目标。
  • call第二个参数以及后面的参数,是数组解构后,数组的元素。
  • apply第二个参数是一个数组。

call的实现

Function.prototype.call = function(context) {
    // 基础类型转包装类型
    if(context === undefined || context === null) {
        context = window
    }else if(typeof context === 'string'){
        context = new String(context)
    }else if(typeof context === 'number'){
        context = new Number(context)
    }else if(typeof context === 'boolean'){
        context = new Boolean(context)
    }
    // 将原函数绑定到目标对象的fn属性上
    context.fn = this
    // 获取第二参数以及之后参数
    const args = [...arguments].slice(1)
    const fnValue = context.fn(args)
    // 解绑删除被绑定的函数
    delete context.fn
    // 返回结果
    return fnValue
}

基础类型转包装类型目的:

  • context.fn = this,普通类型无法绑定属性,包装类型可以
  • 符合ECMAScript规范

apply的实现

Function.prototype.apply = function(context,arr){
    if(context === undefined || context === null) {
        context = window
    }else if(typeof context === 'string'){
        context = new String(context)
    }else if(typeof context === 'number'){
        context = new Number(context)
    }else if(typeof context === 'boolean'){
        context = new Boolean(context)
    }
    if(typeof arr !== 'Object' || typeof arr !== undefined || typeof arr !== null ){
        throw new TypeError('CreateListFromArrayLike called on non-object')
    }
    arr = Array.isarray(arr) && arr || []
    context.fn = this
    const fnValue = context.fn(...arr)
    delete context.fn
    return fnValue
}

bind的实现

Function.prototype.bind = function(context){
    const ofn = this
    const args = [...arguments].slice(1)
    function O(){}
    function fn(){
        ofn.apply(this instanceof O ? this : context, args.concat(Array.from(arguments)))
    }
    //建立原型链继承关系
    O.prototype = this.prototype 
    fn.prototype = new O() 
    return fn
}

this instanceof O ? this : context这句话的作用

区分普通调用和 new 调用

1. 普通调用(非 new)
const obj = { name: "Alice" };
function greet() {
  console.log(this.name);
}

const boundGreet = greet.bind(obj);
boundGreet(); // 普通调用

在这种情况下:

  • this instanceof O 返回 false
  • 执行 ofn.apply(context, args)
  • this 指向 bind 时传入的 context(即 obj
2. new 调用
function Person(name) {
  this.name = name;
}

const BoundPerson = Person.bind({});
const instance = new BoundPerson("Bob"); // new 调用

在这种情况下:

  • this instanceof O 返回 true
  • 执行 ofn.apply(this, args)
  • this 指向新创建的实例对象(即 instance

再详细一点

当调用 new fn() 时,this 指向谁?

答案:指向新创建的实例对象

执行过程分析:
  1. new fn() 执行时

    • 创建一个新对象,其原型指向 fn.prototype
    • 将这个新对象作为 this 上下文执行 fn 函数体
  2. fn 函数体内的判断

    javascript

    ofn.apply(this instanceof O ? this : context, ...)
    
    • this instanceof O:检查 this 是否是 O 的实例

    • 由于 fn.prototype = new O(),所以通过 new fn() 创建的实例:

      • 其 [[Prototype]] 指向 new O()
      • new O() 的 [[Prototype]] 指向 O.prototype(即原函数的 prototype
    • 因此 this instanceof O 返回 true

  3. 结果

    • ofn.apply(this, ...) 执行原函数,this 指向新创建的实例
    • 保持了 new 操作符的正常行为

现代实现方法

补充一段call的现代实现方法,更简洁

Function.prototype.call = function(context) {
  // 使用 Object() 统一处理基础类型转换
  context = context ? Object(context) : window
  
  const fn = Symbol('fn')  // 使用 Symbol 避免属性冲突
  context[fn] = this
  
  const result = context[fn](...arguments)
  delete context[fn]
  
  return result
}

call,apply,bind的应用场景

1. call 的应用场景

类数组对象转数组
// 将类数组对象转换为真正的数组
function example() {
    const args = Array.prototype.call(arguments);
    // 或更现代的写法:const args = Array.from(arguments);
    return args;
}
继承中的构造函数调用
function Parent(name) {
    this.name = name;
}

function Child(name, age) {
    Parent.call(this, name);  // 继承父类属性
    this.age = age;
}

const child = new Child('小明', 10);
调用对象的方法
const obj1 = {
    name: 'Alice',
    greet: function() {
        console.log(`Hello, ${this.name}`);
    }
};

const obj2 = {
    name: 'Bob'
};

obj1.greet.call(obj2);  // "Hello, Bob"

2. apply 的应用场景

数组参数传递
// 求数组最大值
const numbers = [5, 2, 8, 1, 9];
const max = Math.max.apply(null, numbers);  // 9

// 数组合并
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
Array.prototype.push.apply(arr1, arr2);  // arr1 = [1,2,3,4,5,6]
函数柯里化

function add(a, b, c) {
    return a + b + c;
}

function partialAdd(a) {
    return function(b, c) {
        return add.apply(this, [a, b, c]);
    }
}

const add5 = partialAdd(5);
console.log(add5(3, 2));  // 10

3. bind 的应用场景

事件处理函数
class Button {
    constructor() {
        this.text = 'Click me';
        this.handleClick = this.handleClick.bind(this);  // 绑定this
    }
    
    handleClick() {
        console.log(this.text);  // 确保this指向Button实例
    }
}

const btn = new Button();
document.querySelector('button').addEventListener('click', btn.handleClick);
设置定时器
class Timer {
    constructor() {
        this.message = 'Timeout!';
    }
    
    start() {
        // 使用bind确保setTimeout中的this正确
        setTimeout(this.showMessage.bind(this), 1000);
    }
    
    showMessage() {
        console.log(this.message);
    }
}
预设参数
function multiply(a, b) {
    return a * b;
}

// 创建固定第一个参数的函数
const double = multiply.bind(null, 2);
console.log(double(5));  // 10
console.log(double(8));  // 16

// 创建固定多个参数的函数
const triplePlus = multiply.bind(null, 3, 4);
console.log(triplePlus());  // 12

实际项目中的综合应用

React 类组件中的方法绑定
class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = { count: 0 };
        
        // 方法1:在构造函数中bind
        this.handleClick = this.handleClick.bind(this);
    }
    
    // 方法2:使用箭头函数(自动绑定this)
    handleClick = () => {
        this.setState({ count: this.state.count + 1 });
    }
    
    render() {
        return <button onClick={this.handleClick}>点击</button>;
    }
}
函数借用
// 借用数组方法处理类数组对象
const arrayLike = {
    0: 'a',
    1: 'b', 
    2: 'c',
    length: 3
};

// 使用数组的forEach方法
Array.prototype.forEach.call(arrayLike, item => {
    console.log(item);  // a, b, c
});

// 使用数组的map方法
const result = Array.prototype.map.call(arrayLike, item => item.toUpperCase());
console.log(result);  // ['A', 'B', 'C']
性能优化场景
// 在循环中避免重复创建函数
function processItems(items, processor) {
    const boundProcessor = processor.bind(null, 'prefix');
    
    for (let i = 0; i < items.length; i++) {
        boundProcessor(items[i]);  // 只绑定一次,重复使用
    }
}