一、JS的数据类型有哪些?
JavaScript中的数据类型可以分为原始数据类型(基本数据类型)和引用数据类型两种。
原始数据类型包含:
- Number:数值类型
- String:字符串类型
- Boolean:布尔类型
- Undefined:undefined,表示值未定义
- Null:null,空值
- Symbol(ES6新增):符号类型
- Bingint(ES10新增):大整数类型
引用数据类型统称为Object,主要包含:
- Object:对象
- Array:数组
- Function:函数
- 除了上述三种之外,还包括Date、RegExp、Map、Set等......
二、数据类型介绍
1.Number
Number类型使用IEEE 754格式表示整数和浮点值,不同的数值类型有不同的数值字面量格式。
最基本的数值字面量格式是十进制整数,也可以用八进制(以8为基数,以0o开头)或十六进制(以16为基数,以0x开头)字面量表示。
let intNum = 55; //十进制整数55
let octalNum = 0o70; //八进制整数56
let hexNum = 0x39; //十六进制整数57
浮点值数值中必须包含小数点,且小数点后必须至少有一个数字。对于非常大或非常小的数值,浮点值可以用科学计数法来表示,ES中科学计数法的格式要求是一个数值后跟一个大写或小写的字母e,再加上一个要乘的10的多少次幂。浮点值精确度最高可达17位小数,但在算术计算中远不如整数准确。
let floatNum1 = 1.1;
let floatNum2 = 0.1;
let floatNum3 = .1; // 有效,但不推荐
let floatNum4 = 3.125e7; // 等于 31250000
let a = 0.1, b = 0.2;
console.log(a + b); //0.30000000000000004 !== 0.3
有一个特殊的数值叫NaN,意思是“不是数值”(Not a Number),用于表示本来要返回数值的操作失败了(而不是抛出错误)。比如,用0除以任意数值在其他语言中通常会导致错误,但在ECMAScript中,0、+0、-0相除会返回NaN。如果分子是非0值,分母是有符号0或无符号0,则会返回Infinity(无穷大)或-Infinity(由于JavaScript保存数值的方式,实际中可能存在+0和-0,+0和-0在所有情况下都被认为是相同的)。
NaN独有的属性:①任何涉及NaN的操作始终返回NaN;②NaN不等于包含NaN在内的任何值。为此,ECMAScript提供了isNaN()函数,用来判断该函数接收到的参数是否“不是数值”。
console.log(0/0); //NaN
console.log(5/0); //Infinity
console.log(5/-0); //-Infinity
console.log(NaN == NaN); //false
console.log(isNaN(NaN)); //true
console.log(isNaN(10)); //false,10是数值
console.log(isNaN("10")); //false,"10"可以转换为数值10
console.log(isNaN("ES")); //true,"ES"无法转换为数值
console.log(isNaN(true)); //false,true可以转换为数值1,false可以转换为数值0
数值转换:有3 个函数可以将非数值转换为数值:Number()、parseInt()和parseFloat()。Number()是转型函数,可用于任何数据类型。后两个函数主要用于将字符串转换为数值。
Number()函数基于如下规则执行转换:
布尔值,true 转换为1,false 转换为0。
数值,直接返回。
null,返回0。
undefined,返回NaN。
字符串,应用以下规则:
如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值。因此,Number("1")返回1,Number("123")返回123,Number("011")返回11(忽略前面的零)。
如果字符串包含有效的浮点值格式如"1.1",则会转换为相应的浮点值(同样,忽略前面的零)。
如果字符串包含有效的十六进制格式如"0xf",则会转换为与该十六进制值对应的十进制整数值。
如果是空字符串(不包含字符),则返回0。
如果字符串包含除上述情况之外的其他字符,则返回NaN。
对象,调用valueOf()方法,并按照上述规则转换返回的值。如果转换结果是NaN,则调用toString()方法,再按照转换字符串的规则转换。let num1 = Number("Hello world!"); // NaN
let num2 = Number(""); // 0
let num3 = Number("000011"); // 11
let num4 = Number(true); // 1
parseInt()函数更专注于字符串是否包含数值模式。字符串最前面的空格会被忽略,从第一个非空格字符开始转换。如果第一个字符不是数值字符、加号或减号,parseInt()立即返回NaN。这意味着空字符串也会返回NaN(这一点跟Number()不一样,它返回0)。如果第一个字符是数值字符、加号或减号,则继续依次检测每个字符,直到字符串末尾,或碰到非数值字符。
let num1 = parseInt("1234blue"); // 1234
let num2 = parseInt(""); // NaN
let num3 = parseInt("0xA"); // 10,解释为十六进制整数
let num4 = parseInt(22.5); // 22
let num5 = parseInt("70"); // 70,解释为十进制值
let num6 = parseInt("0xf"); // 15,解释为十六进制整数
不同的数值格式很容易混淆,因此parseInt()也接收第二个参数,用于指定底数(进制数)。如果知道要解析的值是十六进制,那么可以传入16 作为第二个参数。
let num1 = parseInt("AF", 16); // 175
let num2 = parseInt("AF"); // NaN
let num1 = parseInt("10", 2); // 2,按二进制解析
let num2 = parseInt("10", 8); // 8,按八进制解析
let num3 = parseInt("10", 10); // 10,按十进制解析
let num4 = parseInt("10", 16); // 16,按十六进制解析
parseFloat()函数也是从位置 0 开始检测每个字符,它始终忽略字符串开头的零,解析到字符串末尾或者解析到一个无效的浮点数值字符为止。这意味着第一次出现的小数点是有效的,但第二次出现的小数点就无效了,此时字符串的剩余字符都会被忽略。parseFloat()只解析十进制值,不能指定底数。
let num1 = parseFloat("1234blue"); // 1234,按整数解析
let num2 = parseFloat("0xA"); // 0
let num3 = parseFloat("22.5"); // 22.5
let num4 = parseFloat("22.34.5"); // 22.34
let num5 = parseFloat("0908.5"); // 908.5
let num6 = parseFloat("3.125e7"); // 31250000
2.String
String(字符串)类型表示0或多个16位Unicode字符序列。字符串可以使用双引号(")、单引号(')或反引号(`)标示。
let name1 = "Jack";
let name2 = 'Mary';
let name3 = `ECMAScript`;
ECMAScript中的字符串一旦创建,其值就不可以改变,若要修改某个变量中的值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量。
let language = "Java"; //Java
language = language + "Script"; //JavaScript
在这里,变量language 一开始包含字符串"Java"。紧接着,language 被重新定义为包含"Java"和"Script"的组合,也就是"JavaScript"。整个过程首先会分配一个足够容纳10 个字符的空间,然后填充上"Java"和"Script"。最后销毁原始的字符串"Java"和字符串"Script",因为这两个字符串都没有用了。
字符串转换:有两种方式把一个值转换为字符串。首先是使用几乎所有值都有的toString()方法。这个方法唯一的用途就是返回当前值的字符串等价物。null 和undefined 值没有toString()方法。
let age = 11;
let ageAsString = age.toString(); // 字符串"11"
let found = true;
let foundAsString = found.toString(); // 字符串"true"
如果不确定一个值是不是null 或undefined,可以使用String()转型函数,它始终会返回表示相应类型值的字符串。String()函数遵循如下规则:
• 如果值有toString()方法,则调用该方法(不传参数)并返回结果。
• 如果值是null,返回"null"。
• 如果值是undefined,返回"undefined"。
let value1 = 10;
let value2 = true;
let value3 = null;
let value4;
console.log(String(value1)); // "10"
console.log(String(value2)); // "true"
console.log(String(value3)); // "null"
console.log(String(value4)); // "undefined"
3.Boolean
Boolean(布尔值)类型有两个字面值:true 和false。这两个布尔值不同于数值,因此true 不等于1,false 不等于0。
let found = true;
let lost = false;
虽然布尔值只有两个,但所有其他ECMAScript 类型的值都有相应布尔值的等价形式。要将一个其他类型的值转换为布尔值,可以调用特定的Boolean()转型函数:
let message = "Hello ECMAScript!";
console.log(typeof message); //string
let messageAsBoolean = Boolean(message);
console.log(typeof messageAsBoolean); //boolean
不同类型与布尔值之间的转换规则:
数据类型会转换成为true的值会转换成为false的值BooleantruefalseString非空字符串""(空字符串)Number非零数值0、NaNObject任意对象nullUndefinedN/A(不存在)undefined
4.Undefined
Undefined 类型只有一个值,就是特殊值undefined。当使用 var 或 let 声明了变量但没有初始化时,就相当于给变量赋予了undefined 值,一般来说,永远不用显式地给某个变量设置undefined 值。
let message;
console.log(message,typeof message); // undefined 'undefined'
包含undefined 值的变量跟未定义变量是有区别的,对于已声明但未初始化的变量,在执行console.log()方法时,会指出变量的值,即‘undefined’,但是对于未声明的变量,只能执行一个有用的操作,就是对他调用typeof。无论是声明还是未声明,typeof 返回的都是字符串"undefined"。
let message;
console.log(message); //undefined
console.log(age); //报错
console.log(typeof message); //undefined
console.log(typeof age); //undefined
5.Null
Null 类型同样只有一个值,即特殊值null。逻辑上讲,null 值表示一个空对象指针,这也是给typeof 传一个 null 会返回"object"的原因。
undefined 值是由 null 值派生而来的,因此它们被定义为表面上相等,但是在全等比较操作时,他们是不同的。
let car = null;
console.log(typeof car); // "object"
console.log(null == undefined); //true
console.log(null === undefined); //false
在定义将来要保存对象值的变量时,可以使用 null 来初始化。这样只要检查这个变量的值是不是 null 就可以知道这个变量是否在后来被重新赋予了一个对象的引用。
if(car != null){
//car是一个对象的引用
}
即使 null 和 undefined 有关系,它们的用途也是完全不一样的。如前所述,永远不必显式地将变量值设置为 undefined。但 null 不是这样的。任何时候,只要变量要保存对象,而当时又没有那个对象可保存,就要用 null 来填充该变量。这样就可以保持 null 是空对象指针的语义,并进一步将其与 undefined 区分开来。
6.Symbol
Symbol(符号)是ECMAScript 6 新增的数据类型。Symbol是原始值,且Symbol实例是唯一、不可变的。Symbol的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
符号需要使用Symbol()函数初始化。因为符号本身是原始类型,所以typeof 操作符对符号返回symbol。
调用Symbol()函数时,也可以传入一个字符串参数作为对符号的描述,将来可以通过这个字符串来调试代码。但是,这个字符串参数与符号定义或标识完全无关。let sym1 = Symbol();
let sym2 = Symbol();
console.log(sym1,sym2); //Symbol() Symbol()
console.log(sym1 == sym2); //false
let sym3 = Symbol('foo');
let sym4 = Symbol('foo');
console.log(sym3 == sym4); //false
Symbol()函数不能与 new 关键字一起作为构造函数使用。
let mySymbol = new Symbol(); //报错
可以使用Symbol.for()方法在全局符号注册表中创建并重用符号。Symbol.for()对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。
即使采用相同的符号描述,在全局注册表中定义的符号跟使用Symbol()定义的符号也并不等同。
let sym5 = Symbol.for('foo'); //创建新符号
let sym6 = Symbol.for('foo'); //重用已有符号
console.log(typeof sym5); //symbol
console.log(sym5 === sym6); //true
let sym7 = Symbol('foo');
console.log(sym5 === sym7); //false
7.BigInt
BigInt 是一种数字类型的数据,它可以表示大于 2^53-1 的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。
可以用在一个整数字面量后面加 n 的方式定义一个 BigInt ,如:10n,或者调用函数 BigInt()(但不包含 new 运算符)并传递一个整数值或字符串值。
const theBiggestInt = 9007199254740991n;
const alsoHuge = BigInt(9007199254740991); // 9007199254740991n
const hugeString = BigInt("9007199254740991"); // 9007199254740991n
const hugeHex = BigInt("0x1fffffffffffff"); // 9007199254740991n
const hugeBin = BigInt("0b11111111111111111111111111111111111111111111111111");
// 9007199254740991n
BigInt 不同于 Number 之处:不能用于 Math 对象中的方法;不能和任何 Number 实例混合运算,两者必须转换成同一种类型。在两种类型来回转换时要小心,因为 BigInt 变量在转换成 Number 变量时可能会丢失精度。
以下操作符可以和 BigInt 一起使用: +、`*`、`-`、`**`、`%` 。除 >>> (无符号右移)之外的位操作也可以支持。因为 BigInt 都是有符号的, >>> (无符号右移)不能用于 BigInt 。
const MaxSafe = BigInt(Number.MAX_SAFE_INTEGER); // ↪ 9007199254740991n
const maxPlusOne = MaxSafe + 1n; // ↪ 9007199254740992n
const multi = MaxSafe * 2n; // ↪ 18014398509481982n
const subtr = multi – 10n; // ↪ 18014398509481972n
const mod = multi % 10n; // ↪ 2n
const bigN = 2n ** 54n; // ↪ 18014398509481984n
bigN * -1n // ↪ –18014398509481984n
8.Object
ECMA-262 将对象定义为一组属性的无序集合。严格来说,这意味着对象就是一组没有特定顺序的值。对象的每个属性或方法都由一个名称来标识,这个名称映射到一个值。
创建自定义对象的通常方式是创建Object 的一个新实例,然后再给它添加属性和方法。
let person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";
person.sayName = function() {
console.log(this.name);
};
现今创建 Object 的常用方式为对象字面量表示法。
let person = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
9.Array
ECMAScript 数组也是一组有序的数据,但跟其他语言不同的是,数组中每个槽位可以存储任意类型的数据。这意味着可以创建一个数组,它的第一个元素是字符串,第二个元素是数值,第三个是对象。ECMAScript 数组也是动态大小的,会随着数据添加而自动增长。
使用Array构造函数创建数组:
let colors = new Array(1, "blue", {age: 22});
使用数组字面量表示法创建数组:
let colors = ["red","yellow","blue"];
let names = [];
let values = [1,2,];
Array 构造函数还有两个ES6 新增的用于创建数组的静态方法:from()和of()。from()用于将类数组结构转换为数组实例,而 of() 用于将一组参数转换为数组实例。
console.log(Array.from("Matt")); // ["M", "a", "t", "t"]
console.log(Array.of(1, 2, 3, 4)); // [1, 2, 3, 4]
要取得或设置数组的值,需要使用中括号并提供相应值的数字索引:
let colors = ["red", "blue", "green"]; // 定义一个字符串数组
console.log(colors[0]); // 显示第一项 red
colors[2] = "black"; // 修改第三项
colors[3] = "brown"; // 添加第四项
console.log(colors); //['red', 'blue', 'black', 'brown']
10.Function
函数实际上是对象。每个函数都是Function类型的实例,而Function 也有属性和方法,跟其他引用类型一样。因为函数是对象,所以函数名就是指向函数对象的指针,而且不一定与函数本身紧密绑定。函数通常以函数声明的方式定义,比如:
function sum (num1, num2) {
return num1 + num2;
}
另一种定义函数的语法是函数表达式。函数表达式与函数声明几乎是等价的:
let sum = function(num1, num2) {
return num1 + num2;
};
还有一种定义函数的方式与函数表达式很像,叫作“箭头函数”,如下所示:
let sum = (num1, num2) => {
return num1 + num2;
};
最后一种定义函数的方式是使用 Function 构造函数。这个构造函数接收任意多个字符串参数,最后一个参数始终会被当成函数体,而之前的参数都是新函数的参数。不推荐使用。
let sum = new Function("num1", "num2", "return num1 + num2"); // 不推荐
三、数据类型存储方式
原始数据类型和引用数据类型的存储位置不同,原始数据类型存储在栈中,引用数据类型存储在堆中。堆和栈的概念存在于数据结构中和操作系统内存中。
在数据结构中,栈中数据的存取方式为后进先出。而堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。完全二叉树是堆的一种实现方式。
在操作系统中,内存被分为栈区和堆区。栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。堆区内存一般由程序员分配释放,若程序员不释放,程序结束时可能由垃圾回收机制回收。
原始数据类型是直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用的数据,所以放入栈中存储。
引用数据类型是存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
let a = 100;
let b = a;
b = 200;
console.log(a,b); //100,200
let obj1 = {};
let obj2 = obj1;
obj2.name = "ES6";
console.log(obj1.name); //ES6
JS数据类型及存储
总结
- 声明变量时不同的内存地址分配:
- 简单类型的值存放在栈中,在栈中存放的是对应的值
- 引用类型对应的值存储在堆中,在栈中存放的是指向堆内存的地址
- 不同的类型数据导致赋值变量时的不同:
- 简单类型赋值,是生成相同的值,两个对象对应不同的地址
- 复杂类型赋值,是将保存对象的内存地址赋值给另一个变量。也就是两个变量指向堆内存中同一个对象