你不懂js系列学习笔记-类型与文法- 02
第二章:值
原文:You-Dont-Know-JS
JS 內建的值類型:
1. Array
和其他強制類型的語言相比,JavaScript 的 array 只是值的容器,而這些值可以是任何類型:string 或者 number 或者 object,甚至是另一個 array(這也是你得到多維數組的方法)。
var a = [1, "2", [3]];a.length; // 3 a[0] === 1; // true a[2][0] === 3; // true 復制代碼你不需要預先指定 array 的大小,你可以僅聲明它們并加入你覺得合適的值:
var a = [];a.length; // 0a[0] = 1; a[1] = "2"; a[2] = [3];a.length; // 3 復制代碼警告: 在一個 array 值上使用 delete 將會從這個 array 上移除一個值槽,但就算你移除了最后一個元素,它也 不會 更新 length 屬性,所以多加小心!我們會在第五章討論 delete 操作符的更多細節。
要小心創建“稀散”的 array(留下或創建空的/丟失的值槽):
var a = [];a[0] = 1; // 這里沒有設置值槽 `a[1]` a[2] = [3];a[1]; // undefineda.length; // 3 復制代碼雖然這可以工作,但你留下的“空值槽”可能會導致一些令人困惑的行為。雖然這樣的值槽看起來擁有 undefined 值,但是它不會像被明確設置(a[1] = undefined)的值槽那樣動作。更多信息可以參見第三章的“Array”。
array 是被數字索引的(正如你所想的那樣),但微妙的是它們也是對象,可以在它們上面添加 string 鍵/屬性(但是這些屬性不會計算在 array 的 length 中):
var a = [];a[0] = 1; a["foobar"] = 2;a.length; // 1 a["foobar"]; // 2 a.foobar; // 2 復制代碼然而,一個需要小心的坑是,如果一個可以被強制轉換為 10 進制 number 的 string 值被用作鍵的話,它會認為你想使用 number 索引而不是一個 string 鍵!
var a = [];a["13"] = 42;a.length; // 14 復制代碼一般來說,向 array 添加 string 鍵/屬性不是一個好主意。最好使用 object 來持有鍵/屬性形式的值,而將 array 專用于嚴格地數字索引的值。
類 Array
偶爾你需要將一個類 array 值(一個數字索引的值的集合)轉換為一個真正的 array,通常你可以對這些值的集合調用數組的工具函數(比如 indexOf(..)、concat(..)、forEach(..) 等等)。
舉個例子,各種 DOM 查詢操作會返回一個 DOM 元素的列表,對于我們轉換的目的來說,這些列表不是真正的 array 但是也足夠類似 array。另一個常見的例子是,函數為了像列表一樣訪問它的參數值,而暴露了 arugumens 對象(類 array,在 ES6 中被廢棄了)。
一個進行這種轉換的很常見的方法是對這個值借用 slice(..) 工具:
function foo() {var arr = Array.prototype.slice.call(arguments);arr.push("bam");console.log(arr); }foo("bar", "baz"); // ["bar","baz","bam"] 復制代碼如果 slice() 沒有用其他額外的參數調用,就像上面的代碼段那樣,它的參數的默認值會使它具有復制這個 array(或者,在這個例子中,是一個類 array)的效果。
在 ES6 中,還有一種稱為 Array.from(..) 的內建工具可以執行相同的任務:
... var arr = Array.from( arguments ); ... 復制代碼注意: Array.from(..) 擁有其他幾種強大的能力,我們將在本系列的 ES6 與未來 中涵蓋它的細節。
2. String
一個很常見的想法是,string 實質上只是字符的 array。雖然內部的實現可能是也可能不是 array,但重要的是要理解 JavaScript 的 string 與字符的 array 確實不一樣。它們的相似性幾乎只是表面上的。
舉個例子,讓我們考慮這兩個值:
var a = "foo"; var b = ["f", "o", "o"]; 復制代碼String 確實與 array 有很膚淺的相似性 -- 也就是上面說的,類 array -- 舉例來說,它們都有一個 length 屬性,一個 indexOf(..) 方法(在 ES5 中僅有 array 版本),和一個 concat(..) 方法:
a.length; // 3 b.length; // 3a.indexOf("o"); // 1 b.indexOf("o"); // 1var c = a.concat("bar"); // "foobar" var d = b.concat(["b", "a", "r"]); // ["f","o","o","b","a","r"]a === c; // false b === d; // falsea; // "foo" b; // ["f","o","o"] 復制代碼那么,它們基本上都僅僅是“字符的數組”,對吧? 不確切:
a[1] = "O"; b[1] = "O";a; // "foo" b; // ["f","O","o"] 復制代碼JavaScript 的 string 是不可變的,而 array 是相當可變的。另外,在 JavaScript 中用位置訪問字符的 a[1] 形式不總是廣泛合法的。老版本的 IE 就不允許這種語法(但是它們現在允許了)。相反,正確的 方式是 a.charAt(1)。
string 不可變性的進一步的后果是,string 上沒有一個方法是可以原地修改它的內容的,而是創建并返回一個新的 string。與之相對的是,許多改變 array 內容的方法實際上 是 原地修改的。
c = a.toUpperCase(); a === c; // false a; // "foo" c; // "FOO"b.push("!"); b; // ["f","O","o","!"] 復制代碼另外,許多 array 方法在處理 string 時非常有用,雖然這些方法不屬于 string,但我們可以對我們的 string “借用”非變化的 array 方法:
a.join; // undefined a.map; // undefinedvar c = Array.prototype.join.call(a, "-"); var d = Array.prototype.map.call(a, function(v) {return v.toUpperCase() + ".";}).join("");c; // "f-o-o" d; // "F.O.O." 復制代碼讓我們來看另一個例子:翻轉一個 string(順帶一提,這是一個 JavaScript 面試中常見的細節問題!)。array 擁有一個原地的 reverse() 修改器方法,但是 string 沒有:
a.reverse; // undefinedb.reverse(); // ["!","o","O","f"] b; // ["!","o","O","f"] 復制代碼不幸的是,這種“借用” array 修改器不起作用,因為 string 是不可變的,因此它不能被原地修改:
Array.prototype.reverse.call(a); // 仍然返回一個“foo”的 String 對象包裝器(見第三章) :( 復制代碼另一種迂回的做法(也是黑科技)是,將 string 轉換為一個 array,實施我們想做的操作,然后將它轉回 string。
var c = a// 將 `a` 切分成一個字符的數組.split("")// 翻轉字符的數組.reverse()// 將字符的數組連接回一個字符串.join("");c; // "oof" 復制代碼如果你覺得這很難看,沒錯。不管怎樣,對于簡單的 string 它 好用,所以如果你需要某些快速但是“臟”的東西,像這樣的方式經常能滿足你。
警告: 小心!這種方法對含有復雜(unicode)字符(星型字符、多字節字符等)的 string 不起作用。你需要支持 unicode 的更精巧的工具庫來準確地處理這種操作。在這個問題上可以咨詢 Mathias Bynens 的作品:Esrever(github.com/mathiasbyne…
另外一種考慮這個問題的方式是:如果你更經常地將你的“string”基本上作為 字符的數組 來執行一些任務的話,也許就將它們作為 array 而不是作為 string 存儲更好。你可能會因此省去很多每次都將 string 轉換為 array 的麻煩。無論何時你確實需要 string 的表現形式的話,你總是可以調用 字符的array 的 join("") 方法。
3. Number
JavaScript 只有一種數字類型:number。這種類型包含“整數”值和小數值。我說“整數”時加了引號,因為 JS 的一個長久以來為人詬病的原因是,和其他語言不同,JS 沒有真正的整數。這可能在未來某個時候會改變,但是目前,我們只有 number 可用。
所以,在 JS 中,一個“整數”只是一個沒有小數部分的小數值。也就是說,42.0 和 42 一樣是“整數”。
你可以直接在 number 的字面上訪問這些方法。但你不得不小心 . 操作符。因為 . 是一個合法數字字符,如果可能的話,它會首先被翻譯為 number 字面的一部分,而不是被翻譯為屬性訪問操作符。
// 不合法的語法: 42.toFixed( 3 ); // SyntaxError// 這些都是合法的: (42).toFixed( 3 ); // "42.000" 0.42.toFixed( 3 ); // "0.420" 42..toFixed( 3 ); // "42.000" 復制代碼42.toFixed(3) 是不合法的語法,因為 . 作為 42. 字面(這是合法的 -- 參見上面的討論!)的一部分被吞掉了,因此沒有 . 屬性操作符來表示 .toFixed 訪問。
42..toFixed(3) 可以工作,因為第一個 . 是 number 的一部分,而第二個 . 是屬性操作符。但它可能看起來很古怪,而且確實在實際的 JavaScript 代碼中很少會看到這樣的東西。實際上,在任何基本類型上直接訪問方法是十分不常見的。但是不常見并不意味著 壞 或者 錯。
使用二進制浮點數的最出名(臭名昭著)的副作用是(記住,這是對 所有 使用 IEEE 754 的語言都成立的 —— 不是許多人認為/假裝 僅 在 JavaScript 中存在的問題):
0.1 + 0.2 === 0.3; // false 復制代碼簡單地說,0.1 和 0.2 的二進制表示形式是不精確的,所以它們相加時,結果不是精確地 0.3。而是 非常 接近的值:0.30000000000000004,但是如果你的比較失敗了,“接近”是無關緊要的。
我們可以使用這個 Number.EPSILON 來比較兩個 number 的“等價性”(帶有錯誤舍入的容差):
function numbersCloseEnoughToEqual(n1, n2) {return Math.abs(n1 - n2) < Number.EPSILON; }var a = 0.1 + 0.2; var b = 0.3;numbersCloseEnoughToEqual(a, b); // true numbersCloseEnoughToEqual(0.0000001, 0.0000002); // false 復制代碼測試整數
測試一個值是否是整數,你可以使用 ES6 定義的 Number.isInteger(..):
Number.isInteger(42); // true Number.isInteger(42.0); // true Number.isInteger(42.3); // false 復制代碼可以為前 ES6 填補 Number.isInteger(..):
if (!Number.isInteger) {Number.isInteger = function(num) {return typeof num == "number" && num % 1 == 0;}; } 復制代碼要測試一個值是否是 安全整數,使用 ES6 定義的 Number.isSafeInteger(..):
Number.isSafeInteger(Number.MAX_SAFE_INTEGER); // true Number.isSafeInteger(Math.pow(2, 53)); // false Number.isSafeInteger(Math.pow(2, 53) - 1); // true 復制代碼可以為前 ES6 瀏覽器填補 Number.isSafeInteger(..):
if (!Number.isSafeInteger) {Number.isSafeInteger = function(num) {return Number.isInteger(num) && Math.abs(num) <= Number.MAX_SAFE_INTEGER;}; } 復制代碼4. 特殊值
4.1 undefined
對于 undefined 類型來說,有且僅有一個值:undefined。對于 null 類型來說,有且僅有一個值:null。所以對它們而言,這些文字既是它們的類型也是它們的值。
undefined 和 null 作為“空”值或者“沒有”值,經常被認為是可以互換的。另一些開發者偏好于使用微妙的區別將它們區分開。舉例來講:
- null 是一個空值
- undefined 是一個丟失的值
或者:
- undefined 還沒有值
- null 曾經有過值但現在沒有
不管你選擇如何“定義”和使用這兩個值,null 是一個特殊的關鍵字,不是一個標識符,因此你不能將它作為一個變量對待來給它賦值(為什么你要給它賦值呢?!)。然而,undefined(不幸地)是 一個標識符。噢。
4.2 NaN
NaN 在字面上代表“不是一個 number(Not a Number)”,但是正如我們即將看到的,這種文字描述十分失敗而且容易誤導人。將 NaN 考慮為“不合法數字”,“失敗的數字”,甚至是“壞掉的數字”都要比“不是一個數字”準確得多。
var a = 2 / "foo"; // NaNtypeof a === "number"; // true 復制代碼換句話說:“‘不是一個數字’的類型是‘數字’”!NaN 是一種“哨兵值”(一個被賦予了特殊意義的普通的值),它代表 number 集合內的一種特殊的錯誤情況。這種錯誤情況實質上是:“我試著進行數學操作但是失敗了,而這就是失敗的 number 結果。”
var a = 2 / "foo";a == NaN; // false a === NaN; // false 復制代碼NaN 是一個非常特殊的值,它從來不會等于另一個 NaN 值(也就是,它從來不等于它自己)。實際上,它是唯一一個不具有反射性的值(沒有恒等性 x === x)。所以,NaN !== NaN。
那么,如果不能與 NaN 進行比較(因為這種比較將總是失敗),我們該如何測試它呢?
var a = 2 / "foo";isNaN(a); // true 復制代碼isNaN(..) 工具有一個重大缺陷。
var a = 2 / "foo"; var b = "foo";a; // NaN b; // "foo"window.isNaN(a); // true window.isNaN(b); // true -- 噢! 復制代碼很明顯,"foo" 根本 不是一個 number,但它也絕不是一個 NaN 值!這個 bug 從最開始的時候就存在于 JS 中了(存在超過了十九年的坑)。
在 ES6 中,終于提供了一個替代它的工具:Number.isNaN(..)。有一個簡單的填補,可以讓你即使是在前 ES6 的瀏覽器中安全地檢查 NaN 值:
if (!Number.isNaN) {Number.isNaN = function(n) {return typeof n === "number" && window.isNaN(n);}; }var a = 2 / "foo"; var b = "foo";Number.isNaN(a); // true Number.isNaN(b); // false -- 咻! 復制代碼實際上,通過利用 NaN 與它自己不相等這個特殊的事實,我們可以更簡單地實現 Number.isNaN(..) 的填補。在整個語言中 NaN 是唯一一個這樣的值;其他的值都總是 等于它自己。
if (!Number.isNaN) {Number.isNaN = function(n) {return n !== n;}; } 復制代碼4.3 Infinity
來自于像 C 這樣的傳統編譯型語言的開發者,可能習慣于看到編譯器錯誤或者是運行時異常,比如對這樣一個操作給出的“除數為 0”:
var a = 1 / 0; 復制代碼然而在 JS 中,這個操作是明確定義的,而且它的結果是值 Infinity(也就是 Number.POSITIVE_INFINITY)。意料之中的是:
var a = 1 / 0; // Infinity var b = -1 / 0; // -Infinity 復制代碼一旦你溢出了任意一個 無限值,那么,就沒有回頭路了。換句最有詩意的話說,你可以從有限邁向無限,但不能從無限回歸有限。
“無限除以無限等于什么”,這簡直是一個哲學問題。我們幼稚的大腦可能會說“1”或“無限”。事實表明它們都不對。在數學上和在 JavaScript 中,Infinity / Infinity 不是一個有定義的操作。在 JS 中,它的結果為 NaN。
一個有限的正 number 除以 Infinity 呢?簡單!0。那一個有限的負 number 處理 Infinity 呢?接著往下讀!
4.4 零
雖然這可能使有數學頭腦的讀者困惑,但 JavaScript 擁有普通的零 0(也稱為正零 +0) 和 一個負零 -0。在我們講解為什么 -0 存在之前,我們應該考察 JS 如何處理它,因為它可能十分令人困惑。
除了使用字面量 -0 指定,負的零還可以從特定的數學操作中得出。比如:
var a = 0 / -3; // -0 var b = 0 * -3; // -0 復制代碼加法和減法無法得出負零。
在開發者控制臺中考察一個負的零,經常顯示為 -0,然而直到最近這才是一個常見情況,所以一些你可能遇到的老版本瀏覽器也許依然將它報告為 0。
但是根據語言規范,如果你試著將一個負零轉換為字符串,它將總會被報告為 "0"。
var a = 0 / -3;// 至少(有些瀏覽器)控制臺是對的 a; // -0// 但是語言規范堅持要向你撒謊! a.toString(); // "0" a + ""; // "0" String(a); // "0"// 奇怪的是,就連 JSON 也加入了騙局之中 JSON.stringify(a); // "0" 復制代碼有趣的是,反向操作(從 string 到 number)不會撒謊:
+"-0"; // -0 Number("-0"); // -0 JSON.parse("-0"); // -0 復制代碼警告: 當你觀察的時候,JSON.stringify( -0 ) 產生 "0" 顯得特別奇怪,因為它與反向操作不符:JSON.parse( "-0" ) 將像你期望地那樣報告-0。
除了一個負零的字符串化會欺騙性地隱藏它實際的值外,比較操作符也被設定為(有意地) 要說謊。
var a = 0; var b = 0 / -3;a == b; // true -0 == 0; // truea === b; // true -0 === 0; // true0 > -0; // false a > b; // false 復制代碼很明顯,如果你想在你的代碼中區分 -0 和 0,你就不能僅依靠開發者控制臺的輸出,你必須更聰明一些:
function isNegZero(n) {n = Number(n);return n === 0 && 1 / n === -Infinity; }isNegZero(-0); // true isNegZero(0 / -3); // true isNegZero(0); // false 復制代碼那么,除了學院派的細節以外,我們為什么需要一個負零呢?
在一些應用程序中,開發者使用值的大小來表示一部分信息(比如動畫中每一幀的速度),而這個 number 的符號來表示另一部分信息(比如移動的方向)。
在這些應用程序中,舉例來說,如果一個變量的值變成了 0,而它丟失了符號,那么你就丟失了它是從哪個方向移動到 0 的信息。保留零的符號避免了潛在的意外信息丟失。
4.5 特殊等價
正如我們上面看到的,當使用等價性比較時,值 NaN 和值 -0 擁有特殊的行為。NaN 永遠不會和自己相等,所以你不得不使用 ES6 的 Number.isNaN(..)(或者它的填補)。相似地,-0 撒謊并假裝它和普通的正零相等(即使使用 === 嚴格等價 —— 見第四章),所以你不得不使用我們上面建議的某些 isNegZero(..) 黑科技工具。
在 ES6 中,有一個新工具可以用于測試兩個值的絕對等價性,而沒有任何這些例外。它稱為 Object.is(..):
var a = 2 / "foo"; var b = -3 * 0;Object.is(a, NaN); // true Object.is(b, -0); // trueObject.is(b, 0); // false 復制代碼對于前 ES6 環境,這是一個相當簡單的 Object.is(..) 填補:
if (!Object.is) {Object.is = function(v1, v2) {// 測試 `-0`if (v1 === 0 && v2 === 0) {return 1 / v1 === 1 / v2;}// 測試 `NaN`if (v1 !== v1) {return v2 !== v2;}// 其他情況return v1 === v2;}; } 復制代碼Object.is(..) 可能不應當用于那些 == 或 === 已知 安全 的情況(見第四章“強制轉換”),因為這些操作符可能高效得多,并且更慣用/常見。Object.is(..) 很大程度上是為這些特殊的等價情況準備的。
復習
在 JavaScript 中,array 僅僅是數字索引的集合,可以容納任何類型的值。string 是某種“類 array”,但它們有著不同的行為,如果你想要將它們作為 array 對待的話,必須要小心。JavaScript 中的數字既包括“整數”也包括浮點數。
幾種特殊值被定義在基本類型內部。
null 類型只有一個值 null,undefined 類型同樣地只有 undefined 值。對于任何沒有值存在的變量或屬性,undefined 基本上是默認值。void 操作符允許你從任意另一個值中創建 undefined 值。
number 包含幾種特殊值,比如 NaN(意為“不是一個數字”,但稱為“非法數字”更合適);+Infinity 和 -Infinity;還有 -0。
簡單基本標量(string、number 等)通過值拷貝進行賦值/傳遞,而復合值(object 等)通過引用拷貝進行賦值/傳遞。引用與其他語言中的引用/指針不同 —— 它們從不指向其他的變量/引用,而僅指向底層的值。
轉載于:https://juejin.im/post/5ae9508bf265da0b7451f88b
總結
以上是生活随笔為你收集整理的你不懂js系列学习笔记-类型与文法- 02的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Dataguard学习笔记
- 下一篇: Git 别名(分布式版本控制系统)