JavaScript数据类型(超详细,超精简)

数据类型概览

首先需要明确 JavaScript 中的数据类型都有哪些,其中包括基本数据类型(7种)引用数据类型

基本数据类型:Undefined,Null,Boolean, Number,String,Bigint,symbol(ES6)

引用数据类型:Object

关于引用类型,Object 中又包含 普通对象-Object,数组对象-Array,正则对象-RegExp,日期对象-Date,数学对象-Math,函数对象-Function

  • 值类型

    • 占用空间固定,保存在 栈 中:当一个方法执行时,每个方法都会建立自己的内存栈,也就是所谓的函数作用域,基础变量的值是存储在栈中的,而引用类型变量存储在栈中的是指向堆中的数组或者对象的”地址”
    • 保存与复制的是值本身
    • 可以用 typeof 检测值类型
    • 基本数据类型是值类型
  • 引用类型

    • 占用空间不固定,保存在堆中:由于对象的创建成本比较大,在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用,这个运行时数据区就是堆内存
    • 保存与复制的是指向对象的一个指针
    • 使用 instanceof() 检测数据类型
    • 使用 new() 方法构造出的对象是引用型
function test(person) {
  person.age = 26;
  person = {
    name: "hzj",
    age: 18,
  };
  return person;
}
const p1 = {
  name: "fyq",
  age: 19,
};
const p2 = test(p1);
console.log(p1); // p1:{name: “fyq”, age: 26}
console.log(p2); // p2:{name: “hzj”, age: 18}

判断数据类型

判断数据类型主要有两种方法,一种是 typeof,另一种是 instanceof

typeof 用法介绍

关于 typeof 需要知道以下几点:

  • 对于基本数据类型,除了 null,都可以调用 typeof 显示正确的类型
  • 对于引用数据类型,除了函数之外都会显示 object
  • typeof 可以返回的数据类型有七种: string、boolean、number、Object、Function、undefined、symbol(ES6)

基本数据类型例子:

typeof 1; //"number"
typeof undefined; //"undefined"
typeof NaN; // "number"
typeof Symbol(); //"symbol"
typeof Math.max(); //"number"

引用数据类型例子:

typeof []; //"object"
typeof {}; //"object"
typeof function() {}; //"function"
typeof [1, 2, 3, 4, 5]; //"object"
typeof new Date(); //"object"
typeof Math; //"object"
typeof console.log; // ”function“
typeof console.log(); //"undefined"

因此 typeof 用来判断对象数据类型是不合适的,需要使用 instanceof

typeof 原理解析

typeof(null) === 'object'的原因在小黄书《你不知道的 JavaScript》中有这么一段:

原理是这样的, 不同的对象在底层都表示为二进制, 在 JavaScript 中二进制前三位都为 0 的话会被判断为 object 类型, null 的二进制表示是全 0, 自然前三位也是 0, 所以执行 typeof 时会返回“object”。

在 javascript 的最初版本中,使用的 32 位系统,为了性能考虑使用低位存储了变量的类型信息

000:对象
1:整数
010:浮点数
100:字符串
110:布尔

有 2 个值比较特殊:

undefined:用 - (−2^30)表示。
null:对应机器码的 NULL 指针,一般是全零。

instanceof 用法介绍

关于 instanceof 需要知道以下几点:

  • instanceof 是基于原型链查询的,只要处于原型链中,判断永远为 true
  • 在表达式 left instanceof right 中,会沿着 left 的原型链查找,看看是否存在 right 的 prototype 对象left.__proto__.__proto__... =?= right.prototype

举例

const Person = function() {};
const p1 = new Person();
p1 instanceof Person; // true

var str1 = "hello world";
str1 instanceof String; // false

var str2 = new String("hello world");
str2 instanceof String; // true

instanceof 实现

首先 instanceof 左侧必须是对象, 才能找到它的原型链

instanceof 右侧必须是函数, 函数才会 prototype 属性

迭代 , 左侧对象的原型不等于右侧的 prototype 时, 沿着原型链重新赋值左侧

function myInstanceof(left, right) {
  if (typeof left !== "object" || left === null) return false;
  let proto = Object.getPrototypeOf(left),
    prototype = right.prototype;
  while (true) {
    if (!proto) return false;
    if (proto === prototype) return true;
    proto = Object.getPrototypeOf(prop);
  }
}

除了使用 Object.getPrototypeOf(left)获取原型,也可以使用__proto__,下面这种添加了对基本类型的判断

function instance_of(L, R) {
  if (typeof L !== "object" || L === null) return false;

  let RP = R.prototype;
  L = L._proto_;
  while (true) {
    if (L === null) {
      return false;
    }
    if (L === RP) {
      return true;
    }
    L = L._proto_;
  }
}

有一个问题是 instanceof 到底能不能判断基本数据类型?比如下面这样

class PrimitiveNumber {
  static [Symbol.hasInstance](x) {
    return typeof x === "number";
  }
}
console.log(111 instanceof PrimitiveNumber); // true

当然也可以判断复杂数据类型

class Array1 {
  static [Symbol.hasInstance](instance) {
    return Array.isArray(instance);
  }
}

console.log([] instanceof Array1);

Symbol.hasInstance 用于判断某对象是否为某构造器的实例。因此你可以用它自定义 instanceof 操作符在某个类上的行为,通过这个就可以使用 instanceof 来判断基本数据类型了

其他判断数据类型的方法

其实除了上面两种方式,还有其他方式来判断 js 变量的类型 constructor、 prototype

Object.prototype.toString.call()

例子:

const an = ["Hello", "An"];
an.toString(); // "Hello,An"
Object.prototype.toString.call(an); // "[object Array]"

Object.prototype.toString.call("An"); // "[object String]"
Object.prototype.toString.call(1); // "[object Number]"
Object.prototype.toString.call(Symbol(1)); // "[object Symbol]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(function() {}); // "[object Function]"
Object.prototype.toString.call({ name: "An" }); // "[object Object]"

function f(name) {
  this.name = name;
}
var f1 = new f("martin");
console.log(Object.prototype.toString.call(f1)); //[object Object]
["Hello", "An"].toString(); //"Hello,An"

每一个继承 Object 的对象都有 toString 方法,如果 toString 方法没有重写的话,会返回 [Object type],其中 type 为对象的类型。但当除了 Object 类型的对象外,其他类型直接使用 toString 方法时,会直接返回都是内容的字符串,所以我们需要使用 call 或者 apply 方法来改变 toString 方法的执行上下文

  • 优点:这种方法对于所有基本的数据类型都能进行判断,即使是 null 和 undefined 。
  • 缺点:不能精准判断自定义对象,对于自定义对象只会返回[object Object]

所以 Object.prototype.toString.call() 常用于判断浏览器内置对象。

Array.isArray()

这个常用来判断数组,这里将它与 instance 做一下比较

var iframe = document.createElement("iframe");
document.body.appendChild(iframe);
xArray = window.frames[window.frames.length - 1].Array;
var arr = new xArray(1, 2, 3); // [1,2,3]

// Correctly checking for Array
Array.isArray(arr); // true
Object.prototype.toString.call(arr); // "[object Array]"
// Considered harmful, because doesn't work though iframes
arr instanceof Array; // false

优点:当检测 Array 实例时,Array.isArray 优于 instanceof ,因为 Array.isArray 可以检测出 iframes,

缺点:只能判别数组

Array.isArray()是 ES5 新增的方法,当不存在 Array.isArray() ,可以用 Object.prototype.toString.call() 实现。

if (!Array.isArray) {
  Array.isArray = function(arg) {
    return Object.prototype.toString.call(arg) === "[object Array]";
  };
}

数据类型转换

说到类型转换就必须知道 js 有显式类型转换和隐式类型转换

先给出他们的定义(以下定义来自小黄书):

显式类型转换:隐式强制类型转换指的是那些隐蔽的强制类型转换,副作用也不是很明显。换句话说,你自己觉得不够明显的强制类型转换都可以算作隐式强制类型转换,比如 parseInt,parseFloat,Number

隐式类型转换:显式强制类型转换是那些显而易见的类型转换,很多类型转换都属于此列,比如 + -

显式强制类型转换旨在让代码更加清晰易读,而隐式强制类型转换看起来就像是它的对立面,会让代码变得晦涩难懂

解析字符串中的数字和将字符串强制类型转换为数字的返回结果都是数字,它们之间的区别是什么

解析允许字符串(如 parseInt() )中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停止。而转换(如 Number ())不允许出现非数字字符,否则会失败并返回 NaN

哪些会被隐式转换为 false

ES5 规范 9.2 节中定义了抽象操作 ToBoolean,列举了布尔强制类型转换所有可能出现的结果

Undefined、null、关键字 false、NaN、0 +0 -0、空字符串 会转换为 false

假值的布尔强制类型转换结果为 false。从逻辑上说,假值列表以外的都应该是真值。

浏览器在某些特定情况下,在常规 JavaScript 语法基础上自己创建了一些外来值,这些就是“假值对象”。假值对象看起来和普通对象并无二致(都有属性,等等),但将它们强制类型转换为布尔值时结果为 false 最常见的例子是 document.all,它是一个类数组对象,包含了页面上的所有元素,由 DOM(而不是 JavaScript 引擎)提供给 JavaScript 程序使用

  • if (..) 语句中的条件判断表达式。
  • for ( .. ; .. ; .. ) 语句中的条件判断表达式(第二个)。
  • while (..) 和 do..while(..) 循环中的条件判断表达式。
  • ? : 中的条件判断表达式。
  • 逻辑运算符 ||(逻辑或)和 &&(逻辑与)左边的操作数(作为条件判断表达式)

ES6 允许从符号到字符串的显式强制类型转换,然而隐式强制类型转换会产生错误。Symbol 值不能够被强制类型转换为数字(显式和隐式都会产生错误),但可以被强制类型转换为布尔值(显式和隐式结果都是 true )。

其他值到字符串的转换规则

规范的 9.8 节中定义了抽象操作 ToString ,它负责处理非字符串到字符串的强制类型转换

非字符串到字符串的强制类型转换

  • Null 和 Undefined 类型 ,null 转换为 “null”,undefined 转换为 “undefined”,
  • Boolean 类型,true 转换为 “true”,false 转换为 “false”。
  • Number 类型的值直接转换,不过那些极小和极大的数字会使用指数形式。
  • Symbol 类型的值直接转换,但是只允许显式强制类型转换,使用隐式强制类型转换会产生错误。
  • 对普通对象来说,除非自行定义 toString() 方法,否则会调用 toString()(Object.prototype.toString())来返回内部属性 [[Class]] 的值,如"[objectObject]"。如果对象有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值

复杂数据类型到字符串的转换

  • 首先,会调用 valueOf 方法,如果方法的返回值是一个基本数据类型,就返回这个值
  • 如果调用 valueOf 方法之后的返回值仍旧是一个复杂数据类型,就会调用该对象的 toString 方法
  • 如果 toString 方法调用之后的返回值是一个基本数据类型,就返回这个值,
  • 如果 toString 方法调用之后的返回值是一个复杂数据类型,就报一个错误。
1;
var obj = {
  valueOf: function() {
    return 1;
  },
};
console.log(obj + ""); //'1'
2;
var obj = {
  valueOf: function() {
    return [1, 2]; //Object.prototype.valueOf.call([1,2]) -> [1, 2]
  },
};
console.log(obj + ""); //'[object Object]';
3;
var obj = {
  valueOf: function() {
    return [1, 2];
  },
  toString: function() {
    return 1;
  },
};
console.log(obj + ""); //'1';
4;
var obj = {
  valueOf: function() {
    return [1, 2];
  },
  toString: function() {
    return [1, 2, 3]; //Object.prototype.toString.call([1, 2, 3]) -> "[object Array]"
  },
};
console.log(obj + ""); // 报错 Uncaught TypeError: Cannot convert object to primitive value
var arr = [
  new Object(),
  new Date(),
  new RegExp(),
  new String(),
  new Number(),
  new Boolean(),
  new Function(),
  new Array(),
  Math,
];

console.log(arr.length); //9

for (var i = 0; i < arr.length; i++) {
  arr[i].valueOf = function() {
    return [1, 2, 3];
  };
  arr[i].toString = function() {
    return "toString";
  };
  console.log(arr[i] + ""); //9次"toString"
}

其他值到数字值的转换规则

有时我们需要将非数字值当作数字来使用,比如数学运算。为此 ES5 规范在 9.3 节定义了抽象操作 ToNumber。

  • Undefined 类型的值转换为 NaN。
  • Null 类型的值转换为 0。
  • Boolean 类型的值,true 转换为 1,false 转换为 0。
  • String 类型的值转换如同使用 Number() 函数进行转换,如果包含非数字值则转换为 NaN,空字符串为 0。
  • Symbol 类型的值不能转换为数字,会报错。
  • 对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。为了将值转换为相应的基本类型值,抽象操作 ToPrimitive 会首先(通过内部操作 DefaultValue)检查该值是否有 valueOf() 方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值(如果存在)来进行强制类型转换。如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误

其他

{} 的 valueOf 结果为 {} ,toString 的结果为 “[object Object]”

[] 的 valueOf 结果为 [] ,toString 的结果为 “”

== 中,左右两边都需要转换为数字然后进行比较。

[]转换为数字为 0。

![] 首先是转换为布尔值,由于[]作为一个引用类型转换为布尔值为 true,

因此![]为 false,进而在转换成数字,变为 0。

0 == 0 , 结果为 true

  • 字符串和数字之间的相等比较,将字符串转换为数字之后再进行比较。
  • 其他类型和布尔类型之间的相等比较,先将布尔值转换为数字后,再应用其他规则进行比较。
  • null 和 undefined 之间的相等比较,结果为真。其他值和它们进行比较都返回假值。
  • 对象和非对象之间的相等比较,对象先调用 ToPrimitive 抽象操作后,再进行比较。
  • 如果一个操作值为 NaN ,则相等比较返回 false( NaN 本身也不等于 NaN )。
  • 如果两个操作值都是对象,则比较它们是不是指向同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回 true,否则,返回 false
var a = {
  value: 0,
  valueOf: function() {
    this.value++;
    return this.value;
  },
};
console.log(a == 1 && a == 2); //true

其他

关于 typeof null = ‘Object’

小黄书中对 typeof null = ‘object’中的描述

你可能注意到 null 类型不在此列。它比较特殊, typeof 对它的处理有问题:
typeof null === "object"; // true
正确的返回结果应该是 "null" ,但这个 bug 由来已久,在 JavaScript 中已经存在了将近
二十年,也许永远也不会修复,因为这牵涉到太多的 Web 系统,“修复”它会产生更多的
bug,令许多系统无法正常工作

还有一段:

注意,简单基本类型( stringbooleannumbernullundefined )本身并不是对象。
null 有时会被当作一种对象类型,但是这其实只是语言本身的一个 bug,即对 null 执行
typeof null 时会返回字符串 "object"1 实际上, null 本身是基本类型。

有一种常见的错误说法是“JavaScript 中万物皆是对象”,这显然是错误的。

实际上,JavaScript 中有许多特殊的对象子类型,我们可以称之为复杂基本类型。
函数就是对象的一个子类型(从技术角度来说就是“可调用的对象”)。JavaScript 中的函
数是“一等公民”,因为它们本质上和普通的对象一样(只是可以调用),所以可以像操作
其他对象一样操作函数(比如当作另一个函数的参数)。

数组也是对象的一种类型,具备一些额外的行为。数组中内容的组织方式比一般的对象要
稍微复杂一些。

Object.is 与 === 的区别

Object 在严格等于的基础上修复了一些特殊情况下的失误,具体来说就是+0 和-0,NaN 和 NaN

例子:

Object.is("foo", "foo"); // true
Object.is(window, window); // true

Object.is("foo", "bar"); // false
Object.is([], []); // false

var foo = { a: 1 };
var bar = { a: 1 };
Object.is(foo, foo); // true
Object.is(foo, bar); // false

Object.is(null, null); // true

// 特例
Object.is(0, -0); // false
Object.is(0, +0); // true
Object.is(-0, -0); // true
Object.is(NaN, 0 / 0); // true
if (!Object.is) {
  Object.is = function(x, y) {
    // SameValue algorithm
    if (x === y) {
      //运行到1/x === 1/y的时候x和y都为0,但是1/+0 = +Infinity, 1/-0 = -Infinity, 是不一样的
      return x !== 0 || 1 / x === 1 / y;
    } else {
      //NaN===NaN是false,这是不对的,我们在这里做一个拦截,x !== x,那么一定是 NaN, y 同理
      //两个都是NaN的时候返回true
      return x !== x && y !== y;
    }
  };
}

代码和例子来源于 MDN

‘1’.toString()为什么可以调用?

其实在这个语句运行的过程中做了这样几件事情:

var s = new Object("1");
s.toString();
s = null;
  • 第一步: 创建 Object 类实例。注意为什么不是 String ? 由于 Symbol 和 BigInt 的出现,对它们调用 new 都会报错,目前 ES6 规范也不建议用 new 来创建基本类型的包装类。
  • 第二步: 调用实例方法。
  • 第三步: 执行完方法立即销毁这个实例。

整个过程体现了基本包装类型的性质,而基本包装类型恰恰属于基本数据类型,包括 Boolean, Number 和 String。

0.1+0.2 为什么不等于 0.3?

js 精度不能精确到 0.1,计算过程中有三个会造成精度丢失的地方

第一个:当计算机计算 0.1+0.2 的时候,实际上计算的是这两个数字在计算机里所存储的二进制,0.1 和 0.2 在转换为二进制表示的时候会出现位数无限循环的情况。js 中是以 64 位双精度格式来存储数字的,只有 53 位的有效数字,超过这个长度的位数会被截取掉这样就造成了精度丢失的问题

第二个:在对两个以 64 位双精度格式的数据进行计算的时候,首先会进行对阶的处理,对阶指的是将阶码对齐,也就是将小数点的位置对齐后,再进行计算,一般是小阶向大阶对齐,因此小阶的数在对齐的过程中,有效数字会向右移动,移动后超过有效位数的位会被截取掉

第三个:当两个数据阶码对齐后,进行相加运算后,得到的结果可能会超过 53 位有效数字,因此超过的位数也会被截取掉

如何解决

方法一:我们可以将其转换为整数后再进行运算,运算后再转换为对应的小数

方法二:将两个数相加的结果和右边相减,如果相减的结果小于一个极小数,那么我们就可以认定结果是相等的,这个极小数可以使用 es6 的 Number.EPSILON

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

x = 0.2;
y = 0.3;
z = 0.1;
equal = Math.abs(x + z - y) < Number.EPSILON; //true

Polyfill

if (Number.EPSILON === undefined) {
  Number.EPSILON = Math.pow(2, -52);
}

理解 BigInt

BigInt 是一种新的数据类型,用于当整数值大于 Number 数据类型支持的范围时。这种数据类型允许我们安全地对大整数执行算术操作,表示高分辨率的时间戳,使用大整数 id,等等,而不需要使用库

在 JS 中,所有的数字都以双精度 64 位浮点格式表示,那这会带来什么问题呢?

这导致 JS 中的 Number 无法精确表示非常大的整数,它会将非常大的整数四舍五入,确切地说,JS 中的 Number 类型只能安全地表示-9007199254740991(-(2^53-1))和 9007199254740991((2^53-1)),任何超出此范围的整数值都可能失去精度,同时也会有一定的安全性问题:

console.log(999999999999999); //=>10000000000000000
9007199254740992 === 9007199254740993; // → true 居然是true!

这有两种方式使用 BigInt

方式一:要创建 BigInt,只需要在数字末尾追加 n 即可

console.log(9007199254740995n); // → 9007199254740995n
console.log(9007199254740995); // → 9007199254740996

方式二:用 BigInt()构造函数

BigInt("9007199254740995"); // → 9007199254740995n

例子:

10n + 20n; // → 30n
10n - 20n; // → -10n
+10n; // → TypeError: Cannot convert a BigInt value to a number
-10n; // → -10n
10n * 20n; // → 200n
20n / 10n; // → 2n
23n % 10n; // → 3n
10n ** 3n; // → 1000n

const x = 10n;
++x; // → 11n
--x; // → 9n
console.log(typeof x); //"bigint"

BigInt 不支持一元加号运算符, 这可能是某些程序可能依赖于 + 始终生成 Number 的不变量,或者抛出异常。另外,更改 + 的行为也会破坏 asm.js 代码。

asm.js是由Mozilla提出的一个基于JS的语法标准,主要是为了解决JS引擎的执行效率问题,尤其是使用Emscripten从C/C++语言编译成JS的程序的效率,目前只有Mozilla的Firefox Nightly中支持

因为隐式类型转换可能丢失信息,所以不允许在 bigint 和 Number 之间进行混合操作。当混合使用大整数和浮点数时,结果值可能无法由 BigInt 或 Number 精确表示

10 + 10n; // → TypeError

不能将 BigInt 传递给 Web api 和内置的 JS 函数,这些函数需要一个 Number 类型的数字。尝试这样做会报 TypeError 错误

Math.max(2n, 4n, 6n); // → TypeError

当 Boolean 类型与 BigInt 类型相遇时,BigInt 的处理方式与 Number 类似,换句话说,只要不是 0n,BigInt 就被视为 truthy 的值

if (0n) {
  //条件判断为false
}
if (3n) {
  //条件为true
}

元素都为 BigInt 的数组可以进行 sort

BigInt 可以正常地进行位运算,如|、&、<<、>>和^

最后:兼容性并不怎么好,只有 chrome67、firefox、Opera 这些主流实现,要正式成为规范,其实还有很长的路要走

原文链接:https://juejin.cn/post/7337898389450735666 作者:跌倒的小黄瓜

(0)
上一篇 2024年2月22日 上午10:36
下一篇 2024年2月22日 上午10:47

相关推荐

发表回复

登录后才能评论