大話javascript 6期:this深度解析

一、引言

在執行上下文的創建階段,會分別生成變量對象,建立作用域鏈,確定this指向。this的指向,是在函數被調用的時候確定的。也就是執行上下文被創建時確定的。因此,一個函數中的this指向,可以是非常靈活的

二、this對象的定義

this對象代表函數運行時,自動生成的一個內部對象,只能在函數內部使用
在全局執行環境中(在任何函數體外部)this 都指向全局對象。
在函數內部,this的值取決於函數被調用的方式。

關鍵點:

  1. this永遠指向一個對象,並且擁有着個對象的值
  2. 在嚴格模式下,在全局作用域中和匿名函數中,this指向undefined
  3. this在一個函數內出現的時候,this指向調用這個函數的對象

三、this指向

全局環境

無論是否在嚴格模式下,在全局執行環境中(在任何函數體外部)this 都指向全局對象
// 在瀏覽器中, window 對象同時也是全局對象:
console.log(this === window); // true

a = 37;
console.log(window.a); // 37

this.b = "MDN";
console.log(window.b)  // "MDN"
console.log(b)         // "MDN"

函數(運行)環境

在函數內部,this的值取決於函數被調用的方式。

1.對象方法調用模式

當函數作爲對象裏的方法被調用時, this 指向調用該方法的對象

如果函數作爲一個對象的屬性方法,並且被調用的時候,那麼這個屬性方法中的this 就指向這個對象
下面的例子中,當 o.f()被調用時,函數內的this將綁定到o對象

var o = {
  prop: 37,
  f: function() {
    return this.prop;
  }
};

console.log(o.f()); // logs 37

請注意,這樣的行爲,根本不受函數定義方式或位置的影響。在前面的例子中,我們在定義對象o的同時,將函數內聯定義爲成員 f 。但是,我們也可以先定義函數,然後再將其附屬到o.f。這樣做會導致相同的行爲:

var o = {prop: 37};

function independent() {
  return this.prop;
}

o.f = independent;

console.log(o.f()); // logs 37

這表明函數是從o的f成員調用的纔是重點。

同樣,this 的綁定只受最靠近的成員引用的影響。在下面的這個例子中,我們把一個方法g當作對象o.b的函數調用。在這次執行期間,函數中的this將指向o.b。事實證明,這與他是對象 o 的成員沒有多大關係,最靠近的引用纔是最重要的。

o.b = {g: independent, prop: 42};
console.log(o.b.g()); // 42

2.函數調用模式

如果是普通函數調用方式。非嚴格模式下,this指向window,嚴格模式下,thisundefined

非嚴格模式下,this 的值默認指向全局對象。在瀏覽器中,全局對象是window

function f1(){
  return this;
}
//在瀏覽器中:
f1() === window;   //在瀏覽器中,全局對象是window

//在Node中:
f1() === global;

在嚴格模式下,this將保持他進入執行環境時的值,所以下面的this將會默認爲undefined。

function f2(){
  "use strict"; // 這裏是嚴格模式
  return this;
}

f2() === undefined; // true

所以,在嚴格模式下,如果 this 沒有被執行環境(execution context)定義,那它將保持爲 undefined。

3.構造函數調用模式

如果是構造函數調用方式,this指向實例化出來的新對象

/*
 * 構造函數這樣工作:
 *
 * function MyConstructor(){
 *   // 函數實體寫在這裏
 *   // 根據需要在this上創建屬性,然後賦值給它們,比如:
 *   this.fum = "nom";
 *   // 等等...
 *
 *   // 如果函數具有返回對象的return語句,
 *   // 則該對象將是 new 表達式的結果。 
 *   // 否則,表達式的結果是當前綁定到 this 的對象。
 *   //(即通常看到的常見情況)。
 * }
 */


function C(){
  this.a = 37;
}

var o = new C();
console.log(o.a); // logs 37

雖然構造器返回的默認值是this所指的那個對象,但它仍可以手動返回其他的對象(如果返回值不是一個對象,則返回this對象)。

function C2(){
  this.a = 37;
  return {a:38};
}

o = new C2();
console.log(o.a); // logs 38

在剛剛的例子中(C2),因爲在調用構造函數的過程中,手動的設置了返回對象,與this綁定的默認對象被丟棄了。(這基本上使得語句 “this.a = 37;”成了“殭屍”代碼,實際上並不是真正的“殭屍”,這條語句執行了,但是對於外部沒有任何影響,因此完全可以忽略它)。

4.call / apply 調用模式

call()apply()方式調用,this指向被綁定的對象

如果要想把 this 的值從一個環境傳到另一個,就要用 call 或者apply 方法。

// 將一個對象作爲call和apply的第一個參數,this會被綁定到這個對象。
var obj = {a: 'Custom'};

// 這個屬性是在global對象定義的。
var a = 'Global';

function whatsThis(arg) {
  return this.a;  // this的值取決於函數的調用方式
}

whatsThis();          // 'Global'
whatsThis.call(obj);  // 'Custom'
whatsThis.apply(obj); // 'Custom'

當一個函數在其主體中使用 this 關鍵字時,可以通過使用函數繼承自Function.prototypecallapply 方法將 this 值綁定到調用中的特定對象

function add(c, d) {
  return this.a + this.b + c + d;
}

var o = {a: 1, b: 3};

// 第一個參數是作爲‘this’使用的對象
// 後續參數作爲參數傳遞給函數調用
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16

// 第一個參數也是作爲‘this’使用的對象
// 第二個參數是一個數組,數組裏的元素用作函數調用中的參數
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34

使用 call 和 apply 函數的時候要注意,如果傳遞給 this 的值不是一個對象,JavaScript 會嘗試使用內部 ToObject 操作將其轉換爲對象。因此,如果傳遞的值是一個原始值比如 7 或 'foo',那麼就會使用相關構造函數將它轉換爲對象,所以原始值 7 會被轉換爲對象,像 new Number(7) 這樣,而字符串 'foo' 轉化成 new String('foo') 這樣,例如:

function bar() {
  console.log(Object.prototype.toString.call(this));
}

//原始值 7 被隱式轉換爲對象
bar.call(7); // [object Number]

5.bind方法調用

bind()方式調用,this指向被綁定的對象

ECMAScript 5 引入了 Function.prototype.bind。調用f.bind(someObject)會創建一個與f具有相同函數體和作用域的函數,但是在這個新函數中,this永久地被綁定到了bind的第一個參數,無論這個函數是如何被調用的。

function f(){
  return this.a;
}

var g = f.bind({a:"azerty"});
console.log(g()); // azerty

var h = g.bind({a:'yoo'}); // bind只生效一次!
console.log(h()); // azerty

var o = {a:37, f:f, g:g, h:h};
console.log(o.f(), o.g(), o.h()); // 37, azerty, azerty

6.箭頭函數

在箭頭函數中,this與封閉詞法環境的this保持一致。在全局代碼中,它將被設置爲全局對象:

如果是箭頭函數,是根據當前的詞法作用域來決定this, 具體來說,箭頭函數會繼承外層函數調用的this綁定

var globalObject = this;
var foo = (() => this);
console.log(foo() === globalObject); // true

注意:如果將this傳遞給call、bind、或者apply,它將被忽略。不過你仍然可以爲調用添加參數,不過第一個參數(thisArg)應該設置爲null。

// 接着上面的代碼
// 作爲對象的一個方法調用
var obj = {foo: foo};
console.log(obj.foo() === globalObject); // true

// 嘗試使用call來設定this
console.log(foo.call(obj) === globalObject); // true

// 嘗試使用bind來設定this
foo = foo.bind(obj);
console.log(foo() === globalObject); // true

無論如何,foo 的 this 被設置爲他被創建時的環境(在上面的例子中,就是全局對象)。這同樣適用於在其他函數內創建的箭頭函數:這些箭頭函數的this被設置爲封閉的詞法環境的。

// 創建一個含有bar方法的obj對象,
// bar返回一個函數,
// 這個函數返回this,
// 這個返回的函數是以箭頭函數創建的,
// 所以它的this被永久綁定到了它外層函數的this。
// bar的值可以在調用中設置,這反過來又設置了返回函數的值。
var obj = {
  bar: function() {
    var x = (() => this);
    return x;
  }
};

// 作爲obj對象的一個方法來調用bar,把它的this綁定到obj。
// 將返回的函數的引用賦值給fn。
var fn = obj.bar();

// 直接調用fn而不設置this,
// 通常(即不使用箭頭函數的情況)默認爲全局對象
// 若在嚴格模式則爲undefined
console.log(fn() === obj); // true

// 但是注意,如果你只是引用obj的方法,
// 而沒有調用它
var fn2 = obj.bar;
// 那麼調用箭頭函數後,this指向window,因爲它從 bar 繼承了this。
console.log(fn2()() == window); // true

在上面的例子中,一個賦值給了 obj.bar的函數(稱爲匿名函數 A),返回了另一個箭頭函數(稱爲匿名函數 B)。因此,在 A 調用時,函數B的this被永久設置爲obj.bar(函數A)的this。當返回的函數(函數B)被調用時,它this始終是最初設置的。在上面的代碼示例中,函數B的this被設置爲函數A的this,即obj,所以即使被調用的方式通常將其設置爲 undefined全局對象(或者如前面示例中的其他全局執行環境中的方法),它的 this 也仍然是 obj

7.原型鏈的this

在對象原型鏈上某處定義的方法,this指向的是調用這個方法的對象

對於在對象原型鏈上某處定義的方法,同樣的概念也適用。如果該方法存在於一個對象的原型鏈上,那麼this指向的是調用這個方法的對象,就像該方法在對象上一樣。

var o = {
  f: function() { 
    return this.a + this.b; 
  }
};
var p = Object.create(o);
p.a = 1;
p.b = 4;

console.log(p.f()); // 5

在這個例子中,對象p沒有屬於它自己的f屬性,它的f屬性繼承自它的原型。雖然在對 f 的查找過程中,最終是在 o 中找到 f 屬性的,這並沒有關係;查找過程首先從 p.f的引用開始,所以函數中的 this 指向p。也就是說,因爲f是作爲p的方法調用的,所以它的this指向了p。這是 JavaScript 的原型繼承中的一個有趣的特性。

8.getter 與 setter 中的 this

當函數在一個 getter 或者 setter 中被調用。用作 getter 或 setter 的函數都會把 this
綁定到設置或獲取屬性的對象
function sum() {
  return this.a + this.b + this.c;
}

var o = {
  a: 1,
  b: 2,
  c: 3,
  get average() {
    return (this.a + this.b + this.c) / 3;
  }
};

Object.defineProperty(o, 'sum', {
    get: sum, enumerable: true, configurable: true});

console.log(o.average, o.sum); // logs 2, 6

9.作爲一個DOM事件處理函數

當函數被用作事件處理函數時,它的this指向觸發事件的元素(一些瀏覽器在使用非addEventListener的函數動態添加監聽函數時不遵守這個約定)。
// 被調用時,將關聯的元素變成藍色
function bluify(e){
  console.log(this === e.currentTarget); // 總是 true

  // 當 currentTarget 和 target 是同一個對象時爲 true
  console.log(this === e.target);        
  this.style.backgroundColor = '#A5D9F3';
}

// 獲取文檔中的所有元素的列表
var elements = document.getElementsByTagName('*');

// 將bluify作爲元素的點擊監聽函數,當元素被點擊時,就會變成藍色
for(var i=0 ; i<elements.length ; i++){
  elements[i].addEventListener('click', bluify, false);
}

10.作爲一個內聯事件處理函數

當代碼被內聯on-event 處理函數調用時,它的this指向監聽器所在的DOM元素
<button onclick="alert(this.tagName.toLowerCase());">
  Show this
</button>

上面的 alert 會顯示button。注意只有外層代碼中的this是這樣設置的:

<button onclick="alert((function(){return this})());">
  Show inner this
</button>

在這種情況下,沒有設置內部函數的this,所以它指向 global/window 對象(即非嚴格模式下調用的函數未設置this時指向的默認對象)。

11.作爲定時器的參數

作爲定時器的參數時, this 指向 window
setInterval(function() {
    console.log(this);
}, 1000);



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