数据类型
# 目录介绍
# 2.1 数据类型概述
JavaScript 是一种动态类型语言,变量的数据类型在运行时确定。JavaScript 提供了多种数据类型,分为原始类型(Primitive Types)和引用类型(Reference Types)。
# 2.1.1 类型分类
JavaScript 语言的每一个值,都属于某一种数据类型。JavaScript 的数据类型,共有六种。
- 1.字符串(string):文本(比如
Hello World)。 - 2.数值(number):整数和小数(比如
1和3.14)。 - 3.布尔值(boolean):表示真伪的两个特殊值,即
true(真)和false(假)。 - 4.undefined型:表示“未定义”或不存在,即由于目前没有定义,所以此处暂时没有任何值。
- 5.null型:表示空值(Null),即此处的值为空。
- 6.对象(object):各种值组成的集合。
通常,数值、字符串、布尔值这三种类型,合称为原始类型(primitive type)的值,即它们是最基本的数据类型,不能再细分了。对象则称为合成类型(complex type)的值,因为一个对象往往是多个原始类型的值的合成,可以看作是一个存放各种值的容器。至于undefined和null,一般将它们看成两个特殊值。
# 2.1.2 对象类型
对象是最复杂的数据类型,又可以分成三个子类型。
- 狭义的对象(object)
- 数组(array)
- 函数(function)
狭义的对象和数组是两种不同的数据组合方式,除非特别声明,本教程的“对象”都特指狭义的对象。函数其实是处理数据的方法,JavaScript 把它当成一种数据类型,可以赋值给变量,这为编程带来了很大的灵活性,也为 JavaScript 的“函数式编程”奠定了基础。
# 2.1.3 数据类型差异
JavaScript 和 Java 在数据类型方面的差异,不仅仅是两者所采用的术语不同。
- JavaScript:
JavaScript是一种动态类型语言,变量的数据类型在运行时确定。1.不需要显式声明变量类型;2.类型检查在运行时进行。 - Java:
Java是一种静态类型语言,变量的数据类型在编译时确定。1.必须显式声明变量类型;2.类型检查在编译时进行。
像 Java 这样,变量具有数据类型的语言,被称为静态数据类型语言;而像 JavaScript 这样,变量没有类型的语言,则被称为动态数据类型语言。
# 2.1.4 变量常量
变量是可以在程序运行过程中被重新赋值的标识符。在 JavaScript 中,变量可以通过 var、let 或 const 声明。
1.
var
- 作用域:函数作用域(function-scoped)。
- 提升:变量声明会被提升到函数或全局作用域的顶部。
- 重新声明:允许在同一作用域内重新声明。
var x = 10;
if (true) {
var x = 20; // 重新声明,覆盖外部的 x
}
console.log(x); // 输出: 20
2
3
4
5
2.
let
- 作用域:块级作用域(block-scoped)。
- 提升:变量声明会被提升,但在声明之前访问会抛出
ReferenceError(暂时性死区)。 - 重新声明:不允许在同一作用域内重新声明。
let y = 10;
if (true) {
let y = 20; // 新的块级作用域,不影响外部的 y
}
console.log(y); // 输出: 10
2
3
4
5
常量是声明后不能被重新赋值的标识符。在 JavaScript 中,常量通过 const 声明。
- 作用域:块级作用域(block-scoped)。
- 提升:常量声明会被提升,但在声明之前访问会抛出
ReferenceError(暂时性死区)。 - 重新赋值:不允许重新赋值。
- 重新声明:不允许在同一作用域内重新声明。
- 注意:
const声明的常量如果是对象或数组,其属性或元素可以被修改。
const z = 10;
// z = 20; // 报错: Assignment to constant variable
const obj = { name: 'Alice' };
obj.name = 'Bob'; // 允许修改属性
console.log(obj.name); // 输出: Bob
2
3
4
5
6
# 2.1.5 基于原型
基于类(Class-based)
类是对象的蓝图,定义了对象的属性和方法。对象是类的实例,通过 new 关键字创建。
强调封装、继承和多态。类是静态的,对象是动态的。典型的语言:Java、C++、Python。
基于原型
对象直接从其他对象继承属性和方法,而不是通过类。每个对象都有一个原型(prototype),通过原型链实现继承。对象可以动态修改,甚至可以在运行时添加或删除属性和方法。
没有类的概念,对象本身就是模板。典型的语言:JavaScript。
JavaScript 选择基于原型的设计,主要基于以下原因:
- JavaScript 最初是为浏览器设计的脚本语言,需要处理动态的、不可预测的用户交互。基于原型的模型允许对象在运行时动态修改,更适合这种场景。
- 基于原型的模型比基于类的模型更简单,不需要引入类的概念。
- JavaScript 支持函数式编程,函数是一等公民。基于原型的模型与函数式编程的结合更加自然,例如通过闭包实现封装。
JavaScript 中的类(ES6)
尽管 JavaScript 是基于原型的语言,但 ES6 引入了 class 语法糖,使得开发者可以使用类似基于类的语法。
注意:ES6 的 class 本质上是基于原型的语法糖,底层仍然使用原型链。
# 2.2 原始类型
原始类型是直接存储在栈内存中的简单数据,按值传递。
原始类型与引用类型的内存原理:JavaScript 引擎使用栈内存(Stack) 和堆内存(Heap) 两种存储方式。原始类型(number、string、boolean 等)的值直接存储在栈中,赋值时进行值拷贝;引用类型(object、array、function 等)的实际数据存储在堆中,栈中只保存一个指向堆内存的引用(指针)。因此,当你将一个对象赋值给另一个变量时,两个变量指向的是同一个堆内存地址,修改其中一个会影响另一个。这也是为什么需要"深拷贝"的原因。
# 2.2.1 number
Number 类型用来表示整数和浮点数,最常用的功能就是用来表示10进制的整数和浮点数。
let age = 25;
let price = 99.99;
2
number类型的底层原理:JavaScript 的 number 遵循 IEEE 754 双精度浮点数标准,使用64位存储:1位符号位 + 11位指数位 + 52位尾数位。这意味着:
- 最大安全整数为
Number.MAX_SAFE_INTEGER(2^53 - 1 = 9007199254740991) - 超过安全范围的整数运算会丢失精度
- 浮点数运算存在精度问题
// 经典的浮点数精度问题
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
// 解决方案1:使用 Number.EPSILON 比较
function floatEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
console.log(floatEqual(0.1 + 0.2, 0.3)); // true
// 解决方案2:转为整数运算
console.log((0.1 * 10 + 0.2 * 10) / 10); // 0.3
// 特殊值
console.log(Infinity); // 正无穷
console.log(-Infinity); // 负无穷
console.log(NaN); // Not a Number
console.log(NaN === NaN); // false(NaN不等于自身!)
console.log(Number.isNaN(NaN)); // true(推荐的判断方式)
console.log(isNaN('hello')); // true(全局isNaN会先转换类型,不推荐)
console.log(Number.isNaN('hello')); // false(推荐)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
V8 的 Smi 优化:虽然规范要求所有数字都是双精度浮点数,但 V8 引擎会对小整数(Small Integer, Smi) 做特殊优化——在32位系统中,-2^30 到 2^30-1 范围内的整数直接存储为机器整数(不用分配堆内存),从而大幅提升整数运算性能。
# 2.2.2 string
String用于表示一个字符序列,即字符串。字符串需要使用 单引号 或 双引号 括起来。
let name = "Alice";
let message = 'Hello, world!';
let greeting = `Hello, ${name}!`; // 模板字符串,这个是变量拼接
2
3
如果要在单引号字符串的内部,使用单引号,就必须在内部的单引号前面加上反斜杠,用来转义。双引号字符串内部使用双引号,也是如此。
'Did she say \'Hello\'?'
// "Did she say 'Hello'?"
"Did she say \"Hello\"?"
// "Did she say "Hello"?"
2
3
4
5
# 2.2.3 boolean
表示逻辑值,只有 true 和 false 两个值。
let isActive = true;
let isCompleted = false;
2
布尔值使用特殊情况。如果 JavaScript 预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是除了下面六个值被转为false,其他值都视为true。
undefinednullfalse0NaN""或''(空字符串)
if ('') {
console.log('true');
}
// 没有任何输出
if ([]) {
console.log('true');
}
// true
2
3
4
5
6
7
8
9
# 2.2.4 undefined
表示变量已声明但未赋值。undefined 型只能够取 undefined 这一个值。对 undefined 值进行 typeof 运算,其结果为 "undefined"。
let x;
console.log(x); // undefined
2
下面总结了会出现 undefined 值的情况。
- 未初始化的变量的值
- 不存在的属性的值
- 在没有传入实参而调用函数时,该函数内相应参数的值
- 没有 return 语句或是 return 语句中不含表达式的函数的返回值
- 对 void 运算符求值的结果(常常会通过使用 void 0 来获取一个 undefined 值)
# 2.2.5 null
表示空值或对象不存在。
let y = null;
console.log(y); // null
2
对于null和undefined,大致可以像下面这样理解。
null表示空值,即该处的值现在为空。调用函数时,某个参数未设置任何值,这时就可以传入null,表示该参数为空。比如,某个函数接受引擎抛出的错误作为参数,如果运行过程中未出错,那么这个参数就会传入null,表示未发生错误。undefined表示“未定义”
null 型没有与之相对应的 Null 类。因此,如果像下面这样对 null 值进行点运算,就会产生 TypeError 异常。
null.toString();
TypeError: null has no properties
2
# 2.2.6 symbol
ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值,它是 JavaScript 语言的第七种数据类型,是一种类似于字符串的数据类型。通常用于对象属性的键。
const id = Symbol("id");
console.log(id); // Symbol(id)
2
注意:遇到唯一性的场景时要想到 Symbol
# 2.2.7 bigint
表示任意精度的整数,用于处理大整数。
const bigNumber = 123456789012345678901234567890n;
console.log(bigNumber); // 123456789012345678901234567890n
2
# 2.3 引用类型
引用类型是存储在堆内存中的复杂数据,按引用传递。
# 2.3.1 object
表示键值对的集合,是 JavaScript 中最常用的数据类型。
const person = {
name: "Alice",
age: 25,
};
console.log(person.name); // Alice
2
3
4
5
# 2.3.2 array
数组定义。表示有序的元素集合,是一种特殊的对象。数组(array)是按次序排列的一组值。每个值的位置都有编号(从0开始),整个数组用方括号表示。
var arr = ['a', 'b', 'c'];
const numbers = [1, 2, 3, 4, 5];
console.log(numbers[0]); // 1
2
3
数组的空位。当数组的某个位置是空元素,即两个逗号之间没有任何值,我们称该数组存在空位(hole)。虽然这个位置没有值,引擎依然认为这个位置是有效的。
var a = [1, , 1];
a.length // 3
2
数组的本质。数组属于一种特殊的对象。typeof运算符会返回数组的类型是object。
typeof [1, 2, 3] // "object"
# 2.3.3 function
表示可执行的代码块,也是一种对象。
function greet(name) {
console.log(`Hello, ${name}!`);
}
greet("Alice"); // Hello, Alice!
2
3
4
# 2.3.4 date
表示日期和时间。
const now = new Date();
console.log(now); // 当前日期和时间
2
# 2.3.5 regexp
表示正则表达式,用于匹配字符串。
const regex = /hello/i;
console.log(regex.test("Hello, world!")); // true
2
# 2.3.6 map
表示键值对的集合,键可以是任意类型。
const map = new Map();
map.set("name", "Alice");
console.log(map.get("name")); // Alice
2
3
# 2.3.7 set
表示唯一值的集合。
const set = new Set([1, 2, 3, 3, 4]);
console.log(set); // Set { 1, 2, 3, 4 }
2
# 2.4 类型检测
JavaScript 有三种方法,可以确定一个值到底是什么类型。
typeof运算符instanceof运算符Object.prototype.toString方法
# 2.4.1 typeof
typeof运算符可以返回一个值的数据类型。检测变量的原始类型。
console.log(typeof 42); // "number"
console.log(typeof "hello"); // "string"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object" (历史遗留问题)
console.log(typeof Symbol("id")); // "symbol"
console.log(typeof 123n); // "bigint"
console.log(typeof {}); // "object"
console.log(typeof []); // "object"
console.log(typeof function () {}); // 函数返回 `function`。
2
3
4
5
6
7
8
9
10
利用这一点,typeof可以用来检查一个没有声明的变量,而不报错。
v
// ReferenceError: v is not defined
// 变量`v`没有用`var`命令声明,直接使用就会报错。但是,放在`typeof`后面,就不报错了,而是返回`undefined`。
typeof v
// "undefined"
2
3
4
5
6
实际编程中,这个特点通常用在判断语句。
// 错误的写法
if (v) {
// ...
}
// ReferenceError: v is not defined
// 正确的写法
if (typeof v === "undefined") {
// ...
}
2
3
4
5
6
7
8
9
10
# 2.4.2 instanceof
检测对象是否属于某个引用类型。
console.log([] instanceof Array); // true
console.log({} instanceof Object); // true
console.log(function () {} instanceof Function); // true
2
3
# 2.4.3 Object.prototype.toString
更精确地检测数据类型。
console.log(Object.prototype.toString.call(42)); // "[object Number]"
console.log(Object.prototype.toString.call("hello")); // "[object String]"
console.log(Object.prototype.toString.call([])); // "[object Array]"
2
3
# 2.5 对象
# 2.5.1 对象概述
从底层实现来看,JavaScript 的对象和 Java 的对象在基本原则上是相同的。两者都是内存中的实体,保持着某种状态,并且是用于编程操作的目标对象。但是,从高层概念来看的话,就会发现两者有着不小的差别。
Java 中的对象可以认为是类的一种实例化结果, 而 JavaScript 中并没有类这样的语言构造。
JavaScript 中的对象(Object)是一种复合数据类型,用于存储键值对(key-value pairs)。
对象是 JavaScript 中最核心的概念之一,几乎所有的 JavaScript 实体(如数组、函数等)都是对象或基于对象。
JavaScript 中的对象是一个名称与值配对的集合。这种名称与值的配对被称为属性。这样一来,JavaScript 对象可以定义为属性的集合。
# 2.5.2 对象定义
对象是由一组属性和方法组成的集合。属性是键值对,键是字符串(或 Symbol),值可以是任意数据类型(包括其他对象)。
1.对象字面量。使用花括号 {} 定义对象。
const person = {
name: "Alice",
age: 25,
greet: function() {
console.log(`Hello, my name is ${this.name}`);
}
};
2
3
4
5
6
7
2.使用 new Object()。使用构造函数创建对象。
const person = new Object();
person.name = "Alice";
person.age = 25;
person.greet = function() {
console.log(`Hello, my name is ${this.name}`);
};
2
3
4
5
6
# 2.5.3 访问对象属性
第一种方式:使用.来访问。
console.log(person.name); // Alice
person.greet(); // Hello, my name is Alice
2
第二种方式:使用 [] 来访问。适用于动态属性名或属性名包含特殊字符的情况。
console.log(person["name"]); // Alice
const key = "age";
console.log(person[key]); // 25
2
3
# 2.5.4 修改对象属性
直接赋值即可修改属性值。
person.age = 30;
console.log(person.age); // 30
2
# 2.5.5 添加新属性
直接赋值即可添加新属性。
person.job = "Developer";
console.log(person.job); // Developer
2
# 2.5.6 删除属性
使用 delete 关键字删除属性。
var person = new Object();
person.name = "孙悟空";
person.age = 18;
console.log(person);
delete person.name
console.log(person);
2
3
4
5
6
7
# 2.5.7 对象方法
对象的方法是一个函数类型的属性。
const person = {
name: "Alice",
greet: function() {
console.log(`Hello, my name is ${this.name}`);
}
};
person.greet(); // Hello, my name is Alice
2
3
4
5
6
7
# 2.5.8 this关键字
this 指向当前对象,用于访问对象的属性和方法。
const person = {
name: "Alice",
greet: function() {
console.log(`Hello, my name is ${this.name}`);
}
};
person.greet(); // Hello, my name is Alice
2
3
4
5
6
7
# 2.5.9 对象遍历
1.for...in 循环。遍历对象的可枚举属性。
for (let key in person) {
console.log(`${key}: ${person[key]}`);
}
2
3
2.Object.keys()。返回对象的所有键组成的数组。
const keys = Object.keys(person);
console.log(keys); // ["name", "age", "greet"]
2
3.Object.values()。返回对象的所有值组成的数组。
const values = Object.values(person);
console.log(values); // ["Alice", 25, function]
2
4.Object.entries()。返回对象的键值对组成的数组。
const entries = Object.entries(person);
console.log(entries); // [["name", "Alice"], ["age", 25], ["greet", function]]
2
# 2.5.10 对象解构
从对象中提取属性并赋值给变量。
const { name, age } = person;
console.log(name); // Alice
console.log(age); // 25
2
3
# 2.5.11 对象合并
1.Object.assign()。将多个对象合并到一个对象中。
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const merged = Object.assign({}, obj1, obj2);
console.log(merged); // { a: 1, b: 2 }
2
3
4
2.扩展运算符(...)
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const merged = { ...obj1, ...obj2 };
console.log(merged); // { a: 1, b: 2 }
2
3
4
# 2.5.12 对象原型
每个对象都有一个原型(prototype),用于继承属性和方法。
const person = {
name: "Alice",
greet: function() {
console.log(`Hello, my name is ${this.name}`);
}
};
const student = Object.create(person);
student.name = "Bob";
student.greet(); // Hello, my name is Bob
2
3
4
5
6
7
8
9
10
# 2.5.13 对象冻结
1.Object.freeze()。冻结对象,使其不可修改。
const obj = { a: 1 };
Object.freeze(obj);
obj.a = 2; // 无效
console.log(obj.a); // 1
2
3
4
2.Object.seal()。密封对象,使其不能添加或删除属性,但可以修改现有属性。
const obj = { a: 1 };
Object.seal(obj);
obj.a = 2; // 有效
obj.b = 3; // 无效
console.log(obj); // { a: 2 }
2
3
4
5
# 2.5.14 属性描述符与Object.defineProperty
JavaScript 对象的每个属性都有一个属性描述符(Property Descriptor),控制属性的行为:
const obj = {};
// 使用 Object.defineProperty 精确控制属性
Object.defineProperty(obj, 'name', {
value: 'Alice',
writable: false, // 不可修改
enumerable: true, // 可枚举(for...in 可遍历)
configurable: false // 不可删除,不可重新配置
});
obj.name = 'Bob'; // 静默失败(严格模式下会报错)
console.log(obj.name); // 'Alice'
// getter / setter(访问器属性)
const user = {
_age: 25,
get age() {
return this._age;
},
set age(value) {
if (value < 0 || value > 150) {
throw new RangeError('年龄必须在 0-150 之间');
}
this._age = value;
}
};
console.log(user.age); // 25
user.age = 30; // OK
// user.age = -1; // RangeError
// 查看属性描述符
console.log(Object.getOwnPropertyDescriptor(obj, 'name'));
// { value: 'Alice', writable: false, enumerable: true, configurable: false }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
底层原理:V8 引擎使用隐藏类(Hidden Class / Map) 来优化对象属性访问。当你用对象字面量创建对象时,V8 为其分配一个隐藏类;每次添加新属性,V8 都会创建一个新的隐藏类(通过转换链连接)。如果多个对象以相同顺序添加相同属性,它们共享隐藏类,这使得属性访问可以通过固定偏移量完成(类似 C++ 的结构体),而非哈希表查找。
// 推荐:在构造函数/字面量中一次性声明所有属性
const good = { a: 1, b: 2, c: 3 }; // 一个隐藏类
// 不推荐:动态添加属性(每次添加都生成新的隐藏类)
const bad = {};
bad.a = 1; // 隐藏类转换
bad.b = 2; // 隐藏类转换
bad.c = 3; // 隐藏类转换
2
3
4
5
6
7
8
# 2.6 类型转换
# 2.6.1 显式转换
使用函数或方法显式转换类型。
let num = Number("123"); // 字符串转数字
let str = String(123); // 数字转字符串
let bool = Boolean(0); // 数字转布尔值
2
3
# 2.6.2 隐式转换
JavaScript 在特定场景下自动转换类型。
隐式转换的底层原理:JavaScript 引擎在执行运算时,会调用内部的抽象操作进行类型转换。+ 运算符遇到字符串时会调用 ToPrimitive() → ToString(),将另一个操作数转为字符串进行拼接;-、*、/ 运算符则调用 ToNumber() 将操作数转为数字。对象转原始类型时,引擎先调用 [Symbol.toPrimitive](hint) 方法,若不存在则依次尝试 valueOf() 和 toString()(hint 为 "number" 时先 valueOf,hint 为 "string" 时先 toString)。理解这一机制,就能解释 [] + [] 为 ""、[] + {} 为 "[object Object]" 等看似奇怪的行为。
let result = "5" + 2; // "52" (字符串拼接)
let sum = "5" - 2; // 3 (字符串转数字)
let isTrue = !0; // true (数字转布尔值)
2
3
# 2.6.3 类型转换的完整规则表
| 原始值 | 转Number | 转String | 转Boolean |
|---|---|---|---|
undefined | NaN | 'undefined' | false |
null | 0 | 'null' | false |
true | 1 | 'true' | true |
false | 0 | 'false' | false |
0 | 0 | '0' | false |
''(空字符串) | 0 | '' | false |
'123' | 123 | '123' | true |
'hello' | NaN | 'hello' | true |
NaN | NaN | 'NaN' | false |
[](空数组) | 0 | '' | true |
[1] | 1 | '1' | true |
{} | NaN | '[object Object]' | true |
疑惑:为什么 [] == false 是 true,但 if([]) 却进入了真分支?
答疑:这涉及两种不同的转换路径。if([]) 是将 [] 转为布尔值,非空对象一律为 true。而 [] == false 是抽象相等比较,两边都会经过 ToPrimitive 转换——[] 先调用 valueOf()(返回自身),再调用 toString()(返回 ""),最后 "" == false → 0 == 0 → true。
// 验证
console.log(Boolean([])); // true(直接转布尔值)
console.log([] == false); // true(抽象相等比较)
console.log([] == ![]); // true(![] 为 false,回到上面的情况)
console.log({} == ![]); // false
// Symbol.toPrimitive 自定义转换
const customObj = {
[Symbol.toPrimitive](hint) {
if (hint === 'number') return 42;
if (hint === 'string') return 'custom';
return true; // default
}
};
console.log(+customObj); // 42
console.log(`${customObj}`); // 'custom'
console.log(customObj + ''); // 'true'(default hint)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17