上下文绑定

恼人的 this

对于 JavaScript 初学者来说,this 关键字一直是一个恼人的东西,关于 this 的解释和描述,最好的文章是 You-Dont-Know-JS 下的 this & Object Prototypes章节 。文中,作者认为,人们通常会因为 this 的字面意思而产生如下两种误解

  • itself

this 理解为 itself,也就是认为 this 指向了其所在的函数:

function foo(num) {
    console.log( "foo: " + num );

    // keep track of how many times `foo` is called
    this.count++;
}

foo.count = 0;

var i;

for (i=0; i<10; i++) {
    if (i > 5) {
        foo( i );
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9

// how many times was `foo` called?
console.log( foo.count ); // 0 -- WTF?

当我们认为 this 指代的是所在函数 foo 时,就想要最后打印 foo.count 的结果是 10,然而事与愿违。不过这种误解也是情有可原的,因为在 js 中,函数也是对象,而在对象中,似乎 this 确是指向了自身 itself:

var obj = {
  count : 10;
  foo: function(){
    console.log(this.count);
  }
}

obj.foo(); // => 10
  • its scope

还有一种误解显得 “高明” 许多,他们认为 this 由其函数所在的词法作用域决定,比如下例:

function foo() {
    var a = 2;
    this.bar();
}

function bar() {
    console.log('a is:'+ this.a );
}

foo(); //a is:undefined

这个例子非常有意思,他不只犯了一个错误:

  • 首先,代码书写者认为 this 由其函数所在的词法作用域决定,函数 foo 所在的词法作用域是在全局作用域,所以 this 就指向了全局作用域,那么,由于函数 bar 所在的词法作用域也是全局作用域,this.bar() 理应能够工作。事实也确实如此,但是工作的原因却并非如此,之后会做解释。
  • 代码书写者认为 this 也能像闭包那样被使用:当内部作用域中 this.a 寻找失败时,会去外部作用于寻找 this.a。千万要记住,this 并不具备 跨作用域 的能力。

this 究竟是谁?

this 关键字并不神秘,我们需要知道,this 指代的对象并不能在函数定义就确定,他由 函数执行时所在的上下文 确定。换言之,一个函数如果不跑起来,他内部的 this 就永远无法确定。看到如下代码:

function foo() {
  console.log(this.a);
}

var a = 2;
foo(); // => 2

当函数 foo 调用时,亦即 foo() 语句执行时,其所在的 上下文 是全局对象,所以,this 绑定到了全局对象(在浏览器环境的话就是 window 对象,foo() 也相当于是 window.foo())。再看下面的一个代码:

function foo() {
  console.log(this.a);
}

var obj = {
  a: 2,
  foo: foo
}

obj.foo(); // => 2

当成员方法obj.foo被调用时,以及obj.foo()语句执行时,函数foo所在的执行上下文变成了obj,即this指向了obj

综上,我们知道,想要知道this最终“情归何处”,就一定是在运行时(runtime)环境,而不是函数定义时。但是,js同样为我们提供了手段,来手动绑定函数中的this关键字,我将他分为两类: 1. 调用型绑定:进行一次函数调用,并绑定当中的this 2. 定义型绑定:不进行函数调用,新创建一个函数,指定其中的this归属

注意,这两型的绑定都不会改变 “ this 只有在运行时才能够绑定 ” 这一事实。

调用型绑定

Function.prototype 上,提供了 applycall 两个方法来绑定 this 指代的上下文对象,二者的第一个参数都是待绑定对象,而第二个参数都是调用参数,只是表现形式不同,apply 传递的参数是 数组型 的,call 则是 逐个传递参数

function add(a, b) {
  var result = a+b;
  console.log(this.name+" wanna get result:"+result);
}

var caller1 = {
  name: 'wxj'
};

var caller2 = {
  name: 'zxy'
};

// call需要逐个传递参数
add.call(caller1, 3, 4);
// => "wxj wanna get result:7"

// apply传递参数数组
add.apply(caller2, [3,4]);
// => "zxy wanna get result:7"

定义型绑定

而在 ES5 中,Function.prototype 还提供了 bind 来绑定 this 指代的上下文,与 调用型绑定 不同的是,定义型绑定 不会立即调用函数,而是返回一个被固定了执行上下文的新函数:

function showName() {
  console.log('my name is:'+this.name);
}

var student = {
  name: 'wxj',
  age: 13
};

var showWxjName = showName.bind(student);
showWxjName();
// => "my name is:wxj"

定义型绑定也不能改变 “this 只有在运行时才能够绑定” 这一事实,他只是创建了一个新函数,然后让新函数在执行过程中通过 call 或者 apply 来调用绑定了执行上下文的老函数。看到 MDN 给出的 polyfill:

if (!Function.prototype.bind) {
  Function.prototype.bind = function (oThis) {
    if (typeof this !== "function") {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var aArgs = Array.prototype.slice.call(arguments, 1),
        // 待绑定对象
        fToBind = this,
        fNOP = function () {},
        fBound = function () {
          return fToBind.apply(this instanceof fNOP
                                 ? this
                                 : oThis || this,
                               aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();

    return fBound;
  };
}

这个 polyfill 非常值得玩味,当中出现的各个 this 令人眼花缭乱,下面着重解释一下该 polyfill:

  • 设置待绑定函数
// ...
  fToBind = this;
// ...

当我们讨论 this 的时候,一定要到执行环境下:

// ...
  function foo() {
    console.log("a is:"+a);
  }

  var obj = {
    a: 2
  };

  // bind被调用,处于执行环境
  foo.bind(obj);
// ...

此处的 this 绑定到了 foo,即 fToBind === foo

  • 创建绑定函数
// ...
  fNOP = function () {},
  fBound = function () {
    // `apply`时进行上下文校正
    return fToBind.apply(this instanceof fNOP
                           ? this
                           : oThis || this,
                         aArgs.concat(Array.prototype.slice.call(arguments)));
  };
  fNOP.prototype = this.prototype;
  fBound.prototype = new fNOP();
// ...

注意,以上语句的意图是要照顾到绑定后的函数作为构造函数时(是否被 new 操作)的情况:

function func() {
    this.name = "anonymous";
}

var Person = func.bind(obj);
var person = new Person();

我们知道,new 操作符会将构造函数中的 this 的绑定到实例化对象上,如果我们不进行上下文校验,那么 this 会被绑定到 obj 上,这显然不是我们期望看到的。

所以,当 fBound 执行时,如果是通过 new 操作符调用 fBound,即 fBound 被当做构造函数使用,执行上下文应当是实例化对象本身,由于实例化对象已经绑到了 this,所以不需要再改变 this 的指向。

executeBound

underscore 中也提供了绑定函数上下文的方法 _.bind_.bindAll,当所处环境不支持 Function.prototype.bind 时,二者的绑定上下文及执行过程都由内部函数 executeBound 负责:

// 执行绑定后的函数
var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {
  if (!(callingContext instanceof boundFunc))
      return sourceFunc.apply(context, args);
  var self = baseCreate(sourceFunc.prototype);
  var result = sourceFunc.apply(self, args);
  if (_.isObject(result))
      return result;
  return self;
};

executeBound 接受 5 个参数:

  • sourceFunc:待绑定函数
  • boundFunc: 绑定后函数
  • context:待绑定上下文
  • callingContext:执行上下文,通常就是 this
  • args:函数执行所需参数

并且,类似于 MDN 中提供的 polyfill,executeBound 也考虑到了当绑定后的函数 boundFunc 作为构造函数被 new 运算的情形,进行了执行上下文的修正。另外,为了支持链式调用,所以有如下语句:

var result = sourceFunc.apply(self, args);
if (_.isObject(result))
    return result;
return self;

_.bind

_.bind(func, context):将 func 的执行上线文绑定到 context

源码

_.bind = function (func, context) {
    if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
    if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
    var args = slice.call(arguments, 2);
    var bound = function () {
        return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));
    };
    return bound;
};

_.bind 将为我们返回一个绑定了上下文的函数,该函数的执行过程会限定执行上下文。

用例

// 测试一般函数绑定
function add(a,b) {
    var result = a+b;
    console.log(this.name + ' wanna get add result:' + result);
}

var obj = {
    name: 'wxj'
}

var bound = _.bind(add, obj, 3, 4);
bound();
// => "wxj wanna get add result:7"

// 测试绑定函数作为构造函数使用
function constr() {
    console.log('my name is:' + this.name);
}

var Person = _.bind(constr, obj);
// 一般函数使用
Person();
// => "my name is wxj"

// 构造函数使用
var person = new Person();
// => "my name is:undefined"

_.bindAll

_.bindAll(obj):绑定对象 obj 的所有指定成员方法中的执行上下文 到 obj

先看到下面这样一个例子:

// 我们模拟一个DOM元素
var button = {
      title: 'button#1',
      onClick: function() {
          console.log(this.title + ' has been clicked!');
      }
};

button.onClick();
// => "button#1 has been clicked!"

setTimeout(button.onClick, 0);
// => "undefined has been clicked!"

悲剧,当我们把成员方法作为 setTimeout 的回调传入后,this 在运行时的绑定事与愿违。记住,button.onClick 不是 button.onClick()setTimeout 的回调函数执行时的上下文不再是 button 对象,为此,我们就需要绑定 button 所有成员方法的 this 到该对象上,underscore 通过 _.bindAll 来实现这个目标。

源码

_.bindAll = function (obj) {
    var i, length = arguments.length,
        key;
    if (length <= 1) throw new Error('bindAll must be passed function names');
    for (i = 1; i < length; i++) {
        key = arguments[i];
        obj[key] = _.bind(obj[key], obj);
    }
    return obj;
};

用例

var button = {
    title: 'button#1',
    onClick: function() {
        console.log(this.title + ' has been clicked!');
    },
    onHover: function() {
        console.log(this.title + ' hovering!');
    }
}

_.bindAll(button, 'onClick', 'onHover');

setTimeout(button.onClick, 0);
setTimeout(button.onHover, 0);
// => "button#1 has been clicked!"
// => "button#1 hovering!"

results matching ""

    No results matching ""