1. 数据类型

1.1 基本类型和引用类型的值

在JavaScript中,变量的数据类型分为基本类型和引用类型。
基本类型指那些简单的数据段,包括Boolean、Number、String、Undefined、Null、Symbol,基本数据类型是直接按值访问的,可以操作保存在变量中的实际的值。
引用类型的值是保存在内存中的对象,JavaScript不能直接访问内存中的位置,而是通过指针将变量与内存中的对象联系起来。因此在操作对象时,实际上是在操作对象的引用。

1.2 栈内存和堆内存

为了更加深入的理解JS变量基本类型和引用类型的区别,我们还需要了解JS变量的值在内存中的存储方式。

1.2.1 栈内存

栈,是一种数据结构,有着先进后出、后进先出的特点。就像一个羽毛球筒,只有一个口(即是出口也是入口),最先进入的球只能最后拿出来:

image.png

如上图,入栈的顺序:1、2、3,出栈的顺序:3、2、1

1.2.2 栈内存中的数据存储

栈内存用来保存基本类型的变量或变量的指针,举个例子:

var num1 = 3;

变量 num1 在内存中的保存形式为:

image.png

对于基本类型的变量,当我们进行复制操作时,比如这个例子:

var num1 = 3;
vat num2 = num1;

此时JS会创建一个新值并将新值分配给新的变量,内存中的变量对象表示为:

image.png

变量 num2 得到的是一个全新的值,与变量 num1 中的值无关。

1.2.3 堆内存

堆内存与栈内存不同,对于变量值的保存没有需要遵循的规律。

image.png

1.2.4 堆内存中的数据存储

堆内存用来保存对象类型的变量值,对象的内容和大小也会随时变化。而此时变量对象中的变量保存的是一个指向堆内存中对象的指针,由于存在这种引用关系,对象也被称为引用类型。例:

var num1 = 3;
var obj = {
  a: 1
}

image.png

当我们对对象类型的值进行复制操作时,实际上copy的是对象的指针,因此两个变量指向的是同一个对象,例:

var obj 1 = { a: 1 }
var obj2 = obj1;

在内存中表现为:

image.png

此时,修改变量 obj1 的属性等同于修改变量 obj2,例:

var obj1 = { a: 1 };
vat obj2 = obj1;
obj1.b = 2;
alert(obj2.b); // 2

1.3 包装对象

对于引用类型的值,我们可以为其添加属性和方法,也可以改变和删除其属性和方法:

var person = new Object();
person.name = 'Ian';
alert(person.name); // 'Ian'

但是,我们不能给基本类型的值添加属性,尽管这样做不会导致错误:

var name = 'Ian';
name.age = 29;
alert(name.age); // undefined

我们可能听过一种说法,基本类型的值是没有属性或方法的。可有意思的是,上述给基本类型的值添加属性的操作不会报错,我们还能读取某些基本类型的值,比如我们可以读到String类型值的length属性:

var name = 'Ian';
name.length; // 3
那么基本类型值的属性是从哪里来的呢?

其实,当我们操作基本类型值的属性时,JS会创建一个临时的包装对象,我们操作的实际上是这个包装对象的属性,而这个包装对象是用完立即销毁的。在上面的例子中,name.length 的 length 属性实际上来自包装对象。

var name = 'Ian';
name.length;
var nameObj = new String(name); // 包装对象
nameObj.length; // 3

由于包装对象用完立即销毁的特性,我们对基本类型值的属性修改都是“无效”的:

var name = 'Ian';
name.length = 4;
name.length; // 3

上面的例子中,当我们再次读取 name.length 时,实际上又创建了一个新的包装对象,读取的是新包装对象的 length 属性。

2. 执行环境

执行环境(也称环境)是JavaScript重要的概念,执行环境定义了变量或函数有权访问的数据。每个执行环境都有一个相关联的变量对象,这个变量对象中保存了当前执行环境中定义的所有变量和函数。

2.1 全局执行环境

JS代码执行时最外层的执行环境称为全局执行环境,由于宿主环境的不同,表示全局执行环境的对象也不同,在web浏览器中将window对象作为全局执行环境,全局的变量和函数都是作为window对象的属性和方法声明的。

2.2 执行环境栈

函数也有执行环境,当开始执行一个函数时,函数执行环境会被压入一个执行环境栈中,在函数执行性完成后,执行环境栈将函数环境弹出,将主导权交给上层环境。我们通过一段代码来展示执行环境栈的变化过程:

var outerName = 'Ian';
function changeName() {
  var innerName = 'Jack';
  outerName = innerName;
}
alert(outerName); // 'Jack'

在初始状态下,执行环境栈是这样的:

image.png

当开始执行函数 changeName 时,函数的环境被压入环境栈:

image.png

当函数 changeName 执行完成后,该环境被弹出环境栈并销毁(环境栈恢复到上一步的状态),保存在其中的变量和函数的定义也随之销毁。全局环境只到程序退出,在web浏览器中关闭网页或浏览器程序时才会销毁。
JS这种执行机制也引出了另一个重要的概念——作用域链。

2.3 作用域链

JS代码在进入一个新的执行环境时会创建一个作用域链(scope chain),用来规定对当前执行环境有权访问的变量或函数的访问顺序。作用域链是由执行环境的变量对象组成的,如果是函数环境就将函数的活动对象作为变量对象。
作用域链的最前端永远是当前执行环境的变量对象,上一级变量对象来自上一层执行环境,直到全局环境的变量对象。当我们访问一个变量时,会先在当前的变量对象中查找,如果找不到就会沿着作用域链逐级向上查找,直到返回变量的值或报错(变量未声明)。
下面我们通过一个例子来理解这种结构:

var outerName = 'Ian';
function changeName() {
  var innerName = 'Jack';
  outerName = innerName;
  function alertName() {
    alert(outerName); // 'Jack'
  }
}
alert(innerName); // Uncaught ReferenceError: innerName is not defined

在上面这段代码中,函数可以访问到外部定义的变量 outerName,而在全局环境中不能访问函数内部定义的变量 innerName。当代码进入 alertName 执行环境时,作用域链如下图所示:

image.png

3. 小结

在JavaScript中,变量的数据类型分为基本类型和引用类型,他们具有以下特点:

  • 基本类型的值大小固定保存在栈内存中,当复制基本类型的值时,会创建一个值的副本;
  • 引用类型的值是对象,保存在堆内存中,而变量保存的是对象的引用,当复制引用类型的值时,复制的是对象的引用,两个变量都指向同一个对象;
  • 全局执行环境和函数执行环境;
  • 在进入一个新的执行环境时会创建一个作用域链(scope chain),用来规定对当前执行环境有权访问的变量或函数的访问顺序;
  • 函数局部环境能访问父级(直到全局)环境的变量,而全局(父级)环境不能访问函数的局部变量;

前端小酱
1.8k 声望1.6k 粉丝

做一个有温度的前端攻城狮