重新学习 JavaScript 中的 Types (类型)

简介: 重新学习 JavaScript 中的 Types (类型)

类型

ECMAScript 语言中所有的值都有一个对应的语言类型。ECMAScript 语言类型包括 Undefined、Null、Boolean、String、Number 和 Object。 —— ES5.1规范

喜欢强类型(又称静态类型)语言的人也许会认为“类型”一词用在这里不妥。“类型”在强类型语言中的涵义要广很多。

那可以试着这样定义“类型”(与规范类似):对语言引擎和开发人员来说,类型 是值的内部特征,它定义了值的行为,以使其区别于其他值。

内置类型

JS 有七种内置类型:

  • null
  • undefined
  • boolean
  • number
  • string
  • object
  • symbol
typeof undefined === 'undefined'; // true
typeof true === 'boolean'; // true
typeof 42 === 'number'; // true
typeof '42' === 'string'; // true
typeof { a: 1 } === 'object'; // true

typeof Symbol() === 'symbol'; // true

typeof null === 'object'; // true 这是个遗留的 BUG

对 null 进行复合检测

var a = null;

(!a && typeof a === 'object'); // true

函数是 object 的一个“子类型”,是“可调用对象”,它有一个内部属性 [[Call]] ,该属性使其可以被调用。函数对象的 length 属性是其声明的参数的个数。

typeof function a(b, c) {} === 'function'; // true

a.length; // 2

数组也是 object 的一个“子类型”,数组的元素按数字顺序来进行索引(而非普通像对象那样通过字符串键值),其 length 属性是元素的个数。

typeof [1,2,3] === 'object'; // true

值和类型

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

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

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

var a = 1;
typeof a; // number

a = true;
typeof a; // boolean

typeof 运算符总是会返回一个字符串

typeof typeof 42 === 'string'; // true

undefined 和 undeclared

变量在未持有值的时候为 undefined

var a;

typeof a; // undefined

var b = 42;
var c;

b = c;

typeof b; // undefined
typeof c; // undefined

已在作用域中声明但还没有赋值的变量,是 undefined 的。相反,还没有在作用域中声明过的变量,是 undeclared 的。“undefined” 和 “is not defined” 是两码事

对于 undeclared(或者 not defined)变量,typeof 照样返回 "undefined" 。虽然 b 是一个 undeclared 变量,但 typeof b 并没有报错。这是因为 typeof 有一个特殊的安全防范机制。

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

typeof a; // undefined
typeof b; // undefined WTF?

typeof Undeclared

该安全防范机制对在浏览器中运行的 JavaScript 代码来说还是很有帮助的,因为多个脚本文件会在共享的全局命名空间中加载变量。

// 这种情况可能会报 ReferenceError
if (DEBUG) {
  console.log('Debugging...');
}

// 这是安全的
if (typeof DEBUG !== 'undefined') {
  console.log('Debugging...');
}

与 undeclared 变量不同,访问不存在的对象属性(甚至是在全局对象 window 上)不会产生 ReferenceError 错误。

// 正常工作
if (window.DEBUG) {
  console.log('Debugging...');
}

数组

在 JavaScript 中,数组可以容纳任何类型的值,可以是字符串、数字、对象(object ),甚至是其他数组。对数组声明后即可向其中加入值,不需要预先设定大小。

var a = [];

a.length; // 0

a[0] = 1;
a[1] = '2';
a[2] = [3];

a.length; // 3

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

在创建“稀疏”数组(sparse array,即含有空白或空缺单元的数组)时要特别注意:

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

a[1]; // undefined

a.length; // 3

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

var a = [];

a[0] = 1;
a['foo'] = 2;

a.length; // 1
a.foo; // 2
a['foo']; // 2

// 注意,如果字符串键值能够被强制类型转换为十进制数字的话,它就会被当作数字索引来处理
a['13'] = 99;
a.length; // 14

::: tip
建议使用对象来存放键值 / 属性值,用数组来存放数字索引值。
:::

类数组

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

function foo() {
  var arr = Array.prototype.slice.call(arguments);

  // ES6 support
  // var arr = Array.from(arguments);

  arr.push('bam');
  console.log(arr);
}
foo('bar', 'baz'); // ['bar', 'baz', 'bam']

字符串

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

字符串和数组的确很相似,它们都是类数组,都有 length 属性以及 indexOf(..) (从 ES5 开始数组支持此方法)和 concat(..) 方法:

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; // foo
b; // ['f', 'o', 'o']

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

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

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

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

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.

// 注意 reverse() 方法不能直接借用,需要先转数组再借用
var e = Array.prototype.reverse.call(a).join('');
e; // 'oof'

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

数字

BigInt 之前,JavaScript 只有一种数值类型:number (数字),包括“整数”和带小数的十进制数。此处“整数”之所以加引号是因为和其他语言不同,JavaScript 没有真正意义上的整数,这也是它一直以来为人诟病的地方。

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

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

  • 数字的语法:
var a = 42;
var b = 42.3;

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

// 小数点后面的 0 可以省略
var e = 42.0;
var f = 42.; // 同上

// 特别大的数和特别小的数用指数表示
var g = 1e6; // 1 * 10^6
var h = 1e-6; // 1 * 10^-6
g; // 1000000
g.toExponential(); // 1e6

// 由于数字值可以使用 Number 对象进行封装,因此数字值可以调用 Number.prototype 中的方法。
var i = 42.59;
i.toFixed(0); // 43
i.toFixed(1); // 42.6
i.toFixed(2); // 42.59
i.toFixed(3); // 42.590

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

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

// 42.toFixed(3); // SyntaxError
(42).toFixed(3); // 42.000
0.42.toFixed(3); // 0.420
42..toFixed(3); // 42.000
42 .toFixed(3); // 42.000 注意此处有空格,不建议这样写

var j = 0xf3; // 243 的十六进制表示
var k = 0o363; // 243 的八进制表示
var l = 0b11110011; // 243 的二进制表示
  • 较小的数值:
0.1 + 0.2 === 0.3; // false

二进制浮点数中的 0.1 和 0.2 并不是十分精确,它们相加的结果并非刚好等于 0.3 ,而是一个比较接近的数字 0.30000000000000004 ,所以条件判断结果为 false。

在处理带有小数的数字时需要特别注意。很多(也许是绝大多数)程序只需要处理整数,最大不超过百万或者万亿,此时使用 JavaScript 的数字类型是绝对安全的。

那么应该怎样来判断 0.1 + 0.2 和 0.3 是否相等呢?最常见的方法是设置一个误差范围值,通常称为“机器精度”(machine epsilon),对 JavaScript 的数字来说,这个值通常是 2^-52 (2.220446049250313e-16) 。

// 从 ES6 开始,该值定义在 Number.EPSILON 中,我们可以直接拿来用
function numbersCLoseEnoughToEqual(a, b) {
  return Math.abs(a - b) < Number.EPSILON;
}

var a = 0.1 + 0.2;
var b = 0.3;
numbersCLoseEnoughToEqual(a, b); // true
numbersCLoseEnoughToEqual(0.0000001, 0.0000002); // false
  • 整数的安全范围

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

特殊的数值

  • 不是值的值

    • undefined 类型只有一个值,即 undefined ,undefined 指从未赋值
    • null 类型只有一个值,即 null,null 指曾赋过值,但是目前没有值
    • null 是一个特殊关键字,不是标识符,不能将其当作变量来使用和赋值
    • undefined 是一个标识符,可以被当作变量来使用和赋值
  • 特殊的数字

    • NaN

      • 不是数字的数字,即 NaN
      • 如果数学运算的操作数不是数字类型(或者无法解析为常规的十进制或十六进制数字),就无法返回一个有效的数字,这种情况下返回值为 NaN
      • “不是数字的数字”仍然是数字类型
    • Infinity

      • 正无穷,即 Infinity
      • 负无穷,即 -Infinity
      • 计算结果一旦溢出为无穷数 (infinity)就无法再得到有穷数
    • 0

      • 有些应用程序中的数据需要以级数形式来表示(比如动画帧的移动速度),数字的符号位(sign)用来代表其他信息(比如移动的方向)。此时如果一个值为 0 的变量失去了它的符号位,它的方向信息就会丢失。
  • 特殊等式

    • ES6 中 Object.is(a, b) 用来判断两个值是否绝对相等

值和引用

在 C++ 中如果要向函数传递一个数字并在函数中更改它的值,就可以这样来声明参数 int& myNum ,即如果传递的变量是 x ,myNum 就是指向 x 的引用。引用就像一种特殊的指针,是来指向变量的指针(别名 )。如果参数不声明为引用的话,参数值总是 通过值复制的方式传递,即便对复杂的对象值也是如此。

JavaScript 中没有指针,引用的工作机制也不尽相同。在JavaScript 中变量不可能成为指向另一个变量的引用。

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

JavaScript 对值和引用的赋值 / 传递在语法上没有区别,完全根据值的类型来决定。

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]

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

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

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

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]

不能通过引用 x 来更改引用 a 的指向,只能更改 a 和 x共同指向的值。如果要将 a 的值变为 [4,5,6,7] ,必须更改 x 指向的数组,而不是为 x 赋值一个新的数组。

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

如果通过值复制的方式来传递复合值(如数组),就需要为其创建一个复本,这样传递的就不再是原始值。

foo(a.sclice());

如果要将标量基本类型值传递到函数内并进行更改,就需要将该值封装到一个复合值(对象、数组等)中,然后通过引用复制的方式传递。

function foo(wrapper) {
  wrapper.value = 4;
}

var obj = {
  value: 1
};

foo(obj);

obj.value; // 4
相关文章
|
12天前
|
JavaScript 前端开发 开发者
如何在 JavaScript 中处理不同类型的错误?
【10月更文挑战第29天】通过对不同类型错误的准确识别和恰当处理,可以提高JavaScript程序的可靠性和稳定性,减少错误对程序运行的影响。
|
1月前
|
JavaScript 前端开发 开发者
VUE 开发——Node.js学习(一)
VUE 开发——Node.js学习(一)
64 3
|
1月前
|
JavaScript 前端开发 安全
深入理解TypeScript:增强JavaScript的类型安全性
【10月更文挑战第8天】深入理解TypeScript:增强JavaScript的类型安全性
45 0
|
11天前
|
JavaScript 前端开发 Java
除了 JavaScript,还有哪些编程语言支持 Set 类型
【10月更文挑战第30天】这些编程语言中的 `Set` 类型虽然在语法和具体实现细节上有所不同,但都提供了类似的集合操作功能,方便开发者在不同的编程场景中处理集合相关的数据和逻辑。
|
12天前
|
存储 JavaScript 前端开发
js的基础类型和引用类型
【10月更文挑战第29天】理解 JavaScript 中的基础类型和引用类型的区别对于正确地编写代码和理解程序的行为非常重要。在实际开发中,需要根据具体的需求合理地选择和使用不同的数据类型,以避免出现一些意想不到的错误和问题。同时,在处理引用类型数据时,要特别注意对象的引用关系,避免因共享引用而导致的数据不一致等问题。
|
1月前
|
JavaScript
js学习--制作猜数字
js学习--制作猜数字
37 4
js学习--制作猜数字
|
1月前
|
JavaScript
webpack学习五:webpack的配置文件webpack.config.js分离,分离成开发环境配置文件和生产环境配置文件
这篇文章介绍了如何将webpack的配置文件分离成开发环境和生产环境的配置文件,以提高打包效率。
46 1
webpack学习五:webpack的配置文件webpack.config.js分离,分离成开发环境配置文件和生产环境配置文件
|
2月前
|
算法 JavaScript 前端开发
第一个算法项目 | JS实现并查集迷宫算法Demo学习
本文是关于使用JavaScript实现并查集迷宫算法的中国象棋demo的学习记录,包括项目运行方法、知识点梳理、代码赏析以及相关CSS样式表文件的介绍。
第一个算法项目 | JS实现并查集迷宫算法Demo学习
|
2月前
|
JavaScript 前端开发 API
紧跟月影大佬的步伐,一起来学习如何写好JS(上)
该文章跟随月影老师的指导,探讨了编写优质JavaScript代码的三大原则:各司其职、组件封装与过程抽象,通过具体示例讲解了如何在实际开发中应用这些原则以提高代码质量和可维护性。
紧跟月影大佬的步伐,一起来学习如何写好JS(上)
|
1月前
|
JavaScript
js学习--制作选项卡
js学习--制作选项卡
37 4