總有一些面試官喜歡問你一段不可能這麼寫的代碼。
看一道經典且古老的面試題(學完本文後,末尾會有一道更復雜的面試題等着你哦!)javascript
代碼以下: ```javascript var a = 5; var obj = { a : 10, foo: function(){ console.log(this.a) } } var bar = obj.foo obj.foo() bar() ```
我在讀 Events 的 lib/events 源碼的時候發現屢次用到call關鍵字,看來有必要搞懂 this 與 call 相關的全部內容。前端
其中幾句代碼是這樣寫的java
// 場景1: function EventEmitter() { EventEmitter.init.call(this); } // 場景2: return this.listener.call(this.target); // 場景3: return listenerCount.call(emitter, type);
3.箭頭函數使用不當報錯,在封裝 Node.js 的一個 ORM 映射框架 Sequelize 時,封裝表關聯關係,因爲使用箭頭函數形成了讀到的上下文發生變化,不是想要的 model 信息,而是指向了全局 。git
代碼例子以下:程序員
var person = { "name": "koala" }; function changeJob(company, work) { this.company = company; this.work = work; }; changeJob.call(person, '百度', '程序員'); console.log(person.work); // '程序員'
文章會同步到GitHub,地址爲:
程序員成長指北博客地址github
注意:本文不特殊說明的都是在瀏覽器中輸出結果。面試
JS(ES5)裏面有三種函數調用形式:數組
func(p1, p2) obj.child.method(p1, p2) func.call(context, p1, p2) // 這裏先不講 apply
好多初學者都只用到過前兩種狀況,並且認爲前二者優於第三者。直到幾天前想系統複習一下this關鍵字,找this相關的各類資料,在知乎看到了一個關於this的討論。
說第三種形式纔是正常的調用形式。瀏覽器
func.call(context,p1,p2)
其它兩種都是語法糖,能夠等價的變爲call
形式。func(p1, p2)等價於 func.call(undefined, p1, p2);
閉包
obj.child.method(p1, p2) 等價於 obj.child.method.call(obj.child, p1, p2);
這麼看咱們的函數調用只有一種形式:
func.call(context,p1,p2)
這時候是否是就知道this是什麼了,就是上面的context。回到我開篇提到的面試題。
var a = 5; var obj = { a : 10, foo: function(){ console.log(this.a) } } var bar = obj.foo obj.foo() bar()
因此this指向了obj
因爲沒有傳 context,因此 this 就是 undefined,若是是在瀏覽器中最後給你一個默認的 this——window 對象。若是是在 Node.js 環境中運行 this——globel對象。
在瀏覽器中運行結果爲5 在 Node.js 環境中爲 undefined。
爲何在瀏覽器或者前端環境能夠直接正常輸出值,而在 Node.js 環境中輸出的倒是undefined
。
看一下這段代碼你可能就懂了。
(function(exports, require, module, __filename, __dirname) { { // 模塊的代碼 // 因此那整個代碼應該在這裏吧 var a = 10; function A(){ a = 5; console.log(a); console.log(this.a); } // const haha = new A(); A(); } });
先說一下 Node.js 環境下在運行某個 js 模塊代碼時候發生了什麼,Node.js 在執行代碼以前會使用一個代碼封裝器進行封裝,例以下面所示:
(function(exports, require, module, __filename, __dirname) { { // 模塊的代碼 // 因此那整個代碼應該在這裏吧 } });
這段代碼在 Node.js 環境下輸出結果爲5,undefined
是否是就能理解了。
這裏面的this是默認綁定指向全局,當輸出this.a的時候,全局應該指向這個閉包的最外層。因此輸出結果式是undefined。
function fn (){ console.log(this) } var arr = [fn, fn2] arr[0]() // 這裏面的 this 又是什麼呢?
咱們能夠把 arr0 想象爲arr.0( ),雖而後者的語法錯了,可是形式與轉換代碼裏的 obj.child.method(p1, p2) 對應上了,因而就能夠愉快的轉換了:
arr[0]() 假想爲 arr.0() 而後轉換爲 arr.0.call(arr) 那麼裏面的 this 就是 arr 了
默認綁定是函數針對的獨立調用的時候,不帶任何修飾的函數引用進行調用,非嚴格模式下 this 指向全局對象(瀏覽器下指向 Window,Node.js 環境是 Global ),嚴格模式下,this 綁定到 undefined ,嚴格模式不容許this指向全局對象。
var a = 'hello' var obj = { a: 'koala', foo: function() { console.log(this.a) } } let bar = obj.foo bar() // 瀏覽器中輸出: "hello"
這段代碼,bar()
就是默認綁定,函數調用的時候,前面沒有任何修飾調用,也能夠用以前的 call
函數調用形式理解,因此輸出結果是hello
。
在函數中以函數做爲參數傳遞,例如setTimeOut
和setInterval
等,這些函數中傳遞的函數中的this
指向,在非嚴格模式指向的是全局對象。
例子:
var name = 'koala'; var person = { name: '程序員成長指北', sayHi: sayHi } function sayHi(){ setTimeout(function(){ console.log('Hello,', this.name); }) } person.sayHi(); setTimeout(function(){ person.sayHi(); },200); // 輸出結果 Hello,koala // 輸出結果 Hello,koala
判斷 this 隱式綁定的基本標準:函數調用的時候是否在上下文中調用,或者說是否某個對象調用函數。
例子:
var a = 'koala' var obj = { a: '程序員成長指北', foo: function() { console.log(this.a) } } obj.foo() // 瀏覽器中輸出: "程序員成長指北"
foo 方法是做爲對象的屬性調用的,那麼此時 foo 方法執行時,this 指向 obj 對象。
隱式綁定的另外一種狀況
當有多層對象嵌套調用某個函數的時候,如 對象.對象.函數
,this 指向的是最後一層對象。
例子:
function sayHi(){ console.log('Hello,', this.name); } var person2 = { name: '程序員成長指北', sayHi: sayHi } var person1 = { name: 'koala', friend: person2 } person1.friend.sayHi(); // 輸出結果爲 Hello, 程序員成長指北
看完這個例子,是否是也就懂了隱式調用的這種狀況。
顯式綁定,經過函數call apply bind 能夠修改函數this的指向。call 與 apply 方法都是掛載在 Function 原型下的方法,全部的函數都能使用。
不傳參數
,例如fun.call()
,非嚴格模式,this默認仍是綁定到全局對象unc.call(thisArg, arg1, arg2, ...) // call 用法 func.apply(thisArg, [arg1, arg2, ...]) // apply 用法
看代碼例子:
var person = { "name": "koala" }; function changeJob(company, work) { this.company = company; this.work = work; }; changeJob.call(person, '百度', '程序員'); console.log(person.work); // '程序員' changeJob.apply(person, ['百度', '測試']); console.log(person.work); // '測試'
這兩個方法在調用的時候,若是咱們傳入數字或者字符串,這兩個方法會把傳入的參數轉成對象類型。
例子:
var number = 1, string = '程序員成長指北'; function getThisType () { var number = 3; console.log('this指向內容',this); console.log(typeof this); } getThisType.call(number); getThisType.apply(string); // 輸出結果 // this指向內容 [Number: 1] // object // this指向內容 [String: '程序員成長指北'] // object
bind 方法
會建立一個新函數。當這個新函數被調用時,bind() 的第一個參數將做爲它運行時的 this,以後的一序列參數將會在傳遞的實參前傳入做爲它的參數。(定義內容來自於 MDN )
func.bind(thisArg[, arg1[, arg2[, ...]]]) // bind 用法
例子:
var publicAccounts = { name: '程序員成長指北', author: 'koala', subscribe: function(subscriber) { console.log(subscriber + this.name) } } publicAccounts.subscribe('小紅') // 輸出結果: "小紅 程序員成長指北" var subscribe1 = publicAccounts.subscribe.bind({ name: 'Node成長指北', author: '考拉' }, '小明 ') subscribe1() // 輸出結果: "小明 Node成長指北"
使用new調用函數的時候,會執行怎樣的流程:
執行構造函數中的代碼
例子:
function study(name){ this.name = name; } var studyDay = new study('koala'); console.log(studyDay); console.log('Hello,', studyDay.name); // 輸出結果 // study { name: 'koala' } // hello,koala
在new study('koala')
的時候,會改變this指向,將this指向指定到了studyDay對象
。
注意:若是建立新的對象,構造函數不傳值的話,新對象中的屬性不會有值,可是新的對象中會有這個屬性。
function New(func) { var res = {}; if (func.prototype !== null) { res.__proto__ = func.prototype; } var ret = func.apply(res, Array.prototype.slice.call(arguments, 1)); if ((typeof ret === "object" || typeof ret === "function") && ret !== null) { return ret; } return res; } var obj = New(A, 1, 2); // equals to var obj = new A(1, 2);
上面介紹了 this 的四種綁定規則,可是一段代碼有時候會同時應用多種規則,這時候 this 應該如何指向呢?其實它們也是有一個前後順序的,具體規則以下:
new綁定 > 顯式綁定 > 隱式綁定 > 默認綁定
在講箭頭函數中的 this 以前,先講一下箭頭函數。
定義
MDN:箭頭函數表達式的語法比函數表達式更短,而且不綁定本身的this,arguments,super或 new.target。這些函數表達式最適合用於非方法函數(non-method functions),而且它們不能用做構造函數。
常規函數能夠直接拿到 arguments 屬性,可是在箭頭函數中若是使用 arguments 屬性,拿到的是箭頭函數外層函數的 arguments 屬性。
例子:
function constant() { return () => arguments[0] } let result = constant(1); console.log(result()); // 1
若是咱們就是要訪問箭頭函數的參數呢?
你能夠經過 ES6 中 命名參數 或者 rest 參數的形式訪問參數
let nums = (...nums) => nums;
箭頭函數與正常的函數不一樣,箭頭函數沒有構造函數 constructor,由於沒有構造函數,因此也不能使用 new 來調用,若是咱們直接使用 new 調用箭頭函數,會報錯。
例子:
let fun = ()=>{} let funNew = new fun(); // 報錯內容 TypeError: fun is not a constructor
原型 prototype 是函數的一個屬性,可是對於箭頭函數沒有它。
例子:
let fun = ()=>{} console.loh(fun.prototype); // undefined
上面說了沒有原型,連原型都沒有,天然也不能經過 super 來訪問原型的屬性,因此箭頭函數也是沒有 super 的,不過跟 this、arguments、new.target 同樣,這些值由外圍最近一層非箭頭函數決定。
箭頭函數中沒有本身的this
箭頭函數中沒有本身的 this,箭頭函數中的 this 不能用 call()、apply()、bind() 這些方法改變 this 的指向,箭頭函數中的 this 直接指向的是調用函數的 上一層運行時
。
var a = 'kaola' var obj = { a: '程序員成長指北', foo: () => { console.log(this.a) } } obj.foo() // 輸出結果: "koala"
看完輸出結果,怕你們有疑問仍是分析一下,前面我說的箭頭函數中this直接指向的是調用函數的上一層運行時
,這段代碼obj.foo
在調用的時候若是是不使用箭頭函數this應該指向的是 obj ,可是使用了箭頭函數,往上一層查找,指向的就是全局了,因此輸出結果是koala
。
什麼是自執行函數?
自執行函數在咱們在代碼只可以定義後,無需調用,會自動執行。開發過程當中有時間測試某一小段代碼報錯會使用。
代碼例子以下:
(function(){ console.log('程序員成長指北') })()
或者
(function(){ console.log('程序員成長指北') }())
可是若是使用了箭頭函數簡化一下就只能使用第一種狀況了。使用第二種狀況簡化會報錯。
(() => { console.log('程序員成長指北') })()
應用場景其實就是開篇說到的爲何寫這篇文章,再重複一下。
學到這裏是否是發現開篇那道面試題有點簡單,已經不能知足你目前對於 this 關鍵字的知識儲備。好的,咱們來一道複雜點的面試題。
代碼以下:
var length = 10; function fn() { console.log(this.length); } var obj = { length: 5, method: function(fn) { fn(); arguments[0](); } }; obj.method(fn, 1);//輸出是什麼?
這段代碼的輸出結果是:10,2
認真讀文章的應該都能正確的答出答案,每個細節文章中都講了,我在這就不具體分析,若是不懂能夠再讀文章,或者直接加我好友咱們一塊兒討論,kaola 是一個樂於分享的人,期待與你共同進步。
聲明:任何形式轉載都請聯繫本人,若有問題也感謝您的指出和建議哦。