JavaScript的this機制很複雜,雖然從一開始從事前端工做就和它打交道,但一直未能弄清楚,道明白。在工做中遇到this相關問題,就知道
var self = this
,一旦去面試遇到各類this相關面試題目時腦子就一片空白,拿不定結果。本文綜合了一些書籍和網上文章對this的分析和講解,提供一些實例來分析各類場景下this是如何指向的。javascript
在瀏覽器宿主環境中,this
指向window
對象,而且在全局做用域下,使用var
聲明變量其實就至關於操做全局this
。html
this === window; // true var foo = 'bar'; this.foo === window.foo; // true
在嚴格模式下,this
會綁定到undefined
。前端
var a = 2; function foo() { 'use strict'; console.log(this.a); } foo(); // TypeError: this is not undefined
若是在變量的聲明過程沒有使用let
或者var
,會隱式建立一個全局變量,但這個變量和普通全局變量的區別在於它是做爲window
的一個屬性建立的。兩者在使用delete
操做符上有明顯的區別:變量不能夠刪除,而對象的屬性是能夠刪除的java
var a = 2; b = 3; a; // 2 b; // 3 delete a; delete b; a; // 2 b; // Uncaught ReferenceError: b is not defined
這裏的做用域主要是指在對象、函數中的this
指向。面試
做爲函數調用時,函數中的this
默認指向window
。瀏覽器
var a = 1; function foo() { console.log(this.a); } foo(); // 1
若是在當即執行函數中使用了this
,它一樣指向window
。app
var a = 1; (function() { var a = 2; console.log(this.a); })(); // 1
做爲方法調用時,函數中的this
老是指向方法所在的對象。函數
var obj = { a: 1, foo: function() { console.log(this.a); } } obj.foo();
構造函數調用將一個全新的對象做爲this
變量的值,並隱式返回這個新對象做爲調用結果。也就是說指向新生成的實例。this
function Foo(name) { this.name = name; this.getName = function() { console.log(this.name); } } var a = new Foo('a'); a.getName(); // "a"
能夠經過call()
和apply()
方法顯示改變函數的this
指向。prototype
var a = 1; var obj = { a: 2 } function foo() { console.log(this.a); } foo(); // 1 foo.call(obj); // 2 foo.apply(obj); // 2
bind()方法建立一個新的函數, 當被調用時,將其this關鍵字設置爲提供的值,在調用新函數時,在任何提供以前提供一個給定的參數序列,而後返回由指定的this值和初始化參數改造的原函數拷貝。
var a = 1; var obj = { a: 2 } function foo() { console.log(this.a); } var bar = foo.bind(obj); bar();
ES6引入了箭函數的概念,在箭頭函數中因爲沒有this
綁定,因此它的默認指向是由外圍最近一層非箭頭函數決定的。
var a = 1; function Foo(a) { this.a = a; this.getA = function() { var x = () => { this.a = 3; // 改變了外圍函數Foo屬性a的值 console.log(this.a); // 3 } x(); console.log(this.a); // 3 } } var foo = new Foo(1); foo.getA();
上面列舉了在正常狀況下this
的指向結果。可是在實際開發過程當中,對於不一樣場景,不一樣的聲明方式、調用方式、賦值和傳值方式都會影響到this
的具體指向。
函數的調用方式最多見的是方法調用、構造函數調用,或者使用apply/bind/call
調用,也能夠是當即執行函數。
var a = 10; var obj = { a: 20, fn: function() { var a = 30; console.log(this.a); } } obj.fn(); // 20 obj.fn.call(); // 10 (obj.fn)(); // 20 (obj.fn, obj.fn)(); // 10 (obj.fn = obj.fn)(); // 10 new obj.fn(); // undefined
對於apply
和call
第一個參數若是不傳或者傳遞undefined
和null
則默認綁定到全局對象,因此obj.fn.call()
的調用實際上把this
指向了window
對象。
對於(obj.fn)()
,咋一看,是當即執行函數,那麼它的this
確定指向了window
對象,其實否則,這裏obj.fn
只是一個obj
對象方法的引用,並無改變this
的指向。
對於(obj.fn, obj.fn)()
,這種操做比較少見,工做中也不會去這樣寫。這裏首先咱們須要瞭解逗號操做符會對每一個操做數求值,並返回最後一個操做數的值,其次是這裏使用了逗號操做符,裏面必然是一個表達式,這種狀況下里面的函數this
指向其實已經改變了,指向了全局。對於(obj.fn = obj.fn)()
中this
一樣指向全局。所以能夠大膽猜想:若是(x)();中x
是一個表達式,而且返回一個函數(引用),那麼函數x
中的this
指向全局window
。這裏還更多的方式來達到一樣目的,好比:(true && obj.fn)()
或者 (false || obj.fn)()
。總的來講,咱們經過這種方式建立了一個函數的「間接引用」,從而致使函數綁定規則的改變。
對於new obj.fn()
的結果其實也沒有什麼好說的,函數使用new
操做符調用後返回一個新的實例對象,因爲該對象並無一個叫a
的屬性,因此返回undefined
。
不少時候,函數的定義在一個地方,而對象定義方法時只是引用了該函數。一樣在調用對象方法時,先把它賦值給一個變量(別名),而後使用函數別名進行調用。使用時有可能致使this
綁定的改變。
var a = 10; function foo() { console.log(this.a); } var obj = { a: 20, foo: foo } var bar = obj.foo; // 函數別名 bar(); // 10
雖然bar
是obj.foo
的一個引用,可是實際上,它引用的是foo
函數自己,所以應用了函數的默認綁定規則。
var a = 10; function foo() { console.log(this.a); } function doFoo(cb) { cb(); // cb 實際上引用的仍是foo } var obj = { a: 20, foo: foo } doFoo(obj.foo); // 10 setTimeout(obj.foo, 100); // 10
這裏咱們將obj.foo
以參數的形式傳遞給函數doFoo
和內置函數setTimeout
。參數傳遞實際上就是一種賦值,和示例一的結果是同樣的。所以,調用回調函數的函數會丟失this的指向。
構造函數使用new
操做符調用後會返回一個新的實例對象,可是在定義構造函數時,能夠在函數中返回任何值來覆蓋默認該返回的實例,這樣一來極可能致使實例this
的指向改變。
var a = 10; function f() { this.a = 20; function c() { console.log(this.a); } return c(); } new f(); // 10
這裏咱們將構造函數foo
的默認返回值改爲返回一個函數c
執行後的結果。當調用new f()
後,內部函數c
中的this
實際上指向的是全局。可是若是咱們將return c()
改爲return new c()
的話,那麼new foo()
執行的結果是返回一個構造函數c
的實例,因爲實例對象中並無屬性a
,所以結果爲undefined
。
在方法的調用中由調用表達式自身來肯定this
變量的綁定。綁定的this
變量的對象被稱爲調用接收者。
var buffer = { entries: [], add: function(s) { this.entries.push(s); }, concat: function() { return this.entries.join(''); } } var source = ['123', '-', '456']; source.forEach(buffer.add); // Uncaught TypeError: Cannot read property 'push' of undefined
因爲方法buffer.add()
的接收者不是buffer
自己,而是forEach
方法。事實上,forEach
方法的實現使用全局對象做爲默認的接收者。因爲全局沒有entries
屬性,所以會拋出一個錯誤。
要解決上面的問題,一個是使用forEach
方法提供的可選參數做爲函數的接收者。
source.forEach(buffer.add, buffer);
其次是使用bind
方法來指定接收者
source.forEach(buffer.add.bind(buffer));
這裏想要說明的是,在一個對象的實例中,this
便可以訪問實例對象的值,也能夠獲取原型上的值。
function Foo() {} Foo.prototype.name = 'bar'; Foo.prototype.logName = function() { console.log(this.name); } Foo.prototype.setName = function(name) { this.name = name; } Foo.prototype.deleteName = function() { delete this.name; } var foo = new Foo(); foo.setName('foo'); foo.logName(); // "foo" foo.deleteName(); foo.logName(); // "bar" delete foo.name; foo.logName(); // "bar"
當執行foo.setName('foo')
後,給實例對象foo
增長了一個屬性name
,同時覆蓋了原型中的同名屬性。當執行foo.deleteName()
時,其實是將新增值刪除了,還原了初始狀態。執行delete foo.name
時,試圖刪除的仍是新增的屬性,可是如今已經不存在這個值了。若是須要刪除原始值,能夠經過delete foo.__proto__.name
來實現。