【克服 JS 】第四章(上)物件與函式
JavaScript 全攻略:克服 JS 的奇怪部分,第四章(上)物件與函式 Object and Function,學習紀錄。
Udemy 課程連結:
JavaScript 全攻略:克服 JS 的奇怪部分
4-1 物件與點
在其他的程式中,物件與函數是不一樣的東西,但在 JavaScript 中,物件與函數非常的相關,很多情況下幾乎是一樣的,物件是一群名稱/值的組合,物件中也可以包含物件與函式。
關於物件:
- 物件裡的原始的設定,又稱屬性 Properties。
- 物件裡的成員變數,又稱屬性 Property。
- 物件裡的成員函式,又稱方法 Method。
範例程式碼:
var person = new Object() //宣告變數並建立個新物件
person["firstname"] = 'Tony'
person["lastname"] = 'Alicea'
console.log(person)
console.log(person['firstname'] + person['lastname'])
輸出結果會是:
Object {
firstname: "Tony",
lastname: "Alicea"
}
"TonyAlicea"
屬性除了使用中括弧 []
來存取外,也可以使用點 .
來存取:
person.address = new Object()
person.address.street = '111 Main St.'// 可以用兩個點運算子存取第二層物件的屬性
person.address.cicty = 'New York'
person['address']['state'] = 'NY' //也可用兩個[]來存取
4-2 物件與物件實體(object literal)
JavaScript 可以透過 new Object()
來建立物件,但開發時相對少見這種寫法。
另一種使用物件實體 object literal 的語法為
var person = {}
使用大括弧 {}
的結果與使用 new Object()
的結果相同。
大括弧的方法還可以直接將屬性的名稱/值配對寫進去:
var tony = { firstname: 'Tony', lastname: 'Alicea' }
當有一個函式需要輸入一個物件時,也可以直接傳入物件實體,例如:
var tony = { firstname: 'Tony', lastname: 'Alicea' }
function greet(person) {
console.log('Hi ' + person.firstname)
}
greet(tony) // "Hi Tony"
greet({
firstname: 'Mary',
lastname: 'Doe'
})
// "Hi Mary"
4-3 框架小叮嚀:偽裝命名空間
命名空間(Namespace):一群變數與函式的容器,用來將相同名稱的變數或函式分開。
JavaScript 沒有命名空間,但可以用物件的特性假裝出來。
var greet = 'Hello!'
var greet = 'Hola!'
console.log(greet); // Hola!
第一個 Hello!
會被覆蓋。
區分兩種語言的 greet,可利用物件特性:
var english = {greet: 'Hello!'}
var spanish = {greet: 'Hola!'}
console.log(english.greet); // Hello!
4-4 JSON 與物件實體
JSON(JavaScript Object Notation),受 JavaScript 的物件實體語法啟發而產生的一種資料格式,比起使用 XML 傳輸資料,JSON 格式在檔案大小上更為輕量,也是現在主流的傳輸格式。
兩者雖然看起來差不多,但還是有一點不一樣。
這是物件實體:
var objectLiteral = {
firstname: 'Mary',
isAProgrammer: true
}
JSON 格式:
{
'firstname': 'Mary',
'isAProgrammer': true
}
在 JSON 中,成員的名稱必需加上引號變成字串的型式,但在物件實體中,成員的名稱可以加上引號,也可以不用加。
所以 JSON 的規則比較嚴格,所以 JSON 並不是 JavaScript 的一部份。
JavaScript 有一些內建函式可以轉換這兩者。
JSON.stringify()
,JavaScript 物件轉為 JSON 字串。SON.parse()
,將 JSON 字串轉為 JavaScript 物件
參考資料
JSON|MDN:https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/JSON
4-5 函式就是物件
在 JavaScript 中,函式就是物件,其函式的特性被稱為一級函式 first class functions。
function greet() {
console.log('hi')
}
//可以為函式加上屬性,因為函式就是物件
greet.language = 'english'
console.log(greet.language); // english
- 函式可以沒有名稱,稱為匿名函式。
- 在函式裡寫的程式也會成為函式物件的一個屬性,使用小括弧
()
可呼叫此屬性。
4-6 函式陳述句與函式表達式
陳述句(Statement):程式碼的單位,不會產生一個值。 表示式(Expression):程式碼的單位,會回傳一個值,可以用指派給變數存起來。
舉例:
a = 3 // 回傳 3 (表示式)
1 + 2 // 回傳 3 (表示式)
a === 3 // 回傳 true (表示式)
if(a === 3){} // a===3 會回傳布林值,但用if()包起來後沒回傳值了,是一個陳述句
函式的定義也有分為陳述句與表示式。
// 函式陳述句,沒有回傳值
function greet(){
console.log('hi')
}
greet() // 執行函數,顯示 hi
// 函式表示式,有回傳值
var anonymousGreet = function(){
console.log('hi')
}
anonymousGreet() // 執行這個函式,顯示 hi
函式表達式,就是使用匿名函數的方法,建立並回傳一個函數物件,然後存到變數。
如果這樣寫:
anonymousGreet() // 顯示錯誤: undefined is not a function
var anonymousGreet = function(){
console.log('hi')
}
變數 anonymousGreet
會先載入記憶體,但沒有賦值,所以是 undefined
,undefined
是一個值,非函式。
可以把函式表示式當成參數傳進去:
function log(a) {
console.log(a)
}
log(function() {
console.log('hi')
});
4-7 傳值與傳址
- 傳值 by value:當我們創造變數並給值時,變數會指向值在電腦記憶體中的位置,若我們以這個值為參照,指定另一個變數指向這個值時,電腦會在記憶體中新增(複製)一個新值,讓後來的這個變數指向新的值。
- 傳址 by reference:當我們創造變數並給值(物件)時,變數會指向物件在電腦記憶體中的位置,若我們以這個物件為參照,指定另一個變數指向這物件,這個變數就會指向電腦記憶體中同樣的物件,不會有新的物件在記憶體中被創造出來。
- 布林值、字串、數值、null、undefined 是 call by value。
- 物件、陣列、函式是 call by reference。
// by value
var a = 3
var b = a // 將 a 的值 3 指派給 b
a = 2 // 改變 a 的值為 2
console.log(a) // 2
console.log(b) // 3 (b 的值不會因為改變 a 而跟著變)
// by reference
var c = { greeting: 'hi' }
var d = c // 將 c 的物件指派給 d
c.greeting = 'hello' // 改變 c 物件的屬性值
console.log(c) // Object {greeting: "hello"}
console.log(d) // d 物件隨著 c 物件一起變了,因為就是同一個物件
function changeGreeting(obj) { //建立一個函式
obj.greeting = 'Hola' // 將傳入物件的屬性值改為'Hola'
}
changeGreeting(d) // 傳入 d 物件給這個函式
console.log(c) // Object {greeting: "Hola"}
console.log(d) // 在函數裡將物件obj的屬性改變後,c跟d也變了
// 使用 = 指派物件產生了新的記憶體位置
c = { greeting: 'howdy' }
console.log(c) // Object {greeting: "howdy"}
console.log(d) // Object {greeting: "Hola"}
// 使用物件實體語法建立新的物件給c後,
// c和d不再是同一個物件了
在其他程式語言有語法可以決定要傳值還是傳參考,但在 JavaScript 中沒有選擇,純值就是使用傳值,物件就是使用傳參考。
4-8 物件、函式與「this」
JavaScript 在建立執行環境時,不論是全域、區域執行環境,會自動產生一個變數 this ,用來指向目前所在的物件。
如果我們直接這麼寫,this 會指向誰?
console.log(this)
在全域環境時,this 就是指向預設的全域物件 window。
那麼如果這樣呢?
function a() {
console.log(this)
}
var b = function() {
console.log(this)
}
a()
b()
還是指向 window,由此可知,不管是用函式陳述句或函數表示式,裡面的 this 還是指向全域環境的 window 物件。
若是在物件的方法(Method)中呢?
var c = {
name: 'The c object',
log: function() {
console.log(this)
}
}
c.log()
當函式變成連結到物件的方法,函式的 this 關鍵字就會指向這個物件。
可以這樣衍生:
var c = {
name: 'The c object',
log: function() {
this.name = 'Udate c object'
console.log(this)
}
}
c.log()
物件的 name,其值被替換,這表示在開發時,可以使用方法中的this,影響包住這個方法的物件,或是取用同一物件的其他方法或屬性。
在 Javascript 有一個常令人感到困惑,有些人認為是 bug 的地方。
若是在物件的成員函式中,建立一個函數,那麼在這個成員函數中的函數裡,this 指向什麼呢?
var c = {
name: 'The c object',
log: function(){
this.name = 'Udate c object'
console.log(this.name)
var setname = function(newname) {
this.name = newname
}
setname('Updated again! The c object')
console.log(this.name)
}
}
c.log()
原本預期輸出結果會是:
"Udate c object"
"Updated again! The c object"
但實際結果卻是:
"Udate c object"
"Udate c object"
為何會這樣,因為 log 方法裡的函式表達式 setname,其 this 指向了全域物件。
在函式內定義的函式,裏頭函式的 this 會指向全域物件。
這個時候,開發者通常會為了避免這種 bug,而用變數保存 this。
var c = {
name: 'The c object',
log: function() {
var _this = this
this.name = 'Udate c object'
console.log(this.name)
var setname = function(newname) {
_this.name = newname
}
setname('Updated again! The c object')
console.log(this.name)
}
}
c.log()
用個變數 _this
把 this 存起來,可確定是指向物件 c。
關於 this,在之後的 ES6 中的箭頭函式,又會有不同的狀況,之後會提到。
setname 函式中,沒有變數 _this
,於是電腦便往外部執行環境找。
Codepen:克服 JS - 4-8 物件、函式與「this」
4-9 觀念小叮嚀:陣列 - 任何東西的集合
Javascript 的陣列可以同時存各種資料型態,包含物件和函式。
要建立一個 Javascript 陣列可以這樣寫:
var arr = new Array()
也可以使用陣列實體語法來建立:
var arr = []
Javascript 的陣列與物件很像:
var arr = [
1, // 數字
false, // 布林值
{ name: 'Knuckles' }, // 物件
function(name){ // 函式
var greeting = 'Hello'
console.log(greeting + name)
},
'hello' // 字串
]
陣列是以 0 為基準,如果要執行陣列序號 3 的函式,並帶入陣列序號2的物件屬性值,可以這樣寫:
arr[3](arr[2].name)
4-10 arguments 與 spread
arguments(參數),是在函式的執行環境時自動建立的陣列。
function greet(firstname, lastname, language){
console.log(firstname + ',' + lastname + ',' + language)
console.log(arguments)
}
greet() // 顯示 undefined, undefined, undefined 與 []
greet('John') // 顯示 John, undefined, undefined 與 ["John"]
greet('John', 'Doe') // 顯示 John, Doe, undefined 與 ["John","Doe"]
greet('John', 'Doe', 'es') // 顯示 John, Doe, es 與 ["John","Doe","es"]
spread(運算子),ES6 新增的內容,它會保存所有傳進函式(當參數)的值。
spread operator(展開運算子)和rest operator(其餘運算子)都是 ...
符號,並且是 ES6 以後才新增的內容,這兩者根據使用的狀況和情境有很大的差別。
參考資料
spread 是 ES6 新增內容,作者教學中只有帶過。
4-11 框架小叮嚀:重載函式
Javascript 不像其他程式語言有重載函式的功能,只能在函式裡寫判斷,並用複數函式互相呼叫,來達成類似的效果。
參考資料
4-12 危險小叮嚀:自動插入分號
JavaScript 會幫我們自動加上分號,更確切的說是語法解析器幫我們加上的。
function getPerson(){
return
{
firstname: 'Tony'
}
}
console.log(getPerson()) // undefined
如果語法解析器發現 return 後面有鍵盤 Enter 換行,會以為開發者忘記加上分號而替我們補上。
為了避免這樣的情況發生,物件實體語法的 {
符號最好接在上一行的句尾,for迴圈、if陳述句也是,這樣可以避免被自動加上 ;
而產生 bug。
function getPerson(){
return {
firstname: 'Tony'
}
}
console.log(getPerson())
4-13 框架小叮嚀:空白
在你寫的程式碼中, 像是 Enter 鍵、tab 鍵、空白鍵,可以使程式碼可讀性提高,那它們也不會被真正執行,而 JavaScript 的語法解析器對於空格的規範很自由。可以利用這項特性增加註解,方便你過一段時間,或回頭看你的程式時,幫助你看得懂它,很多框架的原始碼,用了很多這樣的空格,用了很多這樣註解。
4-14 立即呼叫的函式表示式
IIFE 全名為 Immediately Invoked Functions Expressions,指的是可以立即執行的Functions Expressions函式表示式,中文多譯為立即(執行)函式。
程式碼:
// Functions Expressions
var greetFunc = function(name) {
console.log('Hello ' + name)
}
greetFunc() // Hello undefined
這是一個 Functions Expressions 函式表示式,要呼叫它通常會寫成 greetFunc()。
如果將 greetFunc() 刪除,並在後方加上 ()
,變這樣:
var greetFunc = function(name) {
console.log('Hello ' + name)
}()
電腦在函式表示式後面讀到 ()
,就知道要立刻呼叫這個函式,這種立刻執行的函式寫法就稱為 IIFE。
如果將函式裏頭的 console.log
改成 return
,透過另一個 console.log()
呼叫變數指向的函式。
var greeting = function(name) {
return('Hello ' + name)
}
console.log(greeting)
函式結尾沒有 ()
表示立即執行,所以 console.log
印出
變數所指向的函式。
如果是 IIFE:
var greeting = function(name) {
return('Hello ' + name)
}()
console.log(greeting)
會印出指向函式立即執行的結果值。
但注意 greeting 是一個字串不是函式,如果試著以函式呼叫:
var greeting = function(name) {
return('Hello ' + name)
}()
console.log(greeting())
會得到錯誤結果。
若是這個函式沒有輸出值,只有要執行裡面的程式時,就不用加上 var greeting =
來接收輸出值,變成像這樣:
function(name) {
return('Hello ' + name)
}()
結果出現錯誤。
因為一行的開頭第一個字若是 function 的話 JavaScript 會認為這是一個函式陳述句,function 後應該要接函式名稱才對。
解決方法就是讓一行的第一個字不要是 function 就可以了
普遍的寫法是把整個立即執行函數用 ()
包起來:
(function(name) {
return('Hello ' + name)
}())
立即呼叫的 ()
,要放在裡面或外面都可以。
(function(name) {
return('Hello ' + name)
})()
兩個結果都一樣,不過擇一使用。
4-15 框架小叮嚀:IIFEs 與安全程式碼
ES5 版本的 JavsScript 只有全域執行環境(作用域)、函式執行環境(作用域)兩種,之後 ES6 才出現塊級作用域。
在 ES6 之前,為了避免設定太多的全域變數,開發者往往會將變數設定在函式中,使其成為區域變數,尤其是設定在 IIFE 中,確保不會汙染到全域環境的變數。
不少 JavsScript 框架、套件的開頭與結尾被 ()
包住,程式碼被立即函式包著,其目的是怕污染到使用者(開發者)的全域環境。
IIFE 內如何取用全域的變數:
(function(global) {
global.greeting = 'Hello'
})(window)
將 window 當參數傳入,使其成為這個 IIFE 的區域物件,確保在 IIFE 內的程式能故意取用到全域的特定變數。