Write By Monkeyfly
以下内容均为原创如需转载请注明出处。
第 4 章 变量、作用域和内存问题
本章内容
- 理解基本类型和引用类型的值
- 理解执行环境
理解垃圾收集
4.1 基本类型和引用类型的值4.1.1 动态的属性4.1.2 复制变量值4.1.3 传递参数 【疑难点】4.1.4 检测类型4.2 执行环境及作用域4.2.1 延长作用域链4.2.2 没有块级作用域 4.3 垃圾回收4.3.1 标记清除 4.3.2 引用计数4.3.3 性能问题 4.3.4 管理内存
前言
Javascript 变量松散类型的本质决定了它只是在特定时间用于保存特定值的一个名字而已。 由于不存在定义某个变量必须要保存何种数据类型值的规则变量的值及其数据类型可以在脚本的生命周期内改变。
4.1 基本类型和引用类型的值
ECMAScript变量包含两种不同数据类型的值基本类型值 和 引用类型值。
- 基本类型值就是简单的数据段。
- 引用类型值指那些可能由多个值构成的对象。
注意 当把一个值赋值给变量时JS解析器必须确定这个值是基本类型值还是引用类型值。
我们都知道存在
- 5种基本数据类型Undefined、Null、Boolean、Number、String
- 1种复杂数据引用类型Object
说明
- 这 5种基本数据类型 是 按值访问 的。
- 而这 1种复杂数据引用类型的值 则是 按引用访问 的。
Q
- 问什么叫按值访问
答就是变量中保存的是实际的值而且这个值是可以直接进行操作的。
问为什么复杂数据类型的值是按引用访问的
- 答因为复杂数据引用类型的值保存在了内存中的对象中。而对象并不能直接访问只能通过引用来访问。
我们要知道 与其他语言不同Javascript不允许直接访问内存中的位置。换句话说就是不能直接操作对象的内存空间。
现在就明白了 我们平时在操作对象时操作的并不是对象本身或者说实际的对象而是对象的引用。
因此引用类型的值是按引用访问的。
提示 在很多语言中字符串都是以对象的形式来表示的所以它才会被认为是引用类型的。但是在ECMAScript中放弃了这一传统。
4.1.1 动态的属性
定义 基本类型值 和 引用类型值 的方式都差不多创建一个变量并为该变量赋值。
但是 当这个值保存到变量中以后对不同类型值执行的操作是不一样的。
说明 1对引用类型值来说我们可以给这个值添加属性和方法也可以改变和删除它的属性和方法。 2但是对基本类型值来说我们不能给它添加属性。虽然这种做法不会导致任何错误
示例1
var person new Object();person.name "fly";alert(person.name);
分析: 1首先是创建了一个对象并将其保存在了变量 person 中。 2然后我们为这个对象添加了一个名为name的属性并且将一个字符串值"fly"赋值给了这个name属性。 3接下来通过alert()方法 访问了这个新添加的 name属性。
说明 如果对象不被销毁或者这个属性不被删除那么这个属性将会一直存在。
示例2
var name "fly";name.age 24;alert(name.age); //undefined
分析 1首先我们定义了一个name变量其中保存了一个字符串类型的值"fly" 2然后我们为字符串name定义了一个名为age的属性并且为该属性赋值24 3但是等我们访问这个age属性时发现并没有该属性并且提示undefined。
总结 1以上案例说明只能给引用类型值动态的添加属性。 2动态添加属性的目的就是为了方便将来使用。
4.1.2 复制变量值
除了保存方式的不同之外基本类型值和引用类型值还存在不同之处
在从一个变量向另一个变量复制值时就体现出了两者的差异。
1、如果复制的是基本类型的值则会在变量对象上再创建一个新值然后将这个新值赋值到为新变量分配的位置上。此时相当于拥有了两个相同的值
即复制基本类型值时先创建新值后赋值分配。
示例1
var num1 5;var num2 num1;
分析 1 刚开始时num1 中保存的值是5。 2当使用num1 的值来初始化num2 时num2 中也保存了值5。 3但是这两个值不是同一个5num 2 中的5 是新创建的它与num1中的 5是完全独立的。 4虽然都是5但是 num2 中的 5 只是num1 中 5 的一个副本。相当于从num1中克隆了一个5然后赋值给了num2 5既然这两个5是完全独立的那么它们就可以互不影响的参与任何操作。
复制基本类型值的过程
2、如果复制的是引用类型的值同样会将存储在变量对象中的值先复制一份然后放到为新变量分配的空间中。
疑问那么它与基本类型的值复制有什么不同呢
这里有个前提别忘了
- 引用类型的值存放的不是真实的对象而是对象的引用。
- 就算复制了一份值这个值也是对象的引用并不会开辟一块新的内存空间再去创建同一个对象。
问题何为对象的引用呢 答
- 其实这个引用就是对象的地址即对象在堆内存当中被分配的地址。
- 有了这个地址标识就能找到这个对象。这就和去电影院看电影时对号入座的道理是一样的。
下面举例说明
1.当你走进观影大厅时你会根据电影票中的座位号对号入座有了座位号你就能找到对应的位置坐下。2.当你把电影票送给其他人看别人拿着你的电影票去看电影时同样能够找到位置。
情形1
- 因为电影院的座位是固定的电影票中的座位号指向了固定的一个座位当你购买电影票时系统会随机给你分配一个座位看电影的时候只要根据这个座位号就能够找到自己的座位很方便。
- 这种做法就相当于
- 引用类型值指向了堆内存中一个特定对象的地址根据这个地址就能找到该对象在内存空间中对应的位置。
情形2
- 当你把这张电影票赠送给朋友时你朋友拿着你的电影票同样能够对号入座。但是座位在你自己订票时已经分配好了即座位固定下来了是不能随意更改的。所以说不管谁拿着这张票去看电影座位都是不会改变的。
- 所以说这种行为就相当于
- 不仅自己本身可以引用而且其他值也能引用该对象。即只会分配一次空间而且只能以引用的方式去访问对象。即使不同的值引用同一个对象访问的也是同一个对象的引用并不会创建多个内存空间要不然多浪费
这样做的好处就是不用为该值再单独开辟一块内存空间防止内存空间的滥用和浪费。
3、基本类型值的复制 与 引用类型值复制 的区别
- 基本类型值的复制它会在变量对象上创建一个新值然后将新值复制给变量。复制操作结束之后两个变量是相互独立的互不相识的它们之间的任何操作都不会影响到对方。这两个变量的值是不相关的
- 引用类型值的复制将存储在变量对象的值复制一份给新的变量复制的那个值是初始值的副本没错但是这个副本实际上是一个指针。而这个指针保存的就是对象在堆内存空间中的地址它指向了存储在堆内存当中的一个对象。复制操作结束后两个变量实际上引用的是同一个对象。相当于两个变量指向的是同一个对象。因此只要改变其中的一个变量的引用值就会影响到另一个变量。这两个变量的值是关联的
注意【比较重要的一句话一定要理解】
对引用类型的值而言当复制保存着对象的某个变量时操作的是对象的引用。但在为对象添加属性时操作的是实际的对象。
示例2
//引用类型值,以对象为例var obj1 new Object();var obj2 obj1;obj1.name "fly";alert(obj2.name); //"fly"
分析 1首先新创建了一个对象并且将这个对象保存在了变量obj1中 此时变量obj1中就保存了该对象的一个新的实例。
2然后将这个变量值复制到了 obj2中了。 此时obj1和 obj2指向的是同一个对象。
3接下来为obj1这个引用类型值添加了一个name属性之后就可以通过 obj.name来访问这个属性。
因为这两个变量引用的都是同一个对象。
保存在变量对象中的变量 和 保存在堆中的对象 之间的关系如图4-2所示
示例3【该示例是后来新增的为了便于理解引用类型值的复制。因为书中提供的案例不多而且看完之后还是令人难以理解】
//引用类型值,以数组为例//1.对其中一个变量直接赋值不会影响到另一个变量并未操作引用的对象var a [1,2,3];var b a;a [1,2,3,4];alert(a);//1,2,3,4alert(b); //1,2,3//2.使用push(操作了引用的对象)var a [1,2,3];var b a;a.push(4);alert(a);//1,2,3,4alert(b); //1,2,3,4
4.1.3 传递参数【疑难点】
ECMAScript中所有函数的参数都是按值传递的。【记住这句话就行了】
问这句话什么意思呢 答把函数外部的值 复制给 函数内部的参数就相当于把值从一个变量 复制到 另一个变量。
即
- 基本类型值的传递 就如同 基本类型变量的复制一样
- 引用类型值的传递 就如同 引用类型变量的复制一样。
注意【有不少开发人员在这一点上可能会感到困惑】
因为访问变量有两种方式按值访问 和 按引用访问。而参数只能按值传递。
说明
- 在向参数传递基本类型的值时被传递的值会被复制给一个局部变量。 这个局部变量就是命名参数即形参。或者用ECMAScript 的概念来说就是arguments对象中的一个元素
- 在向参数传递引用类型值时会把这个值在内存中的地址复制给一个局部变量。 因此这个局部变量的变化会反映在函数的外部。
示例1
fucntion addTen(num){num 10;return num;}var count 20;var result addTen(count);alert(count); //20没有变化alert(result); //30
分析 1函数addTen()中有一个参数num而参数实际上是函数的局部变量。 2在调用这个函数时变量count作为参数被传递给函数这个变量的值是20。 3于是数值20被复制给参数num以便在addTen()函数中使用。 4在函数内部参数num的值自加10但是这一变化并不会影响函数外部的 count变量。
注意 参数num与变量count是互不相识的它们只是具有相同的值而已。
提示 如果num是按引用传递的话那么变量count的值也将会变成30。
问为什么变量count的值也将会变成30呢 答
- 如果按引用传递变量count会将引用传递给num传递的并不是真实的值只是该值在内存中的地址而已。
- 然后num就可以通过这个传递过来的地址引用找到内存空间中真实存在的那个值。
- 此时如果num通过某种方式改变了它的真实值那么相对应的变量count中引用的值也会改变。
示例2
function setName(obj){obj.name "fly";}var person new Object();setName(person);alert(person.name); //"fly"
分析 1首先在代码中创建了一个对象并将该对象的引用保存在了变量person中 2然后将这个变量传递到了setName()函数中并复制给了参数obj 3在这个函数内部obj和person引用的是同一个对象。
疑问明明参数是按值传递的为什么创建的对象实例person也能获取到在setName()中添加的name属性呢 答此时即使person变量是按值传递的那么obj也会按引用来访问同一个对象。因为对象是引用类型的数据只能以引用的方式来访问。
4当在函数内部为obj添加name 属性后函数外部的person 也将同步改变。 因为person指向的对象在堆内存中只有一个而且是全局对象。
注意【有很多开发人员错误的认为】
如果在局部作用域中修改的对象会在全局作用域中反映出来就说明参数是按引用传递的。
其实并不是这样的为了证明对象是按值传递的再看看下面的这个经过修改的例子
示例3
function setName(obj){obj.name "fly";//此时person 中的 name 属性被设置为"fly"obj new Object();//为 obj 重新定义了一个对象obj.name "monkey";//为该对象定义了一个带有不同值的 name 属性}var person new Object();setName(person);alert(person.name); //"fly"
说明 与上面的例子唯一的区别就是在setName()函数中添加了两行代码 1第一行代码为obj重新定义了一个对象 2第二行代码 为该对象定义了一个带有不同值的 name属性。
分析 1在把person 作为参数传递给 setName()之后其 name 属性被设置为"fly"。 2然后又将一个新对象赋值给变量obj同时将其 name 属性设置为 "monkey"。
在这里我们假设person 作为参数它是按照引用传递的那么 person 就会自动被修改为指向 其name属性值为"monkey"的新对象。
但是当接下来再访问 person.name时显示的值仍然是"fly"。
这就说明即使在函数内部修改了参数的值但原始的引用依然保持不变。所以参数并不是按照引用来传递的。
示例4后来新增的对书中提供的案例作以补充。就一个案例而言有些人可能还是比较困惑或者说难以理解
function setName(obj){obj.name "fly";//此时person 中的 name 属性被设置为"fly"obj {name:"monkey",age:"20"}//将obj对象重新赋值改变了该对象的引用但是初始对象的引用不会改变。return console.log(obj);//因为参数是按值传递的可以这样理解按值传递了对象的引用。}var person new Object();setName(person);//Object {name: "monkey", age: "20"}console.log(person.name);//"fly"
注意
- 实际上当在函数内部重写 obj时这个变量引用的就是一个局部对象了。
- 而这个局部对象会在函数执行完毕后立即被销毁。
提示可以把ECMAScript函数的参数想象成局部变量这样就容易理解了。
注意
- 刚开始看的时候很容易将4.1.2 复制变量值和4.1.3 传递参数即不同类型的变量之间值的复制与参数值的传递这两个部分的内容搞混了。
- 头脑一定要清晰有个明确的界限即复制变量值 和 参数的传递 是不一样的区别如下。
1复制变量值分为基本类型值的复制和引用类型值的复制。引用类型又称复杂类型
当从一个变量向另一个变量复制值时
- 基本类型值的复制会创建一个新值然后将新值复制给新变量。
- 引用类型值的复制也会将变量中的值复制给新变量但是复制的这个值其实是一个指针并不是真实的值。只是这个指针指向了存储在堆中的一个对象。等复制操作结束后两个变量引用的实际上是同一个对象。
2传递参数不管是传递给参数的是基本类型的值还是引用类型的值对于这两种类型的值将它们的值传递给函数内部的参数时都是按值传递的。
提示
- 看完上面的介绍估计有很多人都会被书中作者的说法绕进去表示看不懂。没关系我已经为大家找到了出口。
- 对在传递参数中对象是按值传递的这个知识点还心存疑问的小伙伴可以认真看看下面的内容能够帮助大家解决心中的疑惑。
下面这部分更新于2018/04/27 16:38
缠绕很久的疑问JS是按值传递还是按引用传递?【新手必读】
- 说实话写到这里我自己都被自己绕进去了我也不明白对象的按值传递到底是什么意思。而且这个按值传递中传递的是什么值。这部分内容已经反反复复看了3遍了每一次都是停留在这块止步不前。
- 我就是难以理解这些抽象的概念不知道是个人原因还是书中讲述不清的原因。感觉作者在书中解释的并不是很清晰越看越绕。
- 一开始询问了群里的几个大神他们看完之后感觉也很绕也不是很明白表示不能帮我解决这个问题。这时就有人推荐我去知乎寻找一下答案。在他们的指引下我找到了这个问题的答案
参考链接
JS是按值传递还是按引用传递? | BOSN.ME【推荐看这篇博文讲的很详细】
Javascript传递参数如果是object的话是按值传递还是按引用传递
结论
【感谢龙石为本站提供数据质量管理系统,http://www.longshidata.com/pages/quality.html】