Bootstrap

前端面试题:手写call、apply、bind函数

一、Function.prototype.call()

call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

说白了就是改变当前调用函数的this指向,并向调用函数中传参。

语法: function.call(thisArg, arg1, arg2, …)

call方法中也可以不传参,这时函数中的this在严格模式(use strick)下为 undefined ,非严格模式指向全局对象 window 。

// 严格模式
'use strick';
var name= 'zhangsan';
function display() {
  console.log('name is ', this.name);
}
display.call();  // Uncaught TypeError: Cannot read properties of undefined (reading 'name')
// 非严格模式
var name= 'zhangsan';
function display() {
  console.log('name is ', this.name);
}
display.call();  // name is zhangsan 

手写call方法:

原理:foo.call(obj, a, b) 相当于 obj.foo(a, b)

逻辑梳理:

  1. 接受多个参数,第一个参数为this对象
  2. 若无传参时,this默认指向window
  3. 函数中若存在返回值时,return返回值
// ES6写法
Function.prototype.myCall = function (thisArg, ...args) {
	// call必须由函数调用
	if (typeof this !== 'function') {
    	throw new Error(`${this} must be a function`)
    }
    const fn = this;
    thisArg = thisArg == null ? window : Object(thisArg);
    args = args || [];
    thisArg.fn = fn;
    const result = thisArg.fn(...args);
    delete thisArg.fn;
    return result;
}

// ES5写法
Function.prototype.myCall = function () {
	// call必须由函数调用
	if (typeof this !== 'function') {
    	throw new Error(`${this} must be a function`)
    }
    var fn = this;
    var args = [];
    for (var i = 0; i < arguments.length; i++) {
        args.push(arguments[i]);  // 将arguments类数组转化为数组
    }
    var thisArg = args.shift(); // 获取this值,thisArg = args[0]
    thisArg = thisArg == null ? window : Object(thisArg); // this为空时默认指定为window对象,否则包装为对象使用
    thisArg.fn = fn;  // 改变this指向
    var result = args.length ? eval('thisArg.fn(' + args + ')') : thisArg.fn(); // 执行函数,获取返回结果
    delete thisArg.fn; // 执行完成后删除添加的方法fn
    return result;
}

二、Function.prototype.apply()

apply() 方法调用一个具有给定 this 值的函数,以及以一个数组(或一个类数组对象)的形式提供的参数。

apply 与 call 非常相似,不同之处在于提供参数的方式。apply 使用参数数组而不是一组参数列表。

语法: apply(thisArg) | apply(thisArg, argsArray)

// call()
fn.call(this, a, b);

// apply()
fn.apply(this, [a, b])

手写apply方法:

原理:foo.apply(obj, [a, b]) 相当于 obj.foo([a, b])

逻辑梳理:

  1. 接受2个参数,第一个参数为this对象,第二个参数(可忽略)为数组或类数组
  2. 若无传参时,this默认指向window
  3. 函数中若存在返回值时,return返回值
// ES6写法
Function.prototype.myApply = function (thisArg, args) {
	// apply必须由函数调用
	if (typeof this !== 'function') {
    	throw new Error(`${this} must be a function`)
    }
	var fn = this;
    thisArg = thisArg == null ? window : Object(thisArg);
    args = Array.isArray(args) ? args : [];
    thisArg.fn = fn;
    const result = thisArg.fn(...args);
    delete thisArg.fn;
    return result;
}

// ES5写法
Function.prototype.myApply = function (thisArg, args) {
	// apply必须由函数调用
	if (typeof this !== 'function') {
    	throw new Error(`${this} must be a function`)
    }
    var fn = this;
    thisArg = thisArg == null ? window : Object(thisArg); // this为空时默认指定为window对象,否则包装为对象使用
    args = args instanceof Array ? args : []; // 判断参数是否为数组,不为数组时使用空数组
    thisArg.fn = fn;  // 切换this指向到thisArg
    const result = args.length ? eval('thisArg.fn(' + args + ')') : thisArg.fn(); // 执行函数,获取返回结果
    delete thisArg.fn;  // 执行完成后删除添加的方法fn
    return result;
}

三、Function.prototype.bind()

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

语法: function.bind(thisArg[, arg1[, arg2[, …]]])

手写bind方法:

逻辑梳理:

  1. bind() 函数会创建一个新的绑定函数,绑定函数包装了原函数对象。调用绑定函数通常会导致执行包装函数。
  2. thisArg 调用绑定函数时作为 this 参数传递给目标函数的值。
  3. 如果使用new运算符构造绑定函数,则忽略thisArg,使用new构造函数的返回值规则。
  4. 如果 bind 函数的参数列表为空,或者 thisArg 是 null 或 undefined ,执行作用域的 this 将被视为新函数的 thisArg。

使用到的知识点:

  • 闭包
  • 函数柯理化、合并参数
  • new构造函数
  • 原型、原型链
  • 改变this指向
// ES6写法
Function.prototype.myBind = function (thisArg, ...args) {
	// bind必须由函数调用
	if (typeof this !== 'function') {
    	throw new Error(`${this} must be a function`)
    }
    // this就是当前调用bind的函数
    let fn = this;
    const boundFn = function () {
        // 合并参数
        const concatArgs = [...args, ...arguments];
        // 判断是否是new调用方式
        if (this instanceof boundFn) {
            // obj构造函数的返回值,注意这里的this为实例对象
            const obj = fn.apply(this, concatArgs);
            // 判断返回值是否为对象,是对象则返回,否则返回当前实例
            if (obj instanceof Object) {
                return obj;
            } else {
                return this;
            }
        } else {
            // 不是new调用时,绑定this为传参对象thisArg
            return fn.apply(thisArg, concatArgs);
        }
    }
    if (fn.prototype) {
        // 此处使用 Object.create ,是为了防止修改 boundFn.prototype 属性时,将 fn.prototype 的属性误修改
        // Object.create 会隔离两个对象之间的修改关系,但能保持访问关系。
        // 若直接使用 boundFn.prototype = fn.prototype,则为引用传递,不安全。
        boundFn.prototype = Object.create(fn.prototype);
        boundFn.prototype.constructor = fn;  // 修正构造函数
    }
    return boundFn;
}

参考文章:
Function.prototype.bind() - JavaScript | MDN
面试让写一个“bind”函数,详解五层bind函数进阶写法,带你写出一个让面试官满意的 “bind” 函数
柯里化函数(Currying),什么是柯里化,为什么要进行柯里化,高级柯里化函数的实现
Object.create() 详解

;