一、什么是JavaScript
1、DOM
文档对象模型(Document Object Model)是一个应用编程接口(API),用于在HTML中使用扩展的XML,DOM 将整个页面抽象为一组分层节点。HTML 或 XML 页面的每个组成部分都是一种节点,包含不同的数据。比如下面的 HTML 页面:
<html>
<head>
<title>Sample Page</title>
</head>
<body>
<p> Hello World!</p>
</body>
</html>
这些代码通过 DOM 可以表示为一组分层节点,如图 1-2 所示。
DOM 通过创建表示文档的树,让开发者可以随心所欲地控制网页的内容和结构。使用 DOM API,
可以轻松地删除、添加、替换、修改节点。
2、BOM
浏览器对象模型(Browser Object Model)API。提供与浏览器交互的方法和接口。
- 弹出新浏览器窗口的能力;
- 移动、缩放和关闭浏览器窗口的能力;
- navigator 对象,提供关于浏览器的详尽信息;
- location 对象,提供浏览器加载页面的详尽信息;
- screen 对象,提供关于用户屏幕分辨率的详尽信息;
- performance 对象,提供浏览器内存占用、导航行为和时间统计的详尽信息
二、HTML中的JavaScript
1、<script>元素
常用属性:
- async 表示应该立即开始下载脚本,但不能阻止其他页面动作,比如下载资源或等待其他脚本加载。只对外部脚本文件有效。(并不保证能按照它们出现的次序执行)
- defer 表示应该立即开始下载脚本,但延迟执行。
- crossorigin 配置相关请求的CORS(跨源资源共享)设置。默认不使用CORS
- src 表示包含要执行的代码的外部文件
- type 代替 language ,表示代码块中脚本语言的内容类型,按照惯例,这个值始终都是“text/javascript”,如果这个值是module,则代码会被当成ES6模块,而且只有这时候代码中才能出现import和export关键字
在使用行内 JavaScript 代码时,要注意代码中不能出现字符串 ,想避免这个问题,只需要转义字符“\”即可。
三、语言基础
1、语法
标识符:使用驼峰形式,关键字、保留字、true、false、null不可作为标识符
语句以分号结尾
关键字:
break do in typeof
case else instanceof var
catch export new void
class extends return while
const finally super with
continue for switch yield
debugger function this
default if throw
delete import try
这些词汇不能用作标识符。
2、变量
2.1 var关键字
作用域:函数作用域
if (true) {
var name = 'Matt';
console.log(name); // Matt
}
console.log(name); // Matt
2.2 let声明
作用域:块作用域,块作用域是函数作用域的子集,因此适用于 var 的作用域限制同样也适用于 let。
if (true) {
let age = 26;
console.log(age); // 26
}
console.log(age); // ReferenceError: age 没有定义
let 也不允许同一个块作用域中出现冗余声明
var name;
var name;
let age;
let age; //SyntaxError;标识符 age 已经声明过了
let声明的变量不会在作用域中被提升。
// name 会被提升
console.log(name); // undefined
var name = 'Matt';
// age 不会被提升
console.log(age); // ReferenceError:age 没有定义
let age = 26;
使用let在全局作用域声明的变量不会成为window对象的属性。
var name = 'Matt';
console.log(window.name); // 'Matt'
let age = 26;
console.log(window.age); // undefined
for循环迭代变量使用var会渗透到循环体外部,let则不会
for (var i = 0; i < 5; ++i) {
// 循环逻辑
}
console.log(i); // 5
for (let i = 0; i < 5; ++i) {
// 循环逻辑
}
console.log(i); // ReferenceError: i 没有定义
2.3 const声明
const行为与let基本相同,唯一一个重要的区别是用const声明的变量必须同时初始化变量。且变量不可被修改。
const age = 26;
age = 36; //TypeError: 给常量赋值
// const 声明的作用域也是块
const name = 'Matt';
if (true) {
const name = 'Nicholas';
}
console.log(name); // Matt
2.4 声明风格及最佳实践
- 不使用var
- const优先,let次之
3、数据类型
3.1 typeof操作符
返回变量的数据类型
- “undefined” 表示值未定义;
- “boolean” 表示值为布尔值;
- “string” 表示值为字符串;
- “number” 表示值为数值;
- “object” 表示值为对象(而不是函数)或 null ;
- “function” 表示值为函数;
- “symbol” 表示值为符号。
let message = "some string";
console.log(typeof message); // "string"
console.log(typeof(message)); // "string"
console.log(typeof 95); // "number"
console.log(typeof null); // "object"
3.2 Undefined类型
就是一个特殊值:Undefined,当使用var或let声明了变量但没有初始化时,就相当于给变量赋了Undefined值。
let message;
console.log(message == undefined); // true
3.3 Null类型
Null类型同样只有一个值:null,从逻辑上讲,null值表示一个空对象指针。
let car = null;
console.log(typeof car); // "object"
undefined 值是由 null 值派生而来的,因此 ECMA-262 将它们定义为表面上相等。
console.log(null == undefined); // true
3.4 Boolean类型
要将一个其他类型的值转换为布尔值,可以调用特定的 Boolean() 转型函数:
let message = "Hello world!";
let messageAsBoolean = Boolean(message);
if 等流控制语句会自动执行其他类型值到布尔值的转换:
let message = "Hello world!";
if (message) {
console.log("Value is true");
}
3.5 Number类型
let floatNum = 3.125e7; // 等于 31250000
有一个特殊的数值叫 NaN ,意思是“不是数值”,用 0 除任意数值在其他语言中通常都会导致错误,从而中止代码执行.
console.log(0/0); // NaN
console.log(-0/+0); // NaN
如果分子是非 0 值,分母是有符号 0 或无符号 0,则会返回 Infinity 或 -Infinity :
console.log(5/0); // Infinity
console.log(5/-0); // -Infinity
数值转换:
- Number()
- 布尔值, true 转换为 1, false 转换为 0。
- 数值,直接返回。
- null ,返回 0。
- undefined ,返回 NaN 。
- 字符串,如果包含数值,则转换为一个十进制数值。
- 对象,调用 valueOf() 方法,并按照上述规则转换返回的值。如果转换结果是 NaN ,则调用toString() 方法,再按照转换字符串的规则转换。
let num1 = Number("Hello world!"); // NaN
let num2 = Number(""); // 0
let num3 = Number("000011"); // 11
let num4 = Number(true); // 1
可以看到,字符串 “Hello world” 转换之后是 NaN ,因为它找不到对应的数值。空字符串转换后是 0。字符串 000011 转换后是 11,因为前面的零被忽略了。最后, true 转换为 1。
let num1 = parseInt("1234blue"); // 1234
let num2 = parseInt(""); // NaN
let num3 = parseInt("0xA"); // 10,解释为十六进制整数
let num4 = parseInt(22.5); // 22
let num5 = parseInt("70"); // 70,解释为十进制值
let num6 = parseInt("0xf"); // 15,解释为十六进制整数
let num1 = parseFloat("1234blue"); // 1234,按整数解析
let num2 = parseFloat("0xA"); // 0
let num3 = parseFloat("22.5"); // 22.5
let num4 = parseFloat("22.34.5"); // 22.34
let num5 = parseFloat("0908.5"); // 908.5
let num6 = parseFloat("3.125e7"); // 31250000
3.6 String类型
字面量 | 含义 |
---|---|
\n | 换行 |
\t | 制表 |
\b | 退格 |
\r | 回车 |
\f | 换页 |
\\ | 反斜杠(\) |
\’ | 单引号(') |
\" | 双引号(") |
\` | 反引号(`) |
\xnn | 以十六进制编码 nn 表示的字符(其中 n 是十六进制数字 0~F),例如 \x41 等于 “A” |
\unnnn | 以十六进制编码 nnnn 表示的 Unicode 字符(其中 n 是十六进制数字 0~F),例如 \u03a3 等于希腊字符 “Σ” |
toString()
把一个值转换为字符串
let age = 11;
let ageAsString = age.toString(); // 字符串"11"
let found = true;
let foundAsString = found.toString(); // 字符串"true"
模板字面量
模板字面量保留换行字符,可以跨行定义字符串。
模板字面量在定义模板时特别有用,比如下面这个 HTML 模板:
let pageHTML = `
<div>
<a href="#">
<span>Jake</span>
</a>
</div>`;
字符串插值
字符串插值通过在 ${}
中使用一个 JavaScript 表达式实现:
let value = 5;
let exponent = 'second';
// 以前,字符串插值是这样实现的:
let interpolatedString =
value + ' to the ' + exponent + ' power is ' + (value * value);
// 现在,可以用模板字面量这样实现:
let interpolatedTemplateLiteral =
`${ value } to the ${ exponent } power is ${ value * value }`;
console.log(interpolatedString); // 5 to the second power is 25
console.log(interpolatedTemplateLiteral); // 5 to the second power is 25
将表达式转换为字符串时会调用 toString() .
3.7 Symbol类型
Symbol(符号)是ES6新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。
3.8 Object类型
4、操作符
4.1 一元操作符
- ++a和a++的区别
++a:先加再赋值,等价于:a = a+1
let num1 = 2;
let num2 = 20;
let num3 = ++num1 + num2;
let num4 = num1 + num2;
console.log(num3); // 23
console.log(num4); // 23
a++:先赋值再加,等价于:a = a;b = a + 1
let num1 = 2;
let num2 = 20;
let num3 = num1++ + num2;
let num4 = num1 + num2;
console.log(num3); // 22
console.log(num4); // 23
4.2 位操作符
- 按位非 ~
let num1 = 25; // 二进制 00000000000000000000000000011001
let num2 = ~num1; // 二进制 11111111111111111111111111100110
console.log(num2); // -26
- 按位与 &
let result = 25 & 3;
console.log(result); // 1 规则:有0为0,全1为1
//25 = 0000 0000 0000 0000 0000 0000 0001 1001
// 3 = 0000 0000 0000 0000 0000 0000 0000 0011
//---------------------------------------------
//AND= 0000 0000 0000 0000 0000 0000 0000 0001
- 按位或 |
let result = 25 | 3;
console.log(result); // 27 规则:有1为1,全0为0
//25 = 0000 0000 0000 0000 0000 0000 0001 1001
// 3 = 0000 0000 0000 0000 0000 0000 0000 0011
//---------------------------------------------
//OR = 0000 0000 0000 0000 0000 0000 0001 1011
- 按位异或 ^
let result = 25 ^ 3;
console.log(result); // 26 规则:相同为0,不同为1
//25 = 0000 0000 0000 0000 0000 0000 0001 1001
// 3 = 0000 0000 0000 0000 0000 0000 0000 0011
//---------------------------------------------
//XOR= 0000 0000 0000 0000 0000 0000 0001 1010
- 左移 <<
let oldValue = 2; // 等于二进制 10
let newValue = oldValue << 5; // 等于二进制 1000000,即十进制 64
注意:左移会保留它所操作数值的符号。比如,如果-2 左移 5 位,将得到-64,而不是正 64。
- 有符号右移 >>
let oldValue = 64; // 等于二进制 1000000
let newValue = oldValue >> 5; // 等于二进制 10,即十进制 2
- 无符号右移 >>>
let oldValue = 64; // 等于二进制 1000000
let newValue = oldValue >>> 5; // 等于二进制 10,即十进制 2
let oldValue = -64; // 等于二进制 11111111111111111111111111000000
let newValue = oldValue >>> 5; // 等于十进制 134217726
在对-64 无符号右移 5 位后,结果是 134 217 726。这是因为-64 的二进制表示是 11111111111111111111111111000000,无符号右移却将它当成正值,也就是 4 294 967 232。把这个值右移 5 位后,结果是00000111111111111111111111111110,即 134 217 726。
4.3 布尔操作符
- 逻辑非 !
- 逻辑与 &&
- 逻辑或 ||
4.4 乘性操作符
- 乘法操作符 *
- 除法操作符 /
- 取模(余数)操作符 %
4.5 指数操作符
ES7新增了指数操作符,Math.pow()
现在有了自己的操作符**
console.log(Math.pow(3, 2)); // 9
console.log(3 ** 2); // 9
console.log(Math.pow(16, 0.5)); // 4
console.log(16** 0.5); // 4
指数赋值操作符 **=
let squared = 3;
squared **= 2;
console.log(squared); // 9
let sqrt = 16;
sqrt **= 0.5;
console.log(sqrt); // 4
4.6 加性操作符
- 加法操作符 +
- 减法操作符 -
4.7 关系操作符
- 小于 <
- 大于 >
- 小于等于 <=
- 大于等于 >=
这里需要注意的就是:字符串比较时,会逐个比较每个字符的字符编码。
4.8 相等操作符
- 等于 == 和不等于 !=
这两个操作符都会先进行类型转换(通常称为强制类型转换)再确定操作数是否相等。
- 全等 === 和不全等 !==
全等和不全等操作符与相等和不相等操作符类似,只不过它们在比较相等时不转换操作数。
注意: 由于相等和不相等操作符存在类型转换问题,因此推荐使用全等和不全等操作符。这样有助于在代码中保持数据类型的完整性。
4.9 条件操作符
let max = (num1 > num2) ? num1 : num2;
4.10 赋值操作符
简单赋值用等于号( = )表示。
4.11 逗号操作符
逗号操作符可以用来在一条语句中执行多个操作,如下所示:let num1 = 1, num2 = 2, num3 = 3;
5、语句
5.1 if语句
if (i > 25) {
console.log("Greater than 25.");
} else if (i < 0) {
console.log("Less than 0.");
} else {
console.log("Between 0 and 25, inclusive.");
}
5.2 do-while语句
先执行循环体,再判断语句,因此,循环体内代码在退出前至少要执行一次。
let i = 0;
do {
i += 2;
} while (i < 10);
5.3 while语句
先判断语句,再执行循环体。
let i = 0;
while (i < 10) {
i += 2;
}
5.4 for语句
for 语句也是先判断语句,只不过增加了进入循环之前的初始化代码,以及循环执行后要执行的表达式。
let count = 10;
for (let i = 0; i < count; i++) {
console.log(i);
}
for (;;) { // 无穷循环
doSomething();
}
// 以下for循环等价while循环
let count = 10;
let i = 0;
for (; i < count; ) {
console.log(i);
i++;
}
5.5 for-in语句
for-in 语句是一种严格的迭代语句,用于枚举对象中的非符号键属性。
for (const propName in window) {
document.write(propName);
}
这个例子使用 for-in 循环显示了 BOM 对象 window 的所有属性。每次执行循环,都会给变量propName 赋予一个 window 对象的属性作为值,直到 window 的所有属性都被枚举一遍。与 for 循环一样,这里控制语句中的const 也不是必需的。但为了确保这个局部变量不被修改,推荐使用 const 。
5.6 for-of语句
for-of 语句是一种严格的迭代语句,用于遍历可迭代对象的元素。
for (const el of [2,4,6,8]) {
document.write(el);
}
for-of 循环会按照可迭代对象的 next() 方法产生值的顺序迭代元素。
5.7 标签语句
标签语句用于给语句加标签。
start: for (let i = 0; i < count; i++) {
console.log(i);
}
start 是一个标签,可以在后面通过 break 或 continue 语句引用。标签语句的典型应用场景是嵌套循环。
5.8 break 和 continue 语句
break 语句用于立即退出循环,强制执行循环后的下一条语句。
let num = 0;
for (let i = 1; i < 10; i++) {
if (i % 5 == 0) {
break;
}
num++;
}
console.log(num); // 4
continue 语句也用于立即退出循环,但会再次从循环顶部开始执行。
let num = 0;
for (let i = 1; i < 10; i++) {
if (i % 5 == 0) {
continue;
}
num++;
}
console.log(num); // 8
break 和 continue 都可以与标签语句一起使用,返回代码中特定的位置。这通常是在嵌套循环中,
let num = 0;
outermost:
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (i == 5 && j == 5) {
break outermost;
}
num++;
}
}
console.log(num); // 55
添加标签不仅让 break 退出(使用变量 j 的)内部循环,也会退出(使用变量 i 的)外部循环。
let num = 0;
outermost:
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (i == 5 && j == 5) {
continue outermost;
}
num++;
}
}
console.log(num); // 95
continue 语句会强制循环继续执行,但不是继续执行内部循环,而是继续执行外部循环。
5.9 with语句
使用 with 语句的主要场景是针对一个对象反复操作,这时候将代码作用域设置为该对象能提供便利。
let qs = location.search.substring(1);
let hostName = location.hostname;
let url = location.href;
上面代码中的每一行都用到了location对象,如果使用 with 语句,就可以少写一些代码:
with(location) {
let qs = search.substring(1);
let hostName = hostname;
let url = href;
}
警告: 由于 with 语句影响性能且难于调试其中的代码,通常不推荐在产品代码中使用 with 语句。
5.10 switch语句
注意: switch 语句在比较每个条件的值时会使用全等操作符,因此不会强制转换数据类型(比如,字符串 “10” 不等于数值 10)。
6、函数
函数使用 function 关键字声明,后跟一组参数,然后是函数体。
function sayHi(name, message) {
console.log("Hello " + name + ", " + message);
}
ECMAScript 中的函数不需要指定是否返回值。任何函数在任何时间都可以使用 return 语句来返回函数的值,用法是后跟要返回的值。比如:
function sum(num1, num2) {
return num1 + num2;
}
注意: 最佳实践是函数要么返回值,要么不返回值。只在某个条件下返回值的函数会带来麻烦,尤其是调试时。
四、变量、作用域与内存
1、原始值与引用值
原始值就是最简单的数据,引用值则是由多个值构成的对象。
保存原始值的变量是按值(by value)访问的;保存引用值的变量是按引用(by reference)访问的。
1.1 动态属性
对于引用值而言,可以随时添加、修改和删除其属性和方法。只有引用值可以动态添加后面可以使用的属性。
let person = new Object();
person.name = "Nicholas";
console.log(person.name); // "Nicholas"
1.2 复制值
把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。区别在于,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。操作完成后,两个变量实际上指向同一个对象。
let obj1 = new Object();
let obj2 = obj1;
obj1.name = "Nicholas";
console.log(obj2.name); // "Nicholas"
1.3 传递参数
ECMAScript 中所有函数的参数都是按值传递的。
function addTen(num) {
num += 10;
return num;
}
let count = 20;
let result = addTen(count);
console.log(count); // 20,没有变化 如果 num 是按引用传递的,那么 count 的值也会被修改为 30。
console.log(result); // 30
但是,如果变量中传递的是对象,就没那么清楚了,例如:
function setName(obj) {
obj.name = "Nicholas";
}
let person = new Object();
setName(person);
console.log(person.name); // "Nicholas"
这里很多人会误认为对象是按引用传递的,其实不是,即使对象是按值传进函数的, obj 也会通过引用访问对象。因为 obj 指向的对象保存在全局作用域的堆内存上。为证明对象是按值传递的,我们再来看看下面这个修改后的例子:
function setName(obj) {
obj.name = "Nicholas";
obj = new Object();
obj.name = "Greg";
}
let person = new Object();
setName(person);
console.log(person.name); // "Nicholas"
这个例子前后唯一的变化就是 setName() 中多了两行代码,将 obj 重新定义为一个有着不同 name的新对象。当 person 传入 setName() 时,其 name 属性被设置为 “Nicholas” 。然后变量 obj 被设置为一个新对象且 name 属性被设置为 “Greg” 。如果 person 是按引用传递的,那么 person 应该自动将指针改为指向 name 为 “Greg” 的对象。可是,当我们再次访问 person.name 时,它的值是 “Nicholas” ,这表明函数中参数的值改变之后,原始的引用仍然没变。当 obj 在函数内部被重写时,它变成了一个指向本地对象的指针。而那个本地对象在函数执行结束时就被销毁了。
1.4 确定类型
ECMAScript 提供了 instanceof
操作符。
console.log(person instanceof Object); // 变量 person 是 Object 吗?
console.log(colors instanceof Array); // 变量 colors 是 Array 吗?
console.log(pattern instanceof RegExp); // 变量 pattern 是 RegExp 吗?
如果变量是给定引用类型,则instanceof
操作符返回true
。
2、执行上下文与作用域
变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object),而这个上下文中定义的所有变量和函数都存在于这个对象上。
全局上下文是最外层的上下文。在浏览器中,全局上下文就是我们常说的window对象,因此所有通过 var 定
义的全局变量和函数都会成为 window 对象的属性和方法。
每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。
此外,局部作用域中定义的变量可用于在局部上下文中替换全局变量。看一看下面这个例子:
var color = "blue";
function changeColor() {
let anotherColor = "red";
function swapColors() {
let tempColor = anotherColor;
anotherColor = color;
color = tempColor;
// 这里可以访问 color、anotherColor 和 tempColor
}
// 这里可以访问 color 和 anotherColor,但访问不到 tempColor
swapColors();
}
// 这里只能访问 color
changeColor();
2.1 作用域链增强
- try / catch 语句的 catch 块
- with 语句
这两种情况下,都会在作用域链前端添加一个变量对象。对 with 语句来说,会向作用域链前端添加指定的对象;对 catch 语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明。看下面的例子:
function buildUrl() {
let qs = "?debug=true";
with(location) {
let url = href + qs;
}
return url;
}
当 with 语句中的代码引用变量 href
时,实际上引用的是location.href
,也就是自己变量对象的属性。
2.2 变量声明
let 和 const
成为首选
2.2.1 使用 var 的函数作用域声明
function add(num1, num2) {
var sum = num1 + num2;
return sum;
}
let result = add(10, 20); // 30
console.log(sum); // 报错:sum 在这里不是有效变量
如果省略上面例子中的关键字 var ,那么 sum 在 add()
被调用之后就变成可以访问的了。
function add(num1, num2) {
sum = num1 + num2;
return sum;
}
let result = add(10, 20); // 30
console.log(sum); // 30
这一次sum没有使用var声明,在调用 add() 之后, sum被添加到了全局上下文,在函数退出之后依然存在,从而在后面可以访问到。
2.2.2 使用 let 的块级作用域声明
let的作用域是块级,块级作用域由最近的一对包含花括号{}
界定。
let 与 var 的另一个不同之处是在同一作用域内不能声明两次。
let 的行为非常适合在循环中声明迭代变量。
for (var i = 0; i < 10; ++i) {}
console.log(i); // 10
for (let j = 0; j < 10; ++j) {}
console.log(j); // ReferenceError: j 没有定义
2.2.3 使用const的常量声明
使用 const 声明的变量必须同时初始化为某个值。
2.2.4 标识符查找
如果局部上下文中有一个同名的标识符,那就不能在该上下文中引用父上下文中的同名标识符,例如:
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'
3、垃圾回收
3.1 标记清理
JavaScript中最常用的垃圾回收策略是标记清理。
原理:当变量进入上下文,比如在函数内部声明一个变量时,这个变量会被加上存在于上下文中的标记。而在上下文中的变量,逻辑上讲,永远不应该释放它们的内存,因为只要上下文中的代码在运行,就有可能用到它们。当变量离开上下文时,也会被加上离开上下文的标记。
垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。
3.2 引用计数
另一种没那么常用的垃圾回收策略是引用计数。
原理:对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为 1。如果同一个值又被赋给另一个变量,那么引用数加 1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。当一个值的引用数为 0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序下次运行的时候就会释放引用数为 0 的值的内存。
注意: 引用计数在代码中存在循环引用时会出现问题。