JavaScript 基本类型与引用类型详解:判断、转换与复制
本文深入解析 JavaScript 类型系统,区分基本类型与引用类型的内存存储差异。详细对比 typeof 与 instanceof 的判断局限,提供基于 Object.prototype.toString 的可靠类型检测方法。阐述 + 运算符在类型转换中的双重角色,并重点讲解数组与对象的浅拷贝及递归深拷贝实现,帮助开发者避免引用类型赋值与复制时的常见陷阱。
基本类型与引用类型的内存区别
JavaScript 属于弱类型(或松散类型)语言,其数据类型主要分为基本类型和引用类型。两者的核心区别在于存储方式:基本类型拥有固定的存储空间,直接保存在栈内存中;而引用类型没有固定空间,其实际数据保存在堆内存中,栈内存仅保存一个指向堆内存中实际位置的指针。
市面上许多书籍都花费大量篇幅讨论此话题。本文旨在梳理几个关键方面,阅读前建议读者对 JavaScript 类型系统有基础了解。若对此尚不熟悉,建议先查阅相关入门书籍,再阅读本文以获得更好效果。
一、基本类型与引用类型
1. 基本类型包括:Undefined、Null、Boolean、Number、String。 2. 引用类型包括:Object、Array、Function、Date、RegExp、Error、Map、Set 等。
引用类型并未完全枚举,因为掌握上述常见类型已足够应对本文讨论的场景。其他类型使用频率较低,且部分如 Map 和 Set 并非所有浏览器都支持,故在此不做深入展开。
JavaScript 类型判断方法
二、JavaScript 类型的判断
JavaScript 提供了 typeof 和 instanceof 两个运算符用于类型判断,但二者配合并不默契,且结果常不可靠。仅在少数情况下准确,多数场景下存在偏差。具体表现如下:
代码如下:
// 靠谱的时候:
typeof 'sofish' // string
new String('sofish') instanceof String // true
// 不靠谱的时候:
typeof [] // object
typeof null // object
'sofish' instanceof String // false
这可能导致初学 JavaScript 的程序员感到困惑甚至沮丧。许多开发者在使用 jQuery 等库时已习惯其封装好的类型检测功能,觉得手动检测并不麻烦。事实上,既然“JavaScript 中一切皆对象”(注:undefined、NaN、Infinity 实为全局属性),我们可以利用对象特性来辅助检测:
代码如下:
/* 检测对象类型
* @param: obj {JavaScript Object}
* @param: type {String} 以大写开头的 JS 类型名
* @return: {Boolean}
*/
function is(obj, type) {
return Object.prototype.toString.call(obj).slice(8, -1) === type;
}
通过上述 is 函数,我们可以实现兼容性好且准确的类型判断,可直接应用于项目中。示例如下:
代码如下:
is('sofish', 'String') // true
is(null, 'Null') // true
is(new Set(), 'Set') // true
JavaScript 类型转换机制
三、JavaScript 类型的转换
在 JavaScript 中,变量(属性)的类型是可变的。最常见的是 String 与 Number 之间的转换。例如如何将 1 + '2' 变为 '12'?这需要理解 + 运算符的双重角色:它既是数学加法运算符,也是字符串连接符。因此新手常遇到使用 + 号得到非预期结果,而使用 - 号反而得到“正确”数值的情况。
代码如下:
1 + '2' // '12'
1 + (+'2') // 3
1 - '2' // -1上述现象源于 + 号的双重身份。注意第二条表达式中,String 前使用了 + 号,强制将其转换为 Number 类型。理解类型转换时,掌握 + 号的双重角色通常已足够。其他类型转换类似,均可通过赋值或重载机制修改,甚至包括 Error 对象:
代码如下:
var err = new Error();
console.log(err instanceof Error); // true
err = 'sofish';
console.log(err); // 'sofish'四、JavaScript 引用类型
引用类型的操作与复制
这是本文的一个难点。与基本类型不同,引用类型可以添加属性和方法;引用类型的值本质是一个指向堆内存中同一数据的指针。变量(属性)可被重新赋值,但复制行为较为特殊,下文将详细阐述。
1. 添加属性和方法
以下代码展示了对基本类型赋值属性的情况:赋值不报错,但获取时失效。
代码如下:
var arr = [1,2,3];
arr.hello = 'world';
console.log(arr.hello); // 'world'
var str = 'sofish';
str.hello = 'world';
console.log(str.hello); // undefined
2. 引用类型值的操作
由于引用类型在栈内存中仅保存引用,指向同一原始值时,对值的操作会影响所有引用。但若重新赋值(非直接操作值),则会创建新对象,不改变原始值。例如:
代码如下:
var arr = [1,2,3], sofish = arr;
sofish.push('hello world');
console.log(arr); // [1, 2, 3, 'hello world']
// 非相同类型
sofish = ['not a fish']; // 当 sofish 重新赋值时,不会改变原始值
console.log(arr);// [1, 2, 3, 'hello world']
3. 引用类型值的复制
对原始值的操作会影响所有引用,但这并非总是所需。有时我们需要复制全新对象,使操作不影响其他引用。通常 Date、Function、RegExp 等操作较少,主要涉及 Array 和 Object 的添加项或属性操作。因此,重点在于理解如何复制 Array 和 Object。
3.1 数组的复制
Array 对象中,slice 方法返回截取后的新数组,ES5 的 filter 等方法也返回新数组。我们可以利用这些方法实现复制。
代码如下:
var arr = [1, 2, 3];
var sofish = arr.slice();
// 对新的数组进行操作并不会影响到原始数组
sofish.push('hello world');
console.log(arr); // [1, 2, 3]3.2 对象的复制
除了使用 slice 方法,对于 Array 和 Object,还可以利用 for ... in 循环遍历并赋值来实现复制。
代码如下:
var obj = { name: 'sofish' }, sofish = {}, p;
for (p in obj) sofish[p] = obj[p];
// 对新的对象操作并不会影响原始值
sofish.say = function() {};
console.log(obj); // { name: 'sofish' }
3.3 Shadow / Deep Copy
上述操作称为浅拷贝(Shadow Copy)。由于 Array 和 Object 可能包含多层结构,浅拷贝仅处理最外层值,内部存在的 Array 或 Object 仍指向原始对象。例如:
代码如下:
var arr = [1, { bio: 'not a fish' } ], sofish = [], p;
for(p in arr) {
sofish[p] = arr[p];
}
// 对 `sofish` 中包含的对象 `cat` 的操作会影响原始值
sofish[1].bio = 'hackable';
console.log(arr);// [1, cat: { bio: 'hackable' } ]如何实现深拷贝?可通过以下 copy() 函数解决:
代码如下:
/* 复制对象
* @param: obj {JavaScript Object} 原始对象
* @param: isDeep {Boolean} 是否为深拷贝
* @return: {JavaScript Object} 返回一个新的对象
*/
function copy(obj, isDeep) {
var ret = obj.slice ? [] : {}, p, prop;
// 配合 is 函数使用
if(!isDeep && is(obj, 'Array')) return obj.slice();
for(p in obj) {
if(!obj.hasOwnProperty(p)) continue;
prop = obj[p];
ret[p] = (is(prop, 'Object') || is(prop, 'Array')) ?
copy(prop, isDeep) : prop;
}
return ret;
}通过 copy(obj, isDeep) 函数,我们可以复制 Array 或 Object。测试示例如下:
代码如下:
var arr = [1, {bio: 'not a fish'}];
var sofish = copy(arr);
// 浅拷贝对于第一层的操作不影响原始值,但影响第二层
sofish.push('cat');
console.log(arr); // [1, {bio: 'not a fish'}]
sofish[1].bio = 'hello world';
console.log(arr) // [1, {bio: 'hello world'}]
// 深拷贝则不会影响原始值
sofish = copy(arr, 1);
sofish[1].bio = 'foo or bar';
console.log(arr); // [1, {bio: 'hello world'}]
至此,关于类型较难的部分已基本涵盖。复制是最复杂的环节,除常用的 Array 和 Object 外,还需注意 Date、Function、RegExp 的复制问题。
JS脚本在App中运行报错怎么解决
2026-11-04 23:11:03
JS怎么使用padStart方法给数字补零
2026-11-04 11:12:03
JS是什么意思?JavaScript全称及基本定义
2026-11-04 08:05:29
JS 使用 toFixed(1) 保留一位小数的基本用法与注意事项
2026-11-03 18:34:57
JS字符串转整数:parseInt与Number的区别及用法
2026-11-03 17:26:54
