Javascript中this與閉包學習筆記

博客原址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/applybind 手動改變 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方法,最終將圓移動了位置,最終效果以下圖:
apply使用圓示意圖

  • 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/applybind的做用的另一種說法: 擴充做用域

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

能夠看到這裏都實現了同樣的效果。值得說的是使用callaplly()來擴充做用域的最大好處就是對象不須要與方法有任何耦合關係。

 閉包

簡單定義

先來看這樣的一段代碼,在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,而且保存在本身的閉包做用域內,因此一直輸出執行的話,也會累加輸出。

小tips

須要咱們記住的是 每次函數調用的時候建立一個新的閉包

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;咱們一樣會獲得一樣的結果

點擊li顯示對應編號案例解析

其實上面這些我是很暈的,來看一個咱們實際在前端編程過程當中常常遇到的問題。
咱們有一個列表,分別爲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調試工具看看),可是達到的效果是同樣的。這樣咱們就爲每一個lionclick事件的匿名函數,都保存下了本身閉包變量。就能夠實如今點擊每一個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);
        };
    }
})();

若是有錯誤之處,請指正。謝謝!

相關文章
相關標籤/搜索