JavaScript 内置值类型

215 阅读8分钟

数组

和其他强类型语言不同,在 JavaScript 中,数组可以容纳任何类型的值

使用 delete 运算符可以将单元从数组中删除,但是,单元删除后,数组的 length 属性并不会发生变化

// 稀疏数组
var a = []
a[0] = 1
a[2] = [3]
a[1] === "undefined"
a.length === 3

数组通过数字进行索引,但有趣的是它们也是对象,所以也可以包含字符串键值和属性 (但这些并不计算在数组长度内)

类数组

有时候需要将类数组(一组通过数字索引的值)转换为真正的数组,这一般通过数组工具函数 (如 indexOf(...)、concat(...)、forEach(...)等)来实现

例如,一些 DOM 查询操作会返回 DOM 元素列表,它们并非真正意义上的数组,但十分类似,另一个例子是通过 arguments 对象(类数组),将函数的参数当作列表来访问(ES6 开始已废止)

工具函数 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)

字符串

字符串和数组的确很相似,它们都是类数组,都有length 属性以及 indexOf(...)concat(...) 方法

JavaScript 中的字符串和字符数组并不是一回事,

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

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[1] = 'O'
b[1] = 'O'

a // "foo"
b // ["f", "O", "o"]

JavaScript 中字符串是不可变的,而数组是可变的,并且 a[1] 在JavaScript 中并非总是合法语法,正确的方法应该是 a.charAt(1)

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

许多数组函数用来处理字符串很方便,虽然字符串没有这些函数,但可以通过 ”借用“ 数组的非变更方法来处理字符串

var a = "foo"
a.join // undefined
a.map // undefined

var c = Array.prototype.join.call(a, '-')
var d = Array.propotype.map.call(a, v => {
    return v.toUpperCase() + "."
})

数组有一个字符串没有的可变更成员函数 reverse()

可惜我们无法借用数组的 可变更成员函数,因为字符串是不可变的

所以,我们需要用一个比较粗暴的方法:

var a = "foo"
var c = a.split("").reverse().join("")
c // "oof"

这种方法虽然粗暴,但对简单的字符串却完全适用

数字

JavaScript 只有一种数值类型: number,包括”整数“ 和带小数的十进制数,此处”整数“之所以加引号是因为和其他语言不同,JavaScript没有真正意义上的整数

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

数字的语法

// 数字常量一般使用十进制表示
var a = 42
var b = 42.3

// 数字前的 0 可以省略
var c = 0.42
var d = .42

// 小数点后小数部分最后的 0 也可以省略
var f = 42.0
var g = 42.

// 特别大和特别小的数字默认用指数格式显示,与 `toExponential()` 函数的输出结果相同
var a = 5E10
a // 50000000000
a.toExponential() // 5e+10

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

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

由于数字值可以使用 Number对象进行封装,因此数字值可以调用 Number.prototype 中的方法

var a = 42.59

a.toFixed(0) // 43
a.toFixed(1) // 42.6
a.toFixed(2) // 42.59
a.toFixed(3) // 42.590

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

这些方法也适用于数字常量,不过

// 无效语法
42.toFixed(3)

// 有效语法
(42).toFixed(3)
0.42.toFixed(3)
42..toFixed(3)
42 .toFixed(3)

因为数字常量是一个有效的数字字符,会被优先识别为数字常量的一部分,然后才是对象属性访问运算符

较小的数值

二进制浮点数最大的问题:

0.1 + 0.2 === 0.3 // false

最常见的方法是设置一个误差范围值,通常称为”机器精度“,对JavaScript的数字来说,这个值通常是2^-52

从 ES6 开始,该值定义在 Number.EPSILON中,我们可以直接拿来用,也可以为 ES6 之前的版本写 polyfill:

if (!Number.EPSILON) {
    Number.EPSILON = Math.pow(2, -52)
}

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

能够呈现的最大浮点数约 1.798e+308,它定义在Number.MAX_VALUE中 最小浮点数定义在Number.MIN_VALUE中,大约是 5e-324,它不是负数,但无限接近于 0

整数的安全范围

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

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

整数检测

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

// before ES6 polyfill
if (!Number.isInteger) {
    Number.isInteger = function (num) {
        return typeof num === "number" && num % 1 === 0
    }
}

// ES6 
Number.isSafeInteger(Number.MAX_SAFE_INTEGER) // true
Number.isSafeInteger(Math.pow(2, 53)) // false
Number.isSafeInteger(Math.pow(2, 53) - 1) // false

// before ES6 polyfill
if (!Number.isSafeInteger) {
    Number.isSafeInteger = function(num) {
        return Number.isInteger(num) &&
            Math.abs(num) <= Number.MAX_SAFE_INTEGER
    }
}

32位有符号整数

虽然整数最大能够达到 53 位,但是有些数字操作(如数位操作)只适用于 32 位数字,所以这些操作中数字的安全范围就要小很多,变成从 Math.pow(-2,31)(-2147483648, 约-21 亿)到 Math.pow(2,31) - 1(2147483647,约 21 亿)。

a | 0可以将变量 a 中的数值转换为 32 位有符号整数,因为数位运算符 | 只适用于 32 位整数(它只关心 32 位以内的值,其他的数位将被忽略),因此与 0 进行操作即可截取 a 中 的 32 位数位

特殊数值

不是值的值

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

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

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

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

void 运算符

var a = 42
void a  // undefined
a // 42

特殊的数字

  • 不是数字的数字 NaN
var a = 2 / "foo"
a === NaN // false
NaN !== NaN // true

NaN 是一个特殊值,它和自身不相等,是唯一一个非自反的值

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

a // NaN
b // "foo"

window.isNaN(a) // true
window.isNaN(b) // true ————晕了!

很明显 "foo" 不是一个数字,但是它也不是 NaN。这个 bug 自 JavaScript 问世以来就一直存在,至今已超过 19 年

不过从 ES6 开始我们可以使用工具函数 Number.isNaN(..)

// before ES6 polyfill
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——好!

// 还有一个更简单的方法

if (Number.isNaN) {
    Number.isNaN = function(n) {
        return n !== n
    }
}
  • 无穷数 Infinity
var a = 1 / 0; // Infinity 
var b = -1 / 0; // -Infinity

var a = Number.MAX_VALUE; // 1.7976931348623157e+308
a + a; // Infinity
a + Math.pow( 2, 970 ); // Infinity
a + Math.pow( 2, 969 ); // 1.7976931348623157e+308

Number.POSITIVE_INfiNITY

Number.NEGATIVE_INfiNITY

  • 零值
var a = 0 / -3 // -0
var b * -3 // -0
// 加减法运算不会得到负零

// 对负零进行字符串化会返回 ‘0’
a.toString() // "0"
a + "" // "0"
String(a) // "0"
JSON.stringify(a) // "0"

// 反过来是准确的
+ "-0" // -0
Number("-0") // -0
JSON.parse("-0") // -0

// 比较
-0 === 0 // true

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

特殊等式

// ES6
var a = 2 / "foo";
var b = -3 * 0;
Object.is( a, NaN ); // true
Object.is( b, -0 ); // true
Object.is( b, 0 ); // false

// before ES6 polyfill
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;
    };
}

值和引用

var a = 2;
var b = a; // b是a的值的一个副本
b++;
a; // 2
b; // 3
var c = [1,2,3];
var d = c; // d是[1,2,3]的一个引用
d.push( 4 );
c; // [1,2,3,4]
d; // [1,2,3,4]

简单值总是通过值复制的方式来赋值/传递

复合值————对象、函数,总是通过引用复制的方式来赋值/传递

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

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]

函数参数经常让人产生这样的困惑:

function foo(x) {
    x.push( 4 );
    x; // [1,2,3,4]
    // 然后
    x = [4,5,6];
    x.push( 7 );
    x; // [4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; // 是[1,2,3,4],不是[4,5,6,7]

//如果要将 a 的值变为 [4,5,6,7],必须更改 x 指向的数组,而不是为 x 赋值一个新的数组
function foo(x) {
    x.push( 4 );
    x; // [1,2,3,4]
    // 然后
    x.length = 0; // 清空数组
    x.push( 4, 5, 6, 7 );
    x; // [4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; // 是[4,5,6,7],不是[1,2,3,4]

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

JavaScript 中的引用和其他语言中的引用 / 指针不同,它们不 能指向别的变量 / 引用,只能指向值