基本类型与引用类型

JavaScript 变量松散类型的本质,决定了它只是在特定时间用于保存特定值的一个名字而已。

基本类型:undefined、Null、Boolean、Number、String。

引用类型:当复制保存着对象的某个变量时,操作的是对象的引用。但在为对象添加属性时,操作的是实际的对象。

注意:只能给引用类型的值动态的添加属性。

1
2
3
4
5
6
7
var person = new Object();
person.name = "nike";
console.log(person.name) //nike
var name = "peter";
name.age = 27;
console.log(name.age) // undefined
复制变量值

变量访问方式分为两种,按值访问和按引用访问。

在对变量复制时,基本类型是直接复制了变量代表的值,而引用类型则只是复制了变量的引用。

常规例子:

1
2
3
4
5
6
7
8
let a = 1;
let b = {};
let c = a;
b = 2
let d = b;
d.name = 'nike';
console.log(a) // 1
console.log(b.name) // nike
传递参数

在ECMAScript 中所有函数参数都是按值传递的。

常规例子:

1
2
3
4
5
6
7
8
9
let a = 1;
let b = {};
function test(x,y) { // y其实是b的值,也就是对一个对象的引用
x += 1;
y.name = 'peter';
}
test(a,b);
console.log(a) // 1
console.log(b.name) // 'peter'

特殊例子:

1
2
3
4
5
6
let b = {};
function test(y) {
y = { name:'peter' };
}
test(b);
console.log(b.name) // undefined

这里为什么b没有变成新对象,是因为在向函数传递参数时,复制的变量会赋值给函数内部的局部变量(arguments对象中的一个元素)。所以当我们修改y时候,其实只是修改了局部变量y的引用,而b的引用是没有变的,并且局部变量在函数执行完毕后立即被销毁。

类型判断

typeof 操作符是确定一个变量是字符串、数值、布尔值,还是 undefined 的最佳工具。如果变量的值是一个对象或 null ,则 typeof 操作符返回 “object”。

但是typeof在检测引用类型时作用不打,因为一般都会返回 ‘object’ 。所以还提供了一个方法 instanceof。其原理是根据对象的原型链查找,是否属于该对象的子类,是则会返回true,不过要注意的是如果对非对象使用,则都会返回false:

1
2
3
4
[] instanceof Array; // true
[] instanceof Object; // true
{} instanceof Object; // true
'a' instanceof String; // false

还有一种比较常见的方式是通过调用对象的toString方法来判断类型:

1
2
3
4
5
6
7
8
Object.prototype.toString.call('a')
// "[object String]"
Object.prototype.toString.call([])
// "[object Array]"
Object.prototype.toString.call(null)
// "[object Null]"
Object.prototype.toString.call(undefined)
// "[object Undefined]"

执行环境及作用域

执行环境定义了变量或函数有权访问的其他数据,决定它们各自的行为。

每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。

执行环境一般嵌套的,比如浏览器中最外围的执行环境对应的变量对象就是window.因此所有全局变量和函数都是作为window对象的属性和方法创建的。

某个执行环境中的代码执行完毕后,其中定义的变量和函数就会被销毁。(最外层的比如window只会在关闭浏览器时才销毁);

每个函数都有自己的执行环境。当执行流进入一个函数,函数的环境就会被推入执行环境栈,执行完毕后,又会弹出。

当代码在一个环境中执行,会创建对象的一个作用域链(scope chain)。作用域链的用于主要是控制执行环境中代码访问变量时的有序性。作用域链的最前端,始终是当前执行代码所在环境的变量对象。如果这个环境是对象,则函数的活动对象(activation object)就是变量对象。最初的变量对象只包含一个变量,即arguments对象(这个对象在全局环境中是不存在的)。作用域链的下一个环境来之包含(外一层)环境。而再下一层来自包含环境的包含环境,以此原理一直延续到全局执行环境。

变量的标识符解析就是沿着作用域链来搜索的过程,搜索会从作用域链的前端开始,然后逐级的向上(注意这是不可逆的规则,即外面是不能访问里面的),直到找到标识符或者最外层停止,如果找不到,通常就会报错。

变量查询也不是没有代价的。很明显,访问局部变量要比访问全局变量更快,因为不用向上搜索作用域链。

延长作用域链

虽然执行环境的类型总共只有两种——全局和局部(函数),但还是有其他办法来延长作用域链。这么说是因为有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。

  • try catch
  • with

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

1
2

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

块级作用域

在没有es6的let之前,js是不存在块级作用域的(只有函数和全局作用域)。

使用 var 声明的变量会自动被添加到最接近的环境中。在函数内部,最接近的环境就是函数的局部环境;如果初始化变量时没有使用 var 声明,该变量会自动被添加到全局环境。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 没有块级作用域,所以最近的环境就是全局环境
iftrue){
var color = 'blue';
}
console.log(color); //blue
// 最近的环境就是函数环境
function add(a, b) {
var sum = a+b;
return sum;
}
var result = add(10, 20); // 30
console.log(sum); // error
function add1(a, b) {
sum = a+b; // 没有用var 定义
return sum;
}
var result1 = add(10, 20);
console.log(sum) // 30

垃圾回收

垃圾收集机制的原理其实很简单:找出那些不再继续使用的变量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间),周期性地执行这一操作。

两种策略:

  • JavaScript 中最常用的垃圾收集方式是标记清除(mark-and-sweep)。当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。

    垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。

  • 另一种不太常见的垃圾收集策略叫做引用计数(reference counting)。引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 1。如果同一个值又被赋给另一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存。

    引用计数的致命问题是循环引用,如果在一个函数中有两个循环引用的变量,那么在函数退出后,就不会回收,最终引起内存泄露。

现在主流的浏览器已经都采用标记清除,不过在IE中的BOM和DOM对象,任有引用计数的策略,需要注意。

浏览器分配到的内存一般不多,所以在js中要注意全局变量的引用,因为它们会一直存在与执行环境,手动的清除方法为将其指定为NULL,那么下一次垃圾回收就会将其回收。

总结:

JavaScript 变量可以用来保存两种类型的值:基本类型值和引用类型值。基本类型的值源自以下 5种基本数据类型: Undefined 、 Null 、 Boolean 、 Number 和 String 。基本类型值和引用类型值具有以下特点:

  • 基本类型值在内存中占据固定大小的空间,因此被保存在栈内存中;
  • 从一个变量向另一个变量复制基本类型的值,会创建这个值的一个副本;
  • 引用类型的值是对象,保存在堆内存中;
  • 包含引用类型值的变量实际上包含的并不是对象本身,而是一个指向该对象的指针;
  • 从一个变量向另一个变量复制引用类型的值,复制的其实是指针,因此两个变量最终都指向同一个对象;
  • 确定一个值是哪种基本类型可以使用 typeof 操作符,而确定一个值是哪种引用类型可以使用instanceof 操作符。

所有变量(包括基本类型和引用类型)都存在于一个执行环境(也称为作用域)当中,这个执行环境决定了变量的生命周期,以及哪一部分代码可以访问其中的变量。以下是关于执行环境的几点总结:

  • 执行环境有全局执行环境(也称为全局环境)和函数执行环境之分;
  • 每次进入一个新执行环境,都会创建一个用于搜索变量和函数的作用域链;
  • 函数的局部环境不仅有权访问函数作用域中的变量,而且有权访问其包含(父)环境,乃至全局环境;
  • 全局环境只能访问在全局环境中定义的变量和函数,而不能直接访问局部环境中的任何数据;
  • 变量的执行环境有助于确定应该何时释放内存。

JavaScript 是一门具有自动垃圾收集机制的编程语言,开发人员不必关心内存分配和回收问题。可以对 JavaScript 的垃圾收集例程作如下总结:

  • 离开作用域的值将被自动标记为可以回收,因此将在垃圾收集期间被删除。
  • “标记清除”是目前主流的垃圾收集算法,这种算法的思想是给当前不使用的值加上标记,然后再回收其内存。
  • 另一种垃圾收集算法是“引用计数”,这种算法的思想是跟踪记录所有值被引用的次数。JavaScript引擎目前都不再使用这种算法;但在 IE 中访问非原生 JavaScript 对象(如 DOM 元素)时,这种算法仍然可能会导致问题。
  • 当代码中存在循环引用现象时,“引用计数”算法就会导致问题。
  • 解除变量的引用不仅有助于消除循环引用现象,而且对垃圾收集也有好处。为了确保有效地回收内存,应该及时解除不再使用的全局对象、全局对象属性以及循环引用变量的引用。