目录
1、存储位置的区别
基本数据类型和引用数据类型存储在内存中的位置不同:
① 基本数据类型存储在栈中 ② 引用类型的对象存储于堆中
原始数据类型,也就基本数据类型。它的原始值存储在栈stack中的简单数据段。也就是说,它们的值直接存储在变量访问的位置。这是因为,这些原始类型会占据的空间是固定的,所以可以将它们存储在较小的内存区域——栈中,这样存储便于迅速查询变量的值。
基本数据类型的特点:占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
引用类型的值存储在堆heap中的对象,也就是说,存储在变量处的值是一个指针,指向存储对象的内存地址。这是因为:引用值的大小会改变,所以不能把它放在栈中,否则会降低变量查寻的速度。相反,放在变量的栈空间中的值是该对象存储在堆中的地址。地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响。
当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
堆和栈的概念存在于数据结构和操作系统内存中;
在数据结构中,栈中数据的存取方式为先进后出。
堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定在操作系统中,内存被分为栈区和堆区
栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
堆区内存一般由开发着分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收.
2、变量赋值时的区别
当我们把变量赋值给一个变量时,解析器首先要确认的就是这个值是基本类型值还是引用类型值。
① 基本数据类型
基本类型数据是存放在栈中
let a = 10; let b = a; // 赋值操作 b = 20; console.log(a); // 10值
a
的值为一个基本类型,是存储在栈中,将a
的值赋给b
,虽然两个变量的值相等,但是两个变量保存了两个不同的内存地址。下图演示了基本类型赋值的过程:
② 引用数据类型
引用类型数据存放在堆中,每个堆内存对象都有对应的引用地址指向它,引用地址存放在栈中。
由于这种值的大小不固定,因此不能把它们保存到栈内存中。但内存地址大小的固定的,因此可以将内存地址保存在栈内存中。
当查询引用类型的变量时, 先从栈中读取内存地址, 然后再通过地址找到堆中的值。
var obj1 = {} var obj2 = obj1; obj2.name = "小草莓"; console.log(obj1.name); // 小草莓
obj1
是一个引用类型,在赋值操作过程中,实际是将堆内存对象在栈内存的引用地址复制了一份给了obj2
,实际上他们共同指向了同一个堆内存对象,所以更改obj2
会对obj1
产生影响。下图演示这个引用类型赋值过程:
3、参数传递的不同
首先我们应该明确一点:ECMAScript中所有函数的参数都是按值来传递的。
但是为什么涉及到原始类型与引用类型的值时仍然有区别呢,还不就是因为内存分配时的差别。 (我对比了一下,这里和复制变量时遵循的机制完全一样的嘛,你可以简单地理解为传递参数的时候,就是把实参复制给形参的过程)
① 原始值:只是把变量里的值传递给参数,之后参数和这个变量互不影响。
function addTen(num) {
num += 10;
return num;
}
let count = 20;
let result = addTen(count);
console.log(count); // 20,没有变化
console.log(result); // 30
② 引用值:对象变量它里面的值是这个对象在堆内存中的内存地址
这一点你要时刻铭记在心!因此它传递的值也就是这个内存地址,这也就是为什么函数内部对这个参数的修改会体现在外部的原因了,因为它们都指向同一个对象呀。
所以,如果是按引用传递的话,是把第二格中的内容(也就是变量本身)整个传递进去(就不会有第四格的存在了)。但事实是变量把它里面的值传递(复制)给了参数,让这个参数也指向原对象。
因此如果在函数内部给这个参数赋值另一个对象时,这个参数就会更改它的值为新对象的内存地址指向新的对象,但此时原来的变量仍然指向原来的对象,这时候他们是相互独立的;但如果这个参数是改变对象内部的属性的话,这个改变会体现在外部,因为他们共同指向的这个对象被修改了呀!
来看下面这个例子吧:(传说中的call by sharing)
var obj1 = { value:'111' }; var obj2 = { value:'222' }; function changeStuff(obj){ obj.value = '333'; obj = obj2; return obj.value; } var foo = changeStuff(obj1); console.log(foo); // '222' 参数obj指向了新的对象obj2 console.log(obj1.value); //'333'
obj1仍然指向原来的对象, 之所以value改变了, 是因为changeStuff里的第一条语句,这个时候obj是指向obj1的 。再啰嗦一句,如果是按引用传递的话,这个时候obj1.value应该是等于'222'的
例子2:
function setName(obj) { obj.name = "Nicholas"; obj = new Object(); obj.name = "Greg"; } let person = new Object(); setName(person); console.log(person.name); // "Nicholas"
当 person 传入 setName()时,其 name 属性被设置为"Nicholas"。然后变量 obj 被设置
为一个新对象且 name 属性被设置为"Greg"。如果 person 是按引用传递的,那么 person 应该自动将指针改为指向 name 为"Greg"的对象。可是,当我们再次访问 person.name 时,它的值是"Nicholas",这表明函数中参数的值改变之后,原始的引用仍然没变。
当 obj 在函数内部被重写时,它变成了一个指向本地对象的指针。而那个本地对象在函数执行结束时就被销毁了。
注意 ECMAScript 中函数的参数就是局部变量。
例子3:
function test(m) { m = { v: 5 }; } var m = { k: 30 }; test(m); console.log(m); // { k:30 }
var obj = { value: 1 }; function foo(o) { o = 2; console.log(o); // 2 } foo(obj); console.log(obj.value) // 1
如果 JavaScript 采用的是引用传递,外层的值也会被修改呐,这怎么又没被改呢?所以真的不是引用传递吗?
这就要讲到其实还有第三种传递方式,叫按共享传递。
而共享传递是指,在传递对象的时候,传递对象的引用的副本。
注意:
按引用传递是传递对象的引用,而按共享传递是传递对象的引用的副本!
所以修改 o.value,可以通过引用找到原值,但是直接修改 o,并不会修改原值。所以第二个和第三个例子其实都是按共享传递。
最后,你可以这样理解:
参数如果是基本类型是按值传递,如果是引用类型按共享传递。
但是因为拷贝副本也是一种值的拷贝,所以在高程中也直接认为是按值传递了。
4、小结
① 声明变量时不同的内存地址分配
简单数据类型存储在栈中,在栈中存放的是对应的值,是按值来访问的;
引用数据类型对应的值存储在堆中,在栈中存放的是指向堆内存的地址,是按引用访问;
在javascript中是不允许直接访问保存在堆内存中的对象的,所以在访问一个对象时,首先得到的是这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象中的值,这就是传说中的 按引用访问。
而原始类型的值则是可以直接访问到的。
② 不同的类型数据导致赋值变量时的不同
简单类型赋值,是生成相同的值,两个变量对应不同的地址,是完全独立的;
复杂类型赋值,是将保存对象的内存地址赋值给另一个变量。也就是两个变量指向堆内存中同一个对象;当其中一个对象改变时,另一个对象也会变化。
简单类型赋值:在将一个保存着原始值的变量复制给另一个变量时,会将原始值的副本赋值给新变量,此后这两个变量是完全独立的,他们只是拥有相同的value而已。
复杂类型赋值:复制对象时并不会在堆内存中新生成一个一模一样的对象,只是多了一个保存指向这个对象指针的变量罢了
③ 不同类型数据的参数传递
参数如果是基本类型是按值传递,如果是引用类型按共享传递,传递对象的引用的副本!