在JavaScript中绑定已绑定函数的更多参数

33

我试着整理一下关于 JavaScript 中 bind() 方法的工作原理的思路。

我发现如果我执行

var f = function (a) { ... }
var g = f.bind(obj);
g(1)

那么当使用obj作为this1作为a来调用f函数。

我的想法是g是一个包装了f函数的函数。

但当我执行以下操作时:

var f = function (a) { ... }
var g = f.bind(obj);
g.call(1)

然后当用1作为this并且a未定义时,称f被调用。

因此看起来g不仅仅是一个简单的包装器,而是call在正常和绑定函数之间有所区别。

还有一件事是我不能对函数进行部分应用多次。

var f = function (a) { ... }
var g = f.bind(obj);
var h = g.bind(1);
h();

那么将使用obj作为this,并且a为未定义时,将调用函数f。

这种行为的原因是什么?

编辑

这个问题中的结构实际上是错误的,请参考被接受的答案中它们应该是什么样的(一般来说我没有注意到callbind总是需要第一个参数作为上下文参数)。

3个回答

53

一旦你使用bind将一个对象绑定到函数上,就无法覆盖它。正如你可以在MDN文档中看到的那样,这在规范中已经明确写明:

“bind()函数创建一个新函数(一个被绑定的函数),其具有与调用它的函数(被绑定函数的目标函数)相同的函数主体(在ECMAScript 5术语中为内部调用属性),将this值绑定到bind()的第一个参数上,而这个值是不可被覆盖的。”

也就是说,即使你执行以下操作:

g.call(1);
你将会在遵循规范的浏览器中获得obj作为this,而不是1
当然,你可以绑定多个参数,例如:
var sum = function(a, b, c) { return a + b + c };
var sumAB = sum.bind(null, 1, 5);
var sumC = sumAB.bind(null, 2);

console.log(sumC());

然而,上下文对象将始终是使用第一个bind指定的对象,因为它无法被覆盖。

仅为避免混淆,call的第一个参数是上下文对象(this),然后才是其余的参数。

这意味着:

var obj = { foo: function(bar) { console.log(bar) } };

obj.foo('hello');

// equivalent to:
var foo = obj.foo;

foo.call(obj, 'hello');

希望这有所帮助。


谢谢,这似乎有点帮助。我的第一个想法是我可以想象每个函数都有this作为它的第零个参数,然后在bindcallapply的调用中部分地提供它的参数。那绝对是不正确的。但我看到如果我绑定了this,然后想使用call调用绑定的函数,我只需要在call中提供一些foo值作为第一个参数。这很好。但我仍然不能接受你的答案,因为它没有解释为什么多个绑定不像部分应用程序那样行为。 - Martin Pecka
1
我提到了为什么多个绑定不像部分应用一样行为。你可以多次绑定多个参数,完全像部分应用,但是上下文对象只能绑定一次,一旦绑定就无法覆盖它,因为这是由我链接的规范定义的:_"bind()函数创建一个新函数(绑定函数),其函数体(在ECMAScript 5术语中的内部调用属性)与被调用的函数(绑定函数的目标函数)相同,并将this值绑定到bind()的第一个参数上,无法覆盖。"_ - ZER0
你说得对,我的小测试中出现了错误。所以部分应用实际上是有效的,但在后续调用中必须使用 foo 作为第一个参数调用 bind。这就是我想知道的 :) 从你的回答中并不清楚它也适用于进一步参数的部分应用,对我来说似乎你只是在谈论上下文参数。谢谢 ;) - Martin Pecka
我更新了答案,希望现在更清晰了。但是,我建议您编写自己的“局部”函数来应用部分应用程序,而不要使用“bind”。这正是因为“bind”会更改上下文对象,而这并不总是您想要进行部分应用程序的事情。 - ZER0
2
MDN的文档已经更改,您的引用不再存在。 - XY Li
谢谢留言!这是由于“异类函数对象”术语的引入;但行为仍然相同。我认为旧引用比实际描述更清晰,因为你无法覆盖this的事实更加微妙。 - ZER0

3
您从未传递任何参数 - 您只是设置了上下文。 `call`的第一个参数被视为上下文(即`this`),第2个及以后的参数被视为所调用函数的第1个及以后的参数。同时,`bind`创建一个具有新上下文的新函数 - 在调用时传递参数。以下是在第一个代码块之后将`1`作为函数`f`的参数`a`传递的方法:
f( 1 );
g( 1 );
g.call( this, 1 );
g.apply( this, [ 1 ] );

3

Function.prototype.call()

使用call()方法可以编写一个可用于不同对象的方法。换句话说,使用call(),一个对象可以使用属于另一个对象的方法。更多信息

const person = {
  fullName: function() {
    return this.firstName + " " + this.lastName;
  }
}
const person1 = {
  firstName:"John",
  lastName: "Doe"
}
const person2 = {
  firstName:"Mary",
  lastName: "Doe"
}

// This will return "John Doe":
console.log(person.fullName.call(person1));

call()函数允许将一个对象中的函数/方法分配并调用到另一个对象中。

使用call()可以为函数/方法提供一个新的this值。使用call(),你可以编写一个方法,然后在另一个对象中继承它,而不必为新对象重写该方法。

> 使用call链接对象的构造函数

你可以使用call()来链接对象的构造函数(类似于Java)。

以下示例中,Product对象的构造函数被定义为具有两个参数:name和price。

Food和Toy两个函数调用Product,并传递this,name和price。Product初始化属性name和price,这两个特定的函数定义了category。

function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call(this, name, price);
  this.category = 'food';
}

function Toy(name, price) {
  Product.call(this, name, price);
  this.category = 'toy';
}

const cheese = new Food('feta', 5);
const fun = new Toy('robot', 40);
console.log(cheese);
console.log(fun);

>使用call调用匿名函数

在这个例子中,我们创建一个匿名函数,并使用call在数组中的每个对象上调用它。

这里匿名函数的主要目的是为每个对象添加一个打印函数,该函数能够打印对象在数组中的正确索引。

const animals = [
  { species: 'Lion', name: 'King' },
  { species: 'Whale', name: 'Fail' }
];

for (let i = 0; i < animals.length; i++) {
  (function(i) {
    this.print = function() {
      console.log('#' + i + ' ' + this.species
                  + ': ' + this.name);
    }
    this.print();
  }).call(animals[i], i);
}

> 使用call方法调用函数并指定'this'的上下文

在下面的例子中,当我们调用greet函数时,this的值将被绑定到obj对象。

function greet() {
  const reply = [this.animal, 'typically sleep between', this.sleepDuration].join(' ');
  console.log(reply);
}

const obj = {
  animal: 'cats', sleepDuration: '12 and 16 hours'
};

greet.call(obj);  // cats typically sleep between 12 and 16 hours

> 使用 call 来调用函数,且没有指定第一个参数

在下面的示例中,我们调用 display 函数时没有传递第一个参数。如果没有传递第一个参数,this 的值将绑定到全局对象。

var sData = 'Wisen';

function display() {
  console.log('sData value is %s ', this.sData);
}

display.call();  // sData value is Wisen

注意:在严格模式下,this的值将为undefined。请看下文。

'use strict';

var sData = 'Wisen';

function display() {
  console.log('sData value is %s ', this.sData);
}

display.call(); // Cannot read the property of 'sData' of undefined

Function.prototype.bind()

bind() 方法创建一个新的函数,在调用时将其this关键字设置为提供的值,并在调用新函数时提供任何提供的一系列参数之前。

如需了解更多,请访问参考文档

const module = {
  x: 42,
  getX: function() {
    return this.x;
  }
};

const unboundGetX = module.getX;
console.log(unboundGetX()); // The function gets invoked at the global scope
// expected output: undefined

const boundGetX = unboundGetX.bind(module);
console.log(boundGetX());
// expected output: 42

> 创建一个绑定函数

bind() 最简单的用法是创建一个函数,无论如何调用它,都会使用特定的 this 值。

新的 JavaScript 程序员常犯的一个错误是从对象中提取一个方法,然后稍后调用该函数,并期望它将原始对象作为其 this(例如,在基于回调的代码中使用该方法)。

然而,通常情况下,原始对象会被忽略。通过从该函数创建一个绑定函数并使用原始对象可以很好地解决这个问题:

this.x = 9;    // 'this' refers to global 'window' object here in a browser
const module = {
  x: 81,
  getX: function() { return this.x; }
};

module.getX();
//  returns 81

const retrieveX = module.getX;
retrieveX();
//  returns 9; the function gets invoked at the global scope

//  Create a new function with 'this' bound to module
//  New programmers might confuse the
//  global variable 'x' with module's property 'x'
const boundGetX = retrieveX.bind(module);
console.log(boundGetX());
//  returns 81

> 部分应用函数

bind() 的下一个最简单的用途是创建一个带有预定义初始参数的函数。

这些参数(如果有)跟随所提供的this值,然后被插入到传递给目标函数的参数开头,接着是在调用绑定函数时传递的任何参数。

function list() {
  return Array.prototype.slice.call(arguments);
}

function addArguments(arg1, arg2) {
  return arg1 + arg2;
}

const list1 = list(1, 2, 3);
//  [1, 2, 3]

const result1 = addArguments(1, 2);
//  3

// Create a function with a preset leading argument
const leadingThirtysevenList = list.bind(null, 37);

// Create a function with a preset first argument.
const addThirtySeven = addArguments.bind(null, 37);

const list2 = leadingThirtysevenList();
//  [37]

const list3 = leadingThirtysevenList(1, 2, 3);
//  [37, 1, 2, 3]

const result2 = addThirtySeven(5);
//  37 + 5 = 42

const result3 = addThirtySeven(5, 10);
//  37 + 5 = 42
//  (the second argument is ignored)

> 使用setTimeout()

默认情况下,在setTimeout()内部,this关键字会被设置为window(或global)对象。当使用需要将this指向类实例的类方法时,可以显式地将this绑定到回调函数中,以维护实例。

function LateBloomer() {
  this.petalCount = Math.floor(Math.random() * 12) + 1;
}

// Declare bloom after a delay of 1 second
LateBloomer.prototype.bloom = function() {
  window.setTimeout(this.declare.bind(this), 1000);
};

LateBloomer.prototype.declare = function() {
  console.log(`I am a beautiful flower with ${this.petalCount} petals!`);
};

const flower = new LateBloomer();
flower.bloom();
//  after 1 second, calls 'flower.declare()'

如果你想了解更多关于bind方法的内容,请阅读这个资源


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接