--- title: 'JS 核心 20 - this:DOM、課後練習' tags: JS 核心 ,JS , JavaScript, this:DOM description: 2021/02/17 --- JS 核心 -- this:DOM、課後練習 === ## this:DOM DOM : 針對網頁上元素的操作 來看看若在 DOM 元素上操作的話,this 有什麼不同 ? ### :pencil2: 範例 : 直接把方法寫在元素上 ### this 直接綁定這個 DOM 元素,此種綁定方式讓開發會更為輕鬆 ```typescript= <ul> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> </ul> <button onclick="console.log(this)">這是一個按鈕</button> ``` 使用 console.log 的方式會連 HTML 標籤一起呈現,就看不到原始內容 * 按下按鈕後,會直接把**元素**取出來 ![](https://i.imgur.com/hyAeEHY.png) 使用 console.dir 的方式查看原始內容 * 按下按鈕後,直接把**單純物件**取出來 * 可得知此物件有哪些屬性可運用 ``` <button onclick="console.dir(this)">這是一個按鈕</button> ``` ![](https://i.imgur.com/fFePbhy.png) ### :pencil2: 範例 : 針對監聽器來綁定 this 的運用 > DOM 的事件監聽會直接指向觸發事件的元素 ``` <ul> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> </ul> <button>這是一個按鈕</button> ``` ``` var fn = function () { console.dir(this); // this 受到監聽器影響 (this 指向所點擊的物件),把 this 綁在監聽器上面 this.style.backgroundColor = 'orange'; // 點擊元素的背景色會變色 } var els = document.querySelectorAll('li'); // 把所有的 li 取出 for (var i = 0; i < els.length; i++) { els[i].addEventListener('click', fn); // 為所有的 li 補上監聽器,每點擊一次 li 都會觸發事件 } ``` ![](https://i.imgur.com/yv8IBu9.png) --- ## this 課後練習 ### :pencil2: 範例 * 這裡的 this 是指 obj 物件,此 obj 物件下並沒有包含 name 的屬性,所以才會是 undefined。 * this 的觀念在這邊重點在於「他是怎麼在函式呼叫的」,而該題目他是在 obj 物件底下呼叫的,所以 this 就只會查找 obj。 ``` var name = '小明'; var obj = { x : function(){ name = '小王'; console.log(this.name); }, y : '2', } obj.x(); // undefined ``` ### :pencil2: 範例 使用簡易呼叫,在非嚴格模式下,他的 this 會指向 window。 由於 (第 9 行) 第一個參數值是 undefined,故 name 為 undefined。 由於 (第 10 行) 使用 call 方法,所以第一個參數值是 帶入物件 auntie; 後續依續帶入參數,故 name 值為 '小明'。 ```typescript= function callName(name){ console.log(this.name, name); } var name = '全域阿婆'; var auntie = { name : '漂亮阿姨', } callName(undefined, '小明'); // (使用簡易呼叫) 全域阿婆 undefined callName.call(auntie, '小明'); // 漂亮阿姨 小明 ``` ### :pencil2: 範例 (第 15 行) 在物件底下呼叫函式,this 指向就是前面的物件 x。 (第 7 行) 「雖然 setTimeout 是在 callName 裡面,但 setTimeout 裡面的 function 依然是屬於 callback function,我們不用看這個函式是在那裡執行,重點是他是怎麼去執行他。所以這段他依然是屬於 simple call 所以他的 this 指向 window。 ```typescript= var name = '小明'; var obj = { x: { name: '小虎', myname: function() { console.log(this.name); // 小虎 setTimeout(function () { console.log(this.name); // 小明 (簡易呼叫的 this 就會是 window) },500) } }, y: '2', name: '小王', } var a = obj.x.myname(); ``` --- ## 總結:函式的常見陷阱題 ### :pencil2: 範例 : <span class="red">**this 的指向和如何定義他沒有關係,關鍵在怎麼去執行**</span> > (第 9 行) 得知此為簡易呼叫,所以 this 指向全域 window。 this.myName 答案是 '全域'。 ```typescript= var myName = '全域'; // 全域變數 var person = { myName: '小明', getName: function () { return this.myName; } } var getName = person.getName; // 把函式取出,但未執行他。把它賦予在另一個變數上 console.log(getName()); // 執行 getName 函式 ``` ### :pencil2: 範例 思考使用 bind 方法時,this 會指向誰、以及參數會怎麼帶 ? * fn.bind(第一個參數,後面參數) * 第一個參數會做為 this 的指向,若傳入為 null 或 undefined,this 預設會指向全域 * 後面參數將依續帶入 以下答案為 全域 0 1 2 ```typescript= var myName = '全域'; // 全域變數 var obj = { myName: '奇怪的函式', fn: function (a, b, c) { // 函式帶上三個參數 return this.myName + ',' + a + ',' + b + ',' + c } } var fnA = obj.fn; // 宣告 fn 變數,把 obj 物件下的 fn 取出 var fnB = fnA.bind(null, 0); // 對 fnA 做綁定,思考若傳入 null,this 指向為何 ? console.log(fnB(1, 2)); // 執行 fnB ,再把其他參數帶入 ``` [承上原始碼] 試著改變 (第 10 行) 程式碼,查看變化。 由於參數 c 沒有值帶入,故顯示為 undefined。 以下答案為 全域 0 1 undefined。 ``` console.log(fnB(1)); // 改成只有傳入一個參數 ``` [承上原始碼] Q : 若要把 bind 方法的第一個參數傳入值為 null,null 傳入就作為 this 使用 (null 的 this 不會變成全域、也不會變成 obj 物件),請問該如何修正程式碼 ? A : 直接把 fn 函式轉為嚴格模式。在嚴格模式下,null 和 undefined 就不會指向全域。 在 (第 4 行) fn 函式下插入嚴格模式 'use strict'。此時會跳錯,因為 null 裡面不會有 myName 的屬性。 ``` 'use strict'; ``` 把 (第 5 行) myName 屬性移除,即可得到答案為 null 0 1 2 ``` return this + ',' + a + ',' + b + ',' + c ``` ### :pencil2: 範例 : 考 this 的觀念 ``` var value = 'global'; // 全域變數 var foo = { value: 'local', bar: function () { return this.value; } } var b = {}; // 定義一個 b 物件 Object.defineProperty(b, 'a', { // 在 b 物件下定義屬性 a value: undefined, // 屬性 a 的值 undefined writable: false // 屬性 a 的值無法被寫入、無法被覆蓋 }); b.a = 'a'; // 因為 b.a 無法寫入,就算賦予新值 'a',b.a 還是維持 undefined console.log(b.a); // undefined console.log(foo.bar()); // local (在 foo 物件下執行 bar) console.log((foo.bar = foo.bar)()); // global 賦值 (表達式) console.log((false || foo.bar)()); // global or (表達式)(第一個值若為 false,則回傳第二個值) console.log((b.a = foo.bar)()); // global // 以上三行為表達式,會回傳一個值,直接對值作執行 (由此段可看出是執行 bar 函式的結果) (如下圖) ``` ![](https://i.imgur.com/U258jWP.png) ### :pencil2: 範例 : 考 call back function 1. 宣告變數 arr,後面帶上 ['1', '2', '3'] 陣列,其中陣列值皆為字串,字串透過 parseInt 來轉為數值 2. map 方法會把執行結果回傳至前方的 arr 陣列上 3. arr 陣列值即依據字串 '1', '2', '3' 分別套用 parseInt 後回傳的結果 4. 請問 arr 陣列值為何 ? map 方法就是將傳入的**陣列值**一一取出,並且計算之後回傳結果 ``` var arr = ['1', '2', '3'].map(parseInt); console.log(arr); // [1, NaN, NaN] ``` 「map 方法」運作原理 ``` var arr = ['1', '2', '3'].map(function(item) { // 這段函式就是 call back function console.log(item); // 分別回傳 1 2 3 return ('a' + item); }); console.log(arr); // (3) ["a1", "a2", "a3"] ``` parseInt() 方法 : 把傳入的字串轉為整數 ![](https://i.imgur.com/MeloLA5.png) * map() 方法傳入三個參數 : arr.map(function(item, index, arr)) * item : 當前的值 * index : 索引位置 * arr : 陣列本身 ``` var arr = ['1', '2', '3'].map(function(item, i) { // 這段函式就是 call back function console.log(item, i); // 分別回傳 1 2 3 return parseInt(item, 10); // 分別把字串傳入,並使用 10 進位 }); console.log(arr); // (結果如上右圖) ``` 回頭看一下程式碼並改寫。 [parseInt( ) 方法 MDN 參考](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/map),改寫如下段程式碼。 > 僅需要知道這個觀念就可以了,實際撰寫程式碼時,並不會使用進位。 (第 1 行) parseInt 所帶的值就是**前面陣列的值**以及**索引位置**。原始碼為(第 3 行)。 (第 3 行) 對該回呼函式來說 Array.prototype.map 帶了三個參數:元素、索引、陣列。第三個參數會被 parseInt 忽略,但它可不會忽略第二個。 (第 4 行) parseInt 通常只用上一個參數 argument,但他其實用了兩個:第一個是表達式,第二個則是進位數。 (第 5 行) 把 parseInt 回傳出來。parseInt 會帶上值、索引位置。分別把字串傳入,i 索引位置代表進位模式。 (第 8 行) 2 進位只會有 0 跟 1,此數值不存在回傳 NaN。 ```typescript= // var arr = ['1', '2', '3'].map(parseInt); // 帶入的 parseInt 就是 call back 函式 // call back 函式帶入 item 值和索引位置 var arr = ['1', '2', '3'].map(function(item, i) { return parseInt(item, i); // 當 item 為 1,i 索引位置為 0 (即代表進位模式為 0),並沒有 0 進位,直接回傳 1 // 當 item 為 2,i 索引位置為 1,代表數值為 1 就進位,此數值不存在回傳 NaN // 當 item 為 3,i 索引位置為 2,代表數值為 2 就進位 }); console.log(arr); // (3)[1, NaN, NaN] ``` parseInt("1",0); // radix 為10也就是10進制的1轉10進制,也就是 1 parseInt("2",1); // radix 介於2和36之間的整數,當 radix 不在這個範圍內的解析全都返回 NaN parseInt("3",2); // 3不是2進制的數字,3前面也沒有其他數字,所以這裡返回 NaN ### 蛋老師影片 - [JavaScript map()和parseInt()](https://www.youtube.com/watch?v=9QOsNbHxDYI&list=PLMN9fM-0gQ90NMxWjL2xnWQoFh-28pwa_&index=19) ### 參考文章 : [快速理解[“1″,”2″,”3”].map(parseInt)](https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/751726/)、[ [1, 7, 11].map(parseInt)](https://medium.com/hybrid-maker/%E7%82%BA%E4%BB%80%E9%BA%BC-1-7-11-map-parseint-%E6%9C%83%E5%9B%9E-1-nan-3-6ce698c75de) ### :pencil2: 範例 : 陷阱題 ```typescript= (function () { console.log('六角學院 A'); // 六角學院 A }()); (function () { console.log('六角學院 B'); }); ``` 把 4-6 行程式放進「開發人員工具」 console 看看,結果如下。 ![](https://i.imgur.com/sXGCwaO.png) ### :pencil2: 範例 > 若函式的參數是一個函式,當函式被傳入之前被呼叫的話就會優先被執行。 * 關於 a is not a function,是因為 b() 沒有回傳東西所以結果是 undefined,流程如下: 一開始是將 c() 傳入 b() 並執行,所以出現 'casper',但此時的 b() 並沒有回傳任何內容,而又將 b() 執行的結果傳入 a(),但是 b() 沒有回傳,所以結果為 undefined。undefined 自然無法被執行,所以結果會是 a is not a function。 * 因此執行順序是 b() -> c() -> a() 這是因為 function 是預先執行 b(),且函式已經被執行釋放掉,所以順序並非為 a() -> b() -> c()。 ``` function a(a) { a(); } function b(b) { b(); } function c(c) { console.log('casper'); } a(b(c)); // a is not a function ``` 再試著了解函式的執行順序 依照執行順序來看是 b() - c() - a() 拆解後就會像這樣子,其原因在於函式我已經預先執行 b(),因此函式已經被執行釋放掉,因此這一題的函式執行順序並不是 a() - b() - c(),而會出現 a is not a function 函式呼叫了自己。 ``` function a(a) { console.log('a:' + a); a() // undefined } function b(b) { console.log('b:' + b) b(); // c function (b接收參數(c 的 function),是因為 callback function 概念) } function c(c) { console.log('c') console.log('casper'); } a(b(c)); ``` ## :memo: 學習回顧 :::info * <span class="red">this 的指向和如何定義他沒有關係,關鍵在怎麼去執行</span> ::: <style> .red { color: red; } .green { color: green; } </style>