call() 方法中的上下文如何工作

How context in the call() method works

对于重新创建 Underscore.js 的一系列练习,我试图了解 call() 方法在幕后是如何工作的。

我确实理解 call() 方法在下面的示例中是如何工作的。

let person = {
    firstName:"John",
    lastName: "Doe",
    fullName: function() {
        return this.firstName + " " + this.lastName;
    }
}
let myObject = {
    firstName:"Mary",
    lastName: "Doe",
}
person.fullName.call(myObject);  // Will return "Mary Doe"

但是,我很难理解 iteratee.call(context, collection[i], i, collection) 中上下文的概念。

这是我正在做的练习:

// _.each(collection, iteratee, [context])
// Iterates over a collection of elements (i.e. array or object),
// yielding each in turn to an iteratee function, that is called with three arguments:
// (element, index|key, collection;), and bound to the context if one is passed.
// Returns the collection for chaining.

_.each = function (collection, iteratee, context) {
  if (Array.isArray(collection)) {
    for (let i = 0; i < collection.length; i++) {
      iteratee.call(context, collection[i], i, collection);
    }
  } else if (collection !== null) {
    Object.entries(collection).map(([key, value]) => {
      iteratee.call(context, value, key, collection);
    });
  }

  return collection;
};

在此先感谢您的帮助。

很多年前,通常使用“上下文”一词来指代 this 在函数调用期间应该具有的值。 (这不是一个好术语,已经失宠了。)_.each 定义正在做的是接受一个可选参数 context,并使用它来设置调用时 this 是什么传入的 iteratee 函数使得 iteratee 调用中的 thiscontext 的任何内容。 (如果未提供 context,它将是 undefined,并且 iteratee 中的 this 将是 undefined [在严格模式下] 或全局对象[松散模式].)

例如,假设您有一个带有方法的对象,并且您想要使用 _.each 为数组中的每个条目调用该方法。你可以这样做:

const obj = {
    id: "some ID",
    method(value) {
        console.log(`this.id = ${this.id}, value = ${value}`);
    }
};
_.each([1, 2, 3], obj.method, obj);

实例:

"use strict";

const _ = {};
_.each = function (collection, iteratee, context) {
  if (Array.isArray(collection)) {
    for (let i = 0; i < collection.length; i++) {
      iteratee.call(context, collection[i], i, collection);
    }
  } else if (collection !== null) {
    Object.entries(collection).map(([key, value]) => {
      iteratee.call(context, value, key, collection);
    });
  }

  return collection;
};


const obj = {
    id: "some ID",
    method(value) {
        console.log(`this.id = ${this.id}, value = ${value}`);
    }
};
_.each([1, 2, 3], obj.method, obj);

如果您没有提供 obj 作为第三个参数,obj.method 中的 this.id 将无法正常工作,因为 this 不会是 obj,它将是 undefined(严格模式)或全局对象。例如(严格模式):

"use strict";

const _ = {};
_.each = function (collection, iteratee, context) {
  if (Array.isArray(collection)) {
    for (let i = 0; i < collection.length; i++) {
      iteratee.call(context, collection[i], i, collection);
    }
  } else if (collection !== null) {
    Object.entries(collection).map(([key, value]) => {
      iteratee.call(context, value, key, collection);
    });
  }

  return collection;
};


const obj = {
    id: "some ID",
    method(value) {
        console.log(`this.id = ${this.id}, value = ${value}`);
    }
};
_.each([1, 2, 3], obj.method); // <== Note no third argument

内置数组方法 forEach 也有此参数(称为 thisArg),许多其他内置数组方法也是如此。

但是现在,在我看来,使用包装箭头函数更常见:

_.each([1, 2, 3], value => obj.method(value));

你可以把this看成是所有函数的隐式参数。为了说明,以下函数将抛出错误,因为没有声明变量 banana:

function fruit() {
    console.log(banana);
}

fruit();

但下面的函数没问题,因为 this 是一个隐式声明的参数:

'use strict';

function fruit() {
    console.log(this);
}

fruit();

您不能将 this 作为参数直接传递,因为那样您将不得不重新声明名称:

function broken(this) {
    console.log(this.name);
}

broken({name: 'Ann'});

this 参数传递给函数的唯一有效方法是将其作为方法调用,或者使用 callapply:

// the following lines are equivalent
anObject.aMethod(argument1, argument2);
aMethod.call(anObject, argument1, argument2);
aMethod.apply(anObject, [argument1, argument2]); 

在所有情况下,aMethod 都会将 anObject 视为其 this 参数。但是,第一种情况仅在 aMethodanObject 的 属性 时有效,而其他情况始终有效。 callapply 之间的唯一区别是后者在单个数组中接受 this 之后的所有参数。

现在开始让您感到困惑的那一行:

iteratee.call(context, collection[i], i, collection)

iteratee 只是任何函数,context 只是一个变量,将作为其隐式 this 参数提供。 context 变量可以有任何其他名称,例如 thisArgobject.