第四節:物件與函數 == ###### tags: `JavaScript` ## <font color="#3733FF">Objects And The Dot</font> 物件本身會有一個記憶體位址,它可以參考到其他屬性、物件或方法的所在位址 了解JavaScript如何找出物件 屬性 和 方法 的記憶體位置。 ```javascript! let person = new Object() person["firstname"] = " Tony" //創造屬性firstname let firstNameProperty = "firstname" console.log(person[firstNameProperty]) //Tony ``` 物件本身和屬性方法都待在記憶體內,`.` 點(dot notation)、`[ ]` 中括號(bracket notation)都只是函數,它們是運算子,一個取出資訊的方式 new Object()不是建立物件的好方法,只是範例 ```javascript! person.address = new Object() // 在person物件中新增另一個物件address person.address.street = "111 Main St." person.address.city = "New York" console.log(person) // { // firstname: ' Tony', // address: { street: '111 Main St.', city: 'New York' } //} console.log(person.address.street) //111 Main St. console.log(person["address"]["city"]) //New York ``` 雖然我們可以用 dot notation也可以用bracket notation 中括號去取用屬性和方法 最好還是用dot notation運算子就好,簡潔且容易除錯,除非真的需要用動態字串取用屬性(一些可能會改變的字串),我在寫weather-cast時有遇到 ## <font color="#3733FF">Object and Object literal</font> 剛剛的new Object(),可以改寫成下面這樣,直接透過`{}`建立物件 物件也可以被傳進函數當作參數 ```javascript const person = { firstname: 'Tony', lastname: 'Alicea'} function greet(person) { console.log("Hi" + person.firstname) } greet(person) // 也可以同時建立函數和物件 greet({ firstname: 'Mary', lastname: 'Doe' }) ``` ## <font color="#3733FF">JSON and Object literal</font> JSON : JavaScript Object Notation JSON 是被 JavaScript的物件實體語法啟發的,並不是JavaScript的一部分 JSON對於名稱的使用,括號的需求比較嚴格,外層要用`{}`花括號包住,屬性都要加上`""` 用==JSON.stringify== 把物件轉換成JSON語法的字串 ```javascript! const objectLiteral = { firstname: "Mary", isAProgrammer: true } console.log(JSON.stringify(objectLiteral)) //{"firstname":"Mary", "isAProgrammer":true} ``` 用==JSON.parse== 把字串轉為物件 ```javascript! const jsonValue = JSON.parse('{"firstname": "Mary", "isAProgrammer": true}') console.log(jsonValue) //object{ firstname :"Mary",isAProgrammer:true} ``` ## <font color="#3733FF">Function are Objects</font> 在 JavaScript 中函數就是物件 First Class Functions,一級函數可以被傳入別的地方,可以被創造、使用,把函數給另一個函數, 所以當我們說JavaScript的函數就是物件時,函數物件長的什麼樣子? 其實就像 JavaScript 的其他物件一樣,在記憶體裡,它是一個特殊型態的物件,它有所有物件的特色 還有一些其他屬性 函數可以有屬性和方法,在 JavaScript, 函數物件有一些隱藏版的特殊屬性(紅色線) ``` graphviz digraph graphname{ T [label="Function(a special type of object)"] P [label="Primitive (property)"] A [label="Object (property)"] B [label="Function (method)"] C [label="Name", color=red,fontcolor=red ] D [label="CODE", color=red,fontcolor=red ] T->P T->A T->B T->C[label="optional, can be anonymous", color=red,fontcolor=red] T->D[label="invocable", color=red,fontcolor=red] } ``` Code : 我們寫的程式會成為函數物件的特殊屬性,並非是函數本身,這個函數是有其他屬性的物件,我們寫的程式只是其中一種屬性 這個屬性特別的是,它是可以呼叫的,代表你可以執行這個程式碼,這是當整個執行環境的創造和執行時會發生的 必須把函數想像成物件,而它的程式碼是那個物件的屬性之一,還有許多其他東西函數能夠包含,還有許多其他事情函數可以做,他可以被移動、複製、傳入另一個東西,就像是任何物件或值一樣, 就像是字串或數字一樣 Example: ```javascript! function greet() { console.log('hi') } greet.language = 'english' // 用 . 創造屬性 console.log(greet) // function greet() {console.log('hi')} console.log(greet.language) //english,存在記憶體中 ``` 當 greet 函數被創造,這個函數物件會被放進記憶體,是全域物件,名稱是我們命名的 greet,然後有名稱屬性和程式屬性(寫在裡面的程式碼),如果用greet()呼叫函數,會執行函數,此時執行環境會被創造 ```javascript function greet() { console.log('hi') } greet() ``` ``` graphviz digraph graphname{ T [label="Function(a special type of object)"] C [label="Name greet"] D [label="CODE console.log('hi')" ] T->C T->D[label=""] } ``` ## <font color="#3733FF">Function Statements and Function Expressions</font> 函數陳述句與函數表示式 用法差異 - Expression:a unit of code that results in a value (it doesn't hanve to save to a variable) 表示式是程式碼的單位,會形成一個值,所以當我們說函數陳述句會做某件事,但函數表示式或任何表示式,最終會創造一個值,而這個值不一定要儲存在某個變數 簡單的兩種表示式,都會回傳一個值 ```javascript! a = 3 //3 1 + 2 //3 ``` 來看看兩者的差別 譬如:if 條件是就是陳述句,不會回傳任何值 - 函數陳述句 ```javascript! function greet() { console.log('hi') } ``` 當函數被執行,它不會回傳值,這個函數只會被放進記憶體中,但它只是陳述句,不會回傳值,直到函數被執行,但它會被提升(hoist) 在執行環境的創造階段,它是全域執行環境 會放進記憶體中,所以可以被取用,我們可以在宣告greet之前、在建立函數陳述句之前 呼叫greet。 ```javascript! greet() function greet() { console.log('hi') } ``` ![](https://i.imgur.com/1IPOTUe.png) <font color="#999999">圖片來源:udemy:JavaScript 全攻略:克服 JS 的奇怪部分 </font> --- - 函數表示式 寫一個變數名稱 命名它為 anonymousGreet. 這個function 是匿名的,匿名函數就是沒有名稱屬性的函數 ```javascript! var anonymousGreet = function() { console.log('hi') } anonymousGreet() ``` 在 JavaScript 中,函數就像是物件一樣,可以在記憶體中被建立並且傳遞,我們要建立一個物件,設定它等於anonymousGreet這個變數,因為我們可以利用指向物件位址的變數名稱來參照到函數,寫`anonymousGreet()`就可以觸發函數 當等號運算子被執行,它把這個陳述句、這個新的函數物件的值,放到這個變數中,這個變數就指向了記憶體中的一個點 使用表達式的會在執行階段就被執行,他的值會是一個物件(由匿名函數產生的) 而陳述句的函數會被放在記憶體,當執行階段在執行時,只會看到這邊有一個函數不會做任何事 ```javascript! anonymousGreet() // 匿名函數 var anonymousGreet = function() { console.log('hi') } ``` ![](https://i.imgur.com/8MD89YI.png) <font color="#999999">圖片來源:udemy:JavaScript 全攻略:克服 JS 的奇怪部分 </font> 但不能像陳述句一樣,把呼叫寫在上面, 會出現錯誤: ==Uncaught TypeError: undefined is not a function== JS執行到anonymousGreet() 這行時,會知道他在記憶體中是undefinded,undefinded是純值不是function,直到執行到下一行,anonymousGreet才被創造出來,匿名函數沒有被提升的特性,必須先將變數賦值給函數物件,才能在程式中呼叫函數,所以一定要先設定值,再呼叫 從下面範例可以看到我們可以透過一級函數做一些事,譬如將物件或函數傳到log當作參數 ```javascript function log(a) { console.log(a) } log({ greeting: "hi"}) log(function () { console.log("hi") }) ``` 如果想要執行傳進來的function,可以寫成`function log(a) { a() }`這樣, ## <font color="#3733FF">By Value VS By Reference</font> 傳值 還是 傳址 ? 在一些程式語言中,可以用語法決定 要傳值(pass by value)還是傳參考(pass by reference),在JavaScript裡面是沒有選擇的,所有純值都是 by value, 而所有的物件都是 by reference. 在第三節有提到純值的type(數字/布林/字串),都是屬於按值拷貝 by value 建立一個變數b,寫上 b = a ,指的是記憶體的不同位置,所以改變b的值不會影響到a ```javascript! var a = 3 var b = 3 b = a a = 2 console.log('a', a //2 console.log('b', b) //3 ``` ![](https://i.imgur.com/gC2mi1k.png =60%x) <font color="#999999">圖片來源:udemy:JavaScript 全攻略:克服 JS 的奇怪部分 </font> 物件都是屬於按址拷貝 by reference(記憶體的位置) 建立一個變數b,寫上 b = a,這時已經a的記憶體位置已經被創造, 而b = a 代表b也等於同樣的記憶體位址,所以沒有新的物件被創造,此時a和 b都指向同一個位址,所以改變其中一個值的內容,另一個的值也會被改變 ```javascript! //by reference (all objects(including functions)) var a = { greeting: 'hi'} var b b = a console.log(a) //{ greeting: 'hi' } console.log(b) //{ greeting: 'hi' } a.greeting = 'hello' console.log(a) //{ greeting: 'hello' } console.log(b) //{ greeting: 'hello' } //寫成新的function也一樣,都是mutate greeting function changeGreeting(obj) { obj.greeting = 'hola' } changeGreeting(b) console.log(a) //{ greeting: 'hola' } console.log(b) //{ greeting: 'hola' } ``` ![](https://i.imgur.com/htSJjUX.png =60%x) <font color="#999999">圖片來源:udemy:JavaScript 全攻略:克服 JS 的奇怪部分 </font> :::info 補充 b.greeting = 'hello' 改變原本c物件 greeting的值,稱為Mutate,代表的是Change Something,可能是新增、移除屬性或修改屬性值(這個觀念在React中也很重要) 另一個詞是Immutate: mean it can't be changed ::: 但如果我們設定一個新的值給a,使用等號運算子,會設定一個新的記憶體空間給a,此時a 和 b就不再指向同一個記憶體位置 equals operator sets up new memory space(new address) ```javascript! a = { greeting: 'howdy'} console.log(a) //{ greeting: 'howdy' } console.log(b) //{ greeting: 'hola' } ``` 這是個特殊例子,這不是by reference,因為 等號運算子 看到`a = { greeting: 'howdy'}`還不存在於記憶體,這是新的創造物件的方法 藉由這個物件實體語法 因為他看到第二個參數不是已經存在的物件,他必須建立另一個記憶體空間給物件 [replit練習檔](https://replit.com/@Tsai-WeiWei/value-and-reference#index.js) ## <font color="#3733FF">[物件、函數與「this」](https://hackmd.io/@weii/rksaogww3)</font> this 獨立出來一個章節 ## <font color="#3733FF">'arguments' 與 spread</font> JS執行環境除了會有 Variable Environment、Outer Environment、this之外,還有一個特殊的關鍵字: argument(也可以稱為parameters) argument: The parameters you pass to a function,它包含了所有傳入函數的參數 範例:觀察console.log變化,沒有提供參數時,會印出undefined, ```javascript! function greet(firstname, lastname, language) { console.log(firstname) console.log(lastname) console.log(language) console.log('-------------') } greet() greet('John') greet('John','Doe') greet('John','Doe','es') ``` ![](https://i.imgur.com/dRUUIpZ.png) 我們也可以給參數預設值,`language='es'` ![](https://i.imgur.com/2TX7HXE.png) 或是`language = language || 'en'` ![](https://i.imgur.com/ffnFw28.png) ### augument 不用經過宣告,已經內建好的,使用`console.log(arguments)`就會看到我們傳入的所有參數,但他不是陣列,只是array-like,有一部分的陣列功能而已,但augument目前已經逐漸少在使用 ![](https://i.imgur.com/AvsE3fs.png) ### spread ES6 新用法,spread parameter 表示傳入函數的參數,可以用 ... 增加一個參數 例如: 新增address的street name和city name = '111 main st', 'new york', 他就會被歸類在...other陣列裡面 (`console.log('other',other)`) ```javascript! greet('John','Doe','es','111 main st','new york') ``` 可以看到`'111 main st','new york'`被單獨印出來 ![](https://i.imgur.com/XbxcEOi.png) ## <font color="#3733FF">[立即呼叫的函數表示式(IIFEs)](https://hackmd.io/@weii/SkjQHNPw2)</font> 獨立出來一個章節 ## <font color="#3733FF">[Understanding Closures 了解閉包](https://hackmd.io/@weii/SJANBmdv3)</font> 獨立出來一個章節 ## <font color="#3733FF">框架小叮嚀:Function Factories</font> 了解閉包可以怎麼應用,可以利用這個特性,創造新的函數,用閉包製造預設的參數 來看一個範例: 建立兩個變數 greetEnglish 和 greetSpanish 來給 `makeGreeting()`不同的參數,而這兩個變數都是函數物件,雖然是呼叫一樣的的函數`makeGreeting()`,但也代表著不同的執行環境,可以藉由這兩個函數再給參數 所以` makeGreeting()` 函數 就像 factory function,我們利用閉包,去設定裡面這個被回傳的函數,所需要的參數值 ```javascript! function makeGreeting(language) { return function (firstname, lastname) { if (language === 'en') { console.log('Hello ' + firstname + ' ' + lastname) } if (language === 'es') { console.log('Hola ' + firstname + ' ' + lastname) } } } const greetEnglish = makeGreeting('en') const greetSpanish = makeGreeting('es') console.log(greetEnglish) // [Function (anonymous)] console.log(greetSpanish) // [Function (anonymous)] greetEnglish('John','Doe') // Hello John Doe greetSpanish('John','Doe') // Hola John Doe ``` ### <font color="#3733FF">過程發生了什麼</font> 1. 執行程式碼,產生全域執行環境,執行 `var greetEnglish = makeGreeting('en')`,makeGreeting()有自己的執行環境,裡面的language是en ,執行後會回傳函數,存入在greetEnglish裡面 ![](https://i.imgur.com/jy5CKGv.png) --- 2. makeGreeting() 離開執行堆,接著執行`var greetSpanish = makeGreeting('es')`,記得每次呼叫一個函數,都會得到新的執行環境,有自己的變數環境,所以裡面的language是es,執行後回傳函數, 離開執行堆 ![](https://i.imgur.com/SxN8QUx.png) --- 3. 現在我們有兩個記憶體中的位置,是兩個不同著執行環境,呼叫`greetEnglish('John','Doe')`時,這創造一個新的執行環境,firstname 是 John, lastname 是 Doe,JS引擎知道第一個language,是在第一個執行環境時被創造的,執行環境回傳的就是閉包所在地,greetEnglish回傳了 Hello John Doe ![](https://i.imgur.com/zwJumrF.png) --- 4. 呼叫`greetSpanish('John','Doe')`時,產生自己的執行環境,因為函數物件是在第二次呼叫被創造的,所以它的外部參考會指向,第二次呼叫產生的第二個執行環境,它有自己的閉包,會找到language是es,greetSpanish回傳了 Hola John Doe ![](https://i.imgur.com/nhqR6ms.png) --- 要知道每當你呼叫一個函數,它會得到自己的執行環境,然後在裡面被創造的函數會指向那個執行環境,做他該做的事,指向記憶體空間,就好像其他執行環境沒有消失一樣,他知道該指向哪個,內部函數在哪裡在何時被創造 ## <font color="#3733FF">Closures and Callbacks</font> setTimeout ,其實這也是在使用函數表示式和閉包 執行setTimeout時,三秒後,JS engine會觀察到還要執行`function() {console.log(greeting)}`,目的是要印出console.log(greeting),但greeting不在這個函數裡面,sayHiLater這個函數也已經執行完了,所以透過scope chain,在閉包內找greeting這個變數 ```javascript! function sayHiLater() { const greeting = 'Hi' setTimeout(function() { console.log(greeting) },3000) } sayHiLater() // Hi (after 3 sec) ``` ### <font color="#3733FF">callback function 回呼函數</font> A function you give to another function, to be run when the other function is finished 我呼叫函數a,然後給它函數b,當a結束,a呼叫函數b,這就是回呼函數 簡單範例: 我呼叫tellMeWhenDone,並給他一個函數當作參數,執行完一些事情後,執行callback(),呼叫我提供的function ```javascript! function tellMeWhenDone(callback) { const a = 1000 const b = 2000 callback() // the callback, it runs the function I gave it } tellMeWhenDone(function() { console.log('I am done!') }) tellMeWhenDone(function() { console.log('All done!') }) ``` ## <font color="#3733FF">[Call()、Apply() and Bind()](https://hackmd.io/@weii/HJ8NwI_v3)</font> 獨立出來一個章節 ## <font color="#3733FF">Functional Programming 程式設計</font> 從範例了解程式設計 陣列計算,建立兩個陣列 arr1、arr2,arr2存放把arr1裡面的值,透過for迴圈乘以2後的值 ```javascript! const arr1 = [ 1,2,3 ] console.log(arr1) // [ 1,2,3 ] const arr2 = [] for ( let i = 0; i < arr1.length; i++) { arr2.push(arr1[i] *2) console.log(arr2) } console.log(arr2) // [ 2,4,6 ] ``` ### 練習 1 身為工程師都希望可以減少重複做的事情,或者將要做的事情放在函數裡面,在JS中一級函數可以被當作參數傳遞、賦值給變數或儲存在數據結構中,並且能夠作為return value 我們創造一個 mapForEach函數,可以接受兩個參數(arr, func),在裡面建立一個陣列newArr和for迴圈 在newArr.push裡面做程式設計,`func(arr[i])`,呼叫函數func,傳入參數`arr[i]`,函數執行結束後,回傳newArr 函數表示式,建立arr3,呼叫`mapForEach`函數,將arr1陣列做為參數,並提供一個function 負責return item * 2的值 因為傳入的參數是arr1,會對arr1進行for迴圈遍歷,item 會是 arr1的內容」[1,2,3],遍歷後得到的結果push給newArr,得到[2,4,6] ```javascript! function mapForEach(arr, func) { const newArr = [] for (let i = 0; i < arr.length; i++) { newArr.push( func(arr[i]) ) } return newArr } const arr3 = mapForEach(arr1, function(item) { return item * 2 }) console.log(arr3) // [2,4,6] ``` ### 練習 2 我們可以重複利用mapForEach做不同的任務 只要傳入函數,傳入要他做的運算,這是函數程式設計的經典例子 這樣就可以透過不同的需求,傳入不同的function,對陣列做不同的處裡, 譬如要判斷item 是否 > 2 ```javascript! const arr4 = mapForEach(arr1, function(item) { console.log(item) return item < 2 }) console.log(arr4) // [ true, false, false ] ``` ### 練習 3 檢查陣列中數字是否有超過限制自定義的數值 建立一個新函數checkLimit,return item > limiter , 只是用limiter 變數取代固定的數字 但這個函數接受兩個參數,而mapForEach需要接受一個參數的函數,要如何呼叫這個函數? 可以透過bind,我們可以讓函數當中的參數變成預設值,因此等於只需要填入另一個參數就可以了 ```javascript! const checkLimit = function(limiter, item) { return item > limiter } const arr5 = mapForEach(arr1, checkLimit.bind(this,1)) console.log(arr5) // [ false, true, true ] ``` `mapForEach(arr1, checkLimit.bind(this,1))`使用mapForEach函數對arr1陣列進行迭代,並傳入一個函數`checkLimit.bind(this,1)`作為第二個參數。 使用`bind()`方法將checkLimit 函數中的第一個參數limiter綁定為1,因為使用`bind()`會複製一個新函數,所以checkLimit.bind(this,1)實際上是第一個參數limiter綁定1的新函數 當mapForEach函數對arr1陣列進行迭代時,它會呼叫新函數`checkLimit.bind(this,1)`, 並將arr1中的每個元素作為第二個參數傳入,在新函數中,limiter已被綁定為1,所以新函數將檢查傳入的每個元素(item)是否大於1 ### 練習4 有沒有辦法只代入Limiter這個參數,所以不用每次都用.bind來達到一樣的效果呢? 建立一個新函數checkLimitSimplified,它會回傳一個函數,並接受兩個參數 limitNumber 和 item 一樣使用 bind 將 limiter 參數綁定到回傳的函數中,這樣當調用函數時, limiter 參數的值就會被固定下來 執行 checkPastLimitSimplified時,它會給我一個已經用bind處理的函數 limitNumber的值是透過checkLimitSimplified(2)裡面的參數2給的 ```javascript! const checkLimitSimplified = function(limiter) { return function(limitNumber, item ) { return item > limitNumber }.bind(this, limiter) } const arr6 = mapForEach(arr1, checkLimitSimplified(2)) console.log(arr6) // [ false, false, true ] ``` 我更喜歡下面這個寫法,使用閉包的寫法 ```javascript! const checkLimiterSimplified = function(limiter) { return function(item) { return item > limiter; } } const arr6 = mapForEach(arr1, checkLimiterSimplified(2)); console.log(arr6); // [ false, false, true ] ``` 參考[[筆記] 了解JavaScript中functional programming的概念](https://pjchender.blogspot.com/2016/06/javascriptfunctional-programming.html) --- 當你開始用函數程式設計,最好是能夠在層級高的函數,或盡量不要更動他們,而是直接回傳一個新的東西,像是這邊的新陣列,沒有動到原來的陣列,這是函數程式設計的小提醒