揭秘JavaScript数据类型:那些你不知道的知识

156 阅读9分钟

JavaScript的数据类型让我们能够在程序中表达各种各样的信息。在这篇文章中,我们将深入分析 undefined, object, symbol和bigint。特别关注一些不太为人所知的应用场景和技巧。

一、基础数据类型一瞥

在8大基本数据类型number, string, boolean, null, undefined, object, symbol和bigint中,symbolbigIntobject是最值得讨论的类型

二、深入分析

1. Symbol:独一无二的标识符

Symbol是一种特殊的数据类型,用于创建唯一的标识符。它的应用场景包括:

  • 防止属性名冲突: 当我们在一个对象中使用Symbol作为属性名时,可以确保这个属性名不会与其他属性名冲突,因为每个Symbol都是唯一的。
  • 创建私有属性: 通过使用Symbol作为属性名,我们可以为对象创建私有属性,因为这些属性不会被常规的遍历方法(如for...inObject.keys()等)访问到。

示例代码:

const privateProp = Symbol('privateProp');
const obj = {
  [privateProp]: 'This is a private property',
  publicProp: 'This is a public property',
};

console.log(obj[privateProp]); // 输出:'This is a private property'
console.log(obj.publicProp); // 输出:'This is a public property'
  • 迭代器(Iterator): Symbol还可以用于创建对象的迭代器。通过在对象上定义一个Symbol.iterator属性,我们可以自定义对象的迭代行为(比如for...of)

示例代码:

const objWithIterator = {
  data: [1, 2, 3],
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        return index < this.data.length
          ? { value: this.data[index++], done: false }
          : { value: undefined, done: true };
      },
    };
  },
};

for (const value of objWithIterator) {
  console.log(value); // 输出:1,2,3
}

2. Undefined:未定义的值

Undefined表示一个变量已经被声明,但还没有被赋值。在某些情况下,我们可以利用这个特性来实现特定的功能:

  • 判断一个变量是否已经赋值:通过检查变量的值是否为undefined,我们可以判断这个变量是否已经赋值。

示例代码:

let x;
console.log(x === undefined); // 输出:true

x = 42;
console.log(x === undefined); // 输出:false

3. BigInt:大整数

BigInt是一种新的数据类型,用于表示大整数。它可以表示任意大小的整数,不受Number类型的最大安全整数(Number.MAX_SAFE_INTEGER)限制。BigInt的应用场景包括:

  • 处理大整数:当我们需要处理超过Number类型最大安全整数范围的整数时,可以使用BigInt

示例代码:

const bigInt = 1234567890123456789012345678901234567890n;
console.log(bigInt); // 输出:1234567890123456789012345678901234567890n

当使用BigInt来存储和读取后端返回的大整数类型时,需要注意以下几个陷阱:

3.1. JSON解析问题:

当后端通过JSON返回大整数时,需要注意JSON的解析器可能无法正确处理BigInt。因为JSON.stringify()JSON.parse()方法并不支持BigInt类型。如果你直接使用这两个方法序列化和解析BigInt,它们会抛出TypeError

解决方案:在从后端接收到大整数数据时,可以将其作为字符串传输。然后,在前端接收到数据后,使用BigInt()构造函数将字符串转换为BigInt类型。

示例代码:

// 后端返回的JSON数据
const jsonString = '{"largeInteger": "1234567890123456789012345678901234567890"}';

// 在前端解析JSON数据
const parsedData = JSON.parse(jsonString);
const bigInt = BigInt(parsedData.largeInteger);
console.log(bigInt); // 输出:1234567890123456789012345678901234567890n

一个简单的包含大整数的JSON解析示例

// 自定义reviver函数
function bigIntReviver(key, value) {
  // 检测值是否为不带n结尾的大整数字符串
  if (typeof value === 'string' && /^\d+$/.test(value)) {
    const numberValue = Number(value);
    if (numberValue > Number.MAX_SAFE_INTEGER) {
      return BigInt(value);
    }
    return numberValue;
  }
  
  // 检测值是否为超过Number.MAX_SAFE_INTEGER的整数
  if (typeof value === 'number' && value > Number.MAX_SAFE_INTEGER) {
    return BigInt(value);
  }

  return value;
}

// 使用自定义的bigIntReviver函数解析JSON
function parseBigIntJSON(jsonString) {
  return JSON.parse(jsonString, bigIntReviver);
}

// 示例
const jsonString = '{"largeInteger": "1234567890123456789012345678901234567890", "regularNumber": 42, "text": "hello"}';
const parsedData = parseBigIntJSON(jsonString);

console.log(parsedData.largeInteger); // 输出:1234567890123456789012345678901234567890n
console.log(parsedData.regularNumber); // 输出:42
console.log(parsedData.text); // 输出:"hello"

3.2. 与Number类型的运算:

BigInt类型不能与Number类型直接进行运算,否则会抛出TypeError。在需要对BigIntNumber类型的值进行运算时,需要先将Number类型的值转换为BigInt类型,然后再进行运算。

示例代码:

const num = 42;
const bigInt = 1234567890123456789012345678901234567890n;

// 以下运算会抛出TypeError
// const result = bigInt + num;

// 将Number类型转换为BigInt类型后再进行运算
const result = bigInt + BigInt(num);
console.log(result); // 输出:1234567890123456789012345678901234567932n

3.3. 浏览器兼容性问题:

虽然大部分现代浏览器都已经支持BigInt类型,但仍然存在一些旧版本或不支持BigInt的浏览器。在使用BigInt之前,需要检查浏览器是否支持BigInt,以确保代码的正常运行。

示例代码:

if (typeof BigInt !== 'undefined') {
  // 浏览器支持BigInt,可以放心使用
  const bigInt = 1234567890123456789012345678901234567890n;
} else {
  // 浏览器不支持BigInt,需要考虑降级方案或提示用户升级浏览器
  console.warn('BigInt is not supported in this browser.');
}

总结:

在使用BigInt来存储和读取后端的大整数类型时,需要注意JSON解析问题、与Number类型的运算问题以及浏览器兼容性问题。通过采用相应的解决方案,我们可以充分利用BigInt的优势,避免处理大整数时可能遇到的问题。

4:Object类型

JavaScript中的对象是一种引用类型,它的值是由键值对组成的无序集合。在本节中,我们将深入分析Object类型的以下几个方面:

4.1. 对象创建

在JavaScript中,我们可以使用多种方法创建对象:

  • 字面量表示法:

    const obj1 = {
      key1: 'value1',
      key2: 'value2',
    };
    
  • 构造函数表示法:

    const obj2 = new Object();
    obj2.key1 = 'value1';
    obj2.key2 = 'value2';
    
  • Object.create()方法:

    const obj3 = Object.create(null);
    obj3.key1 = 'value1';
    obj3.key2 = 'value2';
    

4.2. 属性访问和修改

我们可以使用点表示法和括号表示法来访问和修改对象的属性:

  • 点表示法:

    console.log(obj1.key1); // 输出:'value1'
    obj1.key1 = 'updatedValue1';
    console.log(obj1.key1); // 输出:'updatedValue1'
    
  • 括号表示法:

    console.log(obj1['key1']); // 输出:'value1'
    obj1['key1'] = 'updatedValue1';
    console.log(obj1['key1']); // 输出:'updatedValue1'
    

4.3. 属性删除

使用delete操作符可以删除对象的属性:

delete obj1.key1;
console.log(obj1.key1); // 输出:undefined

4.4. 原型链

在JavaScript中,对象通过原型链实现继承。每个对象都有一个指向其原型的内部属性[[Prototype]],通常通过__proto__访问。当我们试图访问一个对象上不存在的属性时,JavaScript引擎会沿着原型链向上查找该属性。

const parentObj = {
  parentKey: 'parentValue',
};

const childObj = Object.create(parentObj);
console.log(childObj.parentKey); // 输出:'parentValue'

4.5. 枚举和非枚举属性

对象的属性可以是可枚举的或不可枚举的。可枚举属性会出现在for...in循环和Object.keys()方法的结果中,而不可枚举属性则不会。我们可以使用Object.defineProperty()方法来设置属性的枚举性:

const obj4 = {
  enumerableProp: 'enumerableValue',
};

Object.defineProperty(obj4, 'nonEnumerableProp', {
  value: 'nonEnumerableValue',
  enumerable: false,
});

console.log(Object.keys(obj4)); // 输出:['enumerableProp']

4.6. 深拷贝和浅拷贝

在JavaScript中,对象的赋值和传递都是按引用进行的。这意味着,当我们将一个对象赋值给另一个变量或将其作为函数参数传递时,实际上我们只是复制了对象的引用,而不是对象本身。因此,对一个对象的修改可能会影响到其他引用了该对象的变量。这就是所谓的“浅拷贝”。

为了避免这种情况,我们可以使用“深拷贝”技术创建对象的副本,从而将源对象与副本对象完全隔离。这样,对副本对象的任何修改都不会影响到源对象。

以下是几种实现深拷贝和浅拷贝的方法:

  • 浅拷贝:

    使用Object.assign()

    const objA = { key1: 'value1', key2: { subKey: 'subValue' } };
    const shallowCopyA = Object.assign({}, objA);
    

    使用展开运算符:

    const objB = { key1: 'value1', key2: { subKey: 'subValue' } };
    const shallowCopyB = { ...objB };
    
  • 深拷贝:

    使用JSON.parse()JSON.stringify()(注意:这种方法不能处理循环引用、函数和特殊数据类型,如BigInt、Symbol等):

    const objC = { key1: 'value1', key2: { subKey: 'subValue' } };
    const deepCopyC = JSON.parse(JSON.stringify(objC));
    

    使用递归方法(支持处理循环引用和多种数据类型,但可能存在性能问题):

    function deepClone(obj, hash = new WeakMap()) {
      if (obj === null || typeof obj !== 'object') {
        return obj;
      }
    
      if (hash.has(obj)) {
        return hash.get(obj);
      }
    
      const clone = Array.isArray(obj) ? [] : {};
      hash.set(obj, clone);
    
      for (const key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
          clone[key] = deepClone(obj[key], hash);
        }
      }
    
      return clone;
    }
    
    const objD = { key1: 'value1', key2: { subKey: 'subValue' } };
    const deepCopyD = deepClone(objD);
    

在本章中,我们深入分析了Object类型的创建、属性访问和修改、属性删除、原型链、属性枚举性以及深拷贝和浅拷贝。了解这些概念对于编写健壮的JavaScript代码至关重要。

三、数据转换的陷阱

在JavaScript中,我们经常需要在不同的数据类型之间进行转换。然而,在这个过程中,我们可能会遇到一些陷阱:

1. 隐式类型转换

JavaScript会在某些情况下自动进行类型转换,这可能会导致一些不符合预期的结果。

示例代码:

console.log('5' - 3); // 输出:2
console.log('5' + 3); // 输出:'53'

在上述示例中,字符串'5'和数字3在相减时会自动转换为数字,而在相加时会自动转换为字符串。为了避免这种情况,我们应该显式地进行类型转换。

2. 转换为布尔值

当我们将其他类型的数据转换为布尔值时,需要注意一些特殊情况。例如,以下值在转换为布尔值时都为false:

  • false
  • 0-0NaN
  • ''(空字符串)
  • null
  • undefined

其他所有值在转换为布尔值时都为true。

四、数据引用的陷阱

在JavaScript中,对象和数组是通过引用传递的。这意味着,当我们将一个对象或数组赋值给另一个变量时,它们实际上指向的是同一个内存地址。这可能会导致一些意想不到的问题。

示例代码:

const obj1 = { a: 1, b: 2 };
const obj2 = obj1;

obj2.a = 42;
console.log(obj1.a); // 输出:42

为了避免这种情况,我们可以使用深拷贝来复制对象和数组。在JavaScript中,可以使用JSON.parse(JSON.stringify(obj))(不支持处理循环引用)或第三方库(如lodash的_.cloneDeep()方法)实现深拷贝。

五、Immutable(不可变对象)

上文提到了数据引用的陷阱,为了解决上述问题,不可变对象的概念被提出。

1. Immutable

如果只谈论Javascript本身的不可变对象,那么不可变对象是一种特殊的对象,它的状态在创建之后不能被更改。在JavaScript中,我们可以使用Object.freeze()方法来创建不可变对象。这样,我们就可以确保对象的状态不会意外地被修改

const immutableObj = Object.freeze({ a: 1, b: 2 });

immutableObj.a = 42;
console.log(immutableObj.a); // 输出:1

// 尝试修改不可变对象会抛出TypeError(在严格模式下)

值得注意的是:通过Object.freeze()创建的对象,其深层属性是可以被修改的

const obj = { a: 1, b: { c: 2, d: 3 } }; 
const immutableObj = Object.freeze(obj); 

// 下面的操作将抛出错误,因为对象是不可变的
immutableObj.a = 10;

// 可以正常被修改
immutableObj.b.c = 20;  // obj.b.c = 20

2. Immutable性能

虽然Object.freeze()+deepClone在一定程度上已经可以实现Immutable的概念了,但是使用门槛和性能还是硬伤,于是出现了immutable.jsimmer.js,可以实现更高级的功能,使得处理不可变数据更加简洁、高效和易于理解

以下是一个immer的示例

import produce from "immer";

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

const update = (state, updates) => produce(state, draftState => {
  for (let key in updates) {
    draftState[key] = updates[key];
  }
});

const nextState = update(obj, { a: 10, b: { c: 20, d: 30 } });

console.log("Old state:", obj); // {a: 1, b: {c: 2, d: 3}}
console.log("New state:", nextState); // {a: 10, b: {c: 20, d: 30}}

3. 一个简单的immer实现

function deepCopy(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }

  const copy = Array.isArray(obj) ? [] : {};
  for (let key in obj) {
    copy[key] = deepCopy(obj[key]);
  }

  return copy;
}

function createDraft(baseState) {
  const copiedState = deepCopy(baseState);

  const handler = {
    get(target, key) {
      if (typeof target[key] === 'object' && target[key] !== null) {
        return new Proxy(target[key], handler);
      }
      return target[key];
    },
    set(target, key, value) {
      target[key] = value;
      return true;
    }
  };

  return new Proxy(copiedState, handler);
}

function produce(baseState, producer) {
  const draft = createDraft(baseState);
  producer(draft);
  return draft;
}

// 示例
const baseState = {
  a: 1,
  b: {
    c: 2,
    d: 3
  }
};

const nextState = produce(baseState, draft => {
  draft.a = 10;
  draft.b.c = 20;
});

console.log('Old state:', baseState);
console.log('New state:', nextState);

六、总结

本文特别关注了SymbolUndefinedBigInt的应用场景。我们还探讨了数据转换、数据引用方面的陷阱和技巧,以及创建和操作不可变数据结构。希望这些知识能够帮助您在实际开发中更好地使用JavaScript