L4: 物件、函數與「this」 == ###### tags: `JavaScript` ![](https://i.imgur.com/sl7PtSw.jpg =80%x) <font color="#999999">圖片來源:udemy:JavaScript 全攻略:克服 JS 的奇怪部分 </font> 每次程式碼被呼叫時,執行環境被創造,每個執行環境的有自己的環境變數(variable Environment),他可以參考外部環境(outer Environment),隨著範圍練去找我們要的變數或函數,也就是說如果我要找一個變數,但不在環境變數裡面,JS就會到外部去找這個變數,直到找到全域環境為止 每當函數被執行,JavaScript引擎會給我們一個不曾宣告的東西,this 變數,this會指向不同的物件,一個不同的東西,依據函數如何被呼叫的 全域環境下 ```javascript! console.log(this) // windiw object ``` ```javascript! function a() { console.log(this) this.newvariable = 'hello' } var b = function() { console.log(this) } a() b() console.log(newvariable) // hello ``` 可以觀察到當我們在==全域==創造一個函數,不論是==函數表示式或函數陳述句,this關鍵字都會指向全域物件 windiw object== 甚至是我們可以直接在function a裡面建立一個屬性newvariable,這會連結它到全域物件,呼叫a()後,就可以印出newvariable,他會存在全域物件裡的變數,直接`console.log(newvariable)`就可以印出 ![](https://i.imgur.com/n6mTspO.png) ### 了解物件的this 如果是建立純值,稱為屬性,如果建立的值是一個函數,稱為方法 執行c.log() 會印出`{name: 'the c object', log: ƒ}`,==當函數式連結到物件裡的方法時,this關鍵字成為裡面有方法的物件==,也就是會指向C物件 ```javascript! var c = { name: 'the c object', log: function(){ console.log(this) // object{name: 'the c object', log: ƒ} this.name = "Updated c object" } } c.log() //{name: 'the c object', log: ƒ} ``` 由於this會指向C物件,也可以在裡面透過`this.name` 改變C物件的name值,C物件就變成`{name: 'Updated c object', log: ƒ}` ```javascript! var c = { name: 'the c object', log: function(){ this.name = "Updated c object" console.log(this) } } c.log() //{name: 'Updated c object', log: ƒ} ``` ### JS 的bug ? 在log方法裡面創造一個匿名函數,存在變數setname中,在函數中設定`this.name = newName`,我們在呼叫setname時傳入一個參數到函數裡,命名為newName,我們正在試著用newName改變物件 這時呼叫`setname('Updated again! The c object')`,觀察this會印出什麼? ```javascript! var c = { name: 'the c object', log: function(){ this.name = "Updated c object" console.log(this) //{name: 'Updated c object', log: ƒ} var setname = function(newName) { this.name = newName } setname('Updated again! The c object') console.log(this) //{name: 'Updated c object', log: ƒ} } } c.log() ``` 結果兩個console.log都是`name: 'Updated c object'` , setname並沒有發揮作用 讓我們回到window全域物件看一下,可以找到有一個name值, 這裡的名稱屬性`this.name = newName`,==被等號運算子創造並新增到**全域物件**==,表示當裡面的函數`function(newName) { this.name = newName }`,它的執行環境被創造時,this指向全域物件,即使他在我創造的物件裡面? ![](https://i.imgur.com/0fxhZHz.png) 所以很多人會覺得這是JS的 bug,應該怎麼做來解決這個情形呢? 有一個常用的模式來應付這個情況,**物件是用 by reference 設定**,我可以設定變數,命名為self(有些人會命名為 that),`var self = this` ==這樣 self 會指向和 this 一樣的記憶體位置==,把程式碼的this 都改為 self 執行程式碼時,self 雖然沒有在function(newName)函數裡面被宣告,JavaScript 引擎會往範圍鏈裡面找,往外一層級看到外部環境,繼續尋找一個叫作self的變數 然後找到它,等於我讓self物件等同this物件,然後mutation ```javascript! var c = { name: 'the c object', log: function(){ var self = this self.name = "Updated c object" console.log(self) var setname = function(newName) { self.name = newName } setname('Updated again! The c object') console.log(self) } } c.log() ``` 就可以看到第二個self 成功變成 `Updated again! The c object` ![](https://hackmd.io/_uploads/rJ2auedDh.png) 改成用let來測試this和self,還是用self才可以改變 ![](https://hackmd.io/_uploads/HyQwceuwn.png) --- ### this 題目: #### Q1: 下列程式碼會在 console 輸出什麼?為什麼? ```javascript const myObject = { foo: 'bar', func: function () { const self = this console.log('outer func: this.foo = ', this.foo) console.log('outer func: self.foo = ', self.foo); (function () { console.log('inner func: this.foo = ', this.foo) console.log('inner func: self.foo = ', self.foo) }()) } } myObject.func() ``` 輸出結果 ```javascript outer func: this.foo = bar outer func: self.foo = bar inner func: this.foo = undefined inner func: self.foo = bar ``` 呼叫 myObject.func() 時,this是在物件下調用,那麼 this 指向 myObject 物件 在 func 函式中定義了一個 self 變數,賦值為 this,這樣self 也是指向 myObject 物件,所以第一個和第二個 console.log會顯示 outer func :this.foo = "bar" 和 self.foo = "bar"。 inner func : 為立即執行函式 執行內部函式中的第一個 console.log :因為內部函式的 this 物件是指向全域物件window,而全域中沒有定義foo,因此 this.foo 的值為 undefined。 執行內部函式中的第二個 console.log :selfㄕ.foo 參考了外層函式中的 self,,而 self 指向的是 myObject 物件,因此 self.foo 等於 myObject.foo,也就是 'bar'。 #### Q2: 下列程式的 console.log 結果會是什麼?在四種作法裡的 this 指向了誰? ```javascript var person = { firstName : "Ellen", lastName : "Lee", fullName: function() { return this.firstName + " " + this.lastName; } } function fullName() { return `${this.firstName} ${this.lastName}` } console.log(person.fullName()) console.log(fullName()) console.log(fullName.bind(person)()); console.log(fullName.call(person)); ``` 輸出結果 ```javascript person.fullName() = Ellen Lee fullName() = undefined undefined fullName.bind(person)() = Ellen Lee fullName.call(person) = Ellen Lee ``` 第一個:呼叫person裡面的fullName方法,this 指向 person 物件,所以輸出 "Ellen Lee"。 第二個:直接呼叫 fullName() 時,由於 fullName() 函式並不是作為物件被呼叫的,因此this 會指向全域物件,也就是 window 物件,所以輸出"undefined undefined" 不想讓程式自動設定 this 的綁定對象,想要自行明確定義 this 要綁定在誰身上,這就是顯式綁定 。 使用bind, call, apply 的方法,在後面的括號中,傳入想要this變數指向的物件person,這樣把 fullName這個函式中的 this 指稱為Person物件 所以第三個作法和第四個做法:在呼叫該函式時,this 都會指向 person 物件,所以輸出 "Ellen Lee"。 要注意的是使用`.bind`,不會立刻執行函數,所要題目是使用`.bind(person)()`來立刻執行 #### Q3: 頁面上有 3 個按鈕,按下時都會觸發 hello(),請問當三個按鈕被點擊時,console.log 的結果是什麼,hello 函式中的 this 分別指向了誰? ```htmlembedded <!DOCTYPE html> <html> <head> <title>Page Title</title> <style></style> </head> <body> <button type="button" id="btn0" onclick='hello()'>Click #00</button> <button type="button" id="btn1" onclick='hello.bind(this)()'>Click #01</button> <button type="button" id="btn2">Click #02</button> <script> function hello() { console.log(this.id) } document.querySelector('#btn2').onclick = hello </script> </body> </html> ``` 輸出結果:undefined、 btn1、 btn2 1. 當按下 Click #00 按鈕時,此時的 this 指向的是全域物件。我們並沒有在 global context 中定義 id 屬性,所以 `this.id` 所印出的結果為 undefined。 2. 當按下 Click #01 按鈕時,console.log 會輸出 btn1,透過 bind() 方法,this 被綁定為按鈕 #btn1, `this.id` 就會是按鈕中的id:btn1。 3. 當按下 Click #02 按鈕時,console.log 會輸出 btn2,因為透過 document.querySelector() 設定,按鈕 #btn2 的 onclick 事件被綁定為 hello 函式,所以 this 指向的是按鈕 #btn2,因此 `this.id` = btn2。