内存以及内存图
在JS中,每一个数据都需要一个内存空间。内存空间又被分为两种,栈内存(stock)与堆内存(heap)。JS内存空间分为栈(stack)、堆(heap)、池(一般也会归类为栈中)。 其中栈存放变量,堆存放复杂对象,池存放常量。
JS中的基础数据类型,这些值都有固定的大小,往往都保存在栈内存中(闭包除外),由系统自动分配存储空间。我们可以直接操作保存在栈内存空间的值,因此基础数据类型都是按值访问 数据在栈内存中的存储与使用方式类似于数据结构中的堆栈数据结构,遵循后进先出的原则。 基础数据类型: Number
String
Null
Undefined
Boolean
~ ~ 要简单理解栈内存空间的存储方式,我们可以通过类比乒乓球盒子来分析。
这种乒乓球的存放方式与栈中存取数据的方式如出一辙。处于盒子中最顶层的乒乓球5,它一定是最后被放进去,但可以最先被使用。而我们想要使用底层的乒乓球1,就必须将上面的4个乒乓球取出来,让乒乓球1处于盒子顶层。这就是栈空间先进后出,后进先出的特点。
JS的复杂数据类型,比如数组Array,它们值的大小是不固定的。引用数据类型的值是保存在堆内存中的对象。JS不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。因此,引用类型的值都是按引用访问的。这里的引用,我们可以粗浅地理解为保存在栈内存中的一个地址,该地址与堆内存的实际值相关联。 堆存取数据的方式,则与书架与书非常相似。 书虽然也有序的存放在书架上,但是我们只要知道书的名字,我们就可以很方便的取出我们想要的书,而不用像从乒乓球盒子里取乒乓一样,非得将上面的所有乒乓球拿出来才能取到中间的某一个乒乓球。好比在JSON格式的数据中,我们存储的key-value是可以无序的,因为顺序的不同并不影响我们的使用,我们只需要关心书的名字。
JS里所有的数字都是以64位浮点数储存的,16位存储一个字符,所以在栈内存内都是64位01
我们来看看代码:
var a = 20
var b = a
b=30
b=a,那就把a存的东西复制然后覆盖到b储存的地方。
在栈内存中的数据发生复制行为时,系统会自动为新的变量分配一个新值。var b = a
执行之后,a与b虽然值都等于20,但是他们其实已经是相互独立互不影响的值了。具体如图。所以我们修改了b的值以后,a的值并不会发生变化。
再复杂一点,我们存储复杂类型呢?也就是heap内存里是怎样呢?
var m = { a: 10, b: 20 }
var n = m;
n.a = 15;// 这时m.a的值是多少
就像上述代码,当需要存储字符的时候,一行64位浮点数,只能存储4个字符,非常浪费且再添加属性的时候就需要整体移动下位的代码,很麻烦,所以我们就在栈内存里存储一个地址,地址随意,但是这个地址指向heap内存里相应地址的位置,而我们需要储存的内容就写在这里。
我们通过var n = m
执行一次复制引用类型的操作。引用类型的复制同样也会为新的变量自动分配一个新的值保存在栈内存中,但不同的是,这个新的值,仅仅只是引用类型的一个地址指针。当地址指针相同时,尽管他们相互独立,但是在堆内存中访问到的具体对象实际上是同一个。
如图所示
因此当我改变n时,m也发生了变化。这就是引用类型的特性。
以上就是js中的内存和内存图,遇到不明白的地方,多画图就能弄明白了。
内存释放
因为JavaScript具有自动垃圾收集机制,JavaScript的内存生命周期是
1. 分配你所需要的内存
2. 使用分配到的内存(读、写)
3. 不需要时将其释放、归还
为了便于理解,我们使用一个简单的例子来解释这个周期。
var a = 20; // 在内存中给数值变量分配空间
alert(a + 100); // 使用内存
a = null; // 使用完毕之后,释放内存空间
第一步和第二步我们都很好理解,JavaScript在定义变量时就完成了内存分配。第三步释放内存空间则是我们需要重点理解的一个点。
JavaScript有自动垃圾收集机制,那么这个自动垃圾收集机制的原理是什么呢?其实很简单,就是找出那些不再继续使用的值,然后释放其占用的内存。垃圾收集器会每隔固定的时间段就执行一次释放操作。
var a = {name:'xx'}
var b = {name:'yy'}
// 代码区 // stack //Heapa ADDR 888 xxb ADDR 666 yy
// 当b =a
//代码区 // stack //Heapa ADDR 888 xxb ADDR 888 (yy未被引用,被释放)
在JavaScript中,最常用的是通过标记清除的算法来找到哪些对象是不再继续使用的,因此a = null
其实仅仅只是做了一个释放引用的操作,让 a 原本对应的值失去引用,脱离执行环境,这个值会在下一次垃圾收集器执行操作时被找到并释放。而在适当的时候解除引用,是为页面获得更好性能的一个重要方式。
在局部作用域中,当函数执行完毕,局部变量也就没有存在的必要了,因此垃圾收集器很容易做出判断并回收。但是全局变量什么时候需要自动释放内存空间则很难判断,因此在我们的开发中,需要尽量避免使用全局变量。
深拷贝与浅拷贝
var a = 1 var b = a b = 2 a // 1
像这样,b改变不会影响a,这就是深拷贝
对于所有的基本类型,赋值都是深拷贝,所以我们来研究对象。
var a={name:'}
var b=a
b.name='b'
a.name//'b'
如上述,b.name = 'b' ,改变name的值,引用a,得到的也是改变后的。
像这样,b的改变会导致a的改变,就是浅拷贝。
全局对象 window
ECMAScript 规定全局对象叫做 global,但是浏览器把 window 作为全局对象(浏览器先存在的)
window 就是一个哈希表,有很多属性。
window 的属性就是全局变量。
这些全局变量分为两种:
- 一种是 ECMAScript 规定的
- global.parseInt
- global.parseFloat
- global.Number
- global.String
- global.Boolean
- global.Object
- 一种是浏览器自己加的属性
- window.alert
- window.prompt
- window.comfirm
- window.console.log
- window.console.dir
- window.document
- window.document.createElement
- window.document.getElementById
所有 API 都可以在 MDN 里找到详细的资料。
今天我们学习第一种全局变量。
全局函数
- Number
var n = new Number(1) 创建一个 Number 对象
1 与 new Number(1) 的区别是什么? - String
var s = new String('hello') 创建一个 String 对象
'hello' 与 new String('hello') 的区别是什么? - Boolean
var b = new Boolean(true) 创建一个 Boolean 对象
true 与 new Boolean(true) 的区别是什么? - Object
var o1 = {}
var o2 = new Object()
o1 和 o2 没区别
上面的区别在于,只要我们引用上述字符串的属性,JavaScript就会将字符串值通过调用new String(s)的方式转换成对象,之后字符串继承了对象的方法,处理属性的引用。属性引用结束,这个新创建的对象就被销毁。
我们可以这样来想,我们既想用简单类型,又想获得对象的方法,然后Branden Eich就想了个办法,我们可以建立临时对象,获取属性后返回给调用,然后销毁就可以。
var n = ‘a'
n.toString()
// 代码区 // stack // heapn 'a'temp ADDR 888 888: 'a'toSting()valueOf()
temp就是临时对象,当调用toString返回后,temp马上被销毁了。
其他的数据类型也是一样,
调取属性的背后都是这样一套操作。
原型与原型链
从上面我们可以看到,利用new创建并初始化一个新对象,运算符new后面跟着一个函数调用,叫做构造函数。
var s = new String('hello')
如上述代码,s就是被创建的实例对象,new运算符后跟着的String(注意这里开头必须大写来和string区分)就是构造函数。
我们可以看到从实例对象中调用的属性,但是他们都具有的属性比如toString以及valueOf等,如果每个实例对象都在heap存储处生成这些都有的属性岂不是很费内存?所以我们可以共有属性归拢起来,然后通过 __proto__ 来指向共有属性。公用的属性藏在哪
JavaScript中的对象,都有一个内置属性[[Prototype]]
,指向这个对象的原型对象。当查找一个属性或方法时,如果在当前对象中找不到定义,会继续在当前对象的原型对象中查找;该原型对象也有一个自己的原型对象(__proto__) ,层层向上直到一个对象的原型对象为null
。根据定义,null
没有原型,并作为这个原型链中的最后一个环节。
可以看出,这个查找过程是一个链式的查找,每个对象都有一个到它自身原型对象的链接,这些链接组件的整个链条就是原型链。拥有相同原型的多个对象,他们的共同特征正是通过这种查找模式体现出来的。
在上面的查找过程,我们提到了最顶层的原型对象,这个对象就是Object.prototype
,这个对象中保存了最常用的方法,如toString
、valueOf
、hasOwnProperty
等,因此我们才能在任何对象中使用这些方法。
如下图:
重要公式
var 对象 = new 函数()
对象.__proto__ === 对象的构造函数.prototype// 推论
var number = new Number()
number.__proto__ = Number.prototype
Number.__proto__ = Function.prototype // 因为 Number 是 Function 的实例var object = new Object()
object.__proto__ = Object.prototype
Object.__proto__ = Function.prototype // 因为 Object 是 Function 的实例var function = new Function()
function.__proto__ = Function.prototype
Function.__proto__ == Function.prototye // 因为 Function 是 Function 的实例!
(以上部分资料来自网络,仅供自己学习参考,侵删)