# 进阶篇
# 一、JS基础
# 1 类型及检测方式
1. JS内置类型
JavaScript 的数据类型有下图所示
其中,前 7 种类型为基础类型,最后
1 种(Object)为引用类型
,也是你需要重点关注的,因为它在日常工作中是使用得最频繁,也是需要关注最多技术细节的数据类型
JavaScript
一共有8种数据类型,其中有7种基本数据类型:Undefined
、Null
、Boolean
、Number
、String
、Symbol
(es6
新增,表示独一无二的值)和BigInt
(es10
新增);- 1种引用数据类型——
Object
(Object本质上是由一组无序的名值对组成的)。里面包含function、Array、Date
等。JavaScript不支持任何创建自定义类型的机制,而所有值最终都将是上述 8 种数据类型之一。- 引用数据类型: 对象
Object
(包含普通对象-Object
,数组对象-Array
,正则对象-RegExp
,日期对象-Date
,数学函数-Math
,函数对象-Function
)
- 引用数据类型: 对象
在这里,我想先请你重点了解下面两点,因为各种 JavaScript 的数据类型最后都会在初始化之后放在不同的内存中,因此上面的数据类型大致可以分成两类来进行存储:
- 原始数据类型:基础类型存储在栈内存,被引用或拷贝时,会创建一个完全相等的变量;占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储。
- 引用数据类型:引用类型存储在堆内存,存储的是地址,多个引用指向同一个地址,这里会涉及一个“共享”的概念;占据空间大、大小不固定。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
JavaScript 中的数据是如何存储在内存中的?
在 JavaScript 中,原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。
在 JavaScript 的执行过程中, 主要有三种类型内存空间,分别是代码空间
、栈空间
、堆空间
。其中的代码空间主要是存储可执行代码的,原始类型(Number、String、Null、Undefined、Boolean、Symbol、BigInt
)的数据值都是直接保存在“栈”中的,引用类型(Object)的值是存放在“堆”中的。因此在栈空间中(执行上下文),原始类型存储的是变量的值,而引用类型存储的是其在"堆空间"中的地址,当 JavaScript 需要访问该数据的时候,是通过栈中的引用地址来访问的,相当于多了一道转手流程。
在编译过程中,如果 JavaScript 引擎判断到一个闭包,也会在堆空间创建换一个“closure(fn)”
的对象(这是一个内部对象,JavaScript 是无法访问的),用来保存闭包中的变量。所以闭包中的变量是存储在“堆空间”中的。
JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率。通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间。因此需要“栈”和“堆”两种空间。
题目一:初出茅庐
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
@程序员poetry: 代码已经复制到剪贴板
这道题比较简单,我们可以看到第一个 console 打出来 name 是 'lee',这应该没什么疑问;但是在执行了 b.name='son' 之后,结果你会发现 a 和 b 的属性 name 都是 'son',第二个和第三个打印结果是一样的,这里就体现了引用类型的“共享”的特性,即这两个值都存在同一块内存中共享,一个发生了改变,另外一个也随之跟着变化。
你可以直接在 Chrome 控制台敲一遍,深入理解一下这部分概念。下面我们再看一段代码,它是比题目一稍复杂一些的对象属性变化问题。
题目二:渐入佳境
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
@程序员poetry: 代码已经复制到剪贴板
这道题涉及了 function
,你通过上述代码可以看到第一个 console
的结果是 30
,b
最后打印结果是 {name: "Kath", age: 30}
;第二个 console
的返回结果是 24
,而 a
最后的打印结果是 {name: "Julia", age: 24}
。
是不是和你预想的有些区别?你要注意的是,这里的 function
和 return
带来了不一样的东西。
原因在于:函数传参进来的
o
,传递的是对象在堆中的内存地址值,通过调用o.age = 24
(第 7 行代码)确实改变了a
对象的age
属性;但是第 12 行代码的return
却又把o
变成了另一个内存地址,将{name: "Kath", age: 30}
存入其中,最后返回b
的值就变成了{name: "Kath", age: 30}
。而如果把第 12 行去掉,那么b
就会返回undefined
2. 数据类型检测
(1)typeof
typeof 对于原始类型来说,除了 null 都可以显示正确的类型
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
@程序员poetry: 代码已经复制到剪贴板
typeof
对于对象来说,除了函数都会显示object
,所以说typeof
并不能准确判断变量到底是什么类型,所以想判断一个对象的正确类型,这时候可以考虑使用instanceof
(2)instanceof
instanceof
可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的prototype
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 // console.log(undefined instanceof Undefined); // console.log(null instanceof Null);
@程序员poetry: 代码已经复制到剪贴板
instanceof
可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型;- 而
typeof
也存在弊端,它虽然可以判断基础数据类型(null
除外),但是引用数据类型中,除了function
类型以外,其他的也无法判断
// 我们也可以试着实现一下 instanceof function _instanceof(left, right) { // 由于instance要检测的是某对象,需要有一个前置判断条件 //基本数据类型直接返回false if(typeof left !== 'object' || left === null) return false; // 获得类型的原型 let prototype = right.prototype // 获得对象的原型 left = left.__proto__ // 判断对象的类型是否等于类型的原型 while (true) { if (left === null) return false if (prototype === left) return true left = left.__proto__ } } console.log('test', _instanceof(null, Array)) // false console.log('test', _instanceof([], Array)) // true console.log('test', _instanceof('', Array)) // false console.log('test', _instanceof({}, Object)) // true
@程序员poetry: 代码已经复制到剪贴板
(3)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
@程序员poetry: 代码已经复制到剪贴板
这里有一个坑,如果我创建一个对象,更改它的原型,
constructor
就会变得不可靠了
function Fn(){}; Fn.prototype=new Array(); var f=new Fn(); console.log(f.constructor===Fn); // false console.log(f.constructor===Array); // true
@程序员poetry: 代码已经复制到剪贴板
(4)Object.prototype.toString.call()
toString()
是Object
的原型方法,调用该方法,可以统一返回格式为“[object Xxx]”
的字符串,其中Xxx
就是对象的类型。对于Object
对象,直接调用toString()
就能返回[object Object]
;而对于其他对象,则需要通过call
来调用,才能返回正确的类型信息。我们来看一下代码。
Object.prototype.toString({}) // "[object Object]" Object.prototype.toString.call(
@程序员poetry: 代码已经复制到剪贴板