博客原址javascript
Javascript
中的this
基於不一樣的調用方式this的指向也會有所不一樣,調用方式大體有以下幾種:html
調用方式 | 表達式 |
---|---|
構造函數調用 | new Foo(); |
對象方法調用 | o.method(); |
函數直接調用 | foo(); |
call/apply/bind | func.call(o); |
如今就來看看這些不一樣的調用模式,this的指向會有怎麼樣的區別:前端
function Person(name,age){ this.name = name; this.age = age; this.sayName = function(){ console.info(this.name); }; } var allen = new Person("allen",12); console.info(allen);//{name: "allen", age: 12};...
經過這樣的代碼能夠很清楚的的看出,構造函數 Person 內部的this
指向被建立的調用對象 allenjava
經過上面的代碼很明顯咱們建立了一個 allen 對象,其中有一個 sayName
方法, 直接打印 this.name ,如今咱們就來看一下它會輸出什麼。git
allen.sayName();//allen
很明顯,這裏函數中的this
指向allen
對象自己。github
先來看一段代碼chrome
function add(a, b) { return a + b; } var myNumber = { value: 1, double: function() { function handle() { this.value = add(this.value, this.value); } handle(); } }; console.info(myNumber.value);//1 myNumber.double(); console.info(myNumber.value);//1
解析: 首先咱們定義了一個全局函數add
用於加法運算,接着咱們定義了一個對象,有一屬性value爲1,還有一個方法的目的是讓value值乘以二。咱們在函數內嵌套定義了一個函數handle
,調用add
方法而且執行。可是在調用函數值執行以後並無達到咱們想要的效果。這是爲何呢?
如何你打開chrome調試工具並打下斷點會發如今handle
函數內部的this會指向window!
由此能夠發現,在函數內部建立的函數,在這個函數調用時,函數內部的this
會指向window而不是外部的函數編程
下面就就能夠看一下常見的兩個方案:數組
// 取消 handle函數的定義,直接在對象的方法中使用this double2: function() { this.value = add(this.value, this.value); }, // 使用變量保存外部函數的this。 double3: function() { var that = this; function handle() { that.value = add(that.value, that.value); } handle(); }
call
/apply
與bind
手動改變 this先來看下面這樣一段代碼:瀏覽器
function Point(x, y){ this.x = x; this.y = y; } Point.prototype.move = function(stepX, stepY){ this.x += stepX; this.y += stepY; }; var p = new Point(0, 0); console.log(p);//{x: 0, y: 0} p.move(2,2); console.log(p);//{x: 2, y: 2} var circle = {x:1,y:1,r:1}; p.move.apply(circle, [2,1]); console.info(circle);//{x: 3, y: 2, r: 1}
咱們使用Point
構造函數能夠建立出一個點,在其原型上有一個move
方法可使這個點座標移動。
以後咱們又建立circle
對象,有x/y/r屬性(把它想象成一個圓),以後咱們的需求是:將這個圓的圓心移動,咱們就使用了apply
來借用move
方法,最終將圓移動了位置,最終效果以下圖:
function.prototype.apply/call
在上面咱們能夠看到能實現圓心移動的關鍵方法就是apply
,大體解析以下,p.move
是一個函數它的做用就是將一個點移動,而後咱們經過apply
方法把它借用給circle
這個對象。將circle
對象上的x/y屬性進行變動,分別加2和1,實現了圓心的移動。很明顯在這裏 apply
方法描述的就是一個借用的功能.
爲何會把apply/call
放在一塊兒說呢,由於他們的功能並無實質性的區別。只是在傳入參數的時候,apply須要將參數以數組的形式進行傳遞,而call是將須要傳入的參數一個一個跟在借用的對象後。下面一個小例子足以說明:
function sum(a, b) { return a + b; } function call1(num1, num2) { return sum.call(this, num1, num2); } function apply1(num1, num2) { // return sum.apply(this,[num1,num2]) return sum.apply(this, arguments);//利用函數的arguments對象 } console.info(call1(10, 20));//30 console.info(call1(5, 10));//15
能夠看到咱們在後兩個函數中,能夠直接使用sum方法。
function.prototype.bind
這裏來看看ES5引入的bind
,又有什麼不一樣,仍是和上面相似的代碼
function Point(x, y){ this.x = x; this.y = y; } Point.prototype.move = function(stepX, stepY){ this.x += stepX; this.y += stepY; }; var p = new Point(0, 0); var circle = {x:1,y:1,r:1}; var circleMove = p.move.bind(circle,2,2); circleMove(); console.info(circle);//{x: 3, y: 3, r: 1} circleMove(3,4); console.info(circle);//{x: 5, y: 5, r: 1}
這裏我使用了和 call 相似的調用方法,可是顯然 bind 和 call 不同,使用 bind 時,它會將咱們綁定 this 後的函數引用返回,而後手動執行。能夠看到的是,由於在這裏咱們綁定的對象的後面傳入了x/y兩個值,因此執行後坐標當即變化,而且在後來手動設置偏移量時也再也不起到效果。
這樣的相比於apply當即執行的好處時,咱們可使用定時器,例如:setTimeout(circleMove,1000)
,延遲一秒後移動。
固然,每次只能移動固定的值也不是一件很好的事情,因此咱們在使用 bind 的時候經常不會設置其默認參數, var circleMove2 = p.move.bind(circle,);
,以後在執行函數時,再將參數傳入circleMove(3,4);
,這樣就能夠實現每次自定義偏移量了
這又引出了call
/apply
與bind
的做用的另一種說法: 擴充做用域
var color = 'red'; var obj = {color:'blue'}; var obj1 = {color:'black'}; var obj2 = {color:'yellow'}; function showColor(){ console.info(this.color); } showColor();//red showColor.call(obj);//blue showColor.apply(obj1);//black showColor.bind(obj2)();//yellow
能夠看到這裏都實現了同樣的效果。值得說的是使用call
、aplly()
來擴充做用域的最大好處就是對象不須要與方法有任何耦合關係。
先來看這樣的一段代碼,在chrome中找到Scope
列表,能夠看到,在做用域鏈上咱們已經建立了一個閉包做用域!
(function() { var a = 0; function b() { a = 1; debugger; } b(); })();
閉包一個最簡單的定義就是:閉包就是說在函數內部定義了一個函數,而後這個函數調用到了父函數內的相關臨時變量,這些相關的臨時變量就會存入閉包做用域裏面.這就是閉包最基礎的定義
下面就來看一下閉包的一個基本特性保存變量
function add(){ var i = 0; return function(){ console.info(i++); }; } var f = add(); f();//1 f();//2
咱們定義了一個 add 方法,執行完畢後會返回一個函數,接着咱們就把這個函數賦值給了變量f,因爲 add 函數也是返回一個函數,在咱們每一次執行f()
的時候,它引用了add內的變量i,而且保存在本身的閉包做用域內,因此一直輸出執行的話,也會累加輸出。
須要咱們記住的是 每次函數調用的時候建立一個新的閉包:
var fun = add(); fun();//1 fun();//2
咱們再來經過簡單的例子看看另外一個注意的地方:
function test(){ var a = 0; var ff = function(){ console.info(a); }; a = 1214; return ff; } var b = test(); b();//1214
執行的結果是1214,從這裏咱們能夠看到 閉包中局部變量是引用而非拷貝,其實這樣的改變發散開來咱們就能夠知道,即便在這裏變量 a 未在函數 ff 以前定義,而是var a = 1214;
咱們一樣會獲得一樣的結果
其實上面這些我是很暈的,來看一個咱們實際在前端編程過程當中常常遇到的問題。
咱們有一個列表,分別爲1/2/3,咱們的需求是在點擊不一樣的數字時,也能把它對應的編號彈出來。而後咱們洋洋灑灑寫下了這樣的代碼:
<ul id="#list"> <li>1</li> <li>2</li> <li>3</li> </ul> <script> (function() { var oLi = document.getElementById("#list").getElementsByTagName("li"); for (var i = 0; i < oLi.length; i++) { oLi[i].onclick = function() { alert(i); }; } })(); </script>
一運行,發現懵了。怎麼彈出來的都是3?不對啊,我不是用循環將值都傳進去了嗎?
若是你確實理解了上面的 閉包中局部變量是引用而非拷貝這一節中的兩個案例的話,那麼就應該能瞭解一些。
解析:在這裏咱們爲每個li的onclick
事件 綁定了一個匿名函數,這個匿名函數就造成了一個閉包。這些匿名函數並不當即執行,而是在點擊對應的li的時候纔回去執行它。
而在這時就和上面的a = 1214;
這個例子同樣,此時的循環早已結束,i 就等於oLi.length
,在咱們點擊不一樣的li
時,閉包中引用的其實都是引用的同一個變量i天然彈出來的都是3,(這裏不理解引用的都是用一個i的話,能夠將alert(i);
替換成alert(i++);
,再到瀏覽器上去進行測試)
解決方案:
(function() { var oLi = document.getElementById("#list").getElementsByTagName("li"); for (var i = 0; i < oLi.length; i++) { oLi[i].onclick = (function(j) { return function(){ alert(j); }; })(i); } })(); /* (function() { var oLi = document.getElementById("#list").getElementsByTagName("li"); for (var i = 0; i < oLi.length; i++) { (function(j){ oLi[i].onclick= function(){ alert(j); }; })(i); } })(); */
能夠看到這裏給出了兩個簡單的寫法,但實際上除了寫法不一樣以外、閉包包含範圍、內容也不太同樣(有興趣的能夠打開chrome調試工具看看),可是達到的效果是同樣的。這樣咱們就爲每一個li
的onclick
事件的匿名函數,都保存下了本身閉包變量。就能夠實如今點擊每一個li的時候彈出對應的標號了。(還能夠將alert(j);
替換成alert(j++);
欣賞一下點擊不一樣li時的累加效果)
固然若是你只是想要記住一些標號這麼簡單的事情,其實還能夠將變量保留於元素節點上,也能達到同樣的效果,以下:
(function() { var oLi = document.getElementById("#list").getElementsByTagName("li"); for (var i = 0; i < oLi.length; i++) { oLi[i].flag = i; oLi[i].onclick = function() { alert(this.flag); }; } })();
若是有錯誤之處,請指正。謝謝!