JS Advanced --變數


Posted by s103071049 on 2021-07-26

先從變數開始談起

JS 共有七種資料型態,可以七種型態可以分成兩類,primitive type 和 object。

七種資料型態

primitive type
1. null
2. undefined
3. string
4. number
5. boolean
6. symbol (ES6)

其他都是 object
7. object(array, function, date ...)

一、檢視資料型態

方法一、typeof (用 typeof 並不能保證結果為這七種型態),會傳回一個字串值。

console.log(typeof function() {}) // function
console.log(typeof null) // object 廣為人知的 js bug
  • 說明:JavaScript 的值就總以型別標簽跟著一個值的方式表示。物件的型別標簽是 0. 而 null 這個值是使用 NULL 指標 (在大部份平台上是 0x00) 來表示. 因此, null 看起來像是一個以 0 為型別標簽的值, 並使得 typeof 傳回不甚正確的結果
// 宣告變數但甚麼都沒給他
var a 
console.log(typeof a) // undefined

// 沒有宣告變數
console.log(typeof b) // undefined
console.log(b) // ReferenceError: b is not defined

// 檢測變數是否有用
var c = 100
if (typeof c !== 'undefined') {
  console.log(c)
}
  • 用途:檢測變數是否有用、檢測 function

方法二、Array.isArray (回傳 true/false 告訴你是否為 array)

console.log(typeof []) // object
console.log(Array.isArray([])) // true

方法三、Object.prototype.toString.call() 會將型態判斷更細

console.log(Object.prototype.toString.call('1')) // [object String]
console.log(Object.prototype.toString.call([1, 2, 3])) // [object Array]
console.log(Object.prototype.toString.call(new Date())) // [object Date]
console.log(Object.prototype.toString.call(null)) // [object Null]
  • 說明:有些 library 會用這個檢視型態,對於較舊的瀏覽器並沒有 Array.isArray,所以會利用 Object.prototype.toString.call() 去檢視結果是否同 [object Array]

二、primitive type VS object type

primitive types 是存值,object types 是存記憶體位置,再依據記憶體位置衍伸去改值。

差別一、immutable or not

primitive type is immutable(不能改變)。primitive types 會回傳一個新的值而非直接改變變數,object type 可以改變於原本的值。

因此,對 object type 做操作時,很多都會改變原本的值,要看文件知道這個 method 做了甚麼事情。

var a = 1
a = 2 // 重新賦值
console.log(a) // 2

var str = 'aaaaa'
str.toUpperCase() // 回傳一個新的字串,而非改變自己
console.log(str) // aaaaa

let newStr = str.toUpperCase() // primitive type 會回傳一個新的值
console.log(newStr) // AAAAA

var arr = [1]
arr.push(2) // 真的改變 arr 從 [1] => [1, 2]
console.log(arr) // [ 1, 2 ]

差別二、賦值 (=)

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

======
var obj = {
  number : 10
}

var obj2 = obj
console.log(obj, obj2) // { number: 10 } { number: 10 }
obj2.number = 20
console.log(obj, obj2) // { number: 20 } { number: 20 }

因為底層的運作,導致兩類在賦值上有截然不同的反應。對 primitive types 他是直接存值在記憶體。objective types 存的值是記憶體位置,當我對 object types 用 = 賦值,他會先給一塊記憶體,將記憶體位置的值放進來。用點的話,表示我要存取他指到記憶體底下的某個屬性,

var a = 10
// 記憶體 a : 10
var b = a
// 將 a 的值 copy 過去給 b,所以 b: 10
b = 20
// b : 20
var obj = {
  number : 10
}
/*
  obj : 0x01 
  0x01: {
    number: 10
  }
*/

var obj2 = obj
// obj2: 0x01

obj2.number = 20

/*
  obj : 0x01
  obj2 : 0x01 
  0x01: {
    number: 20
  }
*/

因為 array 也是 object type,所以

var arr = []
var arr2 = arr

console.log(arr, arr2) // [] []
arr2.push(123)
console.log(arr, arr2) // [ 123 ] [ 123 ]
  1. 當我宣告 arr = [ ],底層 arr 存放的是記憶體位置 arr:0x01,其中 0x01 這個位置放的是 [ ]
  2. 宣告 arr2 = arr,將 arr 的記憶體位置複製過來
  3. arr2.push(123) 是對 0x01 這個記憶體位置 push,現在 0x01 這個記憶體位置放的是 [123]
  4. 因為 arr, arr2 指向相同的記憶體位置,所以 log 出來的值相同。
var arr = []
var arr2 = arr

console.log(arr, arr2) // [] []
arr2 = [123]
console.log(arr, arr2) // [] [ 123 ]
  1. arr = [ ] => arr:0x01, 其中 0x01:[ ]
  2. arr2 = arr => arr2: 0x01,其中 0x01:[ ]
  3. arr2 = [123] => arr2: 0x02,其中 0x02:[123],也就是他賦值是指向一塊新的記憶體
  4. 因為 arr, arr2 指向不同記憶體位置,所以兩個 log 出的值會不同

使用等號常見疏漏

  1. var a = 10
  2. 執行到第三行,將 a 賦值為 20,看 if 條件然後 log 出 hi
var a = 10
if (a = 20) {
  console.log('hi')
}
// 結果會是 hi,等同下方代碼
a = 20
if (a) {
  console.log('hi')
}

三、== 與 === 的差別

兩個等號會轉換型態,因為三個等號不會轉換型態,所以型態不同答案就是 false。
推薦:每次都用 ===

console.log(2 === '2') // false
console.log(2 == '2') // true

差別三、===

對 object types 而言, === 成立只有在比較的變數都指向相同記憶體位置才成立。

var obj = {
  number: 1
}

var obj2 = obj
obj2.number = 2

console.log(obj === obj2) // true
  1. obj:0x01 其中 0x01: {number:1}
  2. obj2 = obj 所以 obj2:0x01 其中 0x01: {number:1}
  3. obj2.number = 2 所以指到的記憶體位置 0x01: {number:2}
  4. object types 做 === 比較的會是記憶體位置,而非實際的值
  5. 記憶體位置都是 0x01 所以是 true

同理,array is also object types.

var arr = [1]
var arr2 = [1]
console.log(arr === arr2) // false

var arr3 = []
var arr4 = []
console.log(arr3 === arr4) //false
  1. var arr = [1],arr:0x01 其中 0x01: [1] (將記憶體位置給 arr 讓他可以指到陣列去)
  2. var arr2 = [1],arr:0x02 其中 0x02: [1]
  3. 因為 === 比較的是記憶體位置,所以儘管記憶體位置裡的值相同,但仍印出 false
arr = [1]
arr2 = [1]
arr2 = arr
console.log(arr === arr2) // true

object types 做等號比較就是看他有沒有指向同樣地記憶體位置

console.log([] === []) // false
console.log({} === {}) // false

例外:NAN

NAN (not a number 其實還是一個 number),NAN 不等於任何東西,even 他自身

console.log(typeof NAN) // number
var a = Number('xxx')
console.log(a) // NAN

console.log( a === a) // false

可以透過 isNAN() 去知道他是不是 NAN,但這個函式在舊的瀏覽器並不支援。

參考資料:
js eqiality table

初次見面:let 與 const

一旦以 const 進行宣告,他的值就不能改變。以 const 宣告時就要一併給他初始值,不能之後再賦值。let、var 沒有上述雜七雜八的限制。

對 object types 而言,const 是鎖定記憶體位置,並不是鎖定記憶體位置裡的東西。

const b = {
  number : 1
}

b.number = 100 // 沒有錯誤提示
b = {number : 3} // TypeError: Assignment to constant variable.
  1. b:0x01 其中 0x01 : {number : 1}
  2. b.number = 100 是去記憶體位置 0x01 將 number 改成 100。也就是 b: 0x01 其中 0x01: {number: 100}
  3. b = {number : 3},重給一塊記憶體位置並在裡面放。也就是 b:0x02 其中 0x02: {number : 3}
const arr = [1, 2, 54]
arr.push('1')
arr[0] = 3 // 沒有改變 arr 記憶體位置,而是改變他指的那個記憶體位置的那個值
arr = [3] // TypeError: Assignment to constant variable
  1. const arr = [1, 2, 54] 也就是 arr: 0x01 其中 0x01: [1, 2, 54]
  2. arr[0] = 3 也就是 arr: 0x01 其中 0x01: [3, 2, 54]
  3. arr = [3] 也就是 arr: 0x02 其中 0x02: [3] 因為 const 的規則不允許,所以會出現 TypeError: Assignment to constant variable

三、變數的生存範圍:Scope

作用域也就是變數的生存範圍,一出這個範圍就無法存取到這個變數。scope chain 會依照程式碼的放置的位置而定,跟我在哪裡呼叫 function 無關。

我們會先聚焦在 ES6 以前的作用域。

ES6 以前變數的作用域基本單位是 function,只有 function 可以產生新的作用域。

a 宣告在 test scope 裡面,一旦出了 function,就不能被其他人看到,所以對於外面來說因為找不到 a,a 是 undefined

function test() {
  var a = 10 // a 宣告在 test scope 裡面
  console.log(a)
}

test()
console.log(a) // ReferenceError: a is not defined

全域變數可以想成是以檔案為單位,因為作用域是往上找,當 test 引擎在 test scope 裡面找不到 a,他就往上找。

var a = 20 // global variable

function test() {
  console.log(a) // 20
}

test()
console.log(a) // 20

因為會在 function 的作用域先找,所以 test 裡面的 a 是 10

var a = 20 // global variable

function test() {
  var a = 10
  console.log(a) // 10
}

test()
console.log(a) // 20

test 裡面的 a = 10,但沒有變數宣告,也就是沒有在 test scope 新增一個變數。往上層找,發現 global 有宣告 a,就將 global a 的值改成 10

var a = 20 // global variable

function test() {
   a = 10
  console.log(a) // 10
}

test()
console.log(a) // 10

同樣在 test() 裡面 a = 10,往上層找也找不到值,就自動幫我在 global 宣告變數。所以這時 a 被宣告成 global variable。
在 function 內使用變數一定要宣告,不然會變成 global variable。

function test() {
   a = 10 // global variable
  console.log(a) // 10
}

test()
console.log(a) // 10

往上找的過程構成了 scope chain。
inner scope => test scope => global scope

var a = 'global' // global scope

function test() { // scope
  var a = 'test scope a'
  var b = 'test scope b'
  console.log(a, b) // 'test scope a' 'test scope b'
  function inner() { // scope
    var b = 'inner scope b'
    console.log(a, b) // 'test scope a' 'inner scope b'
  }
  inner()
}

test()
console.log(a) // 'global'

作用域的判斷和在哪邊宣告有關係,和在哪邊呼叫 function 沒有關係。這個 test 和 change 是兩個平行的地方。
test scope -> global scope

var a = 'global' // global scope

function change() { // change scope : a
  var a = 10
  test()
}

function test() { // test scope
  console.log(a)
}

change() // 'global'

同理,test scope -> global scope

var a = 'global' // global scope

function change() { // change scope
  var a = 10
  function inner() { // inner scope
    var a = 'inner'
    test()
  }
  inner()
}

function test() { // test scope
  console.log(a)
}

change() // 'global'

四、let 與 const 的生存範圍

ES6 引進 let, const。因此 ES6 以前的作用域跟 ES6 以後略有不同。

  • ES6 前 : 作用域以 function 為基礎
  • ES6 以後 : let, const 宣告的變數,只要有 block { } 就會產生作用域

用 var 宣告,他的 scope 就是 test 這個 function。所以在 test 裡都可以用到 b 這個變數。

function test() {
  var a = 60
  if (a === 60) {
    var b = 10
  }
  console.log(b)
}

test() // 10

let 或 const 宣告變數,scope 會存在於 block 內。以這個例子 b 只有在 if 裡面才活著,出了 if 就存取不到。

function test() {
  var a = 60
  if (a === 60) {
    let b = 10
  }
  console.log(b)
}

test() // ReferenceError: b is not defined

因為 i 是用 var 宣告,所以在 test 裡面都有作用
i = 9 進入下一圈迴圈 i++ => i = 10,判斷條件 10 < 10 false,

function test() {

  for(var i=0; i<10; i++) {
    console.log('i', i)
  }
  console.log('final value', i) // 10
}

test()

i 用 let 宣告,所以 i 只存在 for loop 的 block 中。一脫離就存取不到這個 i。

function test() {

  for(let i=0; i<10; i++) {
    console.log('i', i)
  }
  console.log('final value', i) // ReferenceError: i is not defined
}

test()

重點整理

  1. var : function scope
  2. let/const : block scope

#scope #var #let #const #variables in js







Related Posts

引領團隊前進:北極星與路標們

引領團隊前進:北極星與路標們

Video Speed Controller UI

Video Speed Controller UI

在vue3+Naive UI下使用Vitest取得n-input輸入值

在vue3+Naive UI下使用Vitest取得n-input輸入值


Comments