javascript:this 關鍵字

前言

看過[阮一峯]()的關於 this 的教程,講了不少比較好的例子,但沒有對其本質的東西解釋清楚,並且部分例證存在問題;因而,打算重寫本章節,從this的本質入手;javascript

本文爲做者的原創做品,轉載需註明出處;html

this 是什麼?

this能夠理解爲一個指針,指向調用對象;java

判斷 this 是什麼的四個法則

官網定義編程

先來看第一段官方的解釋,windows

In JavaScript, as in most object-oriented programming languages, this is a special keyword that is used within methods to refer to the object on which a method is being invoked. The value of this is determined using a simple series of steps:數組

  1. If the function is invoked using Function.call or Function.apply, this will be set to the first argument passed to call/apply. If the first argument passed to call/apply is null or undefined, this will refer to the global object (which is the window object in Web browsers).
  2. If the function being invoked was created using Function.bind, this will be the first argument that was passed to bind at the time the function was created.
  3. If the function is being invoked as a method of an object, this will refer to that object.
  4. Otherwise, the function is being invoked as a standalone function not attached to any object, and this will refer to the global object.

大體翻譯以下,
this是這麼一個特殊的關鍵字,它是用來指向一個當前正在被調用( a being invoked )方法的調用對象的;( 等等,這句話其實隱藏了一個很是關鍵的信息,那就是this是在運行期 生效的,怎麼生效的?在運行期this被賦值,將某個對象賦值給this,與聲明期無關,也就是說,this是運行期相關的 );this的賦值場景,概括起來,分爲以下四種狀況,瀏覽器

  1. 若是方法是被Function.call或者Function.apply調用執行.... bla..bla..
    參考 function prototype call 小節
  2. 若是是被Function.bind... bla...bla
    參考 function prototype bind 小節
  3. 若是某個方法在運行期是被一個對象( an object )調用( 備註:這裏爲了便於理解,我針對這種狀況,本身給起了個名稱叫關聯調用 ),在運行期,會將該 object 的引用賦值給該方法的this
    備註:被一個對象調用?何解?其實就是指語句obj.func(),這個時候,func()方法內部的this將會被賦值爲obj對象的引用,也就是指向obj
  4. 若是該方法在運行期被當作一個沒有依附在任何 object 上的一個獨立方法被調用(is being invoked as a standalone function not attached to any object ),那麼該方法內部的this將會被賦值爲全局對象(在瀏覽器端就是 windows )
    獨立方法 ( standalone function )?在運行期,若是func方法被obj關聯調用的,既是經過obj.func()的方式,那麼它就不是standalone的;若是是直接被調用,沒有任何對象關聯,既是經過func()調用,那麼這就是standalone的。

this 運行期相關

官網定義 2閉包

再來看另一句很是精煉的描述,來加深理解app

The this keyword is relative to the execution context, not the declaration context.

this關鍵字與運行環境有關而與聲明環境無關;(補充,而做用域鏈閉包是在函數的聲明期建立的,參考建立時機)函數

補充,是如何與函數的運行期相關的,參考this 指針運行時賦值

個人補充

法則 #3 和 #4,大多數狀況都很是容易理解,有幾種狀況須要特別注意,

  1. 函數嵌套
    須要注意的是object對象中的函數內部再次嵌套函數的狀況,

    var name = "windows";  
          
    var obj = {  
    
        name:"object",  
    
        f1:function(){  
    
          console.log("this: "+this.name)
    
          function f2(){  
    
                console.log("this: " + this.name)
          }
    
          f2();  
        }  
    };

    執行

    > obj.f1();
     this: object
     this: windows

    能夠看到,在運行期,被調用函數 f1() 中的this指向 obj_,而被調用函數 _f2() 中的this指向的是 windows ( global object );由於 f1 函數在當前的運行時中是經過 obj.f1() 進行的關聯調用,因此,根據定義 #3,在當前的運行期間,_f1()_ 內部的 this 是指向 obj 對象的( 經過將 obj 的引用直接賦值給 this ),而, f2 函數在運行期是沒有與其它 object 進行關聯調用,因此,在當前的運行時期,_f2_ 是一個 standalone 的函數,因此,根據定義 #4,在當前的運行期間,_f2()_ 的內部this是指向 windows 的。(注意,這裏我反覆強調當前運行期間,是由於this是在運行時被賦值的,因此,要特別注意的是,即便某個函數的定義不變,但在不一樣的執行環境(運行環境)中,this是會發生變化;)

  2. 回調函數
    參看函數回調場景-1函數回調場景-2
  3. 函數賦值
    參看將函數賦值-standalone以及相關變種章節

可見,要判斷this運行期到底指的是什麼,並無那麼容易,可是,只要緊緊的把握好兩點,就能夠迎刃而解,

  • this運行期相關的
    更確切的說,this是在運行期被賦值的,因此,它的值是在運行期動態肯定的。
  • this是否與其它對象關聯調用
    這裏的關聯調用指的是 javascript 的一種語法,既是調用語句顯式的寫爲obj.func(),另外須要注意的是,_javascript_ 方法的調用不會隱式的隱含 this。只要沒有顯式的關聯調用,那麼就是standalone的調用,就符合法則 #4,因此,this指向 _Global Object_。

this 的 Object

注意,this定義中所指的Object指的是 javascriptObject 類型,既是經過

var o1 = {};
var o2  = new Object();
var o3 = Object.create(Object.prototype);

這樣的方式構建出來的對象;

備註,最開始,本身有個思惟的誤區,認爲既然 javascript 一切皆爲對象,那麼this指針是指向對象的,那麼是否是也能夠指向FunctionNumber等對象?答案是否認的。

起初,我是按照上面的邏輯來理解的,直到當我總結到bind 是如何實現的小節後,發現Function對象在調用方法屬性bind的時候,bind方法內部的this指向的是Function,這才恍然大悟,thisObject其實是能夠指向任何 javascript Object的,包括 Object_、_Function 等。

this 是變化的

咱們來看這樣一個例子,

var C = "王麻子";

var A = {
  name: '張三',
  describe: function () {
    return '姓名:'+ this.name;
  }
};

var B = {
  name: '李四'
};

// 執行,
> A.describe();
  '張三'

> B.describe = A.describe;
> B.describe()
  '李四'

> var describe = A.describe;
> describe();
  '王麻子'

能夠看到,雖然 A.describe 方法的定義不變,可是其運行時環境發生了變化,_this_ 的指向也就發生了變化。

> B.describe = A.describe;
> B.describe()
  '李四'

在運行時,至關於運行的是 B 的 describe 方法

> var describe = A.describe;
> describe();
  '王麻子'

在運行時,至關於運行的是 windows 的 describe 方法

方法調用沒有隱含 this

常常寫 Java 代碼的緣由,常常會習慣性的認爲只要在對象方法裏面調用某個方法或者屬性,隱含了 this,好比

public class Person{

  String name;

  public String getName(){
    return name;
  }

  public String getName2(){
    return this.name;
  }

}

而 Javascript 實際上並無這種隱含的表達方式;詳細驗證過程參考將函數賦值-standalone

關聯調用 - 容易混淆的場景

this 是什麼章節中,爲了方便對 #3 進行描述,我起了個名字叫作 關聯調用 ;那麼有些狀況看似是 _關聯調用_,實則否則;

咱們有一個標準的對象,定義以下,

var name = "windows";
var obj = {
  name: "obj",
  foo: function () {
    console.log("this: "+ this.name);
  }
};

經過標準的 關聯調用 的方式,咱們進行以下的調用,

> obj.foo() 
  'this: obj'

根據法則 #3 既 關聯調用 的定義,獲得 this -> obj_;若是事事都如此的簡單,如此的標準,那可就行了,總會有些讓人費解的狀況,如今來看看以下的一些特殊的例子,加深對 _關聯調用 的理解。

將函數賦值 - standalone

> var fooo = obj.foo
> fooo();
  'this: windows'

輸出的 windows_,既是 _this -> global object_,而不是咱們指望的 _obj_;爲何?緣由是,_obj.foo 實際上是 foo 函數的函數地址,經過 var fooo = obj.foo 將該函數的地址賦給了變量 _fooo_,那麼當執行

> fooo();

的時候,fooo() 執行的是是一個standalone的方法,根據法則 #4,因此該方法內部的this指向的是 Global Object_;注意,_obj.foo 表示函數 foo 的入口地址,因此,變量 fooo 等價與 foo 函數。

備註:因爲受到寫 Java 代碼習慣的緣由,很容易將這裏解釋爲默認執行的是this.fooo(),_fooo()_ 的調用隱含了this,所以就會想到,因爲this指向的 Global Object_,因此這裏固然返回的就是this: windows;可是,這樣解釋,是不對的,由於 _Javascript 壓根沒有這種隱含this的概念,參看用例,

var name = "windows";

var o = {

  name : "o",

  f2 : function(){
      console.log( "o -> f2");
      console.log( "this: "this.name );
  },

  f : function(){

      console.log("f.this -> " + this.name);

      var f2 = function(){
          console.log( "f -> f2");
          console.log( this.name );
      }

      f2(); // f -> f2

      this.f2(); // o -> f2

  }

}

能夠看到,在 o.f() 函數中,若是 f2() 的調用隱含了this,那麼 f2()this.f2() 二者調用應該是等價的;可是,在實際執行過程當中,_f2()_ 和 this.f2() 執行的是兩個大相徑庭的方法,所以 f2()this.f2()_,因此 _f2() 並無隱示的表示爲 _this.f2()_;

將函數賦值變種 - 匿名 standalone 函數當即執行

> (obj.foo = obj.foo)() 
  'this: windows'

首先,當即執行 foo 函數,而後將 foo 函數賦值給對象 obj 對象的 foo 屬性;等價於執行以下的代碼,

var name = "windows";    
var obj = { name : "obj" };
(obj.foo = function () {
  console.log("this: " + this.name);
})();

輸出,

'this: windows'

能夠看到,_this_ -> _global object_,這裏爲何指向的是 _global object_?其實這裏的當即執行過程,就是執行的以下代碼,

(function () {
  console.log("this: " + this.name);
}());

由此能夠看出,實際上進行一個匿名函數的當即執行;也就是說執行過程當中並無使用 關聯調用_,而是一次 _standalone 函數的自身調用,因此根據法則 #4,_this_ -> _global object_。執行完之後,將該匿名函數賦值給 _obj.foo_。

再次執行,

> obj.foo();
 'this: obj'

此次執行的過程是一次標準的 關聯調用 過程,因此根據法則 #3,_this_ -> _obj_。

做爲判斷條件 - 匿名函數當即執行

> (false || obj.foo)() 
  'windows'

等價於執行,

(false || function () {
  console.log("this: " + this.name);
})()

原理和函數賦值變種-匿名 standalone 函數當即執行 一致,等價於當即執行以下的匿名函數

(function () {
  console.log("this: " + this.name);
})()

其實,把這個例子再作一個細微的更改,其中邏輯就看得更清楚了,爲 foo 函數添加一個返回值 return true

var name = "windows";
var obj ={
  name: "obj",
  foo: function () {
    console.log("this: "+ this.name);
    return true;
  }
};

再次執行,

> (false || obj.foo)() 
  'windows'
  true

可見,_obj.foo_ 函數執行之後,返回 _true_。上述代碼其實等價於執行以下的代碼,

(false || function () {
  console.log("this: " + this.name);
  return true;
})()

函數回調場景 0 - 基本原理

var counter = {
  count: 0,
  inc: function () {
    'use strict';
    this.count++;
  }
};

function callIt(callback) {
  callback();
}

> callIt(counter.inc)
  TypeError: Cannot read property 'count' of undefined

能夠看到,把一個定義有this關鍵字的函數做爲其它函數的回調函數,是危險的,由於this運行期會被從新賦值,上述例子很直觀的描述了這一點,之因此報錯,是由於this指向了 _Global Object_。要解決這樣的問題,可使用bind,調用的時候改成

> callIt(counter.inc.bind(counter))
  1

函數回調場景 1 - setTimeout

var name = "Bob";  
var nameObj ={  
    name : "Tom",  
    showName : function(){  
        console.log(this.name);  
    },  
    waitShowName : function(){  
        setTimeout(this.showName, 1000);  
    }  
};  

// 執行,

> nameObj.waitShowName();
  'Tom'
  undefined

setTimeout(this.showName, 1000);nameObj.showName 函數做爲回調函數參數傳遞給 setTimeout_;那麼爲何當 _setTimeout 執行回調的時候,_nameObj.showName_ 方法返回的是 undefined 呢?爲何不是返回全局對象對應的 name Bob_?緣由只有一個,那就是 _setTimeout 有本身的 this 對象,而它沒有 name 屬性,而在回調 showName 函數的時候,_showName_ 函數中的 this 正是 setTimeout 上下文中的 this_,而該 _this 並無定義 name 屬性,因此這裏返回 _undefined_。

函數回調場景 2 - DOM 對象

var o = new Object();

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

o.f() // true,獲得指望的結果 this -> o

可是,若是將f方法指定給某個click事件,this的指向發生了改變,

$('#button').on('click', o.f);

點擊按鈕之後,返回的是false,是由於在執行過程當中,this再也不指向對象o了而改成指向了按鈕的DOM對象了;Sounds Good,但問題是,怎麼被改動的?看了一下 jQuery 的源碼,_event.js_,摘錄重要的片斷以下,

function on( elem, types, selector, data, fn, one ) {
  .......
  if ( one === 1 ) {
    origFn = fn;
    fn = function( event ) {

      // Can use an empty set, since event contains the info
      jQuery().off( event );
      return origFn.apply( this, arguments );
    };

    // Use same guid so caller can remove using origFn
    fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
  }
  .......
}

o.f 函數的地址賦值給 fn 參數,_fn_ -> origFn_,最後是經過origFn.apply( this, arguments );來調用 _o.f 函數的,而這裏的 this 就是當前的 DOM 對象,既是這個按鈕 button_;經過這樣的方式,在執行過程當中,經過回調函數 _$("button").on(...) 成功的將新的 this 對象 button 注入了 o.f 函數。那麼如何解決呢?參看function.prototype.apply())
的小節#3,動態綁定回調函數。

函數回調場景 3 - 數組對象方法的回調

var obj = {
  name: '張三',
  times: [1, 2, 3],
  print: function () {
    this.times.forEach(function (n) {
      console.log(this.name);
    });
  }
};

> obj.print();
  'undefined'
  'undefined'
  'undefined'

這裏咱們指望的是,依次根據數組 times 的長度,輸出 obj.name 三次,可是實際運行結果是,數組雖然循環了三次,可是每次輸出都是 _undefined_,那是由於匿名函數

function(n){
  console.log(this.name);
}

做爲數組 times 的方法 forEach 的回調函數執行,在 forEach 方法內部該匿名函數必然是做爲 standalone 方法執行的,因此,this指向了 _Global Object_;

進一步,爲何「在 forEach 方法內部該匿名函數必然是做爲 standalone 方法執行的」?爲何必然是做爲 standalone 方法執行?是由於不能在 forEach 函數中使用 this.fn() 的方式來調用該匿名回調函數( fn 做爲參數引用該匿名回調函數 ),由於若是這樣作,在運行時期會報錯,由於在 forEach 函數的 this 對象中找不到 fn 這樣的屬性,而該 this 對象指向的是 obj.times 數組對象。所以,獲得結論「在 forEach 方法內部該匿名函數必然是做爲 standalone 方法執行的」

解決辦法,使用 bind

obj.print = function () {
  this.times.forEach(function (n) {
    console.log(this.name);
  }.bind(this));
};

> obj.print()
  '張三'
  '張三'
  '張三'

obj 對象做爲 this 綁定到該匿名函數上,而後再做爲回調函數參數傳遞給 forEach 函數,這樣,在 forEach 函數中,用 standalone 的方式調用 fn 的時候,_fn_ 中的 this 指向的就是數組對象 obj 對象,這樣,咱們就能順利的輸出 obj.name 了。

綁定 this

有上述描述可知,this的值在運行時根據不一樣上下文環境有不一樣的值,所以咱們說this的值是變化的,這就給咱們的編程帶來了麻煩,有時候,咱們指望,獲得一個固定的this。Javascript 提供了callapply以及bind這三個方法,來固定this的指向;這三個方法存儲在 function.prototype 域中,

function.prototype.call()

總結起來,就是解決函數在調用的時候,如何解決this動態變化的問題。

調用格式,

func.call(thisValue, arg1, arg2, ...)

第一個參數是在運行時用來賦值給 func 函數內部的 this 的。

經過f.call(obj)的方式調用函數,在運行時,將 obj 賦值給 _this_;

var obj = {};

var f = function () {
  return this;
};

f() === this // true
f.call(obj) === obj // true

call方法的參數是一個對象,若是參數爲 空_、_null 或者 _undefined_,則使用默認的全局對象;

var n = 123;
var obj = { n: 456 };

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

> a.call() 
  123
> a.call(null) 
  123
> a.call(undefined) 
  123
> a.call(window) 
  123
> a.call(obj) 
  456

若是call方法的參數是一個原始值,那麼這個原始值會自動轉成對應的包裝對象,而後賦值給 this

var f = function () {
  return this;
};

> f.call(5)
  [Number: 5]

call方法能夠接受多個參數,第一個參數就是賦值給 this 的對象,

var obj = {
    name : 'obj'
}

function add(a, b) {
  console.log(this.name);
  return a + b;
}

> add.call(obj, 1, 2) 
  obj
  3

call方法能夠調用對象的原生方法;

var obj = {};
obj.hasOwnProperty('toString') // false

// 「覆蓋」掉繼承的 hasOwnProperty 方法
obj.hasOwnProperty = function () {
  return true;
};
obj.hasOwnProperty('toString') // true

Object.prototype.hasOwnProperty.call(obj, 'toString') // false

方法 hasOwnProperty 是對象 objObject.prototype 中繼承的方法,若是一旦被覆蓋,就不會獲得正確的結果,那麼,咱們可使用call的方式調用原生方法,將 obj 做爲 this 在運行時調用,這樣,變通的,咱們就能夠調用 obj 對象所繼承的原生方法了。

function.prototype.apply()

總結起來,和call同樣,就是解決函數在調用的時候,如何解決this動態變化的問題。

apply方法的做用與 call方法相似,也是改變 this指向,而後再調用該函數。惟一的區別就是,它接收一個數組做爲函數執行時的參數,使用格式以下。
func.apply(thisValue, [arg1, arg2, ...])
apply方法的第一個參數也是 this所要指向的那個對象,若是設爲null或undefined,則等同於指定全局對象。第二個參數則是一個數組,該數組的全部成員依次做爲參數,傳入原函數。

原函數的參數,在call方法中必須一個個添加,可是在apply方法中,必須以數組形式添加

function f(x,y){
  console.log(x+y);
}

f.call(null,1,1) // 2
f.apply(null,[1,1]) // 2
  1. 找出數組最大的元素

    var a = [10, 2, 4, 15, 9];
    Math.max.apply(null, a)
    // 15
  2. 將數組的空元素變爲 undefined

    Array.apply(null, ["a",,"b"])
    // [ 'a', undefined, 'b' ]

    空元素undefined的差異在於,數組的forEach方法會跳過空元素,可是不會跳過undefined。所以,遍歷內部元素的時候,會獲得不一樣的結果。

    var a = ['a', , 'b'];
    
     function print(i) {
       console.log(i);
     }
    
     a.forEach(print)
     // a
     // b
    
     Array.apply(null, a).forEach(print)
     // a
     // undefined
     // b
  3. 綁定回調函數的對象
    函數回調場景-2咱們看到this被動態的更改成了 DOM 對象 _button_,這每每不是咱們所指望的,因此,咱們能夠再次綁定回調函數來固定this,以下,

    var o = new Object();
    
     o.f = function () {
       console.log(this === o);
     }
    
     var f = function (){
       o.f.apply(o);
       // 或者 o.f.call(o);
     };
    
     $('#button').on('click', f);

    這樣,咱們用 f 函數封裝原來的回調函數 o.f_,並使用apply方法固定住this,使其永遠指向 _object o,這樣,就達到了this不被動態修改的目的。

function.prototype.bind()

總結起來,其實就是在把函數做爲參數傳遞的時候,如何解決this動態變化的問題。

解決的問題

在認識關聯調用 - 容易混淆的場景中,咱們濃墨重彩的描述了將函數賦值之後,致使this在運行期發生變化的種種場景,並且在編程過程中,也是很是容易致使問題的場景;那麼有沒有這麼一種機制,即使是在函數賦值後,在運行期依然可以保護並固定住個人this?答案是有的,那就是bind。下面,咱們來看一個例子,

var d = new Date();
d.getTime() // 1481869925657

咱們使用語句 d.getTime() 經過對象 d 關聯調用函數 getTime()_,根據法則 #3,函數 _getTime() 內部的 this 指向的是對象 d_,而後從 _d 對象中成功獲取到了時間。可是,咱們稍加改動,將對象 d 中的函數 getTime 賦值給另一個變量,在執行呢?

var print = d.getTime;
print() // Uncaught TypeError: this is not a Date object.

Wow~, 畫風突變,得不到時間了,並且還拋出了一個程序異常,好玩,你的程序所以崩潰.. 這就是this在執行期動態變化所致使的,當咱們將函數 d.getTime 賦值給 print_,而後語句 _print() 表示將函數 getTime 做爲 standalone 的函數在運行期調用,因此,內部的this發生變化,指向了 _Global Object_,也所以,咱們得不到時間了,但咱們獲得一個意想不到的異常..

Ok, 別怕,孩子,bind登場了,

var print = d.getTime.bind(d);
print() // 148186992565

賦值過程當中_,將函數經過bind語法綁定this對象 _d 之後,再賦值給一個新的變量;這樣,即使 print() 再次做爲 standalone 的函數在運行期調用,this的指向也再也不發生變化,而是固定的指向了對象 _d_。

bind 是如何實現的

if(!('bind' in Function.prototype)){
  Function.prototype.bind = function(){
    var fn = this; // 當前調用 bind 的當前對象 fn ( fn.bind(..) )
    var context = arguments[0]; // 用來綁定 this 對象的參數
    var args = Array.prototype.slice.call(arguments, 1);
    var fnbound = function(){
      return fn.apply(context, args);
    }
    return fnbound;
  }
}

Function對象的prototype原型中新增一個屬性bind,該bind是一個 function 函數;這裏要特別特別注意,每次bind調用之後,返回的是一個新的function,

var fnbound = function(){
      return fn.apply(context, args);
    }
    return fnbound;

經過 fnbound 函數套一層原函數 fn 做爲閉包,而後返回這個新的 function _fnbound_;大部分教程就是這樣介紹即止了;其實,我想問的是,爲何bind要這麼設計,直接返回fn.apply(context, args);不是挺好嗎?爲何還要在外面套一層新函數 _fnbound_?Ok,這裏我就來試圖解釋下緣由吧;

採用反證法,若是,咱們不套這麼一層新函數 _fubound_,看看,會怎樣?因而,咱們獲得以下的實現,

if(!('bind' in Function.prototype)){
  Function.prototype.bind = function(){
    var fn = this; // 當前調用 bind 的當前對象 fn ( fn.bind(..) )
    var context = arguments[0]; // 用來綁定 this 對象的參數
    var args = Array.prototype.slice.call(arguments, 1);
    return fn.apply(context, args);
  }
}

直接返回fn.apply(context, args),oh,頓時,我明白了,fn.apply(...)這是一條執行命令啊,它會當即執行 fn_,將 _fn 執行的結果返回.. 而咱們這裏的bind的初衷只是擴充 fn 函數的行爲(既綁定this對象),而後返回一個函數的引用,而正式由於咱們沒法在綁定之後,直接返回原有函數的引用,因此,這裏,咱們才須要建立一個新的函數並返回這個新的函數的引用,已達到bind的設計目的。Ok,這下總算是清楚了。

特性

綁定匿名函數
obj.print = function () {
  this.times.forEach(function (n) {
    console.log(this.name);
  }.bind(this));
};

可見,咱們能夠直接改匿名函數執行bind,而後在將其賦值給某個對象;更詳細的用例參考函數回調場景 3 - 數組對象方法的回調

做爲函數直接調用
var altwrite = document.write;
altwrite("hello");

在瀏覽器運行這個例子,獲得錯誤Uncaught ReferenceError: alwrite is not defined,這個錯誤並無真正保留底層的緣由,真正的緣由是,_document_ 對象的 write 函數再執行的時候,內部this指向了 Global Object

爲了解決上述問題,咱們能夠bind document 對象,

altwrite.bind(document)("hello")

注意這裏的寫法,altwrite.bind(document)返回的是一個Function,因此能夠直接跟參數調用。

綁定函數參數

除了綁定this對象意外,還能夠綁定函數中的參數,看以下的例子,

var add = function (x, y) {
  return x * this.m + y * this.n;
}

var obj = {
  m: 2,
  n: 2
};

var newAdd = add.bind(obj, 5);

newAdd(5);
// 20

add.bind(obj, 5);除了綁定 add 函數的this對象爲 obj 之外,將其固定obj 之外,還綁定了 add 函數的第一個參數 x_,並將其固定爲 _5_;這樣,獲得的 _newAdd 函數只能接收一個參數,那就是 y 了,由於 x 已經被bind綁定且固定了,因此能夠看到,隨後執行的語句newAdd(5)傳遞的其實是 y 參數。

若綁定 null 或者 undefined

若是bind方法的第一個參數是 null 或 _undefined_,等於將this綁定到全局對象,函數運行時this指向 _Global Object_。

var name = 'windows';

function add(x, y) {
  console.log(this.name);
  return x + y;
}

var plus = add.bind(null, 5); // 綁定了 x 參數

> plus(10) // 賦值的是 y 參數,因而執行的是 5 + 10
  'windows'
  15
改寫原生方法的使用方式

首先,

> [1, 2, 3].push(4)
  4 // 輸出新增後數組的長度

等價於

Array.prototype.push.call([1, 2, 3], 4)

第一個參數 [1, 2, 3] 綁定 push 函數的this關鍵字,第二個參數 _4_,是須要被添加的值。

補充一下

爲何說這裏是等價的?咱們來解讀一下

> [1, 2, 3].push(4)
  4 // 輸出新增後數組的長度

的執行過程,_[1, 2, 3]_ 做爲數組對象,調用其原型中的 Array.prototype.push 方法,很明顯,採用的是關聯調用,所以 push 函數內部的 this 指向的是數組對象 _[1, 2, 3]_;而這裏,咱們經過

Array.prototype.push.call([1, 2, 3], 4)

這樣的調用方式,只是換湯不換藥,一樣是執行的數組中的原型方法 _push_,只是this的傳遞方式不一樣而已,這裏是經過bind直接將this賦值爲數組對象 _[1, 2, 3]_,而不是經過以前的關聯調用;因此,兩種調用方式是等價的。

補充完畢

再次,

call 方法調用的是 Function 對象的原型方法既 Function.prototype.call(...)_,那麼咱們再來將它 _bind 一下,看看會有什麼結果

> var push = Function.prototype.call.bind(Array.prototype.push);

> push([1, 2, 3], 4);
  4 // 返回數組長度

// 或者寫爲

> var a = [1, 2, 3];
> push(a, 4);
  4
> a
  [1, 2, 3, 4]

咱們獲得了一個具有數組 push 操做的一個新的函數 push(...) ( 注: bind 每次回返回一個新的函數 );

那是爲何呢?

能夠看到,背後的核心是,

push([1, 2, 3], 4);

等價於執行

Array.prototype.push.call([1, 2, 3], 4)

因此,咱們得證實Function.prototype.call.bind(Array.prototype.push)([1, 2, 3], 4)Array.prototype.push.call([1, 2, 3], 4)兩個函數的執行過程是等價的( 注意,爲何比較的是執行過程等價,由於call函數是當即執行的,而bind返回的是一個函數引用,因此必須比較二者的執行過程 );其實,要證實這個問題,最直接方法就是去查看函數Function.prototype.call的源碼,惋惜,我在官網 MDN Function.prototype.call() 上面也沒有看到源碼;那麼這裏,其實能夠作一些推理,

Function.prototype.call.bind(Array.prototype.push)([1, 2, 3], 4)

經過bind,這裏返回一個新的 call 函數,該函數綁定了 Array.prototype.push Function 對象作爲其this對象;那麼Function.prototype.call函數內部會怎麼執行呢?我猜測應該就是執行this.apply(context, params)之類的,this表示的是 Array.prototype.push_,context表示的既是這裏的數組對象 _[1, 2, 3]_, params表示的既是這裏的參數 _4

Array.prototype.push.call([1, 2, 3], 4)
同理,由上述Function.prototype.call函數內部的執行過程是執行this.apply(context, params)的推斷來看,this依然是指向的 Array.prototype.push_,context表示的既是這裏的數組對象 _[1, 2, 3]_, params表示的既是這裏的參數 _4_;因此,這裏的調用方式與 _Function.prototype.call.bind(Array.prototype.push)([1, 2, 3], 4) 的方式等價;因此,咱們得出以下結論,
Array.prototype.push.call([1, 2, 3], 4) <=> Function.prototype.call.bind(Array.prototype.push)([1, 2, 3], 4) <=> push([1, 2, 3], 4)

使用 bind 的一些注意事項

每次返回一個新函數

bind方法每運行一次,就返回一個新函數,這會產生一些問題。好比,監聽事件的時候,不能寫成下面這樣。

element.addEventListener('click', o.m.bind(o));

上面代碼中,click 事件綁定bind方法新生成的一個匿名函數。這樣會致使沒法取消綁定,因此,下面的代碼是無效的。

element.removeEventListener('click', o.m.bind(o));

正確的方法是寫成下面這樣,使得 addremove 使用的是同一個函數的引用。

var listener = o.m.bind(o);
element.addEventListener('click', listener);
//  ...
element.removeEventListener('click', listener);

use strict

使用嚴格模式,該部分能夠參考阮一峯的教程嚴格模式,說得很是詳細;不過應用到面向對象編程裏面,主要就是爲了不this運行期動態指向 _Global Object_,若是發生這類的狀況,報錯;例如

function f() {
  'use strict';
  this.a = 1;
};

f();// 報錯,this未定義

當執行過程當中,發現函數 f 中的this指向了 _Global Object_,則報錯。

構造函數中的 this

this -> Object.prototype instance

構造函數比較特別,_javascript_ 解析過程不一樣於其它普通函數;

假如咱們有以下的構造函數,

var Person = function(name, age){
   this.name = name;
   this.age = age;
}

javascript 語法解析器解析到以下語句之後,

var p = new Person('張三', 35);

實際上執行的是,

function new( /* 構造函數 */ constructor, /* 構造函數參數 */ param1 ) {
  // 將 arguments 對象轉爲數組
  var args = [].slice.call(arguments);
  // 取出構造函數
  var constructor = args.shift();
  // 建立一個空對象,繼承構造函數的 prototype 屬性
  var context = Object.create(constructor.prototype);
  // 執行構造函數
  var result = constructor.apply(context, args);
  // 若是返回結果是對象,就直接返回,則返回 context 對象
  return (typeof result === 'object' && result != null) ? result : context;
}

備註:_arguments_ 可表示一個函數中全部的參數,也就是一個函數全部參數的結合。

下面,咱們一步一步的來分析該構造函數的實現,弄清楚this指的是什麼,

constructor

就是 Person 構造函數,

context

var context = Object.create(constructor.prototype);經過 constructor.prototype 建立了一個新的對象,也就是 Person.prototype 的一個實例 _Person.prototype isntance_;

constructor.apply(context, args);

注意,這步很是關鍵,_context_ 做爲 constructor 構造函數的this,因此

var Person = function(name, age){
   this.name = name;
   this.age = age;
}

中的this在執行過程當中指向的實際上就是該 context 對象。

result

constructor.apply(context, args);方法調用的返回值,咱們當前用例中,_Person_ 構造函數並無返回任何東西,因此,這裏是 _null_。

return (typeof result === 'object' && result != null) ? result : context;

new方法的最後返回值,若是 result 不爲 null_,則返回 _result 不然返回的是 context_;咱們這個用例,當初始化構造函數完成之後,返回的是 _context 既 _Person.prototype instance_,也就是構造函數中的this指針;這也是大多數構造函數應用的場景。

Object.prototype instance -> Object.prototype

var Obj = function (p) {
  this.p = p;
};

Obj.prototype.m = function() {
  return this.p;
};

執行,

> var o = new Obj('Hello World!');

> o.p 
  'Hello World!'

> o.m() 
  'Hello World!'

說實話,當我第一次看到這個例子的時候,_o.p_ 還好理解,_o_ 就是表示構造函數 Obj 內部的this對象,是一個經過 Object.create(Obj.prototype) 獲得的一份 Obj.prototype 的實例對象;可是,當我看到 o.m 的時候,仍是有點懵逼,_Obj.prototype_ 並非表明的this呀,_Object.create(Obj.prototype)_ 纔是( 既 Obj.prototype instance ),因此在 Obj.prototype 上定義的 m 方法,怎麼能夠經過 o.m() 既經過 Obj.prototype instance 來調用呢?( 注意,關係 o -> Object.create(Obj.prototype) -> Obj.prototype instance -> this != Obj.prototype ) 當理解到 prototype 的涵義有,才知道,_Obj.prototype instance_ 會繼承 Obj.prototype 中的公共屬性的,因此,這裏經過 Obj.prototype 對象定義的 m 函數能夠經過 Object.prototype instance 進行調用。

References

本文轉載自筆者的私人博客,傷神的博客,http://www.shangyang.me/2017/...
[Javascript中this關鍵字詳解](
http://www.cnblogs.com/justan...
jQuery Fundamentals Chapter - The this keyword

相關文章
相關標籤/搜索