JavaScript重识bind、call、apply

前言——this的一些误解

  1. 是指向自身
  2. this 在任何情况下都指向函数的词法作用域

思考:

function foo() {
    var a = 2;
    this.bar(); 
}
function bar() { 
    console.log( this.a );
}
foo(); // ReferenceError: a is not defined ?

this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包 含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的 其中一个属性,会在函数执行的过程中用到。

这里要说的call,apply,bind都是来改变this的指向的

1、call、apply、bind对比

call,apply可以多次改变绑定对象;只是apply接受数组格式参数;

  • 1、都是用来改变函数的this对象的指向的。
  • 2、第一个参数都是this要指向的对象。
  • 3、都可以利用后续参数传参。

bind()方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind()方法的第一个参数作为 this,传入 bind() 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。

bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。

bind 这些改变上下文的 API 了,对于这些函数来说,this 取决于第一个参数,如果第一个参数为空,那么就是 window。

2、关于绑定

  1. 默认的 this 绑定,
    就是说 在一个函数中使用了 this, 但是没有为 this 绑定对象. 这种情况下, 非严格默认, this 就是全局变量 Node 环境中的 global, 浏览器环境中的 window.
  2. 隐式绑定:
    使用 obj.foo() 这样的语法来调用函数的时候, 函数 foo 中的 this 绑定到 obj 对象.
  3. 显示绑定:
    foo.call(obj, ...),
    foo.apply(obj,[...]),
    foo.bind(obj,...)
  4. 构造绑定:
    new foo() , 这种情况, 无论 foo 是否做了绑定, 都要创建一个新的对象, 然后 foo 中的 this 引用这个对象.

优先级: 构造绑定>显示绑定>隐式绑定>默认的 this 绑定


image

3、硬绑定

bind只能被绑定一次;以第一次为准;

function foo() {
    console.log("name: " + this.name);
}
var obj = { name: "obj" }, obj2 = { name: "obj2" }, obj3 = { name: "obj3" };
foo.bind(obj).call(obj2)  // name: obj
foo.bind(obj).bind(obj2)()  // name: obj

bind 内部就是 包了一个apply;等到调用的时候再执行这个包含apply的函;
实际上,ES5 中内置的 Function.prototype.bind(..) 更加复杂。下面是 MDN 提供的一种bind(..) 实现:

详细请看 为什么JavaScript多次绑定只有一次生效?

if (!Function.prototype.bind) {
    Function.prototype.bind = function(oThis) {
        //一个函数去调用,也就是说bind,call,apply的this是个函数;
        //然后再去改变这个函数里面的this;
        if (typeof this !== "function") {
         // 与 ECMAScript 5 最接近的
         // 内部 IsCallable 函数 
         throw new TypeError(
          "Function.prototype.bind - what is trying " +
         "to be bound is not callable"
         ); 
        }
        //这里将初始化的参数缓存起来;
        var aArgs = Array.prototype.slice.call( arguments, 1 ),
        // ftoBind 指向要bind的函数;
        fToBind = this,
        // 返回一个新函数
        fNOP = function(){}, 
        fBound = function(){
        //fToBind.apply 改变绑定this;
        // 执行的时候判断,当前this等于fNOP并且传入oThis,就设置成当前this,不然就改变成初始化传入的oThis;
           return fToBind.apply( 
            (this instanceof fNOP && oThis ? this : oThis ),
            aArgs.concat(Array.prototype.slice.call( arguments ) )
            ); 
        };
        fNOP.prototype = this.prototype;
        fBound.prototype = new fNOP();
        return fBound;
    };
}

解释(this instanceof fNOP && oThis ? this : oThis )
这段代码请看 javascript 深入解剖bind内部机制

4、软绑定

硬绑定这种方式可以把 this 强制绑定到指定的对象(除了使用 new 时),防止函数调用应用默认绑定规则。问题在于,硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改 this。

if (!Function.prototype.softBind) {
    Function.prototype.softBind = function(obj) { 
        var fn = this; // 捕获所有 curried 参数
        var curried = [].slice.call( arguments, 1 );
        var bound = function() {
            return fn.apply((!this || this === (window || global)) ?
            obj : this,curried.concat.apply( curried, arguments ) ); 
        }; 
        bound.prototype = Object.create( fn.prototype );
        return bound;
    }; 
} 

它会对指定的函 数进行封装,首先检查调用时的 this,如果 this 绑定到全局对象或者 undefined,那就把 指定的默认对象 obj 绑定到 this,否则不会修改 this。此外,这段代码还支持可选的柯里化;

function foo() {
    console.log("name: " + this.name);
}
var obj = { name: "obj" }, obj2 = { name: "obj2" }, obj3 = { name: "obj3" };
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj); 
obj2.foo(); // name: obj2 <---- 看!!!
fooOBJ.call( obj3 ); // name: obj3 <---- 看! 
setTimeout( obj2.foo, 10 );// name: obj <---- 应用了软绑定

注意

如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值
在调用时会被忽略,实际应用的是 默认绑定规则:

function foo(a,b) {
    console.log( "a:" + a + ", b:" + b );
}
// 把数组“展开”成参数
foo.apply( null, [2, 3] ); // a:2, b:3

// 使用 bind(..) 进行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3

用 null 来忽略 this 绑定可能会有副作用。如果某个函数确实使用了 this(比如第三方库中的一个函数),那默认绑定规则会把 this 绑定到全局对象(在浏览 器中这个对象是 window),这将导致不可预计的后果(比如修改全局对象)。

优化:用Object.create(null)替代 null 或者 undefined


  • 参考:《你不知道的javascript》