原文連接 - http://www.jianshu.com/p/d647aa6d1ae6javascript
首先,瞭解下執行上下文的生命週期。java
在執行上下文的建立階段,會分別生成變量對象,創建做用域鏈,以及肯定this指向。
其中this的指向,是在函數被調用的時候肯定的。也就是執行上下文被建立時肯定的。
因此,同一個函數因爲調用方式的不一樣,this指向了不同的對象。node
var a = 10; var obj = { a: 20 } function fn () { console.log(this.a); } fn(); // 10 fn.call(obj); // 20
另外一個要注意的地方是,在函數執行過程當中,this一旦被肯定,就不可更改了。數組
var a = 10; var obj = { a: 20 } function fn () { this = obj; // 這句話試圖修改this,運行後會報錯 console.log(this.a); } fn();
關於全局對象的this,是一個比較特殊的存在。全局環境中的this,指向window自己。所以,這也相對簡單,沒有那麼多複雜的狀況須要考慮。閉包
// 經過this綁定到全局對象 this.a2 = 20; // 經過聲明綁定到變量對象,但在全局環境中,變量對象就是它自身 var a1 = 10; // 僅僅只有賦值操做,標識符會隱式綁定到全局對象 a3 = 30; // 輸出結果會所有符合預期 console.log(a1, a2, a3); console.log(window.a1, window.a2, window.a3);
在總結函數中this指向以前,我想咱們有必要經過一些奇怪的例子,來感覺一下函數中this的捉摸不定。app
// demo01 var a = 20; function fn() { console.log(this.a); } fn();
// demo02 var a = 20; function fn() { function foo() { console.log(this.a); } foo(); } fn();
// demo03 var a = 20; var obj = { a: 10, c: this.a + 20, fn: function () { return this.a; } } console.log(obj.c); console.log(obj.fn());
在一個函數上下文中,this由調用者提供,由調用函數的方式來決定。
若是調用者函數,被某一個對象所擁有,那麼該函數在調用時,內部的this指向該對象。
在嚴格模式下,若是函數獨立調用,那麼該函數內部的this,則指向undefined。
在非嚴格模式下,當this指向undefined時,它會被自動指向全局對象。函數
從結論中咱們能夠看出,想要準確肯定this指向,找到函數的調用者以及區分他是不是獨立調用就變得十分關鍵。學習
// 爲了可以準確判斷,咱們在這裏使用嚴格模式,由於非嚴格模式會自動指向全局 'use strict'; function fn() { console.log(this); } fn(); // fn是調用者,獨立調用,undefined window.fn(); // fn是調用者,被window所擁有,window
可是須要特別注意的是,在上面的demo03中,對象obj中的c屬性使用this.a + 20
來計算,而他的調用者obj.c
並不是是一個函數。所以不適用於上面的規則,咱們要對這種方式單獨下一個結論。this
當obj在全局聲明時,不管obj.c在什麼地方調用,這裏的this都指向全局對象。spa
當obj在函數環境中聲明時,這個this指向undefined,在非嚴格模式下,會自動轉向全局對象**
'use strict'; var a = 20; function foo () { var a = 1; var obj = { a: 10, c: this.a + 20, fn: function () { return this.a; } } return obj.c; } console.log(foo()); // error
實際開發中,並不推薦這樣使用this
再來看一些容易理解錯誤的例子,加深一下對調用者與是否獨立運行的理解。
var a = 20; var foo = { a: 10, getA: function () { return this.a; } } console.log(foo.getA()); // 10 var test = foo.getA; console.log(test()); // 20
foo.getA()
中,getA是調用者,他不是獨立調用,被對象foo所擁有,所以它的this指向了foo。而test()做爲調用者,儘管他與foo.getA
的引用相同,可是它是獨立調用的,所以this指向undefined,在非嚴格模式,自動轉向全局window。
稍微修改一下代碼,請自行理解。
var a = 20; function getA() { return this.a; } var foo = { a: 10, getA: getA } console.log(foo.getA()); // 10
function foo() { console.log(this.a) } function active(fn) { fn(); // 真實調用者,爲獨立調用 } var a = 20; var obj = { a: 10, getA: foo } active(obj.getA);
JavaScript內部提供了一種機制,讓咱們能夠自行手動設置this的指向,也就是call與apply。
全部的函數都具備這兩個方法。它們除了參數略有不一樣,其功能徹底同樣。它們的第一個參數都爲this將要指向的對象。
以下例所示。fn並不是屬於對象obj的方法,可是經過call,咱們將fn內部的this綁定爲obj,所以就可使用this.a訪問obj的a屬性了。這就是call/apply的用法。
function fn() { console.log(this.a); } var obj = { a: 20 } fn.call(obj);
而call與apply後面的參數,都是向將要執行的函數傳遞參數。其中call以一個一個的形式傳遞,apply以數組的形式傳遞。這是他們惟一的不一樣。
function fn(num1, num2) { console.log(this.a + num1 + num2); } var obj = { a: 20 } fn.call(obj, 100, 10); // 130 fn.apply(obj, [20, 10]); // 50
由於call / apply的存在,這讓JavaScript變得十分靈活。所以就讓call / apply擁有了不少有用處的場景。簡單總結幾點,也歡迎你們補充。
1.將類數組對象轉換爲數組
function exam(a, b, c, d, e) { console.log(arguments); // { '0': 2, '1': 8, '2': 9, '3': 10, '4': 3 } var arg = [].slice.call(arguments); console.log(arg); // [ 2, 8, 9, 10, 3 ] } exam(2, 8, 9, 10, 3); // 也經常使用該方法將DOM中的nodelist轉換爲數組 // [].slice.call( document.getElementsByTagName('li') );
2.根據本身的須要靈活修改this指向
var foo = { name: 'joker', showName: function() { console.log(this.name); } } var bar = { name: 'rose' } foo.showName.call(bar) // rose
3.實現繼承
// 定義父級的構造函數 var Person = function(name, age) { this.name = name; this.age = age; this.gender = ['man', 'woman']; } // 定義子類的構造函數 var Student = function(name, age, high) { Person.call(this, name, age); this.high = high; } Student.prototype.message = function() { console.log('name:'+this.name+', age:'+this.age+', high:'+this.high+', gender:'+this.gender[0]+';'); } new Student('xiaoming', 12, '150cm').message(); // { name:xiaom, age:12, high:150cm, gender:man }
在Student的構造函數中,藉助call方法,將父級的構造函數執行了一次,至關於將Person中的代碼,在Sudent中複製了一份,其中的this指向爲從Student中new出來的實例對象。call方法保證了this的指向正確,所以就至關於實現了基層。Student的構造函數等同於下。
var Student = function(name, age, high) { this.name = name; this.age = age; this.gender = ['man', 'woman']; // Person.call(this, name, age); 這一句話,至關於上面三句話,所以實現了繼承 this.high = high; }
四、在向其餘執行上下文的傳遞中,確保this的指向保持不變
以下面的例子中,咱們期待的是getA被obj調用時,this指向obj,可是因爲匿名函數的存在致使了this指向的丟失,在這個匿名函數中this指向了全局,所以咱們須要想一些辦法找回正確的this指向。
var obj = { a: 20, getA: function() { setTimeout(function() { console.log(this.a) }, 1000) } } obj.getA(); // undefined
常規的解決辦法很簡單,就是使用一個變量,將this的引用保存起來。咱們經常會用到這方法,可是咱們也要藉助上面講到過的知識,來判斷this是否在傳遞中被修改了,若是沒有被修改,就沒有必要這樣使用了。
var obj = { a: 20, getA: function() { var self = this; setTimeout(function() { console.log(self.a) }, 1000) } }
另外就是藉助閉包與apply方法,封裝一個bind方法。
function bind(fn, obj) { return function() { return fn.apply(obj, arguments); } } var obj = { a: 20, getA: function() { setTimeout(bind(function() { console.log(this.a) }, this), 1000) } } obj.getA(); // 20
固然,也可使用ES5中已經自帶的bind方法。它與上面封裝的bind方法是同樣的效果。
var obj = { a: 20, getA: function() { setTimeout(function() { console.log(this.a) }.bind(this), 1000) } }
在封裝對象的時候,咱們幾乎都會用到this,可是,只有少數人搞明白了在這個過程當中的this指向,就算咱們理解了原型,也不必定理解了this。因此這一部分,將會爲這篇文章最重要最核心的部分。理解了這裏,將會對你學習JS面向對象產生巨大的幫助。
結合下面的例子你們思考一下。
function Person(name, age) { // 這裏的this指向了誰? this.name = name; this.age = age; } Person.prototype.getName = function() { // 這裏的this又指向了誰? return this.name; } // 上面的2個this,是同一個嗎,他們是否指向了原型對象? var p1 = new Person('Nick', 20); p1.getName();
咱們已經知道,this是在函數調用過程當中肯定,所以,搞明白new的過程當中到底發生了什麼就變得十分重要。
經過new操做符調用構造函數,會經歷如下4個階段。
建立一個新的對象;
將構造函數的this指向這個新對象;
指向構造函數的代碼,爲這個對象添加屬性,方法等;
返回新對象。
所以,當調用new操做符構造函數時,this其實指向的是這個新建立的對象,最後又將新的對象返回出來,被實例對象p1接收。所以,咱們能夠說,這個時候,構造函數的this,指向了新的實例對象,p1。
而原型方法上的this就好理解多了,根據上邊對函數中this的定義,p1.getName()中的getName爲調用者,他被p1所擁有,所以getName中的this,也是指向了p1。