JavaScript 中一颗有毒的语法糖

在 JavaScript 中 this 实际上是一颗语法糖,可是这糖有毒。this 致命的地方在于它的指向每每不能直观肯定。但愿下面能够一步步去掉有毒的糖衣。安全

1 用 f.call(thisVal, ...args) 指定 this

调用函数的方式有三种,用 Function.prototype.call 调用能够指定 this:闭包

定义 function f(...args){/*...*/}app

调用 f.call(thisVal, ...args);框架

例一函数

function greet(){
  console.log('Hello, ' + this);
}
// 手动指定 `greet` 中的 `this`:
greet.call('ngolin'); // Hello, ngolin

例二this

function whoAreYou(){
  console.log("I'm " + this.name);
}
whoAreYou.call({name: 'Jane'}); // I'm Jane

2 使用语法糖,this 自动指定

先接受函数 f 的正确调用方式是 f.call(thisVal, ...args);, 而后就能够把 f(...args); 理解成语法糖。prototype

可是不用 f.call(thisVal, ...args), this 怎样动态指定?code

1、函数(function)对象

// 1. 在非严格模式下:window
f(); // 解糖为 f.call(window);
// 2. 但在严格模式下:undefined
f(1, 2, 3); // 解糖为 f.call(undefined, 1, 2, 3);

1、方法(method)ip

// 不管是在严格仍是非严格模式:
obj.m(1, 2, 3); // 解糖为 obj.m.call(obj, 1, 2, 3);
obj1.obj2.m(...args); // obj1.obj2.m.call(obj1.obj2, ...args);
obj1.obj2....objn.m(); // obj1.obj2....objn.m.call(obj1.obj2....objn);

经过上面的例子,分别演示了函数 f(..args) 和方法 obj1.obj2....objn.m(..args) 怎样自动指定 this.

严格区分函数(function)和方法(method)这两个概念有利于清晰思考,由于它们在绑定 this 时发生的行为彻底不同。同时函数和方法能够相互赋值(转换),在赋值先后,惟一发生变化的是绑定 this 的行为(固然这种变化在调用时才会体现)。下面先看函数转方法,再看方法转函数。

3 函数方法

函数声明(function f(){})和函数表达式(var f = function(){};)有一些微妙的区别,可是两种方式在调用时绑定this行为彻底同样,下面在严格模式下以函数表达式为例:

var f = function(){
  console.log(this.name);
};

var obj1 = {
  name: 'obj 1',
  getName: f;
};

var obj2 = {
  name: 'obj 2',
  getName: f;
};

// 函数 `f` 转方法 `obj1.getName`
obj1.getName();// 'obj 1' => obj1.getName.call(obj1)
// 不认为函数转方法
obj2.getName.call(obj1);// 'obj 1'(不是 'obj 2')

将函数转成方法一般不太容易出错,由于起码在方法中 this 可以有效地指向一个对象。函数转成方法是一个模糊的说法,实际上能够这样理解:

JavaScript 不能定义一个函数,也不能定义一个方法,是函数仍是方法,要等到它执行才能肯定;当把它当成函数执行,它就是函数,当把它当成方法执行,它就是方法。因此只能说执行一个函数和执行一个方法。\
\
这样理解可能有些极端,可是它可能有助于避免一些常见的错误。由于关系到 this 怎样绑定,重要的是在哪里调用(好比在 obj1, obj2... 上调用)以及怎样调用(好比以 f(), f.call()... 的方式),而不是在哪里定义。

可是,为了表达的方便,这里仍然会使用定义函数定义方法这两种说法。

4 方法函数

将方法转成函数比较容易出错,好比:

var obj = {
  name: 'obj',
  show: function(){
    console.log(this.name);
  }
};

var _show = obj.show;
_show(); // error!! => _show.call(undefined)

button.onClick = obj.show;

button.onClick(); // error!! => button.onClick.call(button)

(function(cb){
  cb(); // error!! =>cb.call(undefined)
})(obj.show);

当一个对象的方法使用了 this 时,若是这个方法最后不是由这个对象调用(好比由其余框架调用),这个方法就可能会出错。可是有一种技术能够将一个方法(或函数)绑定(bind)在一个对象上,从而不管怎样调用,它都可以正常执行。

5 把方法绑定(bind)在对象上

先看这个obj.getName的例子:

var obj = {
  getName: function(){
    return 'ngolin';
  }
};

obj.getName(); // 'ngolin'
obj.getName.call(undefined); // 'ngolin'
obj.getName.call({name: 'ngolin'}); // 'ngolin'

var f = obj.getName;
f(); // 'ngolin'

(function(cb){
  cb(); // 'ngolin'
})(obj.getName);

上面的例子之因此能够成功是由于 obj.getName 根本没有用到 this, 因此 this 指向什么对 obj.getName 都没有影响。

这里有一种技术把使用 this 的方法转成不使用 this 的方法,就是建立两个闭包(即函数),第一个闭包将方法(method)和对象(obj)捕获下来并返回第二个闭包,而第二个闭包用于调用并返回 obj.method.call(obj);. 下面一步步实现这种技术:

第一步 最简单的状况下:

function method(){
  obj.method.call(obj);
}
method(); // correct, :))

存在的缺陷:

  1. 只适合没有参数和返回的 obj.method
  2. 存在两个安全隐患:
    1 后续改变 obj.method,好比 obj.method = null;
    2 后续改变 obj,好比 obj = null

第二步 在方法有参数有返回的状况下:

function method(a, b){
  return obj.method.call(obj, a, b);
}
method(a, b); // correct, :))

存在的缺陷:

  1. 只适合两个参数的 obj.method
  2. 存在两个安全隐患,同上。

第三步 一个传递参数更好的办法:

function method(){
  return obj.method.apply(obj, arguments);
}
method(a, b); // correct, :))

仍存在两个安全隐患。

第四步 更加安全的方式:

var method = (function(){
  return function(){
    return obj.method.apply(obj, arguments);
  };
})(obj.method, obj);

method(a, b); // correct, :))

第五步 抽象出一个函数,用于将方法绑定到对象上:

function bind(method, obj){
  return function(){
    return method.apply(obj, arguments);
  };
}

var obj = {
  name: 'ngolin',
  getName: function(){
    return this.name;
  }
};

var method = bind(obj.getName, obj);
method(); // 'ngolin'

6 Function.prototype.bind

这种方法很常见,后来 ECMAScript 5 就增长了 Function.prototype.bind, 好比:

var binded = function(){
  return this.name;
}.bind({name: 'ngolin'});

binded(); // 'ngolin'

具体来讲,Function.prototype.bind 这样工做:

var bindedMethod = obj.method.bind(obj);
// 至关于:
var bindedMethod = (function(){
  return function(){
    return obj.method.apply(obj, arguments);
  };
})(obj.method, obj);

更多使用 Function.prototype.bind 的例子:

var f = obj.method.bind(obj);

button.onClick = obj.method.bind(obj);

document.addEventListener('click', obj.method.bind(obj));

7 常见问题及容易出错的地方

在定义对象时有没有 this?

obj = {
  firstName: 'First',
  lastName: 'Last',
  // `fullName` 能够获得预期结果吗?
  fullName: this.firstName + this.lastName
}

// 或者:

function makePoint(article){
  if(article.length <= 144) return article;
  return article.substr(0, 141) + '...';
}
obj = {
  fulltext: '...a long article go here...',
  // `abstract` 呢?
  abstract: makePoint(this.fulltext)
}

在方法内的 this 都是同一对象吗?

obj = {
  count: 3,
  field: 'field',
  method: function(){
    function repeat(){
      if(this.count > 100){
        return this.field.repeat(this.count % 100);
      }
      this.field.repeat(this.count);
    }.bind(this);
    // 这个呢?
    return repeat();
  }
}
相关文章
相关标签/搜索