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

本文深入探讨JavaScript中的值类型,包括Array、String、Number等的基本特性和使用细节,以及undefined、NaN、Infinity等特殊值的处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第二章:值

原文: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; // 0

a[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]; // undefined

a.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; // 3

a.indexOf("o"); // 1
b.indexOf("o"); // 1

var c = a.concat("bar"); // "foobar"
var d = b.concat(["b", "a", "r"]); // ["f","o","o","b","a","r"]

a === c; // false
b === d; // false

a; // "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; // undefined

var 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; // undefined

b.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 的作品:Esrevergithub.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"; // NaN

typeof 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; // true

a === b; // true
-0 === 0; // true

0 > -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); // true

Object.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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值