什么是bind()?
bind()
是 JavaScript 中函数对象的一个方法,它用于创建一个新的函数,并指定这个函数的执行上下文(即 this
值),同时可以预设一部分参数。
bind()
方法的语法如下:
js
function.bind(thisArg[, arg1[, arg2[, ...]]])
其中:
function
是要绑定执行上下文的函数。thisArg
是函数执行时的上下文对象,在新函数被调用时作为this
的值。arg1, arg2, ...
是要预设的参数列表。
bind()
方法返回一个新函数,该新函数的 this
值被永久绑定到了 thisArg
,并且在调用时会将预设的参数与新传入的参数合并。
以下是一个简单的示例,演示了如何使用 bind()
方法:
js
const person = {
name: 'Alice',
greet: function(greeting) {
console.log(`${greeting}, ${this.name}!`);
}
};
const greetHello = person.greet.bind(person, 'Hello');
greetHello(); // 输出:Hello, Alice!
const greetGoodbye = person.greet.bind(person, 'Goodbye');
greetGoodbye(); // 输出:Goodbye, Alice!
在这个示例中,我们首先创建了一个 person
对象,该对象有一个 greet
方法用于打招呼。然后使用 bind()
方法创建了两个新函数 greetHello
和 greetGoodbye
,分别预设了不同的问候语。这两个新函数的 this
值被永久绑定到了 person
对象,并且在调用时会将预设的问候语与新传入的参数合并。
函数的 this
值取决于函数被调用的方式,而不是函数的定义方式。在这个例子中,如果不使用 bind()
方法,而是直接调用 person.greet('Hello')
和 person.greet('Goodbye')
,那么函数 greet
内部的 this
将会指向 person
对象,因为这是通过 person
对象来调用的。
js
const person = {
name: 'Alice',
greet: function(greeting) {
console.log(`${greeting}, ${this.name}!`);
}
};
// 直接调用 greet 方法,此时 this 指向 person 对象
person.greet('Hello'); // 输出:Hello, Alice!
person.greet('Goodbye'); // 输出:Goodbye, Alice!
这里 person.greet('Hello')
和 person.greet('Goodbye')
调用方式会将 this
绑定到 person
对象,因此 this.name
将会打印 Alice
。
既然如此,为何还要使用person.greet.bind(person, 'Hello')这种形式?
在这个特定示例中,确实可以看出似乎使用 bind()
方法略显多余。在这个例子中,直接调用 person.greet('Hello')
和 person.greet('Goodbye')
会产生相同的结果。但是,这里的重点在于演示 bind()
方法的用法和功能。
下面是一个不在对象中调用方法的 bind()
方法的示例:
js
function greet(greeting) {
console.log(`${greeting}, ${this.name}!`);
}
const person = {
name: 'Alice'
};
const greetHello = greet.bind(person, 'Hello');
greetHello(); // 输出:Hello, Alice!
const greetGoodbye = greet.bind(person, 'Goodbye');
greetGoodbye(); // 输出:Goodbye, Alice!
在这个示例中,greet
函数定义在全局作用域中,而不是作为对象的方法。然后,我们使用 bind()
方法将 greet
函数与 person
对象绑定,并预设了一个参数 'Hello'
。这样就创建了一个新函数 greetHello
,当调用 greetHello()
时,函数内部的 this
将会指向 person
对象,参数 'Hello'
也会被传入函数中。同样地,我们也创建了另一个新函数 greetGoodbye
,它使用了 'Goodbye'
作为预设参数。
为什么要使用bind()?
bind()
方法常常用于以下几种情况:
1. 指定函数执行的上下文(this
值)
在 JavaScript 中,函数的执行上下文(this
值)取决于函数被调用的方式。使用 bind()
方法可以显式地指定函数执行时的上下文,而不受调用方式的影响。这对于需要在不同的环境中使用相同函数的情况非常有用。
js
const person = {
name: 'Alice',
greet: function() {
console.log(`Hello, my name is ${this.name}.`);
}
};
const anotherPerson = {
name: 'Bob'
};
const greetFn = person.greet.bind(anotherPerson);
greetFn(); // 输出:Hello, my name is Bob.
2. 创建偏函数
bind()
方法还可以用来创建偏函数,即预设函数的一部分参数,而返回一个接受剩余参数的新函数。这在需要多次调用函数且其中一些参数是固定的情况下非常有用。
js
function add(x, y) {
return x + y;
}
const addTwo = add.bind(null, 2);
console.log(addTwo(3)); // 输出:5
console.log(addTwo(5)); // 输出:7
让我们逐步解释这个例子:
- 定义
add
函数 :首先,我们定义了一个名为add
的函数,它接受两个参数x
和y
,并返回它们的和。
js
function add(x, y) {
return x + y;
}
- 使用
bind()
方法创建新函数 :然后,我们使用bind()
方法创建了一个新的函数addTwo
。在bind()
方法中,第一个参数null
表示我们不关心新函数的执行上下文(this
值),因此将其设置为null
。第二个参数2
是预设的参数,表示我们希望在调用新函数时,将2
作为第一个参数传递给add
函数。
js
const addTwo = add.bind(null, 2);
- 调用新函数 :最后,我们分别调用了新函数
addTwo
两次,分别传入参数3
和5
。由于我们预设了参数2
,所以新函数将会将2
与传入的参数相加,并返回结果。
js
console.log(addTwo(3)); // 输出:5
console.log(addTwo(5)); // 输出:7
因此,当我们调用 addTwo(3)
时,实际上相当于调用了 add(2, 3)
,返回结果为 5
;当我们调用 addTwo(5)
时,实际上相当于调用了 add(2, 5)
,返回结果为 7
。这样就实现了在调用函数时预设了一部分参数的效果。
3. 延迟执行
有时候需要延迟执行函数,即在未来某个时间点再调用函数,并且需要保留函数执行时的上下文和部分参数。bind()
方法可以帮助我们实现这一点。
js
//首先,定义了一个名为 `log` 的函数,它接受一个参数 `message` 并将其输出到控制台。
function log(message) {
console.log(message);
}
//然后,使用 `bind()` 方法创建了一个偏函数 `logError`,预设了一个参数 `'Error:'`。
//这样,新函数 `logError` 的 `message` 参数就被固定为 `'Error:'`,
//并且执行时的上下文为 `null`(在这个示例中,上下文不起作用,因此可以设置为任意值)。
const logError = log.bind(null, 'Error:');
//最后,使用 `setTimeout()` 方法将延迟执行 `logError` 函数,传入了一个延迟时间和一个错误信息 `'Something went wrong'`。
setTimeout(logError, 1000, 'Something went wrong'); // 1秒后输出:Error: Something went wrong
在 1 秒后,logError
函数被调用,并且 'Error: Something went wrong'
被输出到控制台。这是因为 logError
函数是通过 bind()
方法预设了参数 'Error:'
,并且延迟执行时保留了这个预设参数。
总的来说,bind()
方法提供了一种方便、灵活地处理函数上下文、部分参数预设等情况的方式,使得函数的使用更加灵活和可控。
如何⼿写 bind()
bind() ⽅法会创建⼀个新函数。当这个新函数被调⽤时,bind() 的第⼀个参数将作为它运⾏时的 this,之后的⼀序列参数将会在传递的实参前传⼊作为它的参数。
由此我们可以⾸先得出 bind 函数的两个特点:
- 返回⼀个函数;
- 可以传⼊参数;
1. 返回函数的模拟实现
js
var foo = {
value: 1
};
function bar() {
console.log(this.value);
}
// 返回了⼀个函数
var bindFoo = bar.bind(foo);
bindFoo(); // 1
关于指定 this 的指向,我们可以使⽤ call 或者 apply 实现
js
// 第⼀版
Function.prototype.bind2 = function (context) {
var self = this;
// 虑到绑定函数可能是有返回值的,加上return
return function () {
return self.apply(context);
}
}
这个 bind2
方法是一个简单的模拟实现,它基本上是一个闭包,返回一个新的函数,这个新函数会调用原始函数并指定执行上下文。
这个方法的实现步骤如下:
- 首先,将原始函数保存在变量
self
中,以便在闭包中使用。 - 然后,返回一个匿名函数,这个函数会调用
self.apply(context)
,即调用原始函数并将执行上下文设置为传入的context
。
这个实现基本上能够满足简单的 bind()
方法的功能,但是它缺少了一些重要的功能,例如预设参数和绑定后函数的执行时机。
2. 传参的模拟实现
js
var foo = {
value: 1
};
function bar(name, age) {
console.log(this.value);
console.log(name);
console.log(age);
}
var bindFoo = bar.bind(foo, 'daisy');
bindFoo('18');
// 1
// daisy
// 18
当需要传 name 和 age 两个参数时,可以在 bind 的时候,只传⼀个 name,在执⾏返回的函数的时 候,再传另⼀个参数 age。
这⾥如果不适⽤rest,使⽤arguments进⾏处理:
js
// 第⼆版
Function.prototype.bind2 = function (context) {
var self = this;
// 获取bind2函数从第⼆个参数到最后⼀个参数(提取除了context以外的参数)
var args = Array.prototype.slice.call(arguments, 1);
return function () {
// 这个时候的arguments是指bind返回的函数传⼊的参数(提取绑定后函数的参数)
var bindArgs = Array.prototype.slice.call(arguments);
// 将绑定后函数的参数与预设参数合并,并调用原始函数
return self.apply(context, args.concat(bindArgs));
}
}
这个方法的主要区别在于它允许传入预设参数,并在调用绑定后函数时将预设参数与新的参数合并起来。这样就可以更灵活地使用 bind()
方法了。
3. 构造函数效果的模拟实现
bind 还有⼀个特点,就是
⼀个绑定函数也能使⽤new操作符创建对象:这种⾏为就像把原函数当成构造器。提供的 this 值被忽 略,同时调⽤时的参数被提供给模拟函数。
也就是说当 bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传⼊的参数依然⽣效。举个例⼦:
js
var value = 2;
var foo = {
value: 1
};
function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'kevin';
var bindFoo = bar.bind(foo, 'daisy');
var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin
尽管在全局和 foo 中都声明了 value 值,最后依然返回了 undefind,说明绑定的 this 失效了,为什么呢?
在这个例子中,使用 bind()
方法将函数 bar
绑定到了对象 foo
上,并预设了一个参数 'daisy'
。然后使用 new
关键字创建了一个新的对象 obj
,并传入了一个参数 '18'
。
在 bar
函数内部,通过 this.value
访问 value
变量。这里的 this
是在 bind()
方法中指定的上下文,即对象 foo
。因此,应该期望输出 1
,即 foo
对象的 value
属性。但实际上输出了 undefined
。
这是因为使用 new
关键字调用一个函数时,会创建一个新的对象,并将这个新对象作为函数的执行上下文。这个新对象将会覆盖 bind()
方法中指定的上下文。所以在 bar
函数内部,this
实际上指向了新创建的对象 obj
,而不是 foo
对象。
因此,this.value
尝试在新对象 obj
上查找 value
属性,但是 obj
对象上并没有 value
属性,所以返回了 undefined
。
要解决这个问题,可以在 bind()
方法的函数中,将新创建的对象的 this
绑定到原始的 this
上,这样 this
就不会被覆盖了。修改后的代码如下:
js
// 第三版
Function.prototype.bind2 = function (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
// 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来⾃绑定函数的值
// 以上⾯的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是⼀个空对象,将 null 改成 this ,实例会具有 habit 属性
// 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
}
// 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
fBound.prototype = this.prototype;
return fBound;
}
这是 bind2
方法的第三版,修复了在作为构造函数时 this
指向的问题,并确保了绑定函数返回的实例可以正确地继承原始函数的原型属性。让我逐步解释这段代码:
-
self
变量保存了原始函数的引用。 -
args
数组保存了除了context
参数以外的其他参数。 -
创建了一个新的函数
fBound
,这个函数的作用是当被调用时执行原始函数,并将this
绑定到正确的上下文。这里采用了一个三元表达式来确定this
的指向:- 如果
fBound
函数被作为构造函数使用(即通过new
关键字调用),则this instanceof fBound
为true
,此时将this
指向实例对象。 - 如果
fBound
函数被作为普通函数调用,则this instanceof fBound
为false
,此时将this
指向传入的context
上下文。
- 如果
-
在
fBound
函数中,使用apply()
方法调用原始函数,并将合并后的参数列表传入。 -
设置
fBound
函数的原型为原始函数的原型,这样通过bind2
方法创建的实例就可以正确地继承原始函数的原型属性。 -
最后,返回这个新的函数
fBound
。
这样一来,通过 bind2
方法创建的函数,无论是作为构造函数使用还是作为普通函数调用,都能正确地绑定执行上下文,并且实例也可以正确地继承原始函数的原型属性。
4. 构造函数效果的优化实现
但是在这个写法中,我们直接将 fBound.prototype = this.prototype ,我们直接修改fBound.prototype 的时候,也会直接修改绑定函数的 prototype。这个时候,我们可以通过⼀个空函数来进⾏中转:
js
// 第四版
Function.prototype.bind2 = function (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {};
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
5. 最终版
调⽤ bind 的不是函数时,提示错误:
js
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
最终代码为:
js
Function.prototype.bind2 = function (context) {
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {};
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
最简化版
js
Function.prototype.myBind = function(context) {
// 判断是否是undefined 和 null
if (typeof context === "undefined" || context === null) {
context = window;
}
self = this;
return function(...args) {
return self.apply(context, args);
}
}
bind()和apply()、call()的区别?
bind
、apply
和 call
是 JavaScript 中用于改变函数执行上下文的方法,它们之间有一些区别:
-
参数传递方式:
bind
方法不会立即调用函数,而是返回一个新函数,并预设了函数执行时的上下文和一部分参数。这个新函数可以稍后被调用,且可以传入额外的参数。bind
方法的语法是function.bind(thisArg[, arg1[, arg2[, ...]]])
。apply
方法立即调用函数,并将函数执行时的上下文设置为传入的第一个参数,同时将第二个参数作为一个数组传递给函数。apply
方法的语法是function.apply(thisArg, [argsArray])
。call
方法也立即调用函数,并将函数执行时的上下文设置为传入的第一个参数,同时将后续的参数逐个传递给函数。call
方法的语法是function.call(thisArg, arg1, arg2, ...)
。
-
返回值:
bind
方法返回一个新函数,不会立即调用原函数。apply
和call
方法会立即调用原函数,并返回原函数的执行结果。
-
参数传递方式:
apply
和call
方法在传递参数时,需要将参数以数组(apply
)或逐个参数(call
)的方式传递。bind
方法在传递参数时,除了可以预设部分参数外,也可以在调用时传入额外的参数。
-
适用场景:
bind
方法常用于预设函数的执行上下文和一部分参数,以创建一个新的函数。这在需要延迟执行函数、创建偏函数或者需要绑定事件处理函数时非常有用。apply
和call
方法常用于立即调用函数,并指定函数执行的上下文和参数。这在需要立即执行函数,并传递一组参数时非常方便。
总的来说,bind
、apply
和 call
方法都是用于改变函数执行上下文的方法,但在使用方式和适用场景上有一些差别。