面试官:手写个call?我:这就是我统治this的方式!

77 阅读3分钟

在JavaScript的世界里,函数调用的方式多种多样,而call、apply、bind这三个方法可以说是操控函数this指向的三大法宝。今天我们就来彻底搞懂它们的实现原理,并亲自动手实现一个完整的call方法。

开篇:为什么要手写这些方法?

相信很多同学在面试中都遇到过这样的问题:"请手写一个call方法"。这不仅仅是为了考察你的编码能力,更是要看你是否真正理解JavaScript中函数调用的底层机制。

先来看看这三个方法的基本用法:

var name = "Trump"
function gretting(...args){
    console.log(args, arguments[0], arguments[1]);
    return `hello , I am ${this.name}.`;
}

const bly = {
    name: '贝利亚'
}

// call - 立即执行,参数逐个传递
console.log(gretting.call(bly, 18, '北京'));

// apply - 立即执行,参数以数组形式传递
console.log(gretting.apply(bly, [18, '上海']));

// bind - 返回新函数,延迟执行
const fn = gretting.bind(bly, 18, '深圳');
setTimeout(() => {
    fn() // 1秒后执行
}, 1000)

核心原理解析

1. 函数调用的this指向规律

在JavaScript中,函数的this指向遵循以下规律:

  • 直接调用:严格模式下为undefined,非严格模式下为window
  • 对象方法调用:this指向调用该方法的对象
  • 构造函数调用:this指向新创建的实例对象

而call、apply、bind的作用就是人为地改变函数内部的this指向。

2. 实现思路

实现call方法的核心思路是:

  1. 将要调用的函数作为目标对象的方法
  2. 通过对象方法调用的方式执行函数
  3. 删除临时添加的方法,避免污染目标对象

手写call方法实现

让我们一步步实现一个完整的call方法:

Function.prototype.myCall = function(context, ...args) {
    console.log('开始执行myCall方法');
    
    // 1. 处理context为null或undefined的情况
    if (context === null || context === undefined) {
        context = window; // 非严格模式下指向window
    }
    
    // 2. 类型检查:确保调用者是函数
    if (typeof this !== 'function') {
        throw new TypeError('Function.prototype.myCall called on non-function')
    }
    
    // 3. 创建唯一属性名,避免覆盖原有属性
    const fnKey = Symbol('fn');
    
    // 4. 将当前函数挂载到context上
    context[fnKey] = this;
    console.log('挂载后的context:', context);
    
    // 5. 通过context调用函数,this自然指向context
    const result = context[fnKey](...args);
    
    // 6. 清理:删除临时属性
    delete context[fnKey];
    
    return result;
}

关键技术点解析

1. Rest运算符的妙用

function gretting(...args) {
    // args是真正的数组,而arguments是类数组对象
    console.log(args, arguments[0], arguments[1]);
    return `hello , I am ${this.name}.`;
}

这里使用了ES6的rest运算符,它比传统的arguments对象更加灵活和现代化。

2. Symbol确保属性唯一性

const fnKey = Symbol('fn');
context[fnKey] = this;

使用Symbol作为属性名是一个巧妙的设计,它确保了我们添加的临时属性不会与context上的现有属性冲突。

3. 严格模式的考量

"use strict";
var name = "Trump"

在严格模式下,当context为null或undefined时,this不会自动指向window,而是保持为null或undefined。这就是为什么我们需要显式处理这种情况。

完整测试用例

// 测试对象
var obj = {
    name: '刘老板',
    fn: function() {
        console.log('原有方法');
    }
}

// 测试函数
function gretting(...args) {
    console.log(args, arguments[0], arguments[1]);
    return `hello , I am ${this.name}.`;
}

// 执行测试
console.log(gretting.myCall(obj, 1, 2, 3));
// 输出:[1, 2, 3] 1 2
// 输出:hello , I am 刘老板.

三大方法的应用场景

call vs apply

  • call: 参数逐个传递,适用于参数数量已知且不多的情况
  • apply: 参数以数组形式传递,适用于参数数量动态或需要传递数组的情况
// 实际应用:数组最大值
const numbers = [1, 2, 3, 4, 5];
const max = Math.max.apply(null, numbers);

bind的特殊性

bind方法返回一个新函数,支持预设参数(柯里化),常用于事件处理和回调函数:

const fn = gretting.bind(bly, 18, '抚州');
setTimeout(() => {
    fn(); // 延迟执行
}, 1000);

进阶:手写apply和bind

基于call的实现,我们可以轻松实现apply:

Function.prototype.myApply = function(context, args) {
    return this.myCall(context, ...args);
}

bind的实现稍微复杂一些:

Function.prototype.myBind = function(context, ...args) {
    const fn = this;
    return function(...newArgs) {
        return fn.myCall(context, ...args, ...newArgs);
    }
}

常见面试陷阱

  1. 忘记处理context为null/undefined的情况
  2. 没有进行类型检查
  3. 直接修改context导致属性污染
  4. 不理解严格模式下的差异

总结

手写call、apply、bind不仅仅是一道面试题,更是深入理解JavaScript函数调用机制的绝佳方式。通过这个过程,我们学会了:

  • 如何巧妙地改变函数的this指向
  • Symbol在避免属性冲突中的应用
  • Rest运算符与arguments的区别
  • 严格模式对函数调用的影响
  • JavaScript对象动态性的利用

掌握了这些核心原理,不仅能让你在面试中游刃有余,更能让你在实际开发中写出更加优雅和高效的代码。

记住:理解原理比死记硬背更重要,只有真正理解了底层机制,才能在各种场景下灵活运用这些方法。