likes
comments
collection
share

JS中的数据类型

作者站长头像
站长
· 阅读数 50

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
UndefinedN/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.toPrimitiveSymbol.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 只存储值,且每个值必须是唯一的。
  • 迭代顺序

    • MapSet 都保留插入顺序,迭代时顺序与插入顺序一致。
  • 方法

    • Map 有更多操作键值对的方法,如 getset
    • Set 专注于值的集合操作,如 adddelete
  • 使用 Map:当需要存储键值对并且需要通过键快速查找对应值时,使用 Map
  • 使用 Set:当需要存储一组唯一值,并且需要快速检查值是否存在时,使用 Set

3. 存储和操作上的差别

3.1 基本类型

let a = 10;
let b = a; // 赋值操作
b = 20;
console.log(a); // 10值

直接存储在变量访问的位置。因为它们的大小固定(例如,一个数值类型的变量在内存中占用固定的字节数),所以在栈(Stack)中分配存储空间。访问和赋值操作是按值传递的。

a的值为一个基本类型,是存储在栈中,将a的值赋给b,虽然两个变量的值相等,但是两个变量保存了两个不同的内存地址

下图演示了基本类型赋值的过程:

JS中的数据类型

3.2 引用类型

var obj1 = {}
var obj2 = obj1;
obj2.name = "xxx";
console.log(obj1.name); // xxx

实际的数据存储在堆(Heap)中,变量本身中保存一个指向中数据的引用(内存地址),这个地址指向堆中的实际对象,访问和赋值操作是按引用传递的。

obj1是一个引用类型,在赋值操作过程汇总,实际是将堆内存对象内存的引用地址复制了一份给了obj2,实际上他们共同指向了同一个堆内存对象,所以更改obj2会对obj1产生影响。

下图演示这个引用类型赋值过程

JS中的数据类型

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 未发生变化。

为什么字符串是不可变的?

字符串的不可变性带来了一些好处:

  1. 性能优化:由于字符串是不可变的,JavaScript 引擎可以在内存中安全地共享和缓存字符串的实例,这样可以减少内存消耗和提升性能。
  2. 安全性:不可变的数据结构在多线程环境中使用时是线程安全的,因为你不需要担心其他线程会修改你的数据。虽然 JavaScript 本身是单线程的,但不可变性依然能防止意外的修改。
  3. 简化调试:不可变的数据使得代码更容易理解和调试,因为你不用担心对象在不同的地方被意外地修改。

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));

但是这种方式存在弊端,会忽略undefinedsymbol函数

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
评论
请登录