L4: 物件、函數與「this」
==
###### tags: `JavaScript`

<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)`就可以印出

### 了解物件的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指向全域物件,即使他在我創造的物件裡面?

所以很多人會覺得這是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`

改成用let來測試this和self,還是用self才可以改變

---
### 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。