zl程序教程

您现在的位置是:首页 >  后端

当前栏目

深入了解JS 数据类型

JS 深入 了解 数据类型
2023-06-13 09:13:56 时间

深入了解JS 数据类型

由于JavaScript 是弱类型语言,而且JavaScript 声明变量的时候并没有预先确定的类型,变量的类型就是其值的类型,也就是说「变量当前的类型由其值所决定」,夸张点说上一秒是String,下一秒可能就是个Number类型了,这个过程可能就进行了某些操作发生了强制类型转换。虽然弱类型的这种「不需要预先确定类型」的特性给我们带来了便利,同时也会给我们带来困扰,为了能充分利用该特性就必须掌握类型转换的原理。本文我们将深入了解JavaScript 的类型机制。

JS 类型分类

JS内置数据类型有 8 种类型,分别是:undefinedNullBooleanNumberStringBigIntSymbolObject

其中又可分为「基础类型」「引用类型」

  • 「基础类型」undefinedNullBooleanNumberStringBigIntSymbol
  • 「引用类型」:统称为 Object 类型。细分的话,有:Object 类型、Array 类型、Date 类型、RegExp 类型、Function 类型等。

依据「存储方式」不同,数据类型大致可以分成两类:

  • 「基础类型」存储在「栈内存」,被引用或拷贝时,会创建一个完全相等的变量。
  • 「引用类型」存储在「堆内存」,在「栈内存」存储的是地址,多个引用指向同一个内存地址。

可以通过以下栗子加深理解:

const obj1 = {
  name: 'obj1',
  id: '123'
}

const obj2 = obj1;
console.log(obj1.name); // obj1

obj2.name = 'obj2';
console.log(obj1.name); // obj2
console.log(obj2.name); // obj2

obj2name被修改后,obj1name也随之改变,这里就体现了引用类型的“共享”的特性,即这两个值都存在同一块内存中共享,一个发生了改变,另外一个也随之跟着变化。

JS 类型转换

ToPrimitive

stringnumberbooleannull undefined 这五种类型统称为「原始类型」Primitive),表示不能再细分下去的基本类型。

ToPrimitive对原始类型不发生转换处理,只「针对引用类型(object)的」,其目的是将引用类型(object)转换为非对象类型,也就是原始类型。

toPrimitive(obj: any, preferedType?: 'string' |'number')

ToPrimitive 运算符「接受一个值,和一个可选的期望类型作参数」ToPrimitive 运算符将值转换为非对象类型,如果对象有能力被转换为不止一种原语类型,可以使用可选的 「期望类型」 来暗示那个类型。

它内部方法,将任意值转换成原始值,转换规则如下:

  • preferedTypestring:
  1. 先调用objtoString方法,如果为原始值,则return,否则进行第2步
  2. 调用objvalueOf方法,如果为原始值,则return,否则进行第3步
  3. 抛出TypeError 异常
  • preferedTypenumber:
  1. 先调用objvalueOf方法,如果为原始值,则return,否则进行第2步
  2. 调用objtoString方法,如果为原始值,则return,否则第3步
  3. 抛出TypeError 异常
  • preferedType参数为空
  1. 该对象为Date,则type被设置为String
  2. 否则,type被设置为Number

接着,我们看下各个对象的转换实现:

「对象」

「valueOf()」

toString()

「默认 preferedType」

Object

原值

"[object Object]"

Number

Function

原值

"function func() {...}" or "() => {...}"

Number

Array

原值

"a, b, c,..."

Number

Date

数字

例如:"Thu Nov 11 2021 19:49:37 GMT+0800 (中国标准时间)"

String

  1. 数组的toString()可以等效为join(","),遇到null, undefined都被忽略,遇到symbol直接报错,遇到无法ToPrimitive的对象也报错。
  2. 使用模板字符串或者使用String()包装时,preferedType=string,即优先调用 .toString()

例如:

[1, null, undefined, 2].toString() // '1,,,2'

// Uncaught TypeError: Cannot convert a Symbol value to a string
[1, Symbol('x')].toString()

// Uncaught TypeError: Cannot convert object to primitive value
[1, Object.create(null)].toString()

toString

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

每个对象都有一个 toString() 方法,当对象被表示为「文本值」时或者当以期望「字符串」的方式引用对象时,该方法被自动调用。

「【注】」toString()valueOf() 在特定的场合下会自行调用。

valueOf

Object.prototype.valueOf()方法返回指定对象的原始值。

JavaScript 调用 valueOf() 方法用来把对象转换成原始类型的值(数值、字符串和布尔值)。但是我们很少需要自己调用此函数,valueOf 方法一般都会被 JavaScript 自动调用。

不同内置对象的valueOf实现:

  • String => 返回字符串值
  • Number => 返回数字值
  • Date => 返回一个数字,即时间值
  • Boolean => 返回Boolean的this值
  • Object => 返回this

下面来看几个栗子:

const Str = new String('123');
console.log(Str.valueOf());//123

const Num = new Number(123);
console.log(Num.valueOf());//123

const Date = new Date();
console.log(Date.valueOf()); //1637131242574

const Bool = new Boolean('123');
console.log(Bool.valueOf());//true

var Obj = new Object({valueOf:()=>{
    return 1
}})
console.log(Obj.valueOf());//1

Number

Number运算符转换规则:

  • null 转换为 0
  • undefined 转换为 NaN
  • true 转换为 1,false 转换为 0
  • 字符串转换时遵循数字常量规则,转换失败返回NaN

**【注】**对象这里要先转换为原始值,调用ToPrimitive转换,type指定为number了,继续回到ToPrimitive进行转换。

接下来看几个栗子:

Number("0") // 0
Number("") // 0
Number("   ") // 0
Number("\n") // 0
Number("\t") // 0
Number(null) // 0
Number(false) // 0

Number(true) // 1

Number(undefined); // NaN
Number("x"); // NaN
Number({}); // NaN

String

String 运算符转换规则

  • null 转换为 'null'
  • undefined 转换为 undefined
  • true 转换为 'true'false 转换为 'false'
  • 数字转换遵循通用规则,极大极小的数字使用指数形式

**【注】**对象这里要先转换为原始值,调用ToPrimitive转换,type就指定为string了,继续回到ToPrimitive进行转换。

接下来看几个栗子:

String(null)                 // 'null'
String(undefined)            // 'undefined'
String(true)                 // 'true'
String(1)                    // '1'
String(-1)                   // '-1'
String(0)                    // '0'
String(-0)                   // '0'
String(Math.pow(1000,10))    // '1e+30'
String(Infinity)             // 'Infinity'
String(-Infinity)            // '-Infinity'
String({})                   // '[object Object]'
String([1,[2,3]])            // '1,2,3'
String(['koala',1])          //koala,1

Boolean

ToBoolean 运算符转换规则

除了下述 6 个值转换结果为 false,其他全部为true

  1. undefined
  2. null
  3. -0
  4. 0+0
  5. NaN
  6. ''(空字符串)

假值以外的值都是真值。其中包括所有对象(包括空对象)的转换结果都是true,甚至连false对应的布尔对象new Boolean(false)也是true

接下来看几个栗子:

Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false

Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // true

什么时候转 string

字符串的自动转换,主要发生在字符串的「加法运算」时。当一个值为字符串,另一个值为非字符串,则后者转为字符串。

遇到对象先执行ToPrimitive转换为基本类型,然后按照基本类型的规则处理

// {}.toString() === "[object Object]"
1 + {} === "1[object Object]"

// [2, 3].toString() === "2,3"
1 + [2, 3] === "12,3"
[1] + [2, 3] === "1,2,3"

function test() {}
// test.toString() === "function test() {}"
10 + test === "10function test() {}"

加法过程中,遇到字符串,则会被处理为「字符串拼接」

上面的对象最后也都转成了字符串,遵循本条规则。接着来几个纯字符串的例子:

1 + "1" === "11"
1 + 1 === 2
1 + 1 + "1" === "21"
"1" + 1 === "11"
"1" + "1" === "11"
1 + "1" + 1 === "111"

对象字面量{}在最前面则不代表对象

不是对象是什么?我们看看下面这个栗子:

// [].toString() === "";
// {}.toString() === "[object Object]";
[] + {} === "[object Object]";

// { // empty block } + [] => [].toString() => "" => Number("") => 0
{} + [] === 0;

{ a: 2 } + [] === 0;

先说 [] + {} 。一个数组加一个对象。加法会进行隐式类型转换,规则是调用其 valueOf()toString() 以取得一个非对象的值(primitive value)。如果两个值中的任何一个是字符串,则进行字符串串接,否则进行数字加法。[]{}valueOf() 都返回对象自身,所以都会调用 toString(),最后的结果是字符串串接。[].toString() 返回空字符串,({}).toString() 返回"[object Object]"。最后的结果就是"[object Object]"

然后说 {} + []。看上去应该和上面一样。但是 {} 除了表示一个对象之外,也可以表示一个空的 block。在 [] + {} 中,[] 被解析为数组,因此后续的+被解析为加法运算符,而 {}就解析为对象。但在{} + []中,{} 被解析为空的 block,随后的 +被解析为正号运算符。即实际上成了:{ // empty block } + []即对一个空数组执行正号运算,实际上就是把数组转型为数字。首先调用 [].valueOf() 。返回数组自身,不是primitive value,因此继续调用[].toString() ,返回空字符串。空字符串转型为数字,返回0,即最后的结果。

「【注】」{}+[] 如果被parsestatement的话,{}会被parse成空的block,但是在需要被parseexpression的话,就会被parse成空的Object。所以{}+[]console.log({}+[])的输出结果还不一样,因为参数列表只接受expression

什么时候转 Number

  • 加法操作时,遇到非字符串的基本类型,都会转Number「除了加法运算符,其他运算符都会把运算自动转成数值。」
1 + true === 2
1 + false === 1
1 + null === 1
1 + undefined // NaN

减法操作时,一律需要把类型转换为Number,进行数学运算

3 - 1 === 2
3 - '1' === 2
'3' - 1 === 2
'3' - '1' - '2' === 0

// [].toString() => "" => Number(...) => 0
3 - [] === 3

// {}.toString() => "[object Object]" => Number(...) => NaN
3 - {} // NaN

+x 和 一元运算 + x 是等效的(以及- x),都会强制转换成Number

+ 0 === 0
- 0 === -0
1 + + "1" === 2
1 + + + + ["1"] === 2
// 负负得正
1 + - + - [1] === 2
// 负负得正
1 - + - + 1 === 2
1 - + - + - 1 === 0

1 + + [""] === 1

// ["1", "2"].toString() => "1,2" => Number(...) => NaN
1 + + ["1", "2"] // NaN

// 多出来的 + 是一元操作符,操作数是后面那个 undefined,Number(undefined) => NaN
("ba" + + undefined + "a").toLowerCase() === "banana"
  • 在宽松的==的比较中,Number优先于String,下面以x == y为例:
  1. 如果x,y均为number,直接比较
  2. 如果存在对象,ToPrimitive()type为number进行转换,再进行后面比较
  3. 存在boolean,按照ToNumberboolean转换为1或者0,再进行后面比较
  4. 如果xstringynumberx转成number进行比较

什么时候转 Boolean

  • 布尔比较时
  • if(obj) , while(obj)等判断时或者 「三元运算符」只能够包含布尔值
// 条件部分的每个值都相当于false,使用否定运算符后,就变成了true
if ( !undefined && !null && !0 && !NaN && !'' ) {
  console.log('true');
} // true

//下面两种情况也会转成布尔类型
expression ? true : false
!! expression

宽松相等 ==

相等于、全等都需要对类型进行判断,当类型不一致时,宽松相等会触发隐式转换。下面介绍规则:

对象与对象类型一致,不做转换

{} != {}
[] != {}
[] != []

对象与基本类型,对象先执行ToPrimitive转换为基本类型

// 小心代码块
"[object Object]" == {}
[] == ""
[1] == "1"
[1,2] == "1,2"

数字与字符串类型对比时,字符串总是转换成数字

"2" == 2
[] == 0
[1] == 1
// [1,2].toString() => "1,2" => Number(...) => NaN
[1,2] != 1

布尔值先转换成数字,再按数字规则操作

// [] => "" => Number(...) => 0
// false => 0
[] == false

// [1] => "1" => 1
// true => 1
[1] == true

// [1,2] => "1,2" => NaN
// true => 1
[1,2] != true

"0" == false
"" == false

nullundefinedsymbol

nullundefined与任何非自身的值对比结果都是false,但是null == undefined 是一个特例。

null == null
undefined == undefined
null == undefined

null != 0
null != false

undefined != 0
undefined != false

Symbol('x') != Symbol('x')

对比 < >

对比不像相等,可以严格相等(===)防止类型转换,对比一定会存在隐式类型转换。

对象总是先执行ToPrimitive为基本类型

[] < [] // false
[] <= {} // true

{} < {} // false
{} <= {} // true

任何一边出现「非字符串」的值,则一律转换成「数字」做对比

// ["06"] => "06" => 6
["06"] < 2   // false 

["06"] < "2" // true
["06"] > 2   // true

5 > null     // true
-1 < null    // true
0 <= null    // true

0 <= false   // true
0 < false    // false

// undefined => Number(...) => NaN
5 > undefined // false

JS 数据类型判断

typeof

typeof操作符可以区分「基本类型」「函数」「对象」

判断结果: 'string''number''boolean''undefined''function''symbol''bigInt''object'

console.log(typeof null) // object
console.log(typeof undefined) // undefined
console.log(typeof 1) // number
console.log(typeof 1.2) // number
console.log(typeof "hello") // string
console.log(typeof true) // boolean
console.log(typeof Symbol()) // symbol
console.log(typeof (() => {})) // function
console.log(typeof {}) // object
console.log(typeof []) // object
console.log(typeof /abc/) // object
console.log(typeof new Date()) // object

缺点:

  1. typeof有个明显的bug就是typeof nullobject;
  2. typeof无法区分各种内置的对象,如Array, Date等。

接下来讲简单介绍一下原理:

JS是动态类型的变量,每个变量在存储时除了存储变量值外,还需要存储变量的类型。JS里使用32位(bit)存储变量信息。低位的1~3个bit存储变量类型信息,叫做类型标签(type tag)

.... XXXX X000 // object
.... XXXX XXX1 // int
.... XXXX X010 // double
.... XXXX X100 // string
.... XXXX X110 // boolean
  1. 只有int类型的type tag使用1个bit,并且取值为1,其他都是3个bit, 并且低位为0。这样可以通过type tag低位取值判断是否为int数据;
  2. 为了区分int,还剩下2个bit,相当于使用2个bit区分这四个类型:object, double, string, boolean
  3. 但是nullundefinedFunction并没有分配type tag

「如何识别Function

函数并没有单独的type tag,因为函数也是对象。typeof内部判断如果一个对象实现了[[call]]内部方法则认为是函数。

「如何识别undefined

undefined变量存储的是个特殊值JSVAL_VOID(0-2^30),typeof内部判断如果一个变量存储的是这个特殊值,则认为是undefined

    #define JSVAL_VOID              INT_TO_JSVAL(0 - JSVAL_INT_POW2(30))

「如何识别null

null变量存储的也是个特殊值JSVAL_NULL,并且恰巧取值是空指针机器码(0),正好低位bit的值跟对象的type tag是一样的,这也导致著名的bug:

typeof null // object

有很多方法可以判断一个变量是一个非null的对象,例如:

// 利用Object函数的装箱功能
function isObject(obj) {
    return Object(obj) === obj;
}

isObject({}) // true
isObject(null) // false

instanceof

语法:A instanceof B , 即判断A是否为B类型的实例,也可以理解为Bprototype是否在A的原型链上

Object.create({}) instanceof Object // true
Object.create(null) instanceof Object // false

Function instanceof Object // true
Function instanceof Function // true
Object instanceof Object // true

[] instanceof Array // true
{a: 1} instanceof Object // true
new Date() instanceof Date // true

// 对于基本类型,使用字面量声明的方式可以正确判断类型
new String('dafdsf') instanceof String // true
'xiaan' instanceof String // false, 原型链不存在

作为类型判断的一种方式,instanceof 操作符不会对变量object进行隐式类型转换:

"" instanceof String; // false,基本类型不会转成对象
new String('') instanceof String; // true

对于没有原型的对象或则基本类型直接返回false

1 instanceof Object // false
Object.create(null) instanceof Object // false

B必须是个对象。并且大部分情况要求是个构造函数(即要具有prototype属性)

// TypeError: Right-hand side of 'instanceof' is not an object
1 instanceof 1

// TypeError: Right-hand side of 'instanceof' is not callable
1 instanceof ({})

// TypeError: Function has non-object prototype 'undefined' in instanceof check
({}) instanceof (() => {})

「原理:」

// 自定义 instanceof
function myInstanceof(obj, objType) {
  // 首先用typeof来判断基础数据类型,如果是,直接返回false
  if(typeof obj !== 'object' || obj === null) return false;
 
  // getProtypeOf是Object对象自带的API,能够拿到参数的原型对象
  let proto = Object.getPrototypeOf(obj);

  while(true) {  //循环往下寻找,直到找到相同的原型对象
    if(proto === null) return false;
    if(proto === objType.prototype) return true;//找到相同原型对象,返回true
    proto = Object.getPrototypeof(proto);
  }
}

// 验证一下自己实现的myInstanceof是否OK
console.log(myInstanceof(new Array('2','3'), Array));    // true
console.log(myInstanceof(123, Number));                // false
console.log(myInstanceof(new Number(123), Number));   //true   

Object.prototype.toString

对于 Object.prototype.toString() 方法,会返回一个形如 "[object XXX]" 的字符串

Object.prototype.toString.call(null)   //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(1)    // "[object Number]"
Object.prototype.toString.call('Miss U')  // “[object String]"
Object.prototype.toString.call(true)  // "[object Boolean]"
Object.prototype.toString({})       // "[object Object]"
Object.prototype.toString.call({})  // "[object Object]"
Object.prototype.toString.call(function(){})  // ”[object Function]"
Object.prototype.toString.call([])       //"[object Array]"
Object.prototype.toString.call(/123/g)    //"[object RegExp]"
Object.prototype.toString.call(new Date()) //"[object Date]"
Object.prototype.toString.call(document)  //[object HTMLDocument]"
Object.prototype.toString.call(window)   //"[object Window]"
  • 如果实参是个基本类型,会自动转成对应的引用类型;
  • Object.prototype.toString不能区分基本类型的,只是用于区分各种对象;
  • nullundefined不存在对应的引用类型,内部特殊处理了;

「原理:」

每个对象都有个内部属性[[Class]],内置对象的[[Class]]的值都是不同的("Arguments", "Array", "Boolean", "Date", "Error", "Function", "JSON", "Math", "Number", "Object", "RegExp", "String"),并且目前[[Class]]属性值只能通过Object.prototype.toString访问。而Object.prototype.toString内部先访问对象的Symbol.toStringTag属性值拼接返回值的。

Object.prototype.toString的内部逻辑:

  1. 如果实参是undefined, 则返回"[object Undefined]"
  2. 如果实参是null, 则返回"[object Null]"
  3. 把实参转成对象
  4. 获取对象的Symbol.toStringTag属性值subType
    • 如果subType是个字符串,则返回[object subType]
    • 否则获取对象的[[Class]]属性值type,并返回[object type]

最后,我们可以封装一个通用的类型检测方法:

function getPrototype(obj){
  let type  = typeof obj;
  if (type !== "object") {    // 先进行typeof判断,如果是基础数据类型,直接返回
    console.log(obj,':',res)
    return type;
  }
  // 对于typeof返回结果是object的,再进行如下的判断,正则返回结果
  const res = Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1')
  console.log(obj,'=',res);
  return res;
}
getPrototype([])     // "Array" typeof []是object,因此toString返回
getPrototype('abc')  // "string" typeof 直接返回
getPrototype(window) // "Window" toString返回
getPrototype(null)   // "Null"首字母大写,typeof null是object,需toString来判断
getPrototype(undefined)   // "undefined" typeof 直接返回
getPrototype()            // "undefined" typeof 直接返回
getPrototype(function(){}) // "function" typeof能判断,因此首字母小写
getPrototype(/123/g)      //"RegExp" toString返回