javascript:this 關鍵字

前言

看過[阮一峯]()的關於 this 的教程,講了很多比較好的例子,但沒有對其本質的東西解釋清楚,而且部分例證存在問題;於是,打算重寫本章節,從this的本質入手;本文轉載自筆者的私人博客 http://www.shangyang.me/2017/03/24/javascript-lang-this/

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

this 是什麼?

this可以理解爲一個指針,指向調用對象;

判斷 this 是什麼的四個法則

官網定義

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

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

再來看另外一句非常精煉的描述,來加深理解

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的,包括 ObjectFunction 等。

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()](#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 域中,
javascript:this 關鍵字

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方法的第一個參數是 nullundefined,等於將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.pushcontext表示的既是這裏的數組對象 [1, 2, 3], params表示的既是這裏的參數 4

Array.prototype.push.call([1, 2, 3], 4)
同理,由上述Function.prototype.call函數內部的執行過程是執行this.apply(context, params)的推斷來看,this依然是指向的 Array.prototype.pushcontext表示的既是這裏的數組對象 [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;我們這個用例,當初始化構造函數完成以後,返回的是 contextPerson.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

Javascript中this關鍵字詳解
jQuery Fundamentals Chapter - The this keyword

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章