【克服 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'  //也可用兩個[]來存取

Codepen:克服 JS - 4-1 物件與點

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"


Codepen:克服 JS - 4-2 物件與物件實體

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 會先載入記憶體,但沒有賦值,所以是 undefinedundefined 是一個值,非函式。

可以把函式表示式當成參數傳進去:

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 中沒有選擇,純值就是使用傳值,物件就是使用傳參考。

Codepen:克服 JS - 4-7 傳值與傳址

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 內的程式能故意取用到全域的特定變數。

參考資料:

JavaScript 基礎二三事|2018 iT 邦幫忙鐵人賽

那克斯的學習筆記

TOP