0001JS数据类型

258 阅读10分钟

JS数据类型


引子: JS有几种数据类型?每种数据类型有什么用?为什么栈和堆变量的表现不同?V8如何标记每种类型?null和对象的标记是什么?undefined值在目前是否可重写?String的最大长度是什么?Boolean为false的数值有哪些?为什么0.1+0.2 !== 0.3? 为什么Symbol、BigInt不能new? 为什么String、Number、Boolean可以new ---> new原理是什么,如何检测是否用了new? 对象的属性分几类?对象的属性可以是什么类型?


1. 分类 [2类 共8种]

基本数据类型:Undefined, Null, Boolean, String, Number, Symbol, BigInt 引用数据类型:Object

Object包含 普通对象{}、数组对象[]、函数对象Fucntion、正则对象RegExp、日期对象Date、数学对象Math

2. 区别 [存储、拷贝、赋值]

(1)存储区域不同: 基本数据类型存储在栈内存中,引用数据类型存储在堆内存中。

(2)拷贝表现不同: 基本数据类型是值传递,即在拷贝时,会创建一个完全相等的变量,在栈中重新开辟一块内存空间存储原变量的副本。 引用数据类型是引用传递,即在拷贝时,会创建一个指针指向原有变量,在栈中重新开辟一块内存空间存储指针以指向原变量。

// 函数形参是值传递,拷贝指针的副本,外在表现为引用传递。
let Tom = {name: 'Tom'};
const changeName = (people) => {pelple.name = '阿吉';}
changePeople(Tom); // 期望 Tom 不发生变化,实际 Tom = {name:'阿吉'},Tom变成了阿吉
      栈              堆
------ -------    ------- ------------- 
| Tom | 0x0001|   |0x0001|{name: 'Tom'}|
------ ------     ------ ------------- 
|people|0x0001|   |      |             | 
------ -------    ------- ------------- 
通过people指针把0x0001内存的name改为阿吉,函数执行完毕people指针被销毁

(3)赋值表现不同: 基本数据类型不可变。 引用数据类型可变。

let a = 'a'; // 'a'内存被销毁,将'b'内存赋值给a
a = 'b'; // 栈中新开辟一块内存空间,赋值为'b'
let obj = {name: 'Tom'};
obj = {name: '阿吉'};
      栈              堆
------ -------    ------- ------------- 
| 旧a |  'a'  |   |0x0001|{name: 'Tom'}|
------ ------     ------ ------------- 
| 新a |  'b'  |   |0x0002|{name: '阿吉'}| 
------ --------   ------- ------------- 
| obj | from  |   |      |             | 
|     | 0x0001|   |      |             | 
|     | to    |   |      |             | 
|     | 0x0002|   |      |             | 
------ --------   ------- ------------- 

3. 深入数据类型

3-0 前言

在 JavaScript 中,数据类型在V8底层都是以二进制形式表示的,二进制的前三位为 0 会被 typeof 判定为对象类型。 在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。 标签如下:

  • 0 - 对象,数据是对象的引用
  • 1 - Number整型,数据是31位带符号整数
  • 010 - Number双精度类型,数据是双精度数字
  • 100 - String类型,数据是字符串
  • 110 - Boolean类型,数据是布尔值
  • 全0 - Null类型,数据是null

3-1 Undefined

(1)定义:表示未定义,即 no value (2)值:undefined (3)不是关键字,是变量。在IE8中,全局的undefined变量可以被重新赋值。在2009年ES5中,被修复。

1.英:The value of undefined is undefined (see 8.1). This property has the attributes { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: false }. 2.中:undefined 的值是 undefined。这个属性有不可写,不可枚举,不可配置的特性。

(4)作用:任何变量在赋值前是 Undefined 类型、值为 undefined。是最原始的状态值,而非人为操作。 (5)V8表示:−2^30 (6)赋值undefined:window.undefined、使用void 0、执行无返回值的函数、引用未定义的对象属性、引用已声明但未初始化的变量;

避免使用 void 0 判断某变量是不是undefined,因其不是自解释的。 解决:可以封装为函数使用。代码健壮性提高,避免了未定义错误。例如 if(value === undefine){}

/**
 * @file isUndefined
 * @author 阿吉
 * @desc 判断一个变量是不是undefined
 * @params value
 * @return {Boolean}
*/
function isUndefined(value){
    //获得undefined,保证它没有被重新赋值
    var undefined = void(0);
    return value === undefined;
}

3-2 Null

(1)定义:表示空值,即 no object value, 代表的是空指针。 (2)值:null (3)是关键字:故可以使用 null 关键字来获取 null 值。若将null作为变量会报错。 (4)作用:用于重置为空对象,而非一个变量最原始的状态。在内存中,栈中变量无指向堆中的内存对象,会被GC回收。 (5)赋值null:直接赋值null (6)V8表示: 全0 ---> 故 typeof null == 'object' 为 true。 判断其类型的解决方案为Object.prototype.toString.call(null) ; // [object Null]

// null只非严格相等于undefined,剩下的他俩和谁都不等。
null == undefined // true
null === undefined // false

3-3 Boolean

(1)定义:表示逻辑意义上的真和假。 (2)值:true, false (3)不是关键字 (4)作用:表示真假 (5)可被转化为false:undefined, null, NaN, +0, -0, '', false, document.all()

JS中所有对象都被当做true,包括{}、[]。 if语句中进行类型转换 (6)包装类(装箱转换):new Boolean()为对象类型。 Boolean()是强制类型转换 (7)V8表示: 110开头

3-4 String

(1)定义:字符以 Unicode 的方式表示,采取 UTF16 编码方式,形成的序列。故一个Unicode码点表示一个字符。

码点 科普: 定义:从0开始,为每个符号指定一个编号,即 "码点"(code point)。 例子:码点0(所有二进制位都是0)的符号就是null,即 U+0000 = null 表示:U+??? ???即 "码点",准确说是十六进制的码点值 (2)值: 其值最大长度是 2^53 - 1。【这里的单位没找到合适的描述】 Unicode 科普: 编号规则:2^16(65536)个号码组成一个平面(plain) 设计原因:性能,实现简单 空间大小:2^21 目前总共有17个平面 分类:1. 一个基本平面BMP:U+0000 - U+FFFF 2. 16个辅助平面SMP:U+010000 - U+10FFFF 注意:处理SMP时要格外小心 (3)不是关键字 (4)作用:用于表示文本数据 (5)特点:不可变。 (6)V8表示: 100开头 (7)包装类(装箱转换):new String()为对象类型 String()是强制类型转换 (8)常考题:千位分隔符 description: 每隔三位数添加分隔符 answer: 从右向左遍历。数值变数组,for循环索引找到对应字符。

3-5 Number

(1)定义:采用 IEEE754 标准中的 “双精度浮点数” 来表示一个数字,不区分整数和浮点数 。

在 IEEE754 中,双精度浮点数采用 64 位存储,即 8 个字节表示一个浮点数: 1位符号位,11位指数位,52位小数位。 (2)值:大小为2^1024 ~ 2^-1023 (MAX_VALUE 到 MIN_VALUE,或-0x1fffffffffffff 至 0x1fffffffffffff),总共有 2^64 - 2^53 + 3 (即18437736874454810627) 个值 NaN:占用了 2^53 即 9007199254740990。用于指示某个值不是数字。 Infinity: 正无穷大 -Infinity: 负无穷大 JS中有 +0 和 -0 的区分。除法运算小心 -0 。Object.is()与 === 基本一致,但+0不等于-0、NaN等于自身。 (3)不是关键字 (4)作用:运算 (5)V8表示: 010开头 (6)包装类(装箱转换):new Number()为对象类型 Number()是强制类型转换 (7)常见问题:精度丢失

// 非整数使用==或===比较时,转化成浮点数(用二进制表示)会导致精度丢失,因其小数位是无限等,用64位表示一定会丢失微小值。
console.log( 0.1 + 0.2 == 0.3); // false
// 错误原因:思路不对,采用比较方案不准确
// 解决方案:使用 JavaScript 提供的最小精度值,检查等式左右两边差的绝对值是否小于最小精度。
console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON); // true
// 故此 方案(取差值绝对值,最小精度判等) 可用于浮点数判等。

// 其他思路:1.先变为整数,计算,再变为小数。 适用于小数位不多。
// 其他思路:2.舍弃末尾小数位->toPrecision(n)保留n位小数
console.log(parseFloat(0.1 + 0.2).toPrecision(12) === 0.3); // true

3-6 Symbol

(1)定义:一切非字符串的对象 key 的集合 (2)值:接受String类型的描述, 形如 Symbol(description),description即String类型 (3)不是关键字 (4)作用:表示全局惟一的常量,即使描述相同,Symbol也不相等。

自ES6起,对象系统被适用Symbol重写了。 (5)类型构造函数:Symbol()是Symbol对象的构造器,不能与new搭配 (6)适用场景:常量值,对象属性

// 常量值 如果都是 'T', 则只会返回 '腾讯'
const COMPANYS = {
      Tencent: Symbol('T'),
      ByteDance: Symbol('T')
};
const getOffer = companyName => {
      switch(companyName){
            case COMPANYS.Tencent:
                  return '腾讯';
            case COMPANYS.ByteDance:
                  return '字节跳动'
      }
};
let offer = getOffer(COMPANYS.ByteDance); // 字节跳动
// 对象属性 手撕call、apply时,防止添加的临时属性已经在原对象中存在
function myCall(context){
      let context = context || window;
      const fn = Symbol('fn');
      context[fn] = this;
      let args = [];
      for(var i = 1; i < arguments.length; i++){
            args.push('arguments['+i+']');
      }
      let result = eval('context.fn('+args+')');
      delete context.fn;
      return result;
}

(7)V8表示:TODO (8)如何检测使用了new调用?为什么Symbol不能new? new时相当于把this绑定在构造函数上。所以可以,在构造函数中,通过 this instanceof FnName 判断是否进行了new调用,或是直接调用了函数。如下:

function mySymbol(){
      if(this instanceof mySymbol){
            throw new Error('Uncaught TypeError: mySymbol is not a constructor');
      }
}

Symbol不是构造函数,所以不能new。

Symbol('1');
new Symbol(1);
// Uncaught TypeError: Symbol is not a constructor

3-7 BigInt

(1)定义: BigInt is a new primitive that provides a way to represent whole numbers larger than 2^53, which is the largest number Javascript can reliably represent with the Number primitive. (2)值:正常数字后面加n即可。如 1n 或 BigInt(1)。 (3)不是关键字 (4)特性:

  1. BigInt不能与Number混用进行运算
console.log(1n === 1); // false
console.log(typeof 1n); // bigint
console.log(typeof 1n); // number

console.log(1n + 1);
// TypeError: Cannot mix BigInt and other types, use explicit conversions
  1. BigInt除法运算会自动 向下取整
console.log(25n / 10n); // 2n
console.log(25 / 10); // 2.5
  1. 不允许使用一元运算符+
console.log(+1n);
// TypeError: Cannot convert a BigInt value to a number

(5)作用:提供大整数运算支持,不会有精度丢失的风险。 (6)类型构造函数:BigInt() (7)V8表示: TODO

3-8 Object

(1)定义:一组属性的集合。或者更标准的说,在计算机科学中, 对象是指内存中的可以被 标识符引用的一块区域。 (2)key(数据属性) 为String或者Symbol类型, value(属性值) 为任意值。

如何验证Object的数据属性key的类型呢?如下: Object的key类型

数组也是对象,数组下标是字符串。 ES6 Map、WeakMap对象的属性可以是任意类型。Map和WeakMaps之间的差别在于,在Map中,对象键是可枚举的。WeakMap的对象键是弱引用,若只剩下weakMap引用对象键,则GC机制自动清空该对象键及其属性值。

(3)不是关键字 (4)设计思想:模糊对象和基本类型之间的关系 (5)注意:. 运算符提供了装箱操作,根据基础类型构造一个临时对象,能在基础类型上调用对应对象的方法。 该临时对象只存在于方法调用那一行代码执行的瞬间,执行方法后立刻被销毁。 装箱机制会频繁产生临时对象,尽量避免。 (6)V8表示: 000开头 (7)属性分类(2种): ECMAScript定义的对象中有两种属性:数据属性和访问器属性。

3-8-1.数据属性

数据属性是键值对,并且每个数据属性拥有下列特性:

特性数据类型描述默认值
[[Value]]任何Javascript类型包含这个属性的数据值。undefined
[[Writable]]Boolean如果该值为 false,则该属性的 [[Value]] 特性 不能被改变。false
[[Enumerable]]Boolean如果该值为 true,则该属性可以用 for...in 循环来枚举。false
[[Configurable]]Boolean如果该值为 false,则该属性不能被删除,并且 除了 [[Value]][[Writable]] 以外的特性都不能被改变。false

3-8-2.访问器属性

访问器属性有一个或两个访问器函数 (get 和 set) 来存取数值,并且有以下特性:

特性类型描述默认值
[[Get]]函数对象或者 undefined该函数使用一个空的参数列表,能够在有权访问的情况下读取属性值。undefined
[[Set]]函数对象或者 undefined该函数有一个参数,用来写入属性值undefined
[[Enumerable]]Boolean如果该值为 true,则该属性可以用 for...in 循环来枚举。false
[[Configurable]]Boolean如果该值为 false,则该属性不能被删除,并且不能被转变成一个数据属性。false

注意:这些特性只有 JavaScript 引擎才用到,因此你不能直接访问它们。所以特性被放在两对方括号中。


后启: 我们已经知道了JS有8种数据类型,因为JS是动态语言,所以它非常灵活,我们最初可能需要某个变量是String类型,但后续若需要该变量是Boolean类型,我们应该如何处理呢?这就涉及了类型转换。 任何事物都是双刃剑,JS太过于灵活,随心所欲的开发一时爽,但并不利于后期的维护工作,大大降低了代码的可维护性和可阅读性。所以业界开始广泛使用TS对其进行类型约束,一方面是为了在编译阶段发现某些错误,另一方面提高了代码的可维护性。