你不懂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相关推荐
- 你不懂js系列学习笔记-类型与文法- 04
第四章:强制转换 原文:You-Dont-Know-JS 1. 转换值 将一个值从一个类型明确地转换到另一个类型通常称为"类型转换(type casting)",当这个操作隐含地完 ...
- 你不懂js系列学习笔记-异步与性能- 02
第二章:回调 原文:You-Dont-Know-JS 主要理解 "回调地狱(callback hell)"痛苦的点到底是哪,以及尝试拯救回调. 1. 首先从实际生活中模拟 我相信大 ...
- WebGL three.js学习笔记 6种类型的纹理介绍及应用
WebGL three.js学习笔记 6种类型的纹理介绍及应用 本文所使用到的demo演示: 高光贴图Demo演示 反光效果Demo演示(因为是加载的模型,所以速度会慢) (一)普通纹理 计算机图形学 ...
- JS学习笔记(五)函数类型、箭头函数、arguments参数、标签函数
JS学习笔记(五) 本系列更多文章,可以查看专栏 JS学习笔记 文章目录 JS学习笔记(五) 一.函数 1. 函数定义 2. 方法( 对象 + 函数 ) 二.函数参数及返回值 1. 传递原始类型参数 ...
- JS 学习笔记--8---Function类型
练习使用的浏览器IE11 JS 中Function类型实际上是一种对象,每一个函数实际上都是Function类型的一个实例,每一个函数都有一些默认的属性和方法.由于函数是对象,故函数名实际上也是一 ...
- You Don’t Know JS 中文电子书 你不懂JS.pdf
前言 我确信你注意到了,但是这个系列图书标题中的"JS"不是一个用来诅咒JavaScript的缩写,虽然有时我们可能都能看出它是在诅咒这门语言的怪异之处! 自从we b的最早期开始 ...
- 千锋Node.js学习笔记
千锋Node.js学习笔记 文章目录 千锋Node.js学习笔记 写在前面 1. 认识Node.js 2. NVM 3. NPM 4. NRM 5. NPX 6. 模块/包与CommonJS 7. 常 ...
- React.js入门笔记
# React.js入门笔记 核心提示 这是本人学习react.js的第一篇入门笔记,估计也会是该系列涵盖内容最多的笔记,主要内容来自英文官方文档的快速上手部分和阮一峰博客教程.当然,还有我自己尝试的 ...
- leanote 信息栏显示笔记本和笔记类型
本文解决如下两个问题: 1. 在列表视图下使用搜索时,不知道搜出来的笔记属于哪个笔记本.(摘要视图下是有显示的) 2. 增加显示笔记类型(markdown 或 富文本) 修改resources\app ...
最新文章
- java ee 期末考试_JAVA EE 期末试题 (1)
- Sales and Distribution (SD)
- 深入理解javascript原型和闭包(2)——函数和对象的关系
- ubuntu8.0中文输入法
- SqlServer图形数据库初体验
- 【蓝桥杯官网试题 - 算法提高 】求最大值 (dp,0-1背包)
- Linux内存管理:内存分配:slab分配器
- 教你流程化梳理外贸工作(附18个全流程邮件模板分享)
- 有哪些有关java类最新发表的毕业论文呢?
- 迅捷路由器造成计算机无法上网,迅捷(FAST)300M无线路由器设置后不能上网的解决方法...
- 1000句最常用英语口语 (四)
- 一号店主页静态页面(HTML)
- 普通文档怎么换成php,wps只读文档怎么修改为普通文档
- 学习要趁早年轻要挣钱
- 体系结构13_Tomasulo算法
- 算法提高 素数环 java 题解 977
- 电脑磁盘右键没有新建文件夹???
- 从头开始搞懂 MySQL(07)为什么同一条 SQL 时快时慢
- 找出100以内的素数
- 《白帽子讲web安全》学习笔记(第一篇)