关于javasript中参数传递的深研究

2022-08-01,,,

关于《javascript高级程序设计》中参数传递这一节,发现好多人不理解,看了博客上许多人在解释这个问题,好多博主解释的也是不够深刻,甚至有些错误。我专门读了一下英文原版,结合李松峰和曹力的翻译中国版,翻译的质量还是十分高的,这一节并没有翻译疏忽或者有谐意,只是大家对于引用的理解不透彻,和对于数据类型的存储方式不清楚,导致许多人一直不明白引用类型的复制和访问还有对象的参数传递问题。

鉴于这个问题:

那我们就好好细说一下这个关于参数传递的问题。

在说这个问题之前呢,我们先来了解一下关于基本类型和引用类型,还有两者的保存方式和访问方式还有两者的复制方式

 在 javascript 中数据类型可以分为两类:

  • 基本类型值        比如Undefined,Null,Boolean,Number,String。
  • 引用类型值       (也就是对象类型)   比如Object,Array,Function,Date等。  

基本类型和引用类型的保存方式:

基本类型存储在栈区。

引用的存储需要内存的栈区和堆区共同完成,栈区内存保存的是变量标识符和堆内存中该对象的指针(地址值),堆区存储的是对象的内容。

首先我们来讲一下栈内存和堆内存

        这是两种不同的内存分配方式:一般代码逻辑,简单变量。结构体都是放在栈区;而对象,以及封装的数据放在堆区。

 基本类型就是保存在栈内存中的简单数据段,而引用类型指的是那些保存在堆内存中的对象

基本类型在内存中分别占有固定大小的空间,他们的值保存在栈空间中,我们通过按值访问

引用类型:值的大小不固定,栈内存中存放地址指向堆内存中的对象,是按引用访问的。栈内存中存放的只是该对象的访问地址

3.在堆内存中为这个值分配空间。由于这种值的大小不固定,因此不能把他们保存在栈内存中。但内存地址大小是固定的,因此可以将内存地址保存在栈内存中。这样,当查询引用类型的变量时,先从栈中读取内存地址,然后再通过地址找到堆中的值。对于这种,我们把它叫做按引用访问。

当我们看到一个变量类型是已知的,就分配在栈里面,比如INT,Double等,其它未知的类型,比如自定义类型,因为系统不知道需要多大,所以程序自己申请,这样就分配在堆里面。基本类型大小固定,引用类型大小不固定,分开存放使得程序运行占有内存最小。

4.基本类型在当前执行环境结束时销毁,而引用类型不会随执行环境结束而销毁,只有当所有引用它的变量不存在是这个对象才被垃圾回收机制回收。

下面来深刻举个引用类型的存储方式例子:

假如有以下几个对象:

这三个对象在内存中保存的情况如下图:


var person1 = {name: 'Mary'};

var person2 = {name: 'xioaming'};

var person3 = {name: 'xiaohua'};

大概现在你就能理解了!

如果没有学过栈和堆的同学下面可以详细看一看下面{{{{{{{{。。。}}}}}}}}}}内蓝色部分,懂得上面讲的同学下面蓝色部分就不用看了!可以跳过继续往下看了!

   {{{{{{{{

引用类型使用原始值和引用值:

原始值和引用值存储在内存中的位置分别是栈和堆。原始值是存储在栈中的简单数据段,他们的值直接存储在变量访问的位置。引用值是存储在堆中的对象。

栈:

主要表现为一种数据结构,是只能在某一端插入和删除的特殊线性表。它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读取数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。 
栈是允许在同一端进行插入和删除操作的特殊线性表。允许进行插入和删除操作的一段成为栈顶,另一端称为栈底;栈底固定,而栈顶浮动 ;栈中元素个数为零时,称为空栈。插入一般称为进栈(PUSH),删除则称为退栈(POP)。栈也称为后进先出表。 
存储在栈中的值是一个指针,指向存储在堆中的实际对象
关于基本变量的交换,按值访问操作的是他们实际保存的值

 function qk()
 {
   var store1 = "China";
   var store2 = store2;
   store1 = "USA";
   alert(store2);//China

 }
 qk();

将基本类型store1传递给另一个变量(赋值)时,其实是分配了一块新的内存空间,因此改变了store1的值对store2没有任何影响。

堆:

在程序中,堆用于动态分配和释放程序所使用的对象。在以下情况中调用堆操作:

1.事先不知道程序所需对象的数量和大小。

2.对象太大,不适合使用堆栈分配器。

堆使用运行期间分配给代码和堆栈以外的部分内存。

在javascript中,引用数据是放在堆中的,例如数组和对象,因为在javascript中,一切都是对象,对象可以进行扩展,放置在堆中可以进行不断的扩展,如果放在内存中就会消耗大量资源。放置在堆中的数据的查询效率比较低。这也是内存优于堆的好处,但是内存的存储空间要比堆的小很多。 
按引用访问,当查询时,需要先从栈中读取内存地址,然后再顺藤摸瓜找到保存在堆内存中的值
 
 

var obj1 = new Object(); 
          obj1.name = "obji name"; 
          console.log(obj1.name);//输出obj1 name 
          var obj2 = obj1; 
          console.log(obj2.name);//输出obj1 name 
          obj1.name = "qk"; 
          console.log(obj2.name);//qk 

栈的优势就是存取速度比堆要快,仅次于直接位于CPU中的寄存器,但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,垃圾收集器会自动地收走这些不再使用的数据,但是缺点是由于在运行时动态分配内存,所以存取速度较慢。

所以相对于简单数据类型而言,他们占用内存比较小,如果放在堆中,查找会浪费很多时间,而把堆中的数据放入栈中也会影响栈的效率。比如对象和数组是可以无限拓展的,正好放在可以动态分配大小的堆中。

}}}}}}}}}}}}}}

下面讲一下变量的复制

   众所周知,js中变量的基本类型和引用类型保存方式是不同的,这也就导致变量复制时也就不同了。如果从一个变量向另一个变量复制基本类型的值时,会将前者的值克隆一个,然后将克隆的值赋值到后者,因此这两个值是完全独立的,只是他们的value相同而已。

下面看书中的这个例题:

var num1 = 10;
var num2 = num1;
console.log(num2);//10

上面的num1中被保存的值为10,当把num1的值赋值给num2时,num2的值也为10。但是这两个10是完全独立的,num2中的10只是被克隆出来的,相当于我写了一个word文档,把它放到了num1的文件夹中,然后我再复制这个word文档,就叫word副本吧,然后把这个副本放到num2的文件夹下,这两个word文档是完全一样的,修改任何一个都不会影响两一个。

num2 += 1;
console.log(num1); //10
console.log(num2); //11

从上面可以看出修改num2的值,num1的值未发生变化。

再来看下引用类型的复制。当从一个变量向另一个变量复制引用类型的值时,同样也会将存储在变量对象中的值复制一份放到为新变量分配的空间中。

var obj1 = {
  name : "java"
};
var obj2 = obj1;
console.log(obj2.name); //java
obj2.name = "c++";
console.log(obj1.name); //c++

第一次打印出的结果为“java”,这个我们很容易理解,但是第二次打印出来的是“c++”,有点莫名其妙了。这就是引用类型和基本类型的不同之处了。复制对象时并不会在堆内存中新生成一个一模一样的对象,只是多了一个保存指向这个对象指针的变量罢了。将obj1的值复制给obj2,而这个值的副本实际上是一个指针,这个指针指向存储在堆中的一个对象,也就是说创建了一个新的内存地址传给了obj2obj1和obj2两个变量同时指向了同一个Object,当去改变这个对象时,他们的值都会改变,也就是说他们中任何一个作出的改变都会反映在另一个身上。下面的简易图可能更明了些。

函数参数的传递 

      《js高级程序设计》上是这样叙述参数传递的:所有函数的参数都是按值传递的,也就是说把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。所以如果能理解变量的复制,那么参数的传递也就很简单了。还是先从基本类型举例说明吧。

var count = 10;
function num(num1){
   num1 = 1;
   return num1;
}
var result = num(count);
console.log(result);//1
console.log(count);//10,并未变成1

这个例子很容易理解,实际就是创建了一个count的副本,然后把count的值传入参数中,因为函数中定义了参数的值,所以1就将10覆盖了,最后的result返回1,而count并未发生变化。看一个有关传递对象的例子。

var person  = {
    name : "Tom"
};
function obj(peo){
    peo.name = "Jerry";
    return peo;
}
var result = obj(person);
console.log(result.name);// Jerry
console.log(person.name);// Jerry

   在上面的例子中,把person复制传入obj()中,peo和person指向了同一个对象,而在peo中修改了name属性,其实修改了它们共同指向的对象的name属性,相对应的外部person所引用的name属性也就改变了,所以打印出来的为Jerry。其实这个乍一看,感觉引用类型的参数是按照引用传递的,这就是我最初犯得错误。再来看一个例子。

var person = {
    name : "Tom"
}; 
function obj(peo){
    peo = {
       name : "Jerry"
    };
    return peo;
}
var result = obj(person);
console.log(result.name);// Jerry
console.log(person.name);// Tom
 

  上面的例子中,在函数中重新定义了一个对象,也就是现在堆内存中有两个对象,外部的person指向的是老的对象,被传入参数后指向的是新定义的对象,所以调用后返回的值是新定义的对象的值。如果是参数是按引用传递的,那么person.name打印出来的结果为Jerry,从这点可以得出参数是按值传递的(有的地方叫做按共享传递)。

一定要理解两者访问方式和复制方式,参数传递方式

访问基本类型和引用类型时:

访问基本类型是按值访问的,访问引用类型是按引用访问的

复制时:

var obj1=new object();

var obj2=obj1;

obj1.name="siyu";

alert(obj2.name);  // "siyu"

 

咱们以上面这个例子为主

基本类型和引用类型都是按值复制的,只是引用类型的复制是复制的地址(即指针,也是值)。(也就是说都是按值复制的,只是引用类型复制的是值是一个地址(这个地址是对象object()的地址,即是堆内存的地址,不是obj1栈内存所在地址),假设引用类型的复制是按引用复制的,从引用的本义来讲,本应该是参数复制变量obj1的所在地址值(实际是复制的obj1中保存的栈区的值,只是栈区的这个值是一个地址值而已,和变量obj1本身所在的地址不是同一个值),注意:变量obj1的所在地址值和变量obj1保存的值是不一样的,一个是值,一个是所在地址但是:实际情况是复制的是变量的值,只是这个值是对象的地址。不是变量的地址。

主要是理解引用类型在复制时,究竟是复制的对象堆区的地址还是变量自己所在的栈区的地址值?

这是区别按值复制和按引用复制的核心点,理解了这点你就能深刻理解这个问题了!

传递参数时:

都是按值传递的,类似与基本类型和引用类型的复制,假设参数是按照引用传递的,从引用的本义来讲,应该是参数复制变量栈区的地址(实际是对象堆区的值,此处堆区对象的值只是一个地址值而已,和变量本身所在的地址不是一个值),而实际是传递的对象堆区的地址值不是变量栈区的地址值,显示假设不成立。由此得出,参数传递都是按照值传递的。

 

 

本文地址:https://blog.csdn.net/weixin_45147894/article/details/107546952

《关于javasript中参数传递的深研究.doc》

下载本文的Word格式文档,以方便收藏与打印。