深入解析 JavaScript 中的 call、apply 和 bind:改变 this 指向的艺术

110 阅读2分钟

js11.png

一、核心概念解析

1. 函数对象的三大法宝

这三个方法都继承自 Function.prototype,是所有函数的内置方法:

function sum(a, b) {
  return a + b;
}

console.log(sum.hasOwnProperty('call'));    // false(来自原型链)
console.log('call' in sum);                 // true

2. 核心能力对比表

方法立即执行参数形式返回新函数常用场景
call参数列表明确参数个数时使用
apply数组处理动态参数时使用
bind参数列表需要延迟执行时使用

二、方法深度剖析

1. call 方法

实现原理伪代码

Function.prototype.myCall = function(context, ...args) {
  context = context || window; // 处理 null 和 undefined
  const fnKey = Symbol('tempFn');
  context[fnKey] = this; // 绑定函数到目标对象
  const result = context[fnKey](...args);
  delete context[fnKey];
  return result;
};

经典应用场景

// 1. 类数组转换
function convertToArray() {
  return [].slice.call(arguments);
}

// 2. 构造函数链式调用
function Parent(name) {
  this.name = name;
}

function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}

2. apply 方法

性能优化案例

// 更优的数组合并方式
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];

// 传统方式(产生中间数组)
arr1.concat(arr2); 

// 高性能方式
Array.prototype.push.apply(arr1, arr2);

数学计算示例

const numbers = [5, 2, 8, 4];
Math.max.apply(null, numbers); // 8

3. bind 方法

手写实现(支持 new 操作)

Function.prototype.myBind = function(context, ...bindArgs) {
  const self = this;
  
  function boundFn(...callArgs) {
    // 判断是否通过 new 调用
    const isNewCall = this instanceof boundFn;
    return self.apply(
      isNewCall ? this : (context || window),
      bindArgs.concat(callArgs)
    );
  }
  
  // 保持原型链
  boundFn.prototype = Object.create(self.prototype);
  
  return boundFn;
};

React 中的典型应用

class Button extends React.Component {
  handleClick = (e) => {
    console.log(this.props.message, e);
  }

  render() {
    return (
      <button onClick={this.handleClick.bind(this)}>
        Click me
      </button>
    );
  }
}

三、综合应用案例

1. 柯里化函数实现

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...moreArgs) {
        return curried.apply(this, args.concat(moreArgs));
      }
    }
  };
}

// 使用示例
const sum = (a, b, c) => a + b + c;
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6

2. 面试题解析

题目:以下代码输入结果?

const obj = {
  x: 42,
  getX: function() {
    return this.x;
  }
};

const unboundGetX = obj.getX;
const boundGetX = unboundGetX.bind(obj);

console.log(unboundGetX());   // undefined(非严格模式为 window.x)
console.log(boundGetX());     // 42

变种题:

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

var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };

(p.foo = o.foo)();  // 输出什么?

四、性能优化与最佳实践

1. 缓存绑定结果

// 错误示范(每次渲染都创建新函数)
element.addEventListener('click', this.handleClick.bind(this));

// 正确做法
constructor() {
  this.handleClick = this.handleClick.bind(this);
}

2. 箭头函数替代方案

// 使用类属性+箭头函数
class MyComponent {
  handleClick = () => {
    console.log(this); // 自动绑定实例
  }
}

五、常见误区警示

1. 严格模式下的差异

'use strict';
function test() {
  console.log(this); // undefined
}

test.call(null); // 正常执行,this 为 null

2. 绑定优先级

const obj1 = { name: 'Alice' };
const obj2 = { name: 'Bob' };

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

const boundFn = showName.bind(obj1);
boundFn.call(obj2); // 仍然输出 'Alice'

六、扩展知识

1. this的绑定优先级

new绑定 > 显示绑定(call,apply,bind)> 隐士绑定(对象方法)> 默认绑定

2. ES6+ 新特性替代方案

// 使用展开运算符替代 apply
Math.max(...[1, 2, 3]);

// 使用箭头函数自动绑定 this
const obj = {
  value: 42,
  getValue: function() {
    setTimeout(() => {
      console.log(this.value); // 正确获取42
    }, 100);
  }
};