JavaScript 编程语言【 数据类型】一
@[toc]
原始类型的方法
JavaScript 允许我们像使用对象一样使用原始类型(字符串,数字等)。JavaScript 还提供了这样的调用方法。我们很快就会学习它们,但是首先我们将了解它的工作原理,毕竟原始类型不是对象(在这里我们会分析地更加清楚)。
我们来看看原始类型和对象之间的关键区别。
一个原始值:
- 是原始类型中的一种值。
- 在 JavaScript 中有 7 种原始类型:
string
,number
,bigint
,boolean
,symbol
,null
和undefined
。
一个对象:
- 能够存储多个值作为属性。
- 可以使用大括号
{}
创建对象,例如:{name: "John", age: 30}
。JavaScript 中还有其他种类的对象,例如函数就是对象。
关于对象的最好的事儿之一是,我们可以把一个函数作为对象的属性存储到对象中。
let john = {
name: "John",
sayHi: function() {
alert("Hi buddy!");
}
};
john.sayHi(); // Hi buddy!
所以我们在这里创建了一个包含 sayHi
方法的对象 john
。
许多内建对象已经存在,例如那些处理日期、错误、HTML 元素等的内建对象。它们具有不同的属性和方法。
但是,这些特性(feature)都是有成本的!
对象比原始类型“更重”。它们需要额外的资源来支持运作。
当作对象的原始类型
以下是 JavaScript 创建者面临的悖论:
- 人们可能想对诸如字符串或数字之类的原始类型执行很多操作。最好使用方法来访问它们。
- 原始类型必须尽可能的简单轻量。
而解决方案看起来多少有点尴尬,如下:
- 原始类型仍然是原始的。与预期相同,提供单个值
- JavaScript 允许访问字符串,数字,布尔值和 symbol 的方法和属性。
- 为了使它们起作用,创建了提供额外功能的特殊“对象包装器”,使用后即被销毁。
“对象包装器”对于每种原始类型都是不同的,它们被称为 String
、Number
、Boolean
、Symbol
和 BigInt
。因此,它们提供了不同的方法。
例如,字符串方法 str.toUpperCase() 返回一个大写化处理的字符串。
用法演示如下:
let str = "Hello";
alert( str.toUpperCase() ); // HELLO
很简单,对吧?以下是 str.toUpperCase()
中实际发生的情况:
- 字符串
str
是一个原始值。因此,在访问其属性时,会创建一个包含字符串字面值的特殊对象,并且具有有用的方法,例如toUpperCase()
。 - 该方法运行并返回一个新的字符串(由
alert
显示)。 - 特殊对象被销毁,只留下原始值
str
。
所以原始类型可以提供方法,但它们依然是轻量级的。
JavaScript 引擎高度优化了这个过程。它甚至可能跳过创建额外的对象。但是它仍然必须遵守规范,并且表现得好像它创建了一样。
数字有其自己的方法,例如,toFixed(n) 将数字舍入到给定的精度:
let n = 1.23456;
alert( n.toFixed(2) ); // 1.23
我们将在后面 数字类型 和 字符串 章节中看到更多具体的方法。
⚠️构造器
String/Number/Boolean
仅供内部使用像 Java 这样的一些语言允许我们使用
new Number(1)
或new Boolean(false)
等语法,明确地为原始类型创建“对象包装器”。在 JavaScript 中,由于历史原因,这也是可以的,但极其 不推荐。因为这样会出问题。
例如:
alert( typeof 0 ); // "number" alert( typeof new Number(0) ); // "object"!
对象在
if
中始终为真,因此此处的 alert 将显示:let zero = new Number(0); if (zero) { // zero 为 true,因为它是一个对象 alert( "zero is truthy?!?" ); }
另一方面,调用不带
new
(关键字)的String/Number/Boolean
函数是完全理智和有用的。它们将一个值转换为相应的类型:转成字符串、数字或布尔值(原始类型)。例如,下面完全是有效的:
let num = Number("123"); // 将字符串转成数字
⚠️null/undefined 没有任何方法
特殊的原始类型
null
和undefined
是例外。它们没有对应的“对象包装器”,也没有提供任何方法。从某种意义上说,它们是“最原始的”。尝试访问这种值的属性会导致错误:
alert(null.test); // error
总结
- 除
null
和undefined
以外的原始类型都提供了许多有用的方法。我们后面的章节中学习这些内容。 - 从形式上讲,这些方法通过临时对象工作,但 JavaScript 引擎可以很好地调整,以在内部对其进行优化,因此调用它们并不需要太高的成本。
✅任务
我能为字符串添加一个属性吗?
重要程度:five:
思考下面的代码:
let str = "Hello";
str.test = 5;
alert(str.test);
你怎么想的呢,它会工作吗?会得到什么样的结果?
解决方案
试试运行一下:
let str = "Hello"; str.test = 5; // (*) alert(str.test);
根据你是否开启了严格模式
use strict
,会得到如下结果:
undefined
(非严格模式)- 报错(严格模式)。
为什么?让我们看看在
(*)
那一行到底发生了什么:
- 当访问
str
的属性时,一个“对象包装器”被创建了。- 在严格模式下,向其写入内容会报错。
- 否则,将继续执行带有属性的操作,该对象将获得
test
属性,但是此后,“对象包装器”将消失,因此在最后一行,str
并没有该属性的踪迹。这个例子清楚地表明,原始类型不是对象。
它们不能存储额外的数据。
数字类型
在现代 JavaScript 中,数字(number)有两种类型:
- JavaScript 中的常规数字以 64 位的格式 IEEE-754 存储,也被称为“双精度浮点数”。这是我们大多数时候所使用的数字,我们将在本章中学习它们。
- BigInt 数字,用于表示任意长度的整数。有时会需要它们,因为常规数字不能安全地超过
253
或小于-253
。由于仅在少数特殊领域才会用到 BigInt,因此我们在特殊的章节 【BigInt】 中对其进行了介绍。
所以,在这里我们将讨论常规数字类型。现在让我们开始学习吧。
编写数字的更多方法
假如我们需要表示 10 亿。显然,我们可以这样写:
let billion = 1000000000;
我们也可以使用下划线 _
作为分隔符:
let billion = 1_000_000_000;
这里的下划线 _
扮演了“[语法糖](语法糖_百度百科 (baidu.com))”的角色,使得数字具有更强的可读性。JavaScript 引擎会直接忽略数字之间的 _
,所以 上面两个例子其实是一样的。
但在现实生活中,我们通常会尽量避免写带一长串零的数。因为我们比较懒……我们会尝试将 10 亿写成 "1bn"
,或将 73 亿写成 "7.3bn"
。对于大多数大的数字来说都是如此。
在 JavaScript 中,我们可以通过在数字后面附加字母 "e"
并指定零的个数来缩短数字:
let billion = 1e9; // 10 亿,字面意思:数字 1 后面跟 9 个 0
alert( 7.3e9 ); // 73 亿(与 7300000000 和 7_300_000_000 相同)
换句话说,e
把数字乘以 1
后面跟着给定数量的 0 的数字。
1e3 === 1 * 1000; // e3 表示 *1000
1.23e6 === 1.23 * 1000000; // e6 表示 *1000000
现在让我们写一些非常小的数字。例如,1 微秒(百万分之一秒):
let mcs = 0.000001;
就像以前一样,可以使用 "e"
来完成。如果我们想避免显式地写零,我们可以这样写:
let mcs = 1e-6; // 1 的左边有 6 个 0
如果我们数一下 0.000001
中的 0 的个数,是 6 个。所以自然是 1e-6
。
换句话说,e
后面的负数表示除以 1 后面跟着给定数量的 0 的数字:
// -3 除以 1 后面跟着 3 个 0 的数字
1e-3 === 1 / 1000; // 0.001
// -6 除以 1 后面跟着 6 个 0 的数字
1.23e-6 === 1.23 / 1000000; // 0.00000123
十六进制,二进制和八进制数字
[十六进制](十六进制(一种计数方式)_百度百科 (baidu.com)) 数字在 JavaScript 中被广泛用于表示颜色,编码字符以及其他许多东西。所以自然地,有一种较短的写方法:0x
,然后是数字。
例如:
alert( 0xff ); // 255
alert( 0xFF ); // 255(一样,大小写没影响)
二进制和八进制数字系统很少使用,但也支持使用 0b
和 0o
前缀:
let a = 0b11111111; // 二进制形式的 255
let b = 0o377; // 八进制形式的 255
alert( a == b ); // true,两边是相同的数字,都是 255
只有这三种进制支持这种写法。对于其他进制,我们应该使用函数 parseInt
(我们将在本章后面看到)。
toString(base)
方法 num.toString(base)
返回在给定 base
进制数字系统中 num
的字符串表示形式。
举个例子:
let num = 255;
alert( num.toString(16) ); // ff
alert( num.toString(2) ); // 11111111
base
的范围可以从 2
到 36
。默认情况下是 10
。
常见的用例如下:
-
base=16 用于十六进制颜色,字符编码等,数字可以是
0..9
或A..F
。 -
base=2 主要用于调试按位操作,数字可以是
0
或1
。 -
base=36 是最大进制,数字可以是
0..9
或A..Z
。所有拉丁字母都被用于了表示数字。对于36
进制来说,一个有趣且有用的例子是,当我们需要将一个较长的数字标识符转换成较短的时候,例如做一个短的 URL。可以简单地使用基数为36
的数字系统表示:alert( 123456..toString(36) ); // 2n9c
⚠️使用两个点来调用一个方法
请注意
123456..toString(36)
中的两个点不是打错了。如果我们想直接在一个数字上调用一个方法,比如上面例子中的toString
,那么我们需要在它后面放置两个点..
。如果我们放置一个点:
123456.toString(36)
,那么就会出现一个 error,因为 JavaScript 语法隐含了第一个点之后的部分为小数部分。如果我们再放一个点,那么 JavaScript 就知道小数部分为空,现在使用该方法。也可以写成
(123456).toString(36)
。
舍入
舍入(rounding)是使用数字时最常用的操作之一。
这里有几个对数字进行舍入的内建函数:
-
Math.floor
向下舍入:
3.1
变成3
,-1.1
变成-2
。 -
Math.ceil
向上舍入:
3.1
变成4
,-1.1
变成-1
。 -
Math.round
向最近的整数舍入:
3.1
变成3
,3.6
变成4
,中间值3.5
变成4
。 -
Math.trunc
(IE 浏览器不支持这个方法)移除小数点后的所有内容而没有舍入:
3.1
变成3
,-1.1
变成-1
。
这个是总结它们之间差异的表格:
Math.floor |
Math.ceil |
Math.round |
Math.trunc |
|
---|---|---|---|---|
3.1 |
3 |
4 |
3 |
3 |
3.6 |
3 |
4 |
4 |
3 |
-1.1 |
-2 |
-1 |
-1 |
-1 |
-1.6 |
-2 |
-1 |
-2 |
-1 |
这些函数涵盖了处理数字小数部分的所有可能方法。但是,如果我们想将数字舍入到小数点后 n
位,该怎么办?
例如,我们有 1.2345
,并且想把它舍入到小数点后两位,仅得到 1.23
。
有两种方式可以实现这个需求:
-
乘除法
例如,要将数字舍入到小数点后两位,我们可以将数字乘以
100
,调用舍入函数,然后再将其除回。let num = 1.23456; alert( Math.round(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23
-
函数 toFixed(n) 将数字舍入到小数点后
n
位,并以字符串形式返回结果。let num = 12.34; alert( num.toFixed(1) ); // "12.3"
这会向上或向下舍入到最接近的值,类似于
Math.round
:let num = 12.36; alert( num.toFixed(1) ); // "12.4"
请注意
toFixed
的结果是一个字符串。如果小数部分比所需要的短,则在结尾添加零:let num = 12.34; alert( num.toFixed(5) ); // "12.34000",在结尾添加了 0,以达到小数点后五位
我们可以使用一元加号或
Number()
调用,将其转换为数字,例如+ num.toFixed(5)
。
不精确的计算
在内部,数字是以 64 位格式 IEEE-754 表示的,所以正好有 64 位可以存储一个数字:其中 52 位被用于存储这些数字,其中 11 位用于存储小数点的位置(对于整数,它们为零),而 1 位用于符号。
如果一个数字真的很大,则可能会溢出 64 位存储,变成一个特殊的数值 Infinity
:
alert( 1e500 ); // Infinity
这可能不那么明显,但经常会发生的是,精度的损失。
考虑下这个(falsy!)相等性测试:
alert( 0.1 + 0.2 == 0.3 ); // false
没错,如果我们检查 0.1
和 0.2
的总和是否为 0.3
,我们会得到 false
。
奇了怪了!如果不是 0.3
,那能是啥?
alert( 0.1 + 0.2 ); // 0.30000000000000004
我擦!想象一下,你创建了一个电子购物网站,如果访问者将价格为 ¥ 0.10
和 ¥ 0.20
的商品放入了他的购物车。订单总额将是 ¥ 0.30000000000000004
。这会让任何人感到惊讶。
但为什么会这样呢?
一个数字以其二进制的形式存储在内存中,一个 1 和 0 的序列。但是在十进制数字系统中看起来很简单的 0.1
,0.2
这样的小数,实际上在二进制形式中是无限循环小数。
什么是 0.1
?0.1
就是 1
除以 10
,1/10
,即十分之一。在十进制数字系统中,这样的数字表示起来很容易。将其与三分之一进行比较:1/3
。三分之一变成了无限循环小数 0.33333(3)
。
在十进制数字系统中,可以保证以 10
的整数次幂作为除数能够正常工作,但是以 3
作为除数则不能。也是同样的原因,在二进制数字系统中,可以保证以 2
的整数次幂作为除数时能够正常工作,但 1/10
就变成了一个无限循环的二进制小数。
使用二进制数字系统无法 精确 存储 0.1 或 0.2,就像没有办法将三分之一存储为十进制小数一样。
IEEE-754 数字格式通过将数字舍入到最接近的可能数字来解决此问题。这些舍入规则通常不允许我们看到“极小的精度损失”,但是它确实存在。
我们可以看到:
alert( 0.1.toFixed(20) ); // 0.10000000000000000555
当我们对两个数字进行求和时,它们的“精度损失”会叠加起来。
这就是为什么 0.1 + 0.2
不等于 0.3
。
ℹ️不仅仅是 JavaScript
许多其他编程语言也存在同样的问题。
PHP,Java,C,Perl,Ruby 给出的也是完全相同的结果,因为它们基于的是相同的数字格式。
我们能解决这个问题吗?当然,最可靠的方法是借助方法 toFixed(n) 对结果进行舍入:
let sum = 0.1 + 0.2;
alert( sum.toFixed(2) ); // 0.30
请注意,toFixed
总是返回一个字符串。它确保小数点后有 2 位数字。如果我们有一个电子购物网站,并需要显示 ¥ 0.30
,这实际上很方便。对于其他情况,我们可以使用一元加号将其强制转换为一个数字:
let sum = 0.1 + 0.2;
alert( +sum.toFixed(2) ); // 0.3
我们可以将数字临时乘以 100(或更大的数字),将其转换为整数,进行数学运算,然后再除回。当我们使用整数进行数学运算时,误差会有所减少,但仍然可以在除法中得到:
alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3
alert( (0.28 * 100 + 0.14 * 100) / 100); // 0.4200000000000001
因此,乘/除法可以减少误差,但不能完全消除误差。
有时候我们可以尝试完全避免小数。例如,我们正在创建一个电子购物网站,那么我们可以用角而不是元来存储价格。但是,如果我们要打 30% 的折扣呢?实际上,完全避免小数处理几乎是不可能的。只需要在必要时剪掉其“尾巴”来对其进行舍入即可。
ℹ️有趣的事儿
尝试运行下面这段代码:
// Hello!我是一个会自我增加的数字! alert( 9999999999999999 ); // 显示 10000000000000000
出现了同样的问题:精度损失。有 64 位来表示该数字,其中 52 位可用于存储数字,但这还不够。所以最不重要的数字就消失了。
JavaScript 不会在此类事件中触发 error。它会尽最大努力使数字符合所需的格式,但不幸的是,这种格式不够大到满足需求。
ℹ️两个零
数字内部表示的另一个有趣结果是存在两个零:
0
和-0
。这是因为在存储时,使用一位来存储符号,因此对于包括零在内的任何数字,可以设置这一位或者不设置。
在大多数情况下,这种区别并不明显,因为运算符将它们视为相同的值。
测试:isFinite 和 isNaN
还记得这两个特殊的数值吗?
Infinity
(和-Infinity
)是一个特殊的数值,比任何数值都大(小)。NaN
代表一个 error。
它们属于 number
类型,但不是“普通”数字,因此,这里有用于检查它们的特殊函数:
-
isNaN(value)
将其参数转换为数字,然后测试它是否为NaN
:alert( isNaN(NaN) ); // true alert( isNaN("str") ); // true
但是我们需要这个函数吗?我们不能只使用
=== NaN
比较吗?不好意思,这不行。值 “NaN” 是独一无二的,它不等于任何东西,包括它自身:alert( NaN === NaN ); // false
-
isFinite(value)
将其参数转换为数字,如果是常规数字而不是NaN/Infinity/-Infinity
,则返回true
:alert( isFinite("15") ); // true alert( isFinite("str") ); // false,因为是一个特殊的值:NaN alert( isFinite(Infinity) ); // false,因为是一个特殊的值:Infinity
有时 isFinite
被用于验证字符串值是否为常规数字:
let num = +prompt("Enter a number", '');
// 结果会是 true,除非你输入的是 Infinity、-Infinity 或不是数字
alert( isFinite(num) );
请注意,在所有数字函数中,包括 isFinite
,空字符串或仅有空格的字符串均被视为 0
。
ℹ️与
Object.is
进行比较有一个特殊的内建方法
Object.is
,它类似于===
一样对值进行比较,但它对于两种边缘情况更可靠:
- 它适用于
NaN
:Object.is(NaN,NaN) === true
,这是件好事。- 值
0
和-0
是不同的:Object.is(0,-0) === false
,从技术上讲这是对的,因为在内部,数字的符号位可能会不同,即使其他所有位均为零。在所有其他情况下,
Object.is(a,b)
与a === b
相同。这种比较方式经常被用在 JavaScript 规范中。当内部算法需要比较两个值是否完全相同时,它使用
Object.is
(内部称为 SameValue)。
parseInt 和 parseFloat
使用加号 +
或 Number()
的数字转换是严格的。如果一个值不完全是一个数字,就会失败:
alert( +"100px" ); // NaN
唯一的例外是字符串开头或结尾的空格,因为它们会被忽略。
但在现实生活中,我们经常会有带有单位的值,例如 CSS 中的 "100px"
或 "12pt"
。并且,在很多国家,货币符号是紧随金额之后的,所以我们有 "19€"
,并希望从中提取出一个数值。
这就是 parseInt
和 parseFloat
的作用。
它们可以从字符串中“读取”数字,直到无法读取为止。如果发生 error,则返回收集到的数字。函数 parseInt
返回一个整数,而 parseFloat
返回一个浮点数:
alert( parseInt('100px') ); // 100
alert( parseFloat('12.5em') ); // 12.5
alert( parseInt('12.3') ); // 12,只有整数部分被返回了
alert( parseFloat('12.3.4') ); // 12.3,在第二个点出停止了读取
某些情况下,parseInt/parseFloat
会返回 NaN
。当没有数字可读时会发生这种情况:
alert( parseInt('a123') ); // NaN,第一个符号停止了读取
ℹ️parseInt(str, radix) 的第二个参数
parseInt()
函数具有可选的第二个参数。它指定了数字系统的基数,因此 parseInt
还可以解析十六进制数字、二进制数字等的字符串:
alert( parseInt('0xff', 16) ); // 255
alert( parseInt('ff', 16) ); // 255,没有 0x 仍然有效
alert( parseInt('2n9c', 36) ); // 123456
其他数学函数
JavaScript 有一个内建的 Math 对象,它包含了一个小型的数学函数和常量库。
几个例子:
-
Math.random()
返回一个从 0 到 1 的随机数(不包括 1)。
alert( Math.random() ); // 0.1234567894322 alert( Math.random() ); // 0.5435252343232 alert( Math.random() ); // ... (任何随机数)
-
Math.max(a, b, c...)
/Math.min(a, b, c...)
从任意数量的参数中返回最大/最小值。
alert( Math.max(3, 5, -10, 0, 1) ); // 5 alert( Math.min(1, 2) ); // 1
-
Math.pow(n, power)
返回
n
的给定(power)次幂。alert( Math.pow(2, 10) ); // 2 的 10 次幂 = 1024
Math
对象中还有更多函数和常量,包括三角函数,你可以在 Math 对象文档 中找到这些内容。
总结
要写有很多零的数字:
- 将
"e"
和 0 的数量附加到数字后。就像:123e6
与123
后面接 6 个 0 相同。 "e"
后面的负数将使数字除以 1 后面接着给定数量的零的数字。例如123e-6
表示0.000123
(123
的百万分之一)。
对于不同的数字系统:
- 可以直接在十六进制(
0x
),八进制(0o
)和二进制(0b
)系统中写入数字。 parseInt(str,base)
将字符串str
解析为在给定的base
数字系统中的整数,2 ≤ base ≤ 36
。num.toString(base)
将数字转换为在给定的base
数字系统中的字符串。
要将 12pt
和 100px
之类的值转换为数字:
- 使用
parseInt/parseFloat
进行“软”转换,它从字符串中读取数字,然后返回在发生 error 前可以读取到的值。
小数:
- 使用
Math.floor
,Math.ceil
,Math.trunc
,Math.round
或num.toFixed(precision)
进行舍入。 - 请确保记住使用小数时会损失精度。
更多数学函数:
- 需要时请查看 Math 对象。这个库很小,但是可以满足基本的需求。
✅任务
来自访问者的数字的总和
重要程度:five:
创建一个脚本,提示访问者输入两个数字,然后显示它们的总和。
P.S. 有一个类型陷阱。
解决方案
let a = +prompt("The first number?", ""); let b = +prompt("The second number?", ""); alert( a + b );
注意在
prompt
前面的一元加号+
。它将立即把值转换成数字。否则,
a
和b
将会是字符串,它们的总和将是它们的连接,即:"1" + "2" = "12"
。
为什么 6.35.toFixed(1) == 6.3?
重要程度: 4
根据文档,Math.round
和 toFixed
都将数字舍入到最接近的数字:0..4
会被舍去,而 5..9
会进一位。
例如:
alert( 1.35.toFixed(1) ); // 1.4
在下面这个类似的示例中,为什么 6.35
被舍入为 6.3
而不是 6.4
?
alert( 6.35.toFixed(1) ); // 6.3
如何以正确的方式来对 6.35
进行舍入?
解决方案
在内部,
6.35
的小数部分是一个无限的二进制。在这种情况下,它的存储会造成精度损失。让我们来看看:
alert( 6.35.toFixed(20) ); // 6.34999999999999964473
精度损失可能会导致数字的增加和减小。在这种特殊的情况下,数字变小了一点,这就是它向下舍入的原因。
那么
1.35
会怎样呢?alert( 1.35.toFixed(20) ); // 1.35000000000000008882
在这里,精度损失使得这个数字稍微大了一些,因此其向上舍入。
如果我们希望以正确的方式进行舍入,我们应该如何解决
6.35
的舍入问题呢?在进行舍入前,我们应该使其更接近整数:
alert( (6.35 * 10).toFixed(20) ); // 63.50000000000000000000
请注意,
63.5
完全没有精度损失。这是因为小数部分0.5
实际上是1/2
。以 2 的整数次幂为分母的小数在二进制数字系统中可以被精确地表示,现在我们可以对它进行舍入:alert( Math.round(6.35 * 10) / 10); // 6.35 -> 63.5 -> 64(rounded) -> 6.4
重复,直到输入的是一个数字
重要程度:five:
创建一个函数 readNumber
,它提示输入一个数字,直到访问者输入一个有效的数字为止。
结果值必须以数字形式返回。
访问者也可以通过输入空行或点击“取消”来停止该过程。在这种情况下,函数应该返回 null
。
解决方案
function readNumber() { let num; do { num = prompt("Enter a number please?", 0); } while ( !isFinite(num) ); if (num === null || num === '') return null; return +num; } alert(`Read: ${readNumber()}`);
该解决方案有点复杂,因为我们需要处理
null
和空行。所以,我们实际上接受输入,直到输入的是一个“常规数字”。
null
(取消)和空行都符合该条件,因为在数字形式中它们是0
。在我们停止之后,我们需要专门处理
null
和空行(返回null
),因为将它们转换为数字将返回0
。
一个偶发的无限循环
重要程度:four:
这是一个无限循环。它永远不会结束。为什么?
let i = 0;
while (i != 10) {
i += 0.2;
}
解决方案
那是因为
i
永远不会等于10
。运行下面这段代码来查看
i
的 实际 值:let i = 0; while (i < 11) { i += 0.2; if (i > 9.8 && i < 10.2) alert( i ); }
它们中没有一个恰好是
10
。之所以发生这种情况,是因为对
0.2
这样的小数时进行加法运算时出现了精度损失。结论:在处理小数时避免相等性检查。
从 min 到 max 的随机数
重要程度:two:
内建函数 Math.random()
会创建一个在 0
到 1
之间(不包括 1
)的随机数。
编写一个 random(min, max)
函数,用以生成一个在 min
到 max
之间的随机浮点数(不包括 max
))。
运行示例:
alert( random(1, 5) ); // 1.2345623452
alert( random(1, 5) ); // 3.7894332423
alert( random(1, 5) ); // 4.3435234525
解决方案
我们需要将区间 0…1 中的所有值“映射”为范围在
min
到max
中的值。这可以分两个阶段完成:
- 如果我们将 0…1 的随机数乘以
max-min
,则随机数的范围将从 0…1 增加到0..max-min
。- 现在,如果我们将随机数与
min
相加,则随机数的范围将为min
到max
。函数实现:
function random(min, max) { return min + Math.random() * (max - min); } alert( random(1, 5) ); alert( random(1, 5) ); alert( random(1, 5) );
从 min 到 max 的随机整数
重要程度:two:
创建一个函数 randomInteger(min, max)
,该函数会生成一个范围在 min
到 max
中的随机整数,包括 min
和 max
。
在 min..max
范围中的所有数字的出现概率必须相同。
运行示例:
alert( randomInteger(1, 5) ); // 1
alert( randomInteger(1, 5) ); // 3
alert( randomInteger(1, 5) ); // 5
你可以使用 【上一个任务】的解决方案作为基础。
解决方案
简单但错误的解决方案
最简单但错误的解决方案是生成一个范围在
min
到max
的值,并取对其进行四舍五入后的值:function randomInteger(min, max) { let rand = min + Math.random() * (max - min); return Math.round(rand); } alert( randomInteger(1, 3) );
这个函数是能起作用的,但不正确。获得边缘值
min
和max
的概率比其他值低两倍。如果你将上面这个例子运行多次,你会很容易看到
2
出现的频率最高。发生这种情况是因为
Math.round()
从范围1..3
中获得随机数,并按如下所示进行四舍五入:values from 1 ... to 1.4999999999 become 1 values from 1.5 ... to 2.4999999999 become 2 values from 2.5 ... to 2.9999999999 become 3
现在我们可以清楚地看到
1
的值比2
少两倍。和3
一样。正确的解决方案
这个题目有很多正确的解决方案。其中之一是调整取值范围的边界。为了确保相同的取值范围,我们可以生成从 0.5 到 3.5 的值,从而将所需的概率添加到取值范围的边界:
function randomInteger(min, max) { // 现在范围是从 (min-0.5) 到 (max+0.5) let rand = min - 0.5 + Math.random() * (max - min + 1); return Math.round(rand); } alert( randomInteger(1, 3) );
另一种方法是使用
Math.floor
来取范围从min
到max+1
的随机数:function randomInteger(min, max) { // here rand is from min to (max+1) let rand = min + Math.random() * (max + 1 - min); return Math.floor(rand); } alert( randomInteger(1, 3) );
现在所有间隔都以这种方式映射:
values from 1 ... to 1.9999999999 become 1 values from 2 ... to 2.9999999999 become 2 values from 3 ... to 3.9999999999 become 3
所有间隔的长度相同,从而使最终能够均匀分配。
- 点赞
- 收藏
- 关注作者
评论(0)