前端面试系列-JS篇-你真的了解数据类型吗?
数据类型作为 JavaScript 的入门知识,相信大家已经对此都已经很了解,这里主要是带着大家了解一下数据类型、数据是如何存储在内存中的以及全面的类型检测方式
数据类型
JavaScript 中有 7 种基本数据类型,它们分别是:
- Number:
数字类型,包括整数和浮点数。
比如:42(生命、宇宙以及一切的答案)和3.14(圆周率) - String:
字符串类型,用于表示文本。
比如:"Hello, World!"(你好,世界!) - Boolean:
布尔类型,表示真或假。
比如:true(真)和false(假) - Undefined:
未定义类型,表示变量未被赋值。
比如:let x;(声明了一个变量x,但没有给它赋值) - Null:
空类型,表示一个空值。
比如:let x = null;(声明了一个变量x,并给它赋了一个空值) - Symbol:
符号类型,表示唯一的标识符(es6新增,表示独一无二的值)。
比如:Symbol("id")(创建了一个唯一的标识符"id") - BigInt:
大整数类型,表示任意大小的整数(es10新增)。
比如:10n(一个大整数10)
除了这些基本数据类型,JavaScript 还有引用数据类型:
- Object:
表示普通对象
- Array:
表示数组,是一种特殊的对象,可以用来存储一组有序的数据
- Function:
表示函数,是一种特殊的对象,可以被调用执行
- RegExp:
表示正则表达式,用来匹配字符串中的模式
- Date:
表示日期和时间
- ...
JavaScript不支持任何创建自定义类型的机制,而所有值最终都将是上述 8 种数据类型之一。
那么,变量是如何存储在内存中的呢?
存储
基础数据类型:
- 值不变,大小固定;
- 被拷贝时会将原始值的副本赋给目标变量,两个变量之间互不影响;
- 会快速的创建和销毁,所以数据存储在栈中
引用数据类型:
- 大小,值都是动态的;
- 指针存储在栈中,实际的值存储在堆中
- 赋值操作,就是浅拷贝(只拷贝栈中的指针),共享同一块内存空间,如果修改其中一个变量所指向的对象,另一个变量的值也会随之改变。深拷贝(栈中的指针和对堆中的值都拷贝了一份),就相当于基础数据类型的拷贝了,两个数据之间互不影响;
需要注意的是,函数在 JavaScript 中也是一种对象,因此函数也可以存储在堆内存中。函数的代码会存储在代码段中,而函数的作用域链则会存储在堆内存中。当我们调用函数时,JavaScript 引擎会为该函数创建一个执行上下文,其中包含函数的作用域链、参数、局部变量等信息,该执行上下文会被存储在栈内存中并加入调用栈中。当函数执行完毕后,该执行上下文会被弹出调用栈并从栈内存中释放。
这里大家可能会有个疑问,基础数据类型的值明明是可以改变的,为什么说他的值是固定的?
这是因为基础数据类型的值是不可变的,但是如果我们将其赋值给一个变量,那么该变量的值是可变的。例如,如果我们声明一个变量 y 并将其赋值为一个字符串 "Hello" ,那么 y 的值是可变的,我们可以将其改变为另一个字符串 "World" 。但是,字符串 "Hello" 本身的值是不可变的。
综上所述,
变量的值可以改变,但基础数据类型的值不会改变
,这意味着我们可以改变变量指向的值,但不能改变基础数据类型的原始值。
我们下面用两个面试题来验证一下
题目一:初出茅庐
let a = {
name: 'lee',
age: 18
}
let b = a;
console.log(a.name); //第一个console
b.name = 'son';
console.log(a.name); //第二个console
console.log(b.name); //第三个console
这道题考验的是引用数据类型被拷贝的知识点。 第一个 console 输出 a.name 的值为 "lee" ,因为此时 a.name 的值为 "lee" 。 第二个 console 输出 a.name 的值为 "son" ,因为我们将 a 的引用赋值给了 b ,此时 a 和 b 都引用了同一个对象。之后我们修改了 b.name 的值为 "son" ,这同时也修改了 a.name 的值,因为它们引用的是同一个对象。 第三个 console 输出 b.name 的值为 "son" ,因为此时 b.name 的值为 "son" 。
题目二:渐入佳境
let a = {
name: 'Julia',
age: 20
}
function change(o) {
o.age = 24;
o = {
name: 'Kath',
age: 30
}
return o;
}
let b = change(a); // 注意这里没有new,后面new相关会有专门文章讲解
console.log(b.age); // 第一个console
console.log(a.age); // 第二个console
第一次打印输出 b.age 时,会输出 30 ,因为此时 b 引用的对象是被 change 函数创建的新对象,其中 age 属性的值为30。
第二次打印输出 a.age 时,会输出 24 ,因为虽然在 change 函数中修改了 a 对象的 age 属性的值为 24,但是在 change 函数中重新为函数参数 o 赋值时,o 引用了一个新的对象,a 并没有被修改。
具体来说,当我们调用 change
函数,并将 a
对象作为参数传递给函数时,函数内部的 o
参数会引用 a
对象。在函数内部,我们首先修改了 o.age
属性的值为 24
,这会同时修改 a.age
属性的值,因为它们引用的是同一个对象。接着,在函数内部我们重新为 o
赋值了一个新的对象,这意味着 o
不再引用a对象,而是指向了一个新的对象。因此,当函数返回时,b
引用的是新的对象,而 a
仍然引用原始对象,并且原始对象的 age
属性已经被修改为 24
。
需要注意的是,虽然在 change 函数中重新为 o 赋值时,o 引用了一个新的对象,但是这并不会影响函数外部的 a 变量,因为 JavaScript 中的函数参数都是按值传递的,即函数参数只是对原始值的一个副本,而不是对原始值本身的引用
。因此,在 change 函数中重新为 o 赋值只是修改了 o 参数的值,而不会修改原始值。
检测方式
typeof
检测基本数据类型非常方便,可以正确判断除 null
以外的所有基础数据类型,而引用数据类型方面除 function
外都不能正确判断
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof 'str'); // string
console.log(typeof []); // object []数组的数据类型在 typeof 中被解释为 object
console.log(typeof function(){}); // function
console.log(typeof {}); // object
console.log(typeof undefined); // undefined
console.log(typeof null); // object null 的数据类型被 typeof 解释为 object
instanceof
检测对象是否属于某个构造函数的实例,内部机制是通过判断对象的原型链中是不是能找到类型的 prototype
,具体原理请查看
instanceof
可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型,原因请查看包装对象
console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
constructor
检测对象的构造函数。对于基本数据类型,需要手动包装基本数据类型
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
这里有一个坑,如果我们创建一个对象,更改它的原型,constructor
就会变得不可靠了
function Fn(){};
Fn.prototype=new Array();
var f=new Fn();
console.log(f.constructor===Fn); // false
console.log(f.constructor===Array); // true
Object.prototype.toString.call()
这种方式之所以可行,是因为所有的对象都继承了 Object.prototype
对象的 toString()
方法,该方法可以返回一个表示对象类型的字符串。
当我们调用 Object.prototype.toString.call()
方法并传入一个值时,该方法会返回一个类似 [object Type]
的字符串,其中 Type
表示值的数据类型。例如,Number 类型的值返回的字符串为 [object Number]
,String类型的值返回的字符串为 [object String]
。
Object.prototype.toString({}) // "[object Object]"
Object.prototype.toString.call({}) // 同上结果,加上call也ok
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call('1') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call(null) //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(/123/g) //"[object RegExp]"
Object.prototype.toString.call(new Date()) //"[object Date]"
Object.prototype.toString.call([]) //"[object Array]"
Object.prototype.toString.call(document) //"[object HTMLDocument]"
Object.prototype.toString.call(window) //"[object Window]"
// 从上面这段代码可以看出,Object.prototype.toString.call() 可以很好地判断引用类型,甚至可以把 document 和 window 都区分开来。
结束
这篇文章我们介绍了基础数据类型和引用数据类型这两类数据类型的分类、存储位置以及全面的类型检测方式。
相信通过这篇文章大家对此都会有更深入的了解。
最后,祝大家变得更强!
转载自:https://juejin.cn/post/7244337505495515197