JavaScript 進階 01:變數


資料型態

JavaScript 有 7 種資料型態,而這些型態又可被分列到兩大類:原始型態以及物件型態。

原始型態(primitive type)

Immutable 不能改變 (註一)(註二)

  • null
  • undefined
  • string
  • number
  • boolean
  • symbol(ES6)

物件型態(object)

只要不歸類到原始型態,皆屬於物件型態

  • array
  • function
  • date
    ...

(註一)原始型態與物件型態的差別

var str = 'hello'
str.toUpperCase() // 不能改變 str 這個變數
console.log(str) // hello
===
var str = 'hello'
var newStr = str.toUpperCase()
console.log(str, newStr) // hello, HELLO
  • 舉 array 的例子
var arr = [1]
arr.push(2)
console.log(arr) // [1, 2]

關於賦值

(註二)原始資料型態直接存放數值,物件資料型態存放記憶體位置

var a = 10
var b = a
console.log(a, b) // 10, 10

b = 200
console.log(a, b) // 10,200
0x01: {
    number:10
}
obj: 0x01     // 存放記憶體位置
obj2: 0x01   // 宣告 var obj2 = obj,把 obj 的記憶體位置,複製到 obj2
===
var obj = {
    number: 10
}
var obj2 = obj
console.log(obj, obj2) // 印出{number:10}, {number:10}
obj2.number = 20 // 存取 obj2 指向記憶體位置下的屬性,來做調整
console.log(obj, obj2) // 印出{number:20}, {number:20}
  • 同樣適用在 array
0x10: []
arr: 0x10
arr2: 0x10
===
var arr = []
var arr2 = arr
console.log(arr, arr2) // 印出[], []
arr.push('arr2')
console,log(arr, arr2) // 印出['arr2'], ['arr2']
  • 更進一步
0x10: []
0x20: ['arr2']

arr: 0x10
arr2: 0x20
===
var arr = []
var arr2 = arr
console.log(arr, arr2) // [], []
arr2 = ['arr2'] // 指向一個新記憶體位置,放新的值
console,log(arr, arr2) // [], ['arr2']

= 與 == 的差別

var a = 10

if (a == 20) {
    console.log(123) //  1. a(10) 不等於 20  2. 無印出
}
===
var a = 10

if (a = 20) {
    console.log(123) 
    // 執行步驟 1. a 被重新賦值  2. 判斷是 a 是不是 true 3. 再印出
}

== 與 === 的差別

  • 轉換型態
console.log(2 == '2') // true,轉換型態
console.log(2 === '2') // false,不會幫你轉換型態
  • 相同型態比較
var obj = {
    number: 1
}

var obj2 = obj
obj2.number = 2

console.log(obj === obj2) //  true 
// 用兩個等號還是三個等號都可以,因為沒有型態轉換的問題,相比的是記憶體的位置
  • 再次強調:物件型態與記憶體位置關係
0x01: [1]
0x02: [2]

arr: 0x01
arr2: 0x02
===
var arr = [1]
var arr2 = [1]
console.log(arr === arr2)  // false
console.log([] === []) // false,記憶體位置不一樣

Special Case- NaN

  • NaN,意即 not a number,屬於數字型態,但又不是真的數字
console.log(typeof NaN) // number
===
var a = Number('hello')
console.log(a)  // NaN
console.log(typeof a)  // number
  • 超級特例
    NaN 不等於任何型態,甚至是她本身自己。
var a = Number('hello')
console.log(a === a)  // false
  • 如何檢查 NaN
var a = Number('hello')
console.log(isNaN(a))

let 與 const

ES6 引進兩個宣告變數的方式。

  • constant 常數(const)
  • variable 變數(var & let 差別在作用域)

const

// 宣告 const 時,就要先給初始值
const b
b = 20
===
// 因為是常數,不能改變他的值
const a = 20
a = 30

再次來到 object 的世界

  • const 與 物件型態關係
0x01:{
    number: 2
}
obj: 0x01

const obj = {
    number: 1
} // 指向同個記憶體,不會報錯
const arr = [1, 2, 3]
arr[0] = 3   // 沒有改變記憶體位置,只是改變底下的值
  • 報錯的例子
0x01:{
    number: 2
}
obj: 0x01
const obj = {
    number: 1
} 
obj = {number: 2}  // 改變記憶體位置,會報錯

變數的生存範圍- Scope

Scope 又稱為作用域、變數的生存範圍。

談談 ES6 以前的作用域

  • 作用域的基本單位:function
function test() {  // 產生了 test 的 scope
    var a = 10 //  a 宣告在 test scope 裡面
    console.log(a)
}
// 所以只有在這個 function 裡面,a 才能被其他人看到

test()
console.log(a) // a is not defined
// 一但跑出了作用域外,找不到 a 的變數宣告,印出 a 就會報錯
  • 全域變數:global variable
var a = 10 // 所有人都得到,以檔案為單位
function test() { 
    console.log(a)
}
test()
console.log(a)
// 1. 會先在 function 裡面找,有沒有宣告 a
// 2. 如果沒有,就往上一層找(也就是 global),找到了 a = 10
  • 綜合練習:function 作用域 & global variable
    第一題:
    var a = 20
    function test() {
      var a = 10
      console.log(a)
    }
    test()
    console.log(a)
    // 印出 10, 20
    
    第二題:
    var a = 20
    function test() {
      a = 10 // 並非變數宣告,往上一層找變數宣告,然後去改變 a 值
      console.log(a)
    }
    test()
    console.log(a)
    // 印出 10, 10
    // 1. 會先在 function 裡面找,有沒有宣告 a
    // 2. 發現沒有之後,往上層找,找到 global variable
    // 3. 找到之後,去改變 a 值
    
    第三題:
    function test() {
      a = 10
      console.log(a)
    }
    test()
    console.log(a)
    // 印出 10, 10
    // 1. 會先在 function 裡面找,沒有的話,往上層找
    // 2. global variable 也找不到的話,就會直接在宣告一個全域變數`var a`
    
    第四題:
    var a = 'global'
    function test() {
      var a = 'test scope a'
      var b = 'test scope b'
      console.log(a, b) // 印出 test scope a, test scope b
      function inner() {
      var b = 'inner scope b'
      console.log(a, b) // 印出 test scope a, inner scope b
      } 
      inner()
    }
    test()
    console.log(a) // global
    // 形成 scope chain
    // inner scope => test scope => global scope
    
    第五題:
    var a = 'global'
    function change() { 
      var a = 10
      test() // 儘管  test() 被呼叫的地方是在 change scope
    }
    function test() { 
      console.log(a) // 印出 
    }
    change()
    // O test scope -> global scope
    // X test scope -> change scope -> global scope
    
    第六題:
    var a = 'global'
    function change() { 
      var a = 10
      function inner() {
          var a = 'inner'
          test()
      }
      inner()
    }
    function test() { 
      console.log(a)  // 印出 global
    }
    change()
    // 作用域的判斷,與 function 在哪裡被呼叫,一點關係都沒有
    // 作用域的判斷,唯一判斷是:變數在哪裡被宣告,scope chain 就被定義好
    
    第七題:
    var a = 'global'
    function change() { 
      var a = 'change'
      function inner() {
          var a = 'inner'
          test()
      }
      function test() { 
          console.log(a)  // 印出 change
      } 
      inner()
    }
    change()
    // test chain -> change chain -> global chain
    // 如何決定作用域,看程式碼放置的位置
    

談談 ES6 之後的作用域

作用域的概念不同,跟新引進的兩種變數宣告有關:let & const

  • before ES6:用 var 來宣告變數,作用域的基本單位是 function scope
  • after ES6:用 let const 宣告變數,作用域為 block scope

  • 舉例來說

    • 第一題
      function test() {
          var a = 60
          if (a === 60) {
              var b = 10
          }
          console.log(b)        
      }
      test()
      // 印出 10
      // 作用域以 function 為主
      // `var b = 10`會存在 test() 裡面
      
    • 第二題
      function test() {
          var a = 60
          if (a === 60) {
              let b = 10
          }
          console.log(b)        
      }
      test()
      // 印出 b is not defined
      // 變數 b 只能在 if 的 block 可以被用到
      // 作用域以 block {} 為主
      
    • 第三題
      function test() {
          for(var i=0; i<3; i++) { //執行到 i=3 時,是 false,然後跳出迴圈
              console.log('i:', i)
          }
          console.log('final value:', i)
      }
      test()
      // 印出 i:0, i:1, i:2, final value:3
      // 如果 var 改成 let,會印出 i:0, i:1, i:2, i is not defined
      

如何知道變數的資料型態是什麼?

使用 typeof

console.log(typeof 10)  // number
console.log(typeof 'hello') // string
console.log(typeof undefined) // undefined
console.log(typeof true) // boolean

令人混淆的地方

console.log(typeof null) // object
console.log(typeof []) // object
console.log(typeof function(){}) // function
var a // 宣告一個變數
console.log(typeof a) // undefined
// 變數都沒有宣告
console.log(typeof a) // undefined

更進一步

  • 判斷 array
    console.log(Array.isArray([])) // true
  • 判斷 function
    console.log(typeof function(){}) // function

較為精準的判斷方式

console.log(Object.prototype.toString.call())

  • 檢查變數有沒有被使用
console.log(a) // 出錯,變數沒宣告:a is not defined
===
if (typeof a !== 'undefined') {
    console.log(a)  // 不會出錯,程式照跑
}

補充資料

  1. 更多 == v.s. === 比較:https://dorey.github.io/JavaScript-Equality-Table/







你可能感興趣的文章

DataTables 試作X捲軸

DataTables 試作X捲軸

Day 61 - Flask WTF & Bootstrap [Hard]

Day 61 - Flask WTF & Bootstrap [Hard]

[JS Behind The Scene] 從 for loop 理解 scope 和 event loop 的運作機制

[JS Behind The Scene] 從 for loop 理解 scope 和 event loop 的運作機制






留言討論