分类
原始类型包括:
Number:表示数字,例如 42 或 3.14159。String:表示文本(或字符串),例如 "Hello World"。Boolean:表示真或假的逻辑类型。Null:表示空或不存在的值。Undefined:表示未定义的值,通常用于声明的变量但未赋值的情况。Symbol:表示唯一的值,通常用于创建对象的唯一属性名。BigInt:表示大整数。
引用类型包括:
Object:表示一种复杂的数据结构,包含键值对。Array:表示一组有序的值。Function:表示可执行的代码块。Date:表示日期和时间。RegExp:表示正则表达式。
原始类型的值是不可变的,而引用类型的值是可变的。原始类型的比较是通过它们的值来进行的,而引用类型的比较是通过它们的引用来进行的。
// Number
typeof 1; // 'number'
typeof NaN; // 'number'
typeof Infinity; // 'number'
typeof Number(1); // 'number'
NaN === NaN; // false
NaN == NaN; // false
Infinity === Infinity; // true
// String
typeof ''; // 'string'
typeof 'string'; // 'string'
typeof String(''); // 'string'
// Boolean
typeof true; // 'boolean'
typeof false; // 'boolean'
typeof Boolean(); // 'boolean'
Boolean() === false; // true
// null
typeof null; // 'object'
null instanceof Object; // false
null === null; // true
// undefined
typeof undefined; // 'undefined'
undefined === undefined; // true
// Symbol
typeof Symbol('description'); // 'symbol'
Symbol('description') === Symbol('description'); // false
Symbol('description') == Symbol('description'); // false
// BigInt
typeof BigInt(123123123123123); // 'bigint'
BigInt(111) === 111; // false
BigInt('120312930129030912903123901293091233213123123123123123123') === 120312930129030912903123901293091233213123123123123123123n; // true
判断数据类型的方法
在JavaScript中,有多种方法可以用来判断数据类型:
- typeof运算符:
typeof是最常用的判断数据类型的方法。它返回一个字符串,表示未经计算的操作数的类型。
console.log(typeof 'Hello'); // 输出 "string"
console.log(typeof 123); // 输出 "number"
优点:简单、快速,对于原始类型能够返回正确的结果。
缺点:对于 null 返回 "object",对于数组和对象都返回 "object",无法区分对象和数组。
- instanceof运算符:
instanceof运算符用来测试构造函数的prototype属性是否出现在对象的原型链中的任何位置。
console.log([] instanceof Array); // 输出 true
console.log({} instanceof Object); // 输出 true
优点:可以正确识别数组和普通对象。
缺点:不能正确处理跨iframe的数据类型判断。
- Object.prototype.toString方法:
Object.prototype.toString方法返回对象的类型字符串,因此可以用来检测对象的具体类型。
console.log(Object.prototype.toString.call([])); // 输出 "[object Array]"
console.log(Object.prototype.toString.call({})); // 输出 "[object Object]"
优点:最准确、最稳定的类型判断方法,可以正确区分数组、对象、null 等类型。
缺点:代码稍微复杂一些。
准确判断数组类型:由于 typeof 对数组的判断不准确,instanceof 对跨iframe的数组判断不准确,所以最准确的判断数组的方法是使用 Object.prototype.toString。
function isArray(obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
}
console.log(isArray([])); // 输出 true
console.log(isArray({})); // 输出 false
null和undefined的区别
在JavaScript中,null 和 undefined 都表示变量没有值,但它们在使用上有一些区别:
- 声明变量未赋值:当声明一个变量但未给它赋值时,它的值就是
undefined。
let test;
console.log(test); // 输出 "undefined"
- 函数默认返回值:如果一个函数没有明确的返回值,那么它的返回值就是
undefined。
function testFunc() {}
console.log(testFunc()); // 输出 "undefined"
- 对象属性不存在:如果你试图访问一个对象的属性,但这个属性并不存在,那么得到的值也是
undefined。
let obj = {};
console.log(obj.test); // 输出 "undefined"
- 表示"无"或"空":
null通常用来表示一个变量应该"没有值"或"空值"。这通常是你明确设置的。
let test = null;
console.log(test); // 输出 "null"
- 类型转换:
null和undefined在类型转换时也有一些区别。例如,当转换为数字时,null转换为0,而undefined转换为NaN。
console.log(Number(null)); // 输出 0
console.log(Number(undefined)); // 输出 NaN
总的来说,undefined 表示系统级的、出乎意料的或类似错误的空值,而 null 表示程序级的、正常的或在逻辑上应该的空值。
Symbol
在JavaScript中,Symbol 是一种特殊的、不可变的数据类型,可以作为对象属性的标识符。每个 Symbol 都是唯一的,即使我们创建了两个完全相同的 Symbol,它们也是不相等的。
Symbol 在实际开发中的应用:
- 创建私有属性:由于
Symbol是唯一的,我们可以使用Symbol作为对象属性的键,创建出真正的私有属性。Symbol 值作为属性名,遍历对象的时候,该属性不会出现在for...in、for...of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。有一个Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
let privateProperty = Symbol();
let obj = {
[privateProperty]: 'Hello, world!'
};
console.log(obj[privateProperty]); // 输出 "Hello, world!"
- 使用预定义的
Symbol值:JavaScript 提供了一些预定义的Symbol值,可以用来改变默认的语言行为。例如,Symbol.iterator可以用来定义一个对象的默认迭代器。
let arr = [1, 2, 3];
let iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // 输出 {value: 1, done: false}
手动实现一个简单的 Symbol:
由于 Symbol 的实现涉及到 JavaScript 引擎的内部机制,我们无法完全在 JavaScript 中实现。但我们可以创建一个函数,生成类似 Symbol 的值。
let symbolPolyfill = (function() {
let counter = 0;
return function description() {
return `Symbol(${description || ''})_${counter++}`;
};
})();
console.log(symbolPolyfill('test')); // 输出 "Symbol(test)_0"
console.log(symbolPolyfill('test')); // 输出 "Symbol(test)_1"
let key1 = symbolPolyfill('key');
let key2 = symbolPolyfill('key');
let obj = {};
obj[key1] = 'value1';
obj[key2] = 'value2';
console.log(obj[key1]); // 输出 "value1"
console.log(obj[key2]); // 输出 "value2"
这个 symbolPolyfill 函数生成的字符串始终是唯一的,类似于 Symbol 的行为。但请注意,这只是一个简化的模拟,并不具有 Symbol 的所有特性。
类型转换
在JavaScript中,类型转换可以分为两种:显式类型转换和隐式类型转换。
显式类型转换:明确要求改变某个值的类型。例如:
let num = '3';
console.log(Number(num)); // 输出 3
隐式类型转换:是JavaScript在执行过程中自动改变数据类型。例如:
let num = '3';
console.log(num * 2); // 输出 6
在这个例子中,字符串 '3' 被自动转换为了数字 3,以进行乘法运算。
可能发生隐式类型转换的场景包括:
-
算术运算符:如
+、-、*、/。如果其中一个操作数是字符串,那么另一个操作数会被转换为字符串。如果其中一个操作数是数字,那么另一个操作数会被转换为数字。 -
比较运算符:如
==。如果比较的两个值类型不同,JavaScript会尝试将它们转换为相同的类型。 -
逻辑运算符:如
&&、||和!。这些运算符会将操作数转换为布尔值。
转换原则:
-
转换为字符串:通过调用对象的
toString方法或String函数进行转换。 -
转换为数字:通过调用对象的
valueOf方法或Number函数进行转换。null转换为0,undefined转换为NaN。 -
转换为布尔值:
undefined、null、0、NaN、空字符串转换为false,其他所有值转换为true。
避免隐式类型转换的方法:
-
使用
===和!==运算符进行比较,这些运算符不会进行类型转换。 -
在进行算术运算或比较之前,明确地将值转换为预期的类型。
巧妙应用隐式类型转换:
- 使用
+运算符将数字转换为字符串。
let num = 123;
console.log('Value: ' + num); // 输出 "Value: 123"
- 使用
!!运算符将值转换为布尔值。
let truthyValue = 'hello';
let falsyValue = '';
console.log(!!truthyValue); // 输出 true
console.log(!!falsyValue); // 输出 false
原始类型和引用类型的理解和区别
原始类型:也被称为基本类型,包括Number、String、Boolean、Null、Undefined、Symbol和BigInt。原始类型的特点是它们的值是不可变的。当你改变一个原始类型的变量时,实际上是创建了一个新的值。原始类型的变量直接存储值,它们在内存中占用固定大小的空间,因此可以直接按值访问。
引用类型:包括Object、Array和Function。引用类型的特点是它们的值是可变的。引用类型的变量不直接存储值,而是存储值的引用(或者说指针)。当你改变一个引用类型的变量时,实际上是改变了这个变量引用的值。
以下是原始类型和引用类型的主要区别:
-
存储方式:原始类型直接存储在变量访问的位置(也就是说,它们被存储在栈内存中),这是因为这些类型占用的空间是固定的,所以可以直接存储在变量的访问位置。而引用类型的值是对象,存储在堆内存中,变量实际上存储的是一个指向堆内存中该对象的指针。
-
复制变量:当你复制一个原始类型的变量到另一个变量,源变量和目标变量会有各自独立的存储空间,改变一个变量的值不会影响另一个变量。但是当你复制一个引用类型的变量到另一个变量时,它们会指向堆内存中的同一个对象,改变一个变量会影响到另一个变量。
-
比较变量:当比较两个原始类型的变量时,是直接比较它们的值。而比较两个引用类型的变量时,是比较它们是否指向堆内存中的同一个对象。
-
参数传递:JavaScript中所有函数的参数都是按值传递的。但是如果函数参数是原始类型的,传递的就是原始类型的值,而如果函数参数是引用类型的,传递的就是对象的引用,而不是实际的对象。
原始类型对应的内置对象以及装箱拆箱操作
在JavaScript中,每种原始类型都有对应的内置对象,这些对象提供了一些方法和属性,可以用来操作原始类型的值。原始类型和它们对应的内置对象如下:
Number对应Number对象String对应String对象Boolean对应Boolean对象Symbol对应Symbol对象BigInt对应BigInt对象
注意,null 和 undefined 没有对应的内置对象。
装箱操作是将原始类型转换为对应的对象。这通常是隐式进行的,例如当你试图调用原始类型值的方法时:
let str = "hello";
console.log(str.toUpperCase()); // "HELLO"
在这个例子中,字符串 "hello" 被转换为一个 String 对象,然后调用了 toUpperCase 方法。
拆箱操作是将对象转换回原始类型。这也通常是隐式进行的,例如当你试图将对象用在需要原始类型的地方时:
let strObj = new String("hello");
console.log(strObj + "!"); // "hello!"
在这个例子中,String 对象 strObj 被转换回了字符串,然后与 "!" 连接。
注意,尽管装箱和拆箱操作在某些情况下很有用,但通常建议直接使用原始类型,因为对象可能会带来额外的性能开销。
JavaScript中变量在内存中的具体存储形式
在JavaScript中,变量的存储方式取决于它们的数据类型:原始类型或引用类型。
原始类型:当你创建一个原始类型(如Number、String、Boolean、Null、Undefined、Symbol、BigInt)的变量时,JavaScript引擎会在栈内存中为这个变量分配一个空间,并将其值直接存储在这个空间中。每个原始类型的变量都有自己的内存空间,即使它们的值相同,它们也是完全独立的。
let a = 10;
let b = a;
b = 20;
console.log(a); // 输出 10,a的值并未改变
引用类型:当你创建一个引用类型(如Object、Array、Function)的变量时,JavaScript引擎会在栈内存中为这个变量分配一个空间,但是这个空间中存储的是这个对象在堆内存中的地址(或者说引用)。实际的对象值是存储在堆内存中的。当你将一个对象赋值给另一个变量时,你实际上是在复制这个引用,而不是对象本身。
let obj1 = { value: 10 };
let obj2 = obj1;
obj2.value = 20;
console.log(obj1.value); // 输出 20,obj1的值被改变
在这个例子中,obj1 和 obj2 都指向堆内存中的同一个对象,所以改变 obj2 的值也会影响 obj1。
JavaScript中对象的底层数据结构
在JavaScript中,对象的底层数据结构是一种称为哈希表(Hash Table)或哈希映射(Hash Map)的数据结构。哈希表是一种键值对的集合,它通过一个哈希函数将键(也称为属性或字段)映射到表中的一个位置,然后在这个位置存储对应的值。
在JavaScript对象中,键通常是字符串(尽管在ES6中,也可以是Symbol类型),值可以是任何JavaScript支持的数据类型。
以下是一个简单的JavaScript对象:
let obj = {
"name": "John",
"age": 30,
"city": "New York"
};
在这个对象中,"name"、"age"和"city"是键,"John"、30和"New York"是对应的值。
当你访问对象的一个属性时,JavaScript引擎会使用哈希函数将属性名转换为一个哈希值,然后使用这个哈希值来查找对应的值。这使得对象属性的查找和访问非常快速。
需要注意的是,虽然哈希表提供了快速的查找和访问,但它们并不保证元素的顺序。这意味着如果你遍历一个对象的属性,你可能会得到不同的顺序,即使你在创建对象时使用了相同的代码。如果你需要保证顺序,你可以使用Map对象,它是ES6中引入的一种新的数据结构。
数据精度
JavaScript中的数字是以64位浮点数格式存储的,也就是说,它们是二进制的,而不是我们通常使用的十进制。这就导致了一些小数在二进制中不能精确表示,从而产生了精度丢失的问题。例如,0.1 + 0.2 在JavaScript中的结果是0.30000000000000004,而不是我们期望的0.3。
JavaScript可以表示的最大数字是Number.MAX_VALUE,约等于1.8e+308。如果一个数字大于这个值,JavaScript会返回Infinity。
JavaScript可以安全表示的最大整数是Number.MAX_SAFE_INTEGER,等于9007199254740991。这是因为JavaScript的数字是以64位浮点数格式存储的,其中52位用于存储数值,所以最大安全整数是2的53次方减1。
处理大数字的方法:
- BigInt:在JavaScript中,
BigInt可以用来表示任意大的整数。你可以通过在整数的末尾添加n来创建一个BigInt。
let bigInt = 1234567890123456789012345678901234567890n;
console.log(bigInt); // 输出 1234567890123456789012345678901234567890n
- 第三方库:例如decimal.js或bignumber.js,这些库提供了更多的操作和更高的精度。
避免精度丢失的方法:
- 整数运算:将浮点数转换为整数,进行整数运算后再转换回浮点数。
function add(num1, num2) {
let factor = Math.pow(10, Math.max(getDecimalLength(num1), getDecimalLength(num2)));
return (num1 * factor + num2 * factor) / factor;
}
function getDecimalLength(num) {
let str = num.toString();
return str.indexOf('.') === -1 ? 0 : str.length - str.indexOf('.') - 1;
}
console.log(add(0.1, 0.2)); // 输出 0.3
- toFixed方法:使用
Number.prototype.toFixed方法将结果四舍五入到指定的小数位数。
let result = (0.1 + 0.2).toFixed(2);
console.log(result); // 输出 "0.30"
请注意,toFixed方法返回的是一个字符串,如果需要一个数字,你可能需要使用Number函数进行转换。