iT邦幫忙

1

【You Don't Know JS: Types & Grammar】Chapter 2 筆記

WM 2019-02-23 22:53:55957 瀏覽

陣列(Array)

可以將陣列視為能夠裝載任何值的容器,不論是number , string , object,甚至是另一個陣列(產生多維陣列)。
https://ithelp.ithome.com.tw/upload/images/20190221/20112573fYMvtrBedY.jpg

無需為陣列先預設大小,它會自動維護length的值。
https://ithelp.ithome.com.tw/upload/images/20190221/20112573Qi0ZSsIxwa.jpg

請避免建立「稀疏(sparse)」陣列,使陣列產生空插槽(empty slots)。
https://ithelp.ithome.com.tw/upload/images/20190221/20112573ZVeITLDIgO.jpg

稀疏陣列不會發生錯誤,但請避免這樣的方式。

稀疏陣列的a[1]顯示undefined,但這跟我們明確指定a[1] = undefined還是有所不同。

陣列最大的特點就是以數值來索引的,但陣列也是物件,所以它也擁有物件的行為「string鍵值(keys)/屬性(properties)」。

如若使用string鍵值,length將不列入計算。
https://ithelp.ithome.com.tw/upload/images/20190221/20112573rvHh5HnRCF.jpg

如果,鍵值能夠被強制轉型為十進位的number,那JS就會將之當作number索引使用,而非string鍵值。
https://ithelp.ithome.com.tw/upload/images/20190221/20112573yfSV3b2yqa.jpg

雖然陣列可以使用string鍵值(keys)/屬性(properties),然而,陣列最大的特點就是使用number作為索引。

與其如此,不如直接使用object就好。

類陣列(Array-Links)

以數值做索引的集合,但非真正的陣列。

例如,DOM的查詢,都會回傳由DOM元素所組成的串列(list),但非陣列。

如果我們要進行轉換,可使用ES6的新功能Array.form(),會在ES6筆記另行介紹。

字串(string)

注意,這觀點修正了筆者之前對字串(string)的認知

一般的認知是「string是字元(characters)所組成的陣列」,表面看似如此,但實際上有些行為卻是大不相同。
https://ithelp.ithome.com.tw/upload/images/20190221/201125732LrXR4OZd2.jpg

以上的結果都一樣,似乎string就是字元(characters)所組成的陣列。

但,不盡然如此。

https://ithelp.ithome.com.tw/upload/images/20190221/20112573vKkGSzWXQi.jpg

字串的"o"依然是小寫,但陣列卻改變了,這正是字串與陣列最大的差異點。

JS中的字串是不可變(immutable)的,而陣列是可變(mutable)的

另外,a[1]這種方式,並非洽當,較正確的作法是a.charAt(1)

不可變的意思是,字串的內容無法就地(in-place)修正,而是會回傳一個新的字串。

可變的意思是,陣列的內容可以就地修正。

https://ithelp.ithome.com.tw/upload/images/20190221/20112573KCAekAoytw.jpg

我們也可以使用不會更動內容的陣列方法join( )以及map( )

因為目標為字串,所以我們藉由Function.prototype.call來呼叫陣列方法。

https://ithelp.ithome.com.tw/upload/images/20190221/20112573SN4K0fY8pT.jpg

反轉字串(reversing string)

字串並沒有像陣列一樣擁有反轉方法reverse( )

因為字串是不可變的,所以也無法使用Array.prototype.reverse.call(a),會擲出錯誤。

變通就是將字串轉為陣列,再反轉,再轉為字串。
https://ithelp.ithome.com.tw/upload/images/20190221/20112573tUyo5LjMXW.jpg

如果我們對於字串的處理較常當作陣列的對待的話,比較好的方式是直接把字串儲存為陣列,而非字串,省去轉來轉的去的麻煩,若需要使用字串表示,再使用join()方法轉成字串。

數值(number)
JS中只有一種數值型別「number」。包含整數以及帶有小數點的十進位數字。

JS並沒有真正的整數,89跟89.0一樣都是整數。

JS number的實作是基於「IEEE754」標準的「雙精度(double precision)」格式(64位元的二進位數字)。

數值語法

數字字面值通常以10為基底(base-10)表示。
https://ithelp.ithome.com.tw/upload/images/20190221/201125734IAC4dQlzf.jpg

小數點之後的0會被移除。
https://ithelp.ithome.com.tw/upload/images/20190221/20112573dHRs4LQAWS.jpg

非常大或非常小的數值預設會以指數形式輸出,與toExponential()方法輸出的格式相同。
https://ithelp.ithome.com.tw/upload/images/20190221/20112573IOmdwn0YXK.jpg

toFixed()方法能夠指定使用幾個小數位數。
https://ithelp.ithome.com.tw/upload/images/20190221/201125737XL7xOxFcz.jpg
注意,輸出的型別為字串,若要求超過數值本身的位數,會以0補齊。

toPrecision()方法指定的是多少位的有效數字(significant digits)
https://ithelp.ithome.com.tw/upload/images/20190221/20112573OrfYIy7G7i.jpg

也可以不透過變數,直接在數字字面值上處理,不過這種方式相當少見。

也可以使用指數形式來表示很大的字面值。
https://ithelp.ithome.com.tw/upload/images/20190221/20112573WeEUdxmBa3.jpg

小數值的陷阱
https://ithelp.ithome.com.tw/upload/images/20190221/201125738jecJ9137j.jpg

從數學上來說0.1+0.2=0.3是絕對成立的,但為何答案是false?

原因就在於IEEE754的二進位浮點數表示並不是完全100%的精確,還是有極小的誤差。
https://ithelp.ithome.com.tw/upload/images/20190221/20112573ccylc8yo1J.jpg

所以,當處理小數值的時候,要特別注意這種情況,但大部分處理整數的時候,JS的數值表示是安全的。

如果真的要比較0.1+0.2與0.3的話,我們可以使用「約整誤差(rounding error)」值作為比較誤差的容許值(2^-52 (2.220446049250313e-16) )。

ES6的Number.EPSILON已經預先定義該容許值

所以我們可以這樣使用:
https://ithelp.ithome.com.tw/upload/images/20190221/20112573CBI1b7fU7N.jpg

Number.MAX_VALUE,能夠表示的最大浮點數。
Number.MIN_VALUE,能夠表示的最小浮點數。
https://ithelp.ithome.com.tw/upload/images/20190221/20112573F8bSflD1Mx.jpg

安全的整數範圍

JS中number值的表示,有一個安全範圍(所請求的值都可以精確地表示)

ES6中已預先定義安全值。

Number.MAX_SAFE_INTEGER,能夠表示的最大安全值。
Number.MIN_SAFE_INTEGER,能夠表示的最小安全值。
https://ithelp.ithome.com.tw/upload/images/20190221/20112573gYL85EDm6e.jpg

JS遇到大數字的情況大多是處理資料庫的64位元ID,此類的值無法以number精確地表示出來,所以在JS中都以string儲存或傳輸。

測試整數

若要測試某個值是否為整數,可使用ES6規格的Number.isInteger( )
https://ithelp.ithome.com.tw/upload/images/20190221/201125736tA3mq8zNL.jpg

若要測試某個值是否為安全整數(safe integer),可使用ES6規格的Number.isSafeInteger( )
https://ithelp.ithome.com.tw/upload/images/20190221/201125738boPqplV6o.jpg

特殊值

undefined型別只有「undefined」值。

null型別只有「null」值。

undefined與null的差異:

  • null是一個空值。
  • undefined是一個缺少(missing)的值。

或是

  • undefined代表尚未有值。
  • null代表曾有過值,但現在沒有了。

null是一個關鍵字(keyword),而非識別字(identifier),意味著它不能是變數。

undefined是識別字,在非strict模式中,可以指定值給undefined識別字,但非常不建議這樣做。

NaN

執行數學運算,若無法產生一個有效值,此時就會得到NaN值。

NaN按照字面上解釋,代表「not a number」(不是一個數字)。但更為洽當的解釋應該將之視為「無效的數字」、「不合格的數字」或是「壞掉的數字」更為適合。

https://ithelp.ithome.com.tw/upload/images/20190221/20112573rCEun5prIg.jpg

既然已經明確指出a為NaN,但它的型別卻是number,這是令人比較困惑的地方。

發生NaN可以解釋的情況是「我們嘗試做數學運算,但失敗了,所以得到一個失敗的number結果」。

如果我們想要確認變數是否為NaN值的話,使用下列方式會導致失敗。
https://ithelp.ithome.com.tw/upload/images/20190221/20112573AZIOogQeLn.jpg

NaN有個特別之處就是,它永遠都不等於自己。

可以使用isNaN( )來測試。
https://ithelp.ithome.com.tw/upload/images/20190221/20112573biuQZMYTos.jpg

問題看似解決了,卻還是有缺陷。
https://ithelp.ithome.com.tw/upload/images/20190221/20112573NwmwukI6kU.jpg
"foo"的確不是number,但它也不是NaN。會造成這結果主要是因為isNaN( )太過依賴於NaN這個字面上的意義了。

ES6提供了一個解決方案,Number.isNaN( )方法,我們還可以利用它永遠都不等於自己的特性來判斷。
https://ithelp.ithome.com.tw/upload/images/20190221/20112573Fp8aWRJ7ZD.jpg

雖然看似奇怪,但的確可行。

無限值

在傳統的程式語言執行除以0的數學運算,會發生除以0的錯誤。

但在JS中,並不會發生錯誤,而是會產Infinity/-Infinity值。
https://ithelp.ithome.com.tw/upload/images/20190221/20112573ScsFxtS7vg.jpg

ES6以經預先定義了無限值。
https://ithelp.ithome.com.tw/upload/images/20190221/20112573T989NZKlYc.jpg

JS使用的是有限的數值表示法,所以像是數學運算的結果,有可能會產生溢位(overflow)。
https://ithelp.ithome.com.tw/upload/images/20190221/20112573yQuYAZdZzG.jpg

如果運算結果產生了超出顯示範圍的值,就會依據IEEE754的「約整至最接近值(round-to-nearest)」的規範來決定顯示結果。

a + Math.pow( 2, 969 )的結果比較接近Number.MAX_VALUE,而非Infinity,所以會「向下約整(rounds down)」,顯示Number.MAX_VALUE。

a + Math.pow( 2, 969 )的結果比較接近Infinity,所以會「向上約整(rounds up)」,顯示Infinity。

從數學角度來看,無限除以無限是一個無法定義的運算,所以,Infinity/Infinity的結果會是NaN。

若把任何有限的number除以Infinity,那結果會是0。
https://ithelp.ithome.com.tw/upload/images/20190221/201125733gGVbxMrpA.jpg

特殊相等性

ES6新增一個測試特殊值相等性的方法,Object.is( )方法。

它可以測試NaN與-0的值是否相等。
https://ithelp.ithome.com.tw/upload/images/20190223/20112573q9DB5WDVec.jpg

值 & 參考

值可藉由值的拷貝(value-copy)或參考的拷貝(reference-copy)來指定或傳遞。

在JS中,沒有指標(pointers)這種機制,無法從某個變數指向另一個變數的參考。

在JS中,值的型別完全決定該值是藉由值的拷貝(by value-copy)或參考的拷貝(by reference-copy)來指定的。

https://ithelp.ithome.com.tw/upload/images/20190223/20112573P2xFXsJs8R.jpg

純量的基型值(primitives)永遠都是藉由值的拷貝(by value-copy)來指定或傳遞的:null、undefined、string、number、boolean。

複合值的object(array、封裝的物件包裹器boxed object wrappers、與function),永遠都是藉由參考的拷貝(by reference-copy)來指定或傳遞的。

以上面的例子來說,a的值是2,為純量的基型值(primitives),b就會被指定該值的另一份拷貝,變更b時,不會動到a的值。

c和d是指向[1,2,3]的個別參考,兩者所參考的都是同一個[1,2,3],當透過其中一個參考來更動array時,影響到的是唯一共有值[1,2,3],而兩者之後都會參考到經過修改的新值[1,2,3,4]。

https://ithelp.ithome.com.tw/upload/images/20190223/20112573lHC2EqGDBI.jpg
一開始a跟b都指向同一個值[1,2,3],之後我們將b的參考指向另一個值[4,5,6],這並不會影響到a的參考。
這時,a跟b個別指向不同的值。

function foo(x) {
	x.push( 4 );
	x; //[1,2,3,4]
	
	x = [4,5,6];
	x.push( 7 );
	x; //[4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; //[1,2,3,4]

當我們將a傳給foo( )時,它會將a的參考拷貝給x,所以ax是指向同一個值([1,2,3])的不同參考。接下來,我們使用push( 4 )來變更值。

但是,執行x = [4,5,6]的時候,我們只是把x的參考指向[4,5,6]a的參考並無變動。

所以,我們無法透過x的參考來改變a要指向哪個值。我們只能修改ax所指向共有值的內容。

如若要讓a持有[4,5,6,7],就必須修改a所指向的值的內容。

function foo(x) {
	x.push( 4 );
	x; //[1,2,3,4]

	x.length = 0;
	x.push( 4, 5, 6, 7 );
	x; //[4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; //[4,5,6,7]

執行x.length = 0x.push( 4, 5, 6, 7 )會直接修改共有的array,所以a自然就會持有[4,5,6,7]

注意,我們無法控制值拷貝(by value-copy)或參考拷貝(by reference-copy)的行為,一切都由值的型別來決定。

如果我們希望,array的值,能夠有值拷貝(by value-copy)行為的話,就必須透過slice( )方法,它能複製出所指向值的淺層拷貝,以上面的例子來說,foo( a.slice() ),所傳入的是藉由slice( )製作出來的a參考的拷貝,並非a的參考,因此不會影響a所參考的值。

如果,我們希望基型值(primitives)能有參考拷貝(by reference-copy)的行為該怎麼做?

那就得將值包在能有有參考拷貝(by reference-copy)行為的型別(object、array)之中了。

function foo(wrapper) {
	wrapper.a = 42;
}
var obj = {
	a: 2
};
foo( obj );
obj.a; // 42

物件obj的屬性a值是基型值(primitives),將obj傳入foo( ),行為就像是參考拷貝(by reference-copy),透過wrapper.a的指定,也會影響obj.a。

JS並沒有指標,不會指向其他變數或參考,只會指向底層的值。

參考來源:
https://ithelp.ithome.com.tw/upload/images/20190221/201125739FY75WdXxA.jpg

此為You Don't Know JS系列的筆記。


尚未有邦友留言

立即登入留言