您的位置:首页 > Web前端 > JavaScript

[置顶] 《JavaScript高级程序设计》学习笔记(四)变量、作用域和内存问题

2018-03-28 22:54 435 查看
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 & A:

问:什么叫按值访问?

答:就是变量中保存的是实际的值,而且这个值是可以直接进行操作的。

问:为什么复杂数据类型的值是按引用访问的?

答:因为复杂数据(引用)类型的值保存在了内存中的对象中。而
对象并不能直接访问,只能通过引用来访问


我们要知道:

与其他语言不同,
JavaScript
不允许直接访问内存中的位置。换句话说,就是不能直接操作对象的内存空间

现在就明白了:

我们平时在操作对象时
操作的
并不是对象本身或者说实际的对象,而
是对象的引用


因此,引用类型的值是按引用访问的。

提示:

在很多语言中,字符串都是以对象的形式来表示的,所以它才会被认为是引用类型的。但是在
ECMAScript
中放弃了这一传统。

4.1.1 动态的属性

定义
基本类型值
引用类型值
的方式都差不多:创建一个变量并为该变量赋值。

但是,

当这个值保存到变量中以后,对不同类型值执行的操作是不一样的。

说明:

(1)对
引用类型值
来说,我们
可以给这个值添加属性和方法
,也可以改变和删除它的属性和方法。

(2)但是对
基本类型值
来说,我们
不能给它添加属性
(虽然这种做法不会导致任何错误)

示例1:

var person = new Object();
person.name = "fly";
alert(person.name);


[b]分析:


(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)但是,这两个值不是同一个
5
num 2
中的
5
是新创建的,它与
num1
中的
5
是完全独立的。


(4)虽然都是
5
,但是
num2
中的
5
只是
num1
5
的一个副本。(相当于从
num1
中克隆了一个
5
,然后赋值给了
num2


(5)既然这两个
5
是完全独立的,那么它们就可以互不影响的参与任何操作。

复制基本类型值的过程:



(2)如果复制的是引用类型的值,同样会将存储在变量对象中的值先复制一份,然后放到为新变量分配的空间中。

疑问:那么,它与基本类型的值复制有什么不同呢?

这里有个前提,别忘了:

引用类型的值存放的不是真实的对象,而是对象的引用。


就算复制了一份值,这个值也是对象的引用,并不会开辟一块新的内存空间再去创建同一个对象。


问题:何为对象的引用呢?

答:

其实这个引用就是对象的地址,即对象在堆内存当中被分配的地址。


有了这个地址标识,就能找到这个对象。(这就和去电影院看电影时对号入座的道理是一样的。)

下面举例说明:

1.当你走进观影大厅时,你会根据电影票中的座位号对号入座,有了座位号你就能找到对应的位置坐下。

2.当你把电影票送给其他人看,别人拿着你的电影票去看电影时,同样能够找到位置。


情形1:

因为电影院的座位是固定的,电影票中的座位号指向了固定的一个座位,当你购买电影票时系统会随机给你分配一个座位,看电影的时候只要根据这个座位号就能够找到自己的座位,很方便。

这种做法就相当于:

引用类型值指向了堆内存中一个特定对象的地址,根据这个地址就能找到该对象在内存空间中对应的位置。


情形2:

当你把这张电影票赠送给朋友时,你朋友拿着你的电影票同样能够对号入座。但是座位在你自己订票时已经分配好了,即座位固定下来了,是不能随意更改的。所以说,不管谁拿着这张票去看电影,座位都是不会改变的。

所以说,这种行为就相当于:

不仅自己本身可以引用,而且其他值也能引用该对象。(即只会分配一次空间,而且只能以引用的方式去访问对象。即使不同的值引用同一个对象,访问的也是同一个对象的引用,并不会创建多个内存空间,要不然多浪费)


这样做的好处就是:不用为该值再单独开辟一块内存空间,防止内存空间的滥用和浪费。

基本类型值的复制
引用类型值复制
的区别:


引用类型值复制的那个值是初始值的副本没错,但是这个副本实际上是一个指针。而这个指针指向存储在堆中的一个对象。

复制操作结束后,两个变量实际上引用的是同一个对象。相当于两个变量指向的是同一个对象。


因此,只要改变其中的一个变量的引用值,就会影响到另一个变量。(这两个变量的值是关联的)

示例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所示)




4.1.3 传递参数

ECMAScript
中所有函数的参数都是按值传递的。【记住这句话就行了

问:这句话什么意思呢?

答:
函数外部的值
复制给
函数内部的参数
,就相当于
把值从一个变量
复制到
另一个变量


即,

基本类型值的传递 就如同 基本类型变量的复制一样;

引用类型值的传递 就如同 引用类型变量的复制一样。

注意:【有不少开发人员在这一点上可能会感到困惑】

因为访问变量有两种方式:
按值访问
按引用访问
。而
参数只能按值传递


说明:

在向参数
传递基本类型的值
时,
被传递的值
会被复制给一个局部变量 (即命名参数)

在向参数
传递引用类型值
时,会把
这个值在内存中的地址
复制给一个局部变量。

因此,这个局部变量的变化会反映在函数的外部。

示例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
变量是
按值传递
的,那么
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"


这就说明,即使在函数内部修改了参数的值,但原始的引用依然保持不变。所以,参数并不是按照引用来传递的。

注意:

实际上,当在函数内部重写
obj
时,这个变量引用的就是一个局部对象了。

而这个局部对象会在函数执行完毕后立即被销毁。

提示:可以把
ECMAScript
函数的参数想象成局部变量,这样就容易理解了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: