彻底搞懂JS类型、类型判断、类型转换

作者:前端小帅

掘金:https://juejin.cn/post/7092225590102589470

博客:https://ssscode.com/pages/b92fbe

JS类型

最新的 ECMAScript 标准定义了 8 种数据类型:

  • 7种原始类型:NumberStringBooleanNullUndefinedSymbolBigInt
  • Object引用类型:ArrayObjectDateRegExpErrorMapSetWeakMapWeakSet,几乎所有通过 new 创建的,即构造函数类型
  • Symbol类型Symbol
    • ECMAScript 2015 新增
    • 符号(Symbols)类型是唯一不可修改的原始值
    • 可以用来作为对象的键(key)
  • BigInt类型BigInt
    • ECMAScript 2020 新增
    • BigInt 可以表示任意大的整数。
    • 可以用在一个整数字面量后面加 n 的方式定义一个 BigInt ,如:10n,或者调用函数 BigInt()(但不包含 new 运算符)并传递一个整数值或字符串值。
typeof 1n === 'bigint'// true
typeof BigInt('1') === 'bigint'// true
彻底搞懂JS类型、类型判断、类型转换
emoji

堆和栈

为了更清晰的明白数据在内存中的存储结构,在介绍数据类型之前我们先了解下堆和栈的概念。

  • **栈(stack)**:是栈内存的简称,栈是自动分配相对固定大小的内存空间,并由系统自动释放,栈数据结构遵循FILO(first in last out)先进后出的原则
  • **堆(heap)**:是堆内存的简称,堆是动态分配内存,内存大小不固定,也不会自动释放,堆数据结构是一种无序的树状结构

在JS中的存在形式以及我们应该如何理解:

  • 基本数据类型:都是直接按值存放在栈内存中,占用的内存空间的大小是确定的,并由系统自动分配和自动释放。
    • 包括:NumberStringBooleanNullUndefinedSymbolBigInt
    • 这样带来的好处就是,内存可以及时得到回收,相对于堆来说,更加容易管理内存空间
  • 引用数据类型:指那些可能由多个值构成的对象
    • 如对象(Object)、数组(Array)、函数(Function) ,它们是通过拷贝和new出来的,这样的数据存储于堆中

传值和传址的区别:

  • 基本类型:采用的是值传递。
    • 基本类型是把数据的值存储于栈中
    • 在赋值的时候,会把值复制一份到栈中,然后把栈顶的值赋给变量
  • 引用类型:则是地址传递
    • 引用类型的是把数据的地址指针存储于栈中
    • 在赋值的时候是将存放在栈内存中的地址指针赋值给接收的变量
// 1. 基本类型
// b被赋值为a,传递的是值1
let a = 1;
let b = a;
b = 2
// a 1
// b 2

// 2. 引用类型
// obj2被赋值为obj1,传递的是地址
let obj1 = { name'zhangsan'age18 };
let obj2 = obj1;
obj2.age = 20;
// obj1、obj2都会变化,因为地址指向的是同一个
// obj1 {name: 'zhangsan', age: 20}
// obj2 {name: 'zhangsan', age: 20}

延伸:浅拷贝、深拷贝的作用

null和undefined区别

  • 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefinednull
  • undefined 代表的含义是未定义,null 代表的含义是空对象。
    • 一般变量声明了但还没有定义的时候会返回 undefinednull主要用于赋值给一些可能会返回对象的变量,作为初始化。
  • undefinedJavaScript 中不是一个保留字
    • 这意味着可以使用 undefined 来作为一个变量名,但是这样的做法是非常危险的,它会影响对 undefined 值的判断。我们可以通过一些方法获得安全的 undefined 值,比如说 void 0
  • 当对这两种类型使用 typeof 进行判断时,Null 类型会返回 “object”,这是一个历史遗留的问题(见下面类型判断部分)。
  • 当使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false

拓展:

  • 在实际开发过程中会使用 if(xx == null) 来判断变量 xx是否为undefinednull,这样可以更加简洁。(在目前最新的语法中??就是只针对undefinednull做判断处理的,如 let a = xx ?? 123
  • 使用引用类型时在数据不使用的时候赋值为null,这样可以避免内存泄漏。

0.1+0.2 ! == 0.3

计算机是通过二进制的方式存储数据的,所以计算机计算0.1+0.2的时候,实际上是计算两个数的二进制的和,但是这两个数的二进制都是无限循环的数。

一般我们认为数字包括整数和小数,但是在 JavaScript 中只有一种数字类型:Number,它的实现遵循IEEE 754标准,使用64位固定长度来表示,也就是标准的double双精度浮点数

在二进制科学表示法中,双精度浮点数的小数部分最多只能保留52位,再加上前面的1,其实就是保留53位有效数字,剩余的需要舍去,遵从0舍1入的原则。

根据这个原则,0.1和0.2的二进制数相加,再转化为十进制数就是:0.30000000000000004

如何实现 0.1+0.2=0.3:

对于这个问题,一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对JavaScript来说,这个值通常为2^-52,在ES6中,提供了Number.EPSILON属性,而它的值就是2^-52,只要判断0.1+0.2-0.3是否小于Number.EPSILON,如果小于,就可以判断为 0.1+0.2 ===0.3

Number.EPSILON 属性表示 1 与Number可表示的大于 1 的最小的浮点数之间的差值。

EPSILON 属性的值接近于 2.2204460492503130808472633361816E-16,或者 2^-52。

function numberepsilon(arg1,arg2){                   
  return Math.abs(arg1 - arg2) < Number.EPSILON;        
}        

console.log(numberepsilon(0.1 + 0.20.3)); // true

isNaN 和 Number.isNaN 函数的区别

  • 函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。
  • 函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。

JS类型判断

typeof

typeof 是由JS提供的,主要是检查数据类型,即这8种数据类型,不过对于引用类型返回值都是Object(function除外),无法做到正确区分是哪种具体类型,比较合适的方式是使用 instanceof关键字来判断引用类型。

返回值均为小写字符串:如 typeof undefined === "undefined"typeof {} === "object"

特别注意

  • typeof null === "object"

在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null 也因此返回 “object”。

  • typeof可以检测出函数类型

除 Function 外的所有构造函数的类型都是 ‘object’

两个特殊:

  • undefined(JSVAL_VOID)是整数−2^30(整数范围之外的数字)
  • null(JSVAL_NULL) 为机器码NULL的空指针,或者说:为0的object类型标签。

在判断数据类型时,是根据机器码低位标识来判断的,而null的机器码标识全为0,而对象的机器码低位标识为000。所以typeof null的结果被误判为Object

function a({}
console.log(typeof a); // function

var func = new Function();
typeof func; // 返回 'function'

instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

function Person({}
var boy = new Person()
console.log(boy instanceof Person) // true,因为 Object.getPrototypeOf(o) === C.prototype
console.log(boy instanceof Object// true,因为 Object.prototype.isPrototypeOf(o) 返回 true

D.prototype = new Person() // 继承
var o3 = new D()
o3 instanceof Person // true 因为 Person.prototype 现在在 o3 的原型链上

Tips:有一个逐级向上查找的过程(原型的终点是null

o => Person.prototype => Object.prototype => null

实现原理:

function myInstanceof(left, right{
  // 获取对象的原型
  let proto = Object.getPrototypeOf(left)
  // 获取构造函数的 prototype 对象
  let prototype = right.prototype; 
 
  // 判断构造函数的 prototype 对象是否在对象的原型链上
  while (true) {
    if (!proto) return false;
    if (proto === prototype) return true;
    // 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
    proto = Object.getPrototypeOf(proto);
  }
}

// console.log(myInstanceof(boy, Person)) // true
// console.log(myInstanceof(boy, Object)) // true

Object.prototype.toString

toString() 方法返回一个表示该对象的字符串。

Object.prototype.toString.call(new Date// [object Date]

同样是检测对象obj调用toString方法,obj.toString()的结果和Object.prototype.toString.call(obj)的结果不一样,这是为什么?

这是因为toStringObject的原型方法,而Arrayfunction等类型作为Object的实例,都重写了toString方法

目前该种检测方法算是比较全面准确的,且使用较为广泛的一种。

isArray

Array.isArray([]) // true

类型检查通用方法

  • 官方版本
function type(obj, fullClass{
    // get toPrototypeString() of obj (handles all types)
    // Early JS environments return '[object Object]' for null, so it's best to directly check for it.
    if (fullClass) {
        return obj === null ? '[object Null]' : Object.prototype.toString.call(obj)
    }
    if (obj == null) {
        return (obj + '').toLowerCase()
    } // implicit toString() conversion

    var deepType = Object.prototype.toString.call(obj).slice(8-1).toLowerCase()
    if (deepType === 'generatorfunction') {
        return 'function'
    }

    // Prevent overspecificity (for example, [object HTMLDivElement], etc).
    // Account for functionish Regexp (Android <=2.3), functionish <object> element (Chrome <=57, Firefox <=52), etc.
    // String.prototype.match is universally supported.

    return deepType.match(/^(array|bigint|date|error|function|generator|regexp|symbol)$/)
        ? deepType
        : typeof obj === 'object' || typeof obj === 'function'
        ? 'object'
        : typeof obj
}
  • Jquery版本
var class2type = {};

// 生成class2type映射
"Boolean Number String Function Array Date RegExp Object Error".split(" ").map(function(item, index{
    class2type["[object " + item + "]"] = item.toLowerCase();
})

function type(obj{
    // 一箭双雕
    if (obj == null) {
        return obj + "";
    }
    return typeof obj === "object" || typeof obj === "function" ?
        class2type[Object.prototype.toString.call(obj)] || "object" :
        typeof obj;
}

类型转换、相等(==)判断

等于运算符(==)检查其两个操作数是否相等,并返回Boolean结果。与严格相等运算符(===)不同,它会尝试强制类型转换并且比较不同类型的操作数。

相等运算符(==!=)使用抽象相等比较算法比较两个操作数。可以大致概括如下:

  • 如果两个操作数都是对象,则仅当两个操作数都引用同一个对象时才返回true
  • 如果一个操作数是null,另一个操作数是undefined,则返回true。(undefined == null
  • 如果两个操作数是不同类型的,就会尝试在比较之前将它们转换为相同类型
    • 数字字符串进行比较时,会尝试将字符串转换为数字值
    • 如果操作数之一是Boolean,则将布尔操作数转换为1或0
    • 如果操作数之一是对象,另一个是数字或字符串,会尝试使用对象的valueOf()toString()方法将对象转换为原始值
  • 如果操作数具有相同的类型,则对值进行比较

一些说明,针对对象转换的情况

目的就是为了转换为原始值

为了进行转换,JavaScript 尝试查找并调用三个对象方法:Symbol.toPrimitivetoStringvalueOf

  • Symbol.toPrimitive优先级最高,不过这只限于存在Symbol类型时
  • 对于字符串转换:优先级是 toString() > valueOf()
  • 对于数学运算:优先级是 valueOf() > toString()

看个列子:

let user = {name"John"};

user.toString() // [object Object]
user.valueOf() // user.valueOf() === user // true valueOf方法返回对象本身 {name: 'John'}

user == '[object Object]' // true

默认情况下,普通对象具有 toStringvalueOf 方法:

  • toString 方法返回一个字符串 "[object Object]"
  • valueOf 方法返回对象自身

历史原因:

由于历史原因,如果 toStringvalueOf 返回一个对象,则不会出现 error,但是这种值会被忽略(就像这种方法根本不存在)。这是因为在 JavaScript 语言发展初期,没有很好的 “error” 的概念。

相反,Symbol.toPrimitive 必须返回一个原始值,否则就会出现 error。

验证数学运算:

从上面的规则我们知道数学运算转换规则的优先级是:valueOf() > toString()

let obj = {
  // toString 在没有其他方法的情况下处理所有转换
  toString() {
    return "2";
  }
};
console.log(obj * 2// 4,对象被转换为原始值字符串 "2",之后它被乘法转换为数字 2

转换过程:

  1. 对象被转换为原始值(valueOf

     obj.valueOf() // {toString: ƒ} 对象本身
  2. 如果生成的原始值的类型不正确,则继续进行转换(toString

     obj.toString() // '2' 字符串

对象 — 原始值转换的阶段性总结

对象到原始值的转换,是由许多期望以原始值作为值的内建函数和运算符自动调用的。

这里有三种类型(hint):

  • “string”(对于 alert 和其他需要字符串的操作)
  • “number”(对于数学运算)
  • “default”(少数运算符)

规范明确描述了哪个运算符使用哪个 hint。很少有运算符“不知道期望什么”并使用 “default” hint。通常对于内建对象,”default” hint 的处理方式与 “number” 相同,因此在实践中,最后两个 hint 常常合并在一起。

转换算法是:

  1. 调用 obj[Symbol.toPrimitive](hint) 如果这个方法存在
  2. 否则,如果 hint 是 “string”
    • 尝试 obj.toString()obj.valueOf(),无论哪个存在。
  3. 否则,如果 hint 是 “number” 或者 “default”
    • 尝试 obj.valueOf()obj.toString(),无论哪个存在。

一个简单测试

const string3 = new String("hello");
const string4 = new String("hello");

console.log(string3 == string4); // false

以上题目应该类比于下面的写法,即通过 new 构造的为对象类型,需要根据对象和对象的比较规则判断(因为是两个不同的引用地址,所以返回false

const object1 = {"key""value"}
const object2 = {"key""value"};

object1 == object2 // false

总结

数据类型做为一门语言的基础,是一个非常重要的概念。对于这方面的知识点一定要弄清楚,并深入理解,这样才能更好地应用在 JavaScript 中。

各种面试题的变种和骚操作,都是来自对语言的深入理解,对于一个 toString的使用,就可以玩出很多花样来。

不过,由于JS的诞生背景比较特殊,我们现在再回头深入学习JS的时候,经常会发现一些有坑的地方,各种历史遗留和设计之类的问题等。对于这些“特性”我们也应该有所了解,做到知其然、知其所以然。但是,我们也不能以偏概全,不得不说JS确实是一门非常强大的语言(毕竟能用JS实现的最终都会用JS实现🤪)

新的ECMAScript规范和标准也在不断地完善JS语言,各种新特性,如ES6、ES7等等,以及TypeScript的出现,也使得JS这门语言更加丰富规范(扶我起来,我还能学🤓)

资料

  • JavaScript 数据类型和数据结构
  • JavaScript专题之类型判断
  • 为什么JavaScript里面typeof(null)的值是”object”?
  • 【译】谈谈“typeof null为object”这一bug的由来
  • typeof
  • Equality (==)
  • 对象 — 原始值转换


原文始发于微信公众号(前端小帅):彻底搞懂JS类型、类型判断、类型转换

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/114513.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!