面試官出不少考題,基本都會變着方式來考察this
指向,看候選人對JS
基礎知識是否紮實。
讀者能夠先拉到底部看總結,再谷歌(或各技術平臺)搜索幾篇相似文章,看筆者寫的文章和別人有什麼不一樣(歡迎在評論區評論不一樣之處),對比來看,驗證與本身現有知識是否有盲點,多看幾篇,天然就會完善自身知識。html
附上以前寫文章寫過的一段話:已經有不少關於
this
的文章,爲何本身還要寫一遍呢。學習就比如是座大山,人們沿着不一樣的路爬山,分享着本身看到的風景。你不必定能看到別人看到的風景,體會到別人的心情。只有本身去爬山,才能看到不同的風景,體會才更加深入。
函數的this
在調用時綁定的,徹底取決於函數的調用位置(也就是函數的調用方法)。爲了搞清楚this
的指向是什麼,必須知道相關函數是如何調用的。前端
非嚴格模式和嚴格模式中this都是指向頂層對象(瀏覽器中是window
)。git
this === window // true 'use strict' this === window; this.name = '軒轅Rowboat'; console.log(this.name); // 軒轅Rowboat
// 非嚴格模式 var name = 'window'; var doSth = function(){ console.log(this.name); } doSth(); // 'window'
你可能會誤覺得window.doSth()
是調用的,因此是指向window
。雖然本例中window.doSth
確實等於doSth
。name
等於window.name
。上面代碼中這是由於在ES5
中,全局變量是掛載在頂層對象(瀏覽器是window
)中。
事實上,並非如此。es6
// 非嚴格模式 let name2 = 'window2'; let doSth2 = function(){ console.log(this === window); console.log(this.name2); } doSth2() // true, undefined
這個例子中let
沒有給頂層對象中(瀏覽器是window)添加屬性,window.name2和window.doSth
都是undefined
。github
嚴格模式中,普通函數中的this
則表現不一樣,表現爲undefined
。面試
// 嚴格模式 'use strict' var name = 'window'; var doSth = function(){ console.log(typeof this === 'undefined'); console.log(this.name); } doSth(); // true,// 報錯,由於this是undefined
看過的《你不知道的JavaScript
》上卷的讀者,應該知道書上將這種叫作默認綁定。
對call
,apply
熟悉的讀者會類比爲:segmentfault
doSth.call(undefined); doSth.apply(undefined);
效果是同樣的,call
,apply
做用之一就是用來修改函數中的this
指向爲第一個參數的。
第一個參數是undefined
或者null
,非嚴格模式下,是指向window
。嚴格模式下,就是指向第一個參數。後文詳細解釋。
常常有這類代碼(回調函數),其實也是普通函數調用模式。數組
var name = '軒轅Rowboat'; setTimeout(function(){ console.log(this.name); }, 0); // 語法 setTimeout(fn | code, 0, arg1, arg2, ...) // 也能夠是一串代碼。也能夠傳遞其餘函數 // 類比 setTimeout函數內部調用fn或者執行代碼`code`。 fn.call(undefined, arg1, arg2, ...);
var name = 'window'; var doSth = function(){ console.log(this.name); } var student = { name: '軒轅Rowboat', doSth: doSth, other: { name: 'other', doSth: doSth, } } student.doSth(); // '軒轅Rowboat' student.other.doSth(); // 'other' // 用call類比則爲: student.doSth.call(student); // 用call類比則爲: student.other.doSth.call(student);
但每每會有如下場景,把對象中的函數賦值成一個變量了。
這樣其實又變成普通函數了,因此使用普通函數的規則(默認綁定)。瀏覽器
var studentDoSth = student.doSth; studentDoSth(); // 'window' // 用call類比則爲: studentDoSth.call(undefined);
call、apply、bind
調用模式上文提到call
、apply
,這裏詳細解讀一下。先經過MDN
認識下call
和apply
MDN 文檔:Function.prototype.call()
語法緩存
fun.call(thisArg, arg1, arg2, ...)
thisArg
在fun
函數運行時指定的this
值。須要注意的是,指定的this
值並不必定是該函數執行時真正的this
值,若是這個函數處於非嚴格模式下,則指定爲null
和undefined
的this
值會自動指向全局對象(瀏覽器中就是window
對象),同時值爲原始值(數字,字符串,布爾值)的this
會指向該原始值的自動包裝對象。
arg1, arg2, ...
指定的參數列表
返回值
返回值是你調用的方法的返回值,若該方法沒有返回值,則返回undefined
。apply
和call
相似。只是參數不同。它的參數是數組(或者類數組)。
根據參數thisArg
的描述,能夠知道,call
就是改變函數中的this
指向爲thisArg
,而且執行這個函數,這也就使JS
靈活不少。嚴格模式下,thisArg
是原始值是值類型,也就是原始值。不會被包裝成對象。舉個例子:
var doSth = function(name){ console.log(this); console.log(name); } doSth.call(2, '軒轅Rowboat'); // Number{2}, '軒轅Rowboat' var doSth2 = function(name){ 'use strict'; console.log(this); console.log(name); } doSth2.call(2, '軒轅Rowboat'); // 2, '軒轅Rowboat'
雖然通常不會把thisArg
參數寫成值類型。但仍是須要知道這個知識。
以前寫過一篇文章:面試官問:可否模擬實現JS
的call
和apply
方法
就是利用對象上的函數this
指向這個對象,來模擬實現call
和apply
的。感興趣的讀者思考如何實現,再去看看筆者的實現。
bind
和call
和apply
相似,第一個參數也是修改this
指向,只不過返回值是新函數,新函數也能當作構造函數(new
)調用。
MDN Function.prototype.bind
bind()
方法建立一個新的函數, 當這個新函數被調用時this
鍵值爲其提供的值,其參數列表前幾項值爲建立時指定的參數序列。
語法:
fun.bind(thisArg[, arg1[, arg2[, ...]]])
參數:
thisArg
調用綁定函數時做爲this參數傳遞給目標函數的值。 若是使用new
運算符構造綁定函數,則忽略該值。當使用bind
在setTimeout
中建立一個函數(做爲回調提供)時,做爲thisArg
傳遞的任何原始值都將轉換爲object
。若是沒有提供綁定的參數,則執行做用域的this
被視爲新函數的thisArg
。
arg1, arg2, ...
當綁定函數被調用時,這些參數將置於實參以前傳遞給被綁定的方法。
返回值
返回由指定的this
值和初始化參數改造的原函數拷貝。
以前也寫過一篇文章:面試官問:可否模擬實現JS
的bind
方法
就是利用call
和apply
指向這個thisArg
參數,來模擬實現bind
的。感興趣的讀者思考如何實現,再去看看筆者的實現。
function Student(name){ this.name = name; console.log(this); // {name: '軒轅Rowboat'} // 至關於返回了 // return this; } var result = new Student('軒轅Rowboat');
使用new
操做符調用函數,會自動執行如下步驟。
- 建立了一個全新的對象。
- 這個對象會被執行
[[Prototype]]
(也就是__proto__
)連接。- 生成的新對象會綁定到函數調用的
this
。- 經過
new
建立的每一個對象將最終被[[Prototype]]
連接到這個函數的prototype
對象上。- 若是函數沒有返回對象類型
Object
(包含Functoin
,Array
,Date
,RegExg
,Error
),那麼new
表達式中的函數調用會自動返回這個新的對象。
由此能夠知道:new
操做符調用時,this
指向生成的新對象。
特別提醒一下,new
調用時的返回值,若是沒有顯式返回對象或者函數,纔是返回生成的新對象。
function Student(name){ this.name = name; // return function f(){}; // return {}; } var result = new Student('軒轅Rowboat'); console.log(result); {name: '軒轅Rowboat'} // 若是返回函數f,則result是函數f,若是是對象{},則result是對象{}
不少人或者文章都忽略了這一點,直接簡單用typeof
判斷對象。雖然實際使用時不會顯示返回,但面試官會問到。
以前也寫了一篇文章面試官問:可否模擬實現JS
的new
操做符,是使用apply來把this指向到生成的新生成的對象上。感興趣的讀者思考如何實現,再去看看筆者的實現。
function Student(name){ this.name = name; } var s1 = new Student('軒轅Rowboat'); Student.prototype.doSth = function(){ console.log(this.name); } s1.doSth(); // '軒轅Rowboat'
會發現這個似曾相識。這就是對象上的方法調用模式。天然是指向生成的新對象。
若是該對象繼承自其它對象。一樣會經過原型鏈查找。
上面代碼使用ES6
中class
寫法則是:
class Student{ constructor(name){ this.name = name; } doSth(){ console.log(this.name); } } let s1 = new Student('軒轅Rowboat'); s1.doSth();
babel
es6
轉換成es5
的結果,能夠去babeljs網站轉換測試
自行試試。
'use strict'; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var Student = function () { function Student(name) { _classCallCheck(this, Student); this.name = name; } _createClass(Student, [{ key: 'doSth', value: function doSth() { console.log(this.name); } }]); return Student; }(); var s1 = new Student('軒轅Rowboat'); s1.doSth();
由此看出,ES6
的class
也是經過構造函數模擬實現的,是一種語法糖。
先看箭頭函數和普通函數的重要區別:
一、沒有本身的this
、super
、arguments
和new.target
綁定。
二、不能使用new
來調用。
三、沒有原型對象。
四、不能夠改變this
的綁定。
五、形參名稱不能重複。
箭頭函數中沒有this
綁定,必須經過查找做用域鏈來決定其值。
若是箭頭函數被非箭頭函數包含,則this
綁定的是最近一層非箭頭函數的this
,不然this
的值則被設置爲全局對象。
好比:
var name = 'window'; var student = { name: '軒轅Rowboat', doSth: function(){ // var self = this; var arrowDoSth = () => { // console.log(self.name); console.log(this.name); } arrowDoSth(); }, arrowDoSth2: () => { console.log(this.name); } } student.doSth(); // '軒轅Rowboat' student.arrowDoSth2(); // 'window'
其實就是至關於箭頭函數外的this
是緩存的該箭頭函數上層的普通函數的this
。若是沒有普通函數,則是全局對象(瀏覽器中則是window
)。
也就是說沒法經過call
、apply
、bind
綁定箭頭函數的this
(它自身沒有this
)。而call
、apply
、bind
能夠綁定緩存箭頭函數上層的普通函數的this
。
好比:
var student = { name: '軒轅Rowboat', doSth: function(){ console.log(this.name); return () => { console.log('arrowFn:', this.name); } } } var person = { name: 'person', } student.doSth().call(person); // '軒轅Rowboat' 'arrowFn:' '軒轅Rowboat' student.doSth.call(person)(); // 'person' 'arrowFn:' 'person'
DOM
事件處理函數調用<button class="button">onclick</button> <ul class="list"> <li>1</li> <li>2</li> <li>3</li> </ul> <script> var button = document.querySelector('button'); button.onclick = function(ev){ console.log(this); console.log(this === ev.currentTarget); // true } var list = document.querySelector('.list'); list.addEventListener('click', function(ev){ console.log(this === list); // true console.log(this === ev.currentTarget); // true console.log(this); console.log(ev.target); }, false); </script>
onclick
和addEventerListener
是指向綁定事件的元素。
一些瀏覽器,好比IE6~IE8
下使用attachEvent
,this
指向是window
。
順便提下:面試官也常常考察ev.currentTarget
和ev.target
的區別。ev.currentTarget
是綁定事件的元素,而ev.target
是當前觸發事件的元素。好比這裏的分別是ul
和li
。
但也可能點擊的是ul
,這時ev.currentTarget
和ev.target
就相等了。
<button class="btn1" onclick="console.log(this === document.querySelector('.btn1'))">點我呀</button> <button onclick="console.log((function(){return this})());">再點我呀</button>
第一個是button
自己,因此是true
,第二個是window
。這裏跟嚴格模式沒有關係。
固然咱們如今不會這樣用了,但有時不當心寫成了這樣,也須要了解。
其實this
的使用場景還有挺多,好比對象object
中的getter
、setter
的this
,new Function()
、eval
。
但掌握以上幾種,去分析其餘的,就天然迎刃而解了。
使用比較多的仍是普通函數調用、對象的函數調用、new
調用、call、apply、bind
調用、箭頭函數調用。
那麼他們的優先級是怎樣的呢。
而箭頭函數的this
是上層普通函數的this
或者是全局對象(瀏覽器中是window
),因此排除,不算優先級。
var name = 'window'; var person = { name: 'person', } var doSth = function(){ console.log(this.name); return function(){ console.log('return:', this.name); } } var Student = { name: '軒轅Rowboat', doSth: doSth, } // 普通函數調用 doSth(); // window // 對象上的函數調用 Student.doSth(); // '軒轅Rowboat' // call、apply 調用 Student.doSth.call(person); // 'person' new Student.doSth.call(person);
試想一下,若是是Student.doSth.call(person)
先執行的狀況下,那new
執行一個函數。是沒有問題的。
然而事實上,這代碼是報錯的。運算符優先級是new
比點號低,因此是執行new (Student.doSth.call)(person)
而Function.prototype.call
,雖然是一個函數(apply
、bind
也是函數),跟箭頭函數同樣,不能用new
調用。因此報錯了。
Uncaught TypeError: Student.doSth.call is not a constructor
這是由於函數內部有兩個不一樣的方法:[[Call]]
和[[Constructor]]
。
當使用普通函數調用時,[[Call]]
會被執行。當使用構造函數調用時,[[Constructor]]
會被執行。call
、apply
、bind
和箭頭函數內部沒有[[Constructor]]
方法。
從上面的例子能夠看出普通函數調用優先級最低,其次是對象上的函數。call(apply、bind)
調用方式和new
調用方式的優先級,在《你不知道的JavaScript》是對比bind
和new
,引用了mdn
的bind
的ployfill
實現,new
調用時bind以後的函數,會忽略bind
綁定的第一個參數,(mdn
的實現其實還有一些問題,感興趣的讀者,能夠看我以前的文章:面試官問:可否模擬實現JS
的bind
方法),說明new
的調用的優先級最高。
因此它們的優先級是new
調用 > call、apply、bind
調用 > 對象上的函數調用 > 普通函數調用。
若是要判斷一個運行中函數的 this
綁定, 就須要找到這個函數的直接調用位置。 找到以後
就能夠順序應用下面這四條規則來判斷 this
的綁定對象。
new
調用:綁定到新建立的對象,注意:顯示return
函數或對象,返回值不是新建立的對象,而是顯式返回的函數或對象。call
或者 apply
( 或者 bind
) 調用:嚴格模式下,綁定到指定的第一個參數。非嚴格模式下,null
和undefined
,指向全局對象(瀏覽器中是window
),其他值指向被new Object()
包裝的對象。undefined
,不然綁定到全局對象。ES6
中的箭頭函數:不會使用上文的四條標準的綁定規則, 而是根據當前的詞法做用域來決定this
, 具體來講, 箭頭函數會繼承外層函數,調用的 this 綁定( 不管 this 綁定到什麼),沒有外層函數,則是綁定到全局對象(瀏覽器中是window
)。 這其實和 ES6
以前代碼中的 self = this
機制同樣。
DOM
事件函數:通常指向綁定事件的DOM
元素,但有些狀況綁定到全局對象(好比IE6~IE8
的attachEvent
)。
必定要注意,有些調用可能在無心中使用普通函數綁定規則。 若是想「 更安全」 地忽略 this
綁
定, 你可使用一個對象, 好比 ø = Object.create(null)
, 以保護全局對象。
面試官考察this
指向就能夠考察new、call、apply、bind
,箭頭函數等用法。從而擴展到做用域、閉包、原型鏈、繼承、嚴格模式等。這就是面試官樂此不疲的緣由。
讀者發現有不妥或可改善之處,歡迎指出。另外以爲寫得不錯,能夠點個贊,也是對筆者的一種支持。
this
指向考題常常結合一些運算符等來考察。看完本文,不妨經過如下兩篇面試題測試一下。
小小滄海:一道常被人輕視的前端JS面試題
從這兩套題,從新認識JS的this、做用域、閉包、對象
你不知道的JavaScript 上卷
冴羽:JavaScript深刻之從ECMAScript規範解讀this
這波能反殺:前端基礎進階(五):全方位解讀this
做者:常以軒轅Rowboat若川爲名混跡於江湖。前端路上 | PPT愛好者 | 所知甚少,惟善學。
我的博客segmentfault
前端視野專欄,開通了前端視野專欄,歡迎關注
掘金專欄,歡迎關注
知乎前端視野專欄,開通了前端視野專欄,歡迎關注
github,歡迎follow
~