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)特性:
- 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
- BigInt除法运算会自动
向下取整
console.log(25n / 10n); // 2n
console.log(25 / 10); // 2.5
- 不允许使用一元运算符+
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的类型呢?如下:
数组也是对象,数组下标是字符串。 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对其进行类型约束,一方面是为了在编译阶段发现某些错误,另一方面提高了代码的可维护性。