一夜彻底掌握数据类型及相互间的转换规则

一夜彻底掌握数据类型及相互间的转换规则

概览

  • 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.asyncIteratoriteratorhasInstancetoPrimitivetoStringTag...是某些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.hasInstanceinstanceof 底层机制中会使用到, 详情请看 「一夜彻底掌握数据类型检测原理」

toPrimitive

数据类型转换中会使用到,请看下文 数据类型间相互转换

toStringTag

Symbol.toStringTagObject.prototype.toString.call([value]) 进行数据类型判断时,底层机制中会使用到, 详情请看 「一夜彻底掌握数据类型检测原理」

BigInt

JS中的最大安全数: Number.MAX_SAFE_INTEGER ( 9007199254740991 )

JS中的最小安全数: Number.MIN_SAFE_INTEGER ( -9007199254740991 )

超过安全数后,进行运算或者访问,结果会不准确!!!

解决方案

方案一

使用第三方库:Math.js 、decimal.js、big.js ...

方案二

  1. 服务器返回给客户端的大数,按照"字符串"格式返回!
  2. 客户端把其变为 BigInt ,然后按照BigInt进行运算
  3. 最后把运算后的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 ...

  • 扩大系数法,将数字转成整数

    ini 复制代码
    function 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 进制来表示。
  • 「标准化」:为了保持编程语言和工具之间的一致性和兼容性,标准化是非常重要的。限制进制范围有助于维持这种标准化,使得不同的系统和语言能够更容易地进行交互和数据交换。
相关推荐
理想不理想v9 分钟前
vue经典前端面试题
前端·javascript·vue.js
小阮的学习笔记23 分钟前
Vue3中使用LogicFlow实现简单流程图
javascript·vue.js·流程图
YBN娜24 分钟前
Vue实现登录功能
前端·javascript·vue.js
阳光开朗大男孩 = ̄ω ̄=24 分钟前
CSS——选择器、PxCook软件、盒子模型
前端·javascript·css
小政爱学习!1 小时前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
魏大帅。1 小时前
Axios 的 responseType 属性详解及 Blob 与 ArrayBuffer 解析
前端·javascript·ajax
花花鱼1 小时前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui
k09331 小时前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
web行路人2 小时前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
番茄小酱0012 小时前
Expo|ReactNative 中实现扫描二维码功能
javascript·react native·react.js