JS中的数据类型
1. 基本类型
基本类型主要有以下7种:
1.1. Number
数值最常见的整数类型格式则为十进制,还可以设置八进制(零开头)、十六进制(0x开头)
let intNum = 55 // 10进制的55
let num1 = 070 // 8进制的56
let hexNum1 = 0xA //16进制的10
浮点类型则在数值汇总必须包含小数点,还可通过科学计数法表示
let floatNum1 = 1.1;
let floatNum2 = 0.1;
let floatNum3 = .1; // 有效,但不推荐
let floatNum = 3.125e7; // 等于 31250000
在数值类型中,存在一个特殊数值NaN
,意为“不是数值”,用于表示本来要返回数值的操作失败了(而不是抛出错误)
console.log(0/0); // NaN
console.log(-0/+0); // NaN
1.2 String
字符串可以使用双引号(")、单引号(')或反引号(`)表示
let firstName = "John";
let lastName = 'Jacob';
let lastName = `Jingleheimerschmidt`
字符串是不可变的,意思是一旦创建,它们的值就不能变了
let lang = "Java";
lang = lang + "Script"; // 先销毁再创建
1.3 Boolean
Boolean
(布尔值)类型有两个字面值: true
和false
通过Boolean
可以将其他类型的数据转化成布尔值
规则如下:
数据类型 | 转换为 true 的值 | 转换为 false 的值 |
---|---|---|
String | 非空字符串 | "" |
Number | 非零数值(包括无穷值) | 0、NaN |
Object | 任意对象 | Object |
Undefined | N/A (不存在) | undefined |
1.4 Undefined
Undefined
类型只有一个值,就是特殊值 undefined
。当使用 var
或 let
声明了变量但没有初始化时,就相当于给变量赋予了 undefined
值
let message;
console.log(message == undefined); // true
包含undefined
值的变量跟未定义变量是有区别的
let message; // 这个变量被声明了,只是值为 undefined
console.log(message); // "undefined"
console.log(age); // 没有声明过这个变量,报错
1.5 null
Null
类型同样只有一个值,即特殊值 null
逻辑上讲, null 值表示一个空对象指针,这也是给typeof
传一个 null
会返回 "object"
的原因
let car = null;
console.log(typeof car); // "object"
undefined
值是由 null
值派生而来
console.log(null == undefined); // true
只要变量要保存对象,而当时又没有那个对象可保存,就可用 null
来填充该变量
1.6 Symbol
Symbol
是 ES6(ECMAScript 2015)中引入的一种新的基本数据类型。与其他基本数据类型(如字符串、数字、布尔值等)不同,Symbol
的主要用途是生成唯一的标识符。每个从 Symbol
创建的值都是唯一的,即使使用相同的描述符来创建 Symbol
,它们之间也是不同的。
- 使用场景
- 避免对象属性名冲突:使用 Symbol 作为对象的属性键,可以保证每个属性都是唯一的,避免了属性名冲突。
- 定义常量:使用 Symbol 可以定义唯一的常量,避免不同模块之间的名称冲突。
- 实现迭代器:通过
Symbol.iterator
可以定义对象的默认迭代器,使对象可以被for...of
循环遍历。 - 自定义语言行为:通过内置 Symbol,可以自定义对象在某些语言操作中的行为,如
Symbol.toPrimitive
、Symbol.hasInstance
等。
Symbol
在 JavaScript 中提供了一种独特且强大的方式来处理对象属性,尤其是在需要避免名称冲突和实现高级功能时,具有重要意义。
//1. 基本用法
//你可以通过调用 `Symbol` 函数来创建一个 Symbol。
//这个函数可以接收一个可选的字符串参数,作为对 Symbol 的描述(这在调试时非常有用)
const sym1 = Symbol();
const sym2 = Symbol('description');
console.log(sym1); // Symbol()
console.log(sym2); // Symbol(description)
//2. Symbol 的唯一性
//即使两个 Symbol 使用相同的描述,它们也不相等。
const sym1 = Symbol('description');
const sym2 = Symbol('description');
console.log(sym1 === sym2); // false
//3. Symbol 作为对象属性的键
//Symbol 常用于对象属性的键,以避免属性冲突,因为每个 Symbol 都是唯一的。
const sym = Symbol('key');
const obj = {};
obj[sym] = 'value';
console.log(obj[sym]); // "value"
console.log(obj); // { [Symbol(key)]: 'value' }
//4. 使用 Symbol 定义常量
//Symbol 常用于定义常量,避免不同模块之间的名称冲突。
const COLOR_RED = Symbol('red');
const COLOR_GREEN = Symbol('green');
function getColor(color) {
switch (color) {
case COLOR_RED:
return 'red';
case COLOR_GREEN:
return 'green';
default:
return 'unknown color';
}
}
console.log(getColor(COLOR_RED)); // "red"
console.log(getColor(COLOR_GREEN)); // "green"
//5. 内置 Symbol
//JavaScript 提供了一些内置的 Symbol,这些 Symbol 用于语言内部的一些特性。
//这些内置 Symbol 定义在 `Symbol` 对象上,如 `Symbol.iterator`、`Symbol.hasInstance` 等。
//`Symbol.iterator` 用于定义对象的默认迭代器。
const iterable = {
[Symbol.iterator]() {
let step = 0;
const iterator = {
next() {
step++;
if (step === 1) {
return { value: 'hello', done: false };
} else if (step === 2) {
return { value: 'world', done: false };
}
return { value: undefined, done: true };
}
};
return iterator;
}
};
for (const value of iterable) {
console.log(value); // "hello", "world"
}
//6. Symbol.for 和 Symbol.keyFor
//`Symbol.for` 用于在全局注册表中创建或获取一个 Symbol。
//每次调用 `Symbol.for` 时,如果提供的键已经存在,则返回已经存在的 Symbol;
//否则,创建一个新的 Symbol。
const globalSym1 = Symbol.for('global');
const globalSym2 = Symbol.for('global');
console.log(globalSym1 === globalSym2); // true
//`Symbol.keyFor` 用于返回一个从全局注册表中找到的 Symbol 的键。
const globalSym = Symbol.for('global');
console.log(Symbol.keyFor(globalSym)); // "global"
//7. Symbol 的不可枚举性
//Symbol 属性默认是不可枚举的,即不会出现在 `for...in` 循环中,
//也不会被 `Object.keys`、`Object.getOwnPropertyNames` 返回。
//但是,它们可以通过 `Object.getOwnPropertySymbols` 方法获取。
const sym = Symbol('key');
const obj = { [sym]: 'value' };
for (const key in obj) {
console.log(key); // 无输出
}
console.log(Object.keys(obj)); // []
console.log(Object.getOwnPropertyNames(obj)); // []
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(key)]
1.7 BigInt(ES2020 引入)
表示任意精度的整数。
let big = BigInt(123456789012345678901234567890);
2. 引用类型
复杂类型统称为Object
,引用类型有以下几种:
2.1 Object
创建object
常用方式为对象字面量表示法,属性名可以是字符串或数值
let person = {
name: "Nicholas",
"age": 29,
5: true
};
2.2 Array
JavaScript
数组是一组有序的数据,但跟其他语言不同的是,数组中每个槽位可以存储任意类型的数据。并且,数组也是动态大小的,会随着数据添加而自动增长
let colors = ["red", 2, {age: 20 }]
colors.push(2)
2.3 Function
函数实际上是对象,每个函数都是 Function
类型的实例,而 Function
也有属性和方法,跟其他引用类型一样
函数存在三种常见的表达方式:
// 1. 函数声明
function sum (num1, num2) {
return num1 + num2;
}
// 2. 函数表达式
let sum = function(num1, num2) {
return num1 + num2;
};
// 3. 箭头函数
let sum = (num1, num2) => {
return num1 + num2;
};
2.4 Date
表示日期和时间。
let date = new Date();
2.5 RegExp
表示正则表达式。
let regex = /ab+c/;
2.6 Map
表示键值对集合,键可以是任意类型。
let map = new Map();
map.set('key', 'value');
2.7 Set
表示值的集合,值必须是唯一的。
let set = new Set();
set.add(1);
比较Map和Set
-
存储内容:
Map
存储键值对,每个键对应一个值。Set
存储唯一的值,没有重复值。
-
键与值:
Map
的键和值可以是任意类型。Set
只存储值,且每个值必须是唯一的。
-
迭代顺序:
Map
和Set
都保留插入顺序,迭代时顺序与插入顺序一致。
-
方法:
Map
有更多操作键值对的方法,如get
和set
。Set
专注于值的集合操作,如add
和delete
。
- 使用
Map
:当需要存储键值对并且需要通过键快速查找对应值时,使用Map
。- 使用
Set
:当需要存储一组唯一值,并且需要快速检查值是否存在时,使用Set
。
3. 存储和操作上的差别
3.1 基本类型
let a = 10;
let b = a; // 赋值操作
b = 20;
console.log(a); // 10值
值
直接存储在变量访问的位置。因为它们的大小固定
(例如,一个数值类型
的变量在内存中占用固定
的字节数),所以在栈(Stack)
中分配存储空间。访问和赋值操作是按值传递
的。
a
的值为一个基本类型,是存储在栈中,将a
的值赋给b
,虽然两个变量的值相等,但是两个变量保存了两个不同的内存地址
下图演示了基本类型赋值的过程:
3.2 引用类型
var obj1 = {}
var obj2 = obj1;
obj2.name = "xxx";
console.log(obj1.name); // xxx
实际的数据
存储在堆(Heap)
中,变量本身
在栈
中保存一个指向堆
中数据的引用
(内存地址),这个地址指向堆中的实际对象,访问和赋值操作是按引用传递
的。
obj1
是一个引用类型,在赋值操作过程汇总,实际是将堆内存对象
在栈
内存的引用地址
复制了一份给了obj2
,实际上他们共同指向了同一个堆内存对象,所以更改obj2
会对obj1
产生影响。
下图演示这个引用类型赋值过程
3.3 小结
3.3.1. 栈和堆的区别:
栈(Stack)
内存用于存储基本类型
的变量以及函数调用。栈内存空间小
且速度快
。堆(Heap)
内存用于存储引用类型
的实际对象。堆内存空间大
但速度相对较慢
。
3.3.2. 按值传递 vs 按引用传递:
-
基本类型
按值传递:复制变量的值。let a = 10; let b = a; b = 20; console.log(a); // 10
-
引用类型
按引用传递:复制变量的引用(地址)。let obj1 = { name: "Alice" }; let obj2 = obj1; obj2.name = "Bob"; console.log(obj1.name); // Bob
3.3.3. 不可变性(Immutability):
基本类型
是不可变的,一旦创建,值就不能引用类型
在 JavaScript 中是可变的
3.3.4 总结
JavaScript
中的基本类型
和引用类型
在存储
和操作
上有显著差异。基本类型
存储在栈
中,按值传递
,大小固定
且速度快
;引用类型
存储在堆
中,按引用传递
,大小可变
且速度相对较慢
。这些差异在实际开发中会影响变量的使用
和性能优化策略
。
4. 基本类型的不可变性(Immutability)
Q:字符串
不是也可以增删改查的吗?为什么说是不可变的?
A:在 JavaScript 中,字符串是不可变的(immutable),这意味着一旦创建,字符串的内容就无法被改变。尽管可以通过各种方法查询和处理字符串,但这些方法都不会修改基本字符串,而是返回一个新的字符串。
不可变性的解释
修改字符串
trim()、trimLeft()、trimRight()
删除前、后或前后所有空格符,再返回新的字符串
let stringValue = " hello world ";
let trimmedStringValue = stringValue.trim();
console.log(stringValue); // " hello world "
console.log(trimmedStringValue); // "hello world"
repeat()
接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果
let stringValue = "na ";
let copyResult = stringValue.repeat(2) // na na
padEnd()
复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件
let stringValue = "foo";
console.log(stringValue.padStart(6)); // " foo"
console.log(stringValue.padStart(9, ".")); // "......foo"
toLowerCase()、 toUpperCase()
大小写转化
let stringValue = "hello world";
console.log(stringValue.toUpperCase()); // "HELLO WORLD"
console.log(stringValue.toLowerCase()); // "hello world"
replace()
let str = "Hello, World!";
let newStr = str.replace("World", "JavaScript");
console.log(str); // "Hello, World!"
console.log(newStr); // "Hello, JavaScript!"
在这个例子中,replace
方法返回了一个新字符串,而基本字符串 str
并没有改变。
任何试图修改字符串的方法都会返回一个新字符串,而不会改变基本字符串。
增加字符串
同样,连接字符串(拼接字符串)也会返回一个新字符串,而不会修改基本字符串。除了常用+
以及${}
进行字符串拼接之外,还可通过concat
let str1 = "Hello";
let str2 = str1 + ", World!";
let str3 = stringValue.concat(", world!");
console.log(str1); // "Hello"
console.log(str2); // "Hello, World!"
console.log(str3); // "Hello, World!"
在这个例子中,通过 +
操作符连接字符串,结果是一个新的字符串,而 str1
仍然是原来的值。
删除字符串
尝试通过切割字符串来删除其部分内容时,也会返回一个新字符串。 常见的有:
- slice(startIndex, endIndex)
- substr(startIndex, length)
- substring(startIndex, endIndex)
方法 | 支持负索引 | startIndex > endIndex ==> 交换索引 |
---|---|---|
slice(startIndex, endIndex) | ✔️ | ✖️ |
substring(startIndex, endIndex) | ✖️ | ✔️ |
substr(startIndex, length) | ✔️ | ✖️ |
let str = "Hello, World!";
let newStr = str.slice(0, 5);
let newStr2 = str.substring(0, 5)
let newStr3 = str.substr(0, 5)
console.log(str); // "Hello, World!"
console.log(newStr); // "Hello"
console.log(newStr2);// "Hello"
console.log(newStr3);// "Hello"
在这个例子中,slice
方法返回一个新字符串,而原始字符串 str
未发生变化。
为什么字符串是不可变的?
字符串的不可变性带来了一些好处:
- 性能优化:由于字符串是不可变的,JavaScript 引擎可以在内存中安全地共享和缓存字符串的实例,这样可以减少内存消耗和提升性能。
- 安全性:不可变的数据结构在多线程环境中使用时是线程安全的,因为你不需要担心其他线程会修改你的数据。虽然 JavaScript 本身是单线程的,但不可变性依然能防止意外的修改。
- 简化调试:不可变的数据使得代码更容易理解和调试,因为你不用担心对象在不同的地方被意外地修改。
5. 引用类型的可变性(mutability)
Q: 引用类型可以改变?
A: 是的,引用类型
在 JavaScript 中是可变的
(mutable)。这意味着你可以改变它们的内容
,而不会改变它们的引用
(即指向它们的变量的地址)。
5.1 对象
对于对象,你可以添加、修改或删除其属性。
let obj = { name: "Alice" };
console.log(obj); // { name: "Alice" }
// 修改属性
obj.name = "Bob";
console.log(obj); // { name: "Bob" }
// 添加属性
obj.age = 25;
console.log(obj); // { name: "Bob", age: 25 }
// 删除属性
delete obj.name;
console.log(obj); // { age: 25 }
5.2 数组
对于数组,你可以修改数组的元素、添加新元素或删除元素。
下面对数组常用的操作方法做一个归纳:
5.2.1 增
5.2.1.1 push()
push()
方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度
let colors = []; // 创建一个数组
let count = colors.push("red", "green"); // 推入两项
console.log(count) // 2
5.2.1.2 unshift()
unshift()
在数组开头添加任意多个值,然后返回新的数组长度
let colors = new Array(); // 创建一个数组
let count = colors.unshift("red", "green"); // 从数组开头推入两项
alert(count); // 2
5.2.1.3 splice()
splice()
传入三个参数,分别是开始位置、0(要删除的元素数量)、插入的元素,返回空数组
let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 0, "yellow", "orange")
console.log(colors) // red,yellow,orange,green,blue
console.log(removed) // []
5.2.1.4 concat()
首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组,不会影响原始数组
let colors = ["red", "green", "blue"];
let colors2 = colors.concat("yellow", ["black", "brown"]);
console.log(colors); // ["red", "green","blue"]
console.log(colors2); // ["red", "green", "blue", "yellow", "black", "brown"]
5.2.2 删
5.2.2.1 pop()
pop()
方法用于删除数组的最后一项,同时减少数组的length
值,返回被删除的项
let colors = ["red", "green"]
let item = colors.pop(); // 取得最后一项
console.log(item) // green
console.log(colors.length) // 1
5.2.2.2 shift()
shift()
方法用于删除数组的第一项,同时减少数组的length
值,返回被删除的项
let colors = ["red", "green"]
let item = colors.shift(); // 取得第一项
console.log(item) // red
console.log(colors.length) // 1
5.2.2.3 splice()
传入两个参数,分别是开始位置,删除元素的数量,返回包含删除元素的数组
let colors = ["red", "green", "blue"];
let removed = colors.splice(0,1); // 删除第一项
console.log(colors); // green,blue
console.log(removed); // red,只有一个元素的数组
4.2.2.4 slice()
slice(start, end)
用于从数组或字符串中提取部分内容而不修改原始数据。
- start可选,开始提取的位置(包含该位置)。如果省略,则从索引0开始。
- end可选,结束提取的位置(包含该位置)。如果省略,则直到数组的末尾。
let colors = ["red", "green", "blue", "yellow", "purple"];
let colors2 = colors.slice(1);
let colors3 = colors.slice(1, 4);
console.log(colors) // red,green,blue,yellow,purple
concole.log(colors2); // green,blue,yellow,purple
concole.log(colors3); // green,blue,yellow
5.2.3 改
5.2.3.1 splice()
传入三个参数,分别是开始位置,要删除元素的数量,要插入的任意多个元素,返回删除元素的数组,对原数组产生影响
let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 1, "red", "purple"); // 插入两个值,删除一个元素
console.log(colors); // red,red,purple,blue
console.log(removed); // green,只有一个元素的数组
5.2.4 查
4.2.4.1 indexOf()
返回要查找的元素在数组中的位置,如果没找到则返回 -1
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.indexOf(4) // 3
5.2.4.2 includes()
返回要查找的元素在数组中的位置,找到返回true
,否则false
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.includes(4) // true
5.2.4.3 find()
返回第一个匹配的元素
const people = [
{
name: "Matt",
age: 27
},
{
name: "Nicholas",
age: 29
}
];
people.find((element, index, array) => element.age < 28)
// {name: "Matt", age: 27}
5.2.5 排序
5.2.5.1 reverse()
顾名思义,将数组元素方向反转
let values = [1, 2, 3, 4, 5];
values.reverse();
alert(values); // 5,4,3,2,1
5.2.5.2 sort()
sort()方法接受一个比较函数,用于判断哪个值应该排在前面
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
let values = [0, 1, 5, 10, 15];
values.sort(compare);
alert(values); // 0,1,5,10,15
6 注意点
6.1 引用传递
当你将一个引用类型赋值给另一个变量时,实际上是复制了其引用。两个变量指向同一个对象或数组,因此对其中一个变量的修改会影响另一个变量。
let obj1 = { name: "Alice" };
let obj2 = obj1; // obj2 现在引用同一个对象
obj2.name = "Bob";
console.log(obj1.name); // "Bob" 因为 obj1 和 obj2 引用同一个对象
6.2. 比较引用:
对于引用类型的变量比较的是引用,而不是值。
let obj1 = { name: "Alice" };
let obj2 = { name: "Alice" };
console.log(obj1 === obj2); // false 因为它们引用不同的对象
6.3. 浅拷贝 vs 深拷贝:
- 浅拷贝只复制对象的引用,不复制对象本身。如果对象中包含其他对象,浅拷贝只复制引用,不复制嵌套的对象。
- 深拷贝会复制对象及其所有嵌套对象,创建一个独立的副本。
浅拷贝简单实现
下面简单实现一个浅拷贝
function shallowClone(obj) {
const newObj = {};
for(let prop in obj) {
if(obj.hasOwnProperty(prop)){
newObj[prop] = obj[prop];
}
}
return newObj;
}
浅拷贝的常见使用
1. Object.assign
var obj = {
age: 18,
nature: ['smart', 'good'],
names: {
name1: 'fx',
name2: 'xka'
},
love: function () {
console.log('fx is a great girl')
}
}
var newObj = Object.assign({}, fxObj);
2. slice()
slice
是 JavaScript 中用于数组和字符串操作的常用方法。它创建一个新数组或字符串的子集,而不修改原始数组或字符串。
arr.slice([begin[, end]])
//begin:可选。起始索引(包括该索引)。如果为负数,表示从数组末尾开始的第几个元素。默认值为 `0`。
//end:可选。结束索引(不包括该索引)。如果为负数,表示从数组末尾开始的第几个元素。默认值为数组的长度。
const array = [1, 2, 3, 4, 5];
console.log(array.slice()); // [1, 2, 3, 4, 5] - 返回整个数组
console.log(array.slice(1, 3)); // [2, 3] - 返回从索引1到索引3之间的元素(不包括索引3)
console.log(array.slice(2)); // [3, 4, 5] - 返回从索引2开始的所有元素
console.log(array.slice(-2)); // [4, 5] - 返回最后两个元素
console.log(array.slice(1, -1)); // [2, 3, 4] - 返回从索引1到倒数第二个元素之间的元素
3. concat()
const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.concat()
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]
4. 拓展运算符
const fxArr = ["One", "Two", "Three"]
const fxArrs = [...fxArr]
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]
深拷贝的常见方式
1. _.cloneDeep()
const _ = require('lodash');
const obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false
2. jQuery.extend()
const $ = require('jquery');
const obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
const obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false
3. JSON.stringify()
const obj2=JSON.parse(JSON.stringify(obj1));
但是这种方式存在弊端,会忽略undefined
、symbol
和函数
const obj = {
name: 'A',
name1: undefined,
name3: function() {},
name4: Symbol('A')
}
const obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // {name: "A"}
4. 循环递归
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 可能是对象或者普通的值 如果是函数的话是不需要深拷贝
if (typeof obj !== "object") return obj;
// 是对象的话就要进行深拷贝
if (hash.get(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
// 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
面试题
转载自:https://juejin.cn/post/7374720837470044179