值类型(基本类型)和栈内存
值类型也称为原始数据或原始值(primitive value).这类值存储在栈(stack)内存中, 基本类型的值不可以修改。每当我们定义一个变量,并赋给它一个基本类型的值时,可以理解为,我们为这个变量绑定了一个内存空间,这个内存空间存放的就是变量的值。因此。基本类型数据是存放在栈内存中的简单数据段,数据大小确定,内存空间大小可以分配。
目前js中的基本类型一共有六种:null
,undefined
,boolean
,number
,,string
,symbol
。其中symbol是es6中新加的数据类型。这些类型在内存中分别占用固定大小的空间,他们的值保存在栈空间,我们通过按值来访问的、
让我们看一个基本类型的值不可以修改的示例
var a = 4;
a = 3; //注意,这里是覆盖,不是修改
var num1 = 5;
var num2 = num1;
num1+=1;
consol.log(num1);//6
console.log(num2);//5
从上面第二个例子可以看到,从一个变量向另一个变量复制基本类型的值,我们会在变量对象上重新创建一个新值,然后把值复制到新变量分配的位置上,这两个值是完全独立的,对着两个变量进行操作是互不影响的。
引用类型
这类值存储在堆内存中,堆是内存中的动态区域,相当于自留空间,在程序运行期间会动态分配给代码和堆栈。对中存储的一般都是对象,然后在栈内存中存储一个变量指针,计算机通过这个变量指针,找到堆中的数据块并进行操作。这种访问方式,我们叫它按引用访问。如图所示。
堆的使用规则
var fruit_1 = "apple";
var fruit_2 = "orange";
var fruit_3 = "banana";
var oArray = [fruit_1,fruit_2,fruit_3];
var newArray = oAarray;
当创建数组时,就会在堆内存中创建一个数组对象,并且在栈内存中创建一个对数组的引用。变量fruit_1、fruit_2、fruit_3为基本数据类型,他们的值直接存放在栈中;newArray、oArray为符合数据类型(引用类型),他们的引用变量存放在栈中,指向于存放在堆中的实际对象。
此时我们改变oAarray中的值,对应的newArray也会改变,因为它们的存储的指针指向同一个堆地址。
console.log(oArray[1]);// 返回 orange
newArray[1]="berry";
console.log(oArray[1]);// 返回 berry
例如,下面代码将newArray赋值为null:
newArray = null;
注意:接触一个值的引用并不意味着自动回收改值所占用的内存。解除引用的真正作用时让值脱离执行环境,以便垃圾收集器下次运行时将其回收。
为什么会有栈内存和堆内存之分?
与垃圾回收机制有关,为了使程序运行时占用的内存最小。
当一个方法执行时,每个方法都会建立自己的内存栈,在这个方法内定义的变量会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁了。因此,所有在方法中定义的变量都是放在栈内存中的;
当我们在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复理由(因为对象的创建成本通常比较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法的结束而销毁,即使方法结束后,这个对象还可能被另一个引用变量所引用(方法的参数传递时很常见),则这个对象依然不会被销毁,只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。
函数中参数传递方式,按值传递和按引用传递
- 按值传递:函数的形参时被调用时所传实参的副本,修改形参并不会影响实参。
var num = 10;
function change(num){
num = num * 10;
}
change(num)
console.log(num); // 2
可以看到这里的变量num在运行完函数以后,值并没有发生改变。
- 按引用传递:函数的形参接收实参的内存地址,而不再是副本。这意味着函数形参的值如果被修改,实参也会被修改。
var ab={
x:1,
y:2
}
function foo(obj){
obj.x = 2
}
foo(ab)
console.log(ab); //{x:2,y:2}
可以看到,原来的ab对象,在函数foo调用之后,其中的对象属性发生变化。由上面的两个例子,我们是不是可以推断在js中,对于基本类型的数据,在函数传递过程中使用的时按值传递,而对于引用类型数据,在函数传递过程中使用的时是按引用传递方式呢?让我们再看另外一个例子。
var ab={
x:1,
y:2
}
function foo(obj){
obj = {
x:2,
y:3
}
}
foo(ab)
console.log(ab); //{x:1,y:2}
这个示例,如果按照我们刚刚说的结论,这里的函数运行后,输出的对象ab的值应该是{x:2,y:3}。但是真正的结果确实{x:1,y:2}。这就奇怪了,到底js中的函数参数的传递方式是按什么样的方式呢。我在网上找了很多资料,发现有个说法叫按共享传递。简单来说,就是对于基本类型,是按值传递,对于对象而言,直接修改形参对实参没有效果,而修改形参的属性却可以同时修改实参的属性。而我的理解是这样的。看一张图。
由我们上面对引用类型与堆内存的介绍,我们知道,当我们定义一个ab对象时,系统会在堆中开出一个空间用来存储改对象,对应的在栈内存中开出一个空间,用来存储指向对象的地址。如上图,根据按引用传递的方式,我们知道,foo函数内部的obj。在函数编译的时候,是指向ab所指向的对象的,二者共用一个地址。当函数执行后,obj指向了新的对象地址,但是之前的ab所指向的对象属性,依旧被ab所引用,没有任何改变。所以,这里输出的ab的值依旧未变。
而对于函数内部,改变形参的属性值这个情况,我们也可以很容易的清楚,因为函数内部的obj和ab对象共同指向同一个对象空间,所以改变前者的对象属性的值,自然会影响后者
一道经典的面试题
var a = {n:1};
var b = a;
a.x = a = {n:2};
console.log(a.x);
console.log(b.x)
这道题考察了很多东西,js中的运算符的优先级,比如赋值运算,是从右到左的。js中的对象存储的问题。但是这道题很容易根据赋值运算是从右到左的顺序运行的来得到错误的结果
a = {n:2}
a.x = a
然后得到了错误的答案。a.x = {n:2}.实际上,要解出这道题,我们至少要知道两个,第一个就是运算符的优先级问题,点运算符的优先级高于赋值运算级。所以这里的执行顺序第一步肯定是a.x,然后才是从右到左的赋值运算;第二个,我们要知道的是,js对象在内存中是如何存储的。知道这两点,那这道题就不难解决了。同样的,我们继续看几张图。。
通过该题的前两行代码的声明a和b,结合上面所说的对象存储的原理,可以很容易看明白。a和b指向同一个对象空间。
a.x = a = {n:2}
这行代码,首先会运行a.x。这样便会在{n:1}对象所存储的空间上添加一个x属性名,并且等待赋值。即原来的{n:1}变成{n:1,x:undefined}。然后,按照赋值运算的顺序,先将变量a的指向变为{n:2},但是这里要注意,{n:1,x:undefined}由于被b引用,所以依旧存在在内存当中。然后执行 ax.x = a,这里要注意,这里的a已经是{n:2}了。而a.x则是b指向的{n:1,x:undefined}中的x属性,所以最终b指向的{n:1,x:undefined}变为{n:1,x:{n:2}}。
所以最后的结果。看下图。
console.log(a.x); //undefined
console.log(b.x); //{n:2}
参考链接:
https://www.imooc.com/article...
https://blog.csdn.net/lxiang2...
https://blog.csdn.net/xdd1991...
https://segmentfault.com/a/11...
https://blog.csdn.net/u012860...