不管是面向對象,仍是基於對象的語言,都會有this,我更喜歡叫他this指針,若是你不理解指針,認爲它是個引用也無妨。
這一片文章就是整理一下在各個狀況下的this到底引用的是誰。一次來明白this的用法,下面將是一段段的代碼,每段代碼後面可能有簡短的說明,就是這樣簡單粗暴。javascript
說明一下,這篇文章是基於瀏覽器的,不是原生js,區別在於瀏覽器全局中的this是Window,而原生js中是global。其次,博主使用的控制檯輸出,若是你使用document.write
方法或alert
輸出this,因爲這兩個方法會調用對象的toString方法,你會獲得[object Window]或[object Object]。css
注意:本文中對通常函數和普通函數的措辭,這個只是博主我的的說法,因爲上下文(context)的解釋並非很容易懂,博主自定義了這2個說法,幫助理解。html
function f(){ console.log(this); //Window }
在js中,凡是沒有定義在對象、構造函數或prototype中的函數,其中的this都是全局對象Window。下文把這樣的函數稱爲通常函數java
var a = [1,2,3,4,5]; var b = a.map(function(x){ console.log(this); //Window return x * 2; });
同理上面這個函數也沒有定義在對象、構造函數或者prototype裏,因此獲得的依然是Window。
注意:Array.prototype.map是定義在數組原型中的,可是給map傳進去的參數函數就是一個通常函數c++
function Person(n, a, g){ this.name = n; this.age = a; this.gender = g; console.log(this); } //做爲構造函數使用 var o = new Person("Lily", 18, "F"); //this爲當前對象 Person {name: "Lily", age: 18, gender: "F"} //做爲普通函數使用 Person("Lily", 18, "F"); //Window
第10行代碼將函數做爲非構造函數使用方式(new方式)調用,本文把這樣調用的函數稱爲普通函數
上面代碼說明一下幾點:segmentfault
function Person(n, a, g){ this.name = n; this.age = a; this.gender = g; this.speak = function (){ //這裏只是說明this,實際應該在prototype上定義對象方法 console.log(this); }; } //做爲構造函數使用 var o = new Person("Lily", 18, "F"); o.speak(); //Person {name: "Lily", age: 18, gender: "F"} //做爲普通函數使用 Person("Lily", 18, "F"); speak(); //Window
多說一句,爲何11行獲得的是$Person{...}$,而不是$Object{...}$。其實這裏顯示的原本就應該是構造函數的名字,若是你經過$var o = {};$建立的對象,至關於$o = new Object();$,這時顯示的纔是$Object{...}$數組
function Person(n, a, g){ this.name = n; this.age = a; this.gender = g; } Person.prototype.speak = function (){ //這裏只是說明this,實際應該在prototype上定義對象方法 console.log(this); }; //做爲構造函數使用 var o = new Person("Lily", 18, "F"); o.speak(); //this爲當前對象 Person {name: "Lily", age: 18, gender: "F"} //做爲普通函數使用 Person("Lily", 18, "F"); speak(); //ReferenceError: speak is not defined
因而可知prototype中的方法和構造函數中直接定義方法中this是同樣的。
最後一行出現錯誤,這個不難理解,這裏很少說了。
若是構造函數有返回值呢?瀏覽器
function Person(n, a){ this.name = n; this.age = a; return { name: "Lucy", }; } var p1 = new Person("Bob", 10); console.log(p1.name); //"Lucy" console.log(p1.age); //undefined
很明顯,這是對象p1中的this指向返回值對象
固然,構造函數還能夠返回函數:閉包
function Fun(x){ console.log(this); return function(){ this.x = x; this.get = function(){ alert(this.x); } } } var o1 = new Fun(2); //Fun {} var o2 = Fun(2); //window console.log(o1 == o2); //false, 這裏的o1,o2形式是同樣的,因爲構成閉包結構,因此應用不一樣
但若是構造函數返回了一個基本類型:app
function Fun(n){ this.name = n; return 2; } var o; console.log(o = new Fun("Bob")); // {name: "Bob"}
此時獲得的對象和返回值無關。
到此咱們就明白了,構造函數的返回值若是是基本數據類型,那返回值和獲得的對象無關;不然,獲得的對象就是返回值的引用並構成閉包。
區分一下面這個具體問題:
<html> <body> <button onclick="click()">Click Here</button> <button id="btn">Click Here</button> <body> <script> function click(){ console.log(this); //window } var btn = document.getElementById("btn"); btn.onclick = function(){ console.log(this); }; </script> </html>
第一個按鈕獲得Window,而第二個獲得input元素!爲何!
再想一想,click函數定義在全局,不在對象上。而btn.onclick = function(){}
中的函數明顯是在btn對象上定義的。
說閉包前先理解一個簡單的:
var o = { name: "Lily", age: 18, gender: "F", speak: function (){ function fun(){ console.log(this); } fun(); } }; o.speak(); //Window
什麼,這裏是Window了?對!咱們仔細想一想,這個fun函數是對象的方法嗎?顯然不是,它是個通常函數。它僅僅是在另外一個函數中的一個函數,顯然符合上文描述的:「凡是沒有定義在對象、構造函數或prototype中的函數,其中的this都是Window」
若是想在內部函數訪問這個對象,也很好解決:
var o = { name: "Lily", age: 18, gender: "F", speak: function (){ var _this = this; //首選_this,有的資料上會用self。 function fun(_this){ console.log(_this); } fun(); } }; o.speak(); //Object {name: "Lily", age: 18, gender: "F"}
下面作個閉包,爲了說明this的值,這裏不定義太多變量,若是對閉包和做用域有疑惑能夠參看博主的另外一篇文章:Javascript 函數、做用域鏈與閉包
var o = { name: "Lily", age: 18, gender: "F", speak: function (){ return function(){ console.log(this); } } }; o.speak()(); //Window
這個難理解嗎?返回的函數依然是個定義在別的函數裏面的通常函數。若是想讓返回的函數能夠繼續訪問該對象,依然使用上面的$var _this = this$解決。不過這裏引出了一個新的問題:
var o = { name: "Lily", age: 18, gender: "F", speak: function (){ console.log(this); } }; var fun = o.speak; fun(); //Window
什麼?這裏仍是Window!o.speak明顯是一個對象方法啊!那麼問題來了?第10行調用的是誰?是fun函數。那麼fun函數怎麼定義的?對,fun的定義決定它是一個通常函數。那怎麼解決?這個不用解決,沒人會試圖在對象外獲取對象方法,即使是有須要也應該獲取對象方法內的閉包。固然,若是你要強行解決它,那就用bind方法吧。
什麼?原型方法中的this? 看看下面代碼就明白了,這個理解起來不會很難
function F(){ return F.prototype.init(); } F.prototype = { init: function(){ return this; }, test: "test" } var f = F(); console.log(f); //F{test:test}
可見,原型中方法裏的this.就是一個該構造函數的實例化對象。jQuery中使用的就是這個構造方法。
這3個方法用來改變調用函數內的this值
將對象綁定到函數,返回內部this值爲綁定對象的函數。
若是咱們不能修改庫中對象的方法,咱們就不能用$var \_this = this;$的方法改變this值,那麼咱們換個角度考慮上面的問題:
var o = { name: "Lily", age: 18, gender: "F", speak: function (){ return function(){ console.log(this); } } }; o.speak()(); //Window
最後一行中,o.speak()執行完後獲得一個函數,這是個臨時函數,定義在全局做用域,若是咱們把這個臨時函數綁定到o對象上,再繼續調用這個函數不就能夠了麼:
var o = { name: "Lily", age: 18, gender: "F", speak: function (){ return function(){ console.log(this); } } }; o.speak().bind(o)(); //Object {name: "Lily", age: 18, gender: "F"}
bind不僅能夠傳入一個參數,後面的多個參數能夠做爲返回函數的綁定參數,以下:
function add(a, b){ console.log(a+b); return a+b; } var add2 = add.bind(null, 2); //參數順序保持一致,第一參爲null,不改變this值(但這裏會改變,由於add2在全局中定義) add2(4); //6
可若是是構造函數呢?記住一點,函數做爲構造函數調用時,bind的第一參數無效,注意,僅僅是第一參數無效。
function Person(pname, page){ this.name = pname; this.age = page; } var Person2 = Person.bind({name:"hello",city:"Beijing"}, "world"); var p = new Person2(12); console.log(p);//Person{name:"world", age:12}
這裏舉幾個和上文不同的例子
function Animal(){ this.name = "Animal"; } Animal.prototype.showName = function(){ alert(this.name); }; function Cat(){ this.name = "cat"; } var cat = new Cat();
這裏Cat沒有showName方法,怎麼實現輸出名字呢?
有c++和java經驗的人會認爲貓屬於動物,因此Cat應該繼承Animal,因此咱們能夠這樣修改:
function Animal(){ this.name = "Animal"; } Animal.prototype.showName = function(){ alert(this.name); }; function Cat(){ this.name = "cat"; } Cat.prototype = Animal.prototype; var cat = new Cat(); cat.showName(); //Cat
或者:
function Animal(){ this.name = "Animal"; } Animal.prototype.showName = function(){ alert(this.name); }; function Cat(){ Animal.call(this, "cat"); //繼承 } var cat = new Cat(); cat.showName(); //Cat
有c++和java經驗就會知道,在作一個大型項目以前都是要作UML設計的,用例圖、活動圖、類圖、狀態圖等等十幾種圖,對於沒有必定經驗的開發者作這個簡直就是噩夢,而js把各類類或模塊獨立出來,須要的時候用call、bind、apply把多個類聯繫起來,這樣的作法即簡化了設計,又簡化了維護。
因此js裏面不多有上面的寫法,怎麼寫看下面:
function Animal(){ this.name = "Animal"; } Animal.prototype.showName = function(){ alert(this.name); } function Cat(){ this.name = "Cat"; } var cat = new Cat(); Animal.prototype.showName.call(cat); //cat Animal.prototype.showName.apply(cat); //cat
對,不過感受那裏怪怪的,call和apply同樣?他們功能上同樣,只是接受的參數不一樣,簡單寫就是下面這樣:
func.call(func1,var1,var2,var3,...); func.apply(func1,[var1,var2,var3,...]);
它們的第一個參數都是指定調用該函數的對象,若是爲空就是全局對象。後面的時傳入該函數的參數,區別在於使用call時參數逐一傳入,而使用apply時參數構成一個數組或類數組對象傳入。
例子1:
//求下列數組元素的最大值 var numbers = [5, 6, 9, 3, 7]; var maxValue = Math.max(numbers); alert(maxValue); //NaN maxValue = Math.max.apply(null, numbers); alert(maxValue); //9 //不然你只能這麼寫: var max = +Infinity; for (var i = 0, len = numbers.length; i < len; i++) { if (numbers[i] > max) max = numbers[i]; }
例子2
//自定義typeof函數(注意,系統自帶的typeof是運算符,不是函數) function typeOf(o){ return Object.prototype.toString.call(o).slice(8,-1); } //自定義typeOf函數測試 console.log(typeOf (2.1)); //Number console.log(typeOf (undefined)); //Undefined console.log(typeOf ({})); //Object console.log(typeOf ("hello")); //String console.log(typeOf (false)); //Boolean console.log(typeOf (typeOf)); //Function console.log(typeOf (null)); //Null console.log(typeOf ([])); //Array console.log(typeOf (new Date)); //Date console.log(typeOf (/\d/)); //RegExp console.log(typeOf (document. getElementsByTagName('body')[0])); //HTMLBodyElement //系統typeof運算符測試 console.log(typeof (2.1)); //number console.log(typeof (undefined)); //Undefined console.log(typeof ({})); //object console.log(typeof ("hello")); //string console.log(typeof (false)); //boolean console.log(typeof (typeOf)); //function console.log(typeof (null)); //object console.log(typeof ([])); //object console.log(typeof (new Date)); //object console.log(typeof (/\d/)); //object console.log(typeof (document. getElementsByTagName('body')[0])); //object //明顯比系統本身的實用多了
例子3
//把類數組對象轉爲數組(類數組對象就是屬性key爲0,1,2,...,還具備一個key爲length的能夠像數組同樣動態改變的值的對象) function(){ return Array.prototype.slice.call(arguments); }
例子4
//用js訪問元素僞類 function getRuleSelector(selector){ return Array.prototype.filter.call(getCssList(), function(x){ return pure(x.selectorText) === pure(selector); }); function pure(selector){ selector.replace(/::/g, ":"); //把雙冒號替換爲單冒號 } function getCssList(){ return Array.prototype.concat.apply([], Array.prototype.map.call(document.styleSheets, function(x){ return Array.prototype.slice.call(x.cssRules); })); } }
例子5
//爲每一個DOM元素註冊事件 Array.prototype.forEach.call(document.querySelectAll('input[type=button]'), function(ele){ ele.addEventLister("click", fun, false); });
例子6
//自定義forEach函數遍歷Dom元素列表(類數組對象) var forEach = Function.prototype.call.bind(Array.prototype.forEach); DOMElementList = document.getElementByTagName("li"); forEach(DOMElementList, function (el) { el.addEventListener('click', handle); //handle定義省略 });
之因此最後說箭頭函數,一方面由於這是ES6中的內容,更重要的時由於箭頭函數中的this永遠不能被call, bind和apply改變,也就是說箭頭函數中的this可不改變,僅僅與其定義的位置有關。
箭頭函數的最大特色是:它不改變this的做用域(上下文環境),可是依然構成局部做用域,咱們以前遇到過閉包內this值被改變的問題,咱們用從新定義局部變量的方式解決了這個問題。若是有了箭頭函數,解決這個問題就簡單多了
這是上面出現過的一段代碼:
var o = { name: "Lily", age: 18, gender: "F", speak: function (){ function fun(){ console.log(this); } fun(); } }; o.speak(); //window
看看用箭頭函數函數怎優雅的解決這個問題
var o = { name: "Lily", age: 18, gender: "F", speak: function (){ (() => {console.log(this);})(); //一個當即執行的箭頭函數 } }; o.speak(); //Object {name: "Lily", age: 18, gender: "F"}
或者這樣也能夠:
var o = { name: "Lily", age: 18, gender: "F", speak: function (){ return () => {console.log(this);}; //返回一個箭頭函數 } }; o.speak()(); //Object {name: "Lily", age: 18, gender: "F"}
with 能夠改變上下文環境,實際開發中十分不建議使用 with, 但關於 with 這裏簡單說明一下,看一個示例:
var a, x, y; var r = 10; with (Math) { a = round(PI * r * r); x = r * cos(PI); y = r * sin(PI / 2); } console.log(a, x, y); //314 -10 10
可是若是在 with 內直接聲明變量會發生什麼:
var obj = { name: 'test' }; with(obj){ //內部定義的變量都註冊在 obj 上 name = "hello"; var salary = 10000; age = 20; } console.log(obj.name); //'hello' console.log(obj.age); //undefined console.log(age); //20, 若是對象不具備這個屬性,該定義會意外的出如今 全局變量中 console.log(obj.salary); //undefined console.log(salary); //10000, 若是對象不具備這個屬性,該定義會意外的出如今 全局變量中