手写call、bind、apply函数

117 阅读6分钟

前情提要:为什么要改变this指向

?改变函数的执行上下文(即改变this指向)的主要原因有以下几点:

  1. 访问外部对象:在某些情况下,我们可能需要在函数内部访问外部对象的属性或方法。通过改变this指向,我们可以在函数内部使用外部对象的上下文,并且可以访问和操作外部对象的属性和方法。
  2. 避免作用域问题:在JavaScript中,函数内部的this指向是基于函数被调用的上下文决定的。但是在一些特殊的情况下,函数内部的this指向可能不是我们期望的对象。通过改变this指向,我们可以确保函数内部的this指向我们想要的对象,避免作用域问题。
  3. 实现函数复用:通过改变函数的执行上下文,我们可以将同一个函数应用到不同的对象上,实现函数的复用。这样可以减少代码的重复性,提高代码的可维护性和可扩展性。
  4. 回调函数中的上下文:在使用回调函数时,有时我们希望回调函数中的this指向特定的对象。通过改变回调函数中的this指向,我们可以确保回调函数中的this指向我们期望的对象,从而正确地处理回调函数。

总的来说,改变函数的执行上下文可以帮助我们在函数内部访问外部对象、避免作用域问题、实现函数复用和处理回调函数中的上下文。这样可以提高代码的灵活性和可读性,并且可以更好地组织和管理代码。

正片预告:this指向(调用方式)

  1. 通过new调用
    • new method(),指向新对象
  2. 直接调用
    • method(),指向全局对象,浏览器里是window,nodejs里面是Global
  3. 通过对象调用
    • obj.method(),指向前面的对象
  4. call、apply、bind
    • method.call(ctx),指向第一个参数

在这里讨论的函数指向指的是一个函数的this指向,如果this不在函数里面,那就看环境了,在浏览器环境里指向全局对象window,在node环境里边指向一个空对象。

执行上下文:创建执行上下文的时候就确定了这一次函数调用他的this指向谁,执行上下文是执行的时候创建的,执行就是调用。

闭包->词法作用域->编译
箭头函数里使用this的时候,由于箭头函数本身没有this,那么基于闭包它会去外层去找这个this,而由于这个闭包属于词法作用域的,而词法作用域是在编译时态就确定了,因此它不用等到这个函数运行的时候,它在编译的时候就能确定了。这就是为什么箭头函数的this指向谁取决于这个箭头函数定义的位置而不是运行的位置。

正片介绍:call、apply和bind语法及区别

  1. 语法:

    • 函数名字.bind(对象,参数1,参数2,...)
    • 函数名字.call(对象,参数1,参数2,...)
    • 函数名字.apply(对象,[参数1,参数2,...])
  2. 区别如下:

    • call和apply方法都是立即调用函数,而bind方法返回一个新的函数。
    • call和apply方法都用于改变函数的执行上下文(即函数内部的this值),并立即调用该函数。它们接受的第一个参数是要绑定给this的值,后续参数是函数的参数列表。区别在于call方法接受的参数是单个的逐个传递,而apply方法接受的参数是一个数组或类数组对象。
    • bind方法也用于改变函数的执行上下文,但是它返回一个新的函数而不是立即调用。它的第一个参数是要绑定给this的值,后续参数是函数的参数列表。返回的新函数可以在稍后的时间点调用,并且绑定的this值不会改变

正片之手写call、bind、apply

// 测试demo
function fn(a,b,c,d){
    console.log(a,b,c,d);
    console.log(this);
}

const newFn=fn.bind('ctx',1,2)
// 直接调用
newFn(3,4);// 1,2,3,4 'ctx'

const result = new newFn(3, 4);
// new执行
console.log(result);// 依次打印 1,2,3,4 fn {} fn{}
  1. 手写call函数
    • 函数名字.bind(对象,参数1,参数2,...)
Function.prototype.myBind = function (ctx) {
    var args = Array.prototype.slice.call(arguments, 1);
    // console.log(arguments,args,this);
    //[Arguments] { '0': 'ctx', '1': 1, '2': 2 } [ 1, 2 ] [Function: fn]
    var fn = this;
    return function A() {
        var restArgs = Array.prototype.slice.call(arguments);
        var allArgs = args.concat(restArgs);
        // console.log(allArgs);// [ 1, 2, 3, 4 ]
        // 判断是否使用new的方式调用这个函数
        if (Object.getPrototypeOf(this) === A.prototype) {
            // ES6展开运算符
            return new fn(...allArgs)

            // 手写new
            // var obj = {};
            // Object.setPrototypeOf(obj, fn.prototype);
            // fn.apply(obj, allArgs);
            // return obj;
        } else {
            return fn.apply(ctx, allArgs);
        }
   };
};

  1. 手写call函数
    • 函数名字.call(对象,参数1,参数2,...)
Function.prototype.myCall = function (ctx, ...args) {
    // 参数归一化
    ctx = ctx === undefined || ctx === null ? globalThis : Object(ctx);
    
    // ctx一定是对象
    const fn = this; // 待执行的函数

    // Symbol作为属性名
    const key = Symbol("temp");

    // 打印结果出现[String: 'aa'] { [Symbol(temp)]: [Function: method] } 2 3,所以采用Object.defineProperty
    // ctx[key] = fn;

    Object.defineProperty(ctx, key, {
        enumerable: false, // 控制属性是否可以枚举
        value: fn,
    });

    const result = ctx[key](...args);
    delete ctx[key];// 删除临时增加的属性
    return result;
};

  1. 手写apply函数
    • 函数名字.apply(对象,[参数1,参数2,...])
    • 语法与call()几乎相同,但根本区别在于,call() 接受一个参数列表,而 apply() 接受一个参数的单数组。
Function.prototype.myApply = function (ctx, args) {
    // 参数归一化
    ctx = ctx === undefined || ctx === null ? globalThis : Object(ctx);
    
    // ctx一定是对象
    const fn = this; // 待执行的函数

    // Symbol定义对象的唯一属性名
    const key = Symbol("temp");

    // 打印结果出现[String: 'aa'] { [Symbol(temp)]: [Function: method] } 2 3,所以采用Object.defineProperty
    // ctx[key] = fn;

    Object.defineProperty(ctx, key, {
        enumerable: false, // 控制属性是否可以枚举
        value: fn,
    });

    const result = ctx[key](...args);
    delete ctx[key];// 删除临时增加的属性
    return result;
}

总结补充

  • Array.prototype.slice.call(arguments)理解 www.cnblogs.com/wphl-27/p/1…]

  • new运算的过程(https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/new)

    1. 创建一个空对象;
    2. 该空对象的原型指向构造函数(链接原型):将构造函数的 prototype 赋值给对象的 __proto__属性;
    3. 绑定 this:将对象作为构造函数的 this 传进去,并执行该构造函数;
    4. 返回新对象:如果构造函数返回的是一个对象,则返回该对象;否则(若没有返回值或者返回基本类型),返回第一步中新创建的对象;
function new(fn,...args){
    const newObj={};
    newObj.__proto__=fn.prototype;
    fn.apply(newObj,...args);
    return newObj;
}