第二章:值

原文: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 键/属性(但是这些属性不会计算在 arraylength 中):

var a = [];a[0] = 1;
a["foobar"] = 2;a.length; // 1
a["foobar"]; // 2
a.foobar; // 2
复制代码

然而,一个需要小心的坑是,如果一个可以被强制转换为 10 进制 numberstring 值被用作键的话,它会认为你想使用 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 的表现形式的话,你总是可以调用 字符的arrayjoin("") 方法。

3. Number

JavaScript 只有一种数字类型:number。这种类型包含“整数”值和小数值。我说“整数”时加了引号,因为 JS 的一个长久以来为人诟病的原因是,和其他语言不同,JS 没有真正的整数。这可能在未来某个时候会改变,但是目前,我们只有 number 可用。

所以,在 JS 中,一个“整数”只是一个没有小数部分的小数值。也就是说,42.042 一样是“整数”。

你可以直接在 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.10.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。所以对它们而言,这些文字既是它们的类型也是它们的值。

undefinednull 作为“空”值或者“没有”值,经常被认为是可以互换的。另一些开发者偏好于使用微妙的区别将它们区分开。举例来讲:

  • 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"
复制代码

有趣的是,反向操作(从 stringnumber)不会撒谎:

+"-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
复制代码

很明显,如果你想在你的代码中区分 -00,你就不能仅依靠开发者控制台的输出,你必须更聪明一些:

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 类型只有一个值 nullundefined 类型同样地只有 undefined 值。对于任何没有值存在的变量或属性,undefined 基本上是默认值。void 操作符允许你从任意另一个值中创建 undefined 值。

number 包含几种特殊值,比如 NaN(意为“不是一个数字”,但称为“非法数字”更合适);+Infinity-Infinity;还有 -0

简单基本标量(stringnumber 等)通过值拷贝进行赋值/传递,而复合值(object 等)通过引用拷贝进行赋值/传递。引用与其他语言中的引用/指针不同 —— 它们从不指向其他的变量/引用,而仅指向底层的值。

转载于:https://juejin.im/post/5ae9508bf265da0b7451f88b

你不懂js系列学习笔记-类型与文法- 02相关推荐

  1. 你不懂js系列学习笔记-类型与文法- 04

    第四章:强制转换 原文:You-Dont-Know-JS 1. 转换值 将一个值从一个类型明确地转换到另一个类型通常称为"类型转换(type casting)",当这个操作隐含地完 ...

  2. 你不懂js系列学习笔记-异步与性能- 02

    第二章:回调 原文:You-Dont-Know-JS 主要理解 "回调地狱(callback hell)"痛苦的点到底是哪,以及尝试拯救回调. 1. 首先从实际生活中模拟 我相信大 ...

  3. WebGL three.js学习笔记 6种类型的纹理介绍及应用

    WebGL three.js学习笔记 6种类型的纹理介绍及应用 本文所使用到的demo演示: 高光贴图Demo演示 反光效果Demo演示(因为是加载的模型,所以速度会慢) (一)普通纹理 计算机图形学 ...

  4. JS学习笔记(五)函数类型、箭头函数、arguments参数、标签函数

    JS学习笔记(五) 本系列更多文章,可以查看专栏 JS学习笔记 文章目录 JS学习笔记(五) 一.函数 1. 函数定义 2. 方法( 对象 + 函数 ) 二.函数参数及返回值 1. 传递原始类型参数 ...

  5. JS 学习笔记--8---Function类型

    练习使用的浏览器IE11   JS 中Function类型实际上是一种对象,每一个函数实际上都是Function类型的一个实例,每一个函数都有一些默认的属性和方法.由于函数是对象,故函数名实际上也是一 ...

  6. You Don’t Know JS 中文电子书 你不懂JS.pdf

    前言 我确信你注意到了,但是这个系列图书标题中的"JS"不是一个用来诅咒JavaScript的缩写,虽然有时我们可能都能看出它是在诅咒这门语言的怪异之处! 自从we b的最早期开始 ...

  7. 千锋Node.js学习笔记

    千锋Node.js学习笔记 文章目录 千锋Node.js学习笔记 写在前面 1. 认识Node.js 2. NVM 3. NPM 4. NRM 5. NPX 6. 模块/包与CommonJS 7. 常 ...

  8. React.js入门笔记

    # React.js入门笔记 核心提示 这是本人学习react.js的第一篇入门笔记,估计也会是该系列涵盖内容最多的笔记,主要内容来自英文官方文档的快速上手部分和阮一峰博客教程.当然,还有我自己尝试的 ...

  9. leanote 信息栏显示笔记本和笔记类型

    本文解决如下两个问题: 1. 在列表视图下使用搜索时,不知道搜出来的笔记属于哪个笔记本.(摘要视图下是有显示的) 2. 增加显示笔记类型(markdown 或 富文本) 修改resources\app ...

最新文章

  1. java ee 期末考试_JAVA EE 期末试题 (1)
  2. Sales and Distribution (SD)
  3. 深入理解javascript原型和闭包(2)——函数和对象的关系
  4. ubuntu8.0中文输入法
  5. SqlServer图形数据库初体验
  6. 【蓝桥杯官网试题 - 算法提高 】求最大值 (dp,0-1背包)
  7. Linux内存管理:内存分配:slab分配器
  8. 教你流程化梳理外贸工作(附18个全流程邮件模板分享)
  9. 有哪些有关java类最新发表的毕业论文呢?
  10. 迅捷路由器造成计算机无法上网,迅捷(FAST)300M无线路由器设置后不能上网的解决方法...
  11. 1000句最常用英语口语 (四)
  12. 一号店主页静态页面(HTML)
  13. 普通文档怎么换成php,wps只读文档怎么修改为普通文档
  14. 学习要趁早年轻要挣钱
  15. 体系结构13_Tomasulo算法
  16. 算法提高 素数环 java 题解 977
  17. 电脑磁盘右键没有新建文件夹???
  18. 从头开始搞懂 MySQL(07)为什么同一条 SQL 时快时慢
  19. 找出100以内的素数
  20. 《白帽子讲web安全》学习笔记(第一篇)

热门文章

  1. LeetCode 142 环形链表 II
  2. maven打包不用eclipse插件
  3. JS任务队列--笔记
  4. 移动端微信公众号开发中问题记录及解决方案
  5. yeoman生成react基本架构
  6. JavaScript单线程和浏览器事件循环简述
  7. Java排序之直接选择排序
  8. Tomcat学习--配置tomcat
  9. merlin.acs的使用方法 merlin.acs添加右键菜单
  10. 详解数据管理发展的5个阶段