面试官:你知道call、apply、bind的实现原理吗?🤔带你手搓!!!

16 阅读4分钟

前言

在JavaScript面试中,"手写call/apply/bind"几乎是必考题目。但这个问题的重要性远不止于面试——理解这些方法的实现原理,能帮助我们真正掌握JavaScript的this绑定机制,写出更优雅的代码。 今天,我们就从零开始,一步步实现这三个重要的函数方法。

一、核心原理:this绑定的本质

在实现之前,我们先理解一个关键概念:隐式绑定

const obj = {
    name: 'Alice',
    sayName() {
        console.log(this.name);
    }
};

obj.sayName(); // 输出:Alice

当函数作为对象的方法调用时,this会自动绑定到该对象。我们的目标就是手动实现这种绑定效果

二、实现Function.prototype.myCall

2.1 基础版本

我们先从最简单的版本开始:

Function.prototype.myCall = function(context, ...args) {
    // 在context上临时添加方法
    context.fn = this;
    // 通过context调用方法,实现this绑定
    const result = context.fn(...args);
    // 清理临时属性
    delete context.fn;
    return result;
};

2.2 问题与改进

基础版本有几个问题:

  1. 可能覆盖context的原有属性
  2. 未处理context为null/undefined的情况
  3. 未处理原始值参数

让我们完善它:

Function.prototype.myCall = function(context, ...args) {
    // 1. 处理context为null或undefined
    if (context == null) {
        context = typeof window !== 'undefined' ? window : globalThis;
    }
    
    // 2. 处理原始值(string、number、boolean等)
    if (typeof context !== 'object') {
        context = Object(context);
    }
    
    // 3. 使用Symbol避免属性冲突
    const fnKey = Symbol('tempFn');
    context[fnKey] = this;
    
    // 4. 执行函数
    const result = context[fnKey](...args);
    
    // 5. 清理临时属性
    delete context[fnKey];
    
    return result;
};

2.3 使用示例

function introduce(age, hobby) {
    console.log(`我是${this.name},今年${age}岁,喜欢${hobby}`);
    return `Hello, ${this.name}`;
}

const person = { name: '张三' };

// 原生call
introduce.call(person, 25, '篮球');

// 我们的myCall
introduce.myCall(person, 25, '篮球');

三、实现Function.prototype.myApply

3.1 apply与call的区别

// call接收参数列表
fn.call(obj, arg1, arg2, arg3);

// apply接收参数数组
fn.apply(obj, [arg1, arg2, arg3]);

3.2 具体实现

Function.prototype.myApply = function(context, argsArray) {
    if (context == null) {
        context = typeof window !== 'undefined' ? window : globalThis;
    }
    
    if (typeof context !== 'object') {
        context = Object(context);
    }
    
    const fnKey = Symbol('tempFn');
    context[fnKey] = this;
    
    // 处理argsArray为null、undefined或非数组的情况
    const result = context[fnKey](...(argsArray || []));
    
    delete context[fnKey];
    return result;
};

四、实现Function.prototype.myBind

4.1 bind的特殊性

bind与前两者最大的不同:

  • 不立即执行,而是返回新函数
  • 支持柯里化:参数可以分次传递
  • 需要处理new操作符

4.2 基础版本

Function.prototype.myBind = function(context, ...bindArgs) {
    const self = this;
    
    return function(...args) {
        return self.call(context, ...bindArgs, ...args);
    };
};

4.3 完整版本(支持new操作)

这是最复杂的部分,需要特殊处理通过new调用的情况:

Function.prototype.myBind = function(context, ...bindArgs) {
    const self = this;
    
    const boundFunction = function(...args) {
        // 关键判断:是否通过new调用?
        // 如果是new调用,this应该是新创建的实例
        const isNewCall = this instanceof boundFunction;
        
        return self.apply(
            isNewCall ? this : context,
            bindArgs.concat(args)
        );
    };
    
    // 维护原型关系:确保instanceof操作符正常工作
    if (self.prototype) {
        boundFunction.prototype = Object.create(self.prototype);
    }
    
    return boundFunction;
};

4.4 为什么需要处理new操作?

function Person(name) {
    this.name = name;
}

const BoundPerson = Person.myBind({});
const p1 = new BoundPerson('张三'); // 应该是Person的实例

console.log(p1 instanceof Person); // 应该是true
console.log(p1 instanceof BoundPerson); // 应该是true

如果不处理new操作,this会错误地绑定到context对象,而不是新创建的实例。

五、完整代码与测试

让我们把所有实现整合起来,并进行全面测试:

<!DOCTYPE html>
<html>
<head>
    <title>手写call、apply、bind测试</title>
</head>
<body>
    <script>
        // 完整的实现
        Function.prototype.myCall = function(context, ...args) {
            if (context == null) context = globalThis;
            if (typeof context !== 'object') context = Object(context);
            
            const fnKey = Symbol('fn');
            context[fnKey] = this;
            const result = context[fnKey](...args);
            delete context[fnKey];
            return result;
        };

        Function.prototype.myApply = function(context, argsArray) {
            if (context == null) context = globalThis;
            if (typeof context !== 'object') context = Object(context);
            
            const fnKey = Symbol('fn');
            context[fnKey] = this;
            const result = context[fnKey](...(argsArray || []));
            delete context[fnKey];
            return result;
        };

        Function.prototype.myBind = function(context, ...bindArgs) {
            const self = this;
            
            const boundFunction = function(...args) {
                const isNewCall = this instanceof boundFunction;
                return self.apply(isNewCall ? this : context, [...bindArgs, ...args]);
            };
            
            boundFunction.prototype = Object.create(self.prototype || {});
            return boundFunction;
        };

        // 测试代码
        console.log('=== 测试开始 ===');

        function demoFunction(a, b, c) {
            console.log('this:', this);
            console.log('参数:', a, b, c);
            console.log('结果:', a + b + c);
            return a + b + c;
        }

        const testObj = { name: '测试对象', value: 100 };

        // 1. 测试call
        console.log('\n--- myCall测试 ---');
        demoFunction.myCall(testObj, 1, 2, 3);

        // 2. 测试apply
        console.log('\n--- myApply测试 ---');
        demoFunction.myApply(testObj, [4, 5, 6]);

        // 3. 测试bind
        console.log('\n--- myBind测试 ---');
        const boundFn = demoFunction.myBind(testObj, 10);
        boundFn(20, 30);

        // 4. 测试new操作
        console.log('\n--- new操作测试 ---');
        function Person(name) {
            this.name = name;
        }
        const BoundPerson = Person.myBind({ other: 'value' });
        const person = new BoundPerson('李四');
        console.log('person实例:', person);
        console.log('是否是Person实例:', person instanceof Person);

        // 5. 边界测试
        console.log('\n--- 边界测试 ---');
        demoFunction.myCall(null, 1, 2, 3); // null上下文
        demoFunction.myCall(123, 1, 2, 3);  // 原始值上下文
    </script>
</body>
</html>

六、实际应用场景

6.1 函数借用(方法借用)

// 类数组对象借用数组方法
const arrayLike = { 0: 'a', 1: 'b', length: 2 };
Array.prototype.push.myCall(arrayLike, 'c');
console.log(arrayLike); // {0: 'a', 1: 'b', 2: 'c', length: 3}

6.2 高阶函数与柯里化

function multiply(a, b, c) {
    return a * b * c;
}

// 使用bind实现柯里化
const double = multiply.myBind(null, 2);
console.log(double(3, 4)); // 输出:24 (2 * 3 * 4)

6.3 事件处理器的this绑定

class Button {
    constructor() {
        this.text = '点击我';
        this.handleClick = this.handleClick.myBind(this);
    }
    
    handleClick() {
        console.log(this.text); // 正确指向Button实例
    }
}

七、面试常见问题

  1. call、apply、bind的区别是什么?

    • call:参数逐个传递,立即执行
    • apply:参数数组传递,立即执行
    • bind:返回新函数,可延迟执行
  2. 为什么bind需要特殊处理new操作?

    • 通过new调用时,this应该指向新实例,而不是绑定的context
  3. 如何避免属性命名冲突?

    • 使用Symbol创建唯一key是最佳实践
  4. 箭头函数能使用call/apply/bind吗?

    • 不能,箭头函数的this在定义时确定,无法被修改

总结

通过手动实现call、apply、bind,我们不仅掌握了面试技巧,更重要的是:

  1. 深入理解了this绑定机制
  2. 熟悉了函数式编程的基本概念
  3. 学会了处理边界情况的编程思维
  4. 提升了代码设计和调试能力