木灵鱼儿

木灵鱼儿

阅读:128

最后更新:2023/01/02/ 5:20:23

类型和值

类型

JavaScript 有七种内置类型:

  • 空值(null)
  • 未定义(undefined)
  • 布尔值( boolean)
  • 数字(number)
  • 字符串(string)
  • 对象(object)
  • 符号(symbol,ES6 中新增)

除对象之外,其他统称为“基本类型”。

其中数组Array和函数Function都是object的子类型,像Date这些也是同理。

其中需要特殊记忆的就是null了,typeof对它的处理是有问题的:

typeof null === "object"; // true

正确的结果应该返回字符串类型的"null",这个bug是出自历史遗留,起因是因为JavaScript的判断机制,所有的代码最终都是二进制代码,我们声明的对象最终也是二进制,在JavaScript中二进制前三位都为0的话会判断为object类型,而null的二进制全是0,所以在执行typeof时会返回”object“。

这个bug也许永远都不会修复,因为时间太久,牵扯的东西太多,也许很多旧的项目都针对其做了处理,如果突然改变,那可能是一场灾难性的事故。

而我们往往会使用两个判定条件来进行判断是不是一个null:

var a = null;
(!a && typeof a === "object"); // true

JavaScript 中的变量是没有类型的,只有值才有。变量可以随时持有任何类型的值。

换个角度来理解就是,JavaScript 不做“类型强制”;也就是说,语言引擎不要求变量总持有与其初始值同类型的值。一个变量可以现在被赋值为字符串类型值,随后又被赋值为数字类型值。

在对变量执行 typeof 操作时,得到的结果并不是该变量的类型,而是该变量持有的值的类型,因为 JavaScript 中的变量没有类型。

undefined 和 undeclared

变量在未持有值的时候为 undefined。此时 typeof 返回 "undefined":

var a;
typeof a; // "undefined"

大多数开发者倾向于将 undefined 等同于 undeclared(未声明),但在 JavaScript 中它们完全是两回事。

已在作用域中声明但还没有赋值的变量,是 undefined 的。相反,还没有在作用域中声过的变量,是 undeclared 的。

var a;
a; // undefined
b; // ReferenceError: b is not defined

但是通过typeof去判断未声明的变量时,你得到的却是“undefined”,这就很令人抓狂!

var a;

typeof a; // "undefined"
typeof b; // "undefined"

所以我们没法通过typeof去判断一个变量是否未声明,导致其这样返回是因为typeof有一个特殊的安全规范机制,虽然它没有准确的返回undeclared,但也不是没有好处,这种安全机制使得我们的代码更加安全,比如:

我们会有一个第三方的插件,它在全局挂载了一个名为$的属性,在后续的脚本中我们使用了该变量,假设有一天我们缺失了这个插件,就会出现一些问题:

if ($) {
    console.log("不安全");
}

if (typeof $ !== "undefined") {
    console.log("安全的");
}

通过这种方式我们可以避免 ReferenceError 的错误。

我们还可以通过对象属性的方式去判断,JavaScript会将不存在的属性返回undefined,而不是直接抛出 ReferenceError 错误。

if (window.$) {
    console.log("安全的");
}

虽然typeof的安全机制是有好处的,但是我们自己还是得明白:undefined 是值的一种,undeclared 则表示变量还没有被声明过。但是在js中它们被混为一谈,因为不管是undefined还是undeclared,typeof都会返回“undefined”,但是通过typeof的这个安全防范机制来检测undeclared避免代码报错,也是个不错的办法。

数组

js中的数组其实是一种稀疏数组,因为在强类型语言中,数组只能存放一种类型的值,甚至长度都是固定的,为此强类型语言衍生出多种“数组类型”,比如c#的ArrayListList,JavaScript的数组是可以容纳任何类型的值,且不需要预先设置长度大小的。

但是在创建的时候需要注意,如果存在空白单元,可能会产生意料之外的问题:

var a = [];
a[0] = 1;

// 此处没有设置a[1]单元
a[2] = [3];

a[1]; // undefined
a.length; // 3

数组本身除了通过数字进行索引,还可以像普通对象一样存放键值对数据,而且这些键值不会计算在数组长度中去。

var a = [];
a[0] = 1;
a["foobar"] = 2;
a.length; // 1
a["foobar"]; // 2
a.foobar; // 2

但是这种用法需要特别注意,如果key值可以被强制转换为十进制数字的话,它就会被作为下标,也就是数字索引来处理。

var a = [];
a["13"] = 42;
a.length; // 14

在数组中加入字符串键值 / 属性并不是一个好主意。建议使用对象来存放键值 / 属性值,用数组来存放数字索引值。

类数组

一些dom的查询操作获取的对象并不是真正意义上的数组,以及arguments对象,ES6之后arguments已经被废弃了。

工具函数 slice(..) 经常被用于类数组的转换:

function foo() {
    var arr = Array.prototype.slice.call(arguments);
    arr.push("bam");
    console.log(arr);
}

foo("bar", "baz"); // ["bar","baz","bam"]

用 ES6 中的内置工具函数 Array.from(..) 也能实现同样的功能:

var arr = Array.from(arguments);

字符串

字符串经常被当成字符数组。字符串的内部实现究竟有没有使用数组并不好说,但JavaScript 中的字符串和字符数组并不是一回事,最多只是看上去相似而已。

var a = "foo";
var b = ["f", "o", "o"];

字符串和数组的确很相似,它们都是类数组,都有 length 属性以及 indexOf(..)(从 ES开始数组支持此方法)和 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 中字符串是不可变的,而数组是可变的。并且 a[1] 在 JavaScript 中并非总是合法语法,在老版本的 IE 中就不被允许(现在可以了)。正确的方法应该是 a.charAt(1)。

字符串不可变是指字符串的成员函数不会改变其原始值,而是创建并返回一个新的字符串。而数组的成员函数都是在其原始值上进行操作。

c = a.toUpperCase();
a === c; // false

a; // "foo"
c; // "FOO"

b.push("!");
b; // ["f","O","o","!"]

另一个不同点就是字符不能反转,数组有一个reverse方法,用于将成员倒叙处理,并返回该数组,也就是说反转会直接改变原数组,而字符串是不可变的,所以即便我们去借用这个方法,结果也是不行的。

var a = "foo";
Array.prototype.reverse.call(a);  //foo

所以我们常常会先通过split("")转为数组,再反转一下,最后join拼接。

var c = a
    // 将a的值转换为字符数组
    .split("")
    // 将数组中的字符进行倒转
    .reverse()
    // 将数组中的字符拼接回字符串
    .join("");

c; // "oof"

但是需要注意,使用split("")处理简单的字符串可以,如果字符是4字节的,那么就会导致4字节被拆成2个2个字节,从而出现乱码,而这个字节问题我在之前的文章已经说明过了,解法就不赘述了,我们还可以使用第三方库:esrever来实现文字的倒叙处理。

如果需要经常以字符数组的方式来处理字符串的话,倒不如直接使用数组。这样就不用字符串和数组之间来回折腾。可以在需要时使用 join("") 将字符数组转换为字符串。

数字

JavaScript 只有一种数值类型:number(数字),包括“整数”和带小数的十进。JavaScript 没有真正意义上的整数,这也是
它一直以来为人诟病的地方。

JavaScript 中的“整数”就是没有小数的十进制数。所以 42.0 即等同于“整数”42。

与大部分现代编程语言(包括几乎所有的脚本语言)一样,JavaScript 中的数字类型是于 IEEE 754 标准来实现的,该标准通常也被称为“浮点数”。JavaScript 使用的是“双精度”格式(即 64 位二进制)。

JavaScript 中的数字常量一般用十进制表示。例如:

var a = 42;
var b = 42.3;

数字前面的 0 可以省略:

var a = 0.42;
var b = .42;

小数点后小数部分最后面的 0 也可以省略:

var a = 42.0;
var b = 42.;

默认情况下大部分数字都以十进制显示,小数部分最后面的 0 被省略,如:

var a = 42.300;
var b = 42.0;
a; // 42.3
b; // 42

特别大和特别小的数字默认用指数格式显示,与 toExponential() 函数的输出结果相同。

一些代码压缩也会将数字转为指数格式,也就是科学计数法表示。

var a = 5E10;
a; // 5000000000

a.toExponential(); // "5e+10"

var b = a * a;
b; // 2.5e+21

var c = 1 / a;
c; // 2e-11

在平时业务中,我们可能常常使用toFixed来四舍五入保留指定位数的小数,由于可能存在补零的情况,所以该方法返回的是string类型的值。

还有一个比较类似的方法:toPrecision

它用来返回一个指定位数的有效数字,这个位数是不包含小数点的。

var a = 42.59;

a.toPrecision(1); // "4e+1"
a.toPrecision(2); // "43"
a.toPrecision(3); // "42.6"
a.toPrecision(4); // "42.59"
a.toPrecision(5); // "42.590"
a.toPrecision(6); // "42.5900"

注意:

对于数字的方法调用,我们可能写过以下代码:

42.toFixed(3); // SyntaxErro

我们可能并不关心为什么会出现错误,可能改一种写法就可以使用了。

(42).toFixed(3); // "42.0

但是如果我们搞明白为什么,就可以很轻易的避免出现这种错误。

.tofixed.被视为了常量42的一部分,导致没有.属性访问运算符来运行tofixed方法。

为此,以下这两种写法反倒是有效的,虽然看着很奇怪:

0.42.toFixed(3); // "0.420"
42..toFixed(3); // "42.000"
42 .toFixed(3); // "42.000" 注意有空格

数字中最常见的还有精度问题,经典的问题:0.1 + 0.2 = 0.3?

从数学的角度来说,就等于0.3,但是二进制浮点数中的 0.1 和 0.2 并不是十分精确,它们想加的结果并不是刚好等于0.3,而是一个非常接近的数字。

那么如何判断是否等于0.3呢?

最常见的方法就是设置一个误差值,通常被称为机器精度,对于JavaScript的数字来说,这个值通常是2^-52 (2.220446049250313e-16),es6直接为我们提供了对应的属性Number.EPSILON,我们可以直接使用它。

我们可以通过0.3 - 0.1 - 0.2 <= Number.EPSILON来判定结果,只要符合机器精度范围,那么这个值就可以认为是等于的。

JavaScript中数字是有限大小的

数字的呈现方式决定了“整数”的安全值范围远远小于 Number.MAX_VALUE

能够被“安全”呈现的最大整数是 2^53 - 1,即 9007199254740991,在 ES6 中被定义为Number.MAX_SAFE_INTEGER。最小整数是 -9007199254740991,在 ES6 中被定义为 Number.MIN_SAFE_INTEGER

有时 JavaScript 程序需要处理一些比较大的数字,如数据库中的 64 位 ID 等。由于JavaScript 的数字类型无法精确呈现 64 位数值(),所以必须将它们保存(转换)为字符串。

在一些超大型的项目中,后端返的值可能是超过64位的int类型,这种情况下,前端就得联系后端将其转换为字符串类型的值,否则前端将无法准确解析。

js还提供了一些整数检测方法:

Number.isInteger(42); // true
Number.isInteger(42.000); // true
Number.isInteger(42.3); // false

要检测一个值是否是安全的整数,可以使用 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

特殊的值

undefined 类型只有一个值,即 undefined。null 类型也只有一个值,即 null。它们的名称既是类型也是值。

undefined 和 null 常被用来表示“空的”值或“不是值”的值。二者之间有一些细微的差别。例如:

  • null 指空值(empty value)
  • undefined 指没有值(missing value)

或者:

  • undefined 指从未赋值
  • null 指曾赋过值,但是目前没有值

null 是一个特殊关键字,不是标识符,我们不能将其当作变量来使用和赋值。然而undefined 却是一个标识符,可以被当作变量来使用和赋值。

在非严格模式下,undefined是可以被重新赋值的:

function foo() {
    undefined = 2; 
}
foo();

function foo() {
    "use strict";
    undefined = 2; // TypeError!
}
foo();

为此我们会使用void运算符来得到准确的undefined值。

void运算符对给定的表达式进行求值,然后返回 undefined。

按惯例我们用 void 0 来获得 undefined(这主要源自 C 语言,当然使用 void true 或其他void 表达式也是可以的)。void 0、void 1 和 undefined 之间并没有实质上的区别。

需要注意的是void会对右侧代码求值,但是不管值是什么,总是返回undefined,所以如果你右侧写了个函数,是会正常触发的。

特殊的数字

1. 不是数字的数字 NaN

var a = 2 / "foo"; // NaN
typeof a === "number"; // true

NaN 意指“不是一个数字”(not a number),但是这个名字容易引起误会,因为虽然它不是一个数字,但仍然是数字类型,我们可以将其理解为一个“无效数值”、“失败数值”或者“坏数值”可能更准确些,即“执行数学运算没有成功,这是失败后返回的结果”。

那么我们如果判断这个值是NaN呢?

var a = 2 / "foo";
a == NaN; // false
a === NaN; // false

显然通过相等和全等是无法准确判断的,因为NaN是一个特殊的值,它和自身不相等,好在系统为我们在全局环境提供了一个工具函数isNaN来判断一个值是否是NaN。

但是这个方法有一个非常严重的缺陷,因为它的原理:如果参数不是一个数字,会强制转换为数字类型,然后判断这个值是否不等于自身。

如果你传入的是number类型的,这个逻辑没有任何问题,但是如果你传入其他类型就会产生很多稀奇古怪的结果。

isNaN(undefined); // true
isNaN({}); // true
isNaN('123ABC'); // true
//...等等,不一一列举了

我们看下官方的polyfill就明白了:

var isNaN = function(value) {
    var n = Number(value);
    return n !== n;
};

这里主要就是应用到了数字的一些特性:1.数字类型转数字类型,得到的还是一样的值; 2.NaN和NaN不相等;

为了能更加准确的判断值是不是NaN,es6在Number上提供了isNaN方法:

var a = 2 / "foo";
var b = "foo";

Number.isNaN(a); // true
Number.isNaN(b); // false

所以现在更加推荐使用Number.isNaN,它能更加准确的判断。

2. 无穷数

熟悉传统编译型语言(如 C)的开发人员可能都遇到过编译错误(compiler error)或者运行时错误(runtime exception),例如“除以 0”:

var a = 1 / 0;

然而在 JavaScript 中上例的结果为 Infinity(即 Number.POSITIVE_INfiNITY)。同样:

var a = 1 / 0; // Infinity
var b = -1 / 0; // -Infinity

如果除法运算中的 个操作数为负数,则结果为 -Infinity(即 Number.NEGATIVE_INfiNITY)。

JavaScript 使用有限数字表示法(finite numeric representation,即之前介绍过的 IEEE 754浮点数),所以和纯粹的数学运算不同,JavaScript 的运算结果有可能溢出,此时结果为Infinity 或者 -Infinity。

3. 零值

这部分内容对于习惯数学思维的读者可能会带来困惑,JavaScript 有一个常规的 0(也叫作+0)和一个 -0。在解释为什么会有 -0 之前,我们先来看看 JavaScript 是如何来处理它的。

-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"

有意思的是,如果反过来将其从字符串转换为数字,得到的结果是准确的:

+"-0"; // -0
Number("-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

要区分 -0 和 0,不能仅仅依赖开发调试窗口的显示结果,还需要做一些特殊处理:

function isNegZero(n) {
    n = Number(n);
    return (n === 0) && (1 / n === -Infinity);
}

isNegZero(-0); // true
isNegZero(0 / -3); // true
isNegZero(0); // false

抛开学术上的繁枝褥节不论,我们为什么需要负零呢?

有些应用程序中的数据需要以级数形式来表示(比如动画帧的移动速度),数字的符号位(sign)用来代表其他信息(比如移动的方向)。此时如果一个值为 0 的变量失去了它的符号位,它的方向信息就会丢失。所以保留 0 值的符号位可以防止这类情况发生。

特殊的等式

如前所述,NaN 和 -0 在相等比较时的表现有些特别。由于 NaN 和自身不相等,所以必须用 ES6 中的 Number.isNaN(..)。而 -0 等于 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

能使用 == 和 ===时就尽量不要使用 Object.is(..),因为前者效率更高更为通用。Object.is(..) 主要用来处理那些特殊的相等比较。

值的引用

在许多编程语言中,赋值和参数传递可以通过值复制(value-copy)或者引用复制(reference-copy)来完成,这取决于我们使用什么语法。

在一些语言中,我们可以传递变量的引用地址(指针)实现在函数中修改它的值,而JavaScript中没有指针,引用的机制也不相同,在 JavaScript 中变量不可能成为指向另一个变量的引用。

JavaScript 引用指向的是值。如果一个值有 10 个引用,这些引用指向的都是同一个值,它们相互之间没有引用 / 指向关系。

var a = 2;
var b = a; // b是a的值的一个副本
b++;
a; // 2
b; // 3

简单值(即标量基本类型值,scalar primitive)总是通过值复制的方式来赋值 / 传递,包括null、undefined、字符串、数字、布尔和 ES6 中的 symbol。

复合值(compound value)——对象(包括数组和封装对象)和函数,则总是通过引用复制的方式来赋值 / 传递。

由于引用指向的是值本身而非变量,所以一个引用无法更改另一个引用的指向。

var a = [1, 2, 3];
var b = a;
a; // [1,2,3]
b; // [1,2,3]

// 然后
b = [4, 5, 6];
a; // [1,2,3]
b; // [4,5,6]

b=[4,5,6] 并不影响 a 指向值 [1,2,3],除非 b 不是指向数组的引用,而是指向 a 的指针,但在 JavaScript 中不存在这种情况!

请记住:我们无法自行决定使用值复制还是引用复制,一切由值的类型来决定。

版权申明

本文系作者 @木灵鱼儿 原创发布在木灵鱼儿 - 有梦就能远航站点。未经许可,禁止转载。

关于作者

站点职位 博主
获得点赞 0
文章被阅读 128

相关文章