20220926 未检查完

1 原始值与引用值

ECMAScript 变量包含两种不同类型的数据:原始值和引用值。原始值(primitive value)是最简单的数据,引用值(reference value)则是由多个值构成的对象。

在把一个值赋给变量时,JavaScript 引擎必须确定这个值是原始值还是引用值。上一节讨论了 6 种原始值:Undefined、Null、Boolean、Number、String 和 Symbol。保存原始值的变量是按值(by value)访问的,因为我们操作的就是存储在变量中的实际值。

引用值是保存在内存中的对象。与其他语言不同,JavaScript 不允许直接访问内存位置,因此也就不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的引用(reference)而非实际的对象本身。为此,保存引用值的变量是按引用(by reference)访问的。

注意 在很多语言中,字符串是使用对象表示的,因此被认为是引用类型。ECMAScript打破了这个惯例。

1.1 动态属性

原始值和引用值的定义方式很类似,都是创建一个变量,然后给它赋一个值。但变量保存了这个值之后的操作有所不同。对于引用值而言,可以随时添加、修改和删除其属性和方法。比如,看下面的例子:

let person = new Object();
person.name = "Nicholas";
console.log(person.name); // "Nicholas"

这里,首先创建了一个对象,并把它保存在变量 person 中。然后,给这个对象添加了一个名为name 的属性,并给这个属性赋值了一个字串"Nicholas"。在此之后,就可以访问这个新属性,直到对象被销毁或属性被显式地删除。

原始值不能有属性,尽管尝试给原始值添加属性不会报错。比如:

let name = "Nicholas";
name.age = 27;
console.log(name.age); // undefined

注意,原始类型的初始化可以只使用原始字面量形式。如果使用的是 new 关键字,则 JavaScript 会创建一个 Object 类型的实例,但其行为类似原始值。下面展示了这两种初始化方式的差异:

let name1 = "Nicholas";
let name2 = new String("Matt");
name1.age = 27;
name2.age = 26;
console.log(name1.age); // undefined
console.log(name2.age); // 26
console.log(typeof name1); // string
console.log(typeof name2); // object

1.2 复制值

除了存储方式不同,原始值和引用值在通过变量复制时也有所不同。在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置。请看下面的例子:

let num1 = 5;
let num2 = num1;

这里,num1 包含数值 5。当把 num2 初始化为 num1 时,num2 也会得到数值 5。这个值跟存储在num1 中的 5 是完全独立的,因为它是那个值的副本。此时这两个变量可以独立使用,互不干扰。

在把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。区别在于,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。操作完成后,两个变量实际上指向同一个对象,因此一个对象上面的变化会在另一个对象上反映出来,如下面的例子所示:

let obj1 = new Object();
let obj2 = obj1;
obj1.name = "Nicholas";
console.log(obj2.name); // "Nicholas"

在这个例子中,变量 obj1 保存了一个新对象的实例。然后,这个值被复制到 obj2,此时两个变量都指向了同一个对象。在给 obj1 创建属性 name 并赋值后,通过 obj2 也可以访问这个属性,因为它们都指向同一个对象。

1.3 传递参数

ECMAScript 中所有函数的参数都是按值传递的。这意味着函数外的值会被复制到函数内部的参数中,就像从一个变量复制到另一个变量一样。如果是原始值,那么就跟原始值变量的复制一样,如果是引用值,那么就跟引用值变量的复制一样。

在按值传递参数时,值会被复制到一个局部变量(即一个命名参数,或者用 ECMAScript 的话说,就是 arguments 对象中的一个槽位)。来看下面这个例子:

function addTen(num) { num += 10; return num;
} let count = 20;
let result = addTen(count);
console.log(count); // 20,没有变化
console.log(result); // 30

这里,函数 addTen()有一个参数 num,它其实是一个局部变量。在调用时,变量 count 作为参数传入。count 的值是 20,这个值被复制到参数 num 以便在 addTen()内部使用。在函数内部,参数 num的值被加上了 10,但这不会影响函数外部的原始变量 count。参数 num 和变量 count 互不干扰,再看这个例子:

function setName(obj) { obj.name = "Nicholas";
} let person = new Object();
setName(person);
console.log(person.name); // "Nicholas"

这一次,我们创建了一个对象并把它保存在变量 person 中。然后,这个对象被传给 setName()方法,并被复制到参数 obj 中。在函数内部,obj 和 person 都指向同一个对象。结果就是,即使对象是按值传进函数的,obj 也会通过引用访问对象。当函数内部给 obj 设置了 name 属性时,函数外部的对象也会反映这个变化,因为 obj 指向的对象保存在全局作用域的堆内存上。

注意 ECMAScript 中函数的参数就是局部变量。

1.4 确定类型

上方全部矫正

待补充 112

2 EXECUTION CONTEXT AND SCOPE

The concept of execution context, referred to as context for simplicity, is of the utmost importance in JavaScript. The execution context of a variable or function defines what other data it has access to, as well as how it should behave. Each execution context has an associated variable object upon which all of its defined variables and functions exist. This object is not accessible by code but is used behind the scenes to handle data.

The global execution context is the outermost one. Depending on the host environment for an ECMAScript implementation, the object representing this context may differ. In web browsers, the
global context is said to be that of the window object (discussed in the Browser Object Model chapter), so all global variables and functions defined with var are created as properties and methods on the window object. Declarations using let and const at the top level are not defined in the global context, but they are resolved identically on the scope chain. When an execution context has executed all of its code, it is destroyed, taking with it all of the variables and functions defined within it (the global context isn’t destroyed until the application exits, such as when a web page is closed or a web browser is shut down).

Each function call has its own execution context. Whenever code execution flows into a function, the function’s context is pushed onto a context stack. After the function has finished executing, the stack is popped, returning control to the previously executing context. This facility controls execution flow throughout an ECMAScript program.

When code is executed in a context, a scope chain of variable objects is created. The purpose of the scope chain is to provide ordered access to all variables and functions that an execution context has access to. The front of the scope chain is always the variable object of the context whose code is executing. If the context is a function, then the activation object is used as the variable object. An activation object starts with a single defined variable called arguments. (This doesn’t exist for the global context.) The next variable object in the chain is from the containing context, and the next after that is from the next containing context. This pattern continues until the global context is reached; the global context’s variable object is always the last of the scope chain.

Identifiers are resolved by navigating the scope chain in search of the identifier name. The search always begins at the front of the chain and proceeds to the back until the identifier is found. (If the identifier isn’t found, typically an error occurs.)

Consider the following code:

var color = "blue"; function changeColor() { if (color === "blue") { color = "red"; } else { color = "blue"; }
} changeColor();

In this simple example, the function changeColor() has a scope chain with two objects in it: its own variable object (upon which the arguments object is defined) and the global context’s variable object. The variable color is therefore accessible inside the function, because it can be found in the
scope chain.

Additionally, locally defined variables can be used interchangeably with global variables in a local context. Here’s an example:

var color = "blue"; function changeColor() { let anotherColor = "red";function swapColors() { let tempColor = anotherColor; anotherColor = color; color = tempColor; // color, anotherColor, and tempColor are all accessible here}// color and anotherColor are accessible here, but not tempColor swapColors();
} // only color is accessible here
changeColor();

There are three execution contexts in this code: global context, the local context of changeColor(), and the local context of swapColors(). The global context has one variable, color, and one function, changeColor(). The local context of changeColor() has one variable named anotherColor and one function named swapColors(), but it can also access the variable color from the global context. The local context of swapColors() has one variable, named tempColor, that is accessible only within that context. Neither the global context nor the local context of swapColors() has access to tempColor. Within swapColors(), though, the variables of the other two contexts are fully
accessible because they are parent execution contexts. Figure 4-3 represents the scope chain for the previous example.

In this figure, the rectangles represent specific execution contexts. An inner context can access everything from all outer contexts through the scope chain, but the outer contexts cannot access anything within
an inner context. The connection between the contexts is linear and
ordered. Each context can search up the scope chain for variables and
functions, but no context can search down the scope chain into another
execution context. There are three objects in the scope chain for the
local context of swapColors(): the swapColors() variable object, the
variable object from changeColor(), and the global variable object.
The local context of swapColors() begins its search for variable and
function names in its own variable object before moving along the
chain. The scope chain for the changeColor() context has only two
objects: its own variable object and the global variable object. This
means that it cannot access the context of swapColors().

NOTE Function arguments are considered to be variables and follow the same access rules as any other variable in the execution context.

2.1 Scope Chain Augmentation

Even though there are only two primary types of execution contexts, global and function (the third
exists inside of a call to eval()), there are other ways to augment the scope chain. Certain statements
cause a temporary addition to the front of the scope chain that is later removed after code execution.
There are two times when this occurs, specifically when execution enters either of the following:

  • The catch block in a try-catch statement
  • A with statement

Both of these statements add a variable object to the front of the scope chain. For the with statement, the specified object is added to the scope chain; for the catch statement, a new variable object is created and contains a declaration for the thrown error object. Consider the following:

function buildUrl() { let qs = "?debug=true"; with(location){ let url = href + qs; } return url;
}

In this example, the with statement is acting on the location object, so location itself is added to the front of the scope chain. There is one variable, qs, defined in the buildUrl() function. When
the variable href is referenced, it’s actually referring to location.href, which is in its own variable
object. When the variable qs is referenced, it’s referring to the variable defined in buildUrl(), which
is in the function context’s variable object. Inside the with statement is a variable declaration for url,
which becomes part of the function’s context and can, therefore, be returned as the function value.

NOTE There is a deviation in the Internet Explorer implementation of
JavaScript through Internet Explorer 8, where the error caught in a catch statement is added to the execution context’s variable object rather than the catch
statement’s variable object, making it accessible even outside the catchblock.
This was fixed in Internet Explorer 9.

2.2 Variable Declaration

With the introduction of ES6, JavaScript underwent a jostling transformation with regard to how
variables are declared in the language. Through ECMAScript 5.1, the one-size-fits-all keyword was
var. With ES6, not only does it introduce the two new keywords let and const, but these new keywords will overwhelmingly be the preferred declarations over var.

2.2.1 Function Scope Declaration Using var

When a variable is declared using var, it is automatically added to the most immediate context available. In a function, the most immediate one is the function’s local context; in a with statement, the
most immediate is the function context. If a variable is initialized without first being declared, it gets
added to the global context automatically, as in this example:

function add(num1, num2) { var sum = num1 + num2; return sum;
} let result = add(10, 20);    // 30
console.log(sum);           // causes an error: sum is not a valid variable

Here, the function add() defines a local variable named sum that contains the result of an addition
operation. This value is returned as the function value, but the variable sum isn’t accessible outside the
function. If the var keyword is omitted from this example, sum becomes accessible after add() has
been called, as shown here:

function add(num1, num2) { sum = num1 + num2; return sum;
} let result = add(10, 20);    // 30
console.log(sum);           // 30

Here, the variable sum is initialized to a value without ever having been declared using var. When
add() is called, sum is created in the global context and continues to exist even after the function has
completed, allowing you to access it later.

NOTE Initializing variables without declaring them is a very common mistake in JavaScript programming and can lead to errors. It’s advisable to always
declare variables before initializing them to avoid such issues. In strict mode, initializing variables without declaration causes an error.

A var declaration will be brought to the top of the function or global scope and before any existing
code inside it. This is referred to as “hoisting”. This allows you to safely use a hoisted variable anywhere in the same scope without consideration for whether or not it was declared yet. However, in
practice, this can lead to legal yet bizarre code in which a variable is used before it is declared. Here is
an example of two equivalent code snippets in the global scope:

var name = "Jake"; // This is equivalent to:name = 'Jake';
var name;

You can prove to yourself that a variable is hoisted by inspecting it before its declaration. The hoisting of the declaration means you will see undefined instead of ReferenceError:

console.log(name); // undefined
var name = 'Jake'; function() {console.log(name); // undefinedvar name = 'Jake';
}
2.2.2 使用let的块级作用域声明

ES6 新增的 let 关键字跟 var 很相似,但它的作用域是块级的,这也是 JavaScript 中的新概念。块级作用域由最近的一对包含花括号{}界定。换句话说,if 块、while 块、function 块,甚至连单独的块也是 let 声明变量的作用域。

if (true) { let a;
}
console.log(a);     // ReferenceError: a 没有定义function foo() { let c;
}
console.log(c);     // ReferenceError: c 没有定义{ let d;
}
console.log(d); // ReferenceError: d 没有定义

let 与 var 的另一个不同之处是在同一作用域内不能声明两次。重复的 var 声明会被忽略,而重复的 let 声明会抛出 SyntaxError。

var a;
var a;
// 不会报错{ let b; let b;
}
// SyntaxError: 标识符 b 已经声明过了

let 的行为非常适合在循环中声明迭代变量。使用 var 声明的迭代变量会泄漏到循环外部,这种情况应该避免。来看下面两个例子:

for (var i = 0; i < 10; ++i) {}
console.log(i); // 10 for (let j = 0; j < 10; ++j) {}
console.log(j); // ReferenceError: j 没有定义

严格来讲,let 在 JavaScript 运行时中也会被提升,但由于“暂时性死区”(temporal dead zone)的缘故,实际上不能在声明之前使用 let 变量。因此,从写 JavaScript 代码的角度说,let 的提升跟 var是不一样的。

2.2.3 使用const的常量声明

除了 let,ES6 同时还增加了 const 关键字。使用 const 声明的变量必须同时初始化为某个值。一经声明,在其生命周期的任何时候都不能再重新赋予新值。

const a; // SyntaxError: 常量声明时没有初始化const b = 3;
console.log(b); // 3
b = 4; // TypeError: 给常量赋值

const 除了要遵循以上规则,其他方面与 let 声明是一样的:

if (true) { const a = 0;
}
console.log(a); // ReferenceError: a 没有定义function foo() { const c = 2;
}
console.log(c); // ReferenceError: c 没有定义{ const d = 3;
}
console.log(d); // ReferenceError: d 没有定义

const修饰对象时,这个对象不能再被重新赋值为其他引用值,但对象的键则不受限制。

const o1 = {};
o1 = {};               // TypeError: 给常量赋值const o2 = {};
o2.name = 'Jake';
console.log(o2.name);   // 'Jake'

如果想让整个对象都不能修改,可以使用 Object.freeze(),这样再给属性赋值时虽然不会报错,但会静默失败:

const o3 = Object.freeze({});
o3.name = 'Jake';
console.log(o3.name); // undefined

由于 const 声明暗示变量的值是单一类型且不可修改,JavaScript 运行时编译器可以将其所有实例都替换成实际的值,而不会通过查询表进行变量查找。谷歌的 V8 引擎就执行这种优化。

注意 开发实践表明,如果开发流程并不会因此而受很大影响,就应该尽可能地多使用const 声明,除非确实需要一个将来会重新赋值的变量。这样可以从根本上保证提前发现重新赋值导致的 bug。

2.2.4 标识符查找

20220926
待补充 原型链是啥
当在特定上下文中为读取或写入而引用一个标识符时,必须通过搜索确定这个标识符表示什么。搜索开始于作用域链前端,以给定的名称搜索对应的标识符。如果在局部上下文中找到该标识符,则搜索停止,变量确定;如果没有找到变量名,则继续沿作用域链搜索。(注意,作用域链中的对象也有一个原型链,因此搜索可能涉及每个对象的原型链。)这个过程一直持续到搜索至全局上下文的变量对象。如果仍然没有找到标识符,则说明其未声明。

为更好地说明标识符查找,我们来看一个例子:

var color = 'blue'; function getColor() { return color;
} console.log(getColor()); // 'blue'

在这个例子中,调用函数 getColor()时会引用变量 color。为确定 color 的值会进行两步搜索。第一步,搜索 getColor()的变量对象,查找名为 color 的标识符。结果没找到,于是继续搜索下一个变量对象(来自全局上下文),然后就找到了名为 color 的标识符。因为全局变量对象上有 color的定义,所以搜索结束。

对这个搜索过程而言,引用局部变量会让搜索自动停止,而不继续搜索下一级变量对象。也就是说,如果局部上下文中有一个同名的标识符,那就不能在该上下文中引用父上下文中的同名标识符,如下面的例子所示:

var color = 'blue'; function getColor() { let color = 'red'; return color;
} console.log(getColor()); // 'red'

使用块级作用域声明并不会改变搜索流程,但可以给词法层级添加额外的层次:

var color = 'blue'; function getColor() { let color = 'red'; { let color = 'green'; return color; }
} console.log(getColor()); // 'green'

在这个修改后的例子中,getColor()内部声明了一个名为 color 的局部变量。在调用这个函数时,变量会被声明。在执行到函数返回语句时,代码引用了变量 color。于是开始在局部上下文中搜索这个标识符,结果找到了值为’green’的变量 color。因为变量已找到,搜索随即停止,所以就使用这个局部变量。这意味着函数会返回’green’。在局部变量 color 声明之后的任何代码都无法访问全局变量color,除非使用完全限定的写法 window.color。

注意 标识符查找并非没有代价。访问局部变量比访问全局变量要快,因为不用切换作用域。不过,JavaScript 引擎在优化标识符查找上做了很多工作,将来这个差异可能就微不足道了。

3 垃圾回收

JavaScript 是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。在 C 和 C++等语言中,跟踪内存使用对开发者来说是个很大的负担,也是很多问题的来源。JavaScript 为开发者卸下了这个负担,通过自动内存管理实现内存分配和闲置资源回收。基本思路很简单:确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动运行。垃圾回收过程是一个近似且不完美的方案,因为某块内存是否还有用,属于“不可判定的”问题,意味着靠算法是解决不了的。

我们以函数中局部变量的正常生命周期为例。函数中的局部变量会在函数执行时存在。此时,栈(或堆)内存会分配空间以保存相应的值。函数在内部使用了变量,然后退出。此时,就不再需要那个局部变量了,它占用的内存可以释放,供后面使用。这种情况下显然不再需要局部变量了,但并不是所有时候都会这么明显。垃圾回收程序必须跟踪记录哪个变量还会使用,以及哪个变量不会再使用,以便回收内存。如何标记未使用的变量也许有不同的实现方式。不过,在浏览器的发展史上,用到过两种主要的标记策略:标记清理和引用计数。

3.1 标记清理

JavaScript 最常用的垃圾回收策略是标记清理(mark-and-sweep)。当变量进入上下文,比如在函数内部声明一个变量时,这个变量会被加上存在于上下文中的标记。而在上下文中的变量,逻辑上讲,永远不应该释放它们的内存,因为只要上下文中的代码在运行,就有可能用到它们。当变量离开上下文时,也会被加上离开上下文的标记。

给变量加标记的方式有很多种。比如,当变量进入上下文时,反转某一位;或者可以维护“在上下文中”和“不在上下文中”两个变量列表,可以把变量从一个列表转移到另一个列表。标记过程的实现并不重要,关键是策略。

垃圾回收程序运行的时候,会标记内存中存储的所有变量(记住,标记方法有很多种)。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。

到了 2008 年,IE、Firefox、Opera、Chrome 和 Safari 都在自己的 JavaScript 实现中采用标记清理(或其变体),只是在运行垃圾回收的频率上有所差异。

3.2 引用计数

另一种没那么常用的垃圾回收策略是引用计数(reference counting)。其思路是对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为 1。如果同一个值又被赋给另一个变量,那么引用数加 1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。当一个值的引用数为 0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序下次运行的时候就会释放引用数为 0 的值的内存。

引用计数最早由 Netscape Navigator 3.0 采用,但很快就遇到了严重的问题:循环引用。所谓循环引用,就是对象 A 有一个指针指向对象 B,而对象 B 也引用了对象 A。比如:

function problem() { let objectA = new Object(); let objectB = new Object();objectA.someOtherObject = objectB; objectB.anotherObject = objectA; }

在这个例子中,objectA 和 objectB 通过各自的属性相互引用,意味着它们的引用数都是 2。在标记清理策略下,这不是问题,因为在函数结束后,这两个对象都不在作用域中。而在引用计数策略下,objectA 和 objectB 在函数结束后还会存在,因为它们的引用数永远不会变成 0。如果函数被多次调用,则会导致大量内存永远不会被释放。为此,Netscape 在 4.0 版放弃了引用计数,转而采用标记清理。事实上,引用计数策略的问题还不止于此。

在 IE8 及更早版本的 IE 中,并非所有对象都是原生 JavaScript 对象。BOM 和 DOM 中的对象是 C++实现的组件对象模型(COM,Component Object Model)对象,而 COM 对象使用引用计数实现垃圾回收。因此,即使这些版本 IE 的 JavaScript 引擎使用标记清理,JavaScript 存取的 COM 对象依旧使用引用计数。换句话说,只要涉及 COM 对象,就无法避开循环引用问题。下面这个简单的例子展示了涉及 COM对象的循环引用问题:

let element = document.getElementById("some_element");
let myObject = new Object();
myObject.element = element;
element.someObject = myObject;

这个例子在一个 DOM 对象(element)和一个原生 JavaScript 对象(myObject)之间制造了循环引用。myObject 变量有一个名为 element 的属性指向 DOM 对象 element,而 element 对象有一个someObject 属性指回 myObject 对象。由于存在循环引用,因此 DOM 元素的内存永远不会被回收,即使它已经被从页面上删除了也是如此。

为避免类似的循环引用问题,应该在确保不使用的情况下切断原生 JavaScript 对象与 DOM 元素之间的连接。比如,通过以下代码可以清除前面的例子中建立的循环引用:

myObject.element = null;
element.someObject = null;

把变量设置为 null 实际上会切断变量与其之前引用值之间的关系。当下次垃圾回收程序运行时,这些值就会被删除,内存也会被回收。

为了补救这一点,IE9 把 BOM 和 DOM 对象都改成了 JavaScript 对象,这同时也避免了由于存在两套垃圾回收算法而导致的问题,还消除了常见的内存泄漏现象。

3.3 性能

垃圾回收程序会周期性运行,如果内存中分配了很多变量,则可能造成性能损失,因此垃圾回收的时间调度很重要。尤其是在内存有限的移动设备上,垃圾回收有可能会明显拖慢渲染的速度和帧速率。开发者不知道什么时候运行时会收集垃圾,因此最好的办法是在写代码时就要做到:无论什么时候开始收集垃圾,都能让它尽快结束工作。

现代垃圾回收程序会基于对 JavaScript 运行时环境的探测来决定何时运行。探测机制因引擎而异,但基本上都是根据已分配对象的大小和数量来判断的。比如,根据 V8 团队 2016 年的一篇博文的说法:“在一次完整的垃圾回收之后,V8 的堆增长策略会根据活跃对象的数量外加一些余量来确定何时再次垃圾回收。”

由于调度垃圾回收程序方面的问题会导致性能下降,IE 曾饱受诟病。它的策略是根据分配数,比如分配了 256 个变量、4096 个对象/数组字面量和数组槽位(slot),或者 64KB 字符串。只要满足其中某个条件,垃圾回收程序就会运行。这样实现的问题在于,分配那么多变量的脚本,很可能在其整个生命周期内始终需要那么多变量,结果就会导致垃圾回收程序过于频繁地运行。由于对性能的严重影响,IE7
最终更新了垃圾回收程序。

IE7 发布后,JavaScript 引擎的垃圾回收程序被调优为动态改变分配变量、字面量或数组槽位等会触发垃圾回收的阈值。IE7 的起始阈值都与 IE6 的相同。如果垃圾回收程序回收的内存不到已分配的 15%,这些变量、字面量或数组槽位的阈值就会翻倍。如果有一次回收的内存达到已分配的 85%,则阈值重置为默认值。这么一个简单的修改,极大地提升了重度依赖 JavaScript 的网页在浏览器中的性能。

3.4 内存管理

在使用垃圾回收的编程环境中,开发者通常无须关心内存管理。不过,JavaScript 运行在一个内存管理与垃圾回收都很特殊的环境。分配给浏览器的内存通常比分配给桌面软件的要少很多,分配给移动浏览器的就更少了。这更多出于安全考虑而不是别的,就是为了避免运行大量 JavaScript 的网页耗尽系统内存而导致操作系统崩溃。这个内存限制不仅影响变量分配,也影响调用栈以及能够同时在一个线程中执行的语句数量。

将内存占用量保持在一个较小的值可以让页面性能更好。优化内存占用的最佳手段就是保证在执行代码时只保存必要的数据。如果数据不再必要,那么把它设置为 null,从而释放其引用。这也可以叫作解除引用。这个建议最适合全局变量和全局对象的属性。局部变量在超出作用域后会被自动解除引用,如下面的例子所示:

function createPerson(name){ let localPerson = new Object(); localPerson.name = name; return localPerson;
} let globalPerson = createPerson("Nicholas");// 解除 globalPerson 对值的引用
globalPerson = null;

在上面的代码中,变量 globalPerson 保存着 createPerson()函数调用返回的值。在 createPerson()内部,localPerson 创建了一个对象并给它添加了一个 name 属性。然后,localPerson 作为函数值被返回,并被赋值给 globalPerson。localPerson 在 createPerson()执行完成超出上下文后会自动被解除引用,不需要显式处理。但 globalPerson 是一个全局变量,应该在不再需要时手动解除其引用,最后一行就是这么做的。

不过要注意,解除对一个值的引用并不会自动导致相关内存被回收。解除引用的关键在于确保相关的值已经不在上下文里了,因此它在下次垃圾回收时会被回收。

3.4.1 通过 const 和 let 声明提升性能

ES6 增加这两个关键字不仅有助于改善代码风格,而且同样有助于改进垃圾回收的过程。因为 const let 都以块(而非函数)为作用域,所以相比于使用 var,使用这两个新关键字可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存。在块作用域比函数作用域更早终止的情况下,这就有可能发生。

3.4.2 隐藏类和删除操作

待补充 123

3.4.3 内存泄漏

写得不好的 JavaScript 可能出现难以察觉且有害的内存泄漏问题。在内存有限的设备上,或者在函数会被调用很多次的情况下,内存泄漏可能是个大问题。JavaScript 中的内存泄漏大部分是由不合理的引用导致的。

意外声明全局变量是最常见但也最容易修复的内存泄漏问题。下面的代码没有使用任何关键字声明变量:

function setName() { name = 'Jake';
}

此时,解释器会把变量 name 当作 window 的属性来创建(相当于 window.name = ‘Jake’)。可想而知,在 window 对象上创建的属性,只要 window 本身不被清理就不会消失。这个问题很容易解决,只要在变量声明前头加上 var、let 或 const 关键字即可,这样变量就会在函数执行完毕后离开作用域。

定时器也可能会悄悄地导致内存泄漏。下面的代码中,定时器的回调通过闭包引用了外部变量:

let name = 'Jake';
setInterval(() => { console.log(name);
}, 100);

待补充 定时器

只要定时器一直运行,回调函数中引用的 name 就会一直占用内存。垃圾回收程序当然知道这一点,因而就不会清理外部变量。

待补充 124

4 变量、作用域与内存相关推荐

  1. JavaScript高级编程设计(第三版)——第四章:变量作用域和内存问题

    系列文章目录 第二章:在html中使用javaScript 第三章:基本概念 第四章:变量作用域和内存问题 第五章:引用类型 目录 系列文章目录 前言 一.基本数据类型和引用类型的值? 1.数据类型 ...

  2. JavaScript变量作用域和内存问题(js高级程序设计总结)

    1,变量 ECMAScript和JavaScript是等同的吗?个人认为是否定的.我的理解是这样的,ECMAScript是一套完整的标准或者说协议,而JavaScript是在浏览器上实现的一套脚本语言 ...

  3. 变量,作用域,和内存问题

    js 变量是松散类型的本质,决定了它只是在特定时间内用于保存特定值的一个名称而已. 基本类型 栈内存中的简单数据段 5种 underfined Null Boolean Number String.固 ...

  4. 读书笔记 - js高级程序设计 - 第四章 变量 作用域 和 内存问题

    5种基本数据类型 可以直接对值操作 判断引用类型 var result = instanceof Array 执行环境 每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对 ...

  5. js基础之--变量 作用域和内存问题

    基本类型:Undefind Null Boolean Number String 引用类型: 对象 在操作对象时,实际上实在操作对象的引用而不是实际的对象.为此,引用类型的值是按引用访问的. 从一个变 ...

  6. 精读《javascript高级程序设计》笔记二——变量、作用域、内存以及引用类型

    变量.作用域和内存问题 执行环境共有两种类型--全局和局部 作用域链会加长,有两种情况:try-catch语句的catch块,with语句. javascript没有块级作用域,即在if,for循环中 ...

  7. 《JavaScript高级程序设计(第四版)》红宝书学习笔记(2)(第四章:变量、作用域与内存)

    个人对第四版红宝书的学习笔记.不适合小白阅读.这是part2.持续更新,其他章节笔记看我主页. (记 * 的表示是ES6新增的知识点,记 ` 表示包含新知识点) 第四章:变量.作用域与内存 4.1 原 ...

  8. JavaScript学习笔记 - 变量、作用域与内存问题

    本文记录了我在学习前端上的笔记,方便以后的复习和巩固. 4.1基本类型和引用类型的值 ECMAScript变量可能包含两种不同数据类型的值:基本类型值和引用类型值.基本类型指的是简单的数据段,而引用类 ...

  9. JS学习笔记(二)变量、作用域及内存问题

    一.基本类型和引用类型的值 变量可能包含两种不同数据类型的值:基本类型值和引用类型值. 基本类型值:简单的数据段. 引用类型值:可能由多个值构成的对象. 当将一个值赋给变量时,解析器必须确定这个值是基 ...

  10. 变量、作用域和内存问题

    1.基本类型与引用类型的值 ECMAScript变量包含两种不同数据类型的值:基本类型值和引用类型值.基本类型值是简单的数据段,而引用类型值指那些可能由多个值构成的对象. 在将一个值赋给变量时,解析器 ...

最新文章

  1. 中安消布局东三省智慧城市市场
  2. python按概率输出分类结果_sklearn例程:多分类输出概率
  3. 2018年11月份GitHub上最热门的开源项目
  4. PHP Smarty变量调节器
  5. 前端学习(1485):restful接口规则
  6. java 中的this
  7. java2048设计说明,Html5中的本地存储设计理念
  8. watershed用法详解
  9. 以人为尊真我生活,Leave the world behind
  10. 从棋盘左上角到右下角共有多少种走法
  11. HashTable Dictionary HashMap
  12. 因社会不公大学生找不到工作
  13. 不用花钱,不用公网ip,用zerotier直接外网高速访问群晖,理论黑群晖白群晖都可以
  14. 服务器 系统 版本查询
  15. 蓝桥杯练习题——数列求和
  16. 软考非计算机专业考难吗,非计算机专业考软考初级哪个更容易过
  17. 请问mysql数据类型是否区分大小写?
  18. 加入爱赏商圈 享专属优惠特权
  19. Python pta题目
  20. uni-app简单应用和页面跳转

热门文章

  1. 防电脑辐射~!10招
  2. [Python3]数独计算器
  3. Kubespray安装kubernetes
  4. hdu 1686 Oulipo(kmp)
  5. php artisan 常用命令,php artisan module常用命令
  6. linux用飞信发短信
  7. OSI与TCP/IP各层的结构与功能
  8. 热动力数据MATLAB代码分享
  9. 冷知识点:COLLATE 关键字是什么意思?
  10. 一些可以参考的文档集合7