本文主要總結自《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] 來訪問它的元素,但它沒有數組的各種方法,比如
pop
,shift
,slice
等,操作 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
- 如果本文對您有幫助,不妨點贊一下,您的鼓勵是我的動力,我會更努力地寫出好文章。