< script >元素
元素的属性
- async:可选。表示应该立即开始下载脚本,但不能阻止其他页面动作,比如下载资源或等待其他脚本加载。只对外部脚本文件有效。
- charset:可选。使用 src 属性指定的代码字符集。这个属性很少使用,因为大多数浏览器不在乎它的值。
- crossorigin:可选。配置相关请求的CORS(跨源资源共享)设置。默认不使用CORS。
- crossorigin= "anonymous"配置文件请求不必设置凭据标志。
- crossorigin="use-credentials"设置凭据标志,意味着出站请求会包含凭据。
- defer:可选。表示脚本可以延迟到文档完全被解析和显示之后再执行。只对外部脚本文件有效。 在 IE7 及更早的版本中,对行内脚本也可以指定这个属性。
- integrity:可选。允许比对接收到的资源和指定的加密签名以验证子资源完整性(SRI, Subresource Integrity)。如果接收到的资源的签名与这个属性指定的签名不匹配,则页面会报错, 脚本不会执行。这个属性可以用于确保内容分发网络(CDN,Content Delivery Network)不会提供恶意内容。
- language:废弃。最初用于表示代码块中的脚本语言(如"JavaScript"、"JavaScript 1.2" 或"VBScript")。大多数浏览器都会忽略这个属性,不应该再使用它。
- src:可选。表示包含要执行的代码的外部文件
- type:可选。代替 language,表示代码块中脚本语言的内容类型(也称 MIME 类型)。按照惯例,这个值始终都是"text/javascript",尽管"text/javascript"和"text/ecmascript" 都已经废弃了。JavaScript 文件的 MIME 类型通常是"application/x-javascript",不过给 type 属性这个值有可能导致脚本被忽略。在非 IE 的浏览器中有效的其他值还有 "application/javascript"和"application/ecmascript"。如果这个值是 module,则代 码会被当成 ES6 模块,而且只有这时候代码中才能出现 import 和 export 关键字。
元素的使用方式
使用< script >的方式有两种:
- 通过它直接在网页中嵌入 JavaScript 代码。
- 包含在< script>内的代码会被从上到下解释。在< script>元素中的代码被计算完成之前,页面的其余内容不会被加载,也不会被显示。
- 浏览器解析行内脚本的方式决定了它在看到字符串<\ scritp>时会将其当成结束的标签。想避免这个问题,只需要转义字符“\”即可
- 通过它在网页中包含外部 JavaScript 文件。
- 要包含外部文件中的 JavaScript,就必须使用 src 属性。这个属性的值是一个 URL,指向包含 JavaScript 代码的文件
- 与解释行内 JavaScript 一样,在解释外部 JavaScript 文件时,页面也会阻塞。(阻塞时间也包含下载文件的时间)
- 使用了 src 属性的< script>元素不应该再在< script>和</ script>标签中再包含其他 JavaScript 代码。如果两者都提供的话,则浏览器只会下载并执行脚本文件,从而忽略行内代码。
元素的特性
< script>元素的一个最为强大、同时也备受争议的特性是,它可以包含来自外部域的 JavaScript 文件。跟img元素很像,script元素的 src 属性可以是一个完整的 URL,而且这个 URL 指向的资源可以跟包含它的 HTML 页面不在同一个域中。
浏览器在解析这个资源时,会向 src 属性指定的路径发送一个 GET 请求,以取得相应资源,假定是一个 JavaScript 文件。这个初始的请求不受浏览器同源策略限制,但返回并被执行的 JavaScript 则受限制。当然,这个请求仍然受父页面 HTTP/HTTPS 协议的限制。
来自外部域的代码会被当成加载它的页面的一部分来加载和解释。这个能力可以让我们通过不同的域分发 JavaScript。不过,引用了放在别人服务器上的 JavaScript 文件时要格外小心,因为恶意的程序员随时可能替换这个文件。在包含外部域的 JavaScript 文件时,要确保该域是自己所有的,或者该域是一个可信的来源。
标签的位置
放在head
这种做法的主要目的是把外部的 CSS 和 JavaScript 文件都集中放到一起。
不过,把所有 JavaScript 文件都放在head里也就意味着必须把所有 JavaScript 代码都下载、解析和解释完成后,才能开始渲染页面(页面在浏览器解析到body的起始标签时开始渲染)。对于需要很多 JavaScript 的页面,这会导致页面渲染的明显延迟,在此期间浏览器窗口完全空白。
放在body
为了解决放在头部引起的白屏问题,现代 Web 应用程序通常将所有 JavaScript 引用放在body元素中的页面内容后面。
这样一来,页面会在处理 JavaScript 代码之前完全渲染页面。用户会感觉页面加载更快了,因为浏览器显示空白页面的时间短了。
推迟执行脚本
defer
这个属性表示脚本在执行的时候不会改变页面的结构。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。
因此在script元素上设置 defer 属性,相当于告诉浏览器立即下载,但延迟执行
这样一来,即使script元素在head中,也会等到浏览器解析到结束的< /html>标签后才执行。
HTML5 规范要求脚本应该按照它们出现的顺序执行,因此第一个推迟的脚 本会在第二个推迟的脚本之前执行,而且两者都会在 DOMContentLoaded 事件之前执行。不过在实际当中,推迟执行的脚本不一定总会按顺序执行或者在 DOMContentLoaded 事件之前执行,因此最好只包含一个这样的脚本。
async
从改变脚本处理方式上看,async 属性与 defer 类似。当然,它们两者也都只适用于外部脚本,都会告诉浏览器立即开始下载。不过,与 defer 不同的是,标记为 async 的脚本并不保证能按照它们出现的次序执行。
给脚本添加 async 属性的目的是告诉浏览器,不必等脚本下载和执行完后再加载页面,同样也不必等到该异步脚本下载和执行后再加载其他脚本。正因为如此,异步脚本不应该在加载期间修改 DOM,同时添加 async 属性的脚本之间不应该存在依赖关系。
其他添加脚本的方式
因为 JavaScript 可以使用 DOM API,所以通过 向 DOM 中动态添加 script 元素同样可以加载指定的脚本。只要创建一个 script 元素并将其添加到 DOM 即可:
let script = document.createElement('script');
script.src = 'gibberish.js';
document.head.appendChild(script);
默认情况下, 以这种方式创建的script元素是以异步方式加载的,相当于添加了 async 属性
不过这样做可能会有问题,因为所有浏览器都支持 createElement()方法,但不是所有浏览器都支持 async 属性。因此, 如果要统一动态脚本的加载行为,可以明确将其设置为同步加载:
let script = document.createElement('script');
script.src = 'gibberish.js';
script.async = false;
document.head.appendChild(script);
以这种方式获取的资源对浏览器预加载器是不可见的。这会严重影响它们在资源获取队列中的优先级。根据应用程序的工作方式以及怎么使用,这种方式可能会严重影响性能。要想让预加载器知道这些 动态请求文件的存在,可以在文档头部显式声明它们
<link rel="preload" href="gibberish.js">
XHTML与 HTML中的script元素的区别
可扩展超文本标记语言(XHTML,Extensible HyperText Markup Language)是将 HTML 作为 XML 的应用重新包装的结果。
与HTML相比,XHTML添加了以下要求:
- 在 XHTML 中使用 JavaScript 必须指定 type 属性且值为 text/javascript(HTML中可以没有这个属性)
- 特殊字符需要转义
- 在 HTML 中,解析< script >元素会使用特殊的规则,但是XHTML中没有这些规则。这意味着 a < b 语句中的小于号(<)会被解释成一个标签的开始,并且由于作为标签开始的小于号后面不能有空格, 这会导致语法错误
- 第一种解决方式是把所有小于号(<)都替换成对应的 HTML 实体形式(<)
- 第二种解决方式是把所有代码都包含到一个 CDATA 块中。在 XHTML(及 XML)中,CDATA 块表示 文档中可以包含任意文本的区块,其内容不作为标签来解析,因此可以在其中包含任意字符,包括小于号,并且不会引发语法错误。(注意:在兼容 XHTML 的浏览器中,这样能解决问题。但在不支持 CDATA 块的非 XHTML 兼容浏览器中 则不行。为此,CDATA 标记必须使用 JavaScript 注释来抵消)
<script type="text/javascript">
//<![CDATA[
function compare(a, b) {
if (a < b) {
console.log("A is less than B");
} else if (a > b) {
console.log("A is greater than B");
} else {
console.log("A is equal to B");
}
}
//]]>
</script>
行内的script元素和外部的js文件
推荐使用外部文件,理由如下。
- 可维护性。JavaScript 代码如果分散到很多 HTML 页面,会导致维护困难。而用一个目录保存所有 JavaScript 文件,则更容易维护,这样开发者就可以独立于使用它们的 HTML 页面来编辑代码。
- 缓存。浏览器会根据特定的设置缓存所有外部链接的 JavaScript 文件,这意味着如果两个页面都用到同一个文件,则该文件只需下载一次。这最终意味着页面加载更快。
- 适应未来。通过把 JavaScript 放到外部文件中,就不必考虑用 XHTML 或前面提到的注释黑科技。 包含外部 JavaScript 文件的语法在 HTML 和 XHTML 中是一样的。
在配置浏览器请求外部文件时,要重点考虑的一点是它们会占用多少带宽。在 SPDY/HTTP2 中, 预请求的消耗已显著降低:比如你在页面a用到了1,2,3三个js文件,在页面b用到了3,4,5三个文件,在页面a请求的时候,如果浏览器支持 SPDY/HTTP2,就可以从同一个地方取得一批文件,并将它们逐个放到浏览器缓存中。从浏览器角度看,通过 SPDY/HTTP2 获取所有这些独立的资源与获取一个大 JavaScript 文件的延迟差不多。在页面b请求时,由于你已经把应用程序切割成了轻量可缓存的文件,第二个页面也依赖的某些组件此时已经存在于浏览器缓存中了。
文档模式
IE5.5 发明了文档模式的概念,即可以使用 doctype 切换文档模式。最初的文档模式有两种:混杂模式(quirks mode)和标准模式(standards mode)。前者让 IE 像 IE5 一样(支持一些非标准的特性), 后者让 IE 具有兼容标准的行为。虽然这两种模式的主要区别只体现在通过 CSS 渲染的内容方面,但对 JavaScript 也有一些关联影响,或称为副作用。
IE 初次支持文档模式切换以后,其他浏览器也跟着实现了。随着浏览器的普遍实现,又出现了第三种文档模式:准标准模式(almost standards mode)。这种模式下的浏览器支持很多标准的特性,但是没 有标准规定得那么严格。主要区别在于如何对待图片元素周围的空白(在表格中使用图片时最明显)。
开启不同的文档模式
-
标准模式通过下列几种文档类型声明开启:
<!-- HTML 4.01 Strict --> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <!-- XHTML 1.0 Strict --> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <!-- HTML5 --> <!DOCTYPE html> -
准标准模式通过过渡性文档类型(Transitional)和框架集文档类型(Frameset)来触发:
<!-- HTML 4.01 Transitional --> <!DOCTYPE HTML PUBLIC 5 "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <!-- HTML 4.01 Frameset --> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd"> <!-- XHTML 1.0 Transitional --> <!DOCTYPE html PUBLIC\ "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <!-- XHTML 1.0 Frameset --> <!DOCTYPE html PUBLIC\ "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd"> -
混杂模式在所有浏览器中都以省略文档开头的 doctype 声明作为开关。
通常来讲,人们所说的标准模式就是除了混杂模式之外其他的模式。
< noscript>
用于在浏览器不支持JavaScript时降级显示,其中可以包含任何出现在body中的html元素,在以下两个条件任一被满足的情况下< noscript>中的内容会显示:
- 浏览器不支持脚本;
- 浏览器对脚本的支持被关闭
基础知识
语法
标识符
所谓标识符,就是变量、函数、属性或函数参数的名称。标识符可以由一或多个下列字符组成:
- 第一个字符必须是一个字母、下划线(_)或美元符号($);
- 剩下的其他字符可以是字母、下划线、美元符号或数字。
变量
var
-
var 声明作用域
使用 var 操作符定义的变量会成为包含它的函数的局部变量。比如,使用 var 在一个函数内部定义一个变量,就意味着该变量将在函数退出时被销毁。
在函数内定义变量时省 略 var 操作符,可以创建一个全局变量。
注意:虽然可以通过省略var操作符定义全局变量,但不推荐这么做。在局部作用域中定 义的全局变量很难维护,也会造成困惑。这是因为不能一下子断定省略 var 是不是有意而 为之。在严格模式下,如果像这样给未声明的变量赋值,则会导致抛出 ReferenceError。
-
var 声明提升
使用这个关键字声明的变量会自动提升到函数作用域顶部。
所谓的“提升”(hoist),也就是把所有变量声明都拉到函数作用域的顶部。此外,反复多次使用 var 声明同一个变量也没有问题。
let
let 跟 var 的作用差不多,但有着非常重要的区别:
- let 声明的范围是块作用域, 而 var 声明的范围是函数作用域
- let 也不允许同一个块作用域中出现重复声明
- 暂时性死区:let 声明的变量不会在作用域中被提升。在 let 声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出 ReferenceError。
- 使用 let 在全局作用域中声明的变量不会成为 window 对象的属性(var 声 明的变量则会)。let 声明仍然是在全局作用域中发生的,相应变量会在页面的生命周期内存续。
- let不能依赖条件声明模式。注意 不能使用let进行条件式声明是件好事,因为条件声明是一种反模式,它让程序变 得更难理解。如果你发现自己在使用这个模式,那一定有更好的替代方式。
- 在 let 出现之前,for 循环定义的迭代变量会渗透到循环体外部,改成使用 let 之后,这个问题就消失了,因为迭代变量的作用域仅限于 for 循环块内部
- let在循环中每次迭代声明一个独立变量实例
const
const 的行为与 let 基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改 const 声明的变量会导致运行时错误。
const 声明的限制只适用于它指向的变量的引用。换句话说,如果 const 变量引用的是一个对象, 那么修改这个对象内部的属性并不违反 const 的限制。
但是不能用 const 来声明迭代变量(因为迭代变量会自增)。
如果你只想用 const 声明一个不会被修改的 for 循环变量,那也是可以的。也就是说,每 次迭代只是创建一个新变量。这对 for-of 和 for-in 循环特别有意义:
let i = 0;
for (const j = 7; i < 5; ++i) {
console.log(j);
}
// 7, 7, 7, 7, 7
for (const key in {a: 1, b: 2}) {
console.log(key);
}
// a, b
for (const value of [1,2,3,4,5]) {
console.log(value);]
}
// 1, 2, 3, 4, 5
如何选择三种变量声明方式
- 不使用 var
- const 优先,let 次之
数据类型
ECMAScript 有 6 种简单数据类型(也称为原始类型):
- Undefined
- Null
- Boolean
- Number
- String
- Symbol。Symbol(符号)是 ECMAScript 6 新增的。
还有一种复杂数据类型叫 Object(对 象)。Object 是一种无序名值对的集合。
typeof
typeof用于确定变量的数据类型,对一个值使用 typeof 操作符会返回下列字符串之一:
- "undefined"表示值未定义;
- "boolean"表示值为布尔值;
- "string"表示值为字符串;
- "number"表示值为数值;
- "object"表示值为对象(而不是函数)或 null;
- "function"表示值为函数;
- "symbol"表示值为符号。
调用 typeof null 返回的是"object"。这是因为特殊值 null 被认为是一个对空对象的引用。
严格来讲,函数在ECMAScript中被认为是对象,并不代表一种数据类型。可是, 函数也有自己特殊的属性。为此,就有必要通过 typeof 操作符来区分函数和其他对象。
Undefined类型
Undefined 类型只有一个值,就是特殊值 undefined。当使用 var 或 let 声明了变量但没有初始 化时,就相当于给变量赋予了 undefined 值。
一般来说,永远不用显式地给某个变量设置 undefined 值。字面值 undefined 主要用于比较,而且在 ECMA-262 第 3 版之前是不存在的。增加这个特殊值的目的就是为 了正式明确空对象指针(null)和未初始化变量的区别。
对未声明的变量,只能执行一个有用的操作,就是对它调用 typeof。(对未声明的变量调用 delete 也不会报错,但这个操作没什么用, 实际上在严格模式下会抛出错误。)
无论是声明未赋值的变量还是未声明的变量,typeof 返回的都是字符串"undefined"。
Null类型
Null 类型同样只有一个值,即特殊值 null。逻辑上讲,null 值表示一个空对象指针,这也是给 typeof 传一个 null 会返回"object"的原因。
即使 null 和 undefined 有关系,它们的用途也是完全不一样的。如前所述,永远不必显式地将变量值设置为 undefined。但 null 不是这样的。任何时候,只要变量要保存对象,而当时又没有那个对象可保存,就要用 null 来填充该变量。这样就可以保持 null 是空对象指针的语义,并进一步将其与 undefined 区分开来。
Boolean类型
有两个字面值:true 和 false。
虽然布尔值只有两个,但所有其他 ECMAScript 类型的值都有相应布尔值的等价形式。要将一个其他类型的值转换为布尔值,可以调用特定的 Boolean()转型函数。
不同类型与布尔值之间的转换规则:
- ""(空字符串)、 0、NaN、 null、undefined转换为false
- 其余全部转换成true
理解以上转换非常重要,因为像 if 等流控制语句会自动执行其他类型值到布尔值的转换。
Number类型
Number 类型使用 IEEE 754 格式表示整 数和浮点值(在某些语言中也叫双精度值)。
因为存储浮点值使用的内存空间是存储整数值的两倍,所以 ECMAScript 总是想方设法把值转换为整数。在小数点后面没有数字的情况下,数值就会变成整数。类似地,如果数值本身就是整数,只是小数点后面跟着 0(如 1.0),那它也会被转换为整数。
例如,0.1 加 0.2 得到的不是 0.3,而是 0.300 000 000 000 000 04。如果两个数值分别是 0.05 和 0.25,或者 0.15 和 0.15,那没问 题。但如果是 0.1 和 0.2,如前所述,测试将失败。 注意 之所以存在这种舍入错误,是因为使用了IEEE754数值,这种错误并非ECMAScript 所独有。其他使用相同格式的语言也有这个问题。
有 3 个函数可以将非数值转换为数值:Number()、parseInt()和 parseFloat()。
Number
Number()函数基于如下规则执行转换:(一元加操作符与Number()函数遵循相同的转换规则。)
-
布尔值,true 转换为 1,false 转换为 0。
-
数值,直接返回。
-
null,返回 0。
-
undefined,返回 NaN。
-
字符串,应用以下规则。
-
如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值。 因此,Number("1")返回 1,Number("123")返回 123,Number("011")返回 11(忽略前面 的零)。
-
如果字符串包含有效的浮点值格式如"1.1",则会转换为相应的浮点值(同样,忽略前面的零)。
-
如果字符串包含有效的十六进制格式如"0xf",则会转换为与该十六进制值对应的十进制整数值。
-
如果是空字符串(不包含字符),则返回 0。
-
如果字符串包含除上述情况之外的其他字符,则返回 NaN。
-
-
对象,调用 valueOf()方法,并按照上述规则转换返回的值。如果转换结果是 NaN,则调用 toString()方法,再按照转换字符串的规则转换
parseInt
parseInt()函数更专注于字符串是否包含数值模式。字符串最前面的空格会被忽略,从第一个非空格字符开始转换。如果第一个字符不是数值字符、加号或减号,parseInt()立即返回 NaN。这意味着空字符串也会返回 NaN(这一点跟 Number()不一样,它返回 0)。如果第一个字符 是数值字符、加号或减号,则继续依次检测每个字符,直到字符串末尾,或碰到非数值字符。比如, "1234blue"会被转换为 1234,因为"blue"会被完全忽略。类似地,"22.5"会被转换为 22,因为小数 6 点不是有效的整数字符。
假设字符串中的第一个字符是数值字符,parseInt()函数也能识别不同的整数格式(十进制、八 进制、十六进制)。换句话说,如果字符串以"0x"开头,就会被解释为十六进制整数。
parseInt()也接收第二个参数,用于指定底数(进制数)
parseFloat
parseFloat()函数的工作方式跟 parseInt()函数类似,都是从位置 0 开始检测每个字符。同样, 它也是解析到字符串末尾或者解析到一个无效的浮点数值字符为止。这意味着第一次出现的小数点是有效的,但第二次出现的小数点就无效了,此时字符串的剩余字符都会被忽略。
parseFloat()函数的另一个不同之处在于,它始终忽略字符串开头的零。十六进制数值始终会返回 0。因为 parseFloat()只解析十进制值,因此不能指定底数。
NaN
意思是“不是数值”(Not a Number),用于表示本来要返回数值的操作失败了(而不是抛出错误)。比如,用 0 除任意数值在其他语言中通常都会导致错误,从而终止代码执行。但在 ECMAScript 中,0、+0 或-0 相除会返回 NaN。
NaN的特殊属性:
- 任何涉及 NaN 的操作始终返回 NaN
- NaN 不等于包括 NaN 在内的任何值
ECMAScript 提供了 isNaN()函数,该函数接收一个参数,可以是任意数据类型,然后判断这个参数是否“不是数值”。
注意 虽然不常见,但isNaN()可以用于测试对象。此时,首先会调用对象的valueOf() 方法,然后再确定返回的值是否可以转换为数值。如果不能,再调用 toString()方法, 并测试其返回值。这通常是 ECMAScript 内置函数和操作符的工作方式
string
字符串。
toString()
toString()方法可见于数值、布尔值、对象和字符串值。(没错,字符串值也有 toString()方法, 该方法只是简单地返回自身的一个副本。)null 和 undefined 值没有 toString()方法。
多数情况下,toString()不接收任何参数。不过,在对数值调用这个方法时,toString()可以接收一个底数参数,即以什么底数来输出数值的字符串表示。默认情况下,toString()返回数值的十 进制字符串表示。而通过传入参数,可以得到数值的二进制、八进制、十六进制,或者其他任何有效基 数的字符串表示。
String()
如果你不确定一个值是不是 null 或 undefined,可以使用 String()转型函数,它始终会返回表 示相应类型值的字符串。String()函数遵循如下规则:
- 如果值有 toString()方法,则调用该方法(不传参数)并返回结果。
- 如果值是 null,返回"null"。
- 如果值是 undefined,返回"undefined"。
用加号操作符给一个值加上一个空字符串""也可以将其转换为字符串
模版字符串
模版字符串是es6新提出的,字符串插值通过在${}中使用一个 JavaScript 表达式实现(在插值表达式中可以调用函数和方法),更加方便。
标签函数
模板字面量也支持定义标签函数(tag function),而通过标签函数可以自定义插值行为。标签函数 会接收被插值记号分隔后的模板和对每个表达式求值的结果。
let a = 6;
let b = 9;
// 因为表达式参数的数量是可变的,所以通常应该使用剩余操作符(rest operator)将它们收集到一个数组中
function simpleTag(strings, aValExpression, bValExpression, sumExpression) { console.log(strings);
console.log(aValExpression);
console.log(bValExpression);
console.log(sumExpression);
return 'foobar';
}
let untaggedResult = `${ a } + ${ b } = ${ a + b }`;
let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`;
// ["", " + ", " = ", ""]
// 6
// 9
// 15
console.log(untaggedResult);
console.log(taggedResult);
// "6 + 9 = 15"
// "foobar"
symbol
Symbol(符号)是 ECMAScript 6 新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。 符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
调用 Symbol()函数时,也可以传入一个字符串参数作为对符号的描述(description),将来可以通 过这个字符串来调试代码。但是,这个字符串参数与符号定义或标识完全无关:
let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(genericSymbol == otherGenericSymbol); // false
console.log(fooSymbol == otherFooSymbol); // false
Symbol()函数不能与 new 关键字一起作为构造函数使用。这样做是为了避免创建符号包装对象,经过new包装后,typeof返回object。
全局符号注册表
Symbol.for()对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。
全局注册表中的符号必须使用字符串键来创建,因此作为参数传给 Symbol.for()的任何值都会被转换为字符串。同时Symbol('foo')!==Symbol.for('foo')。
使用 Symbol.keyFor()来查询全局注册表,这个方法接收符号,返回该全局符号对应的字 符串键。如果查询的不是全局符号,则返回 undefined。
使用符号作为属性
凡是可以使用字符串或数值作为属性的地方,都可以使用符号。这就包括了对象字面量属性和 Object.defineProperty()/Object.defineProperties()定义的属性。
但是
对象字面量只能在计算属性语法中使用符号作为属性,也就是说不能这样用:obj.symbolVal = val,只能这样用:obj[symbolVal]=val。
如何获取对象中的属性:
- Object.getOwnPropertyNames()返回对象实例的常规属性数组
- Object.getOwnPropertySymbols()返回对象实例的符号属性数组。和getOwnPropertyNames方法的返回值彼此互斥。
- Object.getOwnPropertyDescriptors()会返回同时包含常规和符号属性描述符的对象。
- Reflect.ownKeys()会返回两种类型的键的数组
因为符号属性是对内存中符号的一个引用,所以直接创建并用作属性的符号不会丢失。但是,如果 没有显式地保存对这些属性的引用,那么必须遍历对象的所有符号属性才能找到相应的属性键:
let o = {
[Symbol('foo')]: 'foo val',
[Symbol('bar')]: 'bar val'
};
console.log(o); // {Symbol(foo): "foo val", Symbol(bar): "bar val"}
let barSymbol = Object.getOwnPropertySymbols(o) .find((symbol) => symbol.toString().match(/bar/));
console.log(barSymbol); // Symbol(bar)
常用内置符号
ECMAScript 6 也引入了一批常用内置符号(well-known symbol),用于暴露语言内部行为,开发者 可以直接访问、重写或模拟这些行为。这些内置符号都以 Symbol 工厂函数字符串属性的形式存在。 这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为。比如,我们知道 for-of 循环会在相关对象上使用 Symbol.iterator 属性,那么就可以通过在自定义对象上重新定义 Symbol.iterator 的值,来改变 for-of 在迭代该对象时的行为。
- Symbol.asyncIterator:一个方法,该方法返回对象默认的 AsyncIterator。 由 for-await-of 语句使用
- Symbol.hasInstance:一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由 instanceof 操作符使用
- Symbol.isConcatSpreadable:一个布尔值,如果是 true,则意味着对象应 该用 Array.prototype.concat()打平其数组元素
- Symbol.iterator:一个方法,该方法返回对象默认的迭代器。 由 for-of 语句使用
- Symbol.match:一个正则表达式方法,该方法用正则表达式 去匹配字符串。由 String.prototype.match()方法使用
- Symbol.replace:“一个正则表达式方法,该方法替换一个字符 串中匹配的子串。由 String.prototype.replace()方法使用
- Symbol.search:一个正则表达式方法,该方法返回字符串中 匹配正则表达式的索引。由 String.prototype.search()方法使用。
- Symbol.species:一个函数值,该函数作为创建派生对象的构 造函数
- Symbol.split:一个正则表达式方法,该方法在匹配正则表 达式的索引位置拆分字符串。由 String.prototype.split()方法使用
- Symbol.toPrimitive:一个方法,该方法将对象转换为相应的原始值。由 ToPrimitive 抽象操作使用
- Symbol.toStringTag:一个字符串,该字符串用于创建对象的默认 字符串描述。由内置方法 Object.prototype.toString()使用
- Symbol.unscopables:一个对象,该对象所有的以及继承的属性, 都会从关联对象的 with 环境绑定中排除
object
每个 Object 实例都有如下属性和方法。
- constructor:用于创建当前对象的函数。
- hasOwnProperty(propertyName):用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查的属性名必须是字符串(如 o.hasOwnProperty("name"))或符号。
- isPrototypeOf(object):用于判断当前对象是否为另一个对象的原型。
- propertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用for-in 语句枚举。与 hasOwnProperty()一样,属性名必须是字符串。
- toLocaleString():返回对象的字符串表示,该字符串反映对象所在的本地化执行环境。
- toString():返回对象的字符串表示。
- valueOf():返回对象对应的字符串、数值或布尔值表示。通常与 toString()的返回值相同。
操作符
递增/递减操作符
let num = 20;
++num // num=num+1----->21
--num //num=num-1--->20
无论使用前缀递增还是前缀递减操作符,变量的值都会在语句被求值之前改变。(在计算机科学中, 这通常被称为具有副作用。)
后缀版与前缀版的主要 区别在于,后缀版递增和递减在语句被求值后才发生。在某些情况下,这种差异没什么影响,可是,在跟其他操作混合时,差异就会变明显,比如:
let num1 = 2;
let num2 = 20;
let num3 = num1-- + num2;
let num4 = num1 + num2;
console.log(num3); // 22
console.log(num4); // 21
不同数据类型上使用递增/递减操作符
这 4 个操作符可以作用于任何值,意思是不限于整数——字符串、布尔值、浮点值,甚至对象都可以。递增和递减操作符遵循如下规则。
- 对于字符串,如果是有效的数值形式,则转换为数值再应用改变。变量类型从字符串变成数值。
- 对于字符串,如果不是有效的数值形式,则将变量的值设置为 NaN 。变量类型从字符串变成数值。
- 对于布尔值,如果是 false,则转换为 0 再应用改变。变量类型从布尔值变成数值。
- 对于布尔值,如果是 true,则转换为 1 再应用改变。变量类型从布尔值变成数值。
- 对于浮点值,加 1 或减 1。
- 如果是对象,则调用其valueOf()方法取得可以操作的值。对得到的值应用上述规则。如果是 NaN,则调用 toString()并再次应用其他规则。变量类型从对象变成数值。
一元加和减
一元加和减操作符对大多数开发者来说并不陌生,它们在 ECMAScript 中跟在高中数学中的用途一 样。一元加由一个加号(+)表示,放在变量前头,对数值没有任何影响。
let num = 25;
num = +num;
console.log(num); // 25
如果将一元加应用到非数值,则会执行与使用 Number()转型函数一样的类型转换。
一元减由一个减号(-)表示,放在变量前头,主要用于把数值变成负值。
布尔操作符
布尔操作符一共有 3 个:逻辑非、逻辑与和逻辑或。
逻辑非
逻辑非操作符由一个叹号(!)表示,可应用给 ECMAScript 中的任何值。这个操作符始终返回布尔值,无论应用到的是什么数据类型。逻辑非操作符首先将操作数转换为布尔值,然后再对其取反。换句话说,逻辑非操作符返回的结果和Boolean(val)是相反的。
所以逻辑非操作符也可以用于把任意值转换为布尔值。同时使用两个叹号(!!),相当于调用了转型函数 Boolean()
逻辑与
逻辑与操作符由两个和号(&&)表示。只有前后两个操作数都为true时结果才为true,否则为false。
逻辑与操作符可用于任何类型的操作数,不限于布尔值。如果有操作数不是布尔值,则逻辑与并不一定会返回布尔值,而是遵循如下规则:
- 如果第一个操作数是对象,则返回第二个操作数。
- 如果第二个操作数是对象,则只有第一个操作数求值为 true 才会返回该对象。(注意是返回对象而不是true)
- 如果两个操作数都是对象,则返回第二个操作数(返回第二个对象而不是true)。
- 如果有一个操作数是 null,则返回 null。
- 如果有一个操作数是 NaN,则返回 NaN。
- 如果有一个操作数是 undefined,则返回 undefined。
逻辑与操作符是一种短路操作符,意思就是如果第一个操作数决定了结果,那么永远不会对第二个操作数求值。对逻辑与操作符来说,如果第一个操作数是 false,那么无论第二个操作数是什么值,结果也不可能等于 true。第二个操作数也不会计算和执行。
在代码中经常会用到这个特性,简单来说:
let result = null;
// 调用后端接口请求某些数据,返回后赋值给result,需要遍历result进行某些操作
// 但是后端返回的可能是个空数组,也可能是null,经常会如下判断
if(result && result.length){
//forEach...
}
// result为null的时候,如果直接上来就result.length会报错,
// 但是前边加个result,当result为null的时候,第二个操作数根本不会执行
再简单来说,
let a = true
a&&console.log('只有a为true时这段文字的才会被打印')
逻辑或
逻辑或操作符由两个管道符(||)表示,两个操作数中有一个为true则为true。
与逻辑与类似,如果有一个操作数不是布尔值,那么逻辑或操作符也不一定返回布尔值。它遵循如下规则。
- 如果第一个操作数是对象,则返回第一个操作数。
- 如果第一个操作数求值为 false,则返回第二个操作数。
- 如果两个操作数都是对象,则返回第一个操作数。
- 如果两个操作数都是 null,则返回 null。
- 如果两个操作数都是 NaN,则返回 NaN。
- 如果两个操作数都是 undefined,则返回 undefined。
同样与逻辑与类似,逻辑或操作符也具有短路的特性。只不过对逻辑或而言,第一个操作数求值为true,第二个操作数就不会再被求值了。
用这个行为,可以避免给变量赋值 null 或 undefined。比如:
let myObject = preferredObject || backupObject;
在这个例子中,变量 myObject 会被赋予两个值中的一个。其中,preferredObject 变量包含首选的值,backupObject 变量包含备用的值。如果 preferredObject 不是 null,则它的值就会赋给 myObject;如果 preferredObject 是 null,则 backupObject 的值就会赋给 myObject。这种模式在 ECMAScript 代码中经常用于变量赋值。
加性操作符
加法操作符
如果两个操作数都是数值,加法操作符执行加法运算;
如果有一个操作数是字符串,则要应用如下规则:
- 如果两个操作数都是字符串,则将第二个字符串拼接到第一个字符串后面;
- 如果只有一个操作数是字符串,则将另一个操作数转换为字符串,再将两个字符串拼接在一起。
如果有任一操作数是对象、数值或布尔值,则调用它们的 toString()方法以获取字符串,然后再应用前面的关于字符串的规则。对于 undefined 和 null,则调用 String()函数,分别获取 "undefined"和"null"。
所以执行加法操作时最好想一想数据的类型是不是Number,比如:
const str = 3.1415926.toFixed(2)
console.log(str+1)//3.141
相等操作符
存在两组操作符。第一组是等于和不等于,它们在比较之前执行转换。第二组是全等和不全等,它们在比较之前不执行转换。
等于和不等于
等于操作符用两个等于号(==)表示,如果操作数相等,则会返回 true。不等于操作符用叹号和等于号(!=)表示,如果两个操作数不相等,则会返回 true。这两个操作符都会先进行类型转换(通常称为强制类型转换)再确定操作数是否相等。
在转换操作数的类型时,相等和不相等操作符遵循如下规则。 3
-
如果任一操作数是布尔值,则将其转换为数值再比较是否相等。false 转换为 0,true 转换为 1。
-
如果一个操作数是字符串,另一个操作数是数值,则尝试将字符串转换为数值,再比较是否相等。
-
如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf()方法取得其原始值,再根据前面的规则进行比较。
在进行比较时,这两个操作符会遵循如下规则:
- null 和 undefined 相等。
- null 和 undefined 不能转换为其他类型的值再进行比较。
- 如果有任一操作数是 NaN,则相等操作符返回 false,不相等操作符返回 true。记住:即使两个操作数都是 NaN,相等操作符也返回 false,因为按照规则,NaN 不等于 NaN。
- 如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回 true。否则,两者不相等。
全等和不全等
全等和不全等操作符与相等和不相等操作符类似,只不过它们在比较相等时不转换操作数。全等操 作符由 3 个等于号(===)表示,只有两个操作数在不转换的前提下相等才返回 true。
全等遵循如下规则:
- 不同类型返回false
- 一方是NaN则返回false
语句
- if语句
- do-while语句:后测试循环语句,即循环体中的代码执行后才会对退出条件进行求值。换句话说,循环体内的代码至少执行一次。
- while语句
- for语句:无法通过 while 循环实现的逻辑,同样也无法使用 for 循环实现。因此 for 循环只是将循环相关 的代码封装在了一起而已
- for-in语句:用于枚举对象中的非符号键属性,如果 for-in 循环要迭代的变量是 null 或 undefined,则不执行循环体。ECMAScript 中对象的属性是无序的,因此 for-in 语句不能保证返回对象属性的顺序。
- for-of语句:用于遍历可迭代对象的元素。
- break和continue语句:break语句用于立即退出循环,强制执行循环后的下一条语句。而 continue 语句也用于立即退出循环,但会再次从循环顶部开始执行
- with语句:with 语句的用途是将代码作用域设置为特定的对象
- switch语句:为避免不必要的条件判断,最好给每个条件后面都加上 break 语句。如果确实需要连续匹配几个 条件,那么推荐写个注释表明是故意忽略了 break。switch 语句在比较每个条件的值时会使用全等操作符,因此不会强制转换数据类 型
在for-in和for-of中定义循环变量推荐使用const,for循环中推荐使用let。
原始值与引用值
前文介绍了数据类型,数据类型分为两类:基本数据类型和引用数据类型。
ECMAScript 变量可以包含两种不同类型的数据:原始值和引用值。原始值(primitive value)就是 最简单的数据,引用值(reference value)则是由多个值构成的对象。
保存原始值的变量是按值(by value)访问的,因为我们操作的就是存储在变量中的实际值。引用值是保存在内存中的对象。与其他语言不同,JavaScript 不允许直接访问内存位置,因此也就不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的引用(reference)而非实际的对象本身。为此,保存引用值的变量是按引用(by reference)访问的。
原始类型的初始化可以只使用原始字面量形式。如果使用的是 new 关键字,则 JavaScript 会创建一个 Object 类型的实例(typeof会返回object),但其行为类似原始值。
复制值
除了存储方式不同,原始值和引用值在通过变量复制时也有所不同。
在通过变量把一个原始值赋值给另一个变量时,原始值会被复制到新变量的位置,这两个变量可以独立使用,互不干扰。
在把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。区别在于,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。操作完成后,两个变量实际上指向同一个对象,因此一个对象上面的变化会在另一个对象上反映出来。
传递参数
ECMAScript 中所有函数的参数都是按值传递的。这意味着函数外的值会被复制到函数内部的参数中,就像从一个变量复制到另一个变量一样。
在按值传递参数时,值会被复制到一个局部变量(即一个命名参数,或者用 ECMAScript 的话说, 就是 arguments 对象中的一个槽位)。在按引用传递参数时,值在内存中的位置会被保存在一个局部变量,这意味着对本地变量的修改会反映到函数外部。
既然会反映到函数外部,那怎么还说所有的函数参数都是按照值传递的呢?不应该是基本数据类型按照值传递,引用数据类型按照引用传递的吗?来看这个例子:
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 在函数内部被重写时,它变成了一个指向本地对象的指针。而那个本地对象在函数执行结束时就被销毁了。
确定类型
typeof
typeof 操作符最适合用来判断一个变量是否为原始类型。更确切地说,它是判断一个变量是否为字符串、数值、布尔值或 undefined 的最好方式。如果值是对象或 null,那么 typeof 返回"object"。
instanceof
typeof 虽然对原始值很有用,但它对引用值的用处不大。我们通常不关心一个值是不是对象, 而是想知道它是什么类型的对象。
variable instanceof constructor如果变量是给定引用类型的实例,则 instanceof 操作符返回 true。
所有引用值都是 Object 的实例,因此通过 instanceof 操作符检测任何引用值和 Object 构造函数都会返回 true。类似地,如果用 instanceof 检测原始值,则始终会返回 false, 因为原始值不是对象。
执行上下文与作用域
上下文
执行上下文(以下简称“上下文”)的概念在 JavaScript 中是颇为重要的。变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。
每个上下文都有一个关联的变量对象(variable object), 而这个上下文中定义的所有变量和函数都存在于这个对象上。虽然无法通过代码访问变量对象,但后台处理数据会用到它。
上下文分为全局上下文,函数上下文。
全局上下文是最外层的上下文。根据 ECMAScript 实现的宿主环境,表示全局上下文的对象可能不一样。在浏览器中,全局上下文就是我们常说的 window 对象,因此所有通过 var 定义的全局变量和函数都会成为 window 对象的属性和方法。使用 let 和 const 的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)。
每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。 在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。ECMAScript 程序的执行流就是通过这个上下文栈进行控制的。
作用域
上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域链的最前端。如果上下文是函数,则其活动对象(activation object)用作变量对象。活动对象最初只有一个定义变量:arguments。(全局上下文中没有这个变量。)作用域链中的下一个变量对象来自包含上下文,再下一个对象来自再下一个包含上下文。以此类推直至全局上下文;全局上下文的变量对象始终是作用域链的最后一个变量对象。
代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。搜索过程始终从作用域链的最前端开始,然后逐级往后,直到找到标识符。(如果没有找到标识符,那么通常会报错。)
作用域链增强
虽然执行上下文主要有全局上下文和函数上下文两种(eval()调用内部存在第三种上下文),但有 其他方式来增强作用域链。某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执 行后会被删除。通常在两种情况下会出现这个现象,即代码执行到下面任意一种情况时:
- try/catch 语句的 catch 块
- with 语句
这两种情况下,都会在作用域链前端添加一个变量对象。对 with 语句来说,会向作用域链前端添加指定的对象(with的对象);对 catch 语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明。
比如:
function buildUrl() {
let qs = "?debug=true";
with(location){
let url = href + qs;
}
return url;
}
这里,with 语句将 location 对象作为上下文,因此 location 会被添加到作用域链前端。 buildUrl()函数中定义了一个变量 qs。当 with 语句中的代码引用变量 href 时,实际上引用的是 location.href,也就是自己变量对象的属性。在引用 qs 时,引用的则是定义在 buildUrl()中的那 个变量,它定义在函数上下文的变量对象上。而在 with 语句中使用 var 声明的变量 url 会成为函数 上下文的一部分,可以作为函数的值被返回;但像这里使用let声明的变量url,因为被限制在块级作 用域(稍后介绍),所以在 with 块之外没有定义。
变量声明
使用 var 的函数作用域声明
在使用 var 声明变量时,变量会被自动添加到最接近的上下文。在函数中,最接近的上下文就是函数的局部上下文。在 with 语句中,最接近的上下文也是函数上下文。如果变量未经声明就被初始化了, 那么它就会自动被添加到全局上下文。
var 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前。这个现象叫作“提升” (hoisting)。提升让同一作用域中的代码不必考虑变量是否已经声明就可以直接使用。可是在实践中,提升也会导致合法却奇怪的现象,即在变量声明之前使用变量。
使用 let 的块级作用域声明
ES6 新增的 let 关键字跟 var 很相似,但它的作用域是块级的,这也是 JavaScript 中的新概念。块级作用域由最近的一对包含花括号{}界定。换句话说,if 块、while 块、function 块,甚至连单独 的块也是 let 声明变量的作用域。
let 与 var 的另一个不同之处是在同一作用域内不能声明两次。重复的 var 声明会被忽略,而重复的 let 声明会抛出 SyntaxError。
let 的行为非常适合在循环中声明迭代变量。使用 var 声明的迭代变量会泄漏到循环外部,这种情况应该避免。
let 在 JavaScript 运行时中也会被提升,但由于“暂时性死区”(temporal dead zone)的缘故,实际上不能在声明之前使用 let 变量。因此,从写 JavaScript 代码的角度说,let 的提升跟 var 是不一样的。
使用 const 的常量声明
使用 const 声明的变量必须同时初始化为某个值。 一经声明,在其生命周期的任何时候都不能再重新赋予新值。
赋值为对象的 const 变量不能再被重新赋值为其他引用值,但对象的键则不受限制。
垃圾回收
基本思路很简单:确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动运行。
标记清理
JavaScript 最常用的垃圾回收策略是标记清理(mark-and-sweep)。当变量进入上下文,比如在函数内部声明一个变量时,这个变量会被加上存在于上下文中的标记。而在上下文中的变量,逻辑上讲,永远不应该释放它们的内存,因为只要上下文中的代码在运行,就有可能用到它们。当变量离开上下文时,也会被加上离开上下文的标记。
给变量加标记的方式有很多种。比如,当变量进入上下文时,反转某一位;或者可以维护“在上下文中”和“不在上下文中”两个变量列表,可以把变量从一个列表转移到另一个列表。标记过程的实现并不重要,关键是策略。
垃圾回收程序运行的时候,会标记内存中存储的所有变量(记住,标记方法有很多种)。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。
引用计数
另一种没那么常用的垃圾回收策略是引用计数(reference counting)。其思路是对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为 1。如果同一个值又被赋给另一个变量,那么引用数加 1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。当一个值的引用数为 0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序下次运行的时候就会释放引用数为 0 的值的内存。
循环引用
引用计数最早由 Netscape Navigator 3.0 采用,但很快就遇到了严重的问题:循环引用。所谓循环引用,就是对象 A 有一个指针指向对象 B,而对象 B 也引用了对象 A。比如:
function problem(){
let a = {},b={};
a.something = b;
b.something = a;
}
当函数执行完毕之后,原则上来讲其中声明的变量a和b都应该被清理,但是由于两者的引用次数都是2不为0,所以引用计数法永远也不会清除这两个变量。如果函数被多次调用,那么大量的内存永远不会被释放。
事实上,引用计数策略的问题还不止于此。
在 IE8 及更早版本的 IE 中,并非所有对象都是原生 JavaScript 对象。BOM 和 DOM 中的对象是 C++ 实现的组件对象模型(COM,Component Object Model)对象,而 COM 对象使用引用计数实现垃圾回 收。因此,即使这些版本 IE 的 JavaScript 引擎使用标记清理,JavaScript 存取的 COM 对象依旧使用引用计数。换句话说,只要涉及 COM 对象,就无法避开循环引用问题。为避免类似的循环引用问题,应该在确保不使用的情况下切断原生 JavaScript 对象与 DOM 元素之 间的连接。
内存管理
优化内存占用的最佳手段就是保证在执行代码时只保存必要的数据。如果数据不再必要,那么把它设置为 null,从而释放其引用。这也可以叫作解除引用。这个建议最适合全局变量和全局对象的属性。局部变量在超出作用域后会被自动解除引用。
通过 const 和 let 声明提升性能
ES6 增加这两个关键字不仅有助于改善代码风格,而且同样有助于改进垃圾回收的过程。因为 const和 let 都以块(而非函数)为作用域,所以相比于使用 var,使用这两个新关键字可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存。在块作用域比函数作用域更早终止的情况下,这就有可能发生。
隐藏类和删除操作
V8 在将解释后的 JavaScript 代码编译为实际的机器码时会利用“隐藏类”。运行期间,V8 会将创建的对象与隐藏类关联起来,以跟踪它们的属性特征。能够共享相同隐藏类 的对象性能会更好,V8 会针对这种情况进行优化,但不一定总能够做到。比如下面的代码:
function Article() {
this.title = 'Inauguration Ceremony Features Kazoo Band';
}
let a1 = new Article();
let a2 = new Article();
V8 会在后台配置,让这两个类实例共享相同的隐藏类,因为这两个实例共享同一个构造函数和原型。假设之后又添加了下面这行代码:
a2.author = 'Jake';
此时两个 Article 实例就会对应两个不同的隐藏类。根据这种操作的频率和隐藏类的大小,这有可能对性能产生明显影响。
解决方案就是避免 JavaScript 的“先创建再补充”(ready-fire-aim)式的动态属性赋值,并在构造函数中一次性声明所有属性。
动态删除属性与动态添加属性导致的后果一样(导致不再共用隐藏类)。最佳实践是把不想要的属性设置为 null。这样可以保持隐藏类不变和继续共享,同时也能达到删除引用值供垃圾回收程序回收的效果。
内存泄漏
- 意外声明全局变量:没有使用任何关键字声明变量(或者说是非严格模式下未声明直接赋值的变量)
- 定时器
- 闭包
静态分配与对象池
开发者无法直接控制什么时候开始收集垃圾,但可以间接控制触发垃圾回收的条件。理论上,如果能够合理使用分配的内存,同时避免多余的垃圾回收,那就可以保住因释放内存而损失的性能。
浏览器决定何时运行垃圾回收程序的一个标准就是对象更替的速度。如果有很多对象被初始化,然后一下子又都超出了作用域,那么浏览器就会采用更激进的方式调度垃圾回收程序运行,这样当然会影响性能。
解决以上问题的一个策略是使用对象池。在初始化的某一时刻,可以创建一个对象池,用来管理一组可回收的对象。 应用程序可以向这个对象池请求一个对象、设置其属性、使用它,然后在操作完成后再把它还给对象池。 由于没发生对象初始化,垃圾回收探测就不会发现有对象更替,因此垃圾回收程序就不会那么频繁地运行。
静态分配是优化的一种极端形式。如果你的应用程序被垃圾回收严重地拖了后腿, 可以利用它提升性能。但这种情况并不多见。大多数情况下,这都属于过早优化,因此不 用考虑。
基本引用类型
Date
Date 类型将日期保存为自协调世界时(UTC,Universal Time Coordinated)时间 1970 年 1 月 1 日午夜(零时)至今所经过的毫秒数。使用这种存储格式,Date 类型可以精确表示 1970 年 1 月 1 日之前及之后 285 616 年的日期。
Date.parse()
Date.parse()方法接收一个表示日期的字符串参数,尝试将这个字符串转换为表示该日期的毫秒数。
所有实现都必须支持下列日期格式:
- “月/日/年”,如"5/23/2019";
- “月名 日, 年”,如"May 23, 2019";
- “周几 月名 日 年 时:分:秒 时区”,如"Tue May 23 2019 00:00:00 GMT-0700";
- ISO 8601 扩展格式“YYYY-MM-DDTHH:mm:ss.sssZ”,如 2019-05-23T00:00:00(只适用于 兼容 ES5 的实现)。
Date.UTC()
Date.UTC()方法也返回日期的毫秒表示,但使用的是跟 Date.parse()不同的信息来生成这个值。 传给 Date.UTC()的参数是:
- 年
- 零起点月数(1 月是 0,2 月是 1,以此类推)
- 日(1~31)
- 时(0~23)
- 分
- 秒
- 毫秒
这些参数中,只有前两个(年和月)是必需的。如果不提供日,那么默认为 1 日。其他参数的默认值都是 0。
toLocaleString()、toString()和 valueOf()
Date 类型的 toLocaleString()方法返回与浏览器运行的本地环境一致的日期和时间。这通常意味着格式中包含针对时间的 AM(上午)或 PM(下午), 但不包含时区信息(具体格式可能因浏览器而不同),比如:2/1/2019 12:00:00 AM
toString()方法通常返回带时区信息的日期和时间,而时间也是以 24 小时制(0~23)表示的,比如:Thu Feb 1 2019 00:00:00 GMT-0800 (Pacific Standard Time)
现代浏览器在这两个方法的输出上已经趋于一致。在比较老的浏览器上,每个方法返回的结果可能在每个浏览器上都是不同的。这些差异意味着 toLocaleString()和 toString()可能只对调试有用, 不能用于显示
Date 类型的 valueOf()方法根本就不返回字符串,这个方法被重写后返回的是日期的毫秒表示。 因此,操作符(如小于号和大于号)可以直接使用它返回的值:这也是确保日期先后的一个简单方式
let date1 = new Date(2019, 0, 1); // 2019 年 1 月 1 日
let date2 = new Date(2019, 1, 1); // 2019 年 2 月 1 日
console.log(date1 < date2); // true
console.log(date1 > date2); // false
日期格式化方法
Date 类型有几个专门用于格式化日期的方法,它们都会返回字符串:
- toDateString()显示日期中的周几、月、日、年(格式特定于实现);
- toTimeString()显示日期中的时、分、秒和时区(格式特定于实现);
- toLocaleDateString()显示日期中的周几、月、日、年(格式特定于实现和地区);
- toLocaleTimeString()显示日期中的时、分、秒(格式特定于实现和地区);
- toUTCString()显示完整的 UTC 日期(格式特定于实现)。
这些方法的输出与 toLocaleString()和 toString()一样,会因浏览器而异。因此不能用于在用户界面上一致地显示日期。
| 方法 | 说明 |
|---|---|
| getTime() | 返回日期的毫秒表示;与 valueOf()相同 |
| setTime(milliseconds) | 设置日期的毫秒表示,从而修改整个日期 |
| getFullYear() | 返回 4 位数年(即 2019 而不是 19) |
| getUTCFullYear() 返回 UTC 日期的 4 位数年 | |
| setFullYear(year) | 设置日期的年(year 必须是 4 位数) |
| setUTCFullYear(year) | 设置 UTC 日期的年(year 必须是 4 位数) |
| getMonth() | 返回日期的月(0 表示 1 月,11 表示 12 月) |
| getUTCMonth() | 返回 UTC 日期的月(0 表示 1 月,11 表示 12 月) |
| setMonth(month) | 设置日期的月(month 为大于 0 的数值,大于 11 加年) |
| setUTCMonth(month) | 设置 UTC 日期的月(month 为大于 0 的数值,大于 11 加年) |
| getDate() | 返回日期中的日(1~31) |
| getUTCDate() | 返回 UTC 日期中的日(1~31) |
| setDate(date) | 设置日期中的日(如果 date 大于该月天数,则加月) |
| setUTCDate(date) | 设置 UTC 日期中的日(如果 date 大于该月天数,则加月) |
| getDay() | 返回日期中表示周几的数值(0 表示周日,6 表示周六) |
| getUTCDay() | 返回 UTC 日期中表示周几的数值(0 表示周日,6 表示周六) |
| getHours() | 返回日期中的时(0~23) |
| getUTCHours() | 返回 UTC 日期中的时(0~23) |
| setHours(hours) | 设置日期中的时(如果 hours 大于 23,则加日) |
| setUTCHours(hours) | 设置 UTC 日期中的时(如果 hours 大于 23,则加日) |
| getMinutes() | 返回日期中的分(0~59) |
| getUTCMinutes() | 返回 UTC 日期中的分(0~59) |
| setMinutes(minutes) | 设置日期中的分(如果 minutes 大于 59,则加时) |
| setUTCMinutes(minutes) | 设置 UTC 日期中的分(如果 minutes 大于 59,则加时) |
| getSeconds() | 返回日期中的秒(0~59) |
| getUTCSeconds() | 返回 UTC 日期中的秒(0~59) |
| setSeconds(seconds) | 设置日期中的秒(如果 seconds 大于 59,则加分) |
| setUTCSeconds(seconds) | 设置 UTC 日期中的秒(如果 seconds 大于 59,则加分) |
| getMilliseconds() | 返回日期中的毫秒 |
| getUTCMilliseconds() | 返回 UTC 日期中的毫秒 |
| setMilliseconds(milliseconds) | 设置日期中的毫秒 |
| setUTCMilliseconds(milliseconds) | 设置 UTC 日期中的毫秒 |
| getTimezoneOffset() | 返回以分钟计的 UTC 与本地时区的偏移量(如美国 EST 即“东部标准时间” 返回 300,进入夏令时的地区可能有所差异) |
RegExp
ECMAScript 通过 RegExp 类型支持正则表达式:
let expression = /pattern/flags;
等同于:
let expression = new RegExp(pattern, flags);
这个正则表达式的 pattern(模式)可以是任何简单或复杂的正则表达式,包括字符类、限定符、 分组、向前查找和反向引用。每个正则表达式可以带零个或多个 flags(标记),用于控制正则表达式的行为。
flags可能取值:
- g:全局模式,表示查找字符串的全部内容,而不是找到第一个匹配的内容就结束。
- i:不区分大小写,表示在查找匹配时忽略 pattern 和字符串的大小写。
- m:多行模式,表示查找到一行文本末尾时会继续查找。
- y:粘附模式,表示只查找从 lastIndex 开始及之后的字符串。
- u:Unicode 模式,启用 Unicode 匹配。
- s:dotAll 模式,表示元字符.匹配任何字符(包括\n 或\r)
匹配特殊字符:( [ { \ ^ $ | ) ] } ? * + . 需要用反斜杠来转义。
exec()
RegExp 实例的主要方法是 exec(),主要用于配合捕获组使用。
这个方法只接收一个参数,即要应用模式的字符串。
如果找到了匹配项,则返回包含第一个匹配信息的数组; 如果没找到匹配项,则返回 null。
返回的数组虽然是 Array 的实例,但包含两个额外的属性:index 和 input。index 是字符串中匹配模式的起始位置,input 是要查找的字符串。这个数组的第一个元素是匹配整个模式的字符串, 其他元素是与表达式中的捕获组匹配的字符串。如果模式中没有捕获组,则数组只包含一个元素:
let text = "mom and dad and baby";
let pattern = /mom( and dad( and baby)?)?/gi;
let matches = pattern.exec(text);
console.log(matches.index); // 0
console.log(matches.input); // "mom and dad and
baby"
console.log(matches[0]); // "mom and dad and baby"
console.log(matches[1]); // " and dad and baby"
console.log(matches[2]); // " and baby"
原始值包装类型
为了方便操作原始值,ECMAScript 提供了 3 种特殊的引用类型:Boolean、Number 和 String。
let s1 = "some text";
let s2 = s1.substring(2);
在这里,s1 是一个包含字符串的变量,它是一个原始值。第二行紧接着在 s1 上调用了 substring() 方法,并把结果保存在 s2 中。
我们知道,原始值本身不是对象,因此逻辑上不应该有方法。而实际上这个例子又确实按照预期运行了。
这是因为后台进行了很多处理,从而实现了上述操作。具体来说,当第二行访问 s1 时,是以读模式访问的,也就是要从内存中读取变量保存的值。在以读模式访问字符串值的任何时候,后台都会执行以下 3 步:
- 创建一个 String 类型的实例
- 调用实例上的特定方法
- 销毁实例
可以把这 3 步想象成执行了如下 3 行 ECMAScript 代码:
let s1 = new String("some text");
let s2 = s1.substring(2);
s1 = null;
这种行为可以让原始值拥有对象的行为。对布尔值和数值而言,以上 3 步也会在后台发生,只不过 使用的是 Boolean 和 Number 包装类型而已。
引用类型与原始值包装类型的主要区别在于对象的生命周期。在通过 new 实例化引用类型后,得到的实例会在离开作用域时被销毁,而自动创建的原始值包装对象则只存在于访问它的那行代码执行期间。这意味着不能在运行时给原始值添加属性和方法。
在原始值包装类型的实 例上调用 typeof 会返回"object",所有原始值包装对象都会转换为布尔值 true。
使用 new 调用原始值包装类型的构造函数,与调用同名的转型函数并不一样。例如:
let value = "25";
let number = Number(value); // 转型函数
console.log(typeof number); // "number"
let obj = new Number(value); // 构造函数
console.log(typeof obj); // "object"
Object 构造函数作为一个工厂方法,能够根据传入值的类型返回相应原始值包装类型的实例。 如果传给 Object 的是字符串,则会创建一个 String 的实例。如果是数值,则会创建 Number 的实例。布尔值则会得到 Boolean 的实例。
Number
Number 类型提供了几个用于将数值格式化为字符串的方法:
- toFixed()方法返回包含指定小数点位数的数值字符串,接收一个参数,表示结果中小数的位数
- toExponential(),返回以科学记数法表示的数值字符串,接收一个参数,表示结果中小数的位数
- toPrecision()方法会根据情况返回最合理的输出结果,可能是固定长度,也可能是科学记数法形式。这个方法接收一个参数,表示结果中数字的总位数(不包含指数)
既然说上述方法会返回字符串,那么就要特别注意用以上的方法返回的结果进行二元加时需要转化成数字
isInteger()
ES6 新增了 Number.isInteger()方法,用于辨别一个数值是否保存为整数。有时候,小数位的 0 可能会让人误以为数值是一个浮点值console.log(Number.isInteger(1.00)); // true
String
String 类型提供了很多方法来解析和操作字符串:
- length 属性表示字符串长度
- charAt()方法返回给定索引位置的字符,由传给方法的整数参数指定
- charCodeAt()方法可以查看指定码元的字符编码。这个方法返回指定索引位置的码元值
字符串操作方法
拼接字符串
concat()用于将一个或多个字符串拼接成一个新字符串,可以接收任意多个参数,因此可以一次性拼接多个字符串(更常用的方式是使用加号操作符(+)。而且多数情况下,对于拼接多个字符串来说,使用加 号更方便。)
截取字符串
ECMAScript 提供了 3 个从字符串中提取子字符串的方法:slice()、substr()和 substring()。
这 3个方法都返回调用它们的字符串的一个子字符串,而且都接收一或两个参数。
- 第一个参数表示子字符串开始的位置
- 第二个参数含义不同:
- 对 slice()和 substring()而言,第二个参数是提取结束的位置(即该位置之前的字符会被提取出来)。
- 对 substr()而言,第二个参数表示返回的子字符串数量。
let stringValue = "hello world";
console.log(stringValue.slice(3, 7)); // "lo w"
console.log(stringValue.substring(3,7)); // "lo w"
console.log(stringValue.substr(3, 7)); // "lo worl"
与 concat()方法一样,slice()、substr() 和 substring()也不会修改调用它们的字符串,而只会返回提取到的原始新字符串值。
当某个参数是负值时,这 3 个方法的行为又有不同。比如,slice()方法将所有负值参数都当成字符串长度加上负参数值。 而 substr()方法将第一个负参数值当成字符串长度加上该值,将第二个负参数值转换为 0。 substring()方法会将所有负参数值都转换为 0。
let stringValue = "hello world";
console.log(stringValue.slice(-3)); // "rld"
console.log(stringValue.substring(-3)); // "hello world"
console.log(stringValue.substr(-3)); // "rld"
console.log(stringValue.slice(3, -4)); // "lo w"
console.log(stringValue.substring(3, -4)); // "hel"
console.log(stringValue.substr(3, -4)); // "" (empty string)
字符串位置方法
有两个方法用于在字符串中定位子字符串:indexOf()和 lastIndexOf()。这两个方法从字符 串中搜索传入的字符串,并返回位置(如果没找到,则返回-1)。两者的区别在于,indexOf()方法从字符串开头开始查找子字符串,而 lastIndexOf()方法从字符串末尾开始查找子字符串。
这两个方法都可以接收可选的第二个参数,表示开始搜索的位置。这意味着,indexOf()会从这个参数指定的位置开始向字符串末尾搜索,忽略该位置之前的字符;lastIndexOf()则会从这个参数指定的位置开始向字符串开头搜索,忽略该位置之后直到字符串末尾的字符。
字符串包含方法
ECMAScript 6 增加了 3 个用于判断字符串中是否包含另一个字符串的方法:startsWith()、 endsWith()和 includes()。这些方法都会从字符串中搜索传入的字符串,并返回一个表示是否包含 的布尔值。它们的区别在于:
- startsWith()检查开始于索引 0 的匹配项
- endsWith()检查开始于索引(string.length - substring.length)的匹配项
- includes()检查整个字符串
startsWith()和 includes()方法接收可选的第二个参数,表示开始搜索的位置。如果传入第二个参数,则意味着这两个方法会从指定位置向着字符串末尾搜索,忽略该位置之前的所有字符。
endsWith()方法接收可选的第二个参数,表示应该当作字符串末尾的位置。如果不提供这个参数, 那么默认就是字符串长度。如果提供这个参数,那么就好像字符串只有那么多字符一样:
let message = "foobarbaz";
console.log(message.endsWith("bar")); // false
console.log(message.endsWith("bar", 6)); // true
trim()方法
这个方法会创建字符串的一个副本,删除前、 后所有空格符,再返回结果。
repeat()方法
ECMAScript 在所有字符串上都提供了 repeat()方法。这个方法接收一个整数参数,表示要将字 符串复制多少次,然后返回拼接所有副本后的结果
padStart()和 padEnd()方法
padStart()和 padEnd()方法会复制字符串,如果小于指定长度,则在相应一边填充字符,直至 满足长度条件。这两个方法的第一个参数是长度,第二个参数是可选的填充字符串,默认为空格
let stringValue = "foo";
console.log(stringValue.padStart(6)); // " foo"
console.log(stringValue.padStart(9, ".")); // "......foo"
console.log(stringValue.padEnd(6)); // "foo "
console.log(stringValue.padEnd(9, ".")); // "foo......"
字符串大小写转换
包括 4 个方法:toLowerCase()、toLocaleLowerCase()、toUpperCase()和toLocaleUpperCase()。
toLocaleLowerCase()和 toLocaleUpperCase()方法旨在基于特定地区实现。在很多地区,地区特定的方法与通用的方法是一样的。但在少数语言中(如土耳其语), Unicode 大小写转换需应用特殊规则,要使用地区特定的方法才能实现正确转换。
如果不知道代码涉及什么语言,则最好使用地 区特定的转换方法。
字符串模式匹配方法
match()方法返回的数组与 RegExp 对象的 exec()方法返回的数组是一样的
search()。这个方法唯一的参数与 match()方法一样:正则表达 式字符串或 RegExp 对象。这个方法返回模式第一个匹配的位置索引,如果没找到则返回-1。search() 始终从字符串开头向后匹配模式。
replace()方法。这个方法接收两个参数,第一个 参数可以是一个 RegExp 对象或一个字符串(这个字符串不会转换为正则表达式),第二个参数可以是一个字符串或一个函数。如果第一个参数是字符串,那么只会替换第一个子字符串。要想替换所有子字符串,第一个参数必须为正则表达式并且带全局标记
split():这个方法会根据传入的分隔符将字符串拆分成数组。作为分隔符的参数可以是字符串,也可以是 RegExp 对象。(字符串分隔符不会被这个方法当成 正则表达式。)还可以传入第二个参数,即数组大小,确保返回的数组不会超过指定大小。
单例内置对象
ECMA-262 对内置对象的定义是“任何由 ECMAScript 实现提供、与宿主环境无关,并在 ECMAScript 程序开始执行时就存在的对象”。这就意味着,开发者不用显式地实例化内置对象,因为它们已经实例化好了。前面我们已经接触了大部分内置对象,包括 Object、Array 和 String。
Global
isNaN()、isFinite()、parseInt()和 parseFloat(),实际上都是 Global 对象的方法。除了这些,Global 对象上还有另外一些方法。
URL 编码方法
encodeURI()和 encodeURIComponent()方法用于编码统一资源标识符(URI),以便传给浏览器。
有效的 URI 不能包含某些字符,比如空格。使用 URI 编码方法来编码 URI 可以让浏览器能够理解它们, 同时又以特殊的 UTF-8 编码替换掉所有无效字符。
ecnodeURI()方法用于对整个 URI 进行编码,比如"www.wrox.com/illegal value.js"。而 encodeURIComponent()方法用于编码 URI 中单独的组件,比如前面 URL 中的"illegal value.js"。
这两个方法的主要区别是,encodeURI()不会编码属于 URL 组件的特殊字符,比如冒号、斜杠、问号、 井号,而 encodeURIComponent()会编码它发现的所有非标准字符。来看下面的例子:
let uri = "http://www.wrox.com/illegal value.js#start";
// "http://www.wrox.com/illegal%20value.js#start"
console.log(encodeURI(uri));
// "http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.js%23start"
console.log(encodeURIComponent(uri));
这里使用 encodeURI()编码后,除空格被替换为%20 之外,没有任何变化。而 encodeURIComponent()方法将所有非字母字符都替换成了相应的编码形式。这就是使用 encodeURI()编码整个 URI,但只使用 encodeURIComponent()编码那些会追加到已有 URI 后面的字符串的原因。
与encodeURI()和 encodeURIComponent()相对的是 decodeURI()和 decodeURIComponent()。
decodeURI()只对使用 encodeURI()编码过的字符解码。例如,%20 会被替换为空格,但%23 不会被 替换为井号(#),因为井号不是由 encodeURI()替换的。类似地,decodeURIComponent()解码所有被 encodeURIComponent()编码的字符,基本上就是解码所有特殊值。
eval()方法
这个方法就是一个完整的 ECMAScript 解释器,它接收一个参数,即一个要执行的 ECMAScript(JavaScript)字符串。
当解释器发现 eval()调用时,会将参数解释为实际的 ECMAScript 语句,然后将其插入到该位置。 通过 eval()执行的代码属于该调用所在上下文,被执行的代码与该上下文拥有相同的作用域链。这意 味着定义在包含上下文中的变量可以在 eval()调用内部被引用。
window 对象
浏览器将 window 对象实现为 Global 对象的代理。因此,所有全局作用域中声明的变量和函数都变成了 window 的属性。
Math
min()和 max()方法
min()和 max()方法用于确定一组数值中的最小值和最大值。这两个方法都接收任意多个参数。
要知道数组中的最大值和最小值,可以像下面这样使用扩展操作符:
let values = [1, 2, 3, 4, 5, 6, 7, 8];
let max = Math.max(...val);
舍入方法
- Math.ceil()方法始终向上舍入为最接近的整数。
- Math.floor()方法始终向下舍入为最接近的整数。
- Math.round()方法执行四舍五入。
- Math.fround()方法返回数值最接近的单精度(32 位)浮点值表示。
random()方法
Math.random()方法返回一个 0~1 范围内的随机数,其中包含 0 但不包含 1。
可以基于如下公式使用 Math.random()从一组整数中 随机选择一个数:
number = Math.floor(Math.random() * total_number_of_choices + first_possible_value)
如果想从 1~10 范围内随机选择一个数,代码就是这样的:
// 1~10 有10个数,total_number_of_choices=10
// 最小是1,first_possible_value=1
let num = Math.floor(Math.random() * 10 + 1);
如果想选择一个 2~10 范围内的值,则代码就 要写成这样:
let num = Math.floor(Math.random() * 9 + 2);
集合引用类型
Object
创建方式:
- 字面量创建
- new 操作符和 Object 构造函数创建
读取属性:
- 使用点(.)读取
- 使用中括号读取:属性名中包含可能会导致语法错误的字符(比如空格),或者包含关键字/保留字时,或者使用变量读取属性时
Array
ECMAScript 数组也是一组有序的数据,但跟其他语言不同的是,数组中每个槽位可以存储任意类型的数据。这意味着可以创建一个数组,它的第一个元素是字符串,第二个元素是数值,第三个是对象。ECMAScript 数组也是动态大小的,会随着数据添加而自动增长。
创建数组
- 使用 Array 构造函数:
let colors = new Array();- 创建数组时可以给构造函数传一个值。这时候就有点问题了,因为如果这个值是数值,则会创建一 个长度为指定数值的数组;而如果这个值是其他类型的,则会创建一个只包含该特定值的数组。
- 创建数组的时候也可以传入多个参数,多个参数会被当作元素放入到数组中
- 省略new 使用 Array()创建和 new Array()创建是一样的
- 数组字面量创建
Array 构造函数还有两个 ES6 新增的用于创建数组的静态方法:from()和 of()。
- from()用于将类数组结构转换为数组实例
- 比如map、set、arguments等所有可迭代对象
- of()用于将一组参数转换为数组实例:
console.log(Array.of(1, 2, 3, 4)); // [1, 2, 3, 4]
数组索引
数组 length 属性的独特之处在于,它不是只读的。通过修改 length 属性,可以从数组末尾删除或添加元素。如果将 length 设置为大于数组元素数的值,则新添加的元素都将以 undefined 填充
检测数组
- instanceof:
value instanceof Array - isArray:
Array.isArray(value)
迭代器方法
- keys()返回数组索引的迭代器
- values()返回数组元素的迭代器
- entries()返回 索引/值对的迭代器
const a = ["foo", "bar", "baz", "qux"];
for (const [idx, element] of a.entries()) {
alert(idx); alert(element);
}
// 0 // foo // 1 // bar // 2 // baz // 3 // qux
复制和填充方法
ES6 新增了两个方法:批量复制方法 copyWithin(),以及填充数组方法 fill()。这两个方法的函数签名类似,都需要指定既有数组实例上的一个范围,包含开始索引,不包含结束索引。使用这个方法不会改变数组的大小。
转换方法
所有对象都有 toLocaleString()、toString()和 valueOf()方法。其中,valueOf() 返回的还是数组本身。而 toString()返回由数组中每个值的等效字符串拼接而成的一个逗号分隔的字符串。也就是说,对数组的每个值都会调用其 toString()方法,以得到最终的字符串。
栈方法
数组对象可以像栈一样, 也就是一种限制插入和删除项的数据结构。栈是一种后进先出(LIFO,Last-In-First-Out)的结构,也就是最近添加的项先被删除。数据项的插入(称为推入,push)和删除(称为弹出,pop)只在栈的一个 地方发生,即栈顶。ECMAScript 数组提供了 push()和 pop()方法,以实现类似栈的行为。
队列方法
队列以先进先出(FIFO,First-In-First-Out)形式 限制访问。队列在列表末尾添加数据,但从列表开头获取数据。因为有了在数据末尾添加数据的 push() 方法,所以要模拟队列就差一个从数组开头取得数据的方法了。这个数组方法叫 shift(),它会删除数组的第一项并返回它,然后数组长度减 1。使用 shift()和 push(),可以把数组当成队列来使用。
ECMAScript 也为数组提供了 unshift()方法。顾名思义,unshift()就是执行跟 shift()相反的 操作:在数组开头添加任意多个值,然后返回新的数组长度。
排序方法
- reverse():将数组元素反向排列
- sort():
- 默认会按照升序重新排列数组元素,即最小的值在前面,最大的值在后面
- sort()会在每一项上调用 String()转型函数,然后比较字符串来决定顺序。即使数组的元素都是数值, 也会先把数组转换为字符串再比较、排序。所以经常会出现这样的排序结果:0,1,10,15,5
- 为解决上边的问题,sort()方法可以接收一个比较函数:比较函数接收两个参数,如果第一个参数应该排在第二个参数前面,就返回负值;如果两个参数相 等,就返回 0;如果第一个参数应该排在第二个参数后面,就返回正值。
操作方法
concat()方法可以在现有数组全部元素基础上创建一个新数组。它首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组。如果传入一个或多个数组,则 concat()会把这些数组的每一项都添加到结果数组。 如果参数不是数组,则直接把它们添加到结果数组末尾
slice()用于创建一个包含原有数组中一个或多个元素的新数组。slice()方法可以 接收一个或两个参数:返回元素的开始索引和结束索引。如果只有一个参数,则 slice()会返回该索引 到数组末尾的所有元素。如果有两个参数,则 slice()返回从开始索引到结束索引对应的所有元素,其 中不包含结束索引对应的元素。记住,这个操作不影响原始数组。
最强大的数组方法就属 splice()了,使用它的方式可以有很多种。splice()的主要目的是在数组中间插入元素,但有 3 种不同的方式使用这个方法。
- 删除。需要给 splice()传 2 个参数:要删除的第一个元素的位置和要删除的元素数量。可以从数组中删除任意多个元素,比如 splice(0, 2)会删除前两个元素。
- 插入。需要给 splice()传 3 个参数:开始位置、0(要删除的元素数量)和要插入的元素,可以在数组中指定的位置插入元素。第三个参数之后还可以传第四个、第五个参数,乃至任意多个要插入的元素。比如,splice(2, 0, "red", "green")会从数组位置 2 开始插入字符串 "red"和"green"。
- 替换。splice()在删除元素的同时可以在指定位置插入新元素,同样要传入 3 个参数:开始位置、要删除元素的数量和要插入的任意多个元素。要插入的元素数量不一定跟删除的元素数量 一致。比如,splice(2, 1, "red", "green")会在位置 2 删除一个元素,然后从该位置开始 向数组中插入"red"和"green"。
搜索和位置方法
严格相等
ECMAScript 提供了 3 个严格相等的搜索方法:indexOf()、lastIndexOf()和 includes()。其 中,前两个方法在所有版本中都可用,而第三个方法是 ECMAScript 7 新增的。这些方法都接收两个参数:要查找的元素和一个可选的起始搜索位置。indexOf()和 includes()方法从数组前头(第一项) 开始向后搜索,而 lastIndexOf()从数组末尾(最后一项)开始向前搜索。
断言函数
ECMAScript 也允许按照定义的断言函数搜索数组,每个索引都会调用这个函数。
断言函数的返回值决定了相应索引的元素是否被认为匹配。
断言函数接收 3 个参数:元素、索引和数组本身。其中元素是数组中当前搜索的元素,索引是当前元素的索引,而数组就是正在搜索的数组。断言函数返回真值,表示是否匹配。
find()和 findIndex()方法使用了断言函数。这两个方法都从数组的最小索引开始。find()返回第一个匹配的元素,findIndex()返回第一个匹配元素的索引。这两个方法也都接收第二个可选的参数, 用于指定断言函数内部 this 的值。
迭代方法
ECMAScript 为数组定义了 5 个迭代方法。
每个方法接收两个参数:以每一项为参数运行的函数, 以及可选的作为函数运行上下文的作用域对象(影响函数中 this 的值)。
传给每个方法的函数接收 3 个参数:数组元素、元素索引和数组本身。因具体方法而异,这个函数的执行结果可能会也可能不会影响方法的返回值。
- every():对数组每一项都运行传入的函数,如果对每一项函数都返回 true,则这个方法返回 true。
- filter():对数组每一项都运行传入的函数,函数返回 true 的项会组成数组之后返回。
- forEach():对数组每一项都运行传入的函数,没有返回值。
- map():对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组。
- some():对数组每一项都运行传入的函数,如果有一项函数返回 true,则这个方法返回 true。
这些方法都不改变调用它们的数组。
归并方法
ECMAScript 为数组提供了两个归并方法:reduce()和 reduceRight()。这两个方法都会迭代数 组的所有项,并在此基础上构建一个最终返回值。reduce()方法从数组第一项开始遍历到最后一项。 而 reduceRight()从最后一项开始遍历至第一项。
这两个方法都接收两个参数:对每一项都会运行的归并函数,以及可选的以之为归并起点的初始值。
传给 reduce()和 reduceRight()的函数接收 4 个参数:上一个归并值、当前项、当前项的索引和数组本身。
这个函数返回的任何值都会作为下一次调用同一个函数的第一个参数。如果没有给这两个方法传入可选的第二个参数(作为归并起点值),则第一次迭代将从数组的第二项开始,因此传给归并函数的第一个参数是数组的第一项,第二个参数是数组的第二项。
Map
基本 API
new Map()创建一个空映射new Map([ ["key1", "val1"], ["key2", "val2"], ["key3", "val3"] ])使用嵌套数组初始化映射set(key,value)方法再添加键/值对- set()方法返回映射实例,因此可以把多个操作连缀起来
const m = new Map().set("key1", "val1"); m.set("key2", "val2") .set("key3", "val3"); alert(m.size); // 3
- set()方法返回映射实例,因此可以把多个操作连缀起来
get(key)和 has(key)进行查询size属性获取映射中的键/值对的数量delete(key)和 clear()删除值
与 Object 只能使用数值、字符串或符号作为键不同,Map 可以使用任何 JavaScript 数据类型作为键。
在映射中用作键和值的对象及其他“集合”类型,在自己的内容或属性被修改时 仍然保持不变:
const m = new Map();
const objKey = {}, objVal = {},
arrKey = [], arrVal = [];
m.set(objKey, objVal);
m.set(arrKey, arrVal);
objKey.foo = "foo";
objVal.bar = "bar";
arrKey.push("foo");
arrVal.push("bar");
console.log(m.get(objKey)); // {bar: "bar"}
console.log(m.get(arrKey)); // ["bar"]
顺序与迭代
entries与iterator
与 Object 类型的一个主要差异是,Map 实例会维护键值对的插入顺序。
映射实例可以提供一个迭代器(Iterator),能以插入顺序生成[key, value]形式的数组。可以通过 entries()方法(或者 Symbol.iterator 属性,它引用 entries())取得这个迭代器:
const m = new Map([ ["key1", "val1"], ["key2", "val2"], ["key3", "val3"] ]);
alert(m.entries === m[Symbol.iterator]); // true
for (let pair of m.entries()) {
alert(pair);
}
// [key1,val1]
// [key2,val2]
// [key3,val3]
for (let pair of m[Symbol.iterator]()) {
alert(pair);
}
// [key1,val1]
// [key2,val2]
// [key3,val3]
keys()和 values()
keys()和 values()分别返回以插入顺序生成键和值的迭代器.
键和值在迭代器遍历时是可以修改的,但映射内部的引用则无法修改。当然,这并不妨碍修改作为键或值的对象内部的属性,因为这样并不影响它们在映射实例中的身份.
选择 Object 还是 Map
对于多数 Web 开发任务来说,选择 Object 还是 Map 只是个人偏好问题,影响不大。不过,对于 在乎内存和性能的开发者来说,对象和映射之间确实存在显著的差别.
- 内存占用:Object 和 Map 的工程级实现在不同浏览器间存在明显差异,但存储单个键/值对所占用的内存数量 都会随键的数量线性增加。批量添加或删除键/值对则取决于各浏览器对该类型内存分配的工程实现。 不同浏览器的情况不同,但给定固定大小的内存,Map 大约可以比 Object 多存储 50%的键/值对。
- 插入性能:向 Object 和 Map 中插入新键/值对的消耗大致相当,不过插入 Map 在所有浏览器中一般会稍微快 一点儿。对这两个类型来说,插入速度并不会随着键/值对数量而线性增加。如果代码涉及大量插入操作,那么显然 Map 的性能更佳。
- 查找速度:与插入不同,从大型 Object 和 Map 中查找键/值对的性能差异极小,但如果只包含少量键/值对, 则 Object 有时候速度更快。在把 Object 当成数组使用的情况下(比如使用连续整数作为属性),浏 览器引擎可以进行优化,在内存中使用更高效的布局。这对 Map 来说是不可能的。对这两个类型而言, 查找速度不会随着键/值对数量增加而线性增加。如果代码涉及大量查找操作,那么某些情况下可能选择 Object 更好一些。
- 删除性能:使用 delete 删除 Object 属性的性能一直以来饱受诟病,目前在很多浏览器中仍然如此。为此, 出现了一些伪删除对象属性的操作,包括把属性值设置为 undefined 或 null。但很多时候,这都是一 种讨厌的或不适宜的折中。而对大多数浏览器引擎来说,Map 的 delete()操作都比插入和查找更快。 如果代码涉及大量删除操作,那么毫无疑问应该选择 Map。
WeakMap
弱映射中的键只能是 Object 或者继承自 Object 的类型,尝试使用非对象设置键会抛出 TypeError。值的类型没有限制。
基本 API
new WeakMap():使用 new 关键字实例化一个空的 WeakMap- 如果想在初始化时填充弱映射,则构造函数可以接收一个可迭代对象,其中需要包含键/值对数组。
const key1 = {id: 1}, key2 = {id: 2},key3 = {id: 3}; // 使用嵌套数组初始化弱映射 const wm1 = new WeakMap([ [key1, "val1"], [key2, "val2"], [key3, "val3"] ]); alert(wm1.get(key1)); // val1 alert(wm1.get(key2)); // val2 alert(wm1.get(key3)); // val3 // 初始化是全有或全无的操作 // 只要有一个键无效就会抛出错误,导致整个初始化失败 const wm2 = new WeakMap([ [key1, "val1"], ["BADKEY", "val2"], [key3, "val3"] ]); // TypeError: Invalid value used as WeakMap key typeof wm2; // ReferenceError: wm2 is not defined // 原始值可以先包装成对象再用作键 const stringKey = new String("key1"); const wm3 = new WeakMap([ stringKey, "val1" ]); alert(wm3.get(stringKey)); // "val1" set()添加键/值对get()和 has()查询delete()删除
弱键
WeakMap 中“weak”表示弱映射的键是“弱弱地拿着”的。意思就是,这些键不属于正式的引用, 不会阻止垃圾回收。但要注意的是,弱映射中值的引用可不是“弱弱地拿着”的。只要键存在,键/值 对就会存在于映射中,并被当作对值的引用,因此就不会被当作垃圾回收。
来看下面的例子:
const wm = new WeakMap();
wm.set({}, "val");
set()方法初始化了一个新对象并将它用作一个字符串的键。因为没有指向这个对象的其他引用, 所以当这行代码执行完成后,这个对象键就会被当作垃圾回收。然后,这个键/值对就从弱映射中消失了,使其成为一个空映射。在这个例子中,因为值也没有被引用,所以这对键/值被破坏以后,值本身也会成为垃圾回收的目标。
再看一个稍微不同的例子:
const wm = new WeakMap();
const container = { key: {} };
wm.set(container.key, "val");
function removeReference() { container.key = null; }
这一次,container 对象维护着一个对弱映射键的引用,因此这个对象键不会成为垃圾回收的目标。不过,如果调用了 removeReference(),就会摧毁键对象的最后一个引用,垃圾回收程序就可以把这个键/值对清理掉。
不可迭代键
因为 WeakMap 中的键/值对任何时候都可能被销毁,所以没必要提供迭代其键/值对的能力。
当然, 也用不着像 clear()这样一次性销毁所有键/值的方法。WeakMap 确实没有这个方法。
因为不可能迭代, 所以也不可能在不知道对象引用的情况下从弱映射中取得值。即便代码可以访问 WeakMap 实例,也没办法看到其中的内容。
WeakMap 实例之所以限制只能用对象作为键,是为了保证只有通过键对象的引用才能取得值。如果允许原始值,那就没办法区分初始化时使用的字符串字面量和初始化之后使用的一个相等的字符串了。
使用弱映射
WeakMap 实例与现有 JavaScript 对象有着很大不同,可能一时不容易说清楚应该怎么使用它。这个 问题没有唯一的答案,但已经出现了很多相关策略。
- 私有变量
弱映射造就了在 JavaScript 中实现真正私有变量的一种新方式。前提很明确:私有变量会存储在弱 映射中,以对象实例为键,以私有成员的字典为值。
const wm = new WeakMap();
class User {
constructor(id) {
this.idProperty = Symbol('id');
this.setId(id);
}
setPrivate(property, value) {
const privateMembers = wm.get(this) || {};
privateMembers[property] = value;
wm.set(this, privateMembers);
}
getPrivate(property) {
return wm.get(this)[property];
}
setId(id) {
this.setPrivate(this.idProperty, id);
}
getId() {
return this.getPrivate(this.idProperty);
}
}
const user = new User(123);
alert(user.getId()); // 123
user.setId(456);
alert(user.getId()); // 456
// 并不是真正私有的
alert(wm.get(user)[user.idProperty]); // 456
慧眼独具的读者会发现,对于上面的实现,外部代码只需要拿到对象实例的引用和弱映射,就可以 取得“私有”变量了。为了避免这种访问,可以用一个闭包把 WeakMap 包装起来,这样就可以把弱映 射与外界完全隔离开了:
const User = (() => {
const wm = new WeakMap();
class User {
constructor(id) {
this.idProperty = Symbol('id');
this.setId(id);
}
setPrivate(property, value) {
const privateMembers = wm.get(this) || {};
privateMembers[property] = value;
wm.set(this, privateMembers);
}
getPrivate(property) {
return wm.get(this)[property];
}
setId(id) {
this.setPrivate(this.idProperty, id);
}
getId(id) {
return this.getPrivate(this.idProperty);
}
}
return User;
})();
const user = new User(123);
alert(user.getId()); // 123
user.setId(456);
alert(user.getId()); // 456
- DOM 节点元数据
因为 WeakMap 实例不会妨碍垃圾回收,所以非常适合保存关联元数据。来看下面这个例子,其中使用了常规的 Map:
const m = new Map();
const loginButton = document.querySelector('#login');
// 给这个节点关联一些元数据
m.set(loginButton, {disabled: true});
假设在上面的代码执行后,页面被 JavaScript 改变了,原来的登录按钮从 DOM 树中被删掉了。但由于映射中还保存着按钮的引用,所以对应的 DOM 节点仍然会逗留在内存中,除非明确将其从映射中删除或者等到映射本身被销毁。 如果这里使用的是弱映射,如以下代码所示,那么当节点从 DOM 树中被删除后,垃圾回收程序就可以立即释放其内存(假设没有其他地方引用这个对象):
const wm = new WeakMap();
const loginButton = document.querySelector('#login');
// 给这个节点关联一些元数据
wm.set(loginButton, {disabled: true});
Set
基本 API
new Set(): new 关键字和 Set 构造函数可以创建一个空集合new Set(["val1", "val2", "val3"]): 给 Set 构造函数传入一个可迭代对象,其中需要包含插入到新集合实例中的元素add()增加值has()查询size取得元素数量delete() 和 clear()删除元素
add()和 delete()操作是幂等的。delete()返回一个布尔值,表示集合中是否存在要删除的值
顺序与迭代
values与iterator
Set 会维护值插入时的顺序,因此支持按顺序迭代。 集合实例可以提供一个迭代器(Iterator),能以插入顺序生成集合内容。可以通过 values()方 法及其别名方法 keys()(或者 Symbol.iterator 属性,它引用 values())取得这个迭代器:
const s = new Set(["val1", "val2", "val3"]);
alert(s.values === s[Symbol.iterator]); // true
alert(s.keys === s[Symbol.iterator]); // true
for (let value of s.values()) {
alert(value);
}
// val1
// val2
// val3
for (let value of s[Symbol.iterator]()) {
alert(value);
}
// val1
// val2
// val3
WeakSet
ECMAScript 6 新增的“弱集合”(WeakSet)是一种新的集合类型,为这门语言带来了集合数据结 构。WeakSet 是 Set 的“兄弟”类型,其 API 也是 Set 的子集。WeakSet 中的“weak”(弱),描述的 是 JavaScript 垃圾回收程序对待“弱集合”中值的方式。
基本 API
弱集合中的值只能是 Object 或者继承自 Object 的类型,尝试使用非对象设置值会抛出 TypeError。
-
new WeakSet(): 使用 new 关键字实例化一个空的 WeakSet -
如果想在初始化时填充弱集合,则构造函数可以接收一个可迭代对象,其中需要包含有效的值。可迭代对象中的每个值都会按照迭代顺序插入到新实例中:
const val1 = {id: 1}, val2 = {id: 2}, val3 = {id: 3}; // 使用数组初始化弱集合 const ws1 = new WeakSet([val1, val2, val3]); alert(ws1.has(val1)); // true alert(ws1.has(val2)); // true alert(ws1.has(val3)); // true // 初始化是全有或全无的操作 // 只要有一个值无效就会抛出错误,导致整个初始化失败 const ws2 = new WeakSet([val1, "BADVAL", val3]); // TypeError: Invalid value used in WeakSet typeof ws2; // ReferenceError: ws2 is not defined // 原始值可以先包装成对象再用作值 const stringVal = new String("val1"); const ws3 = new WeakSet([stringVal]); alert(ws3.has(stringVal)); // true -
add()添加新值 -
has()查询 -
delete()删除
弱值
WeakSet 中“weak”表示弱集合的值是“弱弱地拿着”的。意思就是,这些值不属于正式的引用, 不会阻止垃圾回收。
来看下面的例子:
const ws = new WeakSet();
ws.add({});
add()方法初始化了一个新对象,并将它用作一个值。因为没有指向这个对象的其他引用,所以当这行代码执行完成后,这个对象值就会被当作垃圾回收。然后,这个值就从弱集合中消失了,使其成为 一个空集合。
再看一个稍微不同的例子:
const ws = new WeakSet();
const container = { val: {} };
ws.add(container.val);
function removeReference() { container.val = null; }
这一次,container 对象维护着一个对弱集合值的引用,因此这个对象值不会成为垃圾回收的目标。不过,如果调用了 removeReference(),就会摧毁值对象的最后一个引用,垃圾回收程序就可以把这个值清理掉.
不可迭代值
因为 WeakSet 中的值任何时候都可能被销毁,所以没必要提供迭代其值的能力。
当然,也用不着 像 clear()这样一次性销毁所有值的方法。WeakSet 确实没有这个方法。
因为不可能迭代,所以也不可能在不知道对象引用的情况下从弱集合中取得值。即便代码可以访问 WeakSet 实例,也没办法看到其中的内容。
WeakSet 之所以限制只能用对象作为值,是为了保证只有通过值对象的引用才能取得值。如果允许原始值,那就没办法区分初始化时使用的字符串字面量和初始化之后使用的一个相等的字符串了。
使用弱集合
相比于 WeakMap 实例,WeakSet 实例的用处没有那么大。不过,弱集合在给对象打标签时还是有价值的。 来看下面的例子,这里使用了一个普通 Set:
const disabledElements = new Set();
const loginButton = document.querySelector('#login');
// 通过加入对应集合,给这个节点打上“禁用”标签
disabledElements.add(loginButton);
这样,通过查询元素在不在 disabledElements 中,就可以知道它是不是被禁用了。不过,假如元素从 DOM 树中被删除了,它的引用却仍然保存在 Set 中,因此垃圾回收程序也不能回收它。 为了让垃圾回收程序回收元素的内存,可以在这里使用 WeakSet:
const disabledElements = new WeakSet();
const loginButton = document.querySelector('#login');
// 通过加入对应集合,给这个节点打上“禁用”标签
disabledElements.add(loginButton);
这样,只要 WeakSet 中任何元素从 DOM 树中被删除,垃圾回收程序就可以忽略其存在,而立即释放其内存(假设没有其他地方引用这个对象).
迭代与扩展操作
有 4 种原生集合类型定义了默认迭代器:
- Array
- 所有定型数组
- Map
- Set
很简单,这意味着上述所有类型都支持顺序迭代,都可以传入 for-of 循环.
这也意味着所有这些类型都兼容扩展操作符。扩展操作符在对可迭代对象执行浅复制时特别有用, 只需简单的语法就可以复制整个对象.
上面的这些类型都支持多种构建方法,比如 Array.of()和 Array.from()静态方法。在与扩展操 作符一起使用时,可以非常方便地实现互操作.