JavaScript 数据类型

简介

JavaScript 中的数据类型分为两大类:原始类型(Primitive)引用类型(Reference)

原始类型(值不可变,按值传递):
  - string      字符串
  - number      数字
  - boolean     布尔值
  - undefined   未定义
  - null        空值
  - symbol      Symbol(ES6)
  - bigint      BigInt(ES2020)

引用类型(值可变,按引用传递):
  - object      对象(包括数组、函数、日期等)

原始数据类型

string(字符串)

表示文本数据,详见下文重点介绍。

number(数字)

表示整数和浮点数,所有数字都是 Number 类型。

let age = 25;
let price = 19.99;
let hex = 0xFF;     // 255
let exp = 1e3;      // 1000

// 特殊值
console.log(Infinity);  // 无穷大
console.log(-Infinity); // 负无穷大
console.log(NaN);      // 非数字(Not a Number)
console.log(NaN === NaN); // false(NaN 不等于任何值)

boolean(布尔值)

只有两个值:truefalse

let isActive = true;
let isDeleted = false;

undefined

变量已声明但未赋值时的值为 undefined

let x;
console.log(x); // undefined

function foo() {}  // 没有 return 的函数默认返回 undefined
console.log(foo()); // undefined

null

表示"空值"或"不存在的对象"。

let user = null;  // 明确表示没有值

// typeof 的怪异行为
console.log(typeof null); // "object"(这是历史遗留 bug)
console.log(null === undefined); // false
console.log(null == undefined);  // true(== 下相等)

symbol(Symbol)

ES6 引入,表示唯一的标识符,常用于对象属性的键。

const id = Symbol('id');
const id2 = Symbol('id');
console.log(id === id2); // false(每个 Symbol 都是唯一的)

bigint(BigInt)

ES2020 引入,表示任意精度的整数,用于超出 Number 范围的大整数。

const big = 12345678901234567890n;  // 末尾的 n 表示 BigInt
const alsoBig = BigInt(9007199254740991);
console.log(typeof big); // "bigint"

字符串(String)—— 重点

定义方式

// 单引号或双引号(推荐保持一致)
let name1 = 'Alice';
let name2 = "Bob";

// 模板字符串(反引号,ES6)—— 支持多行和插值
let name = 'Alice';
let age = 25;
let greeting = `Hello, my name is ${name} and I'm ${age} years old.`;
console.log(greeting); // Hello, my name is Alice and I'm 25 years old.

// 多行字符串
let multiLine = `第一行
第二行
第三行`;
console.log(multiLine);

字符串不可变性

字符串一旦创建就不能被修改,所有"修改"操作都返回新字符串。

let str = 'hello';
str[0] = 'H';          // 不会报错,但无效
console.log(str);        // "hello"(原字符串未改变)

str = 'Hello';           // 这是重新赋值,不是修改原字符串
console.log(str);        // "Hello"

常用属性

let str = 'Hello World';
console.log(str.length);    // 11(字符串长度)
console.log(str[0]);       // "H"(按索引访问)
console.log(str[str.length - 1]); // "d"(最后一个字符)

常用方法

大小写转换:

console.log('Hello'.toUpperCase()); // "HELLO"
console.log('Hello'.toLowerCase()); // "hello"

查找与判断:

let str = 'Hello World';

console.log(str.indexOf('World'));    // 6(首次出现位置,找不到返回 -1)
console.log(str.includes('Hello'));   // true(是否包含)
console.log(str.startsWith('Hello')); // true(是否以...开头)
console.log(str.endsWith('World'));   // true(是否以...结尾)

提取子串:

let str = 'Hello World';

console.log(str.slice(0, 5));    // "Hello"(从索引 0 到 4)
console.log(str.slice(6));       // "World"(从索引 6 到末尾)
console.log(str.slice(-5));      // "World"(负数表示从末尾往前数)

console.log(str.substring(0, 5)); // "Hello"(类似 slice,但不支持负数)

替换:

let str = 'Hello World';
console.log(str.replace('World', 'JavaScript')); // "Hello JavaScript"
console.log(str.replace(/l/g, 'L'));            // "HeLLo WorLd"(正则全局替换)

分割与合并:

// split:字符串 → 数组
let str = 'apple,banana,orange';
let arr = str.split(',');
console.log(arr); // ["apple", "banana", "orange"]

// 数组 → 字符串(见数组部分)

去除空白:

console.log('  hello  '.trim());    // "hello"(去除两端空白)
console.log('  hello  '.trimStart()); // "hello  "(去除开头空白)
console.log('  hello  '.trimEnd());   // "  hello"(去除结尾空白)

其他常用方法:

console.log('hello'.repeat(3));      // "hellohellohello"
console.log('hello'.charAt(0));      // "h"(指定位置的字符)
console.log('hello'.concat(' world')); // "hello world"(拼接)

数组(Array)—— 重点

定义方式

// 字面量(推荐)
let fruits = ['apple', 'banana', 'orange'];

// 构造函数
let numbers = new Array(1, 2, 3);
let empty = new Array(3);  // 创建一个长度为 3 的空数组

基本操作

let arr = ['a', 'b', 'c'];

// 访问和修改
console.log(arr[0]);   // "a"
arr[1] = 'B';          // 修改元素
console.log(arr);       // ["a", "B", "c"]

// 长度
console.log(arr.length); // 3
arr.length = 2;         // 可以修改 length 来截断数组
console.log(arr);       // ["a", "B"]

增删元素

会改变原数组的方法:

let arr = ['a', 'b'];

// 末尾添加/删除
arr.push('c');          // 末尾添加,返回新长度
console.log(arr);       // ["a", "b", "c"]
let last = arr.pop();   // 末尾删除,返回被删除的元素
console.log(last);      // "c"
console.log(arr);       // ["a", "b"]

// 开头添加/删除
arr.unshift('0');       // 开头添加,返回新长度
console.log(arr);       // ["0", "a", "b"]
let first = arr.shift(); // 开头删除,返回被删除的元素
console.log(first);     // "0"
console.log(arr);       // ["a", "b"]

// splice:全能增删改
let nums = [1, 2, 3, 4, 5];
nums.splice(1, 2);           // 从索引 1 开始,删除 2 个元素
console.log(nums);            // [1, 4, 5]

nums = [1, 2, 3, 4, 5];
nums.splice(2, 0, 'a', 'b'); // 从索引 2 开始,删除 0 个,插入 'a','b'
console.log(nums);            // [1, 2, "a", "b", 3, 4, 5]

nums = [1, 2, 3, 4, 5];
nums.splice(1, 2, 'x');      // 从索引 1 开始,删除 2 个,替换成 'x'
console.log(nums);            // [1, "x", 3, 4, 5]

查找元素

let arr = [10, 20, 30, 20];

console.log(arr.indexOf(20));    // 1(首次出现索引,找不到返回 -1)
console.log(arr.lastIndexOf(20)); // 3(最后一次出现索引)
console.log(arr.includes(30));   // true(是否包含)
console.log(arr.find(x => x > 15)); // 20(返回第一个满足条件的元素)
console.log(arr.findIndex(x => x > 15)); // 1(返回第一个满足条件的索引)

遍历与转换

不会改变原数组的方法:

let arr = [1, 2, 3, 4, 5];

// forEach:遍历(无返回值)
arr.forEach((item, index) => {
  console.log(index, item);
});

// map:映射(返回新数组)
let doubled = arr.map(x => x * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
console.log(arr);     // [1, 2, 3, 4, 5](原数组不变)

// filter:过滤
let evens = arr.filter(x => x % 2 === 0);
console.log(evens); // [2, 4]

// reduce:归约(强大!)
let sum = arr.reduce((acc, cur) => acc + cur, 0);
console.log(sum); // 15

// 找出最大值
let max = arr.reduce((a, b) => a > b ? a : b);
console.log(max); // 5

// some:是否存在满足条件的元素
console.log(arr.some(x => x > 3)); // true

// every:是否所有元素都满足条件
console.log(arr.every(x => x > 0)); // true
console.log(arr.every(x => x > 3)); // false

其他常用方法

let a = [1, 2];
let b = [3, 4];

// concat:合并数组(不改变原数组)
console.log(a.concat(b)); // [1, 2, 3, 4]
console.log(a);           // [1, 2](不变)

// slice:提取子数组(不改变原数组)
let arr = [1, 2, 3, 4, 5];
console.log(arr.slice(1, 3)); // [2, 3](从索引1到2)
console.log(arr.slice(-2));   // [4, 5](最后两个)

// join:数组 → 字符串
console.log(['a', 'b', 'c'].join(',')); // "a,b,c"
console.log(['a', 'b', 'c'].join(''));  // "abc"

// sort:排序(会改变原数组!)
let nums = [3, 1, 4, 1, 5];
nums.sort((a, b) => a - b);  // 升序
console.log(nums); // [1, 1, 3, 4, 5]

// reverse:反转(会改变原数组!)
nums.reverse();
console.log(nums); // [5, 4, 3, 1, 1]

解构赋值(ES6)

let arr = ['Alice', 'Bob', 'Charlie'];

// 基本解构
let [first, second] = arr;
console.log(first);  // "Alice"
console.log(second); // "Bob"

// 跳过元素
let [a, , c] = arr;
console.log(c); // "Charlie"

// 默认值
let [x, y, z, w = 'default'] = arr;
console.log(w); // "default"

// 剩余运算符
let [head, ...tail] = arr;
console.log(head); // "Alice"
console.log(tail); // ["Bob", "Charlie"]

对象(Object)—— 重点

定义方式

// 字面量(推荐)
let person = {
  name: 'Alice',
  age: 25,
  isStudent: true
};

// 构造函数
let person2 = new Object();
person2.name = 'Bob';
person2.age = 30;

属性的访问与修改

let person = {
  name: 'Alice',
  age: 25,
  'favorite-color': 'blue'  // 属性名包含特殊字符时,必须用字符串
};

// 点符号(属性名必须是有效的标识符)
console.log(person.name);   // "Alice"
person.age = 26;            // 修改属性
person.city = 'Beijing';    // 添加新属性

// 方括号(属性名可以是变量或特殊字符)
console.log(person['name']);        // "Alice"
console.log(person['favorite-color']); // "blue"

let key = 'age';
console.log(person[key]);  // 26(使用变量访问)

// 删除属性
delete person.city;
console.log(person.city); // undefined

对象方法

对象可以包含函数作为方法。

let person = {
  name: 'Alice',
  age: 25,
  greet: function() {
    console.log('Hello, I am ' + this.name);
  },
  // ES6 简写
  sayAge() {
    console.log('I am ' + this.age + ' years old.');
  }
};

person.greet();  // Hello, I am Alice
person.sayAge(); // I am 25 years old.

对象简写(ES6)

let name = 'Alice';
let age = 25;

// 属性名和变量名相同时可以简写
let person = { name, age };

// 方法也可以简写
let obj = {
  hello() {
    console.log('Hello!');
  }
};

解构赋值(ES6)

let person = { name: 'Alice', age: 25, city: 'Beijing' };

// 基本解构
let { name, age } = person;
console.log(name); // "Alice"
console.log(age);  // 25

// 重命名
let { name: fullName } = person;
console.log(fullName); // "Alice"

// 默认值
let { name, country = 'China' } = person;
console.log(country); // "China"

// 嵌套解构
let user = {
  name: 'Bob',
  address: {
    city: 'Shanghai',
    zip: '200000'
  }
};
let { address: { city } } = user;
console.log(city); // "Shanghai"

展开运算符(ES6)

let person = { name: 'Alice', age: 25 };

// 复制对象(浅拷贝)
let copy = { ...person };
console.log(copy); // { name: 'Alice', age: 25 }

// 合并对象
let info = { ...person, city: 'Beijing', age: 26 };
console.log(info); // { name: 'Alice', age: 26, city: 'Beijing' }

常用静态方法

let obj = { a: 1, b: 2, c: 3 };

// 获取所有键
console.log(Object.keys(obj));    // ["a", "b", "c"]

// 获取所有值
console.log(Object.values(obj));  // [1, 2, 3]

// 获取所有键值对
console.log(Object.entries(obj)); // [["a", 1], ["b", 2], ["c", 3]]

// 遍历对象
Object.entries(obj).forEach(([key, value]) => {
  console.log(key, value);
});

// 判断是否有某个属性
console.log('a' in obj);         // true
console.log(obj.hasOwnProperty('a')); // true

类型检测

// typeof:检测原始类型(有坑!)
console.log(typeof 'hello');  // "string"
console.log(typeof 123);      // "number"
console.log(typeof true);     // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof Symbol());  // "symbol"
console.log(typeof 123n);     // "bigint"
console.log(typeof null);      // "object"(历史 bug!)
console.log(typeof []);       // "object"(数组也是对象)
console.log(typeof {});       // "object"
console.log(typeof function() {}); // "function"

// 正确检测数组
console.log(Array.isArray([]));      // true
console.log(Array.isArray({}));      // false

// instanceof:检测对象是否是指定构造函数的实例
console.log([] instanceof Array);    // true
console.log({} instanceof Object);   // true

// Object.prototype.toString.call():最准确的类型检测
console.log(Object.prototype.toString.call(''));    // "[object String]"
console.log(Object.prototype.toString.call([]));    // "[object Array]"
console.log(Object.prototype.toString.call(null));  // "[object Null]"
console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]"

原始类型 vs 引用类型

值传递 vs 引用传递

// 原始类型:按值传递(复制值)
let a = 10;
let b = a;    // b 得到 a 的一个副本
b = 20;
console.log(a); // 10(a 不受影响)
console.log(b); // 20

// 引用类型:按引用传递(复制引用)
let arr1 = [1, 2, 3];
let arr2 = arr1;    // arr2 和 arr1 指向同一个数组
arr2.push(4);
console.log(arr1);  // [1, 2, 3, 4](arr1 也被影响了!)
console.log(arr2);  // [1, 2, 3, 4]

// 对象同理
let obj1 = { name: 'Alice' };
let obj2 = obj1;
obj2.name = 'Bob';
console.log(obj1.name); // "Bob"(obj1 也被影响了!)

如何正确复制引用类型

// 数组:使用 slice() 或展开运算符(浅拷贝)
let arr1 = [1, 2, 3];
let arr2 = [...arr1];  // 或 arr1.slice()
arr2.push(4);
console.log(arr1); // [1, 2, 3](不受影响)
console.log(arr2); // [1, 2, 3, 4]

// 对象:使用展开运算符或 Object.assign()(浅拷贝)
let obj1 = { name: 'Alice', age: 25 };
let obj2 = { ...obj1 };  // 或 Object.assign({}, obj1)
obj2.name = 'Bob';
console.log(obj1.name); // "Alice"(不受影响)

注意:以上方法都是浅拷贝,如果对象或数组嵌套了其他引用类型,嵌套部分仍然是引用传递。


综合示例

// 学生信息处理
let students = [
  { name: 'Alice', scores: [85, 90, 78] },
  { name: 'Bob', scores: [75, 88, 92] },
  { name: 'Charlie', scores: [95, 87, 91] }
];

// 计算每个学生的平均分
let result = students.map(student => {
  let avg = student.scores.reduce((sum, s) => sum + s, 0) / student.scores.length;
  return {
    name: student.name,
    average: Math.round(avg * 100) / 100
  };
});

console.log(result);
// [
//   { name: 'Alice', average: 84.33 },
//   { name: 'Bob', average: 85 },
//   { name: 'Charlie', average: 91 }
// ]

// 找出平均分最高的学生
let topStudent = result.reduce((top, cur) => cur.average > top.average ? cur : top);
console.log(`Top student: ${topStudent.name} (${topStudent.average})`);
// Top student: Charlie (91)