一夜彻底掌握数据类型及相互间的转换规则
概览
-
ECMA262数据类型的详细剖析
- 数据类型的标准定义
- Symbol和BigInt的实践运用
- typeof检测的底层机制
-
数据类型间相互转换
- Number/parseInt底层处理机制
- 其他数据类型转换为 String 的规则
- 其他数据类型转换为 Boolean 的规则
- "相等"比较中的类型转换
- JS中的装箱和拆箱操作
-
几道思考题
ECMA262数据类型的详细剖析
数据类型的标准定义
「4.2 ECMAScript Overview」 ECMAScript is object-based: basic language and host facilities are provided by objects, and an ECMAScript program is a cluster of communicating objects. In ECMAScript, an object is a collection of zero or more properties each with attributes that determine how each property can be used---for example, when the Writable attribute for a property is set to false, any attempt by executed ECMAScript code to assign a different value to the property fails. Properties are containers that hold other objects, primitive values, or functions. A primitive value is a member of one of the following built-in types: Undefined, Null, Boolean, Number, String, and Symbol; an object is a member of the built-in type Object; and a function is a callable object. A function that is associated with an object via a property is called a method.
「4.2 ECMAScript 概述」 ECMAScript 是基于对象的:基本语言和宿主设施由对象提供,ECMAScript 程序是一组通信对象。 在 ECMAScript 中,对象是零个或多个属性的集合,每个属性都具有确定如何使用每个属性的属性 - 例如,当属性的 Writable 属性设置为 false 时,执行的 ECMAScript 代码尝试分配不同的属性值都将失败。 属性是保存其他对象、原始值或函数的容器。 原始值是以下内置类型之一的成员:未定义、空、布尔、数字、字符串和符号; 对象是内置类型 Object 的成员; 函数是一个可调用的对象。 通过属性与对象关联的函数称为方法。
ECMAScript defines a collection of built-in objects that round out the definition of ECMAScript entities. These built-in objects include the global object; objects that are fundamental to the runtime semantics of the language including Object, Function, Boolean, Symbol, and various Error objects; objects that represent and manipulate numeric values including Math, Number, and Date; the text processing objects String and RegExp; objects that are indexed collections of values including Array and nine different kinds of Typed Arrays whose elements all have a specific numeric data representation; keyed collections including Map and Set objects; objects supporting structured data including the JSON object, ArrayBuffer, and DataView; objects supporting control abstractions including generator functions and Promise objects; and, reflection objects including Proxy and Reflect.
ECMAScript 定义了一组内置对象,这些对象完善了 ECMAScript 实体的定义。 这些内置对象包括全局对象; 对语言运行时语义至关重要的对象,包括对象、函数、布尔值、符号和各种错误对象; 表示和操作数值的对象,包括 Math、Number 和 Date; 文本处理对象 String 和 RegExp; 作为索引值集合的对象,包括数组和九种不同类型的类型数组,其元素都具有特定的数字数据表示形式; 键控集合,包括 Map 和 Set 对象; 支持结构化数据的对象,包括 JSON 对象、ArrayBuffer 和 DataView; 支持控制抽象的对象,包括生成器函数和 Promise 对象; 以及,反射对象包括Proxy和Reflect。
❝
原文地址 ECMAScript 2015 Language Specification -- ECMA-262 6th Edition (ecma-international.org)
❞
原始值类型「值类型/基本数据类型」
- number 数字
- string 字符串
- boolean 布尔
- null 空对象指针
- undefined 未定义
- symbol 唯一值
- bigint 大数
对象类型「引用数据类型」
- 标准普通对象 object
- 标准特殊对象 Array、RegExp、Date、Math、Error......
- 非标准特殊对象 Number、String、Boolean......
- 可调用/执行对象「函数」function
Symbol和BigInt的实践运用
Symbol
Symbol 创建一个唯一值
使用场景
-
给对象设置"唯一值"的属性名
- 字符串
- Symbol类型
- Map数据结构:可以允许属性名是对象
-
Symbol.asyncIterator
、iterator
、hasInstance
、toPrimitive
、toStringTag
...是某些JS知识底层实现的机制 -
在派发行为标识统一进行管理的时候(比如 在使用vuex、redux时),可以基于symbol类型的值,保证行为标识的唯一性
-
...
相关案例
ini
// 创建一个唯一值
let a1 = Symbol('dahuang');
let a2 = Symbol('dahuang');
let a3 = a1;
console.log(a1 === a2); //false
console.log(a1 === a3); //true
// 给对象设置"唯一值"的属性名
const key = Symbol('fn');
const obj = {
n: 10,
10: 100,
true: 200,
[Symbol('xiaohuang')]: 300,
[Symbol('xiaohuang')]: 600,
[key]: 900
};
console.log(obj[Symbol('xiaohuang')]); //undefined
console.log(obj[key]); //900
Symbol.iterator
Symbol.iterator
是一个特殊的内置 Symbol,用于定义一个对象的默认迭代器。这个符号被用在一个对象上时,可以让该对象变得可迭代,意味着它可以使用 for...of
循环。
javascript
let myIterable = {
[Symbol.iterator]: function* () {
yield 1;
yield 2;
yield 3;
}
};
for (let value of myIterable) {
console.log(value);
}
// 输出: 1, 2, 3
hasInstance
Symbol.hasInstance
在 instanceof
底层机制中会使用到, 详情请看 「一夜彻底掌握数据类型检测原理」
toPrimitive
数据类型转换中会使用到,请看下文 数据类型间相互转换
toStringTag
Symbol.toStringTag
在 Object.prototype.toString.call([value])
进行数据类型判断时,底层机制中会使用到, 详情请看 「一夜彻底掌握数据类型检测原理」
BigInt
JS中的最大安全数: Number.MAX_SAFE_INTEGER
( 9007199254740991 )
JS中的最小安全数: Number.MIN_SAFE_INTEGER
( -9007199254740991 )
超过安全数后,进行运算或者访问,结果会不准确!!!
解决方案
方案一
使用第三方库:Math.js 、decimal.js、big.js ...
方案二
- 服务器返回给客户端的大数,按照"字符串"格式返回!
- 客户端把其变为 BigInt ,然后按照BigInt进行运算
- 最后把运算后的BigInt转换为字符串,在传递给服务器即可
javascript
const sum = BigInt('90071992547409912434234') + BigInt(12345)
console.log(sum, typeof sum); // 90071992547409912446579n 'bigint'
console.log(sum.toString(), typeof sum.toString()); // 90071992547409912446579 string
typeof数据类型检测的底层机制
12.5.6.1Runtime Semantics: Evaluation
image-20231204184604110
下载
❝
原文地址: 262.ecma-international.org/6.0/#sec-ty...
❞
数据类型检测方法
- typeof
- instanceof
- constructor
- Object.prototype.toString.call
- Array.isArray()
- isNaN
- ...
typeof原理
-
所有的数据类型值,在计算机底层都是按照 "64位" 的二进制值进行存储的
-
typeof是按照二进制值进行检测类型的
- 二进制的前三位是零,认为是对象,然后再去看是否实现call方法,如果实现了,返回 'function',没有实现,则返回 'object'
- null是64个零 typeof null -> 'object' 「局限性」
❝
其他判断数据类型的原理请查看 「一夜彻底掌握数据类型检测原理」
❞
typeof 特点
-
返回的结果是字符串,字符串中包含了对应的数据类型
- typeof typeof typeof [1,2,3] --> "string"
-
效率高:按照计算机底层存储的二进制进行检测
- 计算机科学:计算机原理、进制转化、计算机网络、数据结构和算法......
- 000 对象
- 1 整数
- 010 浮点数
- 100 字符串
- 110 布尔
- 000000.... null
- -2^30 undefined
- ......
-
typeof null --> "object"
-
typeof 对象 --> "object" && typeof 函数 --> "function"
-
typeof 未被声明的变量 --> "undefined"
场景
1、检测未被声明的变量,值是'undefined'
javascript
console.log(a); //Uncaught ReferenceError: a is not defined
console.log(typeof a); //'undefined'
2、支持更多的模块导入方案
javascript
(function () {
let utils = {};
if (typeof window !== "undefined") window.utils = utils;
if (typeof module === "object" && typeof module.exports === "object") module.exports = utils;
})();
3、检测当前值是否是一个对象
ini
const fn = options => {
const type = typeof options;
if (options !== null && type === "object") {
// 是个对象
}
};
fn({
x: 10,
y: 20
});
fn(10);
❝
后续会封装方法去判断普通标准对象
❞
数据类型间相互转换
「把其他数据类型转换为Number」
Number([val])
typescript
Number([val])
+ 一般用于浏览器的隐式转换中
1、数学运算
2、isNaN检测
3、 == 比较
...
+ 规则:
1、字符串转换为数字:空字符串变为0,如果出现任何非有效数字字符,结果都是NaN
2、把布尔转换为数字:true -> 1 false -> 0
3、null -> 0 undefined -> NaN
4、Symbol无法转换为数字,会报错:Uncaught TypeError: Cannot convert a Symbol value to a number
5、BigInt去除"n"(超过安全数字的,会按照科学计数法处理)
6、把对象转换为数字:
+ 先调用对象的 Symbol.toPrimitive 这个方法,如果不存在这个方法
+ 再调用对象的 valueOf 获取原始值,如果获取的值不是原始值
+ 再调用对象的 toString 把其变为字符串
+ 最后再把字符串基于Number方法转换为数字
案例1
javascript
const time = new Date();
// 首先检测 Symbol.toPrimitive 有没有,结果:有,而且是个函数:此时调用 `time[Symbol.toPrimitive]('number')` 该函数,返回 1701660384465,然后结束
console.log(Number(time)); // 1701660384465 --> number 类型
console.dir(time)
// 验证
console.log(time[Symbol.toPrimitive]('number')); // 1701660384465 --> number 类型
// ⚠️注意 参数不同
console.log(time[Symbol.toPrimitive]('default')); // Mon Dec 04 2023 11:27:02 GMT+0800 (中国标准时间) --> string 类型
image-20231204112535507
案例 2
javascript
const arr = [10];
// Symbol.toPrimitive 返回 undefined --> valueOf() 返回 [10], 不是原始值 --> toString() 返回 10 --> 把字符串基于Number方法转换为数字
console.log(Number(arr)); // 10
// 验证
console.log(arr[Symbol.toPrimitive]); // undefined
console.log(arr.valueOf(), typeof arr.valueOf()); // [10] 'object'
console.log(arr.toString(), typeof arr.toString()); // 10 string
// 思考
const arr2 = [10, 20];
// Symbol.toPrimitive 返回 undefined --> valueOf() 返回 [10, 20], 不是原始值 --> toString() 返回 10,20 --> 把字符串基于Number方法转换为数字: 字符串转换为数字,空字符串变为0,如果出现任何非有效数字字符,结果都是NaN
console.log(Number(arr2)); // NaN
// 验证
console.log(arr2[Symbol.toPrimitive]); // undefined
console.log(arr2.valueOf(), typeof arr2.valueOf()); // [10, 20] 'object'
console.log(arr2.toString(), typeof arr2.toString()); // 10,20 string
console.log(Number(arr2.toString())); // NaN
案例 3
javascript
const num = new Number(10);
// Symbol.toPrimitive 返回 undefined --> valueOf() 返回 10, 是原始值
console.log(Number(num)); // 10
// 验证
console.log(num[Symbol.toPrimitive]); // undefined
console.log(num.valueOf(), typeof num.valueOf()); // 10 'number'
parseInt([val],[radix])
scss
parseInt([val],[radix]) parseFloat([val])
+ 一般用于手动转换
+ 规则:[val]值必须是一个字符串,如果不是则先转换为字符串;然后从字符串左侧第一个字符开始找,把找到的有效数字字符最后转换为数字「一个都没找到就是NaN」;遇到一个非有效数字字符,不论后面是否还有有效数字字符,都不再查找了;
+ parseFloat可以多识别一个小数点
语法说明:
parseInt([val],[radix])
+ [val]必须是字符串,如果不是,要先隐式转换为字符串 String([val])
+ [radix]进制
+ 如果不写,或者写零:默认是10(特殊情况:如果字符串是以0x开始的,默认值是16)
+ 有效范围:2~36之间(如果不在这个区间,结果直接是NaN)
+ 从[val]字符串左侧第一个字符开始查找,查找出符合[radix]进制的值(遇到不符合的则结束查找,不论后面是否还有符合的);把找到的内容,按照[radix]进制,转换为10进制
🤔 进制有效范围为什么是 2~36 ?请参考文末 "其他" 中的问题
案例 1
javascript
console.log(parseInt('10102px13', 2)); // 10
找到符合二进制的 '1010',把这个二进制的值转换为10进制,方法:"「按权展开求和」"
ini
1*2^3+0*2^2+1*2^1+0*2^0 => 8+0+2+0 => 10
🤔 如何使用 js 方式实现 按权展开求和 呢?
案例 2
javascript
console.log(parseInt(0013)); // 11
// 会先将 0013 转成 11, 相当于 parseInt(11,2) --> parseInt('11',2)
console.log(parseInt(0013, 2)); // 3
// 1*2^1+1*2^0 -> 2 + 1 -> 3
console.log(parseInt('11',2)); // 3
⚠️ JS中遇到以0开始"数字",会默认把其按照8进制转为10进制,然后在进行其他操作
ini
0*8^3+0*8^2+1*8^1+3*8^0 -> 0+0+8+3 => 11
「把其他数据类型转换为String」
javascript
转化规则:
1、拿字符串包起来
2、特殊:Object.prototype.toString
出现情况:
1、String([val]) 或者 [val].toString()
2、"+" 除数学运算,还可能代表的字符串拼接
+ "+" 出现在一个值的左边,转换为数字
+ "+" 出现左右"两边",其中一边是字符串,或者是某些对象:会以字符串拼接的规则处理
image-20231208130241013
scss
String(1) // '1'
String(NaN) // 'NaN'
String(null) // 'null'
String(undefined) // 'undefined'
String(Symbol('dahuang')) // 'Symbol(dahuang)'
String([]) // ''
String([10]) // '10'
String([10, 20]) // '10,20'
String(function name(params) {}) // 'function name(params) {}'
String({}) // '[object Object]'
String({ a: 1 }) // '[object Object]'
案例 1
ini
const num = '10';
console.log(+num); // 10 'number'
案例 2
ini
let i = '10';
i++; // 一定是数学运算
console.log(i); //11
i = 10
i += 1;
console.log(i); // 11
i = '10';
i += 1;
console.log(i); //'101'
i = '10';
i = i + 1;
console.log(i); //'101'
案例 3
javascript
console.log(10 + "10"); // "1010"
// new Number(10)[Symbol.toPrimitive] 返回 undefined --> new Number(10).valueOf() -> 10 是原始值
console.log(10 + new Number(10)); // 20
案例 4
javascript
// new Date()[Symbol.toPrimitive]('default') -> 'Wed Apr 06 2022 22:01:38 GMT+0800 (中国标准时间)' 字符串类型,然后和 10 进行字符串拼接
console.log(10 + new Date()); //'10Wed Apr 06 2022 22:01:38 GMT+0800 (中国标准时间)'
// [10].toString() -> '10' => 10+'10'
console.log(10 + [10]); //'1010'
// 验证
console.log([10][Symbol.toPrimitive]); // undefined
console.log([10].valueOf(), typeof [10].valueOf()); // [10] 'object'
console.log([10].toString(), typeof [10].toString()); // 10 string
案例 5
javascript
const result = 100 + true + 21.2 + null + undefined + "Tencent" + [] + null + 9 + false;
console.log(result); // NaNTencentnull9false
执行步骤如下:
javascript
1、100 + true 数学运算, true 转换成 1 --> 101 + 21.2 + null + undefined + "Tencent" + [] + null + 9 + false
2、101 + 21.2 数学运算, 122.2 --> 122.2 + null + undefined + "Tencent" + [] + null + 9 + false
3、122.2 + null 数学运算, null 转换成 0 --> 122.2 + undefined + "Tencent" + [] + null + 9 + false
4、122.2 + undefined 数学运算, undefined 转换成 NaN, 122.2 + NaN = NaN --> NaN + "Tencent" + [] + null + 9 + false
5、NaN + "Tencent" 字符串拼接 --> "NaNTencent" + [] + null + 9 + false
6、"NaNTencent" + [] 字符串拼接,[] 转换成 '' --> "NaNTencent" + null + 9 + false
7、"NaNTencent" + null 字符串拼接,null 转换成 'null' --> "NaNTencentnull" + 9 + false
8、"NaNTencentnull" + 9 字符串拼接,9 转换成 '9' --> "NaNTencentnull9" + false
9、"NaNTencentnull9" + false 字符串拼接,false 转换成 'false' --> "NaNTencentnull9false"
把其他数据类型转换为Boolean
javascript
转换规则:除了"0/NaN/空字符串/null/undefined"五个值是false,其余都是true
出现情况:
1、Boolean([val]) 或者 !/!!
2、条件判断
...
"=="比较时候的相互转换规则
javascript
"=="相等,两边数据类型不同,需要先转为相同类型,然后再进行比较
1、对象==字符串 对象转字符串 Symbol.toPrimitive -> valueOf -> toString
2、null==undefined -> true null/undefined和其他任何值都不相等
null===undefined -> false
3、对象==对象 比较的是堆内存地址,地址相同则相等
4、NaN!==NaN Object.is(NaN, NaN) --> true
5、除了以上情况,只要两边类型不一致,剩下的都是转换为数字,然后再进行比较的
"==="绝对相等,如果两边类型不同,则直接是false,不会转换数据类型
案例
ini
// 都转换为数字 0==0 => true
console.log([] == false); // true
// 先处理 ![] => false false==false => true
console.log(![] == false); // true
装箱和拆箱操作
javascript
// num是原始值,不是对象,按常理来讲,是不能做"成员访问"
// 默认装箱操作:new Number(num) 变为非标准特殊对象,这样就可以调用toFixed了
let num1 = 10;
console.log(num1.toFixed(2)); // 10.00
// 在操作的过程中,浏览器会把num这个非标准特殊对象变为原始值「Symbol.toPrimitive->valueOf...」,这个操作叫做拆箱
let num2 = new Number(10);
console.log(num2 + 10); // 20
思考题
0.1 + 0.2 ?== 0.3
「答案」 :表达式 0.1 + 0.2 == 0.3
的结果是 false
。
「原因」 : JavaScript(以及许多其他编程语言)中的浮点数遵循IEEE 754 标准,而该标准中的浮点数运算可能会带来精度问题。浮点数表示是一种用于近似表示实数的方式,特别是对于那些不能精确表示为分数的数(如 0.1 或 0.2)。在二进制中,像 0.1 这样的十进制分数不能被精确表示,这类似于在十进制中无法精确表示 1/3,那么 2个不能被精确表示的数进行相加,得到的结果肯定也不是精确的数字。
「解决方案」
-
使用三方库:Math.js 、decimal.js、big.js ...
-
扩大系数法,将数字转成整数
inifunction magnifyCoefficient(num1, num2) { const coefficient = function coefficient(num) { num = num + ''; let [, char = ''] = num.split('.'), len = char.length; // 或者使用 10**len return Math.pow(10, len); }; num1 = +num1; num2 = +num2; if (isNaN(num1) || isNaN(num2)) return NaN; let max = Math.max(coefficient(num1), coefficient(num2)); return (num1 * max + num2 * max) / max; }
arr.map(parseInt)
「问题」 let arr = [27.2, 0, '0013', '14px', 123]; arr = arr.map(parseInt);
输出多少?
ini
let arr = [27.2, 0, '0013', '14px', 123];
arr = arr.map(parseInt);
console.log(arr); // [27, NaN, 1, 1, 27]
// 解析
/*
parseInt(27.2,0) -> parseInt('27.2',10)
'27' 把其当做10进制转换为10进制 => 27
parseInt(0,1)
NaN
parseInt('0013',2)
'001' 当做2进制转换为10进制
0*2^2+0*2^1+1*2^0 -> 0+0+1 => 1
parseInt('14px',3)
'1' 当做3进制转换为10进制
1*3^0 -> 1
parseInt(123,4) -> parseInt('123',4)
'123' 当做4进制转换为10进制
1*4^2+2*4^1+3*4^0 -> 16+8+3 => 27
*/
a 等于多少可以打印出 'OK'
ini
var a = ?;
if (a == 1 && a == 2 && a == 3) {
console.log('OK');
}
思路1
利用==比较会转换数据类型,而对象转数字会经历一个详细步骤「Symbol.toPrimitive -> valueOf -> toString...」
方式一
ini
var a = {
i: 0,
[Symbol.toPrimitive]() {
return ++this.i;
}
};
方式二
ini
var a = [1, 2, 3];
a.toString = a.shift;
if (a == 1 && a == 2 && a == 3) {
console.log('OK');
}
思路 2
全局上下文中,获取a的值:首先看VO(G)中有没有,没有再继续去GO(window)中查找
javascript
var i = 0;
Object.defineProperty(window, 'a', {
get() {
return ++i;
}
});
if (a === 1 && a === 2 && a === 3) {
console.log('OK');
}
拓展
进制为什么限定在 2-36 ?
- 「常用进制系统」:最常用的进制系统是二进制(基数为 2)、八进制(基数为 8)、十进制(基数为 10)和十六进制(基数为 16)。这些进制系统在计算机科学和编程中非常普遍,因为它们适应了计算机的二进制逻辑和人类的日常计数习惯。
- 「表示字符的限制」:在大多数编程语境中,我们使用 0-9 的十个数字和 A-Z 的二十六个大写字母来表示不同的进制数位。这共有 36 个字符,因此自然形成了 36 进制(0-9 代表数值 0-9,A-Z 代表数值 10-35)作为实际可实现的最高进制系统。
- 「可读性和实用性」:超过 36 进制的数制需要额外的字符来表示更高的数值,这不仅会降低可读性,而且在大多数实际应用中并不必要。计算机和人类处理的大多数数据都可以方便地使用 2-36 进制来表示。
- 「标准化」:为了保持编程语言和工具之间的一致性和兼容性,标准化是非常重要的。限制进制范围有助于维持这种标准化,使得不同的系统和语言能够更容易地进行交互和数据交换。