JavaScript 是什么类型的语言
- 每种编程语言都具有内建的数据类型,但它们的数据类型常有不同之处,使用方式也很不一样,比如 C 语言在定义变量之前,就需要确定变量的类型,可以看下面这段 C 代码:
int main() { int a = 1; char* b = "hello world"; bool c = true; c = a; return 0; } - 上述代码声明变量的特点是:在声明变量之前需要先定义变量类型;把这种在使用之前就需要确认其变量数据类型的称为静态语言;
- 相反地,把在运行过程中需要检查数据类型的语言称为动态语言;比如 JavaScript 就是动态语言,因为在声明变量之前并不需要确认其数据类型;
- 前面代码中,把 int 型的变量 a 赋值给了 bool 型的变量 c,这段代码也是可以编译执行的,因为在赋值过程中,C 编译器会把 int 型的变量悄悄转换为 bool 型的变量,通常把这种偷偷转换的操作称为隐式类型转换;而支持隐式类型转换的语言称为弱类型语言,不支持隐式类型转换的语言称为强类型语言;在这点上,C 和 JavaScript 都是弱类型语言;
- 对于各种语言的类型,可以参考下图:

JavaScript 数据类型
-
JavaScript 是一种弱类型的、动态的语言;
- 弱类型,意味着不需要告诉 JavaScript 引擎这个或那个变量是什么数据类型,JavaScript 引擎在运行代码的时候自己会计算出来;
- 动态,意味着可以使用同一个变量保存不同类型的数据;
-
JavaScript 中的数据类型一种有 8 种:
类型 数据类型 描述 Boolean 原始类型 只有 true/false 两个值 Null 原始类型 只有一个值 null,typeof 检测 Null 类型时,返回的是 Object (是个 bug,一直没修改是为了兼容老的代码) Undefined 原始类型 一个没有被赋值的变量会有个默认值 undefined,变量提升的时候也是 undefined Number 原始类型 · 安全整数范围:±(253 - 1) 即 ±9,007,199,254,740,991
· 特殊值:Infinity, -Infinity, NaN (Not a Number)Bigint 原始类型 · 可以表示任意大的整数,通过在数字后加 n 或调用 BigInt() 构造函数创建
· 不能与 Number 混合运算 (需要显式转换),不支持 Math 对象中的方法String 原始类型 用于表示文本数据 Symbol 原始类型 符号类型是唯一的并且不可修改的,通常用来作为 Object 的 key Object 引用数据类型 包含了 字面量对象、数组、正则表达式、日期对象、Math、实例对象、函数…
数据类型的检测
typeof
-
typeof 检测数据类型
console.log(typeof 10); // number console.log(typeof 'name'); // string console.log(typeof true); // boolean console.log(typeof undefined); // undefined console.log(typeof Symbol(100)); // symbol console.log(typeof function fn() { }); // function console.log(typeof null); // object console.log(typeof {}); // object console.log(typeof new RegExp(/A/)); // object console.log(typeof []); // object -
核心特点
- ✅ 优点:轻量、快速,适合检测基本类型 (除 Null) 和函数;
- ❌ 缺点:
- 无法区分 Null 和 Object (历史遗留 bug:JS 初始实现中,Null 以 000 二进制标记,与 Object 相同);
- 无法区分 Object 的子类型 (数组、日期、正则等均返回 “object”);
-
💡注意:typeof NaN 返回 “number” (NaN 是 Number 类型的特殊值);typeof 未声明的变量 不会报错,返回 “undefined” (可用于检测变量是否声明);
instanceof
-
原理:
A instanceof B检测 B.prototype 是否存在于 A 的原型链 上,返回布尔值; -
核心逻辑模拟:
function _instanceof(A, B) { while (B.prototype) { if (A.__proto__ == B.prototype) { return true } else { B = B.prototype } } return false } console.log(_instanceof('a', String)); // true -
核心特点:
- ✅ 优点:能区分 Object 的子类型 (数组、函数、日期等);
- ❌ 缺点:
- 无法检测基本类型 (基本类型无原型链,直接返回 false);
- 跨窗口 / iframe 时,同一类型的原型对象不同,检测失效 (如 [] instanceof window.frames[0].Array 返回 false);
- 原型链可被篡改,导致结果不准确;
function Fn() { } let f = new Fn(); console.log(f instanceof Array); // false // 原型链:f.__proto__ -> Fn.prototype -> Object.prototype Fn.prototype.__proto__ = Array.prototype; // 改变原型指向 // 原型链:f.__proto__ -> Fn.prototype -> Array.prototype -> Object.prototype console.log(f instanceof Array); // true
constructor
-
JavaScript 中每个对象 (包括基本类型的包装对象) 都有 constructor 属性,该属性默认指向创建该对象的构造函数:
- 基本类型的包装对象 (如 new String(‘abc’)) 的 constructor 指向 String;
- 引用类型 (如数组、函数) 的 constructor 指向其内置构造函数 (如 Array、Function);
- 自定义实例的 constructor 指向自定义类 / 构造函数;
-
对于基本类型 (如 123、‘abc’),JS 会自动装箱 (临时创建包装对象),因此也能通过 constructor 检测类型;
-
示例
console.log("hello".constructor === String); // true console.log(Symbol('foo').constructor === Symbol); // true console.log([].constructor === Array); // true // 自定义构造函数 function Person(name) { this.name = name; } const p = new Person('张三'); console.log(p.constructor === Person); // true // 自定义类(ES6) class Student {} const s = new Student(); console.log(s.constructor === Student); // true -
核心特点
- ✅ 优点
- 简洁直观:语法比 Object.prototype.toString.call() 更简洁,无需截取字符串;
- 覆盖范围广:可检测基本类型 (自动装箱)、内置引用类型、自定义实例;
- 区分 Object 子类型:能直接区分数组、日期、正则等 (优于 typeof);
- ❌ 缺点
- Null/Undefined 无 constructor:null 和 undefined 是原始值,没有 constructor 属性,直接访问会报错:
null.constructor; // Uncaught TypeError: Cannot read properties of null (reading 'constructor') undefined.constructor; // Uncaught TypeError: Cannot read properties of undefined (reading 'constructor')- constructor 可被篡改:constructor 是可写属性,修改后会导致检测结果错误:
const arr = []; // 篡改 constructor arr.constructor = Object; console.log(arr.constructor === Array); // false(检测失效) console.log(arr.constructor === Object); // true // 甚至篡改原型上的 constructor Array.prototype.constructor = Function; console.log([].constructor === Array); // false- 基本类型包装对象的混淆:包装对象的 constructor 与原始值一致,但本质是对象,可能造成误解:
const num = new Number(123); console.log(num.constructor === Number); // true console.log(typeof num); // "object"(而非 "number")- 跨窗口 /iframe 失效:与 instanceof 类似,跨窗口时内置构造函数的原型对象不同,导致检测失败:
const iframe = document.createElement('iframe'); document.body.appendChild(iframe); const frameArray = window.frames[window.frames.length - 1].Array; const arr = new frameArray(); console.log(arr.constructor === Array); // false(当前窗口的 Array 与 iframe 中的 Array 不是同一个)
- ✅ 优点
Object.prototype.toString.call()
-
Object.prototype.toString是 JS 中最准确的类型检测方法,调用时会返回格式为 [object 类型名] 的字符串,通过 call() 改变 this 指向,可检测任意值的类型; -
案例:
({}).toString.call(250) // "[object Number]" ({}).toString.call(true) // "[object Boolean]" ({}).toString.call('') // "[object String]" ({}).toString.call([]) // "[object Array]" ({}).toString.call(null) // "[object Null]" Object.prototype.toString.call(undefined) // "[object Undefined]" Object.prototype.toString.call({}) // "[object Object]" Object.prototype.toString.call(/^.$/) // "[object RegExp]" Object.prototype.toString.call(() => { }) // "[object Function]" -
核心特点
- ✅ 优点:全类型精准检测,包括 Null、数组、日期等;
- ❌ 缺点:返回值需截取处理,稍显繁琐;
- 💡 注意:直接调用 toString() 会受对象重写影响,必须用
Object.prototype.toString.call();
Array.isArray()
-
Array.isArray(值)专门检测数组; -
特点:比 instanceof 更可靠 (跨窗口 /iframe 仍有效),比 Object.prototype.toString 更简洁;
Array.isArray([]); // true Array.isArray(new Array()); // true Array.isArray({}); // false // 跨窗口场景 const iframe = document.createElement('iframe'); document.body.appendChild(iframe); const frameArray = window.frames[window.frames.length - 1].Array; Array.isArray(new frameArray()); // true(instanceof 会返回 false)
Number.isNaN()
-
Number.isNaN ():精准检测 NaN (NaN 不是一个有效数字,但属于 number 数字类型,NaN 和谁都不相等;其他数据类型转换成数字类型,不能转换就是 NaN); -
区别于全局 isNaN():全局 isNaN() 会先将值转为数字 (如 isNaN(‘abc’) 返回 true),而 Number.isNaN() 仅对 NaN 本身返回 true;
Number.isNaN(NaN); // true Number.isNaN(123); // false Number.isNaN('abc'); // false isNaN('abc'); // true(坑点)
数据类型的隐式转换
-
由于自动转换具有不确定性,建议在预期为布尔值、数值、字符串的地方,全部使用 Boolean()、Number() 和 String() 函数进行显式转换;遇到以下三种情况时,JavaScript 会自动转换数据类型,即转换是自动完成的,用户不可见:
- 第一种情况,不同类型的数据互相运算;
- 第二种情况,对非布尔值类型的数据求布尔值;
- 转化为 false 的值:false、undefined、null、‘’、0、NaN;
- 转化为 true 的值:除了转化为 false 的值;
- 第三种情况,对非数值类型的值使用一元运算符(即
+和-);
-
对象的转化 (求对象的原始值) 会先执行 valueOf() ,若 valueOf() 返回的值还是对象则会执行 toString();
// eg1: let obj1 = { valueOf() { return 100; }, toString() { return 200; }, }; console.log(true + obj1); // 101 // eg2: let obj2 = { valueOf() { return {}; }, toString() { return 200; }, }; console.log(true + obj2); // 201
堆栈内存
-
在 JavaScript 的执行过程中, 主要有三种类型内存空间,分别是代码空间、栈空间和堆空间;
-
其中的代码空间主要是存储可执行代码的 (后面介绍),这里主要来说说栈空间 (调用栈,是用来存储执行上下文) 和堆空间,先看下面这段代码:
function foo() { var a = "hello world" var b = a var c = { name: "hello world" } var d = c } foo() -
当执行一段代码时,需要先编译,并创建执行上下文,然后再按照顺序执行代码;当执行到第 3 行代码时,其调用栈的状态如下:
-
接下来继续执行第 4 行代码,由于 JavaScript 引擎判断右边的值是一个引用类型,这时候处理的情况就不一样了,JavaScript 引擎并不是直接将该对象存放到变量环境中,而是将它分配到堆空间里面,分配后该对象会有一个在 “堆” 中的地址,然后再将该数据的地址写进 c 的变量值,最终分配好内存的示意图如下所示:
- 从上图可以清晰地观察到,对象类型是存放在堆空间的,在栈空间中只是保留了对象的引用地址,当 JavaScript 需要访问该数据的时候,是通过栈中的引用地址来访问的;
- 为什么一定要分 “堆” 和 “栈” 两个存储空间呢?所有数据直接存放在 “栈” 中不就可以了吗?
- 答案是不可以的:这是因为 JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率;比如文中的 foo 函数执行结束了,JavaScript 引擎需要离开当前的执行上下文,只需要将指针下移到上个执行上下文的地址就可以了,foo 函数执行上下文栈区空间全部回收,具体过程可以参考下图:
- 所以通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据;而引用类型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间;
-
最后一步将变量 c 赋值给变量 d 是怎么执行的:在 JavaScript 中,赋值操作和其他语言有很大的不同,原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址,所以 d = c 的操作就是把 c 的引用地址赋值给 d,参考下图:
面试题
Object.prototype.toString 检测自定义类型
-
Symbol.toStringTag 是 ES6 引入的内置 Symbol,用于自定义 Object.prototype.toString.call() 返回的类型标签;其规则如下:
- 当调用 Object.prototype.toString.call(obj) 时,引擎会先检查 obj[Symbol.toStringTag] 是否存在且为字符串;
- 若存在,返回 [object ${obj[Symbol.toStringTag]}];
- 若不存在,使用内置默认值 (如 Array 默认为 “Array”,Object 默认为 “Object”);
-
数组作为内置对象,默认的 Symbol.toStringTag 是 “Array”,但手动修改后,toString.call() 的返回值会被篡改,但不会改变对象的实际类型;
const arr = []; arr[Symbol.toStringTag] = 'MyCustomArray'; // 1. Array.isArray 仍识别为数组(最可靠的数组检测) console.log(Array.isArray(arr)); // true // 2. instanceof 仍识别为 Array console.log(arr instanceof Array); // true // 3. constructor 仍指向 Array console.log(arr.constructor === Array); // true // 4. 数组的原生方法仍可用 arr.push(1); console.log(arr); // [1] // 5. typeof 仍返回 "object"(无变化) console.log(typeof arr); // object -
自定义类通过 Symbol.toStringTag 定制类型标签
// 自定义类默认结果 class Person {} const p = new Person(); console.log(Object.prototype.toString.call(p)); // [object Object] // 定制 Symbol.toStringTag class Person { get [Symbol.toStringTag]() { return 'Person'; } } const p2 = new Person(); console.log(Object.prototype.toString.call(p2)); // [object Person] // 直接给对象设置 Symbol.toStringTag const obj = { [Symbol.toStringTag]: 'MyObject' }; console.log(Object.prototype.toString.call(obj)); // [object MyObject]
输出以下代码的结果
let a = { n: 10 };
let b = a;
b.m = b = { n: 20 };
console.log(a); // { n: 10, m: {n: 20} }
console.log(b); // { n: 20 }
步骤 1:基础变量与引用关系(前两行)
- JS 中对象是引用类型,存储在堆内存,变量 a/b 只存 “堆地址”(栈内存)
- 执行后:a 和 b 指向同一个堆内存地址(记为 地址X),该地址存储 { n: 10 }
- a → 地址X → 堆:{ n: 10 }
- b → 地址X
步骤 2:核心行拆解 b.m = b = { n: 20 }
- 先明确两个规则:
- 赋值运算符(=)是右结合:多个 = 从右向左依次计算(但属性访问 . 优先级更高);
- 属性访问(.)的优先级高于赋值:执行 b.m 时,先确定 b 当前指向的对象(地址 X),再准备给这个对象添加 m 属性(此时还未赋值)
- 阶段 1:解析 b.m 的 “目标对象”
- 当前 b 指向 地址X(因为还没执行右侧的赋值),所以 b.m 指向的是地址 X 对应的对象要新增的 m 属性
- 阶段 2:从右向左执行赋值(右结合)
- 第一步:执行 b = { n: 20 },创建新对象 { n: 20 },存于新堆地址(记为 地址Y),将 b 的栈地址从 地址X 改为 地址Y
- 第二步:执行 b.m = 第一步的返回值,这里的 b.m 是阶段 1 已锁定的目标(地址 X 的对象的 m 属性)
输出以下代码的结果
函数执行之前的准备工作
- 初始化实参集合
- 创建形参变量并赋值
- 代码执行
let x = [12, 23];
function fn(y) {
y[0] = 100;
y = [100];
y[1] = 200;
console.log(y);
}
fn(x);
console.log(x);
输出以下代码的结果
var x = 10;
~ function (x) {
console.log(x);
x = x || 20 && 30 || 40;
console.log(x);
}();
console.log(x);
输出以下代码的结果
let x = [1, 2], y = [3, 4];
~ function (x) {
x.push('A');
x = x.slice(0);
x.push('B');
x = y;
x.push('C');
// [3, 4, "C"] [3, 4, "C"]
console.log(x, y);
}(x);
// [1, 2, "A"] [3, 4, "C"]
console.log(x, y);
object 的 key 值
只能是基本类型值,最后会转成字符串;
会把 引用类型值 转换成 字符串 [object Object];
var a = {}, b = '123', c = 123;
a[b] = 'b';
a[c] = 'c';
// a['123'] 等价 a[123],则 a[b]、a[c] 等于 c
console.log(a[b]); // c
let a = { x: 100 };
let b = { y: 200 };
let obj = {};
obj[a] = 'aaa'; //=> { '[object Object]': 'aaa' }
obj[b] = 'bbb'; //=> { '[object Object]': 'bbb' }
console.log(obj); //=> { '[object Object]': 'bbb' }
object 的 key 值为 Symbol
var a = {}, b = Symbol('123'), c = Symbol('123')
a[b] = 'b';
a[c] = 'c';
// Symbol 是 ES6 中新增的数据类型,创建的值为唯一值,b 不等于 c
console.log(a[b]); // b
值和引用
var foo = {
n: 0,
k: {
n: 0,
},
};
var bar = foo.k;
bar.n++;
bar = {
n: 10,
};
bar = foo;
bar.n++;
bar = foo.n;
bar++;
console.log(foo.n, foo.k.n);
var foo = {
n: 1,
};
var arr = [foo];
function method1(arr) {
var bar = arr[0];
arr.push(bar);
bar.n++;
arr = [bar];
arr.push(bar);
arr[1].n++;
}
function method2(foo) {
foo.n++;
}
function method3(n) {
n++;
}
method1(arr);
method2(foo);
method3(foo.n);
console.log(foo.n, arr.length);
var foo = { bar: 1 };
var arr1 = [1, 2, foo];
var arr2 = arr1.slice(1);
arr2[0]++;
arr2[1].bar++;
foo.bar++;
arr1[2].bar++;
console.log(arr1[1] === arr2[0]);
console.log(arr1[2] === arr2[1]);
console.log(foo.bar);
0.1 + 0.2 是否等于 0.3
console.log(0.1 + 0.2 == 0.3); // ???
1. 在 JavaScript 中,0.1 + 0.2 并不等于 0.3,这是前端开发中经典的浮点数精度问题,核心原因是 JavaScript 遵循 IEEE 754 标准 采用二进制存储浮点数,而部分十进制小数无法被二进制浮点数精确表示,最终导致计算误差;
2. 十进制 => 二进制
0.1(十进制) = 0.00011001100110011...(二进制,循环节为 0011)
0.2(十进制) = 0.0011001100110011...(二进制,循环节为 0011)
3. 再将 二进制 => 十进制
0.1 ≈ 0.1000000000000000055511151231257827021181583404541015625
0.2 ≈ 0.200000000000000011102230246251565404236316680908203125
4. console.log(0.1 + 0.2); // 输出:0.30000000000000004
5. 解决办法:转成整数运算
a 等于什么值会让「a == 1 && a == 2 && a == 3 」条件成立;
// 方式一:数值和对象比较,先执行 valueOf,如果还是对象则再执行 toString
var a = {
i: 1,
toString() {
return a.i++;
}
};
if (a == 1 && a == 2 && a == 3) {
console.log('OK');
}
// 方式二:数值和对象比较,先执行 valueOf,如果还是对象则再执行 toString
var a = {
i: 1,
valueOf() {
return a.i++;
}
};
if (a == 1 && a == 2 && a == 3) {
console.log('OK');
}
// 方式三:
var a = [1, 2, 3];
a.toString = a.shift;
if (a == 1 && a == 2 && a == 3) {
console.log('OK');
}
// 方式四:
var a = [1, 2, 3];
a.valueOf = a.shift;
if (a == 1 && a == 2 && a == 3) {
console.log('OK');
}
// 方式五:
var i = 0;
Object.defineProperty(window, 'a', {
get() {
// 获取 window.a 的时候触发
return ++i;
},
set() {
// 给 window.a 设置属性值的时候触发
}
});
if (a == 1 && a == 2 && a == 3) {
console.log('OK');
}
[] == ![] 的结果是 true 吗
console.log([] == ![]); // true
// 1. 单目运算符优先级更高,![] 是一个表达式,则 ![] => fasle
// 2. [] 要转成原始值,先执行 valueOf,若返回值为对象继续执行 toString
// [].valueOf() = [],[] == false
// [].toString() = '','' == false
// 3. '' == false
// Number('') => 0
// false => 0
// 4. 0 == 0 => true
正则表达式👉 案例
上一篇