详解bind()

什么是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() 方法创建了两个新函数 greetHellogreetGoodbye,分别预设了不同的问候语。这两个新函数的 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

让我们逐步解释这个例子:

  1. 定义 add 函数 :首先,我们定义了一个名为 add 的函数,它接受两个参数 xy,并返回它们的和。
js 复制代码
function add(x, y) {
    return x + y;
}
  1. 使用 bind() 方法创建新函数 :然后,我们使用 bind() 方法创建了一个新的函数 addTwo。在 bind() 方法中,第一个参数 null 表示我们不关心新函数的执行上下文(this 值),因此将其设置为 null。第二个参数 2 是预设的参数,表示我们希望在调用新函数时,将 2 作为第一个参数传递给 add 函数。
js 复制代码
const addTwo = add.bind(null, 2);
  1. 调用新函数 :最后,我们分别调用了新函数 addTwo 两次,分别传入参数 35。由于我们预设了参数 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. 返回⼀个函数;
  2. 可以传⼊参数;

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 指向的问题,并确保了绑定函数返回的实例可以正确地继承原始函数的原型属性。让我逐步解释这段代码:

  1. self 变量保存了原始函数的引用。

  2. args 数组保存了除了 context 参数以外的其他参数。

  3. 创建了一个新的函数 fBound,这个函数的作用是当被调用时执行原始函数,并将 this 绑定到正确的上下文。这里采用了一个三元表达式来确定 this 的指向:

    • 如果 fBound 函数被作为构造函数使用(即通过 new 关键字调用),则 this instanceof fBoundtrue,此时将 this 指向实例对象。
    • 如果 fBound 函数被作为普通函数调用,则 this instanceof fBoundfalse,此时将 this 指向传入的 context 上下文。
  4. fBound 函数中,使用 apply() 方法调用原始函数,并将合并后的参数列表传入。

  5. 设置 fBound 函数的原型为原始函数的原型,这样通过 bind2 方法创建的实例就可以正确地继承原始函数的原型属性。

  6. 最后,返回这个新的函数 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()的区别?

bindapplycall 是 JavaScript 中用于改变函数执行上下文的方法,它们之间有一些区别:

  1. 参数传递方式

    • bind 方法不会立即调用函数,而是返回一个新函数,并预设了函数执行时的上下文和一部分参数。这个新函数可以稍后被调用,且可以传入额外的参数。bind 方法的语法是 function.bind(thisArg[, arg1[, arg2[, ...]]])
    • apply 方法立即调用函数,并将函数执行时的上下文设置为传入的第一个参数,同时将第二个参数作为一个数组传递给函数。apply 方法的语法是 function.apply(thisArg, [argsArray])
    • call 方法也立即调用函数,并将函数执行时的上下文设置为传入的第一个参数,同时将后续的参数逐个传递给函数。call 方法的语法是 function.call(thisArg, arg1, arg2, ...)
  2. 返回值

    • bind 方法返回一个新函数,不会立即调用原函数。
    • applycall 方法会立即调用原函数,并返回原函数的执行结果。
  3. 参数传递方式

    • applycall 方法在传递参数时,需要将参数以数组(apply)或逐个参数(call)的方式传递。
    • bind 方法在传递参数时,除了可以预设部分参数外,也可以在调用时传入额外的参数。
  4. 适用场景

    • bind 方法常用于预设函数的执行上下文和一部分参数,以创建一个新的函数。这在需要延迟执行函数、创建偏函数或者需要绑定事件处理函数时非常有用。
    • applycall 方法常用于立即调用函数,并指定函数执行的上下文和参数。这在需要立即执行函数,并传递一组参数时非常方便。

总的来说,bindapplycall 方法都是用于改变函数执行上下文的方法,但在使用方式和适用场景上有一些差别。

相关推荐
liuweidong08022 小时前
【Pandas】pandas Series rtruediv
前端·javascript·pandas
我想学LINUX4 小时前
【2024年华为OD机试】(C卷,100分)- 攀登者1 (Java & JS & Python&C/C++)
java·c语言·javascript·c++·python·游戏·华为od
然后就去远行吧5 小时前
小程序组件 —— 31 事件系统 - 事件绑定和事件对象
前端·javascript·小程序
夕阳_醉了5 小时前
如何在JS里进行深拷贝
开发语言·javascript·ecmascript
疯狂的沙粒7 小时前
对React的高阶组件的理解?应用场景?
前端·javascript·react.js·前端框架
星云code7 小时前
【HM-React】08. Layout模块
javascript·react.js·ecmascript
互联网-小阿宇8 小时前
【HTML+CSS+JS+VUE】web前端教程-31-css3新特性
前端·javascript·css
NoneCoder8 小时前
JavaScript系列(24)--内存管理机制详解
开发语言·javascript·ecmascript
han_9 小时前
为实现前端截图功能,我的dom-to-image踩坑之旅!
前端·javascript
不修×蝙蝠9 小时前
vue(七) vue进阶
前端·javascript·vue.js·前端框架·vue·ssm·进阶