JavaScript 中的硬绑定与软绑定中文

lang
中文
date
Dec 23, 2021
Property
slug
javascript-bind
status
Published
tags
JavaScript
Theory
summary
在 JavaScript 中,this 的绑定是动态的,在函数被调用的时候绑定,它指向什么完全取决于函数在哪里调用,绑定规则有默认绑定、隐式绑定、显式绑定、 new 绑定等,本文就硬绑定与软绑定的原理进行深入介绍。
type
Post
在 JavaScript 中,this 的绑定是动态的,在函数被调用的时候绑定,它指向什么完全取决于函数在哪里调用,情况比较复杂,光是绑定规则就有默认绑定、隐式绑定、显式绑定、 new 绑定等,而硬绑定是显式绑定中的一种,通常情况下是通过调用函数的 apply()call() 或者 ES5 里提供的 bind() 方法来实现硬绑定的。

什么是硬绑定?

通过 apply()call()bind() 可以将函数的 this 强制绑定到指定的对象上(除了使用 new 绑定可以改变硬绑定外),但是硬绑定存在一个问题,就是会降低函数的灵活性,并且在硬绑定之后无法再使用隐式绑定或者显式绑定来修改 this 的指向。
notion image
可以看出,只有第一次使用 bind() 时改变了 foo 的 this,这也是硬绑定的特性。

bind 使用场景

先回顾一下 bind 的使用场景
语法:fun.bind(thisArg, arg1, ... , argN) bind 方法与 call / apply 最大的不同就是前者返回一个绑定上下文的函数,而后两者是直接执行了函数。
const value = 2; const foo = { value: 1 }; const bar = (name, age) => ({ value: this.value, name: name, age: age }) bar.call(foo, "Jack", 20); // 直接执行了函数 // {value: 1, name: "Jack", age: 20} const bindFoo1 = bar.bind(foo, "Jack", 20); // 返回一个函数 bindFoo1(); // {value: 1, name: "Jack", age: 20} const bindFoo2 = bar.bind(foo, "Jack"); // 返回一个函数 bindFoo2(20); // {value: 1, name: "Jack", age: 20}
通过上述代码可以看出 bind 有如下特性:
  • 1、可以指定 this
  • 2、返回一个函数
  • 3、可以传入参数
  • 4、柯里化
但是这里还有一个难点,也是 bind 的另一个特性
一个绑定函数也能使用 new 操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
const value = 2; const foo = { value: 1 }; function bar(name, age) { this.habit = 'shopping'; console.log(this.value); console.log(name); console.log(age); } bar.prototype.friend = 'kevin'; const bindFoo = bar.bind(foo, 'Jack'); bindFoo(); // 1 // Jack // undefined const obj = new bindFoo(20); // undefined // Jack // 20 obj.habit; // shopping obj.friend; // kevin

bind 绑定原理

了解了 bind 的特性,下面来看一下 bind 是如何实现硬绑定的:
if (!Function.prototype.bind) { Function.prototype.bind = function (oThis) { // 注释1 if (typeof this !== "function") { // 与 ECMAScript 5 最接近的 // 内部 IsCallable 函数 throw new TypeError( "Function.prototype.bind - what is trying " + "to be bound is not callable" ); } // 注释2 var aArgs = Array.prototype.slice.call(arguments, 1), fToBind = this, // this 指向调用者 fNOP = function () { }, fBound = function () { // 注释3 return fToBind.apply( ( this instanceof fNOP && oThis ? this : oThis ), // 注释4 aArgs.concat( Array.prototype.slice.call(arguments) ) ) }; // 注释5 fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; }; }
注释1: 调用 bind 的不是函数,这时候需要抛出异常 注释2: 第1个参数是指定的 this ,所以只截取第1个之后的参数 注释3: 当作为构造函数时,this 指向实例,此时 this instanceof fNOP && oThis 结果为 true,可以让实例获得来自绑定函数的值 注释4: 这时的arguments是指bind返回的函数传入的参数 注释5: 上面实现中 fBound.prototype = this.prototype有一个缺点,直接修改 fBound.prototype 的时候,也会直接修改 this.prototype。例子如下:
var bindFoo = bar.bind2(foo, 'Jack'); // bind2 var obj = new bindFoo(20); // 返回正确 // undefined // Jack // 20 obj.habit; // 返回正确 // shopping obj.friend; // 返回正确 // kevin obj.__proto__.friend = "Kitty"; // 修改原型 bar.prototype.friend; // 返回错误,这里被修改了 // Kitty
解决方案是用一个空对象作为中介,把 fBound.prototype 赋值为空对象的实例(原型式继承)。
var fNOP = function () {}; // 创建一个空对象 fNOP.prototype = this.prototype; // 空对象的原型指向绑定函数的原型 fBound.prototype = new fNOP(); // 空对象的实例赋值给 fBound.prototype
更详细的 bind 实现原理可以看下面的文章:

验证硬绑定特性

  • new 绑定可以改变硬绑定
const value = 2; const foo = { value: 1 }; function bar(name, age) { this.habit = 'shopping'; console.log(this.value); console.log(name); console.log(age); } bar.prototype.friend = 'kevin'; const bindFoo = bar.bind(foo, 'Jack'); bindFoo(); // 1 // Jack // undefined const obj = new bindFoo(20); // undefined // Jack // 20
  • 硬绑定之后无法再使用隐式绑定或者显式绑定来修改 this 的指向 (多次 bind 绑定)
function foo() { console.log(this.value); }; obj = { value: 10 }; obj1 = { value: 100 }; obj2 = { value: 1000 }; var p = foo.bind(obj).bind(obj1).bind(obj2); p(); //10
bind 绑定之后会返回新的函数,所以代码我们可以看成 '新函数'.bind(obj2) ,这个 '新函数' 里面还有 '新函数' 。知道有这些的话,我们只看最外层的一个就好了,当 p() 之后此时最外层的 '新函数' 的 this 指向是 obj2(1000) ,然后接着执行里面的 '新函数',接着 this 从 obj2(1000) 指向了 obj(100) ,一直这样,最后执行到foo.bind(obj) ,此时 this 已经指向 obj(10) 了,所以输出的结果是 10,原理很简单,多次绑定 bind 只是一直在改变 this 的指向,最终还是变回第一次绑定的 this。所以 bind 多次绑定是无效,只有第一次有效果。

什么是软绑定?

所谓软绑定,是和硬绑定相对应的一个词。通过软绑定,我们希望 this 在默认情况下不再指向全局对象(非严格模式)或 undefined(严格模式),而是指向两者之外的一个对象(这点和硬绑定的效果相同),但是同时又保留了隐式绑定和显式绑定在之后可以修改this 指向的能力。

软绑定的实现

在这里,用的是《你不知道的JavaScript 上》中的软绑定的代码实现:
if (!Function.prototype.softBind) { Function.prototype.softBind = function (obj) { var fn = this; var args = Array.prototype.slice.call(arguments, 1); var bound = function () { return fn.apply( (!this || this === (window || global)) ? obj : this, args.concat.apply(args, arguments) ); }; bound.prototype = Object.create(fn.prototype); return bound; }; }
除了软绑定之外, softBind(..) 的其他原理和 ES5 内置的 bind(..) 类似。它会对指定的函 数进行封装,首先检查调用时的 this ,如果 this 绑定到全局对象或者 undefined ,那就把 指定的默认对象 obj 绑定到 this ,否则不会修改 this 。
下面我们看看 softBind 是否实现了软绑定功能:
function foo(){ console.log("name: " + this.name); } var obj1 = { name: "obj1" }, obj2 = { name: "obj2" }, obj3 = { name: "obj3" }; var fooOBJ = foo.softBind(obj1); fooOBJ(); // "name: obj1" 在这里软绑定生效了,成功修改了 this 的指向, // 将 this 绑定到了 obj1 上 obj2.foo = foo.softBind(obj1); obj2.foo(); // "name: obj2" 在这里软绑定的 this 指向成功被 // 隐式绑定修改了,绑定到了 obj2 上 fooOBJ.call(obj3); // "name: obj3" 在这里软绑定的 this 指向成功被 // 硬绑定修改了,绑定到了 obj3 上 setTimeout(obj2.foo, 1000); // "name: obj1" /* 回调函数相当于一个隐式的传参,如果没有软绑定的话,这里将会应用默认绑定 将 this 绑定到全局环境上,但有软绑定,这里 this 还是指向 obj1 */
可以看到,软绑定版本的 foo() 可以手动将 this 绑定到 obj2 或者 obj3 上,但如果应用默认绑定,则会将 this 绑定到 obj1 。
下面我们来具体看一下 softBind() 的实现。
 
未完待续。。

© Matoz 2021 - 2024