【前端面试小册】JS-第21节:实现 call、apply、bind

一、call

1.1 概览

语法func.call(context, arg1, arg2, ...)

作用

  • 接受多个参数,第一个参数 context,让调用函数的 this 指向 context
  • 其余参数传递给函数调用

1.2 call 使用

使用 call

let obj = {
    name: '愚公上岸说'
}

function demo(age) {
    console.log(this.name);  // 愚公上岸说
    console.log(age);         // 22
}
demo.call(obj, 22);

未使用 call

var name = '巴菲特'

function demo(age) {
    console.log(this.name);  // 巴菲特
    console.log(age);        // 22
}

demo(22)

// 没使用 call,this 默认指向 window
// this.name 等价于 window.name
// 注意 var 声明的全局变量才会挂在 window 上
// let 声明挂在 script 上

1.3 call 实现

实现思路

  1. 改变调用 call 的目标函数的 this 指向,并将其指向 call 的第一个参数
  2. 执行调用 call 的目标函数
  3. 删除临时属性,返回函数执行结果

实现代码

Function.prototype.myCall = function (context, ...args) {
    // 处理 context 为 null 或 undefined 的情况
    if (context === null || context === undefined) {
        // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为 window)
        context = window;
    } else {
        // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
        context = Object(context);
    }

    // 利用 Symbol 唯一性,防止参数被覆盖
    const temp = Symbol('temp');

    // 函数的 this 指向隐式绑定到 context 上
    context[temp] = this;

    // 传参并执行函数
    let result = context[temp](...args);
    
    // 删除临时属性
    delete context[temp];
    
    return result;
};

关键点解析

1. context 处理

if (context === null || context === undefined) {
    context = window;
} else {
    context = Object(context);
}

原因

  • nullundefined 需要指向全局对象
  • 原始值需要转换为对象(如 Object(1) 返回 Number 对象)

2. Symbol 的使用

const temp = Symbol('temp');
context[temp] = this;

原因

  • 使用 Symbol 确保属性名唯一,不会覆盖 context 上的原有属性
  • 避免与 context 上的属性名冲突

3. 执行函数

let result = context[temp](...args);

原理

  • 将函数作为 context 的方法调用
  • 这样函数内部的 this 就指向 context

二、apply

2.1 概览

语法func.apply(context, [argsArray])

作用

  • 第一个参数 context,让调用函数的 this 指向 context
  • 第二个参数:数组或者类数组

注意:与 call 的参数类型区别,apply 的第二个参数是数组或类数组。

2.2 apply 使用

使用 apply

let obj = {
    name: '愚公上岸说'
}

function demo(a, b) {
    console.log(this.name);  // 愚公上岸说
    console.log(a, b);        // 1, 2
}
demo.apply(obj, [1, 2]);

未使用 apply

let obj = {
    name: '愚公上岸说'
}

var name = '巴菲特'

function demo(a, b) {
    console.log(this.name);  // 巴菲特
    console.log(a, b);       // 1, 2
}
demo.apply(null, [1, 2]);

// 没使用 apply,this 默认指向 window
// this.name 等价于 window.name
// 注意 var 声明的全局变量才会挂在 window 上
// let 声明挂在 script 上
// 注意是在浏览器环境

2.3 类数组复习

类数组的特点

  • 拥有 length 属性
  • 其它属性(索引)为非负整数(对象中的索引会被当做字符串来处理)
  • 不具有数组所具有的方法

类数组判断函数

function isLikeArray(o) {
    if (typeof o === 'object' && 
        isFinite(o.length) && 
        o.length >= 0 && 
        o.length < 4294967296) {
        // 4294967296: 2^32
        // length 属性的值是一个 0 到 2^32-1 的整数
        return true;
    }
    return false;
}

2.4 apply 完整实现

// 类数组判断
function isLikeArray(o) {
    if (typeof o === 'object' && 
        isFinite(o.length) && 
        o.length >= 0 && 
        o.length < 4294967296) {
        return true;
    }
    return false;
}

// 实现
Function.prototype.myApply = function (context, args) {
    // 处理 context
    if (context === null || context === undefined) {
        context = window;
    } else {
        context = Object(context);
    }

    // 利用 Symbol 唯一性,防止参数被覆盖
    const temp = Symbol('temp');

    // 函数的 this 指向隐式绑定到 context 上
    context[temp] = this;

    let result;
    
    if (args) {
        // 判断参数是否数组或类数组
        if (!Array.isArray(args) && !isLikeArray(args)) {
            throw new TypeError('第二个参数必须是: 数组或类数组');
        } else {
            args = Array.from(args);  // 转为数组
            result = context[temp](...args);  // 执行函数并展开数组,传递函数参数
        }
    } else {
        result = context[temp]();  // 执行函数
    }

    delete context[temp];
    return result;
};

测试用例

function demo(a, b) {
    console.log(this.name);  // 愚公上岸说
    console.log(a, b);        // 1, 2
}

let obj = {
    name: '愚公上岸说'
}

demo.myApply(obj, [1, 2]);

三、bind

3.1 概览

语法fn.bind(context, arg1, arg2, ...)

作用

  • bind() 方法创建一个新的函数
  • bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数
  • 而其余参数将作为新函数的参数,供调用时使用

3.2 bind 使用

function demo(a, b, c) {
    console.log(this.name);  // 愚公上岸说
    console.log(a);          // 1
    console.log(b);          // 2
    console.log(c);          // 3
}

let obj = {
    name: '愚公上岸说'
}

let fn = demo.bind(obj, 1, 2);
fn(3);

3.3 bind 实现

Function.prototype.myBind = function (context, ...args) {
    // 存储源函数以及上方的 args(函数参数)
    const _this = this;

    // 对返回的函数 args1 二次传参
    let fn = function (...args1) {
        // this 是否是 fn 的实例,判断返回的 fn 是否通过 new 调用
        const isUseByNew = this instanceof fn;

        // fn 作为构造函数调用时,_this 指向 fn 的实例,而 fn 的 this 也指向 fn 实例,所以 _this 指向 this 即可
        // fn 作为普通函数调用时,_this 指向 context,这里 Object(context) 原因同上面 apply call
        const _context = isUseByNew ? this : Object(context);

        // 用 call 调用源函数绑定 this 的指向并传递参数,返回执行结果
        return _this.call(_context, ...args, ...args1);
    };

    // 复制源函数的 prototype 给 fn
    // 一些情况下函数没有 prototype,比如箭头函数
    if (_this.prototype) {
        fn.prototype = Object.create(_this.prototype);
    }
    
    return fn;
};

3.4 关键点解析

3.4.1 new 调用的处理

const isUseByNew = this instanceof fn;
const _context = isUseByNew ? this : Object(context);

原因

  • 当使用 new 调用时,this 指向新创建的实例
  • 此时应该使用实例作为 this,而不是 context

3.4.2 参数合并

return _this.call(_context, ...args, ...args1);

原理

  • argsbind 时传入的参数
  • args1 是调用返回函数时传入的参数
  • 使用展开运算符合并参数

3.4.3 prototype 复制

if (_this.prototype) {
    fn.prototype = Object.create(_this.prototype);
}

原因

  • 保持原型链关系
  • 使用 Object.create 避免直接引用

3.5 测试用例

Demo 1:new 调用

function demo(a, b, c) {
    console.log(this.name);  // 成都
    console.log(a);          // 1
    console.log(b);          // 2
    console.log(c);         // 3
}

let obj = {
    name: '愚公上岸说'
}

const fn = demo.myBind(obj, 1, 2);
fn.prototype.name = '成都';
new fn(3);

Demo 2:普通调用

function demo(a, b, c) {
    console.log(this.name);  // 愚公上岸说
    console.log(a);          // 1
    console.log(b);          // 2
    console.log(c);          // 3
}

let obj = {
    name: '愚公上岸说'
}

const fn = demo.myBind(obj, 1, 2);
fn(3);

四、总结:call、apply、bind 区别

4.1 call 与 apply

唯一区别:传参写法不同

  • call:从第 2 个以后的参数,都是传给 call 的调用函数
  • apply:传给它的调用函数的参数就是第二个参数(数组或类数组)
func.call(context, arg1, arg2, arg3);
func.apply(context, [arg1, arg2, arg3]);

4.2 call、apply、bind 区别

特性 call apply bind
执行时机 立即执行 立即执行 返回新函数
参数形式 参数列表 数组或类数组 参数列表
返回值 函数执行结果 函数执行结果 新函数

核心区别

  • call、apply:改变调用函数的 this 指向后,会立马执行他的调用函数
  • bind:改变 this 指向,生成一个新的函数,而不会立马执行他的调用函数

4.3 使用场景

call 的使用场景

// 1. 类数组转数组
const arrayLike = { 0: 'a', 1: 'b', length: 2 };
const arr = Array.prototype.slice.call(arrayLike);

// 2. 获取数组最大值
const numbers = [5, 6, 2, 3, 7];
const max = Math.max.call(null, ...numbers);

apply 的使用场景

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

// 2. 获取数组最大值
const numbers = [5, 6, 2, 3, 7];
const max = Math.max.apply(null, numbers);

bind 的使用场景

// 1. 函数柯里化
function add(a, b) {
    return a + b;
}
const add5 = add.bind(null, 5);
console.log(add5(3));  // 8

// 2. 事件处理
class Button {
    constructor() {
        this.clickCount = 0;
        this.handleClick = this.handleClick.bind(this);
    }
    handleClick() {
        this.clickCount++;
    }
}

五、面试要点总结

核心知识点

  1. call:立即执行,参数列表形式
  2. apply:立即执行,数组或类数组形式
  3. bind:返回新函数,不立即执行
  4. 实现原理:通过将函数作为对象方法调用来改变 this 指向

常见面试题

Q1: call、apply、bind 的区别?

答:

  • callapply 立即执行,区别在于参数形式(列表 vs 数组)
  • bind 返回新函数,不立即执行
  • 都用于改变函数的 this 指向

Q2: 如何实现 call?

答:将函数作为 context 的方法调用,使用 Symbol 避免属性冲突,执行后删除临时属性。

Q3: bind 如何处理 new 调用?

答:判断是否使用 new 调用,如果是,使用实例作为 this;否则使用 context

实战建议

  • ✅ 理解三者的区别和使用场景
  • ✅ 掌握实现原理,能够手写实现
  • ✅ 注意 new 调用时的处理
  • ✅ 理解 Symbol 在实现中的作用
#前端面试##前端#
前端面试小册 文章被收录于专栏

每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!

全部评论

相关推荐

11-13 14:37
门头沟学院 Java
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务