《JS高级程序设计》第四章 作用域和内存问题

把2016年寒假写的对《JavaScript高级程序设计》的笔记写在博客上,同时回看加修改,同时也更新到简书上。尽量一天一篇一章。

<h2>第四章 变量、作用域和内存问题</h2>

<h3>4.1基本类型和引用类型的值</h3>

ECMAScript变量可能包含两个不同类型数据的值:基本类型值和引用类型值。
基本类型值指的是简单的数据段(Boolean类型、Number类型、String类型、Undefined、Null) 引用类型值指那些可能由多个值构成的对象(Object类型、Array类型、Date类型、RegExp类型、Function类型)

ES6中新增了Symbol,是JavaScript的第七种数据类型。

<h4>4.1.1动态的属性</h4>
基本类型值和引用类型值的区别一:对于引用类型值,我们可以为其添加或删除属性和方法,但是基本类型值没有属性和方法。
例子:

var person = new Object();                  var name = "Nicholas"
person.name = "Nicholas";                   name.age = 27;
alert(person.name);    //"Nicholas"         alert(name.age)     //undefined

以上代码一创建了一个对象并给他一个name属性,又通过alert访问成功。代码二给字符串name定义了一个age属性,但当我们访问的时候会发现这个属性不存在。这说明只能给引用类型值动态地添加属性,以便将来使用。

<h4>4.1.2复制变量值</h4>
基本类型值和引用类型值的区别二:在复制变量的时候,复制基本类型值和引用类型值也是有区别的。如果只是复制基本类型值,那就是简单复制到为新变量分配的位置上没毛病。当复制的是引用类型的值时,同样会将存储在变量对象中的值复制一份放到为新变量分配的空间中。不同的是,这个新的副本实际上是一个指针,复制结束后,两个变量实际上将引用同一个对象。因此,如果复制的是引用类型值,当改变其中一个变量,就会影响另一个变量。例子:

var obj1 = new Object();
var obj2 = obj1;
obj1.name="Nicholas";
alert(obj2.name);    //"Nicholas"

变量对象中的变量保存在堆中的对象之间的关系如图:


图片来自《JavaScript高级程序设计》
可以看到当变量复制后,指针仍然指向一开始的Object,而不是复制出多一个Object。.

<h4>4.1.3传递参数</h4>
ECMAScript中所有函数的参数都是按值传递的。(无论参数是引用类型值和基本类型值)。也就是说,把函数外部的值复制给函数内部的参数,就和4.1.2复制变量值的原理一样,把一个变量复制到另一个变量(函数的参数)一样。有不少开发人员在这点会感到困惑,因为访问变量有按值和按引用两种方式,而参数只能按值传递

--------------------------讨论参数传递的是引用类型值的情况--------------------------

function setName(obj){
     obj.name = "Nicholas";
}
var person = new Object();
setName(person);
alert(person.name);   //"Nicholas"

以上代码创建了一个对象person,这个变量被传递到setName()函数中后被复制给了obj,在这个函数内部,obj和person引用的是同一个对象。换句话说,即使这个变量是按值传递的,obj也会按引用来访问同一个对象(遵循4.1.2的复制变量值原理)。于是当为函数内部为obj添加name属性后,函数外部的person也会有所反映。因为person指向的对象在堆内存中只有一个,而且是全局对象。有很多开发人员错误地认为:在局部作用域中修改的对象会在全局作用域中反映出来,就说明参数是按引用传递的(大错特错)。为了证明对象是按值传递的。看下面的例子:

function setName(obj){
     obj.name = "Nicholas";
     obj = new Object();
     obj.name = "Greg";
}
var person = new Object();
setName(person);
alert(person.name);     //"Nicholas"

这段代码增加了两行,为obj重新定义了一个对象,第二行为该对象定义了一个带有不同值的name属性。如果person是按引用传递的,那么person最后会被自动修改为指向其name属性值为“Greg”的新对象。但是在函数外访问person.name时,显示的值仍然是"Nicholas"。这说明即使在函数内部修改了参数的值,但原始的引用仍然保持未变。实际上,当在函数内部重写obj时,这个变量引用的就是一个局部对象了(这个函数范围的局部对象)。而这个局部对象会在函数执行完毕后立即被销毁。

<h4>4.1.4检测类型</h4>
检测变量类型的方法有两种,一种是检测基本类型值的,用typeof,另一种是检测引用类型值的,用instanceof。
typeof操作符是确定一个变量是string,boolean,number,undefined的最佳工具,如果变量是对象(根据规定,所有引用类型的值都是Object的实例)或null,则typeof操作符返回的值会是“object”。例子:

var s = "Nicholas", b = true, i = 22,  u  , n = null,  o = new Object(),  d = new Date();
alert(typeof s);  //string
alert(typeof i);  //number
alert(typeof b);  //boolean
alert(typeof u);  //undefined
alert(typeof n);  //object
alert(typeof i);  //object
alert(typeof d);  //object

因为typeof只能检测基本类型值,检测引用类型值时只会返回object,所以ECMAScript又提供了一个insanceof操作符,用法跟typeof不同,且只返回true 或 false。

<div style="border: 1px solid #ccc; padding:10px"><strong>✎另类的情况</strong>
使用typeof操作符检测函数时,该操作符会返回"function"。在Safari 5 及之前版本和Chrome 7及之前的版本中使用typeof检测正则表达式时,由于规范的原因,这个操作符也返回“function”。ECMA-262规定任何在内部实现 [ [ call ] ] 方法的对象都应该在应用typeof操作符时返回“function“。由于上述浏览器(Safari 5,Chrome 7)中的正则表达式也实现了这个方法,因此对正则表达式应用typeof会返回“function”。在IE和Firefox中,对正则表达式应用typeof会返回“object”.</div>

如果变量是给定引用类型(根据它的原型链来识别,第6章将介绍原型链。#原书句)的实例那么instanceof操作符就会返回true。例子:

alert(person instanceof Object);    //变量person是Object吗?
alert(colors instanceof Array);     //变量colors是Array吗?
alert(pattern instanceof RegExp);   //变量pattern是RegExp吗?

//亲测左右两边位置不可互换,互换不会出现提示框

因为根据规定,所有引用类型的值都是Object的实例,因此把Date,Array,RegExp等引用类型值用instanceof 与Object验证时,始终都会返回true。用instanceof操作符检测基本类型值时,该操作符时钟返回false,因为基本类型不是对象。

<h3>4.2执行环境及作用域</h3>
作用域链重要的一点就是内部执行环境可以使用其外部环境的变量和函数,并且可以改变那个变量的值,只要那个变量不是被当作参数传进去的而是直接使用的。(当作参数传入的是按值传递,改变的是复制出来的变量,不会改变原来的变量)


执行环境(execution context)和作用域其实超级简单。每个执行环境都有一个与之关联的变量对象(variable object),环境变量中定义的所有变量和函数都保存在这个对象中。但是我们无法用代码访问到这个变量对象。但解析器在处理数据时会在后台使用它。

全局执行环境是最外围的执行环境。根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样。在Web浏览器中,全局执行环境被认为是window对象(第七章将详细讨论),因此,所有全局变量和函数都是作为window对象的属性和方法创建的(window对象是个变量对象,全局变量和函数是它的属性和方法)。某个执行环境(例如一个函数)中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出(网页关闭或浏览器关闭时才被销毁))

在Node.js中的全局执行环境是global

每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权交给之前的执行环境。ECMAScript程序中的执行流正是由这个方便的机制在控制。


当代码在其中一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的最前端,始终都是当前执行代码所在环境的变量对象。如果这个环境是函数,则将其活动对象(activation object)作为变量对象。活动对象在最开始时只包含一个变量,即arguments对象(这个对象在全局环境中不存在)。作用域链中的下一个变量对象来自包含它的外部环境,而再下一个变量对象则来自下一个包含环境。这样,一直延伸到全局执行环境;


标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程从作用域链最前端开始,然后逐级向后回溯,直到找到标识符为止(如果找不到,就会发生错误)
例子:

var color = "blue";
function changeColor(){
     if(color == "blue"){
          color = "red";
     }
}
changeColor();
alert(color);   // "red"

在这个例子中,函数changeColor( )的作用域链包含两个对象:它自己的变量对象(其中定义着arguments对象)和全局环境的变量对象。可以在函数内部访问变量color,就是因为可以在这个作用域链中找到它。

此外,在局部作用域中定义的变量可以在局部环境中与全局变量互换使用,如下面这个例子所示:

var color = "blue";
function changeColor(){
     var anotherColor = "red";
     
     function swapColors(){
          var tempColor = anotherColor;
          anotherColor = color;
          color = tempColor;

          //这里可以访问color,anotherColor 和 tempColor
     }
     //这里可以访问color和anotherColor,但不能访问tempColor
     swapColors();
}
//这里只能访问color
changeColor();```
以上代码涉及三个执行环境:全局环境、changeColor()的局部环境和swapColor() 的局部环境。swapColor的局部变量中有一个变量tempColor,该变量只有在swapColor环境中能访问到,但是swapColor()内部可以访问其他两个环境中的所有变量。
越在内部的局部环境,作用域链越长。对于这个例子中的swapColor()而言,其作用域链中包含3个对象:swapColor( )的变量对象、changeColor()的变量对象和全局对象。swapColor()的局部环境开始时会现在自己的变量对象中搜索变量和函数名,如果搜索不到则再搜搜上一级作用域链。changeColor()的作用域链中只包含两个对象:它自己的变量对象和全局对象。也就是说,它不能访问swapColor()的环境。

<h4>4.2.1延长作用域</h4>
有些语句可以在作用域链前端临时增加一个变量对象,该变量对象会在代码执行后被移除。在两种情况下会发生这种现象,具体来说,就是当执行流执行到下列任何一个语句时,作用域链会得到增长

- try-catch语句的catch块

- with语句

这两个语句都会在作用域链的前端添加一个变量对象。对with语句来说,会将指定的对象添加到作用域链中,对catch语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。例子:

function buildUrl(){
var qs = "?debug=true";

 with(location){
      var url = href + qs;
 }
 return url;
 }

✎添加一个with语句块的知识点</strong><br>当在with语句块中使用方法或者变量时,程序会检查该方法是否是本地函数,变量是否是已定义变量,如果不是,它将检查伪对象(with的参数),看它是否为该对象的方法,属性。如上面例子with语句块中的href,本地无定义,则with语句块会自动加上location.href,所以href实际上为href。这个就是with的功能。with 语句是运行缓慢的代码块,尤其是在已设置了属性值时。大多数情况下,如果可能,最好避免使用它。

<br>
在此,with语句接收的是Location对象,因此其变量对象中就包含了location对象的所有属性和方法,而这个变量对象被添加到了作用域链的最前端,buildUrl()函数中定义了一个变量qs。当在with语句中引用变量href时(实际引用的是location.href)。可以在当前执行环境的变量对象中找到。当引用变量qs时,引用的则是在buildUrl( )中定义的那个变量,而该变量位于函数环境的变量对象中。至于with语句的内部,则定义了一个名为url的变量,因而url就成了函数执行环境的一部分,所以可以作为函数的值被返回。

<h4>4.2.2没有块级作用域</h4>
<strong>✎添块级作用域</strong><br>任何一对花括号中的语句都属于一个块,在这之中定义的所有变量在代码块之外都是不可见的,我们称之为块级作用域。
<br>
**作用域有两种,块级作用域和函数作用域**
讲到这就好理解。JS没块级作用域就是说在for循环和if语句块中定义的变量是可见的,可以被外部使用的,但像其他的语言Java,C,C#语言中,在for,if语句中定义的变量在语句执行完毕之后就会被**销毁**。但在JavaScript中,if语句中的变量声明会将变量添加到当前执行环境中。注意只是当前执行环境,如果for循环是在一个函数里,则定义的i在函数里是确定的数,在全局环境中仍然是not defined。例子:
```if(true){
var color = "blue";
}
alert(color)  //"blue"

for(var i=0; i<0; i++){<br>
    dosomething;<br>}
alert(i)  //10<br>
function add(){
for (var i = 0; i<10; i++){<br>
    dosomething;
}<br>
alert(i);     //10<br>
}
add();<br>
alert(i);     // **全局环境中 i is not defined**

但是ECMA2015有办法解决没有块级作用域这个问题,就是用 letconst代替 var 去声明 i 。这样 i 就只会在for循环中被声明,for循环结束之后就会被销毁。

1、声明变量
使用var声明的变量会自动添加到最接近的环境中。在函数内部,最接近的环境就是函数局部环境。在with语句中,最接近环境是函数环境。如果初始化变量时没有使用var声明,该变量会自动被添加到全局环境。在编写JavaScript的过程中,不声明而直接初始化变量是一个常见的错误做法,不建议这样做,在严格模式下,初始化未经声明的变量会导致错误。

2、JavaScript的查询标识符机制
就是当要读取或写入一个标识符时,会通过搜索来确定标识符,搜索过程会从作用域链的最前端开始,向上逐级查找,如果在局部环境中找到了该标识符,则搜索过程停止。如果一直找到全局环境仍未找到这个标识符,则意味着该变量未声明。

<h3>4.3垃圾收集</h3>
JavaScript具有自动垃圾收集机制,执行环境会负责管理代码执行过程中使用的内存。而在C和C++之类的语言中,开发人员的一项基本任务就是手工跟踪内存的使用情况。编写JavaScript的开发人员就不用关心内存使用的问题。所需内存的分配以及无用内存的回收完全实现了自动管理。
垃圾收集机制的原理很简单: 把再也不用的变量找出来,删除。为此,垃圾收集器会按照固定的时间间隔周期性地执行这一操作。
具体到浏览器中,通常有两个垃圾收集策略。

<h4>4.3.1标记清除</h4>
IE、Firefox、Opera、Chrome、和Safari的JavaScript实现使用的都是标记清除式的垃圾收集策略,只不过垃圾收集的时间间隔不同。
标记清除(mark-and-sweep)原理:当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量表记为“进入环境”。被这么标记的变量逻辑上应该随时可能使用到,所以是不会删除的。而当变量离开环境时,则将其标记为“离开环境”。垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记,然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些被标记的值并回收他们所占用的内存空间。

<h4>4.3.2引用计数</h4>
这种垃圾收集策略不太常用。
引用计数的原理简单来说就是每引用一次该变量就对引用次数加一,当引用的变量被替换掉就对引用次数减一。当引用次数变为0就将该变量占用的内存回收回来。
但因为这种机制在循环引用时有BUG,所以现在已经不用。而仍然提到这个垃圾回收机制是因为IE8及以前的浏览器中有一部对象并不是原生JavaScript对象。例如其BOM和DOM中的对象就是使用C++以COM(component Object Model 组件对象模型)对象的形式实现的,而COM对象的垃圾收集机制采用的就是引用技术策略。因此,即使IE的JavaScript引擎是使用标记清除策略实现的,但JavaScript访问的COM对象依然是基于引用计数策略的。(但其实只要不涉及循环引用,引用技术策略就不会有问题)
下面的例子展示了使用COM对象导致的循环引用的问题:

var element = document.getElementById("some_element");
var myObject = new Object();
myObject.element = element;
element.someObject = myObject;

这个例子在一个DOM元素(element)与一个原生JavaScript对象(myObject)之间创建了循环引用。其中,变量myObject有一个名为element的属性指向element对象;而变量element也有个属性名叫someObject回指myObject。由于存在循环引用,即使将例子中的DOM从页面中移除,它也永远不会被回收。
为了避免类似这样的循环问题,最好是在不使用他们的时候手工断开原生JavaScript对象与DOM元素之间的连接。例如这样消除前面例子创建的循环引用:

myObject.element = null;
element.someObject = null;

将变量设置为null意味着切断变量与它此前引用的值之间的连接。当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。
IE9已经把BOM和DOM对象都转换成了真正的JavaScript对象。这样,就避免了两种垃圾收集算法并存导致的问题,也消除了常见的内存泄漏问题。所以这是个IE8及之前的问题了。

<h4>4.3.3性能问题</h4>
性能问题就是各浏览器的垃圾收集机制问题,跟开发人员关系不大,虽然可以手工启动垃圾收集机制但作者不建议我们这么做。
IE中调用window.CollectGarbage( );Opera 7 及更高版本中调用windo.opera.collect( )会立即执行垃圾收集。

<h4>4.3.4管理内存</h4>
有垃圾收集机制的JavaScript在编写时一般不用操心内存管理问题。但是JavaScript在进行内存管理和垃圾收集时面临的问题还是有点与众不同。其中一个主要的问题,就是分配给Web浏览器的可用内存数量通常比分配给桌面应用程序的少,这样是处于安全考虑,防止浏览器占用全部系统内存导致系统崩溃。内存限制问题不仅会影响给变量分配内存,同时还会影响调用栈以及在一个线程中能够同时执行的语句数量
因此,确保占用最少的内存可以让页面获得更好的性能(页面占用内存越少,性能越好)。优化内存的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据不再有用,就通过将其设置为null来释放其引用——这个做法叫做解除引用(dereferencing)。这一做法适用于大多数全局变量和全局对象的属性。局部变量本来就会在离开执行环境后自动解除引用,如下面这个例子所示:

function createPerson(name){
     var localPerson = new Object();
     localPerson.name = name;
     return localPerson;
}
var globalPerson = createPerson("Nicholas");<br>
//手工解除globalPerson的引用
globalPerson = null;```
在这个例子中,变量globalPerson取得了createPerson函数返回的值,由于函数内的局部变量在离开执行环境后就**自动解除引用**,所以不用我们显式地解除引用,但对于全局变量globalPerson,在我们不需要它之后,我们可以**手工解除引用**。不是说解除了引用后该值就会被马上回收,解除引用的作用是让值脱离执行环境,在垃圾收集器下次运行时就会将其回收。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容