1. 程式人生 > >JavaScript this 總結(含 ES6)

JavaScript this 總結(含 ES6)

 

本文主要總結自《JavaScript 語言精粹》、部分總結自《JavaScript 高階程式設計》以及自己的經驗

四種呼叫模式

在 JavaScript 中,this 的值取決於呼叫模式,有四種呼叫模式,分別是方法呼叫模式、函式呼叫模式、構造器呼叫模式、Apply、call 呼叫模式。

方法呼叫模式

當一個函式被儲存為物件的一個屬性時,我們稱它為一個方法。當方法被呼叫時(通過 . 表示式或 [subscript] 下標表達式),this 繫結到該物件。

var name = "window",
    lzh = {
        name: "lzh",
        sayName: function(){
            alert(this.name); // 輸出 "lzh"
        }
    }

lzh.sayName();

函式呼叫模式

當一個函式並非一個物件的屬性時,那麼它就是被當做一個函式來呼叫的,以此模式呼叫函式時,this 被繫結到全域性物件。
這是語言設計上的一個錯誤。倘若語言設計正確,那麼當內部函式被呼叫時,this 應該仍然繫結到外部函式的 this 變數。
這個設計錯誤的後果是方法不能利用內部函式來幫助它工作。
ECMAScript6 的箭頭函式(注意只是箭頭函式)基本糾正了這個設計上的錯誤(注意只是基本上,但不是徹底地糾正了錯誤)

var name = "window",
    lzh = {
        name: "lzh",
        sayName: function(){
            innerFunction();
            function innerFunction(){
                alert(this.name);
            }
            return function(){
                alert(this.name);
            }
        }
    }

lzh.sayName()();

上面這段程式碼 alert 的均是 window,從上面可以看出,不管外部環境的 this 是不是 window,通過函式呼叫模式呼叫的函式,this 指向 window。

來看一段 ES6 箭頭函式中的 this (上面提到箭頭函式基本糾正了設計上的錯誤)

var name = 'window';
var lzh = {
    name: 'lzh',
    sayName: function(){
        return ()=> {
            console.log(this.name);
        }
    }
}

var iny = {
    name: 'iny'
}

lzh.sayName().apply(iny); // 輸出 lzh

其實轉換成 ES5 是這麼幹的:

var name = 'window';
var lzh = {
    name: 'lzh',
    sayName: function(){
        var _this = this;
        return function(){
            console.log(_this.name);
        }
    }
}

var iny = {
    name: 'iny'
}

lzh.sayName().apply(iny); // lzh

但如果ES6 中這麼寫

var name = "window";
var lzh = {
    name: 'lzh',
    sayName: () => {
        console.log(this.name)
    }
}

var iny = {
    name: 'iny'
}
lzh.sayName(); // window
lzh.sayName.apply(iny); // window

轉換成 ES5 卻是這樣的

var name = "window";
var _this = this;
var lzh = {
    name: 'lzh',
    sayName: function() {
        console.log(_this.name)
    }
};

var iny = {
    name: 'iny'
}
lzh.sayName(); // window
lzh.sayName.apply(iny); // window
// 有點失望

構造器呼叫模式

JavaScript 是一門基於原型繼承的語言。這意味著物件可以直接從其他物件繼承屬性。該語言是無型別的。
當今(書的中文版第一版出版時間是2009年)大多數語言都是基於類的語言。儘管原型繼承極富表現力,但它未被廣泛理解。
JavaScript 本身對它原型的本質也缺乏信心,所以它提供了一套和基於類的語言類似的物件構建語法。
如果在一個函式前面帶上 new 來呼叫,那麼背地裡將會建立一個連線到該函式的 prototype 成員的新物件,同時 this 會繫結到那個新物件上
如果建構函式返回的不是物件,則通過 new 呼叫建構函式返回背地裡建立的物件。

var Person = function(name){
    this.name = name;
}

Person.prototype.getName = function(){
    return this.name;
}

var lzh = new Person("lzh");

console.log(lzh.getName()); // lzh

Apply、call 呼叫模式

因為 JavaScript 是一門函式式的面向物件程式語言,所以函式可以擁有方法。
每個函式都包含兩個非繼承而來的方法:apply()和 call()。這兩個方法的用途都是在特定的作用域中呼叫函式,實際上等於設定函式體內 this 物件的值。首先,apply()方法接收兩個引數:一個是在其中執行函式的作用域,另一個是引數陣列。其中,第二個引數可以是 Array 的例項,也可以是arguments 物件。call()方法與 apply()方法的作用相同,它們的區別僅在於接收引數的方式不同。對於 call()方法而言,第一個引數是 this 值沒有變化,變化的是其餘引數都直接傳遞給函式。換句話說,在使用call()方法時,傳遞給函式的引數必須逐個列舉出來

function sum(num1, num2){
    console.log(this);
    return num1 + num2;
}
function callSum1(num1, num2){
    return sum.apply(this, arguments); // 傳入 arguments 物件
}
function callSum2(num1, num2){
    return sum.apply(null, [num1, num2]); // 傳入陣列
}
function callSum3(num1, num2){
    return sum.call(null, num1, num2); // 一個一個地傳遞引數
}
alert(callSum1(10,10)); //20
alert(callSum2(10,10)); //20
alert(callSum3(10,10)); //20

從上面的程式碼可以看出,apply 的第一個引數是一個改變 sum 函式的 this 的值,但這裡不論傳進去的是 window 還是 null,內部 console.log 出來的都是 window 物件,還可以看出,apply 的第二個引數要麼是 arguments、要麼是一個數組。call 從第二個引數開始,就要一個一個的傳遞引數,而不能傳遞陣列或arguments。

單從上面的程式碼,不能很好的看出 apply、call 的長處,既然 call 能設定 this,那麼就能複用其它物件的方法,比如下面這個:

var lzh = {
    name: 'lzh',
    say: function(something){
        alert(something + this.name);
    }
}

var iny = {
    name: 'iny'
}

lzh.say.apply(iny, ['hi, I am ']); // 輸出 hi I am iny
  • iny 物件沒有 say 方法,但是又希望複用 lzh 的 say 方法,那麼就可以用 apply 或 call
  • 這樣還是不能很明顯的看出 call、apply 的優越性,舉個現實點的例子,如何把 arguments 轉換成陣列,因為 arguments 不是陣列,是一個 Array like 的物件,就是有下標元素,可以通過 arguments[0]、arguments[1] 來訪問它的元素,但它沒有陣列的各種方法,比如 popshiftslice 等,操作 arguments 會不大方便,所以我們希望把 arguments 轉換成 陣列。如果我們大概明白 Array.prototype.slice 的實現原理的話,我們可以利用這個方法將 arguments 轉換成陣列。
  • 第一步,講一下 Array.prototype.slice 簡易版的大概實現原理(原版應該是使用 C++ 實現的,功能和效能上都比這個簡易版的要好):
Array.prototype.slice = function(start, end){
    var newArray = [];
    if(start >= 0 && end <= this.length){
        for(var i = start; i < end; i++){
            newArray.push(this[i]);
        }
    }
    return newArray;
}

var testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(testArray.slice(0, 2)); // [1, 2]
  • 從上面我們可以看出,假如我們用 Array.prototype.slice.apply(arguments, 0, arguments.length),就相當於把 slice 內部的 this 換成了 arguments; 就可以把 arguments[0]、arguments[1]...等 push 到一個新陣列,這樣就可以成功的把 arguments 轉換成陣列了,於是就可以利用陣列的各種方法操作引數。當然,這裡只是簡易地重寫了一遍 slice,真實的 slice 可以不傳遞這裡的第三個引數,預設從 0 擷取到末尾。
  • apply、call 在實現函式柯里化、物件繼承上也有很大的作用,這裡不詳細展開。

匿名函式中的 this

  • 在網上看了很多關於 this 的部落格,都有介紹到匿名函式中的 this 指向 window 物件,但這種說法是不正確的,關鍵還是要看怎麼呼叫(就是前面介紹的4中呼叫方式),比如下面的程式碼
var name = "window",
    lzh = function(){
        return function(){
            //這裡是匿名函式,但是 this 的值只有在呼叫的時候才能確定
            alert(this.name);
        }
    }

var iny = {
    name: 'iny',
    sayName: lzh()
}
lzh()(); // window
iny.sayName(); // iny

從上面可以看出,this 的值在呼叫的時候決定

  • 還有就是事件處理程式裡面的 this
    在 DOM0 級、DOM2 級的事件處理程式中(onclick/addEventListener),this 指向繫結事件的那個元素,雖然不知道瀏覽器內部的具體實現,但可以猜測它是由 DOM 物件以方法呼叫的,屬於 DOM 物件的方法,而在 IE 舊版本的實現中,attachEvent 指定的事件處理程式的呼叫模式應該是函式呼叫模式,所以 this 指向 window。如有錯誤,還請指出。
  • setTimeout、setInterval 裡面的 this 也指向 window,這個應該還是由呼叫模式決定的。

下面看幾道題目

  • 題目1
var name = "window",
    lzh = {
        name: "lzh",
        sayName: function(){
            alert(this.name);
            alert(name);
        }
    }

lzh.sayName();

題目1先 alert lzh,再 alert window,alert window 的原因是:sayName 實際上是一個閉包,它的活動物件裡有 this(指向 lzh 物件),但沒有 name,所以它往父級作用域鏈尋找 name, 於是找到了全域性作用域的變數物件中的 windw,所以 alert window如果想了解閉包的更多內容,可以點這裡

  • 題目2
var name = "The Window";

var object = {
    name : "My Object",

    getNameFunc : function(){
        return function(){
            return this.name;
        };
    }
};

alert(object.getNameFunc()());

滑動檢視答案:alert 的是 "The Window"

  • 題目3
var name = "window",
    person = {
        name: 'lzh',
        getName: function(){
            return this.name;
        }
    }

console.log(person.getName());
console.log((person.getName)());
console.log((person.getName = person.getName)());

滑動檢視答案:輸出順序:lzh、lzh、window