在 JavaScript 中,变量可以不先声明,而在使用时,再根据变量的实际作用来确定其所属的数据类型。但是建议在使用变量前就对其声明,因为声明变量的最大好处就是能及时发现代码中的错误。 JavaScript 是采用动态编译的,而动态编译是不易于发现代码中的错误的,特别是变量命名方面的错误。
ECMAScript 变量可以包含两种不同类型的数据:原始值和引用值。原始值( primitive value )就是 最简单的数据,引用值( reference value )则是由多个值构成的对象。
在把一个值赋给变量时, JavaScript 引擎必须确定这个值是原始值还是引用值。 原始值: Undefined 、 Null 、 Boolean 、 Number 、 String 和 Symbol 。保存原始值的变量是按值( by value )访问的,因为我们操作的就是存储在变量中的实际值。 引用值是保存在内存中的对象。与其它语言不同, JavaScript 不允许直接访问内存位置,因此也就 不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的引用( reference )而非 实际的对象本身。为此,保存引用值的变量是按引用( by reference )访问的。
只有引用值可以动态添加后面可以使用的属性。
变量的作用域跟声明方式有很密切的关系。使用 var 声明的变量的作用域有全局作用域和函数作用域,没有块级作用域;使用 let 和 const 声明的变量有全局作用域、局部作用域和块级作用域。 注:严格意义的全局变量都属于 window 对象的属性,但 let 和 const 声明的变量并不属于 window 对象,所以它们并不是严格意义上的全局变量,在此仅仅从它们的作用域这个角度来说它们是全局变量的。
let name1 = 'Tom';
let name2 = new String('Jerry');
name1.age = 2;
name2.age = 1;
console.log(name1.age); // undefined
console.log(name2.age); // 1
console.log(typeof name1); // string
console.log(typeof name2); // object;
在通过变量把一个原始值赋值 到另一个变量时,原始值会被复制到新变量的位置。 在把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。区 别在于,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。操作完成后,两个变量实际 上指向同一个对象,因此一个对象上面的变化会在另一个对象上反映出来 。
ECMAScript 中所有函数的参数都是按值传递的。这意味着函数外的值会被复制到函数内部的参数 中,就像从一个变量复制到另一个变量一样。如果是原始值,那么就跟原始值变量的复制一样,如果是 引用值,那么就跟引用值变量的复制一样。对很多开发者来说,这一块可能会不好理解,毕竟变量有按 值和按引用访问,而传参则只有按值传递。
在按值传递参数时,值会被复制到一个局部变量(即一个命名参数,或者用 ECMAScript 的话说, 就是 arguments 对象中的一个槽位)。在按引用传递参数时,值在内存中的位置会被保存在一个局部变 量,这意味着对本地变量的修改会反映到函数外部。
let 跟 var 的作用差不多,但有着非常重要的区别。最明显的区别是, let 声明的范围是块作用域, 而 var 声明的范围是函数作用域。
暂时性死区
let 与 var 的另一个重要的区别,就是 let 声明的变量不会在作用域中被提升。 let 声明之前的执行瞬间被称为 "暂时性死区 "( temporal dead zone ),在此 阶段引用任何后面才声明的变量都会抛出 ReferenceError 。
全局声明
var 关键字不同,使用 let 在全局作用域中声明的变量不会成为 window 对象的属性( var 声 明的变量则会) 。不过, let 声明仍然是在全局作用域中发生的,相应变量会在页面的生命周期内存续。因此,为了 避免 SyntaxError ,必须确保页面不会重复声明同一个变量。
条件声明
在使用 var 声明变量时,由于声明会被提升, JavaScript 引擎会自动将多余的声明在作用域顶部合 并为一个声明。因为 let 的作用域是块,所以不可能检查前面是否已经使用 let 声明过同名变量,同 时也就不可能在没有声明的情况下声明它。
不能使用 let 进行条件式声明是件好事,因为条件声明是一种反模式,它让程序变 得更难理解。如果你发现自己在使用这个模式,那一定有更好的替代方式。
for 循环中的 let 声明
在使用 let 声明迭代变量时, JavaScript 引擎在后台会为每个迭代循环声明一个新的迭代变量。 每个 setTimeout 引用的都是不同的变量实例。
这种每次迭代声明一个独立变量实例的行为适用于所有风格的 for 循环,包括 for-in 和 for-of 循环。
当程序运行时,值不能改变的量为常量( constant )。常量主要用于为程序提供固定的和精确的值(包括数值和字符串)。比如数字、逻辑值真( true )、逻辑值假( false )等都是常量。常量使用 const 来进行声明,语法如下:
const property: string = 'value';
如果在程序中过多地使用常量,会降低程序的可读性和可维护性。当一个常量在程序内被多次引用时,可以考虑在程序开始处将它设置为变量,再引用。当此值需要修改时,只需更改其变量的值就可以了,既减少出错的机会,又可以提高工作效率。
const 声明的限制只适用于它指向的变量的引用。换句话说,如果 const 变量引用的是一个对象, 那么修改这个对象内部的属性并不违反 const 的限制。
JavaScript 会在预编译期先预处理声明的变量。但是,变量的赋值操作发生在 JavaScript 执行期,而不是预编译期。
在函数调用之前,函数内部定义的全局变量是无效的,因为在 JavaScript 预编译期,仅对函数命名、函数内部各种标识符建立索引。
只有在 JavaScript 执行期,才按顺序进行赋值,并初始化。而在执行期,如果函数未被调用,则函数内代码不被解析。
scope 指变量在程序中的可访问范围,也称可见性。在 JavaScript 中,变量作用域可以分为全局变量和局部变量。在函数内使用全局变量是很危险的 。
执行上下文(以下简称 "上下文 ")的概念在 JavaScript 中是颇为重要的。变量或函数的上下文决定 了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象( variable object ), 而这个上下文中定义的所有变量和函数都存在于这个对象上。虽然无法通过代码访问变量对象,但后台 处理数据会用到它。
每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。 在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。
上下文中的代码在执行的时候,会创建变量对象的一个作用域链( scope chain )。这个作用域链决定 了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域链的最前端。如果上下文是函数,则其活动对象( activation object )用作变量对象。活动对象最初只有 一个定义变量: arguments 。(全局上下文中没有这个变量。)作用域链中的下一个变量对象来自包含上下文,再下一个对象来自再下一个包含上下文。以此类推直至全局上下文;全局上下文的变量对象始终是作用域链的最后一个变量对
虽然执行上下文主要有全局上下文和函数上下文两种( eval() 调用内部存在第三种上下文),但有 其它方式来增强作用域链。某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执 行后会被删除。通常在两种情况下会出现这个现象,即代码执行到下面任意一种情况时:
try/catch
语句的 catch
块这两种情况下,都会在作用域链前端添加一个变量对象。对 with 语句来说,会向作用域链前端添 加指定的对象;对 catch 语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误 对象的声明。
简单数据类型(例如 Number 和 String )是按值进行传递的,这种传递类型被称为传值。当按值传递时,将在计算机内存中分配一块空间并将原值复制到其中,这意味着变量的实际内容会传递给变量。然后,即使更改原来变量的值,也不会影响复制到新变量中的值(反过来也一样),因为这两个值是独立的实体。
复杂数据类型可以包含大量和复杂的信息,所以属于此类型的变量并不包含实际的值,它包含的是对值的引用,这种传递类型称为传址。这种引用类似于指向变量内容的别名(在一些程序语言中称为 "指针" )。当变量需要知道它的值时,该引用会查询内容,然后返回答案,而无需将该值传递给变量。
JavaScript 通过自动内存管理实现内存分配和闲置资源回收。基本思路很简单:确定哪个变量不会再 使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动运行。
JavaScript 最常用的垃圾回收策略是标记清理( mark-and-sweep )。当变量进入上下文,比如在函数 内部声明一个变量时,这个变量会被加上存在于上下文中的标记。而在上下文中的变量,逻辑上讲,永远不应该释放它们的内存,因为只要上下文中的代码在运行,就有可能用到它们。当变量离开上下文时, 也会被加上离开上下文的标记。
另一种没那么常用的垃圾回收策略是引用计数( reference counting )。其思路是对每个值都记录它被 引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为 1 。如果同一个值又被赋给另一个变 量,那么引用数加 1 。类似地,如果保存对该值引用的变量被其它值给覆盖了,那么引用数减 1 。当一 个值的引用数为 0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序 下次运行的时候就会释放引用数为 0 的值的内存。
引用计数最早由 Netscape Navigator 3.0 采用,但很快就遇到了严重的问题:循环引用。所谓循环引 用,就是对象 A 有一个指针指向对象 B ,而对象 B 也引用了对象 A 。
将内存占用量保持在一个较小的值可以让页面性能更好。优化内存占用的最佳手段就是保证在执行 代码时只保存必要的数据。如果数据不再必要,那么把它设置为 null ,从而释放其引用。这也可以叫作解除引用。这个建议最适合全局变量和全局对象的属性。局部变量在超出作用域后会被自动解除引用 。
ES6 增加这两个关键字不仅有助于改善代码风格,而且同样有助于改进垃圾回收的过程。
运行期间, V8 会将创建的对象与隐藏类关联起来,以跟踪它们的属性特征。能够共享相同隐藏类 的对象性能会更好, V8 会针对这种情况进行优化,但不一定总能够做到。