前言:纯手打!!!按照自己思路重写!!!这次是二刷了,想暑假做一次完整的笔记,但用本子来写笔记的话太贵了,可能哪天还丢了。。所以还是博客好==
第四章:变量、作用域和内存问题
4.1 基本类型和引用类型的值:
ECMAScript变量可能包含两种不同数据类型的值:基本类型值和引用类型值。
- 基本类型值:
- 指的是简单的数据段。
- 类型:Undefined、Null、Boolean、Number、String。这五种基本数据类型是按值访问的,因为可以操作保存在变量中的实际的值。
- 在内存中占据固定大小的空间,因此被保存在栈内存中。
- 引用类型值:
- 指的是那些可能由多个值构成的对象。
- 类型:Object。引用类型的值是保存在内存中的对象,JS不能直接操作对象的内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。因此引用类型的值是按引用访问的。
- 值是对象,因此保存在堆内存中。
4.1.1 动态的属性:
定义基本类型值和引用类型值的方式差不多:创建一个变量并为该变量赋值。但之后的操作不一样。只能给引用类型值动态地添加属性。
- 引用类型:(可以为其添加属性和方法、可以改变和删除其属性和方法)
例:var person = new Object (); //创建一个对象并将其保存在变量person中。
person.name = "Nick"; //为该对象添加一个名为name的属性,并将字符串值”Nick“赋给这个属性。
console.log (person.name); //"Nick",若对象不被摧毁则这个属性一直存在。
- 基本类型:不可以给基本类型的值添加属性。
4.1.2 复制变量值:
从一个变量向另一个变量复制基本类型值和引用类型值时,也存在不同。
- 基本类型:
例:var num1 = 5;
var num2 = num1; //使用num1的值来初始化num2时,num2也保存了值5。但num1的5和num2的5是完全独立的,num2的5只是num1中5的一个副本,不相互影响。
复制前的变量对象:
num1 | 5 |
复制后的变量对象:(传的是数值,开辟新的空间)
num2 | 5 |
num1 | 5 |
- 引用类型:同样也会将存储在变量对象中的值复制一份放到为新变量分配的空间中。但不同的是这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象。
复制操作结束后,两个变量实际上将引用同一个对象。因此在引用类型中,改变其中一个变量就会影响到另一个变量。
例:var obj1= new Object (); //变量obj1保存了一个对象的新实例。
var obj2= obj1; //然后这个obj1的值被复制到了obj2中,即obj1和obj2都指向同一个对象。
obj1.name = "Nick";
console.log (obj2.name); //"Nick",当为obj1添加name属性后,可以通过obj2来访问这个属性。
复制前的变量对象: 这个(Object类型)指向堆内存的对象Object1
obj1 | (Object类型) |
复制后的变量对象:这两个(Object类型)指向堆内存的对象Object1(即同一个对象)
obj2 | (Object类型) |
obj1 | (Object类型) |
4.1.3 传递参数:
ES中所有函数的参数都是按值传递的,即把函数外部的值复制给函数内部的参数等同于把值从一个变量复制到另一个变量。
- 基本类型:基本类型值的传递如同基本类型变量的复制一样。
- 在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量(即命名参数,或者说时arguments对象中的一个元素)。
- 基本类型的复制,形参实参互不干扰。
- 在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部。
- 引用类型保存对象的变量,它里面装的值是这个对象在堆内存中的地址。所以在对象变量进行复制时,obj1和obj2都指向同一个内存的地址。
- 所以在参数传递中,形参实参指向同一内存地址,当改变形参的属性时,实参也变。如果直接改变形参本身(如重新给形参分配一块内存:重新定义一个对象,然后该对象定义了一个新的属性),那么此时形参的改变就不会影响到实参。所以说,引用数据类型在传递时时按值传递的!
引用类型:引用类型值的传递如同引用类型变量的复制一样。
例:function addTen (num) { //参数num,参数实际上时函数的局部变量。
num += 10;
return num;
}
var count = 20;
var result = addTen (count); //在调用addTen函数时,变量count作为参数被传递给函数,变量count的值是20,于是数值20被复制给参数num(局部变量 or 命名参数 or arguments对象中的一个元素)以便在函数中使用。
console.log (count); //20。
console.log (result); //30,函数内部num被加上10,但这变化不会影响函数外部的count变量。(ps:若num是按引用传递的话,那count当然会变成30)
例:function setName (obj) {
obj.name = "Nick";
}
var person = new Object (); //创建了一个对象并将其保存在变量person中。
setName (person); //然后,这个变量被传递到setName () 函数中之后被复制给了obj。在这个函数内部,obj和person引用的是同一个对象。(这句话的意思是:此变量按值传递,obj按引用来访问同一个对象)
console.log (person.name); //”Nick"
例:function setName (obj) {
obj.name = "Nick";
obj = new Object ();
obj.name = "Gary";
}
var person = new Object ();
setName (person);
console.log (person.name); //”Nick",说明即使在函数内部修改了参数的值,但原始的引用仍然没变。不就是因为按值传递。
4.1.4 检测类型:
- 检测给定变量的数据类型——typeof,用来检测基本数据类型最好用。
- “undefined”——如果这个值未定义;
- “boolean”——如果这个值是布尔值;
- “string"——如果这个值是字符串;
- ”number“——如果这个值是数值;
- ”object”——如果这个值是对象或者null;
- “function"——如果这个值是函数!!!虽然函数是对象,但可以通过typeof区分是不是函数。
如:var message = "some string";
alert(type of message); //"string"
alert(type of null); //”object"
- 检测引用类型,即想知道它时什么类型的对象——instanceof。
- 语法:result = variable instanceof constructor(某个构造函数)
- 例如:console.log (person instanceof Object);
- 检测一个引用类型值和Object构造函数时,instanceof始终会返回true。
- 若拿去检测基本数据类型,则返回false。
4.2 执行环境及作用域:
每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。虽然我们无法访问这个对象。
- 全局执行环境:最外围的一个环境,等同于window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。
- 每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入环境栈中,而在函数执行之后,栈将其环境弹出,将控制券返回给之前的执行环境。
- 某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也被随之销毁。(全局的话是直到应用程序退出时才会被销毁)。
- 作用域链:当代码在一个环境中执行时,会创建变量对象的一个作用域链,用途是保证对执行环境有权访问的所有变量和函数的有序访问。
- 作用域链的前端始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象作为变量对象。活动对象在最开始时只包含一个变量(arguments对象,这个对象在全局下是不存在的)。
- 作用域链的下一个变量对象来自包含环境,而再下一个变量对象则来自下一个包含环境。。一直延续到全局执行环境。全局执行环境的变量对象始终是作用域链中的最后一个对象。
- 标识符解析:沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直到找到标识符为止(如果找不到标识符,通常会导致错误发生)。
- 例:var color = "blue"; //全局环境,有一个变量color和一个函数changeColor ()
function changeColor () { //changeColor () 的局部环境,有一个名为anotherColor的变量和一个名为swapColors () 的函数,changeColor () 可以访问全局环境中的变量color。
var anotherColor = "red“;
function swapColors () { //swapColor () 的局部环境,有一个变量tempColor,这个变量只能在这个环境中访问到。swapColor () 内部可以访问其他两个环境的所有变量(因为那两个环境是它的父执行环境)。
var tempColor = anotherColor; //无论是全局环境还是changeColor () 的局部环境都无权访问变量tempColor。
anotherColor = color;
color = tempColor;
//这里可以访问color、anotherColor和tempColor。
}
swapColor (); //这里可以访问color、anotherColor。
}
changeColor (); //这里只可以访问color。
- 以下是这个例子的作用域链:
4.2.1 延长作用域链:
虽然执行环境的类型只有全局和局部(函数),但有其他办法来延长作用域链。
- 以下这两个语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。(即当执行流进入下列任意一个语句时,作用域链会得到加长)
- try-catch语句的catch块(会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明)
- 例:try { fn (); } catch (ex) { handleError (ex); } //try发送错误就跳转到catch语句,catch会创建一个新的变量对象,其中包含被抛出的错误对象的声明。(即这个对象只能内部访问)
- with语句(会将指定的对象添加到作用域链中),with 语句的原本用意是为逐级的对象访问提供命名空间式的速写方式,但最好不用,因为我们可以用变量来实现。
- 例:function buildUrl () {
- try-catch语句的catch块(会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明)
var qs = "?debug=true";
with (location) { //location对象现在为作用域链的最前端,由于with关联了location对象,所以location在with内部相当于window对象的地位,所以获取location里的属性可以不写location。
var url = href + qs; //href实际引用的是location.href,qs实际引用的是buildUrl () 中定义的那个变量。url是with语句内部定义的,因此url成为函数执行环境的一部分,所以就可以作为函数的值被返回。
}
return url; //因为with语句不是函数,所以url还是属于buildUrl () 函数的,所以就可以作为函数的值被返回。
}
4.2.2 ES5没有块级作用域,ES6有let、const:
- 例:if (true) { //if是语句不是函数
var color = "blue";
}
alert (color); //"blue"
上面这段代码对于JS来说,if语句中的变量声明会将变量添加到当前的执行环境(即全局环境)。
- 例:for (var i = 0; i < 10; i++) { //for是语句不是函数
doSomething (i);
}
alert (i); //10
上面这段代码对于JS来说,for语句创建的变量i即使在for循环执行结束之后,也依旧会存在于循环外部的执行环境中。
- 声明变量:使用var声明的变量会自动被添加到最接近的环境中。在函数内部,最接近的函数就是函数的局部函数;在with语句中,最接近的环境是函数环境。如果初始化变量时没有使用var声明,该变量会自动被添加到全局环境。
- function add (num1, num2) {
var sum = num1 + num2;
return sum;
}
var result = add (10, 20); //30
alert (sum); //由于sum是var声明的,因此全局访问不到,导致错误。
- function add (num1, num2) {
sum = num1 + num2;
return sum;
}
var result = add (10, 20); //30
alert (sum); //30,因为变量sum在初始化的时候没有使用var声明,该变量会自动被添加到全局环境。所以可以在全局环境里访问到sum=30;
- 查询标识符:
当在某个环境中为了读取或写入而引用一个标识符时,必须通过搜索来确定该标识符实际代表什么。搜索过程从作用域链的前端开始,向上逐级查询与给定名字匹配的标识符。
如果在搜索过程中发现局部环境中存在了同名标识符,那么就不会去使用位于父环境中的标识符啦。
4.3 垃圾收集:
JS具有自动垃圾收集机制,执行环境会负责管理代码执行过程中使用的内存。(自动找出那些不再继续使用的变量,任何释放其占用的内存),为此垃圾收集器会按照固定的时间间隔(或者代码执行中预定的收集时间),周期性地执行这一操作。
局部变量的生命周期:局部变量只在函数执行的过程中存在。这个过程中,会为局部变量在栈(或堆)内存上分配相应的空间以存储它们的值,然后再函数中使用这些变量,直到函数执行结束。所以这个时候局部变量就没有存在的必要了。因此释放它们的内存以供将来使用。
- 常见的垃圾收集方式是标记清除:当变量进入环境时(比如在函数中声明一个变量),就将这个变量标记为“进入环境”(不可以释放进入环境的变量所占用的内存!!),因为只要执行流进入相应的环境,就可能会用到它们。当变量离开环境时,则将其标记为“离开环境”
- 第二个不常用的方式时引用计数,这里不说先。
- 性能问题:先不说。
- 管理内存(解除引用):确保占用最少的内存可以让页面获得更好的性能,而优化内存占用最好的方式时为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为null来释放其引用——解除引用。
- 局部变量:会在它们离开执行环境时自动被解除引用。
- 全局变量:设置null来释放引用,让值脱离执行环境,以便垃圾收集器下次运行时将其回收。
- 好处是不仅有助于消除循环引用现象,而且对垃圾收集也有好处。为了确保有效的回收内存,应该及时解除不再使用的全部对象、全局对象属性以及循环引用变量的引用。