JavaScript引用类型传值原理解析

1,662 阅读4分钟

1、抛出问题

1.1、问题分析

题目如下:

// 例子1,引用类型
var arr = [1,2,3]
function fn(a){
  var a = [4,5,6]
}
fn(arr)
arr; // [1, 2, 3]

为什么arr传进去没有任何变化,难道作为引用类型的arr不应该变成[4, 5, 6]? 理由:

fn()函数内部的变量a是用var关键字重新声明的,即重新申请分配了内存空间,与形参a指向的外部引用类型arr不在同一片内存空间,所以互不影响。

那么,再看一个例子:

// 例子2
var arr = [1,2,3]
function fn(a){
  a = [4,5,6]
}
fn(arr)
arr; // [1, 2, 3]

看似fn()函数内的a已经是用引用类型了,为什么还是没变?理由:

fn()函数的形参a指向引用arr的内存地址,内存地址是0x0000001格式,转换过来也是基本数据类型,a=[4,5,6]相当于1=2,根据基本数据类型值的传递概念,这样子是不会影响到外部的arr的。

这里引入指针的概念即形参跟实参不是指针类型变量,他们在内存中位于不同的位置,形参将实参的内容复制一份,在该函数运行结束的时候形参被释放,而实参内容不会改变。

fn()函数内部引用类型使用应该是 a[0] = 4,这样就改变了引用类型指向的那片内存空间的数据

1.2、参数传递

在JavaScript中,函数的参数传递是按值传递的:

1、基本数据类型值的传递按基本数据类型变量复制,即外部的值跟内部的值不会相互影响

基本数据类型复制时会重新分配内存空间

2、引用类型数据值的传递按引用类型变量复制,即内外都会相互影响

引用类型复制时只是复制其内存地址(指针),不会重新分配内存空间

2、js数据类型在内存中的存放位置

下面讲下js中基本数据类型、引用数据类型在内存中的存放位置

2.1、基本数据类型存放在栈区

基本的数据类型有:Boolean、Number、String、undefined、null、Symbol
举个简单的例子:

var x = 1;
var y = 2;

x、y在内存中的存放位置如下图:

栈区

2.2、引用类型的值同时保存在栈内存和堆内存中

例子1的arr在内存中的存放位置:

再看一个例子

// 例子3,改变引用类型
var arr = [1,2,3]
function fn(a) {
  a[0] = 4
}
fn(arr);
arr; // [4, 2, 3]

分析:

1、调用fn(arr),这里传入的是arr变量名,即arr的堆内存地址
2、函数fn内调用a[0],即访问arr的堆内存地址指向的其在堆区的数据arr[0],即改变了arr[0]在堆区存放的内容1→4
3、所以再次调用arr,其堆内存地址指向的堆区数据已经改变为[4, 2, 3]

同为引用类型的Object也是一样。

3、赋值

3.1、基本类型赋值

var x = 1;
var y = x;
y = 2;
x; // 1

同样以内存分析上面的代码:

基本类型的赋值是在栈内存中开辟一块新的存储区域存放变量,彼此间相互独立,所以改变其中的一个值,不会影响到另一个。

var y = x; 先在栈区开辟一块新内存存储变量y,再复制一份x的值存到y
y=2; 改变的是栈区y地址的数据1→2,所以x的值保持不变

3.2、引用类型赋值

var obj1 = {x: 1, y: 2};
var obj2 = obj1;
obj2.x = '你猜';
obj1; // {x: '你猜', y: 2}

同样以内存分析上面的代码:

引用类型变量在栈区开辟内存存放变量名以及指向堆区数据的地址,在堆区开辟内存存放数据:

var obj2 = obj1; 在栈区开辟一块新地址存放变量obj2,obj2的堆内存地址指向堆区的对象{x:1, y:2}
obj2.x = '你猜'; 改变了堆区的数据1→"你猜"
obj1和obj2指向堆区的同一个内存地址的数据,所以obj1.x的结果也改变

下面这个不会有影响:

var obj1 = {x: 1, y: 2};
var obj2 = obj1;
obj2 = {x: 1, y: 2, z: 3};
obj2.x = 4;
obj1; // {x: 1, y: 2}

一图胜千言:

obj2开始指向与obj1堆区地址一样的数据,在obj2 = {x: 1, y: 2, z: 3};执行后就转而指向了数据{x: 1, y: 2, z: 3}在堆区存放的地址,所以后面对obj2做任何操作都不会影响obj1

扩展

由上自然而然想到深浅拷贝,转下篇js的深拷贝和浅拷贝