JavaScript 是什么类型的语言

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

JavaScript 数据类型

  1. JavaScript 是一种弱类型的、动态的语言;

    1. 弱类型,意味着不需要告诉 JavaScript 引擎这个或那个变量是什么数据类型,JavaScript 引擎在运行代码的时候自己会计算出来;
    2. 动态,意味着可以使用同一个变量保存不同类型的数据;
  2. JavaScript 中的数据类型一种有 8 种:

    类型 数据类型 描述
    Boolean 原始类型 只有 true/false 两个值
    Null 原始类型 只有一个值 nulltypeof 检测 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 原始类型 符号类型是唯一的并且不可修改的,通常用来作为 Objectkey
    Object 引用数据类型 包含了 字面量对象、数组、正则表达式、日期对象、Math、实例对象、函数…

数据类型的检测

typeof

  1. 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
    
  2. 核心特点

    1. ✅ 优点:轻量、快速,适合检测基本类型 (除 Null) 和函数;
    2. ❌ 缺点:
      • 无法区分 NullObject (历史遗留 bug:JS 初始实现中,Null 以 000 二进制标记,与 Object 相同)
      • 无法区分 Object 的子类型 (数组、日期、正则等均返回 “object”)
  3. 💡注意:typeof NaN 返回 “number” (NaN 是 Number 类型的特殊值)typeof 未声明的变量 不会报错,返回 “undefined” (可用于检测变量是否声明)

instanceof

  1. 原理:A instanceof B 检测 B.prototype 是否存在于 A 的原型链 上,返回布尔值;

  2. 核心逻辑模拟:

    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
    
  3. 核心特点:

    1. ✅ 优点:能区分 Object 的子类型 (数组、函数、日期等)
    2. ❌ 缺点:
      • 无法检测基本类型 (基本类型无原型链,直接返回 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

  1. JavaScript 中每个对象 (包括基本类型的包装对象) 都有 constructor 属性,该属性默认指向创建该对象的构造函数:

    1. 基本类型的包装对象 (如 new String(‘abc’))constructor 指向 String
    2. 引用类型 (如数组、函数)constructor 指向其内置构造函数 (如 Array、Function)
    3. 自定义实例的 constructor 指向自定义类 / 构造函数;
  2. 对于基本类型 (如 123、‘abc’)JS 会自动装箱 (临时创建包装对象),因此也能通过 constructor 检测类型;

  3. 示例

    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
    
  4. 核心特点

    1. ✅ 优点
      • 简洁直观:语法比 Object.prototype.toString.call() 更简洁,无需截取字符串;
      • 覆盖范围广:可检测基本类型 (自动装箱)、内置引用类型、自定义实例;
      • 区分 Object 子类型:能直接区分数组、日期、正则等 (优于 typeof)
    2. ❌ 缺点
      • Null/Undefinedconstructornullundefined 是原始值,没有 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()

  1. Object.prototype.toStringJS 中最准确的类型检测方法,调用时会返回格式为 [object 类型名] 的字符串,通过 call() 改变 this 指向,可检测任意值的类型;

  2. 案例:

    ({}).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]"
    
  3. 核心特点

    1. ✅ 优点:全类型精准检测,包括 Null、数组、日期等;
    2. ❌ 缺点:返回值需截取处理,稍显繁琐;
    3. 💡 注意:直接调用 toString() 会受对象重写影响,必须用 Object.prototype.toString.call()

Array.isArray()

  1. Array.isArray(值) 专门检测数组;

  2. 特点:比 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()

  1. Number.isNaN ():精准检测 NaN (NaN 不是一个有效数字,但属于 number 数字类型,NaN 和谁都不相等;其他数据类型转换成数字类型,不能转换就是 NaN)

  2. 区别于全局 isNaN():全局 isNaN() 会先将值转为数字 (如 isNaN(‘abc’) 返回 true),而 Number.isNaN() 仅对 NaN 本身返回 true

    Number.isNaN(NaN);      // true
    Number.isNaN(123);      // false
    Number.isNaN('abc');    // false
    isNaN('abc');           // true(坑点)
    

数据类型的隐式转换

  1. 由于自动转换具有不确定性,建议在预期为布尔值、数值、字符串的地方,全部使用 Boolean()Number()String() 函数进行显式转换;遇到以下三种情况时,JavaScript 会自动转换数据类型,即转换是自动完成的,用户不可见:

    1. 第一种情况,不同类型的数据互相运算;
    2. 第二种情况,对非布尔值类型的数据求布尔值;
      1. 转化为 false 的值:false、undefined、null、‘’、0、NaN
      2. 转化为 true 的值:除了转化为 false 的值;
    3. 第三种情况,对非数值类型的值使用一元运算符(即 +-);
  2. 对象的转化 (求对象的原始值) 会先执行 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
    

堆栈内存

  1. JavaScript 的执行过程中, 主要有三种类型内存空间,分别是代码空间、栈空间和堆空间;

  2. 其中的代码空间主要是存储可执行代码的 (后面介绍),这里主要来说说栈空间 (调用栈,是用来存储执行上下文) 和堆空间,先看下面这段代码:

    function foo() {
      var a = "hello world"
      var b = a
      var c = { name: "hello world" }
      var d = c
    }
    foo()
    
  3. 当执行一段代码时,需要先编译,并创建执行上下文,然后再按照顺序执行代码;当执行到第 3 行代码时,其调用栈的状态如下:

  4. 接下来继续执行第 4 行代码,由于 JavaScript 引擎判断右边的值是一个引用类型,这时候处理的情况就不一样了,JavaScript 引擎并不是直接将该对象存放到变量环境中,而是将它分配到堆空间里面,分配后该对象会有一个在 “堆” 中的地址,然后再将该数据的地址写进 c 的变量值,最终分配好内存的示意图如下所示:

    1. 从上图可以清晰地观察到,对象类型是存放在堆空间的,在栈空间中只是保留了对象的引用地址,当 JavaScript 需要访问该数据的时候,是通过栈中的引用地址来访问的;
    2. 为什么一定要分 “堆”“栈” 两个存储空间呢?所有数据直接存放在 “栈” 中不就可以了吗?
    3. 答案是不可以的:这是因为 JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率;比如文中的 foo 函数执行结束了,JavaScript 引擎需要离开当前的执行上下文,只需要将指针下移到上个执行上下文的地址就可以了,foo 函数执行上下文栈区空间全部回收,具体过程可以参考下图:
    4. 所以通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据;而引用类型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间;
  5. 最后一步将变量 c 赋值给变量 d 是怎么执行的:在 JavaScript 中,赋值操作和其他语言有很大的不同,原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址,所以 d = c 的操作就是把 c 的引用地址赋值给 d,参考下图:

面试题

Object.prototype.toString 检测自定义类型

  1. Symbol.toStringTagES6 引入的内置 Symbol,用于自定义 Object.prototype.toString.call() 返回的类型标签;其规则如下:

    1. 当调用 Object.prototype.toString.call(obj) 时,引擎会先检查 obj[Symbol.toStringTag] 是否存在且为字符串;
    2. 若存在,返回 [object ${obj[Symbol.toStringTag]}]
    3. 若不存在,使用内置默认值 (如 Array 默认为 “Array”,Object 默认为 “Object”)
  2. 数组作为内置对象,默认的 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
    
  3. 自定义类通过 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 属性)

输出以下代码的结果

函数执行之前的准备工作

  1. 初始化实参集合
  2. 创建形参变量并赋值
  3. 代码执行
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 值

  1. 只能是基本类型值,最后会转成字符串;

  2. 会把 引用类型值 转换成 字符串 [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
打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

你好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. JavaScript 是什么类型的语言
  2. 2. JavaScript 数据类型
  3. 3. 数据类型的检测
    1. 3.1. typeof
    2. 3.2. instanceof
    3. 3.3. constructor
    4. 3.4. Object.prototype.toString.call()
    5. 3.5. Array.isArray()
    6. 3.6. Number.isNaN()
  4. 4. 数据类型的隐式转换
  5. 5. 堆栈内存
  6. 6. 面试题
    1. 6.1. Object.prototype.toString 检测自定义类型
    2. 6.2. 输出以下代码的结果
    3. 6.3. 输出以下代码的结果
    4. 6.4. 输出以下代码的结果
    5. 6.5. 输出以下代码的结果
    6. 6.6. object 的 key 值
    7. 6.7. object 的 key 值为 Symbol
    8. 6.8. 值和引用
    9. 6.9. 0.1 + 0.2 是否等于 0.3
    10. 6.10. a 等于什么值会让「a == 1 && a == 2 && a == 3 」条件成立;
    11. 6.11. [] == ![] 的结果是 true 吗