打算在從此的一段時間裏更新前端相關的系列文章。目前計劃更新週期爲一週兩篇。由淺入深地理解一些前端常見的問題。今天是系列第一篇,主要講一下this的指向問題。前端
this又叫作上下文環境,與做用域不一樣的是,他是在函數調用的時候才產生的。首先咱們理解一下什麼是上下文環境?segmentfault
舉個生活中的例子:好比說你在廚房作菜,那廚房就是你的上下文環境,你要拿一點辣椒,順手就拿到了;好比你在試衣間,那試衣間就是你的上下文環境,你想在試衣間拿一點辣椒,可是此次拿不到了。因此咱們有了一個結論:上下文環境不同,作同一件事(調用同一個方法),獲得的結果就不同。數組
來一段代碼感覺一下:閉包
const name = 'Bob' const obj = { name: 'Lucy', getName: function(){ console.log(this.name); } } const getName = function(){ console.log(this.name); } obj.getName(); getName();
咱們看一下上面這一段代碼。obj.getName()中getName這個方法的是被對象obj調用的,因此他的上下文環境就是obj,那this就是指向obj,它輸出的值是Lucy;getName()其實至關於window.getName(),該方法被window調用,因此上下文環境是window,this指向window,輸出的值是Bob。app
好,上面知道了什麼是上下文環境以後,那咱們再從綁定規則來看this的指向問題,一共有下面幾種綁定規則:ide
默認綁定其實就是咱們常常見到的在全局環境中調用函數的狀況,此時this默認指向window。通常這樣調用都很好辨認,可是它還有下面這種變形:函數
var b = { name: "Lucy", getName: function(){ console.log("此時this指向對象b", this.name); return function(){ console.log("此時this指向window", this.name); } } } const setFunc = b.getName(); setFunc();
這是一種閉包的方式。執行b.getName()以後返回一個函數,而且賦值給變量setFunc,而後在全局環境中調用了setFunc()。因此最後這種狀況下的this也是指向window的。this
這種書寫方式也很常見,就是函數做爲對象的方法,指向這個對象下的函數時,該環境下this隱式的指向調用它的對象(這個能夠當成一個結論來記住),就是誰調用了這個函數this就指向誰。es5
const obj = { name: 'Lucy', getName: function(){ console.log(this.name); } } obj.getName();
call,apply是經過傳入指定的對象,讓執行方法的this指向傳入的對象。prototype
call和apply的使用在於傳參的不一樣,將對象指向了this後函數會當即執行。而且這兩個方法綁定的對象能夠是動態變化的。若是第一個參數爲null,undefined或是不傳,則指向全局變量。
看代碼:
const name = 'myName'; const obj1 = { name: 'Lucy' } const obj2 = { name: 'Bob' } function getName(){ console.log(this.name); } getName.call(obj1); // 動態的讓this指向obj1,生效 getName.call(obj2); // 動態的讓this指向obj2,生效 const bFunc = getName.bind(obj1); bFunc(); // 動態的讓this指向obj1,生效 bFunc.call(obj2); // 動態的讓this指向obj2,可是不生效,指向的仍是obj1
要想弄明白爲何this爲何是指向構造函數的實例對象的,首先要先搞明白實例化new的時候作了什麼?這是一個比較繞的問題,就先不拋出代碼了。咱們先來看一下,執行了new以後的實例對象有哪些特色?我總結了一下有如下幾點:
咱們思考一下如何封裝一個函數實現上面的要求,如下是分析過程:
因此代碼能夠是這樣的:
function newFn(Fn, ...args){ // 新建一個普通對象 let obj = {}; // 將構造函數的原型指向對象的__proto__ obj.__proto__ = Fn.prototype; // 執行Fn函數,而且將this指向obj Fn.call(obj, ...args); // 返回有原型方法和this上屬性值的對象 return obj; }
因此使用自定義的newFn方法實現new的操做是這樣的:
function Person(name){ this.name = name; } Person.prototype.getName = function(){ console.log(this.name) } function newFn(Fn, ...args) { // 新建一個普通對象 let obj = {} // 將構造函數的原型指向對象的__proto__ obj.__proto__ = Fn.prototype; // 執行Fn函數,而且將this指向obj Fn.call(obj, ...args); // 返回有原型方法和this上屬性值的對象 return obj; } // 使用自定義的newFn實現實例化的過程 const p = newFn(Person, 'Lucy'); p.getName();
箭頭函數相比較傳統函數,有它的特殊性。首先上面說到傳統的函數的this是在調用它的時候肯定的,而箭頭函數沒有上下文環境的概念,它的this要去聲明該箭頭函數的上下文環境中找。因此,箭頭函數的this是在函數聲明的時候就肯定的,並且沒法更改。就連使用call和apply的方式也不能改變。
因此由於這個特色,箭頭函數的使用有它的侷限性。
// say方法上層做用域中的this指向的是window,call也沒法修改它的指向 let a = 10 const obj = { a: 1, say: () => { console.log(this.a) } } obj.say() // 10 obj.say.call({a: 5}) // 10 // say方法上層做用域中的this指向的是Factory實例,因此輸出的是實例的屬性b var b = 10 function Factory(){ this.a = '1' this.b = '2' this.getVal = { say: () => { console.log(this.b) } } } new Factory().getVal.say() //'2'
this的指向問題(在es5語法中)根據指向的規則能夠分紅劃分爲:
默認綁定
隱式綁定
顯式綁定
它的this指向比較特殊,箭頭函數自己沒有上下文環境的說法,它是在函數定義的時候this指向當前定義函數的上下文環境。因此有些文章說他指向父級函數中this指向的對象這個說法也是正確的。