第四章 变量、作用域和内存问题
基本类型和引用类型的值
ECMAScript 变量可能包含两种不同数据类型的值:基本类型值和引用类型值。基本类型值指的是简单的数据段,而引用类型值指那些可能由多个值构成的对象。
引用类型的值是保存在内存中的对象。在操作对象时,实际上是在操作对象的引用而不是实际的对象。
1、复制变量值
(1)复制基本类型的值
var num1 = 5;
var num2 = num1;
此后,这两个变量是相互独立的,可以参与任何操作而不会相互影响。
(2)复制引用类型的值
var obj1 = new Object();
var obj2 = obj1;
obj1.name = "Nicholas";
alert(obj2.name); //"Nicholas"
2、检测类型
typeof 操作符是确定一个变量是字符串、数值、布尔值,还是 undefined 的最佳工具。
var s = "Nicholas";
var b = true;
var i = 22;
var u;
var n = null;
var o = new Object();
alert(typeof s); //string
alert(typeof i); //number
alert(typeof b); //boolean
alert(typeof u); //undefined
alert(typeof n); //object
alert(typeof o); //object
instanceof 操作符
result = variable instanceof constructor
如果变量是给定引用类型(根据它的原型链来识别;第 6 章将介绍原型链)的实例,那么 instanceof 操作符就会返回 true。
执行环境及作用域
执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。
每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。
在 Web 浏览器中,全局执行环境被认为是 window 对象,因此所有全局变量和函数都是作为 window 对象的属性和方法创建的。
某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出——例如关闭网页或浏览器——时才会被销毁)。
每个函数都有自己的执行环境。当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。
每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。
函数参数也被当作变量来对待,因此其访问规则与执行环境中的其他变量相同。
1、延长作用域链
有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。具体来说,就是当执行流进入下列任何一个语句时,作用域链就会
得到加长:
try-catch 语句的 catch 块;
with 语句。
对 with 语句来说,会将指定的对象添加到作用域链中。对 catch 语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。
function buildUrl() {
var qs = "?debug=true";
with(location){
var url = href + qs;
}
return url;
}
2、没有块级作用域
3、小结
JavaScript 变量可以用来保存两种类型的值:基本类型值和引用类型值。基本类型的值源自以下 5 种基本数据类型:Undefined、Null、Boolean、Number 和 String。基本类型值和引用类型值具有以下特点:
基本类型值在内存中占据固定大小的空间,因此被保存在栈内存中;
从一个变量向另一个变量复制基本类型的值,会创建这个值的一个副本;
引用类型的值是对象,保存在堆内存中;
包含引用类型值的变量实际上包含的并不是对象本身,而是一个指向该对象的指针;
从一个变量向另一个变量复制引用类型的值,复制的其实是指针,因此两个变量最终都指向同一个对象;
确定一个值是哪种基本类型可以使用 typeof 操作符,而确定一个值是哪种引用类型可以使用instanceof 操作符。
所有变量(包括基本类型和引用类型)都存在于一个执行环境(也称为作用域)当中,这个执行环境决定了变量的生命周期,以及哪一部分代码可以访问其中的变量。以下是关于执行环境的几点总结:
执行环境有全局执行环境(也称为全局环境)和函数执行环境之分;
每次进入一个新执行环境,都会创建一个用于搜索变量和函数的作用域链;
函数的局部环境不仅有权访问函数作用域中的变量,而且有权访问其包含(父)环境,乃至全局环境;
全局环境只能访问在全局环境中定义的变量和函数,而不能直接访问局部环境中的任何数据;
变量的执行环境有助于确定应该何时释放内存。
JavaScript 是一门具有自动垃圾收集机制的编程语言,开发人员不必关心内存分配和回收问题。可以对 JavaScript 的垃圾收集例程作如下总结:
离开作用域的值将被自动标记为可以回收,因此将在垃圾收集期间被删除。
“标记清除”是目前主流的垃圾收集算法,这种算法的思想是给当前不使用的值加上标记,然后再回收其内存。
另一种垃圾收集算法是“引用计数”,这种算法的思想是跟踪记录所有值被引用的次数。JavaScript引擎目前都不再使用这种算法;但在 IE 中访问非原生 JavaScript 对象(如 DOM 元素)时,这种算法仍然可能会导致问题。
当代码中存在循环引用现象时,“引用计数”算法就会导致问题。
解除变量的引用不仅有助于消除循环引用现象,而且对垃圾收集也有好处。为了确保有效地回收内存,应该及时解除不再使用的全局对象、全局对象属性以及循环引用变量的引用。
第五章 引用类型
Object 类型
1、创建 Object 实例的方式有两种。
(1)使用 new 操作符后跟 Object 构造函数
var person = new Object();
person.name = "Nicholas";
person.age = 29;
(2)另一种方式是使用对象字面量表示法。(推荐)
var person = {
name : "Nicholas",
age : 29
};
var person = {}; //与 new Object()相同
person.name = "Nicholas";
person.age = 29;
在通过对象字面量定义对象时,实际上不会调用 Object 构造函数。
2、访问对象属性(除非必须使用变量来访问属性,否则我们建议使用点表示法。)
一般来说,访问对象属性时使用的都是点表示法,这也是很多面向对象语言中通用的语法。不过,在 JavaScript 也可以使用方括号表示法来访问对象的属性。在使用方括号语法时,应该将要访问的属性以字符串的形式放在方括号中。
alert(person["name"]); //"Nicholas"
alert(person.name); //"Nicholas"
从功能上看,这两种访问对象属性的方法没有任何区别。但方括号语法的主要优点是可以通过变量来访问属性:
var propertyName = "name";
alert(person[propertyName]); //"Nicholas"
如果属性名中包含会导致语法错误的字符,或者属性名使用的是关键字或保留字,也可以使用方括号表示法:
person["first name"] = "Nicholas";
Array 类型
ECMAScript 数组的每一项可以保存任何类型的数据。而且,ECMAScript 数组的大小是可以动态调整的,即可以随着数据的添加自动增长以容纳新增数据。
1、创建数组的基本方式有两种。
(1)是使用 Array 构造函数
var colors = new Array();
var colors = new Array(20); // 创建 length 值为 20 的数组。
也可以向 Array 构造函数传递数组中应该包含的项。
var colors = new Array("red", "blue", "green");
另外,在使用 Array 构造函数时也可以省略 new 操作符。
var colors = Array(3); // 创建一个包含 3 项的数组
var names = Array("Greg"); // 创建一个包含 1 项,即字符串"Greg"的数组
(2)使用数组字面量表示法
数组字面量由一对包含数组项的方括号表示,多个数组项之间以逗号隔开。
var colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组
var names = []; // 创建一个空数组
var values = [1,2,]; // 不要这样!这样会创建一个包含 2 或 3 项的数组
var options = [,,,,,]; // 不要这样!这样会创建一个包含 5 或 6 项的数组
数组的项数保存在其 length 属性中,这个属性始终会返回 0 或更大的值。
var colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组
var names = []; // 创建一个空数组
alert(colors.length); //3
alert(names.length); //0
2、检测数组
(1)instanceof 操作符
if (value instanceof Array){
//对数组执行某些操作
}
instanceof 操作符的问题在于,它假定只有一个全局执行环境。如果网页中包含多个框架,那实际上就存在两个以上不同的全局执行环境,从而存在两个以上不同版本的 Array 构造函数。如果你从一个框架向另一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有各自不同的构造函数。
(2)Array.isArray()
if (Array.isArray(value)){
//对数组执行某些操作
}
不过这个方法在低版本的 IE 的浏览器中是不支持的。
(3)数组构造函数
let arr = [1, 2, 3];
console.log(arr.constructor === Array) // true
这种方法和方法 1 有相同的缺点,在多个环境下 Array 构造函数有可能是不同的,而且在多个环境下如果要进行数组传递的话很有可能会出现问题。
(4)Object.prototype.toString.call() 方法
在任何值上调用 Object 原生的 toString 方法,都会返回 [object NativeConstructorName] 格式的字符串,利用这一点也可以用来检测是不是数组。
let arr = [1, 2, 3];
console.log(Object.prototype.toString.call(arr) === "[object Array]"); // true
由于原生数组的构造函数名与全局环境无关,因此无论在哪一个环境下这个方法都可以正确的检测一个数组。
3、转换方法
数组继承的 toLocaleString()、toString()和 valueOf()方法,在默认情况下都会以逗号分隔的字符串的形式返回数组项。
而如果使用 join()方法,则可以使用不同的分隔符来构建这个字符串。join()方
法只接收一个参数,即用作分隔符的字符串,然后返回包含所有数组项的字符串。
var colors = ["red", "green", "blue"];
alert(colors.join(",")); //red,green,blue
alert(colors.join("||")); //red||green||blue
如果数组中的某一项的值是 null 或者 undefined,那么该值在 join()、
toLocaleString()、toString()和 valueOf()方法返回的结果中以空字符串表示。
4、栈方法
栈是一种 LIFO(Last-In-First-Out,后进先出)的数据结构,也就是最新添加的项最早被移除。而栈中项的插入(叫做推入)和移除(叫做弹出),只发生在一个位置——栈的顶部。
(1)pop ()
pop()方法则从数组末尾移除最后一项,减少数组的 length 值,然后返回移除的项。
(2)push ()
push()方法可以接收任意数量的参数,把它们逐个添加到数组末尾,并返回修改后数组的长度。
var colors = new Array(); // 创建一个数组
var count = colors.push("red", "green"); // 推入两项
alert(count); //2
count = colors.push("black"); // 推入另一项
alert(count); //3
var item = colors.pop(); // 取得最后一项
alert(item); //"black"
alert(colors.length); //2
4、队列方法
栈数据结构的访问规则是 LIFO(后进先出),而队列数据结构的访问规则是 FIFO(First-In-First-Out,先进先出)。
(1)shift()
移除数组中的第一个项并返回该项,同时将数组长度减 1。
var colors = new Array(); //创建一个数组
var count = colors.push("red", "green"); //推入两项
alert(count); //2
count = colors.push("black"); //推入另一项
alert(count); //3
var item = colors.shift(); //取得第一项
alert(item); //"red"
alert(colors.length); //2
count = colors.unshift("black"); //推入另一项
alert(count); //3
(2)unshift()
能在数组前端添加任意个项并返回新数组的长度。
5、重排序方法
数组中已经存在两个可以直接用来重排序的方法:reverse()和 sort()。
var values = [1, 2, 3, 4, 5];
values.reverse(); // reverse()方法会反转数组项的顺序。
alert(values); //5,4,3,2,1
在默认情况下,sort()方法按升序排列数组项——即最小的值位于最前面,最大的值排在最后面。为了实现排序,sort()方法会调用每个数组项的 toString()转型方法,然后比较得到的字符串,以确定如何排序。即使数组中的每一项都是数值,sort()方法比较的也是字符串。
sort()的比较函数在第一个值应该位于第二个之后的情况下返回 1,而在第一个值应该在第二个之前的情况下返回1。交换返回值的意思是让更大的值排位更靠前,也就是对数组按照降序排序。当然,如果只想反转数组原来的顺序,使用 reverse()方法要更快一些。
6、操作方法
(1)concat() (不会影响原始数组)
可以基于当前数组中的所有项创建一个新数组。具体来说,这个方法会先创建当前数组一个副本,然后将接收到的参数添加到这个副本的末尾,最后返回新构建的数组。
var colors = ["red", "green", "blue"];
var colors2 = colors.concat("yellow", ["black", "brown"]);
alert(colors); //red,green,blue
alert(colors2); //red,green,blue,yellow,black,brown
(2)slice() (不会影响原始数组)
它能够基于当前数组中的一或多个项创建一个新数组。slice()方法可以接受一或两个参数,即要返回项的起始和结束位置,但不包括结束位置的项。在只有一个参数的情况下,slice()方法返回从该参数指定位置开始到当前数组末尾的所有项。
var colors = ["red", "green", "blue", "yellow", "purple"];
var colors2 = colors.slice(1);
var colors3 = colors.slice(1,4);
alert(colors2); //green,blue,yellow,purple
alert(colors3); //green,blue,yellow
如果 slice()方法的参数中有一个负数,则用数组长度加上该数来确定相应的位
置。例如,在一个包含 5 项的数组上调用 slice(-2,-1)与调用 slice(3,4)得到的
结果相同。如果结束位置小于起始位置,则返回空数组。
(4)splice()
splice()的主要用途是向数组的中部插入项,有如下三种规则:
删除:可以删除任意数量的项,只需指定 2 个参数:要删除的第一项的位置和要删除的项数。例如,splice(0,2)会删除数组中的前两项。
插入:可以向指定位置插入任意数量的项,只需提供 3 个参数:起始位置、0(要删除的项数)和要插入的项。如果要插入多个项,可以再传入第四、第五,以至任意多个项。例如,splice(2,0,"red","green")会从当前数组的位置 2 开始插入字符串"red"和"green"。
替换:可以向指定位置插入任意数量的项,且同时删除任意数量的项,只需指定 3 个参数:起始位置、要删除的项数和要插入的任意数量的项。插入的项数不必与删除的项数相等。例如,splice (2,1,"red","green")会删除当前数组位置 2 的项,然后再从位置 2 开始插入字符串"red"和"green"。
splice()方法始终都会返回一个数组,该数组中包含从原始数组中删除的项(如果没有删除任何项,则返回一个空数组)。
7、位置方法
(1)indexOf()
接收两个参数:要查找的项和(可选的)表示查找起点位置的索引,从数组的开头(位置 0)开始向后查找
(2)lastIndexOf()
接收两个参数:要查找的项和(可选的)表示查找起点位置的索引,则从数组的末尾开始向前查找。
这两个方法都返回要查找的项在数组中的位置,或者在没找到的情况下返回-1。。在比较第一个参数与数组中的每一项时,会使用全等操作符;也就是说,要求查找的项必须严格相等(就像使用===一样)。
var numbers = [1,2,3,4,5,4,3,2,1];
alert(numbers.indexOf(4)); //3
alert(numbers.lastIndexOf(4)); //5
alert(numbers.indexOf(4, 4)); //5
alert(numbers.lastIndexOf(4, 4)); //3
var person = { name: "Nicholas" };
var people = [{ name: "Nicholas" }];
var morePeople = [person];
alert(people.indexOf(person)); //-1
alert(morePeople.indexOf(person)); //0
8、迭代方法
ECMAScript 5 为数组定义了 5 个迭代方法。每个方法都接收两个参数:要在每一项上运行的函数和(可选的)运行该函数的作用域对象——影响 this 的值。传入这些方法中的函数会接收三个参数:数组项的值、该项在数组中的位置和数组对象本身。
every():对数组中的每一项运行给定函数,如果该函数对每一项都返回 true,则返回 true。
filter():对数组中的每一项运行给定函数,返回该函数会返回 true 的项组成的新数组。
forEach():对数组中的每一项运行给定函数。这个方法没有返回值,改变原数组。
map():对数组中的每一项运行给定函数,返回每次函数调用的结果组成的新数组,不改变原数组。
some():对数组中的每一项运行给定函数,如果该函数对任一项返回 true,则返回 true。