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();
  }
}
相關文章
相關標籤/搜索