数据存储

84 阅读7分钟

如果你想成为行业专家,并打造高性能前端应用,那么你就必须要搞清楚JavaScript 的内存机制了。

JavaScript 是什么类型的语言?

JS是动态的弱类型语言。

动态指的是它是在运行过程中需要检查数据类型的语言称为动态语言,这意味着你可以使用同一个变量保存不同类型的数据。

而弱类型型指的是可以进行隐式类型转换这意味着你不需要告诉 JavaScript 引擎这个或那个变量是什么数据类型, JavaScript 引擎在运行代码的时候自己会计算出来。

function foo(){
 var a = 1
 var b = a
 a = 2
 console.log(a)
 console.log(b)
}
foo()

function foo(){
 var a = {name:" 极客时间 "}
 var b = a
 a.name = " 极客邦 " 
 console.log(a)
 console.log(b)
}
foo()

执行第一段代码,打印出来 a 的值是 2,b 的值是 1。

执行第二段代码a 和 b 打印出来的值都是{name:"极客邦"}。

思考:这是为什么?

JavaScript 的数据类型

在JS中将数据类型分成原始类型和引用类型,引用类型只有一个就是object其他的都是原始类型,之所以要分成2种是因为他们的存储位置不同。

var bar
bar = 12 
bar = " 极客时间 "
bar = true
bar = null
bar = {name:" 极客时间 "}

从上述代码中你可以看出,我们声明了一个 bar 变量,然后可以使用各种类型的数据值赋予给该变量。

类型检测

基础类型检测-typeof,同时typeof还可以检测出functiton的类型。

var bar
console.log(typeof bar) //undefined
bar = 12 
console.log(typeof bar) //number
bar = " 极客时间 "
console.log(typeof bar)//string
bar = true
console.log(typeof bar) //boolean
bar = null
console.log(typeof bar) //object
bar = {name:" 极客时间 "}
console.log(typeof bar) //object
bar = function(){}
console.log(typeof bar)//function

为什么null是一个objeact?

这是当初 JavaScript 语言的 一个 Bug,一直保留至今,之所以一直没修改过来,主要是为了兼容老的代码。

引用类型检测

首推:object.prototype.toString.call(),object.prototype.toString.call()可以检测所有的数据类型,但是他返回的是'[object xxx]'这样的字符串。

**基础类型判断**
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(“abc”);// "[object String]"
Object.prototype.toString.call(123);// "[object Number]"
Object.prototype.toString.call(true);// "[object Boolean]"
**函数类型**
Function fn(){
  console.log(“test”);
}
Object.prototype.toString.call(fn); // "[object Function]"
**日期类型**
var date = new Date();
Object.prototype.toString.call(date); // "[object Date]"
**数组类型**
var arr = [1,2,3];
Object.prototype.toString.call(arr); // "[object Array]"
**正则表达式**
var reg = /[hbc]at/gi;
Object.prototype.toString.call(reg); // "[object RegExp]"
**自定义类型**
function Person(name, age) {
    this.name = name;
    this.age = age;
}
var person = new Person("Rose", 18);
Object.prototype.toString.call(arr); // "[object Object]"
无法区分自定义对象类型,自定义类型可以采用instanceof区分
console.log(person instanceof Person); // true
**原生JSON对象**var isNativeJSON = window.JSON && Object.prototype.toString.call(JSON);
console.log(isNativeJSON);//输出结果为”[object JSON]”说明JSON是原生的,否则不是;

其次:instaceof

 var a = new Array();// alert(a instanceof Array); // true// alert(a instanceof Object) // true//如上, 会返回 true, 同时 alert(a instanceof Object) 也会返回 true;// 这是因为 Array 是 object 的子类。function Test() {};// var a = new Test();// alert(a instanceof test) // true

我们 new 一个对象,那么这个新对象就是它原型链继承上面的对象了,通过 instanceof 我们能判断这个对象是否是之前那个构造函数生成的对象,这样就基本可以判断出这个新对象的数据类型。

封装一个通用的类型判断

function getType(obj){
  let type  = typeof obj;
  if (type !== "object") {    // 先进行typeof判断,如果是基础数据类型,直接返回
    return type;
  }
  // 对于typeof返回结果是object的,再进行如下的判断,正则返回结果
  return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1');  // 注意正则中间有个空格
}
/* 代码验证,需要注意大小写,哪些是typeof判断,哪些是toString判断?思考下 */
getType([])     // "Array" typeof []是object,因此toString返回
getType('123')  // "string" typeof 直接返回
getType(window) // "Window" toString返回
getType(null)   // "Null"首字母大写,typeof null是object,需toString来判断
getType(undefined)   // "undefined" typeof 直接返回
getType()            // "undefined" typeof 直接返回
getType(function(){}) // "function" typeof能判断,因此首字母小写
getType(/123/g)      //"RegExp" toString返回

内存空间

栈空间和堆空间

这里的栈空间就是我们之前反复提及的调用栈,是用来存储执行上下文的。

function foo(){
 var a = " 极客时间 "
 var b = a
 var c = {name:" 极客时间 "}
 var d = c
}
foo()

当执行一段代码时,需要先编译,并创建执行上下文,然后再 按照顺序执行代码。

从图中可以看出来,当执行到第 3 行时,变量 a 和变量 b 的值都被保存在执行上下文中, 而执行上下文又被压入到栈中,所以你也可以认为变量 a 和变量 b 的值都是存放在栈中的。

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

从上图你可以清晰地观察到,对象类型是存放在堆空间的,在栈空间中只是保留了对象的引 用地址,当 JavaScript 需要访问该数据的时候,是通过栈中的引用地址来访问的,相当于多了一道转手流程。

为什么要分堆栈空间呢?

这是因为 JavaScript 引擎需要用栈来维护程序执行期间上下文的状态, 如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率。比如文中的 foo 函数执行结束了,JavaScript 引擎需要 离开当前的执行上下文,只需要将指针下移到上个执行上下文的地址就可以了,foo 函数执 行上下文栈区空间全部回收。

所以通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类 型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多 大的数据,不过缺点是分配内存和回收内存都会占用一定的时间。

在 JavaScript 中,赋值操作和其他语言有很大的不同,原始类型的赋值会完整复制变量 值,而引用类型的赋值是复制引用地址。

所以d=c的操作就是把 c 的引用地址赋值给 d

总结一下:

JS的数据类型分为原始类型和引用类型,除了object以外的是原始类型。原始类型的值存在栈内存中引用内存的值存在堆内存中,并会在栈内存中存放一个指向堆内存地址的变量。

原始类型我们可以是使用typeof。

引用类型我们可以我们可以使用instance检测引用类型object.prototype.toString.call()检测出所有类型。

为什么要分堆栈2个内存空间是因为 JavaScript 引擎需要用栈来维护程序执行期间上下文的状态, 如果栈空间过大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率。所以通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类 型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多 大的数据,不过缺点是分配内存和回收内存都会占用一定的时间。