《JavaScript 高级程序设计》学习笔记(一)

JavaScript 基础知识巩固

第一章 JavaScript 简介

第二章 在 HTML 中使用 JavaScript

第三章 基本概念

第四章 变量、作用域和内存

JavaScript 简介

JavaScript 简史

JavaScript 是 1995 年 Netscape 公司开发的一种用来处理简单表单验证的客户端语言,起初命名为 LiveScript ,但为了搭上媒体热炒 Java 的顺风车,临时将 LiveScript 改名为 JavaScript 。

1996 年 8 月,微软为进入 Web 浏览器领域在 Internet Explorer 中加入了名为 JScript 的 JavaScript 实现,标志着 JavaScript 作为一门语言的开发向前迈进了一大步。

1997 年,欧洲计算机制造商协会完成了 ECMA-262 ——定义一种名为 ECMAScript 的新脚本语言的标准。

1998 年,ISO/IEC(国标标准化组织/国标电工委员会)也采用了ECMAScript 作为标准(ISO/IEC-16262)。

自此以后,各大浏览器开发商开始致力于将 ECMAScript 作为各自 JavaScript 实现的基础。

JavaScript 实现

一个完整的 JavaScript 实现应该由以下三个不同部分构成:

  • 核心(ECMAScript)
  • 文档对象模型(DOM)
  • 浏览器对象模型(BOM)

ECMAScript

由 ECMA-262 定义的 ECMAScript 与 Web 浏览器之间没有依赖关系。本质上来讲,ECMAScript 本身并不包含输入和输出定义。ECMA-262 定义的只是这门语言的基础,在此基础之上可以构建更完善的脚本语言。常见的 Web 浏览器只是 ECMAScript 实现可能的宿主环境之一。

宿主环境:不仅提供基本的 ECMAScript 实现,同时也会提供该语言的扩展,以便语言与环境之间对接交互。而这些扩展——如 DOM,则利用 ECMAScript 的核心类型和语法提供更多更具体的功能,以便实现针对环境的操作。其他宿主环境包括 Node 和 Adobe Flash。

ECMA-262 虽然没有参照 Web 浏览器,但它规定了以下内容:

  • 语法
  • 类型
  • 语句
  • 关键字
  • 保留字
  • 操作符
  • 对象

ECMAScript就是对实现该标准规定的各个方面内容的语言的描述。JavaScript 实现了 ECMAScript,Adobe ActionScript 同样实现了 ECMAScript 。

文档对象模型(DOM)

文档对象模型(DOM)是针对 XML 但经过扩展用于 HTML 的应用程序编程接口(API)。DOM 将整个页面映射为一个多层节点结构。HTML 或 XML 页面中的每个组成部分都是某种类型的节点,这些节点又包含着不同类型的数据。

DOM 级别

DOM 1级由 DOM 核心(DOM Core)和 DOM HTML 组成。其中 DOM 核心规定的事如何映射基于 XML 的文档结构,以便简化对文档中任意部分的访问和操作。DOM HTML 模块则在 DOM 核心的基础上加以扩展,添加了针对 HTML 的对象和方法。

DOM 2级在原来 DOM 的基础上又扩充了( DHTML 一直都支持的)鼠标和用户界面事件、范围、遍历(迭代 DOM 文档的方法)等细分模块,而且通过对象接口增加了对 CSS 的支持。DOM2级引入了一下新模块:

  • DOM 视图:定义了跟踪不同文档视图(如应用 CSS 之前和之后的文档)的接口
  • DOM 事件:定义了事件和事件处理的接口
  • DOM 样式:定义了基于 CSS 为元素应用样式的接口
  • DOM 遍历和范围:定义了便利和操作文档树的接口

DOM3 级进一步扩展了 DOM,引入了统一方式加载和保存文档的方法——在 DOM 加载和保存模块中定义;新增了验证文档的方法——在 DOM 验证模块中定义。同时 DOM 3级也对 DOM 核心进行了扩展,开始支持 XMl 1.0规范,涉及XML Infoset、XPath 和 XML Base。

浏览器对象模型(BOM)

从根本上讲,BOM 只处理浏览器窗口和框架,单人们习惯上也把所有针对浏览器的 JavaScript 扩展算作 BOM 的一部分。如下:

  • 弹出新浏览器窗口的功能
  • 移动、缩放和关闭浏览器窗口的功能
  • 童工浏览器详细信息的 navigator 对象
  • 提供浏览器所加载页面的详细信息的 location 对象
  • 提供用户显示器分辨率详细信息的 screen 对象
  • 对 cookies 的支持
  • 像 XMLHTTPRequest 和 IE 的 ActiveXObject 这样的自定义对象

小结

JavaScript 是一种专门为网页交互而设计的脚本语言,由以下三个不同部分组成:

  • ECMAScript,由 ECMA-262 定义,提供核心语言功能
  • 文档对象模型(DOM),提供访问和操作网页内容的方法和接口
  • 浏览器对象模型(BOM),提供与浏览器交互的方法和接口

JavaScript 的这三个组成部分,在当前五个主要浏览器( IE、Firefox、Chriome、Safari 和 Opera )中都得到了不同程度的支持。其中,所有浏览器对 ECMAScript 第3版的支持大体上都还不错,而对 ECMAScript5 的支持程度越来越高,但是对 DOM 的支持则彼此相差比较多。对已经正式纳入 HTML5 标准的 BOM 来说,尽管各浏览器都实现了某些众所周知的共同特性,但其他特性还是会因浏览器而异。

在 HTML 中使用 JavaScript

<script>元素

向 HTML 页面中插入 JavaScript 的主要方法是使用<script>元素。HTML4.01<script>定义了下列6个属性:

  • async:可选。表示应该立即下载脚本,但不应妨碍页面中的其他操作,比如下载其他资源或等待加载其他脚本。只对外部脚本文件有效
  • charset:可选。表示通过src属性指定的代码的字符集。由于大多数浏览器会忽略该属性的值,因此这个属性很少有人用
  • defer:可选。表示脚本可以延迟到文档完全被解析和显示之后再执行。只对外部脚本文件有效
  • language:已废弃
  • src:可选。表示包含要执行代码的外部文件
  • type:可选。可以看成是 language 的替代属性;表示编写代码使用的脚本语言的内容类型(即 MIME 类型)。虽然 text/javscripttext/ecmascript 都不被推荐使用,但人们一直以来使用的都还是 text/javascript。实际上,服务器在传送 JavaScript 文件时使用的MIME类型通常是 application/x-javascript ,但在 type 中设置这个值却可能导致脚本被忽略。另外,在非IE浏览器中还可以使用以下值:application/javascriptapplication/ecmascript。考虑到约定俗成和最大限度的浏览器兼容性,目前 type 属性的值依旧还是 text/javascript。不过,这个属性并不是必须的,如果没有指定这个属性,则其默认值仍是 text/javascript

使用<script>元素的方式有两种:直接在页面中嵌入 JavaScript代码和包含外部 JavaScript 文件。

包含在<script>元素内部的 JavaScript 代码将被从上到下依次解释。在解释器对<script>元素内部的所有代码求值完毕以前,页面中的其余内容都不会被浏览器加载或显示。

在使用<script>嵌入 JavaScript 代码时,当浏览器遇到字符串</script>时,就会认为该处即为结束的</script>标签。我们可以通过转义字符 “\”"/" 转义为 “\/” 解决这个问题。

若要通过<script>元素来包含外部 JavaScript 文件,则需为其添加 src 属性。这个属性的值是一个指向外部 JavaScript 文件的链接,也可以用来指向外部域。需要注意的是,指向的外部域必需要是可信任的。另外带有src属性的<script>元素若在其<script></script>标签之间再包含额外的 JavaScript 代码,则包含的代码会被忽略。

若页面中包含多个<script>标签,且标签均无 deferasync 属性,浏览器就会从第一个<script>标签依次进行解析。

标签的位置

传统的做法是,所有<script>标签的位置一般放在<head>中。但在项目应用中,<script>标签的位置一般放在<body>后面。

延迟脚本

<script>标签可以添加defer属性,来告诉浏览器应当立即下载,但要延迟到整个页面解析完毕再执行。HTML5 规范要求脚本按照它们出现的先后顺序执行,因此第一个延迟脚本会先于第二个延迟脚本执行,而这两个脚本会先于 DOMContentLoaded 事件执行。在现实当中,延迟脚本并不一定会按照顺序执行,也不一定会在 DOMContentLoaded 事件触发前执行,因此最好只包含一个延迟脚本。

异步脚本

<script>标签可以添加 async 属性,来告诉浏览器应当立即下载,但与 defer 属性不同的是,标记为 async 的脚本并不保证按照其先后顺序执行。异步脚本一定会在页面的 load 事件前执行,但可能会在 DOMContentLoaded 事件触发之前或之后执行。

嵌入代码与外部文件

嵌入代码指在 HTML 文件中嵌入 JavaScript 代码。但一般认为最好的做法还是使用外部文件来包含 JavaScript 代码。使用外部文件会有以下优点:

  • 可维护性:将 JavaScript 代码放在一个位置统一管理,便于维护
  • 可缓存:浏览器可以根据具体的设置缓存链接的所有外部 JavaScript 文件,加快页面加载速度
  • 适应未来:通过外部文件来包含 JavaScript 无须使用 XHTML 或注释 hackHTMLXHTML 包含外部文件的语法是相同的。

文档模式

文档模式这个概念是 IE5.5 引入的,最初的两种文档模式是:混杂模式和标准模式。HTML5 通过使用下述文档类型来开启:

1
2
<!-- HTML 5 -->
<!DOCTYPE html>

<noscript> 元素

<noscript>用以在不支持 JavaScript 的浏览器中显示替代的内容。包含在<noscript>标签中的内容只有在下列情况才会显示:

  • 浏览器不支持脚本
  • 浏览器支持脚本,但脚本被禁用

小结

JavaScript 插入到 HTML 页面中需要使用<script>元素。需要注意的是:

  • 在包含外部 JavaScript 文件时,必须把 src 属性设置为指向相应文件的 URL。而这个文件既可以是与包含它的页面位于同一个服务器上的文件,也可以是其他任何域中的文件
  • 所有的<script>元素都会按它们在页面中出现的先后顺序依次被解析。在不适用 deferasync 属性时,只有在解析完前面<script>中的代码之后,才会开始解析后面<script>中的代码
  • 由于浏览器会解析完不使用 defer 属性的<script>中的代码再解析后面的内容,所以一般应该把<script>元素放在页面最后面,即主要内容后面,</body>前面
  • 使用 defer 属性可以让脚本在文档完全呈现后再执行。延迟脚本总是按照指定它们的顺序执行
  • 使用async属性可以表示当前脚本不必等待其他脚本,也不必阻塞文档呈现。不能保证异步脚本按照他们在页面中出现的顺序执行。

在不支持脚本的浏览器中显示替代内容可使用<noscript>元素。若启用脚本,则浏览器不会显示<noscript>元素中的任何内容。

基本概念

语法

ECMAScript 的语法大量借鉴了 C 及其他类 C 语言(如 JavaPerl )的语法。

区分大小写

ECMAScript 中的一切(变量、函数名和操作符)都区分大小写。

标识符

标识符指的是变量、函数、属性的名字,或者函数的参数。标识符的第一个字符必须是一个字母、下划线_或者美元符号$

通常 ECMAScript 标识符采用驼峰大小写格式。即第一个字母小写,剩下每个单词的首字母大写。

严格模式

严格模式是为 JavaScript 定义了一种不同的解析与执行步骤。在严格模式下,ECMAScript3 中的一些不确定行为将得到处理,并且会对某些不安全操作会拋错。

在整个 JavaScript 开启严格模式方法:

1
"use strict"

指定函数在严格模式下执行:

1
2
3
4
function dosth(){
'use strict'
// 函数体
}

语句

ECMAScript 中语句结尾一般以分号结尾,虽然不是必需的,但加上分号可以避免错误,解析器也不必花时间推测应该在哪里插入分号。

控制语句中要用代码块{}

关键字和保留字

关键字和保留字不能用作标识符。

变量

ECMAScript 的变量是松散类型的。

1
var msg = 'hi';

这样初始化变量并不会把它标记为字符串类型,因此可以在修改变量的同时修改值的类型,但不建议修改变量所保存值的类型。

使用 var 定义的变量将成为定义这个变量所在作用域中的局部变量,变量在函数退出后会被销毁。若省略 var 创建的变量为全局变量,如下:

1
2
3
4
5
6
7
8
9
10
11
function testUseVar(){
var msg = 'useVar'
}
testUseVar()
console.log(msg) // Uncaught ReferenceError: msg is not defined

function testDontUseVar(){
msg = 'dontUseVar'
}
testDontUseVar()
console.log(msg) // msg

在严格模式下,不能定义名为 evalarguments 的变量,否则会导致语法错误。

数据类型

ECMAScript 中有五种简单数据类型(基本数据类型)以及一种复杂数据类型。具体如下:

  • Undefined
  • Null
  • Boolean
  • Number
  • String
  • Object——复杂数据类型

typeof 操作符

typeof 用来检测给定变量的数据类型,返回值如下:

  • undefined:值未定义
  • boolean:布尔值
  • string:字符串
  • number:数值
  • object:对象或 null
  • function:函数
1
2
3
console.log(typeof 'some string')	// string
console.log(typeof 90) // number
console.log(typeof null) // object

特殊值 null 会被认为是一个空对象的引用。

Undefined 类型

Undefined 类型只有一个值:undefined。使用 var 声明变量但未初始化时,这个变量的值就是 undefined

1
2
var msg								// 等同于 var msg = undefined
console.log(msg === undefined) // true

未初始化的变量 与未定义的变量是两个概念:

1
2
3
4
5
var msg					// 已声明(定义)
console.log(msg) // undefined

// var age // 未定义
console.log(age) // Uncaught ReferenceError: msg is not defined

但使用 typeof 操作符检测未初始化的变量及未声明的变量,返回值均为 undefined

1
2
3
4
5
var msg						// 已声明(定义)
console.log(typeof msg) // undefined

// var age // 未定义
console.log(typeof age) // undefined

综上,未初始化的变量与未声明的变量虽然有本质的区别,但实际中对两种变量都无法执行真正的操作。

Null 类型

Null 类型也同样只有一个值:null

null 表示一个空对象指针。这也是 typeof 操作符检测 null 返回 ’object‘ 的原因。若定义变量是用来保存对象,最好将其初始化为 null

1
console.log(null == undefined)	// true

出现上述输出结果的原因是 undefined 值是派生自 null 值的,因此 ECMA-262 规定对他们的相等性测试要返回 true。但两者也有区别之处:没必要把一个变量的值显式的设置为 undefined ,但一个用来保存对象的变量在未保存对象之前就应该保存 null 值。这样做既可以体现 null 作为空对象指针的惯例,也有助于区分两者。

Boolean 类型

Boolean 类型是 ECMAScript 中使用的最多的一种类型,字面值只有 truefalse

可以对任意数据类型的值调用转型函数 Boolean() 函数,对应转换规则如下:

数据类型转换为true的值转换为false的值
Booleantruefalse
String任何非空字符串“”(空字符串)
Number任何非零数值(包括无穷大)0 和 NaN
Object任何对象null
UndefinedN/A(不适用)undefined

流控制语句(如 if 语句)会自动执行相应的 Boolean 转换。

Number 类型

最基本的数值字面量格式是十进制整数,除了十进制外,整数还可以通过八进制或十六进制的字面值来表示。

八进制字面值的第一位必须是零(0),然后是八进制数字序列(0-7)。若字面值超出范围,则该值会被当作十进制数值解析。

1
2
3
var octalNum1 = 070		// 八进制56
var octalNum2 = 079 // 无效八进制数值,解析为79
var octalNum2 = 08 // 无效八进制数值,解析为8

十六进制数值前两位必须是 0x ,后跟任何十六进制数字(0-9,A-F)。其中字母可大写也可小写。

1
2
var hexNum1 = 0xA		// 十六进制10
var hexNum2 = 0x1f // 十六进制31
浮点数值

数值中包含小数点,切小数点后至少有一位数字。

1
2
3
var floatNum1 = 1.1
var floatNum2 = 0.1
var floatNum3 = .1 // 有效,不推荐

若一个数值小数点后无任何数字,或数值本身表示的就是一个整数,则该数值会被解析为整数。

1
2
var floatNum1 = 1.		// 解析为1
var floatNum1 = 20.0 // 解析为20

对于极大或极小的数值,可以用科学计数法表示的浮点数值表示。

1
var floatNum = 1.45e6	// 等于1450000

上述例子中,1.45e6 = $1.45*10^6$。

浮点数值最高精度是17位小数,但在进行算数计算时其精度远远不如整数。

例如,0.10.2 的结果不是 0.3,而是 0.30000000000000004,这是使用基于
IEEE754 数值的浮点计算的通病,ECMAScript 并非独此一家;其他使用相同数值格
式的语言也存在这个问题。

数值范围

ECMAScript 能表示的最小数值为 Number.MIN_VALUE ——在大多数浏览器中为5e-324,最大数值为Number.MAX_VALUE——在大多数浏览器中为1.7976931348623157e+308

若某次计算结果超出该范围,则结果会被自动转换成特殊的 Infinity(正无穷)或 -Infinity(负无穷),且该结果无法继续参与下一次的计算。

可以使用 isFinite() 函数来确定一个数值是否在最大值与最小值之间,返回值为 truefalse

1
2
var res = Number.MAX_VALUE + Number.MAX_VALUE
console.log(isFinite(res)) // false
NaN

NaN(Not a Number)是一个特殊数值,用于表示一个本来要返回数值的操作数未返回数值的情况。

1
2
3
console.log(5 / 0)			// Infinity
console.log(-5 / 0) // -Infinity
console.log(0 / 0) // NaN

NaN 具有以下两个特点:

  • 任何涉及 NaN 的操作都会返回 NaN
  • NaN 与任何值都不相等,包括其本身
1
console.log(NaN == NaN)		// false

isNaN() 函数可以接受一个任何类型的参数并且确定这个参数是不是数值。该函数在接收到一个值后会尝试将该值转换为数值,若可以转换成功则返回 false,否则将返回 true

1
2
3
4
5
console.log(isNaN(NaN))		// true
console.log(isNaN(100)) // false(10)
console.log(isNaN('200')) // false(200)
console.log(isNaN('red')) // true
console.log(isNaN(false)) // false(0)
数值转换

可以通过 Number()parseInt()parseFloat() 三个函数将非数值转换为数值。

Number() 转换规则:

  • 若为 Boolean 值,truefalse 分别转换为1和0
  • 若为数值,只是简单地传入和返回
  • 若为 null ,返回0
  • 若为undefined,返回 NaN
  • 若为字符串,按以下规则:
    • 若字符串中只包含数字,包括前面带正负号的情况,则将其转换为十进制数值,忽略前导零
    • 若字符串中包含有效的浮点格式,则将其转换为相应的浮点数值,忽略前导零
    • 若字符串中包含有效的十六进制格式,将其转换为相同大小的十进制整数值
    • 若字符串是空的,则将其转换为0
    • 若字符串中包含除上述格式之外的字符,则将其转换为 NaN
  • 若为对象,则调用对象的 valueOf() 方法,然后依照前面规则转换。若转换的结果是 NaN,则调用对象的 toString() 方法,然后依照前面规则转换
1
2
3
4
var num1 = Number('hello world')	// NaN
var num2 = Number('') // 0
var num3 = Number('00032') // 11
var num4 = Number(true) // 1

parseaInt() 转换规则:

使用 parseInt() 转换字符串时,会忽略字符串前面的空格,直至找到第一个非空字符,若第一个字符不是数字字符或正负号,会返回 NaN 。若第一个字符是数字字符,该函数在继续解析后续字符时若遇到非数字字符则会将已解析的值返回,否则将解析完所有后续字符。并且该函数还可以识别各种整数格式(八进制,十进制,十六进制)。

1
2
3
4
5
6
7
var num1 = parseInt('123red')		// 123
var num2 = parseInt('') // NaN
var num3 = parseInt('0xA') // 10(十六进制数)
var num4 = parseInt(22.5) // 22
var num5 = parseInt('070') // 56(八进制数)
var num6 = parseInt('70') // 70(十进制数)
var num7 = parseInt('0xf') // 15(十六进制数)

使用 parseInt() 解析八进制字面量的字符串时,ECMAScript3 和 5 存在分歧:

1
2
var num = parseInt('070')
// ECMAScript3 会输出56(八进制),ECMAScript5 会输出70(十进制)

为了解决这个问题,在使用 parseInt() 函数时可以提供第二个参数作为转换基数,即多少进制。

1
2
3
4
var num1 = parseInt('10',2)		//	2	按二进制解析
var num2 = parseInt('10',8) // 8 按八进制解析
var num3 = parseInt('10',10) // 10 按十进制解析
var num4 = parseInt('10‘,16) // 16 按十六进制解析

parseFloat() 转换规则:

parseFloat() 从第一个字符开始解析,一直到字符串末尾或者一个无效的浮点数为止。字符串中第一个小数点有效,第二个小数点后无效。另外,该函数始终会忽略前导零。parseFloat() 只解析十进制值,因此没有用第二个参数指定基数的用法,若字符串包含的是一个可解析为整数的数,则返回整数。

1
2
3
4
5
6
var num1 = parseFloat('1234red')		// 1234
var num2 = parseFloat('0xA') // 0
var num3 = parseFloat('22.5') // 22.5
var num4 = parseFloat('22.34.5') // 22.34
var num5 = parseFloat('090.3') // 90.3
var num6 = parseFloat('3.125e7') // 31250000

String 类型

String 类型用于表示由零或多个16位 Unicode 字符组成的字符序列,即字符串。

字符字面量

String 数据类型包含一些特殊的字符字面量,也叫转义序列,用于表示非打印或具有其他用途的字符,如下:

字面量含义
\n换行
\t制表
\b空格
\r回车
\f进纸
\斜杠
\’单引号
\”双引号
\xnn以十六进制代码nn表示的一个字符(n为0~F)
\unnnn以十六机制代码nnnn表示的一个Unicode字符(n为0~F)

每个转移序列表示一个字符,字符串的长度可以通过访问 length 属性获取。

字符串特点

ECMAScript中的字符串一旦创建,值就不能改变。如果要改变某变量保存的字符串,则需要将原字符串销毁,再用另外一个包含新值得字符串填充改变量。

转换为字符串

将一个值转为一个字符串有两种方法:toString()String() 。需要注意的是,nullundefined 没有 toString() 方法,而 String() 函数对所有类型的值都适用。String() 函数遵循以下转换规则:

  • 如果值有 toString() 方法,则调用该方法(没有参数)并返回相应结果
  • 如果值是 null ,则返回 "null"
  • 如果值是 undefined ,则返回 "undefined"

Object 类型

Object 类型所具有的任何属性和方法也同样存在于更具体的对象中。Object 的每个实例都具有下列属性和方法:

  • constructor:保存着用于创建当前对象的函数
  • hasOwnProperty(propertyName):用于检查给定的属性是否存在于当前对象实例中(而非在实例的原型中),其中 propertyName 必须以字符串形式指定
  • isPrototypeOf(object) :用于检查传入的对象是否是传入对象的原型
  • propertyIsEnumerable(propertyName) :用于检查给定属性是否能够使用 for-in 语句来枚举
  • toLocaleString() :返回对象的字符串表示,该字符串与执行环境的地区对应
  • toString() :返回对象的字符串表示
  • valueOf() :返回对象的字符串、数值或布尔值表示,通常与 toString() 方法的返回值相同

操作符

一元操作符

只操作一个值的操作符叫一元操作符。

递增和递减操作符

递增和递减操作符各有两个版本:前置型和后置型。

执行前置递增和递减操作时,变量的值都是在语句被求值以前改变的。

执行后置递增和递减操作时,该操作是在包含它们的语句被求值后才执行的。

1
2
3
4
5
6
7
8
9
10
// 前置递增递减操作符
let num1 = 2
let num2 = 20
console.log(--num1 + num2) // 21
console.log(num1 + num2) // 21
// 后置递增递减操作符
let num3 = 2
let num4 = 20
console.log(num3-- + num4) // 22
console.log(num3 + num4) // 21

以上四个操作符适用于任何值,在应用于不同的值得时候,遵循以下规则,输出值均为数值变量:

  • 应用于字符串,若字符串包含有效字符,将其转为数字值再进行加减1操作
  • 应用于字符串,若字符串不包含有效数字字符,返回值为 NaN
  • 应用于布尔值,将 false 转为0,true 转为1,再执行加减1操作
  • 应用于浮点数值时,直接执行加减1操作
  • 应用于对象时,先调用对象的 valueOf() 方法取得一个可供操作的值后再对该值进行前述规则,若结果是 NaN 则调用 toString() 方法再应用前述规则

示例:

1
2
3
4
5
6
7
8
9
10
11
let s1 = '2', s2 = 'z', b = false, f = 1.1
let o = {
valueOf:function(){
return -1
}
}
s1++ // 3
s2++ // NaN
b++ // 1
f-- // 0.10000000000000009(由于浮点舍入错误所致)
o-- // -2
一元加和减操作符

对非数值应用一元加减操作符( +- )时,该操作符会像 Number()转型函数一样对这个值执行隐式转换。因此一元加减操作符主要用于基本的算术运算,也可以用于转换数据类型。

位操作符

按位非(NOT)

按位非操作符由 “~” 表示,执行按位非得结果就是返回数值的反码。

按位非操作的本质:操作数的负值减 1。

1
2
3
let num1 = 25		// 二进制00000000000000000000000000011001
let num2 = ~num1 // 二进制11111111111111111111111111100110
console.log(num2) // -26
按位与(AND)

按位与操作符由 & 表示,有两个操作数

&
1 & 1 = 1
1 & 0 = 0
0 & 1 = 0
0 & 0 = 0

按位与操作只在两个数值的对应位都是 1 时才返回 1,任何一位是 0,结果都是 0

1
2
3
4
5
console.log(25 & 3)	// 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
按位或(OR)

按位或操作符由 | 表示,有两个操作数

\
1 \1 = 1
1 \0 = 1
0 \1 = 1
0 \0 = 0

按位或操作在有一个位是 1 的情况下就返回 1,而只有在两个位都是 0 的情况下才返回 0

1
2
3
4
5
console.log(25 | 3)	// 27
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
按位异或(XOR)

按位或操作符由 ^ 表示,有两个操作数

^
1 ^ 1 = 0
1 ^ 0 = 1
0 ^ 1 = 1
0 ^ 0 = 0

按位异或与按位或的不同之处在于,这个操作在两个数值对应位上只有一个 1 时才返回 1,如果对应的两位都是 1 或都是 0,则返回 0

1
2
3
4
5
console.log(25 ^ 3)	//26
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
左移

左移操作符由 << 表示,这个操作符会将数值的所有位向左移动指定的位数,左移会在原数值的右侧出现的空位,用0来填充。

1
2
3
let oldVal = 2				// 二进制的10
let newVal = oldVal << 5 // 二进制的1000000,十进制的64
注:左移不会影响操作数的符号位,即左移不会改变数值的正负
右移

右移分为有符号右移和无符号右移两种情况。

有符号右移:

操作符为 >> 表示,该操作符会将数值向右移动,但保留负号位,右移会在原数值的左侧,符号位的右侧出现空位,此时会用符号位的值来填充所有空位。

1
2
let oldVal = 64			// 二进制1000000
let newVal = oldVal>>5 // 二进制的10,十进制的2

无符号右移:

无符号右移由 >>> 表示,该操作符会将数值的所有32位都右移,对正数来讲,无符号右移的结果与有符号右移相同,对负数来讲,无符号右移操作符会将负数的二进制码当做正数的二进制码,由于负数以其绝对值的二进制补码形式表示,因此会导致无符号右移之后的结果非常大。

1
2
let oldVal = -64			// 二进制的11111111111111111111111111000000
let newVal = oldVal >>> 5 // 十进制的134217726

####布尔操作符

逻辑非

逻辑非操作符即 ! ,可以应用于任何值,先将该值转换为布尔值,再取反,返回一个布尔值。同时对一个值使用两个逻辑非操作符,实际相当于使用 Boolean() 函数,返回值即为该值对应的布尔值

  • ! 空字符串,返回 true
  • ! 0 ,返回 true
  • ! undefined ,返回 true
  • ! null ,返回 true
  • ! NaN ,返回 true
  • ! 非0数值,返回 false

  • ! 非空字符串,返回 false

  • !对象,返回 false
逻辑与

逻辑与由 && 表示,有两个操作数,真值表如下:

第一个操作数第二个操作数结果
truetruetrue
truefalsefalse
falsetruefalse
falsefalsefalse

逻辑与可以应用于任何类型的操作数,不仅仅是布尔值。若有一个操作数不是布尔值,返回结果就不一定是布尔值,遵循规则如下:

  • 若第一个操作数是对象,返回第二个操作数
  • 若第二个操作数是对象,只有在第一个操作数求值结果为 true 时才会返回第二个操作数
  • 若两个操作数都是对象,返回第二个操作数
  • 若一个操作数是 null , 返回 null
  • 若一个操作数是 NaN ,返回 NaN
  • 若一个操作数是 undefined,返回 undefined

另外,逻辑与是一个短路操作符。若逻辑与的第一个操作数能决定结果,就不会继续执行逻辑与符号右侧的内容。即若第一个操作数是 false,则结果为false,并结束当前逻辑语句的执行

逻辑或

逻辑或操作符由 || 表示,有两个操作数,真值表如下:

第一个操作数第二个操作数结果
truetruetrue
truefalsetrue
falsetruetrue
falsefalsefalse

与逻辑与操作相似,若一个操作数不是布尔值,逻辑或也不一定返回布尔值。返回结果遵循如下规则:

  • 若第一个操作数是对象,则返回第一个操作数
  • 若第一个操作数求值结果是 false,返回第二个操作数
  • 若两个操作数都是对象,则返回第一个操作数
  • 若两个操作数都是 null,则返回 null
  • 若两个操作数都是 NaN,则返回 NaN
  • 若两个操作数都是 undefined,则返回 undefined

逻辑或操作符同样也是短路操作符。若第一个操作数的求值结果为 true,则不会对第二个操作数求值。我们可以利用逻辑或的行为来避免为变量赋 null 或 undefined,如:

1
2
let myObj = preferredObject || backupObject
// 赋值过程中,优先取preferredObject,若其转布尔值为 false,则取 backupObject

乘性操作符

乘性操作符包含乘法,除法,求模。若参与乘性操作的某个操作数不是数值,后台会先将其用 Number() 转型函数转换为数值再进行计算。

乘法

乘法操作符由 * 表示,用于计算两个数值的乘积。处理特殊值的情况下,乘法操作符遵循以下规则:

  • 如果操作数都是数值,则执行常规的乘法计算,若乘积超过 ECMAScript 数值的表示范围,则返回 Infinity-Infinity
  • 若一个操作数是 NaN,则结果是 NaN
  • Infinity * 0 = NaN
  • Infinity * 非0 ,结果是 Infinity-Infinity,取决于 非0 数值的符号
  • Infinity * Infinity = Infinity
  • 若一个操作数不是数值,则在后台调用 Number() 将其转换为数值,然后再应用上面的规则
除法

除法操作符由 / 表示,执行第二个操作数除第一个操作数的计算,遵循以下规则:

  • 若操作数都是数值,执行常规的除法计算,若商超过 ECMAScript 数值的表示范围,则返回 Infinity-Infinity
  • 若一个操作数是 NaN ,则结果为 NaN
  • 0 / 0 ,结果为 NaN
  • 非0 / 0 ,结果为 Infinity-Infinity ,取决于 非0 数值的符号
  • Infinity / 非0 ,结果为 Infinity-Infinity ,取决于 非0 数值的符号
  • 若一个操作符不是数值,则调用 Number() 将其转换为数值,再应用上述规则
求模

求模(余数)操作符由 % 表示,用法如下:

  • 若操作符都是数值,执行常规除法计算,返回除得的余数
  • 无穷大值 % 有限大值 ,结果是 NaN
  • 有限大值 % 0 ,结果为 NaN
  • Infinity % Infinity ,结果为 NaN
  • 有限大值 % 无穷大值 ,结果是被除数
  • 若被除数为0,则结果是0
  • 若一个操作数不是数值,在后台调用 Number() 将其转换为数值,再应用上述规则

加性操作符

与乘性操作符类似,加性操作符也会在后台转换不同的数据类型

加法

操作符 + ,执行加法计算,根据下列规则返回结果:

  • NaN + 任意值 = NaN
  • Infinity + Infinity = Infinity
  • -Infinity + ( -Infinity ) = -Infinity
  • -Infinity + Infinity = NaN
  • +0 + ( +0 ) = +0
  • -0 + ( -0 ) = -0
  • -0 + ( +0 ) = +0
  • 字符串1 + 字符串2 = 字符串1与字符串2拼接
  • 只有一个操作数为字符串,则将另一个操作数转为字符串后将两个字符串拼接
  • 若一个操作数是对象、数值或布尔值,另一个操作数是字符串,则调用 toString() 方法取得相应的字符串,再应用于上述字符串规则
减法

操作符 - ,执行减法操作,根据下列规则返回结果:

  • 若两个操作符都是数值,则执行常规算数减法运算
  • 若有一个操作数为 NaN ,结果是 NaN
  • Infinity - Infinity = NaN
  • -Infinity - -Infinity = NaN
  • Infinity - -Infinity = Infinity
  • -Infinity - Infinity = -Infinity
  • +0 - +0 = +0
  • +0 - -0 = -0
  • -0 - -0 = +0
  • 若一个操作数是字符串、布尔值、null 、或者 undefined ,先调用 Number() 函数将其转换为数值,再根据前面的规则执行减法计算。若转换结果是 NaN ,减法结果就是 NaN
  • 若一个操作数是对象,则优先调用对象的 valueOf() 方法,若对象没有 valueOf() 方法,则调用其 toString() 方法并将得到的字符串转为数值

关系操作符

<><=>= 这几个关系操作符用于两个值进行比较,返回一个布尔值

同其他操作符一样,若操作数使用了非数值,会进行隐式转换,规则如下:

  • 若均为数值,则执行数值比较
  • 若都是字符串,则比较两个字符串对应的字符编码值
  • 若一个操作数为数值,则将另一个操作数转换为数值,再进行比较
  • 若一个操作数是对象,则调用这个对象的 valueOf() 方法,若没该方法则调用 toString() 方法,并将结果按上述规则进行比较
  • 若一个操作数是布尔值,则先转换为数值再执行比较
  • 任何操作数与 NaN 进行关系比较,结果都是 false

相等操作符

相等和不相等

==!= 都会对操作数进行强制转型,然后再比较相等性,转型规则如下:

  • 若一个操作数是布尔值,比较之前会将 true 转换为1,false 转换为0
  • 若一个操作数是字符串,另一个操作数是数值,比较之前先将字符串转换为数值
  • 若一个操作数是对象,另一个操作数不是,则调用 valueOf() 方法,用得到的基本类型值按照前面规则比较
  • null == undefined
  • 比较之前不能将 null 和 undefined 转换成其他任何值
  • NaN != 任意值
  • 若两个操作数都是对象,则比较它们是不是同一个对象,若两者均指向同一对象,则返回 true,否则为 false
1
2
3
4
5
6
7
8
9
10
11
null == undefined		// true
'NaN' == NaN // false
5 == NaN // false
NaN == NaN // false
NaN != NaN // true
false == 0 // true
true == 1 // true
true == 2 // false
undefined == 0 // false
null == 0 // false
'5' == 5 // true
全等和不全等

===!== ,比较之前不转换操作数数值类型,若全等即表示两个操作数未进行转换就相等。

注 :null !== undefined

条件操作符

也称之为三目运算符,如下:

1
let max = ( num1 > num2 ) ? num1 : num2

赋值操作符

= += -= *= /= %= <<= >>= >>>=

使用赋值操作符只能简化赋值操作,并不能带来性能提升

逗号操作符

逗号操作符 , 常用于一条语句中执行多个操作

语句

if 语句

1
2
3
4
5
6
7
if (i > 25) {
alert("Greater than 25.");
} else if (i < 0) {
alert("Less than 0.");
} else {
alert("Between 0 and 25, inclusive.");
}

do-while 语句

do-while 语句是一种后测试循环语句,只有循环体中代码执行后,才会测试出口条件。

1
2
3
4
5
6
let i = 0
do {
i += 2
} while (i<10)

console.log(i) // 10

while 语句

while 语句属于前测试循环语句,在循环体内代码执行之前,会对出口条件求值。

1
2
3
4
5
let i = 0
while (i < 10){
i += 2
}
console.log(i) // 10

for 语句

for 语句是前测试循环语句。

1
2
3
4
var count = 10;
for (var i = 0; i < count; i++){
2alert(i);
}

for-in 语句

for-in 语句可以用来枚举对象的属性,若迭代对象变量为 null 或 undefined,则不再执行循环体。

label 语句与 breakcontinue 语句

label 语句可以在代码中添加标签,一般可以由 breakcontinue 语句引用在 for 语句等循环语句中配合使用。

1
2
3
4
5
6
7
8
9
10
11
12
var num = 0;
outermost:
for (var i=0; i < 10; i++) {
for (var j=0; j < 10; j++) {
if (i == 5 && j == 5) {
// 结束当前的两个 for 循环,退出到最外层执行 console
break outermost;
}
num++;
}
}
console.log(num); //55
1
2
3
4
5
6
7
8
9
10
11
12
var num = 0;
outermost:
for (var i=0; i < 10; i++) {
for (var j=0; j < 10; j++) {
if (i == 5 && j == 5) {
// 不再执行 continue 下方代码,跳出当前循环,继续执行外层循环
continue outermost;
}
num++;
}
}
console.log(num); //95

with 语句

with 语句的作用是将代码的作用域设定到一个特定的对象中。定义 with 语句的目的主要是为了简化多次编写同一个对象的工作,如下:

1
2
3
4
5
6
7
8
9
let qs = location.search.substring(1);
let hostName = location.hostname;
let url = location.href;
// 使用 with 语句
with(location){
let qs = search.substring(1)
let hostName = hostname
let url = href
}

注意:严格模式下不允许使用 with 语句,另外大量使用 with 语句会导致性能下降,不建议使用 with 语句

switch 语句

switch 语句与 if 语句关系最密切。在 switch 语句中,如果省略 break关键字,name 就会在执行完当前 case 后,继续执行下一个 case,而 default 关键字则用于在表达式不匹配前面任何一种情况时执行之后代码(相当于一个 else 语句)。

注意:switch 语句在比较值时使用的是全等操作符,因此不会发生类型转换

函数

ECMAScript 中函数使用 function 关键字来声明,后面跟一组参数及函数体。

函数定义时不必指定是否返回值,任何函数在任何时候都可以通过 return 实现返回值,另外,return 语句后的代码不会被执行。当 return 后不带任何返回值时,函数在停止执行后返回 undefined 值,一般用于需要提前停止函数执行又不需要返回值的情况下。

参数

函数的参数的个数是由调用函数时传入函数中的参数,即实际参数决定的,而非定义函数时的命名参数,即形式参数决定。

在函数体内部可以通过 arguments 对象来访问参数数组,从而获取传递给函数的每一个参数。而 arguments 对象是一个类数组对象,因此可以使用方括号语法 arguments[0] 访问其第一个元素,往后依次类推。另外,可以使用 length 属性可以确定传递到该函数的有多少个参数。

若函数定义时未定义形参,也可以在函数体内部使用 arguments 访问到传入函数的参数,此外 arguments 对象也可以与形参一起使用。

1
2
3
4
5
6
7
8
function doAdd(num1, num2) {
if(arguments.length == 1){
console.log(num1 + 10)
}else if(arguments.length == 2){
// 此处 arguments[0] 相当于形参 num1
console.log(arguments[0] + num2)
}
}

非严格模式下,arguments 的值会与对应命名参数的值保持同步,如下:

1
2
3
4
5
6
7
function doAdd(num1, num2) {
arguments[1] = 10;
console.log(arguments[0] + num2);
}
// 若实参长度大于1,每次执行该函数时都会将第二个参数的值修改为10,由于 arguments 对象中的值会自动反映到对应的命名参数,因此修改 arguments[1] 也就修改了 num2。
// 若实参长度为1或0,则 arguments[1] 设置的值不会反映到 num2 中,因为 arguments 长度由实参长度决定,而非形参
// 注意:arguments[1] 与 num2 的内存空间并不相同,而是互相独立的。

若形参长度大于实参长度,则没有传递值的形参会被自动赋值 undefined

严格模式下,重写 arguments 值会导致语法错误。

没有重载

ECMAScript 函数不能像传统意义上那样实现重载。若定义两个名字相同的函数,则后定义的函数会将先定义的函数覆盖,最终只有后定义的函数生效,不过可以通过检查传入函数中参数的类型和长度分别做处理来模拟重载

小结

  • 基本数据类型:UndefinedNullBooleanNumberString

  • Number 类型可以用于表示所有数值

  • 复杂数据类型:Objecket

  • 严格模式为语言中易出错的地方加了限制

  • 提供了很多与 C 及其他类 C 语言中相同的基本操作符,包括算术操作符、布尔操作符、关系操作符、相等操作符及赋值操作符等

  • 从其他语言中借鉴了很多流控制语句,例如 if 语句、for 语句和 switch 语句等。 ECMAScript 中的函数与其他语言中的函数有诸多不同之处

  • 无需指定函数返回值,任何函数可以在任何时候返回任何值

  • 未指定返回值得函数返回的是一个特殊的 undefined 值

  • 可向函数中传递任意数量的参数,并可通过 arguments 对象来访问这些参数

  • 没有函数签名的概念,因为函数参数是以一个包含零或多个值得数组形式传递的

  • 由于不存在函数签名的特性,所以不能重载

变量、作用域和内存

JavaScript 由于不存在定义某个变量必须要保存何种数据类型值的规则,变量的值及其数据类型可以在脚本的生命周期内改变。

基本类型和引用类型的值

变量可能包含两种不同数据类型的值:基本类型值和引用类型值。

基本类型值指的是简单数据段:UndefinedNullBooleanNumberString, 这五种基本数据类型是按值访问的,因此可以操作保存在变量中的实际的值。

引用类型值指的是可能由多个值构成的对象:ObjectFunction, 引用类型的值是保存在内存中的对象。

将一个值赋给变量时,解析器必须确定这个值是基本类型值还是引用类型值。

JavaScript 不允许直接访问内存中位置,因此不能直接操作对象的内存空间。

操作对象的本质是在操作对象的引用而非实际的对象。当复制保存对象的某个变量时,操作的是对象的内存地址的引用。为对象添加属性时,操作的是实际的对象。

动态的属性

定义基本类型值和引用类型值得方式是类似的:创建一个变量并为该变量赋值。但是,当这个值保存到变量中以后,对不同类型值可执行的操作则大相径庭:

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

但对于基本类型的值,不能添加属性,尽管这样做不会导致任何错误。

复制变量值

在从一个变量向另一个变量复制基本类型值和引用类型值时,也存在不同。

若复制基本类型的值,会在变量对象上创建一个新值,然后把该值复制到新变量分配的位置上。其中新变量和被复制的变量的内存地址是相互独立的,两者的值可以参与任何操作而不会相互影响。

复制基本类型

若复制引用类型,也会将存储在变量对象中的值复制一份到新变量分配的空间中。与基本类型不同的是,复制的值得副本实际是一个指针,指向存储在堆栈中的对象的内存地址,因此两个变量实际上都引用同一个对象,两者均指向内存空间的同一个值。若改变一个变量,就会改变实际的引用对象,由此导致另一个变量随之改变。

复制引用类型

传递参数

ECMAScript 中所有函数的参数都是按值传递的,也就是说将函数外部的值复制给函数内部的参数,就是把值从一个变量复制到另一个变量。

向参数传递基本类型的值时,被传递值会复制给一个局部变量(形参),实参与形参之间,对其中一个进行操作不会影响另外一个。

1
2
3
4
5
6
7
8
function addTen(num) {
num += 10
2return num
}
var count = 20
var result = addTen(count)
console.log(count) //20,没有变化
console.log(result) //30

而向参数传递引用类型值时,会把这个引用类型的值的内存地址复制给一个局部变量(形参),因此这个局部变量的变化会体现在函数的外部。

1
2
3
4
5
6
function setName(obj) {
obj.name = "Nicholas"
}
var person = new Object()
setName(person)
console.log(person.name) //"Nicholas"

向参数传递对象时,实际传递的是该对象对应的内存地址的引用,本质还是值的传递。若在函数内部改变形参的值,改为其他对象的引用,此时再改变形参的属性值,则不会影响实参,如下:

1
2
3
4
5
6
7
8
function setName(obj){
obj.name = 'dong'
obj = new Object() // 此时该形参已与实参无关
obj.name = 'xiao'
}
let person = {}
setName(person)
console.log(person.name) // dong

检测类型

检测基本数据类型用 typeof ,若一个变量的值是一个对象或 null,返回值为 object

检测引用类型则用 instanceof 。所有引用类型的值均为 Object 实例,因此使用 instanceof 操作符检测基本类型的值,返回值始终为 false,若检测引用类型和 Object 构造函数时,始终返回 true。

执行环境及作用域

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

每个执行环境都有一个与之关联的变量对象,环境中定义的变量和函数都保存在这个对象中。这个对象我们编写的代码无法访问,但解析器会在处理数据时在后台使用它。

全局执行环境是最外层的一个执行环境。

在 Web 浏览器中,全局执行环境被认为是 Window 对象。因此所有全局变量和函数都是作为 window 对象的属性和方法创建的。当某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。

全局执行环境会直至应用程序退出(如关闭网页或浏览器)时才会被销毁。

每个函数都有自己的执行环境,当执行流进入一个函数时,函数环境被推入一个环境栈中,函数执行后,栈将其环境弹出,将控制权返回给之前的执行环境。

当代码在一个环境中执行时,会创建变量对象的一个作用域链,用以保证对执行环境有权访问的所有变量和函数的有序访问。

作用域链的前端始终是当前执行代码所在环境的变量对象。若该环境是一个函数,则将其活动对象作为变量对象。活动对象在最开始时只包含一个变量,即 arguments 对象(全局环境中不存在)。作用域链中的下一个变量对象来自其外部环境,而再下一个变量对象则来自下一个包含环境。这样一直延续到全局执行环境:全局执行环境的变量对象始终都是作用域链中的最后一个对象。

标识符解析是沿着作用域链一级一级的从作用域链的前段开始逐渐向后回溯搜索标识符的过程,直至找到标识符。若找不到标识符,通常会导致错误发生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var color = "blue"
function changeColor(){
var anotherColor = "red"

function swpColors(){
var tempColor = anotherColor
anotherColor = color
color = tempColor
// 这里处于作用域链的最前端,可以顺着作用域链访问到该作用域链上其他所有的变量对象,即 color,anotherColor 和 tempColor
}
// 这里可以访问 color 和 anotherColor,但不能访问 tempColor
swapColors()
}
// 这里只能访问 color
changeColor()

以上代码涉及到三个执行环境:全局环境changeColor()swapColors() 的局部环境。

作用域链

内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量核函数。这些环境之间的联系是线性,有次序的。每个环境都可以向上搜索作用域链,以查询变量和函数名。但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。

延长作用域链

当执行流进入 try-catch 语句的 catch 块或者 with 语句时,将会延长作用域链。

对于 catch 语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。

对于 with 语句来说,会将指定的对象添加到作用域链中。

没有块级作用域

if 语句和 for 语句中声明的变量会将变量添加到语句外的执行环境中。

使用 var 声明的变量会自动添加到最接近的环境中,函数内部即为函数的局部环境,with 语句中即为函数环境。

若初始化变量时没使用 var 声明,该变量会被添加到全局环境中。

当在某个环境中查询一个变量标识符时,会从作用域链前端开始,向外层作用域逐层查询。若查到,则停止查询,此时即使父环境中有同名标识符,也会优先使用当前环境中的标识符。若未查到,则继续沿作用域链向上层搜索,直至全局对象,若仍未找到,则该该变量未声明。

垃圾收集

JavaScript 中常用的垃圾收集方式是标记清除

垃圾收集器运行时会将内存中的所有变量都加上标记

将在环境中的变量及被环境中变量引用的变量的标记去除

剩下的还存在标记的变量就是准备删除的变量

最后垃圾收集器会销毁带标记的值并收回它们所占的内存空间,完成内存清除

还有一种垃圾收集方式是引用计数

引用计数含义是记录每个值被引用的次数。

当声明一个变量并将一个引用类型值赋给该变量时,这个值得引用次数就是 1。如果同一个值又被付给了另外一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取了另外一个值(即释放了对初始变量的引用),则这个值得引用次数减 1。当这个值的引用次数变为 0 时,回收其占用的内存空间。当垃圾收集器再次运行时会释放引用次数为 0 的值所占的内存空间。

但是引用计数有一个严重缺陷,当对象 A 中包含一个指向对象 B 的指针,而对象 B 中也包含了一个指向对象 A 的引用,如下:

1
2
3
4
5
6
function problem(){
var objectA = new Object();
var objectB = new Object();
objectA.someOtherObject = objectB
objectB.anotherObject = objectA
}

若采用标记清除,当 problem 函数执行完毕后,这两个对象离开了作用域,则释放其相对应的内存。但是若采用引用计数清除,这两个对象不会被清除,因为它们的引用次数不可能为0。

若要避免类似循环引用问题,则需要在不使用它们的时候手工断开它们之间的联系,如下:

1
2
objectA.someOtherObject = null
objectB.anotherObject = null

解除引用:优化内存占用的最佳方式就是为执行中的代码只保存必要的数据,一旦数据不再有用,最好将其置 null 来释放其引用。这一做法适用于大多数全局变量和全局对象的属性。局部变量会在它们离开执行环境时自动被解除引用。

小结

JavaScript 变量可以用来保存基本类型值引用类型值

基本类型值源于以下五种基本数据类型:UndefinedNullBooleanNumberString

基本类型值和引用类型值具有以下特点:

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

所有变量都存在于一个执行环境(即作用域)中,这个执行环境决定了变量的声明周期,及那一部分代码可以访问其中的变量。

  • 执行环境分为全局执行环境和函数执行环境
  • 每次进入一个新执行环境,都会创建一个用于搜索变量和函数的作用域链
  • 函数的局部环境不仅有权访问函数作用域中的变量,而且有权访问局部环境中的任何数据
  • 变量的执行环境有助于确定应该何时释放内存

JavaScript 是一门具有自动垃圾收集机制的编程语言,开发人员不必关心内存分配和回收问题。

  • 离开作用域的值将被自动标记为可回收,因此在垃圾收集期间被删除
  • 标记清除是目前主流的垃圾收集算法,思想是给当前不使用的值加上标记,然后再回收其内存
  • 引用计数是另外一种垃圾收集算法,思想是跟踪记录所有值被引用的次数。
  • 当代码中存在循环引用的现象时,引用计数算法会导致问题。
  • 解除变量的引用不仅有助于消除循环引用现象,而且对垃圾收集也有好处。为了确保有效的回收内存,应该及时解除不再使用的全局对象、全局对象属性以及循环引用变量的引用。
0%