JavaScript 教程(一)

2019-03-20 16:34:44 1992

入门篇

导论

什么是JavaScript语言

JavaScript 是一种轻量级的脚本语言。所谓“脚本语言”(script language),指的是它不具备开发操作系统的能力,而是只用来编写控制其他大型应用程序(比如浏览器)的“脚本”

JavaScript 也是一种嵌入式(embedded)语言。它本身提供的核心语法不算很多,只能用来做一些数学和逻辑运算。JavaScript 本身不提供任何与 I/O(输入/输出)相关的 API,都要靠宿主环境(host)提供,所以 JavaScript 只合适嵌入更大型的应用程序环境,去调用宿主环境提供的底层 API

目前,已经嵌入 JavaScript 的宿主环境有多种,最常见的环境就是浏览器,另外还有服务器环境,也就是 Node 项目

从语法角度看,JavaScript 语言是一种“对象模型”语言。各种宿主环境通过这个模型,描述自己的功能和操作接口,从而通过 JavaScript 控制这些功能。但是,JavaScript 并不是纯粹的“面向对象语言”,还支持其他编程范式(比如函数式编程)。这导致几乎任何一个问题,JavaScript 都有多种解决方法

JavaScript 的核心语法部分相当精简,只包括两个部分:基本的语法构造(比如操作符、控制结构、语句)和标准库(就是一系列具有各种功能的对象比如Array、Date、Math等)。除此之外,各种宿主环境提供额外的 API(即只能在该环境使用的接口),以便 JavaScript 调用。以浏览器为例,它提供的额外 API 可以分成三大类。

1.浏览器控制类:操作浏览器

2.DOM 类:操作网页的各种元素

3.Web 类:实现互联网的各种功能

如果宿主环境是服务器,则会提供各种操作系统的 API,比如文件操作 API、网络通信 API等等。这些都可以在 Node 环境中找到。

本书主要介绍 JavaScript 核心语法和浏览器网页开发的基本知识,不涉及 Node。全书可以分成以下四大部分。

为什么学习 JavaScript

JavaScript既适合作为学习编程的入门语言,也适合当作日常开发的工作语言。它是目前最有希望、前途最光明的计算机语言之一

操控浏览器的能力

JavaScript 的发明目的,就是作为浏览器的内置脚本语言,为网页开发者提供操控浏览器的能力。它是目前唯一一种通用的浏览器脚本语言,所有浏览器都支持。它可以让网页呈现各种特殊效果,为用户提供良好的互动体验。

目前,全世界几乎所有网页都使用 JavaScript。如果不用,网站的易用性和使用效率将大打折扣,无法成为操作便利、对用户友好的网站。

对于一个互联网开发者来说,如果你想提供漂亮的网页、令用户满意的上网体验、各种基于浏览器的便捷功能、前后端之间紧密高效的联系,JavaScript 是必不可少的工具

广泛的使用领域

近年来,JavaScript 的使用范围,慢慢超越了浏览器,正在向通用的系统语言发展

浏览器的平台化

随着 HTML5 的出现,浏览器本身的功能越来越强,不再仅仅能浏览网页,而是越来越像一个平台,JavaScript 因此得以调用许多系统功能,比如操作本地文件、操作图片、调用摄像头和麦克风等等。这使得 JavaScript 可以完成许多以前无法想象的事情

Node

Node 项目使得 JavaScript 可以用于开发服务器端的大型项目,网站的前后端都用 JavaScript 开发已经成为了现实。有些嵌入式平台(Raspberry Pi)能够安装 Node,于是 JavaScript 就能为这些平台开发应用程序

数据库操作

JavaScript 甚至也可以用来操作数据库。NoSQL 数据库这个概念,本身就是在 JSON(JavaScript Object Notation)格式的基础上诞生的,大部分 NoSQL 数据库允许 JavaScript 直接操作。基于 SQL 语言的开源数据库 PostgreSQL 支持 JavaScript 作为操作语言,可以部分取代 SQL 查询语言

移动平台开发

JavaScript 也正在成为手机应用的开发语言。一般来说,安卓平台使用 Java 语言开发,iOS 平台使用 Objective-C 或 Swift 语言开发。许多人正在努力,让 JavaScript 成为各个平台的通用开发语言。

PhoneGap 项目就是将 JavaScript 和 HTML5 打包在一个容器之中,使得它能同时在 iOS 和安卓上运行。Facebook 公司的 React Native 项目则是将 JavaScript 写的组件,编译成原生组件,从而使它们具备优秀的性能。

Mozilla 基金会的手机操作系统 Firefox OS,更是直接将 JavaScript 作为操作系统的平台语言,但是很可惜这个项目没有成功

内嵌脚本语言

越来越多的应用程序,将 JavaScript 作为内嵌的脚本语言,比如 Adobe 公司的著名 PDF 阅读器 Acrobat、Linux 桌面环境 GNOME 3

跨平台的桌面应用程序

Chromium OS、Windows 8 等操作系统直接支持 JavaScript 编写应用程序。Mozilla 的 Open Web Apps 项目、Google 的 Chrome App 项目、GitHub 的 Electron 项目、以及 TideSDK 项目,都可以用来编写运行于 Windows、Mac OS 和 Android 等多个桌面平台的程序,不依赖浏览器

小结

可以预期,JavaScript 最终将能让你只用一种语言,就开发出适应不同平台(包括桌面端、服务器端、手机端)的程序。早在2013年9月的统计之中,JavaScript 就是当年 GitHub 上使用量排名第一的语言。

易学性

相比学习其他语言,学习 JavaScript 有一些有利条件

学习环境无处不在

只要有浏览器,就能运行 JavaScript 程序;只要有文本编辑器,就能编写 JavaScript 程序。这意味着,几乎所有电脑都原生提供 JavaScript 学习环境,不用另行安装复杂的 IDE(集成开发环境)和编译器

简单性

相比其他脚本语言(比如 Python 或 Ruby),JavaScript 的语法相对简单一些,本身的语法特性并不是特别多。而且,那些语法中的复杂部分,也不是必需要学会。你完全可以只用简单命令,完成大部分的操作

与主流语言的相似性

JavaScript 的语法很类似 C/C++ 和 Java,如果学过这些语言(事实上大多数学校都教),JavaScript 的入门会非常容易。必须说明的是,虽然核心语法不难,但是 JavaScript 的复杂性体现在另外两个方面。

1.它涉及大量的外部 API。JavaScript 要发挥作用,必须与其他组件配合,这些外部组件五花八门,数量极其庞大,几乎涉及网络应用的各个方面,掌握它们绝非易事。

2.JavaScript 语言有一些设计缺陷。某些地方相当不合理,另一些地方则会出现怪异的运行结果。学习 JavaScript,很大一部分时间是用来搞清楚哪些地方有陷阱

尽管如此,目前看来,JavaScript 的地位还是无法动摇。加之,语言标准的快速进化,使得 JavaScript 功能日益增强,而语法缺陷和怪异之处得到了弥补。所以,JavaScript 还是值得学习,况且它的入门真的不难

强大的性能

JavaScript 的性能优势体现在以下方面

灵活的语法,表达力强。

JavaScript 既支持类似 C 语言清晰的过程式编程,也支持灵活的函数式编程,可以用来写并发处理(concurrent)。这些语法特性已经被证明非常强大,可以用于许多场合,尤其适用异步编程。

JavaScript 的所有值都是对象,这为程序员提供了灵活性和便利性。因为你可以很方便地、按照需要随时创造数据结构,不用进行麻烦的预定义。

JavaScript 的标准还在快速进化中,并不断合理化,添加更适用的语法特性

支持编译运行。

JavaScript 语言本身虽然是一种解释型语言,但是在现代浏览器中,JavaScript 都是编译后运行。程序会被高度优化,运行效率接近二进制程序。而且,JavaScript 引擎正在快速发展,性能将越来越好。

此外,还有一种 WebAssembly 格式,它是 JavaScript 引擎的中间码格式,全部都是二进制代码。由于跳过了编译步骤,可以达到接近原生二进制代码的运行速度。各种语言(主要是 C 和 C++)通过编译成 WebAssembly,就可以在浏览器里面运行

事件驱动和非阻塞式设计。

JavaScript 程序可以采用事件驱动(event-driven)和非阻塞式(non-blocking)设计,在服务器端适合高并发环境,普通的硬件就可以承受很大的访问量

开放性

JavaScript 是一种开放的语言。它的标准 ECMA-262 是 ISO 国际标准,写得非常详尽明确;该标准的主要实现(比如 V8 和 SpiderMonkey 引擎)都是开放的,而且质量很高。这保证了这门语言不属于任何公司或个人,不存在版权和专利的问题。

语言标准由 TC39 委员会负责制定,该委员会的运作是透明的,所有讨论都是开放的,会议记录都会对外公布。

不同公司的 JavaScript 运行环境,兼容性很好,程序不做调整或只做很小的调整,就能在所有浏览器上运行

社区支持和就业机会

全世界程序员都在使用 JavaScript,它有着极大的社区、广泛的文献和图书、丰富的代码资源。绝大部分你需要用到的功能,都有多个开源函数库可供选用

实验环境

推荐安装 Chrome 浏览器,进入 Chrome 浏览器的“控制台”

1.直接进入:按下Option + Command + J(Mac)或者Ctrl + Shift + J(Windows / Linux)

2.开发者工具进入:开发者工具的快捷键是 F12,或者Option + Command + I(Mac)以及Ctrl + Shift + I(Windows / Linux),然后选择 Console 面板

JavaScript 语言的历史

诞生

JavaScript 因为互联网而生,紧跟着浏览器的出现而问世。回顾它的历史,就要从浏览器的历史讲起。

1990年底,欧洲核能研究组织(CERN)科学家 Tim Berners-Lee,在全世界最大的电脑网络——互联网的基础上,发明了万维网(World Wide Web),从此可以在网上浏览网页文件。最早的网页只能在操作系统的终端里浏览,也就是说只能使用命令行操作,网页都是在字符窗口中显示,这当然非常不方便。

1992年底,美国国家超级电脑应用中心(NCSA)开始开发一个独立的浏览器,叫做 Mosaic。这是人类历史上第一个浏览器,从此网页可以在图形界面的窗口浏览。

1994年10月,NCSA 的一个主要程序员 Marc Andreessen 联合风险投资家 Jim Clark,成立了 Mosaic 通信公司(Mosaic Communications),不久后改名为 Netscape。这家公司的方向,就是在 Mosaic 的基础上,开发面向普通用户的新一代的浏览器 Netscape Navigator。

1994年12月,Navigator 发布了1.0版,市场份额一举超过90%。

Netscape 公司很快发现,Navigator 浏览器需要一种可以嵌入网页的脚本语言,用来控制浏览器行为。当时,网速很慢而且上网费很贵,有些操作不宜在服务器端完成。比如,如果用户忘记填写“用户名”,就点了“发送”按钮,到服务器再发现这一点就有点太晚了,最好能在用户发出数据之前,就告诉用户“请填写用户名”。这就需要在网页中嵌入小程序,让浏览器检查每一栏是否都填写了。

管理层对这种浏览器脚本语言的设想是:功能不需要太强,语法较为简单,容易学习和部署。那一年,正逢 Sun 公司的 Java 语言问世,市场推广活动非常成功。Netscape 公司决定与 Sun 公司合作,浏览器支持嵌入 Java 小程序(后来称为 Java applet)。但是,浏览器脚本语言是否就选用 Java,则存在争论。后来,还是决定不使用 Java,因为网页小程序不需要 Java 这么“重”的语法。但是,同时也决定脚本语言的语法要接近 Java,并且可以支持 Java 程序。这些设想直接排除了使用现存语言,比如 Perl、Python 和 TCL。

1995年,Netscape 公司雇佣了程序员 Brendan Eich 开发这种网页脚本语言。Brendan Eich 有很强的函数式编程背景,希望以 Scheme 语言(函数式语言鼻祖 LISP 语言的一种方言)为蓝本,实现这种新语言。

1995年5月,Brendan Eich 只用了10天,就设计完成了这种语言的第一版。它是一个大杂烩,语法有多个来源。

1.基本语法:借鉴 C 语言和 Java 语言。

2.数据结构:借鉴 Java 语言,包括将值分成原始值和对象两大类。

3.函数的用法:借鉴 Scheme 语言和 Awk 语言,将函数当作第一等公民,并引入闭包。

4.原型继承模型:借鉴 Self 语言(Smalltalk 的一种变种)。

5.正则表达式:借鉴 Perl 语言。

6.字符串和数组处理:借鉴 Python 语言。

为了保持简单,这种脚本语言缺少一些关键的功能,比如块级作用域、模块、子类型(subtyping)等等,但是可以利用现有功能找出解决办法。这种功能的不足,直接导致了后来 JavaScript 的一个显著特点:对于其他语言,你需要学习语言的各种功能,而对于 JavaScript,你常常需要学习各种解决问题的模式。而且由于来源多样,从一开始就注定JavaScript 的编程风格是函数式编程和面向对象编程的一种混合体。

Netscape 公司的这种浏览器脚本语言,最初名字叫做 Mocha,1995年9月改为 LiveScript。12月,Netscape 公司与 Sun 公司(Java 语言的发明者和所有者)达成协议,后者允许将这种语言叫做 JavaScript。这样一来,Netscape 公司可以借助 Java 语言的声势,而 Sun 公司则将自己的影响力扩展到了浏览器。

之所以起这个名字,并不是因为 JavaScript 本身与 Java 语言有多么深的关系(事实上,两者关系并不深),而是因为 Netscape 公司已经决定,使用 Java 语言开发网络应用程序,JavaScript 可以像胶水一样,将各个部分连接起来。当然,后来的历史是 Java 语言的浏览器插件失败了,JavaScript 反而发扬光大。

1995年12月4日,Netscape 公司与 Sun 公司联合发布了 JavaScript 语言,对外宣传 JavaScript 是 Java 的补充,属于轻量级的 Java,专门用来操作网页。

1996年3月,Navigator 2.0 浏览器正式内置了 JavaScript 脚本语言

JavaScript 与 Java 的关系

JavaScript 和 Java是两种不一样的语言,但是彼此存在联系。

JavaScript 的基本语法和对象体系,是模仿 Java 而设计的。但是,JavaScript 没有采用 Java 的静态类型。正是因为 JavaScript 与 Java 有很大的相似性,所以这门语言才从一开始的 LiveScript 改名为 JavaScript。基本上,JavaScript 这个名字的原意是“很像Java的脚本语言”。

JavaScript 语言的函数是一种独立的数据类型,以及采用基于原型对象(prototype)的继承链。这是它与 Java 语法最大的两点区别。JavaScript 语法要比 Java 自由得多。另外,Java 语言需要编译,而 JavaScript 语言则是运行时由解释器直接执行。

总之,JavaScript 的原始设计目标是一种小型的、简单的动态语言,与 Java 有足够的相似性,使得使用者(尤其是 Java 程序员)可以快速上手

JavaScript 的基本语法

语句

JavaScript 程序的执行单位为行(line),一般情况下每一行就是一个语句。语句(statement)是为了完成某种任务而进行的操作,比如下面就是一行赋值语句

var a = 1 + 3;

1 + 3叫做表达式(expression),指一个为了得到返回值的计算式。语句和表达式的区别在于,前者主要为了进行某种操作,一般不需要返回值;后者则是为了得到返回值。凡是 JavaScript 语言中预期为值的地方,都可以使用表达式。比如,赋值语句的等号右边,预期是一个值,因此可以放置各种表达式。

语句以分号结尾,一个分号就表示一个语句结束。多个语句可以写在一行内。

var a = 1 + 3 ; var b = 'abc';

分号前面可以没有任何内容,JavaScript 引擎将其视为空语句

;;;

上面的代码就表示3个空语句。

表达式不需要分号结尾。一旦在表达式后面添加分号,则 JavaScript 引擎就将表达式视为语句,这样会产生一些没有任何意义的语句

1 + 3;
'abc';

上面两行语句只是单纯地产生一个值,并没有任何实际的意义

变量

概念

变量是对“值”的具名引用。变量就是为“值”起名,然后引用这个名字,就等同于引用这个值。变量的名字就是变量名

注意,JavaScript 的变量名区分大小写,A和a是两个不同的变量。变量的声明和赋值,是分开的两个步骤,也可以将它们合在一起

如果只是声明变量而没有赋值,则该变量的值是undefined。undefined是一个特殊的值,表示“无定义”

如果变量赋值的时候,忘了写var命令,这条语句也是有效的。但是不利于表达意图,而且容易不知不觉地创建全局变量

如果一个变量没有声明就直接使用,JavaScript 会报错,告诉你变量未定义

x  // ReferenceError: x is not defined

上面代码直接使用变量x,系统就报错,告诉你变量x没有声明

可以在同一条var命令中声明多个变量

JavaScript 是一种动态类型语言,也就是说,变量的类型没有限制,变量可以随时更改类型

如果使用var重新声明一个已经存在的变量,是无效的

var x = 1;
var x;
x // 1

上面代码中,变量x声明了两次,第二次声明是无效的。但是,如果第二次声明的时候还进行了赋值,则会覆盖掉前面的值

变量提升

JavaScript 引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升(hoisting)

console.log(a);
var a = 1;

上面代码首先使用console.log方法,在控制台(console)显示变量a的值。这时变量a还没有声明和赋值,所以这是一种错误的做法,但是实际上不会报错,因为存在变量提升。最后的结果是显示undefined,表示变量a已声明,但还未赋值

标识符

标识符(identifier)指的是用来识别各种值的合法名称。最常见的标识符就是变量名,以及后面要提到的函数名。JavaScript 语言的标识符对大小写敏感,所以a和A是两个不同的标识符。

标识符有一套命名规则,不符合规则的就是非法标识符。JavaScript 引擎遇到非法标识符,就会报错。

简单说,标识符命名规则如下:

第一个字符,可以是任意 Unicode 字母(包括英文字母和其他语言的字母),以及美元符号($)和下划线(_)。

第二个字符及后面的字符,除了 Unicode 字母、美元符号和下划线,还可以用数字0-9

中文是合法的标识符,可以用作变量名

var 临时变量 = 1;

JavaScript 有一些保留字,不能用作标识符:

arguments、break、case、catch、class、const、continue、debugger、default、delete、do、else、enum、eval、export、extends、false、finally、for、function、if、implements、import、in、instanceof、interface、let、new、null、package、private、protected、public、return、static、super、switch、this、throw、true、try、typeof、var、void、while、with、yield

注释

源码中被 JavaScript 引擎忽略的部分就叫做注释,它的作用是对代码进行解释。JavaScript 提供两种注释的写法:一种是单行注释,用//起头;另一种是多行注释,放在//之间。此外,由于历史上 JavaScript 可以兼容 HTML 代码的注释,所以也被视为合法的单行注释

x = 1; <!-- x = 2;
--> x = 3;

上面代码中,只有x = 1会执行,其他的部分都被注释掉了。需要注意的是,-->只有在行首,才会被当成单行注释,否则会当作正常的运算

function countdown(n) {
  while (n --> 0) console.log(n);
}
countdown(3)
// 2
// 1
// 0

上面代码中,n --> 0实际上会当作n-- > 0,因此输出2、1、0

区块

JavaScript 使用大括号,将多个相关的语句组合在一起,称为“区块”(block)。对于var命令来说,JavaScript 的区块不构成单独的作用域(scope)

{  var a = 1; }
a // 1

上面代码在区块内部,使用var命令声明并赋值了变量a,然后在区块外部,变量a依然有效,区块对于var命令不构成单独的作用域,与不使用区块的情况没有任何区别。在 JavaScript 语言中,单独使用区块并不常见,区块往往用来构成其他更复杂的语法结构,比如for、if、while、function等

条件语句

JavaScript 提供if结构和switch结构,完成条件判断,即只有满足预设的条件,才会执行相应的语句

if 结构

if结构先判断一个表达式的布尔值,然后根据布尔值的真伪,执行不同的语句。所谓布尔值,指的是 JavaScript 的两个特殊值,true表示真,false表示伪

if...else 结构

if代码块后面,还可以跟一个else代码块,表示不满足条件时,所要执行的代码。对同一个变量进行多次判断时,多个if...else语句可以连写在一起

switch 结构

多个if...else连在一起使用的时候,可以转为使用更方便的switch结构

switch (fruit) {
  case "banana":
    // ...
    break;
  case "apple":
    // ...
    break;
  default:
    // ...
}

上面代码根据变量fruit的值,选择执行相应的case。如果所有case都不符合,则执行最后的default部分。需要注意的是,每个case代码块内部的break语句不能少,否则会接下去执行下一个case代码块,而不是跳出switch结构

switch语句部分和case语句部分,都可以使用表达式

switch (1 + 3) {
  case 2 + 2:
    // ...
    break;
  default:
    // ...
}

需要注意的是,switch语句后面的表达式,与case语句后面的表示式比较运行结果时,采用的是严格相等运算符(===),而不是相等运算符(==),这意味着比较时不会发生类型转换

var x = 1;
switch (x) {
  case true:
    console.log('x 发生类型转换');
    break;
  default:
    console.log('x 没有发生类型转换');
} // x 没有发生类型转换

上面代码中,由于变量x没有发生类型转换,所以不会执行case true的情况。这表明,switch语句内部采用的是“严格相等运算符”

三元运算符 ?:

JavaScript 还有一个三元运算符(即该运算符需要三个运算子)?:,也可以用于逻辑判断。这个三元运算符可以被视为if...else...的简写形式

循环语句

while 循环

While语句包括一个循环条件和一段代码块,只要条件为真,就不断循环执行代码块

for 循环

for语句是循环命令的另一种形式,可以指定循环的起点、终点和终止条件。所有for循环,都可以改写成while循环

do...while 循环

do...while循环与while循环类似,唯一的区别就是先运行一次循环体,然后判断循环条件

do {
  语句
} while (条件);

不管条件是否为真,do...while循环至少运行一次,这是这种结构最大的特点。另外,while语句后面的分号注意不要省略

break 语句和 continue 语句

break语句和continue语句都具有跳转作用,可以让代码不按既有的顺序执行。

break语句用于跳出代码块或循环

var i = 0;
while(i < 100) {
  console.log('i 当前为:' + i);
  i++;
  if (i === 10) break;
}

上面代码只会执行10次循环,一旦i等于10,就会跳出循环。for循环也可以使用break语句跳出循环

for (var i = 0; i < 5; i++) {
  console.log(i);
  if (i === 3)
    break;
}

上面代码执行到i等于3,就会跳出循环

continue语句用于立即终止本轮循环,返回循环结构的头部,开始下一轮循环

var i = 0;
while (i < 100){
  i++;
  if (i % 2 === 0) continue;
  console.log('i 当前为:' + i);
}

上面代码只有在i为奇数时,才会输出i的值。如果i为偶数,则直接进入下一轮循环。如果存在多重循环,不带参数的break语句和continue语句都只针对最内层循环

标签(label)

JavaScript 语言允许语句的前面有标签(label),相当于定位符,用于跳转到程序的任意位置,标签的格式如下

label:
  语句

标签可以是任意的标识符,但不能是保留字,语句部分可以是任意语句。标签通常与break语句和continue语句配合使用,跳出特定的循环

top:
  for (var i = 0; i < 3; i++){
    for (var j = 0; j < 3; j++){
      if (i === 1 && j === 1) break top;
      console.log('i=' + i + ', j=' + j);
    }
  }
// i=0, j=0
// i=0, j=1
// i=0, j=2
// i=1, j=0

上面代码为一个双重循环区块,break命令后面加上了top标签(注意,top不用加引号),满足条件时,直接跳出双层循环。如果break语句后面不使用标签,则只能跳出内层循环,进入下一次的外层循环。标签也可以用于跳出代码块

foo: {
  console.log(1);
  break foo;
  console.log('本行不会输出');
}
console.log(2);
// 1
// 2

上面代码执行到break foo,就会跳出区块。continue语句也可以与标签配合使用。

top:
  for (var i = 0; i < 3; i++){
    for (var j = 0; j < 3; j++){
      if (i === 1 && j === 1) continue top;
      console.log('i=' + i + ', j=' + j);
    }
  }
// i=0, j=0
// i=0, j=1
// i=0, j=2
// i=1, j=0
// i=2, j=0
// i=2, j=1
// i=2, j=2

上面代码中,continue命令后面有一个标签名,满足条件时,会跳过当前循环,直接进入下一轮外层循环。如果continue语句后面不使用标签,则只能进入下一轮的内层循环

数据类型

数据类型概述

简介

JavaScript 语言的每一个值,都属于某一种数据类型。JavaScript 的数据类型,共有六种。(ES6 又新增了第七种 Symbol 类型的值,这里不涉及)

1.数值(number):整数和小数(比如1和3.14)

2.字符串(string):文本(比如Hello World)。

3.布尔值(boolean):表示真伪的两个特殊值,即true(真)和false(假)

4.undefined:表示“未定义”或不存在,即由于目前没有定义,所以此处暂时没有任何值

5.null:表示空值,即此处的值为空。

6.对象(object):各种值组成的集合。

通常,数值、字符串、布尔值这三种类型,合称为原始类型(primitive type)的值,即它们是最基本的数据类型,不能再细分了。对象则称为合成类型(complex type)的值,因为一个对象往往是多个原始类型的值的合成,可以看作是一个存放各种值的容器。至于undefined和null,一般将它们看成两个特殊值。

对象是最复杂的数据类型,又可以分成三个子类型。

1.狭义的对象(object)

2.数组(array)

3.函数(function)

狭义的对象和数组是两种不同的数据组合方式,除非特别声明,本文的“对象”都特指狭义的对象。函数其实是处理数据的方法,JavaScript 把它当成一种数据类型,可以赋值给变量,这为编程带来了很大的灵活性,也为 JavaScript 的“函数式编程”奠定了基础

typeof 运算符

JavaScript 有三种方法,可以确定一个值到底是什么类型

1.typeof运算符

2.instanceof运算符

3.Object.prototype.toString方法

typeof运算符可以返回一个值的数据类型

typeof 123 // "number"
typeof '123' // "string"
typeof false // "boolean"
function f() {}
typeof f // "function"
typeof undefined // "undefined"

利用这一点,typeof可以用来检查一个没有声明的变量,而不报错

v  // ReferenceError: v is not defined
typeof v // "undefined"

上面代码中,变量v没有用var命令声明,直接使用就会报错。但是,放在typeof后面,就不报错了,而是返回undefined。实际编程中,这个特点通常用在判断语句

typeof window // "object"
typeof {} // "object"
typeof [] // "object"
typeof null // "object"

上面代码中,空数组([])的类型也是object,这表示在 JavaScript 内部,数组本质上只是一种特殊的对象。这里顺便提一下,instanceof运算符可以区分数组和对象,下节 会说到

null的类型是object,这是由于历史原因造成的。1995年的 JavaScript 语言第一版,只设计了五种数据类型(对象、整数、浮点数、字符串和布尔值),没考虑null,只把它当作object的一种特殊值。后来null独立出来,作为一种单独的数据类型,为了兼容以前的代码,typeof null返回object就没法改变了

null, undefined 和布尔值

null 和 undefined

概述

null与undefined都可以表示“没有”,含义非常相似。将一个变量赋值为undefined或null,老实说,语法效果几乎没区别。在if语句中,null和undefined都会被自动转为false,相等运算符(==)甚至直接报告两者相等

既然含义与用法都差不多,为什么要同时设置两个这样的值,这不是无端增加复杂度,令初学者困扰吗?这与历史原因有关。

1995年 JavaScript 诞生时,最初像 Java 一样,只设置了null表示"无"。根据 C 语言的传统,null可以自动转为0。但是,JavaScript 的设计者 Brendan Eich,觉得这样做还不够。首先,第一版的 JavaScript 里面,null就像在 Java 里一样,被当成一个对象,Brendan Eich 觉得表示“无”的值最好不是对象。其次,那时的 JavaScript 不包括错误处理机制,Brendan Eich 觉得,如果null自动转为0,很不容易发现错误。因此,他又设计了一个undefined。区别是这样的:null是一个表示“空”的对象,转为数值时为0;undefined是一个表示"此处无定义"的原始值,转为数值时为NaN

Number(null) // 0
5 + null // 5

Number(undefined) // NaN
5 + undefined // NaN

用法和含义

对于null和undefined,大致可以像这样理解:null表示空值,即该处的值现在为空;调用函数时,某个参数未设置任何值,这时就可以传入null,表示该参数为空。比如,某个函数接受引擎抛出的错误作为参数,如果运行过程中未出错,那么这个参数就会传入null,表示未发生错误。undefined表示“未定义”,下面是返回undefined的典型场景

var i; // 变量声明了,但没有赋值
i // undefined

function f(x) { // 调用函数时,应该提供的参数没有提供,该参数等于 undefined
  return x;
}
f() // undefined

var  o = new Object(); // 对象没有赋值的属性
o.p // undefined

function f() {} // 函数没有返回值时,默认返回 undefined
f() // undefined

布尔值

布尔值代表“真”和“假”两个状态。“真”用关键字true表示,“假”用关键字false表示。布尔值只有这两个值。

下列运算符会返回布尔值:

1.前置逻辑运算符: ! (Not)

2.相等运算符:===,!==,==,!=

3.比较运算符:>,>=,<,<=

如果JavaScript预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是除了下面六个值被转为false,其他值都视为true

undefined / null / false / 0 / NaN / "" 或 '' (空字符串)

注意,空数组([])和空对象({})对应的布尔值,都是true

数值

概述

整数和浮点数

JavaScript 内部,所有数字都是以64位浮点数形式储存,即使整数也是如此。所以,1与1.0是相同的,是同一个数。这就是说,JavaScript 语言的底层根本没有整数,所有数字都是小数(64位浮点数)。容易造成混淆的是,某些运算只有整数才能完成,此时 JavaScript 会自动把64位浮点数,转成32位整数,然后再进行运算。由于浮点数不是精确的值,所以涉及小数的比较和运算要特别小心

数值精度

根据国际标准 IEEE 754,JavaScript 浮点数的64个二进制位,从最左边开始,是这样组成的。

第1位:符号位,0表示正数,1表示负数

第2位到第12位(共11位):指数部分

第13位到第64位(共52位):小数部分(即有效数字)

符号位决定了一个数的正负,指数部分决定了数值的大小,小数部分决定了数值的精度。

指数部分一共有11个二进制位,因此大小范围就是0到2047。IEEE 754 规定,如果指数部分的值在0到2047之间(不含两个端点),那么有效数字的第一位默认总是1,不保存在64位浮点数之中。也就是说,有效数字这时总是1.xx...xx的形式,其中xx..xx的部分保存在64位浮点数之中,最长可能为52位。因此,JavaScript 提供的有效数字最长为53个二进制位。

(-1)^符号位 * 1.xx...xx * 2^指数部分

上面公式是正常情况下(指数部分在0到2047之间),一个数在 JavaScript 内部实际的表示形式。精度最多只能到53个二进制位,这意味着,绝对值小于2的53次方的整数,即-253到253,都可以精确表示

Math.pow(2, 53) // 9007199254740992
Math.pow(2, 53) + 1 // 9007199254740992
Math.pow(2, 53) + 2 // 9007199254740994
Math.pow(2, 53) + 3 // 9007199254740996
Math.pow(2, 53) + 4 // 9007199254740996

上面代码中,大于2的53次方后整数运算的结果开始出现错误。所以,大于2的53次方的数值都无法保持精度。由于2的53次方是一个16位的十进制数值,所以简单的法则就是:JavaScript 对15位的十进制数都可以精确处理

数值范围

根据标准,64位浮点数的指数部分长度是11个二进制位,意味着指数部分的最大值是2047(2的11次方减1)。也就是说,64位浮点数的指数部分的值最大为2047,分出一半表示负数,则 JavaScript 能够表示的数值范围为21024到2-1023(开区间),超出这个范围的数无法表示。

如果一个数大于等于2的1024次方,那么就会发生“正向溢出”,即 JavaScript 无法表示这么大的数,这时就会返回Infinity

Math.pow(2, 1024) // Infinity

如果一个数小于等于2的-1075次方(指数部分最小值-1023,再加上小数部分的52位),那么就会发生为“负向溢出”,即 JavaScript 无法表示这么小的数,这时会直接返回0

JavaScript 提供Number对象的MAX_VALUE和MIN_VALUE属性,返回可以表示的具体的最大值和最小值

Number.MAX_VALUE // 1.7976931348623157e+308
Number.MIN_VALUE // 5e-324

数值的表示法

JavaScript 的数值有多种表示方法,可以用字面形式直接表示,比如35(十进制)和0xFF(十六进制)。数值也可以采用科学计数法表示,下面是几个科学计数法的例子

123e3 // 123000
123e-3 // 0.123
-3.1E+12
.1e-23

科学计数法允许字母e或E的后面,跟着一个整数,表示这个数值的指数部分。以下两种情况,JavaScript 会自动将数值转为科学计数法表示,其他情况都采用字面形式直接表示

1.小数点前的数字多于21位

1234567890123456789012 // 1.2345678901234568e+21
123456789012345678901 // 123456789012345680000

2.小数点后的零多于5个

// 小数点后紧跟5个以上的零,就自动转为科学计数法
0.0000003 // 3e-7
// 否则,就保持原来的字面形式
0.000003 // 0.000003

数值的进制

使用字面量(literal)直接表示一个数值时,JavaScript 对整数提供四种进制的表示方法:十进制、十六进制、八进制、二进制

十进制:没有前导0的数值

八进制:有前缀0o或0O的数值,或者有前导0、且只用到0-7的八个阿拉伯数字的数值

十六进制:有前缀0x或0X的数值

二进制:有前缀0b或0B的数值

默认情况下,JavaScript 内部会自动将八进制、十六进制、二进制转为十进制

通常来说,有前导0的数值会被视为八进制,但是如果前导0后面有数字8和9,则该数值被视为十进制,处理时很容易造成混乱。ES5 的严格模式和 ES6,已经废除了这种表示法,但是浏览器为了兼容以前的代码,目前还继续支持这种表示法

特殊数值

JavaScript 提供了几个特殊的数值

正零和负零

前面说过,JavaScript 的64位浮点数之中,有一个二进制位是符号位。这意味着,任何一个数都有一个对应的负值,就连0也不例外。JavaScript 内部实际上存在2个0:一个是+0,一个是-0,区别就是64位浮点数表示法的符号位不同。它们是等价的

几乎所有场合,正零和负零都会被当作正常的0。唯一有区别的场合是,+0或-0当作分母,返回的值是不相等的

(1 / +0) === (1 / -0) // false

上面的代码之所以出现这样结果,是因为除以正零得到+Infinity,除以负零得到-Infinity,这两者是不相等的

NaN

含义

NaN是 JavaScript 的特殊值,表示“非数字”(Not a Number),主要出现在将字符串解析成数字出错的场合

0 / 0 // NaN

需要注意的是,NaN不是独立的数据类型,而是一个特殊数值,它的数据类型依然属于Number,使用typeof运算符可以看得很清楚

运算规则

NaN不等于任何值,包括它本身

NaN === NaN // false

数组的indexOf方法内部使用的是严格相等运算符,所以该方法对NaN不成立

[NaN].indexOf(NaN) // -1

NaN在布尔运算时被当作false

Boolean(NaN) // false

NaN与任何数(包括它自己)的运算,得到的都是NaN

Infinity

含义

Infinity表示“无穷”,用来表示两种场景。一种是一个正的数值太大,或一个负的数值太小,无法表示;另一种是非0数值除以0,得到Infinity。由于数值正向溢出(overflow)、负向溢出(underflow)和被0除,JavaScript 都不报错,所以单纯的数学运算几乎没有可能抛出错误。Infinity大于一切数值(除了NaN),-Infinity小于一切数值(除了NaN)。Infinity与NaN比较,总是返回false

Infinity > NaN // false
-Infinity > NaN // false
Infinity < NaN // false
-Infinity < NaN // false
运算规则

Infinity的四则运算,符合无穷的数学计算规则。0乘以Infinity,返回NaN;0除以Infinity,返回0;Infinity除以0,返回Infinity

0 * Infinity // NaN
0 / Infinity // 0
Infinity / 0 // Infinity

Infinity加上或乘以Infinity,返回的还是Infinity。Infinity减去或除以Infinity,得到NaN。Infinity与null计算时,null会转成0,等同于与0的计算。Infinity与undefined计算,返回的都是NaN

与数值相关的全局方法

parseInt()

基本用法

parseInt方法用于将字符串转为整数;如果字符串头部有空格,空格会被自动去除。如果parseInt的参数不是字符串,则会先转为字符串再转换。字符串转为整数的时候,是一个个字符依次转换,如果遇到不能转为数字的字符,就不再进行下去,返回已经转好的部分

parseInt('8a') // 8
parseInt('12**') // 12
parseInt('12.34') // 12
parseInt('15e2') // 15
parseInt('15px') // 15

如果字符串的第一个字符不能转化为数字(后面跟着数字的正负号除外),返回NaN。所以,parseInt的返回值只有两种可能,要么是一个十进制整数,要么是NaN。

如果字符串以0x或0X开头,parseInt会将其按照十六进制数解析;如果字符串以0开头,将其按照10进制解析;对于那些会自动转为科学计数法的数字,parseInt会将科学计数法的表示方法视为字符串,因此导致一些奇怪的结果

parseInt(1000000000000000000000.5) // 1
// 等同于
parseInt('1e+21') // 1

parseInt(0.0000008) // 8
// 等同于
parseInt('8e-7') // 8
进制转换

parseInt方法还可以接受第二个参数(2到36之间),表示被解析的值的进制,返回该值对应的十进制数。默认情况下,parseInt的第二个参数为10,即默认是十进制转十进制

parseInt('1000') // 1000
// 等同于
parseInt('1000', 10) // 1000

parseInt('1000', 2) // 8
parseInt('1000', 6) // 216
parseInt('1000', 8) // 512

这意味着,可以用parseInt方法进行进制的转换

如果第二个参数不是数值,会被自动转为一个整数。这个整数只有在2到36之间,才能得到有意义的结果,超出这个范围,则返回NaN。如果第二个参数是0、undefined和null,则直接忽略

parseInt('10', 37) // NaN
parseInt('10', 1) // NaN
parseInt('10', 0) // 10
parseInt('10', null) // 10
parseInt('10', undefined) // 10

如果字符串包含对于指定进制无意义的字符,则从最高位开始,只返回可以转换的数值。如果最高位无法转换,则直接返回NaN

前面说过,如果parseInt的第一个参数不是字符串,会被先转为字符串。这会导致一些令人意外的结果

parseInt(0x11, 36) // 43
parseInt(0x11, 2) // 1
// 等同于
parseInt(String(0x11), 36)
parseInt(String(0x11), 2)
// 等同于
parseInt('17', 36)
parseInt('17', 2)

上面代码中,十六进制的0x11会被先转为十进制的17,再转为字符串。然后,再用36进制或二进制解读字符串17,最后返回结果43和1。这种处理方式,对于八进制的前缀0,尤其需要注意

parseInt(011, 2) // NaN
// 等同于
parseInt(String(011), 2)
// 等同于
parseInt(String(9), 2)

上面代码中,第一行的011会被先转为字符串9,因为9不是二进制的有效字符,所以返回NaN。如果直接计算parseInt('011', 2),011则是会被当作二进制处理,返回3。JavaScript 不再允许将带有前缀0的数字视为八进制数,而是要求忽略这个0。但是,为了保证兼容性,大部分浏览器并没有部署这一条规定

parseFloat()

parseFloat方法用于将一个字符串转为浮点数。如果字符串符合科学计数法,则会进行相应的转换。如果字符串包含不能转为浮点数的字符,则不再进行往后转换,返回已经转好的部分

parseFloat('314e-2') // 3.14
parseFloat('0.0314E+2') // 3.14

parseFloat方法会自动过滤字符串前导的空格

parseFloat('\t\v\r12.34\n ') // 12.34

如果参数不是字符串,或者字符串的第一个字符不能转化为浮点数,则返回NaN

parseFloat([]) // NaN
parseFloat('FF2') // NaN
parseFloat('') // NaN

尤其值得注意,parseFloat会将空字符串转为NaN。这些特点使得parseFloat的转换结果不同于Number函数

parseFloat(true)  // NaN
Number(true) // 1
parseFloat(null) // NaN
Number(null) // 0
parseFloat('') // NaN
Number('') // 0
parseFloat('123.45#') // 123.45
Number('123.45#') // NaN

isNaN()

isNaN方法可以用来判断一个值是否为NaN。但是isNaN只对数值有效,如果传入其他值,会被先转成数值。比如,传入字符串的时候,字符串会被先转成NaN,所以最后返回true,这一点要特别引起注意。也就是说,isNaN为true的值,有可能不是NaN,而是一个字符串。出于同样的原因,对于对象和数组,isNaN也返回true。但是,对于空数组和只有一个数值成员的数组,isNaN返回false

isNaN([]) // false
isNaN([123]) // false
isNaN(['123']) // false

上面代码之所以返回false,原因是这些数组能被Number函数转成数值。因此,使用isNaN之前,最好判断一下数据类型

function myIsNaN(value) {
  return typeof value === 'number' && isNaN(value);
}

判断NaN更可靠的方法是,利用NaN为唯一不等于自身的值的这个特点,进行判断

function myIsNaN(value) {
  return value !== value;
}

isFinite()

isFinite方法返回一个布尔值,表示某个值是否为正常的数值

isFinite(Infinity) // false
isFinite(-Infinity) // false
isFinite(NaN) // false
isFinite(undefined) // false
isFinite(null) // true
isFinite(-1) // true

除了Infinity、-Infinity、NaN和undefined这几个值会返回false,isFinite对于其他的数值都会返回true

字符串

概述

定义

字符串就是零个或多个排在一起的字符,放在单引号或双引号之中。单引号字符串的内部,可以使用双引号。双引号字符串的内部,可以使用单引号。如果要在单引号字符串的内部,使用单引号,就必须在内部的单引号前面加上反斜杠,用来转义。双引号字符串内部使用双引号,也是如此。

由于 HTML 语言的属性值使用双引号,所以很多项目约定 JavaScript 语言的字符串只使用单引号。当然,只使用双引号也完全可以,重要的是坚持使用一种风格,不要一会使用单引号表示字符串,一会又使用双引号表示。字符串默认只能写在一行内,分成多行将会报错;如果长字符串必须分成多行,可以在每一行的尾部使用反斜杠

var longString = 'Long \
long \
long \
string';
longString  // "Long long long string"

注意,反斜杠的后面必须是换行符,而不能有其他字符(比如空格),否则会报错。连接运算符(+)可以连接多个单行字符串,将长字符串拆成多行书写,输出的时候也是单行。

var longString = 'Long '
  + 'long '
  + 'long '
  + 'string';

如果想输出多行字符串,有一种利用多行注释的变通方法

(function () { /*
line 1
line 2
line 3
*/}).toString().split('\n').slice(1, -1).join('\n')
// "line 1
// line 2
// line 3"

转义

反斜杠()在字符串内有特殊含义,用来表示一些特殊字符,所以又称为转义符。需要用反斜杠转义的特殊字符,主要有下面这些。

\0 :null(\u0000)
\b :后退键(\u0008)
\f :换页符(\u000C)
\n :换行符(\u000A)
\r :回车键(\u000D)
\t :制表符(\u0009)
\v :垂直制表符(\u000B)
\' :单引号(\u0027)
\" :双引号(\u0022)
\\ :反斜杠(\u005C)

反斜杠还有三种特殊用法

HHH

反斜杠后面紧跟三个八进制数(000到377),代表一个字符。HHH对应该字符的 Unicode 码点,比如251表示版权符号。显然,这种方法只能输出256种字符

xHH

x后面紧跟两个十六进制数(00到FF),代表一个字符。HH对应该字符的 Unicode 码点,比如xA9表示版权符号。这种方法也只能输出256种字符

uXXXX

u后面紧跟四个十六进制数(0000到FFFF),代表一个字符。XXXX对应该字符的 Unicode 码点,比如u00A9表示版权符号

'\251' // "©"
'\xA9' // "©"
'\u00A9' // "©"
'\172' === 'z' // true
'\x7A' === 'z' // true
'\u007A' === 'z' // true

如果在非特殊字符前面使用反斜杠,则反斜杠会被省略。如果字符串的正常内容之中,需要包含反斜杠,则反斜杠前面需要再加一个反斜杠,用来对自身转义

字符串与数组

字符串可以被视为字符数组,因此可以使用数组的方括号运算符,用来返回某个位置的字符(位置编号从0开始)。如果方括号中的数字超过字符串的长度,或者方括号中根本不是数字,则返回undefined

'abc'[3] // undefined
'abc'[-1] // undefined
'abc'['x'] // undefined

但是,字符串与数组的相似性仅此而已。实际上,无法改变字符串之中的单个字符

length 属性

length属性返回字符串的长度,该属性也是无法改变的

字符集

JavaScript 使用 Unicode 字符集。JavaScript 引擎内部,所有字符都用 Unicode 表示。JavaScript 不仅以 Unicode 储存字符,还允许直接在程序中使用 Unicode 码点表示字符,即将字符写成uxxxx的形式,其中xxxx代表该字符的 Unicode 码点。比如,u00A9代表版权符号。解析代码的时候,JavaScript 会自动识别一个字符是字面形式表示,还是 Unicode 形式表示。输出给用户的时候,所有字符都会转成字面形式

var f\u006F\u006F = 'abc';
f // "abc"

我们还需要知道,每个字符在 JavaScript 内部都是以16位(即2个字节)的 UTF-16 格式储存。也就是说,JavaScript 的单位字符长度固定为16位长度,即2个字节。但是,UTF-16 有两种长度:对于码点在U+0000到U+FFFF之间的字符,长度为16位(即2个字节);对于码点在U+10000到U+10FFFF之间的字符,长度为32位(即4个字节),而且前两个字节在0xD800到0xDBFF之间,后两个字节在0xDC00到0xDFFF之间。举例来说,码点U+1D306对应的字符为𝌆,它写成 UTF-16 就是0xD834 0xDF06。

JavaScript 对 UTF-16 的支持是不完整的,由于历史原因,只支持两字节的字符,不支持四字节的字符。这是因为 JavaScript 第一版发布的时候,Unicode 的码点只编到U+FFFF,因此两字节足够表示了。后来,Unicode 纳入的字符越来越多,出现了四字节的编码。但是,JavaScript 的标准此时已经定型了,统一将字符长度限制在两字节,导致无法识别四字节的字符。例如四字节字符𝌆,浏览器会正确识别这是一个字符,但是 JavaScript 无法识别,会认为这是两个字符

'𝌆'.length // 2

总结一下,对于码点在U+10000到U+10FFFF之间的字符,JavaScript 总是认为它们是两个字符(length属性为2)。所以处理的时候,必须把这一点考虑在内,也就是说,JavaScript 返回的字符串长度可能是不正确的

Base64 转码

有时,文本里面包含一些不可打印的符号,比如 ASCII 码0到31的符号都无法打印出来,这时可以使用 Base64 编码,将它们转成可以打印的字符。另外,有时需要以文本格式传递二进制数据,那么也可以使用 Base64 编码。所谓 Base64 就是一种编码方法,可以将任意值转成 0~9、A~Z、a-z、+和/这64个字符组成的可打印字符。使用它的主要目的,不是为了加密,而是为了不出现特殊字符,简化程序的处理。

JavaScript 原生提供两个 Base64 相关的方法:

btoa():任意值转为 Base64 编码

atob():Base64 编码转为原来的值

var string = 'Hello World!';
btoa(string) // "SGVsbG8gV29ybGQh"
atob('SGVsbG8gV29ybGQh') // "Hello World!"

注意,这两个方法不适合非 ASCII 码的字符,会报错。要将非 ASCII 码字符转为 Base64 编码,必须中间插入一个转码环节,再使用这两个方法

function b64Encode(str) {
  return btoa(encodeURIComponent(str));
}
function b64Decode(str) {
  return decodeURIComponent(atob(str));
}
b64Encode('你好') // "JUU0JUJEJUEwJUU1JUE1JUJE"
b64Decode('JUU0JUJEJUEwJUU1JUE1JUJE') // "你好"

对象

概述

生成方法

对象(object)是 JavaScript 语言的核心概念,也是最重要的数据类型。什么是对象?简单说,对象就是一组“键值对”(key-value)的集合,是一种无序的复合数据集合

键名

对象的所有键名都是字符串(ES6 又引入了 Symbol 值也可以作为键名),所以加不加引号都可以

var obj = {
  foo: 'Hello',
  bar: 'World'
};
等同于
var obj = {
  'foo': 'Hello',
  'bar': 'World'
};

如果键名是数值,会被自动转为字符串。如果键名不符合标识名的条件(比如第一个字符为数字,或者含有空格或运算符),且也不是数字,则必须加上引号,否则会报错

var obj = { // 报错
  1p: 'Hello World'
};
var obj = { // 不报错
  '1p': 'Hello World',
  'h w': 'Hello World',
  'p+q': 'Hello World'
};

上面对象的三个键名,都不符合标识名的条件,所以必须加上引号。对象的每一个键名又称为“属性”(property),它的“键值”可以是任何数据类型。如果一个属性的值为函数,通常把这个属性称为“方法”,它可以像函数那样调用

var obj = {
  p: function (x) {
    return 2 * x;
  }
};
obj.p(1) // 2

如果属性的值还是一个对象,就形成了链式引用

var o1 = {};
var o2 = { bar: 'hello' };
o1.foo = o2;
o1.foo.bar // "hello"

属性可以动态创建,不必在对象声明时就指定

对象的引用

如果不同的变量名指向同一个对象,那么它们都是这个对象的引用,也就是说指向同一个内存地址。修改其中一个变量,会影响到其他所有变量;如果取消某一个变量对于原对象的引用,不会影响到另一个变量

var x = 1;
var y = x;
x = 2;
y // 1

上面的代码中,当x的值发生变化后,y的值并不变,这就表示y和x并不是指向同一个内存地址

表达式还是语句

对象采用大括号表示,这导致了一个问题:如果行首是一个大括号,它到底是表达式还是语句?

{ foo: 123 }

JavaScript 引擎读到上面这行代码,会发现可能有两种含义。第一种:这是一个表达式,表示一个包含foo属性的对象;第二种:这是一个语句,表示一个代码区块,里面有一个标签foo,指向表达式123。为了避免这种歧义,JavaScript 引擎的做法是,如果遇到这种情况,无法确定是对象还是代码块,一律解释为代码块

如果要解释为对象,最好在大括号前加上圆括号。因为圆括号的里面,只能是表达式,所以确保大括号只能解释为对象

({ foo: 123 }) // 正确
({ console.log(123) }) // 报错

这种差异在eval语句(作用是对字符串求值)中反映得最明显

eval('{foo: 123}') // 123
eval('({foo: 123})') // {foo: 123}

上面代码中,如果没有圆括号,eval将其理解为一个代码块;加上圆括号以后,就理解成一个对象

属性的操作

属性的读取

读取对象的属性,有两种方法,一种是使用点运算符,还有一种是使用方括号运算符

var obj = {
  p: 'Hello World'
};
obj.p // "Hello World"
obj['p'] // "Hello World"

请注意,如果使用方括号运算符,键名必须放在引号里面,否则会被当作变量处理

var foo = 'bar';
var obj = {
  foo: 1,
  bar: 2
};
obj.foo  // 1
obj[foo]  // 2

上面代码中,引用对象obj的foo属性时,如果使用点运算符,foo就是字符串;如果使用方括号运算符但不使用引号,foo就是一个变量,指向字符串bar

方括号运算符内部还可以使用表达式

obj['hello' + ' world']
obj[3 + 3]

数字键可以不加引号,因为会自动转成字符串

var obj = {
  0.7: 'Hello World'
};
obj['0.7'] // "Hello World"
obj[0.7] // "Hello World"

注意,数值键名不能使用点运算符(因为会被当成小数点),只能使用方括号运算符

属性的赋值

点运算符和方括号运算符,不仅可以用来读取值,还可以用来赋值。JavaScript 允许属性的“后绑定”,也就是说,你可以在任意时刻新增属性,没必要在定义对象的时候,就定义好属性

var obj = { p: 1 };
// 等价于
var obj = {};
obj.p = 1;

属性的查看

查看一个对象本身的所有属性,可以使用Object.keys方法

var obj = {
  key1: 1,
  key2: 2
};
Object.keys(obj); // ['key1', 'key2']

属性的删除:delete 命令

delete命令用于删除对象的属性,删除成功后返回true

var obj = { p: 1 };
Object.keys(obj) // ["p"]
delete obj.p // true
obj.p // undefined
Object.keys(obj) // []

注意,删除一个不存在的属性,delete不报错,而且返回true。因此不能根据delete命令的结果,认定某个属性是存在的。只有一种情况,delete命令会返回false,那就是该属性存在,且不得删除

var obj = Object.defineProperty({}, 'p', { //Object.defineProperty()方法会直接在一个对象上定义一个新属性,或修改一个对象的现有属性,并返回这个对象。默认使用 Object.defineProperty() 添加的属性值是不可修改的
  value: 123,
  configurable: false //表示能否通过delete删除属性从而重新定义属性
});
obj.p // 123
delete obj.p // false

注意:delete命令只能删除对象本身的属性,无法删除继承的属性

var obj = {};
delete obj.toString // true
obj.toString // function toString() { [native code] }

上面代码中,toString是对象obj继承的属性,虽然delete命令返回true,但该属性并没有被删除,依然存在。这个例子还说明,即使delete返回true,该属性依然可能读取到值

属性是否存在:in 运算符

in运算符用于检查对象是否包含某个属性(注意:检查的是键名,不是键值),如果包含就返回true,否则返回false。它的左边是一个字符串,表示属性名,右边是一个对象

var obj = { p: 1 };
'p' in obj // true
'toString' in obj // true

in运算符的一个问题是,它不能识别哪些属性是对象自身的,哪些属性是继承的。就像上面代码中,对象obj本身并没有toString属性,但是in运算符会返回true,因为这个属性是继承的。

这时,可以使用对象的hasOwnProperty方法判断一下,是否为对象自身的属性

var obj = {};
if ('toString' in obj) {
  console.log(obj.hasOwnProperty('toString')) // false
}

属性的遍历:for...in 循环

for...in循环用来遍历一个对象的全部属性

var obj = {a: 1, b: 2, c: 3};
for (var i in obj) {
  console.log('键名:', i);
  console.log('键值:', obj[i]);
}
// 键名: a
// 键值: 1
// 键名: b
// 键值: 2
// 键名: c
// 键值: 3

for...in循环有两个使用注意点:

1.它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性

2.它仅遍历对象自身的属性,会忽略掉那些从原型链上继承到的属性

举例来说,对象都继承了toString属性,但是for...in循环不会遍历到这个属性

var obj = {};
// toString 属性是存在的
obj.toString // toString() { [native code] }
for (var p in obj) {
  console.log(p);
} // 没有任何输出

如果继承的属性是可遍历的,那么就会被for...in循环遍历到。但一般情况下都是只想遍历对象自身的属性,所以使用for...in的时候,应该结合使用hasOwnProperty方法,在循环内部判断一下,某个属性是否为对象自身的属性

var person = { name: '老张' };
for (var key in person) {
  if (person.hasOwnProperty(key)) {
    console.log(key);
  }
}
// name

with 语句

with语句的格式如下:

with (对象) {
  语句;
}

它的作用是操作同一个对象的多个属性时,提供一些书写的方便

// 例一
var obj = {
  p1: 1,
  p2: 2,
};
with (obj) {
  p1 = 4;
  p2 = 5;
}
// 等同于
obj.p1 = 4;
obj.p2 = 5;

// 例二
with (document.links[0]){
  console.log(href);
  console.log(title);
  console.log(style);
}
// 等同于
console.log(document.links[0].href);
console.log(document.links[0].title);
console.log(document.links[0].style);

注意:如果with区块内部有变量的赋值操作,必须是当前对象已经存在的属性,否则会创造一个当前作用域的全局变量

var obj = {};
with (obj) {
  p1 = 4;
  p2 = 5;
}
obj.p1 // undefined
p1 // 4

上面代码中,对象obj并没有p1属性,对p1赋值等于创造了一个全局变量p1。正确的写法应该是,先定义对象obj的属性p1,然后在with区块内操作它

with区块没有改变作用域,它的内部依然是当前作用域。这造成了with语句的一个很大的弊病,就是绑定对象不明确。可以考虑用一个临时变量代替with

with(obj1.obj2.obj3) {
  console.log(p1 + p2);
}
// 可以写成
var temp = obj1.obj2.obj3;
console.log(temp.p1 + temp.p2);

函数

函数是一段可以反复调用的代码块。函数还能接受输入的参数,不同的参数会返回不同的值

概述

函数的声明

JavaScript 有三种声明函数的方法

function 命令

function命令声明的代码区块,就是一个函数。function命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面

function print(s) {
  console.log(s);
}

上面的代码命名了一个print函数,以后使用print()这种形式,就可以调用相应的代码。这叫做函数的声明(Function Declaration)

函数表达式

除了用function命令声明函数,还可以采用变量赋值的写法

var print = function(s) {
  console.log(s);
};

这种写法将一个匿名函数赋值给变量。这时,这个匿名函数又称函数表达式(Function Expression),因为赋值语句的等号右侧只能放表达式。采用函数表达式声明函数时,function命令后面不带有函数名。如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效

var print = function x(){
  console.log(typeof x);
};
x // ReferenceError: x is not defined
print() // function

上面代码在函数表达式中加入了函数名x。这个x只在函数体内部可用,指代函数表达式本身,其他地方都不可用。这种写法的用处有两个,一是可以在函数体内部调用自身,二是方便除错(除错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数)

注意:函数的表达式需要在语句结尾加上分号表示语句结束。而函数的声明在结尾的大括号后面不用加分号。总的来说,这两种声明函数的方式,差别很细微,可以近似认为是等价的

Function 构造函数

第三种声明函数的方式是Function构造函数

var add = new Function(
  'x',
  'y',
  'return x + y'
);
// 等同于
function add(x, y) {
  return x + y;
}

总的来说,这种声明函数的方式非常不直观,几乎无人使用

函数的重复声明

如果同一个函数被多次声明,后面的声明就会覆盖前面的声明

function f() {
  console.log(1);
}
f() // 2
function f() {
  console.log(2);
}
f() // 2

上面代码中,后一次的函数声明覆盖了前面一次。而且,由于函数名的提升,前一次声明在任何时候都是无效的

圆括号运算符,return 语句和递归

调用函数时,要使用圆括号运算符。圆括号之中,可以加入函数的参数

function add(x, y) {
  return x + y;
}
add(1, 1) // 2

函数体内部的return语句表示返回。JavaScript 引擎遇到return语句就直接返回return后面的那个表达式的值,不再执行后面语句。return语句不是必需的,如果没有的话,该函数就不返回任何值,或者说返回undefined。

函数可以调用自身,这就是递归(recursion)。下面就是通过递归,计算斐波那契数列的代码

function fib(num) {
  if (num === 0) return 0;
  if (num === 1) return 1;
  return fib(num - 2) + fib(num - 1);
}

fib(6) // 8

第一等公民

JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。函数只是一个可以执行的值,此外并无特殊之处。由于函数与其他数据类型地位平等,所以在 JavaScript 语言中又称函数为第一等公民

function add(x, y) {
  return x + y;
}
var operator = add; // 将函数赋值给一个变量
function a(op){ // 将函数作为参数和返回值
  return op;
}
a(add)(1, 1) // 2

函数名的提升

JavaScript 引擎将函数名视同变量名,所以采用function命令声明函数时,整个函数会像变量声明一样,被提升到代码头部

f();
function f() {}

表面上,上面代码好像在声明之前就调用了函数f。但是实际上,由于“变量提升”,函数f被提升到了代码头部,也就是在调用之前已经声明了。但是,如果采用赋值语句定义函数,JavaScript 就会报错

f();
var f = function (){}; // TypeError: undefined is not a function
等同于
var f;
f();
f = function () {};  

上面代码调用f的时候,f只是被声明了,还没有被赋值,等于undefined,所以会报错。因此,如果同时采用function命令和赋值语句声明同一个函数,最后总是采用赋值语句的定义

var f = function () {
  console.log('1');
}
function f() {
  console.log('2');
}
f() // 1

函数的属性和方法

name 属性

函数的name属性返回函数的名字

function f1() {}
f1.name // "f1"

如果是通过变量赋值定义的函数,那么name属性返回变量名

var f2 = function () {};
f2.name // "f2"

但是,上面这种情况,只有在变量的值是一个匿名函数时才是如此。如果变量的值是一个具名函数,那么name属性返回function关键字之后的那个函数名

var f3 = function myName() {};
f3.name // 'myName'

上面代码中,f3.name返回函数表达式的名字。注意,真正的函数名还是f3,而myName这个名字只在函数体内部可用。name属性的一个用处,就是获取参数函数的名字

var myFunc = function () {};
function test(f) {
  console.log(f.name);
}
test(myFunc) // myFunc

length 属性

函数的length属性返回函数预期传入的参数个数,即函数定义之中的参数个数

function f(a, b) {}
f.length // 2

上面代码定义了空函数f,它的length属性就是定义时的参数个数。不管调用时输入了多少个参数,length属性始终等于2。length属性提供了一种机制,判断定义时和调用时参数的差异,以便实现面向对象编程的“方法重载”(overload)

toString()

函数的toString方法返回一个字符串,内容是函数的源码

function f() {
  /*
  这是一个
  多行注释
*/
  a();
  b();;
}

f.toString()
// function f() {
//  /*
//  这是一个
//  多行注释
// */
//  a();
//  b();
// }

函数内部的注释也可以返回。利用这点可以变相实现多行字符串

var multiline = function (fn) {
  var arr = fn.toString().split('\n');
  return arr.slice(1, arr.length - 1).join('\n');
};
function f() {/*
  这是一个
  多行注释
*/}
multiline(f);
// " 这是一个
//   多行注释"

函数作用域

定义

作用域(scope)指变量存在的范围。在 ES5 的规范中,JavaScript 只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;另一种是函数作用域,变量只在函数内部存在。ES6 又新增了块级作用域,这里不涉及。对于顶层函数来说,函数外部声明的变量就是全局变量(global variable),它可以在函数内部读取。函数内部定义的变量外部无法读取,称为“局部变量”(local variable)。函数内部定义的变量,会在该作用域内覆盖同名全局变量

var v = 1;
function f(){
  var v = 2;
  console.log(v);
}
f() // 2
v // 1

函数内部的变量提升

与全局作用域一样,函数作用域内部也会产生“变量提升”现象。var命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部

函数本身的作用域

函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关

var a = 1;
var x = function () {
  console.log(a);
};
function f() {
  var a = 2;
  x();
}
f() // 1

上面代码中,函数x是在函数f的外部声明的,所以它的作用域绑定外层,内部变量a不会到函数f体内取值,所以输出1,而不是2。总之,函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。很容易犯错的一点是,如果函数A调用函数B,却没考虑到函数B不会引用函数A的内部变量

同样的,函数体内部声明的函数,作用域绑定函数体内部。正是这种机制构成了“闭包”现象

function foo() {
  var x = 1;
  function bar() {
    console.log(x);
  }
  return bar;
}
var x = 2;
var f = foo();
f() // 1

参数

概述

函数运行的时候,有时需要提供外部数据,不同的外部数据会得到不同的结果,这种外部数据就叫参数

参数的省略

函数参数不是必需的,JavaScript 允许省略参数

function f(a, b) {
  return a;
}
f(1, 2, 3) // 1
f(1) // 1
f() // undefined
f.length // 2

JavaScript中省略的参数值就变为undefined。但是,没办法只省略靠前的参数保留靠后的参数。如果一定要省略靠前的参数,只有显式传入undefined

function f(a, b) {
  return a;
}
//如果省略第一个参数,就会报错
f( , 1) // SyntaxError: Unexpected token ,(…)
f(undefined, 1) // undefined

传递方式

函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)。这意味着,在函数体内修改参数值,不会影响到函数外部。但是,如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值

var obj = { p: 1 };
function f(o) {
  o.p = 2;
}
f(obj);
obj.p // 2

上面代码中,传入函数f的是参数对象obj的地址。因此,在函数内部修改obj的属性p,会影响到原始值。但是,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值

var obj = [1, 2, 3];
function f(o) {
  o = [2, 3, 4];
}
f(obj);
obj // [1, 2, 3]

同名参数

如果有同名的参数,则取最后出现的那个值

function f(a, a) {
  console.log(a);
}
f(1) // undefined

上面代码中,函数f有两个参数,且参数名都是a。取值的时候,以后面的a为准,即使后面的a没有值或被省略,也是以其为准。这时,如果要获得第一个a的值,可以使用arguments对象

function f(a, a) {
  console.log(arguments[0]);
}
f(1) // 1

arguments 对象

定义

由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是arguments对象的由来。arguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用

var f = function (one) {
  console.log(arguments[0]);
  console.log(arguments[1]);
  console.log(arguments[2]);
}
f(1, 2, 3)
// 1
// 2
// 3

正常模式下,arguments对象可以在运行时修改

var f = function(a, b) {
  arguments[0] = 3;
  arguments[1] = 2;
  return a + b;
}
f(1, 1) // 5

上面代码中,函数f调用时传入的参数,在函数内部被修改成3和2。严格模式下,arguments对象与函数参数不具有联动关系。也就是说:严格模式下修改arguments对象不会影响到实际的函数参数

通过arguments对象的length属性,可以判断函数调用时到底带几个参数

function f() {
  return arguments.length;
}
f(1, 2, 3) // 3
f(1) // 1
f() // 0
与数组的关系

虽然arguments很像数组,但它是一个对象。数组专有的方法(比如slice和forEach),不能在arguments对象上直接使用。如果要让arguments对象使用数组方法,真正的解决方法是将arguments转为真正的数组。下面是两种常用的转换方法:slice方法和逐一填入新数组

var args = Array.prototype.slice.call(arguments);
// 或者
var args = [];
for (var i = 0; i < arguments.length; i++) {
  args.push(arguments[i]);
}
callee 属性

arguments对象带有一个callee属性,返回它所对应的原函数

var f = function () {
  console.log(arguments.callee === f);
}
f() // true

可以通过arguments.callee,达到调用函数自身的目的。这个属性在严格模式里面是禁用的

函数的其他知识点

闭包

闭包(closure)是 JavaScript 语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。理解闭包,首先必须理解变量作用域。前面提到,JavaScript 有两种作用域:全局作用域和函数作用域。函数内部可以直接读取全局变量。但是函数外部无法读取函数内部声明的变量。如果出于种种原因,需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数

function f1() {
  var n = 999;
  function f2() {
    console.log(n);
  }
  return f2;
}
var result = f1();
result(); // 999

上面代码中,函数f1的返回值就是函数f2,由于f2可以读取f1的内部变量,所以就可以在外部获得f1的内部变量了。

闭包就是函数f2,即能够读取其他函数内部变量的函数。由于在 JavaScript 语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境,比如f2记住了它诞生的环境f1,所以从f2可以得到f1的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在

闭包的另一个用处,是封装对象的私有属性和私有方法

function Person(name) {
  var _age;
  function setAge(n) {
    _age = n;
  }
  function getAge() {
    return _age;
  }
  return {
    name: name,
    getAge: getAge,
    setAge: setAge
  };
}
var p1 = Person('张三');
p1.setAge(25);
p1.getAge() // 25

上面代码中,函数Person的内部变量_age,通过闭包getAge和setAge,变成了返回对象p1的私有变量。

注意:外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题

立即调用的函数表达式(IIFE)

在 JavaScript 中,圆括号()是一种运算符,跟在函数名之后,表示调用该函数。比如,print()就表示调用print函数

function这个关键字即可以当作语句,也可以当作表达式。为了避免解析上的歧义,JavaScript 引擎规定,如果function关键字出现在行首,一律解释成语句

// 语句
function f() {}
// 表达式
var f = function f() {}

当我们需要在定义函数之后立即调用该函数时,不能只是在函数的定义之后加圆括号。解决方法就是不要让function出现在行首,让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面

(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();

上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表示式,而不是函数定义语句,所以就避免了错误。这就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称 IIFE。注意,上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个 IIFE,JavaScript 会将它们连在一起解释,将第二行解释为第一行的参数,从而报错

推而广之,任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果,比如下面写法

var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();
!function () { /* code */ }();
~function () { /* code */ }();
-function () { /* code */ }();
+function () { /* code */ }();

通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:一是不必为函数命名,避免了污染全局变量;二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量

// 写法一
var tmp = newData;
processData(tmp);
storeData(tmp);

// 写法二
(function () {
  var tmp = newData;
  processData(tmp);
  storeData(tmp);
}());

上面代码中,写法二比写法一更好,因为完全避免了污染全局变量

eval 命令

基本用法

eval命令接受一个字符串作为参数,并将这个字符串当作语句执行

eval('var a = 1;');
a // 1

上面代码将字符串当作语句运行,生成了变量a。如果参数字符串无法当作语句运行,那么就会报错

eval('3x') // Uncaught SyntaxError: Invalid or unexpected token

放在eval中的字符串,应该有独自存在的意义,不能用来与eval以外的命令配合使用。举例来说,下面的代码将会报错

eval('return;'); // Uncaught SyntaxError: Illegal return statement

上面代码会报错,因为return不能单独使用,必须在函数中使用。如果eval的参数不是字符串,那么会原样返回

eval(123) // 123

eval没有自己的作用域,都在当前作用域内执行,因此可能会修改当前作用域的变量的值,造成安全问题

var a = 1;
eval('a = 2');
a // 2

上面代码中,eval命令修改了外部变量a的值,因此eval有安全风险。为了防止这种风险,JavaScript 规定,如果使用严格模式,eval内部声明的变量,不会影响到外部作用域

(function f() {
  'use strict';
  eval('var foo = 123');
  console.log(foo);  // ReferenceError: foo is not defined
})()

上面代码中,函数f内部是严格模式,这时eval内部声明的foo变量,就不会影响到外部。不过,即使在严格模式下,eval依然可以读写当前作用域的变量

(function f() {
  'use strict';
  var foo = 1;
  eval('foo = 2');
  console.log(foo);  // 2
})()

上面代码中,严格模式下,eval内部还是改写了外部变量,可见安全风险依然存在。总之,eval的本质是在当前作用域中注入代码。由于安全风险和不利于 JavaScript 引擎优化执行速度,所以一般不推荐使用。通常情况下,eval最常见的场合是解析 JSON 数据的字符串,不过正确的做法应该是使用原生的JSON.parse方法

eval 的别名调用

前面说过eval不利于引擎优化执行速度。更麻烦的是,还有下面这种情况,引擎在静态代码分析的阶段,根本无法分辨执行的是eval

var m = eval;
m('var x = 1');
x // 1

上面代码中,变量m是eval的别名。静态代码分析阶段,引擎分辨不出m('var x = 1')执行的是eval命令。为了保证eval的别名不影响代码优化,JavaScript 的标准规定,凡是使用别名执行eval,eval内部一律是全局作用域

var a = 1;
function f() {
  var a = 2;
  var e = eval;
  e('console.log(a)');
}
f() // 1

上面代码中,eval是别名调用,所以即使它是在函数中,它的作用域还是全局作用域,因此输出的a为全局变量。这样的话,引擎就能确认e()不会对当前的函数作用域产生影响,优化的时候就可以把这一行排除掉。eval的别名调用的形式五花八门,只要不是直接调用,都属于别名调用,因为引擎只能分辨eval()这一种形式是直接调用

eval.call(null, '...')
window.eval('...')
(1, eval)('...')
(eval, eval)('...')

上面这些形式都是eval的别名调用,作用域都是全局作用域

数组

定义

数组(array)是按次序排列的一组值。每个值的位置都有编号(从0开始),整个数组用方括号表示

var arr = ['a', 'b', 'c'];

除了在定义时赋值,数组也可以先定义后赋值;任何类型的数据都可以放入数组;如果数组的元素还是数组,就形成了多维数组

数组的本质

本质上,数组属于一种特殊的对象。typeof运算符会返回数组的类型是object;数组的特殊性体现在,它的键名是按次序排列的一组整数(0,1,2...)

var arr = ['a', 'b', 'c'];
Object.keys(arr) // ["0", "1", "2"]

上面代码中,Object.keys方法返回数组的所有键名。由于数组成员的键名是固定的(默认总是0、1、2...),因此数组不用为每个元素指定键名,而对象的每个成员都必须指定键名。JavaScript 语言规定,对象的键名一律为字符串,所以,数组的键名其实也是字符串。之所以可以用数值读取,是因为非字符串的键名会被转为字符串。这点在赋值时也成立,一个值总是先转成字符串再进行赋值

var arr = ['a', 'b', 'c'];
arr['0'] // 'a'
arr[0] // 'a'

对象有两种读取成员的方法:点结构(object.key)和方括号结构(object[key])。但是,对于数值的键名,不能使用点结构

length 属性

数组的length属性,返回数组的成员数量。JavaScript 使用一个32位整数保存数组的元素个数,这意味着数组成员最多只有4294967295个(232 - 1)个,也就是说length属性的最大值就是4294967295。只要是数组,就一定有length属性。该属性是一个动态值,等于键名中的最大整数加上1

var arr = ['a', 'b'];
arr.length // 2
arr[2] = 'c';
arr.length // 3
arr[9] = 'd';
arr.length // 10
arr[1000] = 'e';
arr.length // 1001

上面代码表示:数组的数字键不需要连续,length属性的值总是比最大的那个整数键大1。另外,这也表明数组是一种动态的数据结构,可以随时增减数组的成员,length属性是可写的。如果人为设置一个小于当前成员个数的值,该数组的成员会自动减少到length设置的值

var arr = [ 'a', 'b', 'c' ];
arr.length // 3
arr.length = 2;
arr // ["a", "b"]

清空数组的一个有效方法,就是将length属性设为0。如果人为设置length大于当前元素个数,则数组的成员数量会增加到这个值,新增的位置都是空位,,读取新增的位置都会返回undefined。如果人为设置length为不合法的值JavaScript 会报错

// 设置负值
[].length = -1 // RangeError: Invalid array length
// 数组元素个数大于等于2的32次方
[].length = Math.pow(2, 32) // RangeError: Invalid array length
// 设置字符串
[].length = 'abc' // RangeError: Invalid array length

值得注意的是,由于数组本质上是一种对象,所以可以为数组添加属性,但是这不影响length属性的值

var a = [];
a['p'] = 'abc';
a.length // 0
a[2.1] = 'abc';
a.length // 0

上面代码将数组的键分别设为字符串和小数,结果都不影响length属性。因为,length属性的值就是等于最大的数字键加1,而这个数组没有整数键,所以length属性保持为0。如果数组的键名是添加超出范围的数值,该键名会自动转为字符串

var arr = [];
arr[-1] = 'a';
arr[Math.pow(2, 32)] = 'b';
arr.length // 0
arr[-1] // "a"
arr[4294967296] // "b"

上面代码中为数组arr添加了两个不合法的数字键,结果length属性没有发生变化,这些数字键都变成了字符串键名。最后两行之所以会取到值,是因为取键值时,数字键名会默认转为字符串

in 运算符

检查某个键名是否存在的运算符in,适用于对象,也适用于数组

var arr = [ 'a', 'b', 'c' ];
2 in arr  // true
'2' in arr // true
4 in arr // false

上面代码表明,数组存在键名为2的键;由于键名都是字符串,所以数值2会自动转成字符串。注意:如果数组的某个位置是空位,in运算符返回false

var arr = [];
arr[100] = 'a';
100 in arr // true
1 in arr // false

for...in 循环和数组的遍历

for...in循环不仅可以遍历对象,也可以遍历数组,毕竟数组只是一种特殊对象;for...in不仅会遍历数组所有的数字键,还会遍历非数字键;所以不推荐使用for...in遍历数组

var a = [1, 2, 3];
a.foo = true;
for (var key in a) {
  console.log(key);
}
// 0
// 1
// 2
// foo

数组的遍历可以考虑使用for循环或while循环

var a = [1, 2, 3];
// for循环
for(var i = 0; i < a.length; i++) {
  console.log(a[i]);
}
// while循环
var i = 0;
while (i < a.length) {
  console.log(a[i]);
  i++;
}
var l = a.length;
while (l--) { //逆向遍历
  console.log(a[l]);
}

数组的forEach方法,也可以用来遍历数组

var colors = ['red', 'green', 'blue'];
colors.forEach(function (color) {
  console.log(color);
});
// red
// green
// blue

数组的空位

当数组的某个位置是空元素,即两个逗号之间没有任何值,我们称该数组存在空位(hole)。数组的空位不影响length属性。需要注意的是,如果最后一个元素后面有逗号,并不会产生空位。也就是说,有没有这个逗号,结果都是一样的

var a = [1, 2, 3,];
a.length // 3
a // [1, 2, 3]

数组的空位是可以读取的,返回undefined;使用delete命令删除一个数组成员会形成空位,且不会影响length属性,也就是说length属性不过滤空位

数组的某个位置是空位,与某个位置是undefined,是不一样的。如果是空位,使用数组的forEach方法、for...in结构、以及Object.keys方法进行遍历,空位都会被跳过。如果某个位置是undefined,遍历的时候就不会被跳过

var a = [, , ,];
a.forEach(function (x, i) {
  console.log(i + '. ' + x);
}) // 0. undefined
for (var i in a) {
  console.log(i);
} // 0
Object.keys(a) // ["0"]

这就是说,空位就是数组没有这个元素,所以不会被遍历到,而undefined则表示数组有这个元素,值是undefined,所以遍历不会跳过

类似数组的对象

如果一个对象的所有键名都是正整数或零,并且有length属性,那么这个对象就很像数组,语法上称为“类似数组的对象”(array-like object)

var obj = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3
};
obj[0] // 'a'
obj[1] // 'b'
obj.length // 3
obj.push('d') // TypeError: obj.push is not a function

对象obj就是一个类似数组的对象。但是,“类似数组的对象”并不是数组,因为它们不具备数组特有的方法。对象obj没有数组的push方法,使用该方法就会报错。“类似数组的对象”的根本特征,就是具有length属性。只要有length属性,就可以认为这个对象类似于数组。但是有一个问题,这种length属性不是动态值,不会随着成员的变化而变化

典型的“类似数组的对象”是函数的arguments对象,以及大多数 DOM 元素集,还有字符串

// arguments对象
function args() { return arguments }
var arrayLike = args('a', 'b');
arrayLike[0] // 'a'
arrayLike.length // 2
arrayLike instanceof Array // false

// DOM元素集
var elts = document.getElementsByTagName('h3');
elts.length // 3
elts instanceof Array // false

// 字符串
'abc'[1] // 'b'
'abc'.length // 3
'abc' instanceof Array // false

数组的slice方法可以将“类似数组的对象”变成真正的数组

var arr = Array.prototype.slice.call(arrayLike);

除了转为真正的数组,“类似数组的对象”还有一个办法可以使用数组的方法,就是通过call()把数组的方法放到对象上面

function print(value, index) {
  console.log(index + ' : ' + value);
}
Array.prototype.forEach.call(arrayLike, print);

上面代码中,arrayLike代表一个类似数组的对象,本来是不可以使用数组的forEach()方法的,但是通过call(),可以把forEach()嫁接到arrayLike上面调用。下面的例子就是通过这种方法,在arguments对象上面调用forEach方法

// forEach 方法
function logArgs() {
  Array.prototype.forEach.call(arguments, function (elem, i) {
    console.log(i + '. ' + elem);
  });
}
// 等同于 for 循环
function logArgs() {
  for (var i = 0; i < arguments.length; i++) {
    console.log(i + '. ' + arguments[i]);
  }
}

字符串也是类似数组的对象,所以也可以用Array.prototype.forEach.call遍历

注意,这种方法比直接使用数组原生的forEach要慢,所以最好还是先将“类似数组的对象”转为真正的数组,然后再直接调用数组的forEach方法

运算符

算术运算符

运算符是处理数据的基本方法,用来从现有的值得到新的值。JavaScript 提供了多种运算符,覆盖了所有主要的运算

概述

JavaScript 共提供10个算术运算符,用来完成基本的算术运算。

加法运算符:x + y
减法运算符: x - y
乘法运算符: x * y
除法运算符:x / y
指数运算符:x ** y
余数运算符:x % y
自增运算符:++x 或者 x++
自减运算符:--x 或者 x--
数值运算符: +x
负数值运算符:-x

减法、乘法、除法运算法比较单纯,就是执行相应的数学运算。下面介绍其他几个算术运算符,重点是加法运算符

加法运算符

基本规则

加法运算符(+)是最常见的运算符,用来求两个数值的和。JavaScript 允许非数值的相加

true + true // 2
1 + true // 2

上面代码中,第一行是两个布尔值相加,第二行是数值与布尔值相加。这两种情况,布尔值都会自动转成数值,然后再相加。比较特殊的是,如果是两个字符串相加,这时加法运算符会变成连接运算符,返回一个新的字符串,将两个原字符串连接在一起。如果一个运算子是字符串,另一个运算子是非字符串,这时非字符串会转成字符串,再连接在一起

加法运算符是在运行时决定,到底是执行相加还是执行连接。也就是说,运算值的不同导致了不同的语法行为,这种现象称为“重载”(overload)

'3' + 4 + 5 // "345"
3 + 4 + '5' // "75"

上面代码中,由于从左到右的运算次序,字符串的位置不同会导致不同的结果

除了加法运算符,其他算术运算符(比如减法、除法和乘法)都不会发生重载。它们的规则是:所有运算值一律转为数值,再进行相应的数学运算

对象的相加

如果运算值是对象,必须先转成原始类型的值,然后再相加

var obj = { p: 1 };
obj + 2 // "[object Object]2"

上面代码中,对象obj转成原始类型的值是[object Object],再加2就得到了上面的结果

对象转成原始类型的值,规则如下

首先,自动调用对象的valueOf方法

var obj = { p: 1 };
obj.valueOf() // { p: 1 }

一般来说,对象的valueOf方法总是返回对象自身,这时再自动调用对象的toString方法,将其转为字符串

var obj = { p: 1 };
obj.valueOf().toString() // "[object Object]"

对象的toString方法默认返回[object Object],所以就得到了最前面那个例子的结果。知道了这个规则以后,就可以自己定义valueOf方法或toString方法,得到想要的结果

var obj = {
  valueOf: function () {
    return 1;
  }
};
obj + 2 // 3

由于valueOf方法直接返回一个原始类型的值,所以不再调用toString方法。下面是自定义toString方法的例子

var obj = {
  toString: function () {
    return 'hello';
  }
};
obj + 2 // "hello2"

如果运算值是一个Date对象的实例,那么会优先执行toString方法

var obj = new Date();
obj.valueOf = function () { return 1 };
obj.toString = function () { return 'hello' };
obj + 2 // "hello2"

上面代码中,对象obj是一个Date对象的实例,并且自定义了valueOf方法和toString方法,结果toString方法优先执行

余数运算符

余数运算符(%)返回前一个运算值被后一个运算值除,所得的余数。需要注意的是,运算结果的正负号由第一个运算子的正负号决定

-1 % 2 // -1
12 % 5 // 2

所以,为了得到负数的正确余数值,可以先使用绝对值函数

// 错误的写法
function isOdd(n) {
  return n % 2 === 1;
}
isOdd(-5) // false
isOdd(-4) // false
// 正确的写法
function isOdd(n) {
  return Math.abs(n % 2) === 1;
}
isOdd(-5) // true
isOdd(-4) // false

余数运算符还可以用于浮点数的运算。但是,由于浮点数不是精确的值,无法得到完全准确的结果

自增和自减运算符

自增和自减运算符,是一元运算符,只需要一个运算值。它们的作用是将运算值首先转为数值,然后加上1或者减去1。它们会修改原始变量。运算之后,变量的值发生变化,这种效应叫做运算的副作用(side effect)。自增和自减运算符是仅有的两个具有副作用的运算符,其他运算符都不会改变变量的值

自增和自减运算符有一个需要注意的地方,就是放在变量之后,会先返回变量操作前的值,再进行自增/自减操作;放在变量之前,会先进行自增/自减操作,再返回变量操作后的值

var x = 1;
var y = 1;
x++ // 1
++y // 2

数值运算符,负数值运算符

数值运算符(+)同样使用加号,但它是一元运算符(只需要一个操作数),而加法运算符是二元运算符(需要两个操作数)。数值运算符的作用在于可以将任何值转为数值(与Number函数的作用相同)

+true // 1
+[] // 0
+{} // NaN

负数值运算符(-),也同样具有将一个值转为数值的功能,只不过得到的值正负相反。连用两个负数值运算符,等同于数值运算符

var x = 1;
-x // -1
-(-x) // 1

上面代码最后一行的圆括号不可少,否则会变成自减运算符。数值运算符号和负数值运算符,都会返回一个新的值,而不会改变原始变量的值

指数运算符

指数运算符(**)完成指数运算,前一个运算子是底数,后一个运算子是指数

注意,指数运算符是右结合,而不是左结合。即多个指数运算符连用时,先进行最右边的计算

// 相当于 2 ** (3 ** 2)
2 ** 3 ** 2
// 512

赋值运算符

赋值运算符(Assignment Operators)用于给变量赋值。最常见的赋值运算符,当然就是等号(=)。赋值运算符还可以与其他运算符结合,形成变体。下面是与算术运算符的结合

x += y // 等同于 x = x + y
x -= y // 等同于 x = x - y
x *= y // 等同于 x = x * y
x /= y // 等同于 x = x / y
x %= y // 等同于 x = x % y
x **= y // 等同于 x = x ** y

下面是与位运算符的结合

x >>= y // 等同于 x = x >> y
x <<= y // 等同于 x = x << y
x >>>= y // 等同于 x = x >>> y
x &= y // 等同于 x = x & y
x |= y // 等同于 x = x | y
x ^= y // 等同于 x = x ^ y

这些复合的赋值运算符,都是先进行指定运算,然后将得到值返回给左边的变量

比较运算符

概述

比较运算符用于比较两个值的大小,然后返回一个布尔值,表示是否满足指定的条件。

注意:比较运算符可以比较各种类型的值,不仅仅是数值

JavaScript 一共提供了8个比较运算符

> 大于运算符
< 小于运算符
<= 小于或等于运算符
>= 大于或等于运算符
== 相等运算符
=== 严格相等运算符
!= 不相等运算符
!== 严格不相等运算符

这八个比较运算符分成两类:相等比较和非相等比较。两者的规则是不一样的,对于非相等的比较,算法是先看两个运算值是否都是字符串,如果是的,就按照字典顺序比较(实际上是比较 Unicode 码点);否则,将两个运算值都转成数值,再比较数值的大小

非相等运算符:字符串的比较

字符串按照字典顺序进行比较

'cat' > 'dog' // false
'cat' > 'catalog' // false
'cat' > 'Cat' // true'
'大' > '小' // false

JavaScript 引擎内部首先比较首字符的 Unicode 码点。如果相等,再比较第二个字符的 Unicode 码点,以此类推。上面代码中,小写的c的 Unicode 码点(99)大于大写的C的 Unicode 码点(67),所以返回true。由于所有字符都有 Unicode 码点,因此汉字也可以比较

非相等运算符:非字符串的比较

如果两个运算值之中,至少有一个不是字符串,需要分成以下两种情况

原始类型值

如果两个运算值都是原始类型的值,则是先转成数值再比较

5 > '4' // true
// 等同于 5 > Number('4')
// 即 5 > 4
true > false // true
// 等同于 Number(true) > Number(false)
// 即 1 > 0
2 > true // true
// 等同于 2 > Number(true)
// 即 2 > 1

这里注意,任何值(包括NaN本身)与NaN比较,返回的都是false

对象

如果运算值是对象,会转为原始类型的值,再进行比较。对象转换成原始类型的值,算法是先调用valueOf方法;如果返回的还是对象,再接着调用toString方法(前面有讲到)

var x = [2];
x > '11' // true
// 等同于 [2].valueOf().toString() > '11'
// 即 '2' > '11'
x.valueOf = function () { return '1' };
x > '11' // false
// 等同于 [2].valueOf() > '11'
// 即 '1' > '11'
[2] > [1] // true
// 等同于 [2].valueOf().toString() > [1].valueOf().toString()
// 即 '2' > '1'
[2] > [11] // true
// 等同于 [2].valueOf().toString() > [11].valueOf().toString()
// 即 '2' > '11'
{ x: 2 } >= { x: 1 } // true
// 等同于 { x: 2 }.valueOf().toString() >= { x: 1 }.valueOf().toString()
// 即 '[object Object]' >= '[object Object]'

严格相等运算符

JavaScript 提供两种相等运算符:==和===。简单说,它们的区别是相等运算符(==)比较两个值是否相等,严格相等运算符(===)比较它们是否为“同一个值”。如果两个值不是同一类型,严格相等运算符(===)直接返回false,而相等运算符(==)会将它们转换成同一个类型,再用严格相等运算符进行比较。

不同类型的值

如果两个值的类型不同,直接返回false

同一类的原始类型值

同一类型的原始类型的值(数值、字符串、布尔值)比较时,值相同就返回true,值不同就返回false

注意:NaN与任何值都不相等(包括自身);另外,正0等于负0

复合类型值

两个复合类型(对象、数组、函数)的数据比较时,不是比较它们的值是否相等,而是比较它们是否指向同一个地址

{} === {} // false
[] === [] // false
(function () {} === function () {}) // false

上面代码分别比较两个空对象、两个空数组、两个空函数,结果都是不相等。原因是对于复合类型的值,严格相等运算比较的是,它们是否引用同一个内存地址,而运算符两边的空对象、空数组、空函数的值,都存放在不同的内存地址,结果当然是false。如果两个变量引用同一个对象,则它们相等

var v1 = {};
var v2 = v1;
v1 === v2 // true

注意,对于两个对象的比较,严格相等运算符比较的是地址,而大于或小于运算符比较的是值

var obj1 = {};
var obj2 = {};
obj1 > obj2 // false
obj1 < obj2 // false
obj1 === obj2 // false

undefined 和 null

undefined和null与自身严格相等。由于变量声明后默认值是undefined,因此两个只声明未赋值的变量是相等的

undefined === undefined // true
null === null // true
var v1;
var v2;
v1 === v2 // true

严格不相等运算符

严格相等运算符有一个对应的“严格不相等运算符”(!==),它的算法就是先求严格相等运算符的结果,然后返回相反值

1 !== '1' // true
// 等同于
!(1 === '1')

相等运算符

相等运算符用来比较相同类型的数据时,与严格相等运算符完全一样

1 == 1.0
// 等同于
1 === 1.0

比较不同类型的数据时,相等运算符会先将数据进行类型转换,然后再用严格相等运算符比较。下面分成四种情况,讨论不同类型的值互相比较的规则

原始类型值

原始类型的值会转换成数值再进行比较

1 == true // true
// 等同于 1 === Number(true)
0 == false // true
// 等同于 0 === Number(false)
2 == true // false
// 等同于 2 === Number(true)
2 == false // false
// 等同于 2 === Number(false)
'true' == true // false
// 等同于 Number('true') === Number(true)
// 等同于 NaN === 1
'' == 0 // true
// 等同于 Number('') === 0
// 等同于 0 === 0
'' == false  // true
// 等同于 Number('') === Number(false)
// 等同于 0 === 0
'1' == true  // true
// 等同于 Number('1') === Number(true)
// 等同于 1 === 1
'\n  123  \t' == 123 // true
// 因为字符串转为数字时,省略前置和后置的空格

对象与原始类型值比较

对象(这里指广义的对象,包括数组和函数)与原始类型的值比较时,对象转换成原始类型的值,再进行比较

// 对象与数值比较时,对象转为数值
[1] == 1 // true
// 等同于 Number([1]) == 1

// 对象与字符串比较时,对象转为字符串
[1] == '1' // true
// 等同于 String([1]) == '1'
[1, 2] == '1,2' // true
// 等同于 String([1, 2]) == '1,2'

// 对象与布尔值比较时,两边都转为数值
[1] == true // true
// 等同于 Number([1]) == Number(true)
[2] == true // false
// 等同于 Number([2]) == Number(true)

undefined 和 null

undefined和null与其他类型的值比较时,结果都为false,它们互相比较时结果为true

false == null // false
false == undefined // false
0 == null // false
0 == undefined // false
undefined == null // true

相等运算符的缺点

相等运算符隐藏的类型转换,会带来一些违反直觉的结果

0 == ''             // true
0 == '0'            // true
2 == true           // false
2 == false          // false
false == 'false'    // false
false == '0'        // true
false == undefined  // false
false == null       // false
null == undefined   // true
' \t\r\n ' == 0     // true

不相等运算符

相等运算符有一个对应的“不相等运算符”(!=),它的算法就是先求相等运算符的结果,然后返回相反值

布尔运算符

概述

布尔运算符用于将表达式转为布尔值,一共包含四个运算符。

取反运算符:!
且运算符:&&
或运算符:||
三元运算符:?:

取反运算符(!)

取反运算符是一个感叹号,用于将布尔值变为相反值,即true变成false,false变成true

对于非布尔值,取反运算符会将其转为布尔值。可以这样记忆,以下六个值取反后为true,其他值都为false。

undefined
null
false
0
NaN
空字符串('')

如果对一个值连续做两次取反运算,等于将其转为对应的布尔值,与Boolean函数的作用相同。这是一种常用的类型转换的写法

!!x
// 等同于
Boolean(x)

两次取反就是将一个值转为布尔值的简便写法

且运算符(&&)

且运算符(&&)往往用于多个表达式的求值。它的运算规则是:如果第一个运算值为true,则返回第二个运算值(注意是值,不是布尔值);如果第一个运算值为false,则直接返回第一个运算值,且不再对第二个运算值求值

't' && ''  // ""
'' && 'f'  // ""
'' && ''  // ""

var x = 1;
(1 - 1) && ( x += 1) // 0
x  // 1

上面代码的最后一个例子,由于且运算符的第一个运算布尔值为false,则直接返回它的值0,而不再对第二个运算值求值,所以变量x的值没变。这种跳过第二个运算值的机制,被称为“短路”。有些程序员喜欢用它取代if结构,比如下面是一段if结构的代码,就可以用且运算符改写

if (i) {
  doSomething();
}
// 等价于
i && doSomething();

上面代码的两种写法是等价的,但是后一种不容易看出目的,也不容易除错,建议谨慎使用。且运算符可以多个连用,这时返回第一个布尔值为false的表达式的值。如果所有表达式的布尔值都为true,则返回最后一个表达式的值

或运算符(||)

或运算符(||)也用于多个表达式的求值。它的运算规则是:如果第一个运算值的布尔值为true,则返回第一个运算值,且不再对第二个运算值求值;如果第一个运算值的布尔值为false,则返回第二个运算值

't' || '' // "t"
't' || 'f' // "t"
'' || 'f' // "f"
'' || '' // ""

短路规则对这个运算符也适用

var x = 1;
true || (x = 2) // true
x // 1

上面代码中,且运算符的第一个运算值为true,所以直接返回true,不再运行第二个运算值。所以,x的值没有改变。这种只通过第一个表达式的值,控制是否运行第二个表达式的机制,就称为“短路”(short-cut)。或运算符可以多个连用,这时返回第一个布尔值为true的表达式的值。如果所有表达式都为false,则返回最后一个表达式的值

三元条件运算符(?:)

三元条件运算符由问号(?)和冒号(:)组成,分隔三个表达式。它是 JavaScript 语言唯一一个需要三个运算值的运算符。如果第一个表达式的布尔值为true,则返回第二个表达式的值,否则返回第三个表达式的值

通常来说,三元条件表达式与if...else语句具有同样表达效果,前者可以表达的,后者也能表达。但是两者具有一个重大差别,if...else是语句,没有返回值;三元条件表达式是表达式,具有返回值。所以,在需要返回值的场合,只能使用三元条件表达式,而不能使用if..else

console.log(true ? 'T' : 'F');

上面代码中,console.log方法的参数必须是一个表达式,这时就只能使用三元条件表达式。如果要用if...else语句,就必须改变整个代码写法了

二进制位运算符

概述

二进制位运算符用于直接对二进制位进行计算,一共有7个

二进制或运算符(or):符号为|,表示若两个二进制位都为0,则结果为0,否则为1
二进制与运算符(and):符号为&,表示若两个二进制位都为1,则结果为1,否则为0
二进制否运算符(not):符号为~,表示对一个二进制位取反
异或运算符(xor):符号为^,表示若两个二进制位不相同,则结果为1,否则为0
左移运算符(left shift):符号为<<
右移运算符(right shift):符号为>>
带符号位的右移运算符(zero filled right shift):符号为>>>

这些位运算符直接处理每一个比特位(bit),所以是非常底层的运算,好处是速度极快,缺点是很不直观,许多场合不能使用它们,否则会使代码难以理解和查错。

有一点需要特别注意,位运算符只对整数起作用,如果一个运算值不是整数,会自动转为整数后再执行。另外,虽然在 JavaScript 内部,数值都是以64位浮点数的形式储存,但是做位运算的时候,是以32位带符号的整数进行运算的,并且返回值也是一个32位带符号的整数

i = i | 0;

上面这行代码的意思,就是将i(不管是整数或小数)转为32位整数。利用这个特性,可以写出一个函数,将任意数值转为32位整数

function toInt32(x) {
  return x | 0;
}

上面代码中,toInt32可以将小数转为整数。对于一般的整数,返回值不会有任何变化。对于大于或等于2的32次方的整数,大于32位的数位都会被舍去

二进制或运算符

二进制或运算符(|)逐位比较两个运算子,两个二进制位之中只要有一个为1,就返回1,否则返回0

0 | 3 // 3

上面代码中,0和3的二进制形式分别是00和11,所以进行二进制或运算会得到11(即3)。位运算只对整数有效,遇到小数时,会将小数部分舍去,只保留整数部分。所以,将一个小数与0进行二进制或运算,等同于对该数去除小数部分,即取整数位

2.9 | 0 // 2
-2.9 | 0 // -2

需要注意的是,这种取整方法不适用超过32位整数最大值2147483647的数

2147483649.4 | 0; // -2147483647

二进制与运算符

二进制与运算符(&)的规则是逐位比较两个运算子,两个二进制位之中只要有一个位为0,就返回0,否则返回1

二进制否运算符

二进制否运算符(~)将每个二进制位都变为相反值(0变为1,1变为0)。它的返回结果有时比较难理解,因为涉及到计算机内部的数值表示机制

~ 3 // -4

上面表达式对3进行二进制否运算,得到-4。之所以会有这样的结果,是因为位运算时JavaScript 内部将所有的运算值都转为32位的二进制整数再进行运算。3的32位整数形式是00000000000000000000000000000011,二进制否运算以后得到11111111111111111111111111111100。由于第一位(符号位)是1,所以这个数是一个负数。JavaScript 内部采用补码形式表示负数,即需要将这个数减去1,再取一次反,然后加上负号,才能得到这个负数对应的10进制值。这个数减去1等于11111111111111111111111111111011,再取一次反得到00000000000000000000000000000100,再加上负号就是-4。考虑到这样的过程比较麻烦,可以简单记忆成,一个数与自身的取反值相加,等于-1

对一个整数连续两次二进制否运算,得到它自身。所有的位运算都只对整数有效。二进制否运算遇到小数时,也会将小数部分舍去,只保留整数部分。所以,对一个小数连续进行两次二进制否运算,能达到取整效果

~~2.9 // 2
~~47.11 // 47
~~3 // 3

使用二进制否运算取整,是所有取整方法中最快的一种。对字符串进行二进制否运算,JavaScript 引擎会先调用Number函数,将字符串转为数值

对于其他类型的值,二进制否运算也是先用Number转为数值,然后再进行处理

// 相当于~Number('011')
~'011'  // -12
// 相当于~Number('42 cats')
~'42 cats' // -1
// 相当于~Number('0xcafebabe')
~'0xcafebabe' // 889275713
// 相当于~Number('deadbeef')
~'deadbeef' // -1
// 相当于 ~Number([])
~[] // -1
// 相当于 ~Number(NaN)
~NaN // -1
// 相当于 ~Number(null)
~null // -1

异或运算符

异或运算(^)在两个二进制位不同时返回1,相同时返回0

0 ^ 3  // 3

上面表达式中,0(二进制00)与3(二进制11)进行异或运算,它们每一个二进制位都不同,所以得到11(即3)。“异或运算”有一个特殊运用,连续对两个数a和b进行三次异或运算,a^=b; b^=a; a^=b;,可以互换它们的值。这意味着,使用“异或运算”可以在不引入临时变量的前提下,互换两个变量的值

var a = 10;
var b = 99;
a ^= b, b ^= a, a ^= b;
a // 99
b // 10

这是互换两个变量的值的最快方法。异或运算也可以用来取整

12.9 ^ 0 // 12

左移运算符

左移运算符(<<)表示将一个数的二进制值向左移动指定的位数,尾部补0,即乘以2的指定次方

// 4 的二进制形式为100,左移一位为1000(即十进制的8),相当于乘以2的1次方
4 << 1 // 8
-4 << 1 // -8

如果左移0位,就相当于将该数值转为32位整数,等同于取整,对于正数和负数都有效

13.5 << 0  // 13
-13.5 << 0  // -13

左移运算符用于二进制数值非常方便

var color = {r: 186, g: 218, b: 85};
// RGB to HEX  (1 << 24)的作用为保证结果是6位数
var rgb2hex = function(r, g, b) {
  return '#' + ((1 << 24) + (r << 16) + (g << 8) + b)
    .toString(16) // 先转成十六进制,然后返回字符串
    .substr(1);   // 去除字符串的最高位,返回后面六个字符串
}
rgb2hex(color.r, color.g, color.b)  // "#bada55"

上面代码使用左移运算符,将颜色的 RGB 值转为 HEX 值

右移运算符

右移运算符(>>)表示将一个数的二进制值向右移动指定的位数,头部补0,即除以2的指定次方(最高位即符号位不参与移动)

4 >> 1 // 2
// 因为4的二进制形式为 00000000000000000000000000000100,右移一位得到 00000000000000000000000000000010,即为十进制的2
-4 >> 1 // -2
// 因为-4的二进制形式为 11111111111111111111111111111100,右移一位,头部补1,得到 11111111111111111111111111111110,即为十进制的-2

右移运算可以模拟 2 的整除运算

5 >> 1 // 2
// 相当于 5 / 2 = 2
21 >> 2 // 5
// 相当于 21 / 4 = 5
21 >> 3 // 2
// 相当于 21 / 8 = 2
21 >> 4 // 1
// 相当于 21 / 16 = 1

带符号位的右移运算符

带符号位的右移运算符(>>>)表示将一个数的二进制形式向右移动,包括符号位也参与移动,头部补0。所以,该运算总是得到正值。对于正数,该运算的结果与右移运算符(>>)完全一致,区别主要在于负数

4 >>> 1  // 2
-4 >>> 1  // 2147483646
// 因为-4的二进制形式为11111111111111111111111111111100,带符号位的右移一位,得到01111111111111111111111111111110, 即为十进制的2147483646。

这个运算实际上将一个值转为32位无符号整数。查看一个负整数在计算机内部的储存形式,最快的方法就是使用这个运算符

-1 >>> 0 // 4294967295

上面代码表示,-1作为32位整数时,内部的储存形式使用无符号整数格式解读,值为 4294967295(即(2^32)-1,等于11111111111111111111111111111111)

开关作用

位运算符可以用作设置对象属性的开关。假定某个对象有四个开关,每个开关都是一个变量。那么,可以设置一个四位的二进制数,它的每个位对应一个开关

var FLAG_A = 1; // 0001
var FLAG_B = 2; // 0010
var FLAG_C = 4; // 0100
var FLAG_D = 8; // 1000

上面代码设置 A、B、C、D 四个开关,每个开关分别占有一个二进制位。然后,就可以用二进制与运算检验,当前设置是否打开了指定开关

var flags = 5; // 二进制的0101
if (flags & FLAG_C) {
  // ...
}
// 0101 & 0100 => 0100 => true

上面代码检验是否打开了开关C,如果打开会返回true,否则返回false。现在假设需要打开A、B、D三个开关,我们可以构造一个掩码变量

var mask = FLAG_A | FLAG_B | FLAG_D; // 0001 | 0010 | 1000 => 1011

上面代码对A、B、D三个变量进行二进制或运算,得到掩码值为二进制的1011。有了掩码,二进制或运算可以确保打开指定的开关

flags = flags | mask;

二进制与运算可以将当前设置中凡是与开关设置不一样的项,全部关闭

flags = flags & mask;

异或运算可以切换(toggle)当前设置,即第一次执行可以得到当前设置的相反值,再执行一次又得到原来的值

flags = flags ^ mask;

二进制否运算可以翻转当前设置,即原设置为0,运算后变为1;原设置为1,运算后变为0

flags = ~flags;

其他运算符,运算顺序

void 运算符

void运算符的作用是执行一个表达式,然后不返回任何值,或者说返回undefined

void 0 // undefined
void(0) // undefined

上面是void运算符的两种写法,都正确。建议采用后一种形式,即总是使用圆括号。因为void运算符的优先性很高,如果不使用括号,容易造成错误的结果。比如,void 4 + 7实际上等同于(void 4) + 7。

这个运算符的主要用途是浏览器的书签工具(Bookmarklet),以及在超级链接中插入代码防止网页跳转

<script>
function f() {
  console.log('Hello World');
}
</script>
<a href="http://example.com" onclick="f(); return false;">点击</a>

上面代码中,点击链接后,会先执行onclick的代码,由于onclick返回false,所以浏览器不会跳转到 example.com。void运算符可以取代上面的写法

<a href="javascript: void(f())">文字</a>

下面是一个更实际的例子,用户点击链接提交表单,但是不产生页面跳转

<a href="javascript: void(document.form.submit())">
  提交
</a>

逗号运算符

逗号运算符用于对两个表达式求值,并返回后一个表达式的值

'a', 'b' // "b"
var x = 0;
var y = (x++, 10);
x // 1
y // 10

逗号运算符的一个用途是,在返回一个值之前,进行一些辅助操作

var value = (console.log('Hi!'), true); // Hi!
value // true

上面代码中,先执行逗号之前的操作,然后返回逗号后面的值

运算顺序

优先级

JavaScript 各种运算符的优先级别(Operator Precedence)是不一样的。优先级高的运算符先执行,优先级低的运算符后执行

var x = 1;
var arr = [];
var y = arr.length <= 0 || arr[0] === undefined ? x : arr[0];

上面代码中,变量y的值就很难看出来,因为这个表达式涉及5个运算符,到底谁的优先级最高,实在不容易记住。根据语言规格,这五个运算符的优先级从高到低依次为:小于等于(<=)、严格相等(===)、或(||)、三元(?:)、等号(=)。因此上面的表达式,实际的运算顺序如下

var y = ((arr.length <= 0) || (arr[0] === undefined)) ? x : arr[0];

圆括号的作用

圆括号(())可以用来提高运算的优先级,因为它的优先级是最高的,即圆括号中的表达式会第一个运算

注意,因为圆括号不是运算符,而是一种语法结构,所以不具有求值作用,只改变运算的优先级

函数放在圆括号中,会返回函数本身。如果圆括号紧跟在函数的后面,就表示调用函数

function f() {
  return 1;
}
(f) // function f(){return 1;}
f() // 1

圆括号之中,只能放置表达式,如果将语句放在圆括号之中,就会报错

(var a = 1) // SyntaxError: Unexpected token var

左结合与右结合

对于优先级别相同的运算符,大多数情况,计算顺序总是从左到右,这叫做运算符的“左结合”(left-to-right associativity),即从左边开始计算;但是少数运算符的计算顺序是从右到左,即从右边开始计算,这叫做运算符的“右结合”(right-to-left associativity)。其中,最主要的是赋值运算符(=)和三元条件运算符(?:);指数运算符(**)也是右结合的

语法专题

数据类型的转换

概述

JavaScript 是一种动态类型语言,变量没有类型限制,可以随时赋予任意值

var x = y ? 1 : 'a';

上面代码中,变量x到底是数值还是字符串,取决于另一个变量y的值。y为true时,x是一个数值;y为false时,x是一个字符串。这意味着,x的类型没法在编译阶段就知道,必须等到运行时才能知道。虽然变量的数据类型是不确定的,但是各种运算符对数据类型是有要求的。如果运算符发现,运算值的类型与预期不符,就会自动转换类型。比如,减法运算符预期左右两侧的运算值应该是数值,如果不是,就会自动将它们转为数值

'4' - '3' // 1

强制转换

强制转换主要指使用Number()、String()和Boolean()三个函数,手动将各种类型的值,分别转换成数字、字符串或者布尔值

Number()

使用Number函数,可以将任意类型的值转化成数值。下面分成两种情况讨论,一种是参数是原始类型的值,另一种是参数是对象

原始类型值
// 数值:转换后还是原来的值
Number(324) // 324
// 字符串:如果可以被解析为数值,则转换为相应的数值
Number('324') // 324
// 字符串:如果不可以被解析为数值,返回 NaN
Number('324abc') // NaN
// 空字符串转为0
Number('') // 0
// 布尔值:true 转成 1,false 转成 0
Number(true) // 1
Number(false) // 0
// undefined:转成 NaN
Number(undefined) // NaN
// null:转成0
Number(null) // 0

Number函数将字符串转为数值,要比parseInt函数严格很多。基本上,只要有一个字符无法转成数值,整个字符串就会被转为NaN。另外,parseInt和Number函数都会自动过滤一个字符串前导和后缀的空格

parseInt('\t\v\r12.34\n') // 12
Number('\t\v\r12.34\n') // 12.34
对象

简单的规则是,Number方法的参数是对象时,将返回NaN,除非是包含单个数值的数组

Number({a: 1}) // NaN
Number([1, 2, 3]) // NaN
Number([5]) // 5

之所以会这样,是因为Number背后的转换规则比较复杂。

第一步,调用对象自身的valueOf方法。如果返回原始类型的值,则直接对该值使用Number函数,不再进行后续步骤

第二步,如果valueOf方法返回的还是对象,则改为调用对象自身的toString方法。如果toString方法返回原始类型的值,则对该值使用Number函数,不再进行后续步骤

第三步,如果toString方法返回的是对象,就报错

var obj = {x: 1};
Number(obj) // NaN
// 等同于
if (typeof obj.valueOf() === 'object') {
  Number(obj.toString());
} else {
  Number(obj.valueOf());
}

上面代码中,Number函数将obj对象转为数值。背后发生了一连串的操作,首先调用obj.valueOf方法, 结果返回对象本身;于是,继续调用obj.toString方法,这时返回字符串[object Object],对这个字符串使用Number函数,得到NaN。默认情况下,对象的valueOf方法返回对象本身,所以一般总是会调用toString方法,而toString方法返回对象的类型字符串(比如[object Object])。所以,会有下面的结果

Number({}) // NaN

如果toString方法返回的不是原始类型的值,结果就会报错。valueOf和toString方法,都是可以自定义的

Number({
  valueOf: function () {
    return 2;
  }
}) // 2
Number({
  toString: function () {
    return 3;
  }
}) // 3
Number({
  valueOf: function () {
    return 2;
  },
  toString: function () {
    return 3;
  }
}) // 2

valueOf方法先于toString方法执行

String()

String函数可以将任意类型的值转化成字符串,转换规则如下

原始类型值
数值:转为相应的字符串
字符串:转换后还是原来的值
布尔值:true转为字符串"true",false转为字符串"false"
undefined:转为字符串"undefined"
null:转为字符串"null"
对象

String方法的参数如果是对象,返回一个类型字符串;如果是数组,返回该数组的字符串形式

String({a: 1}) // "[object Object]"
String([1, 2, 3]) // "1,2,3"

String方法背后的转换规则,与Number方法基本相同,只是互换了valueOf方法和toString方法的执行顺序

1.先调用对象自身的toString方法。如果返回原始类型的值,则对该值使用String函数,不再进行以下步骤。

2.如果toString方法返回的是对象,再调用原对象的valueOf方法。如果valueOf方法返回原始类型的值,则对该值使用String函数,不再进行以下步骤。

3.如果valueOf方法返回的是对象,就报错

String({a: 1}) // "[object Object]"
// 等同于
String({a: 1}.toString()) // "[object Object]"

如果toString法和valueOf方法,返回的都是对象,就会报错

var obj = {
  valueOf: function () {
    return {};
  },
  toString: function () {
    return {};
  }
};
String(obj) // TypeError: Cannot convert object to primitive value

toString方法先于valueOf方法执行

String({
  toString: function () {
    return 3;
  }
}) // "3"
String({
  valueOf: function () {
    return 2;
  }
}) // "[object Object]"
String({
  valueOf: function () {
    return 2;
  },
  toString: function () {
    return 3;
  }
}) // "3"

Boolean()

Boolean函数可以将任意类型的值转为布尔值。它的转换规则相对简单:除了以下五个值的转换结果为false,其他的值全部为true

Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false

注意:所有对象(包括空对象)的转换结果都是true,甚至连false对应的布尔对象new Boolean(false)也是true

Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // true

自动转换

自动转换是以强制转换为基础的。遇到以下三种情况时,JavaScript 会自动转换数据类型,即转换是自动完成的,用户不可见

1.不同类型的数据互相运算

123 + 'abc' // "123abc"

2.对非布尔值类型的数据求布尔值

if ('abc') {
  console.log('hello')
}  // "hello"

3.对非数值类型的值使用一元运算符(即+和-)

+ {foo: 'bar'} // NaN
- [1, 2, 3] // NaN

自动转换的规则是这样的:预期什么类型的值,就调用该类型的转换函数。比如,某个位置预期为字符串,就调用String函数进行转换。如果该位置即可以是字符串,也可能是数值,那么默认转为数值。由于自动转换具有不确定性,而且不易除错,建议在预期为布尔值、数值、字符串的地方,全部使用Boolean、Number和String函数进行显式转换

自动转换为布尔值

JavaScript 遇到预期为布尔值的地方(比如if语句的条件部分),就会将非布尔值的参数自动转换为布尔值。系统内部会自动调用Boolean函数。因此除了以下五个值,其他都是自动转为true

undefined
null
+0或-0
NaN
''(空字符串)

下面两种写法,有时也用于将一个表达式转为布尔值。它们内部调用的也是Boolean函数

// 写法一
expression ? true : false
// 写法二
!! expression

自动转换为字符串

JavaScript 遇到预期为字符串的地方,就会将非字符串的值自动转为字符串。具体规则是,先将复合类型的值转为原始类型的值,再将原始类型的值转为字符串。字符串的自动转换,主要发生在字符串的加法运算时。当一个值为字符串,另一个值为非字符串,则后者转为字符串

'5' + 1 // '51'
'5' + true // "5true"
'5' + false // "5false"
'5' + {} // "5[object Object]"
'5' + [] // "5"
'5' + function (){} // "5function (){}"
'5' + undefined // "5undefined"
'5' + null // "5null"

这种自动转换很容易出错

自动转换为数值

JavaScript 遇到预期为数值的地方,就会将参数值自动转换为数值。系统内部会自动调用Number函数。除了加法运算符(+)有可能把运算值转为字符串,其他运算符都会把运算值自动转成数值

'5' - '2' // 3
'5' * '2' // 10
true - 1  // 0
false - 1 // -1
'1' - 1   // 0
'5' * []    // 0
false / '5' // 0
'abc' - 1   // NaN
null + 1 // 1
undefined + 1 // NaN

注意:null转为数值时为0,而undefined转为数值时为NaN

一元运算符也会把运算子转成数值

+'abc' // NaN
-'abc' // NaN
+true // 1
-false // 0

错误处理机制

Error 实例对象

JavaScript 解析或运行时,一旦发生错误,引擎就会抛出一个错误对象。JavaScript 原生提供Error构造函数,所有抛出的错误都是这个构造函数的实例

var err = new Error('出错了');
err.message // "出错了"

上面代码中,我们调用Error构造函数,生成一个实例对象err。Error构造函数接受一个参数,表示错误提示,可以从实例的message属性读到这个参数。抛出Error实例对象以后,整个程序就中断在发生错误的地方,不再往下执行。JavaScript 语言标准只提到,Error实例对象必须有message属性,表示出错时的提示信息,没有提到其他属性。大多数 JavaScript 引擎,对Error实例还提供name和stack属性,分别表示错误的名称和错误的堆栈,但它们是非标准的,不是每种实现都有

message:错误提示信息
name:错误名称(非标准属性)
stack:错误的堆栈(非标准属性)

使用name和message这两个属性,可以对发生什么错误有一个大概的了解

if (error.name) {
  console.log(error.name + ': ' + error.message);
}

stack属性用来查看错误发生时的堆栈

function throwit() {
  throw new Error('');
}
function catchit() {
  try {
    throwit();
  } catch(e) {
    console.log(e.stack); // print stack trace
  }
}
catchit()
// Error
//    at throwit (~/examples/throwcatch.js:9:11)
//    at catchit (~/examples/throwcatch.js:3:9)
//    at repl:1:5

上面代码中,错误堆栈的最内层是throwit函数,然后是catchit函数,最后是函数的运行环境

原生错误类型

Error实例对象是最一般的错误类型,在它的基础上,JavaScript 还定义了其他6种错误对象。也就是说,存在Error的6个派生对象

SyntaxError 对象

SyntaxError对象是解析代码时发生的语法错误

// 变量名错误
var 1a; // Uncaught SyntaxError: Invalid or unexpected token
// 缺少括号
console.log 'hello'); // Uncaught SyntaxError: Unexpected string

上面代码的错误,都是在语法解析阶段就可以发现,所以会抛出SyntaxError。第一个错误提示是“token 非法”,第二个错误提示是“字符串不符合要求”

ReferenceError 对象

ReferenceError对象是引用一个不存在的变量时发生的错误

// 使用一个不存在的变量
unknownVariable // Uncaught ReferenceError: unknownVariable is not defined

另一种触发场景是,将一个值分配给无法分配的对象,比如对函数的运行结果或者this赋值

// 等号左侧不是变量
console.log() = 1 // Uncaught ReferenceError: Invalid left-hand side in assignment
// this 对象不能手动赋值
this = 1 // ReferenceError: Invalid left-hand side in assignment

RangeError 对象

RangeError对象是一个值超出有效范围时发生的错误。主要有几种情况,一是数组长度为负数,二是Number对象的方法参数超出范围,以及函数堆栈超过最大值

// 数组长度不得为负数
new Array(-1) // Uncaught RangeError: Invalid array length

TypeError 对象

TypeError对象是变量或参数不是预期类型时发生的错误。比如,对字符串、布尔值、数值等原始类型的值使用new命令,就会抛出这种错误,因为new命令的参数应该是一个构造函数

new 123 // Uncaught TypeError: number is not a func
var obj = {};
obj.unknownMethod() // Uncaught TypeError: obj.unknownMethod is not a function

上面代码的第二种情况,调用对象不存在的方法,也会抛出TypeError错误,因为obj.unknownMethod的值是undefined,而不是一个函数

URIError 对象

URIError对象是 URI 相关函数的参数不正确时抛出的错误,主要涉及encodeURI()、decodeURI()、encodeURIComponent()、decodeURIComponent()、escape()和unescape()这六个函数

decodeURI('%2') // URIError: URI malformed

EvalError 对象

eval函数没有被正确执行时,会抛出EvalError错误。该错误类型已经不再使用了,只是为了保证与以前代码兼容,才继续保留

总结

以上这6种派生错误,连同原始的Error对象,都是构造函数。开发者可以使用它们,手动生成错误对象的实例。这些构造函数都接受一个参数,代表错误提示信息(message)

var err1 = new Error('出错了!');
var err2 = new RangeError('出错了,变量超出有效范围!');
var err3 = new TypeError('出错了,变量类型无效!');
err1.message // "出错了!"
err2.message // "出错了,变量超出有效范围!"
err3.message // "出错了,变量类型无效!"

自定义错误

除了 JavaScript 原生提供的七种错误对象,还可以定义自己的错误对象

function UserError(message) {
  this.message = message || '默认信息';
  this.name = 'UserError';
}
UserError.prototype = new Error();
UserError.prototype.constructor = UserError;

上面代码自定义一个错误对象UserError,让它继承Error对象。然后,就可以生成这种自定义类型的错误了

new UserError('这是自定义的错误!');

throw 语句

throw语句的作用是手动中断程序执行,抛出一个错误

if (x <= 0) {
  throw new Error('x 必须为正数');
} // Uncaught ReferenceError: x is not defined

上面代码中,如果变量x小于等于0,就手动抛出一个错误,告诉用户x的值不正确,整个程序就会在这里中断执行。可以看到,throw抛出的错误就是它的参数,这里是一个Error实例。throw也可以抛出自定义错误

function UserError(message) {
  this.message = message || '默认信息';
  this.name = 'UserError';
}
throw new UserError('出错了!'); // Uncaught UserError {message: "出错了!", name: "UserError"}

实际上,throw可以抛出任何类型的值。也就是说,它的参数可以是任何值

// 抛出一个字符串
throw 'Error!'; // Uncaught Error!
// 抛出一个数值
throw 42; // Uncaught 42
// 抛出一个布尔值
throw true; // Uncaught true
// 抛出一个对象
throw {
  toString: function () {
    return 'Error!';
  }
}; // Uncaught {toString: ƒ}

对于 JavaScript 引擎来说,遇到throw语句,程序就中止了。引擎会接收到throw抛出的信息,可能是一个错误实例,也可能是其他类型的值

try...catch 结构

一旦发生错误,程序就中止执行了。JavaScript 提供了try...catch结构,允许对错误进行处理,选择是否往下执行

try {
  throw new Error('出错了!');
} catch (e) {
  console.log(e.name + ": " + e.message);
  console.log(e.stack);
}
// Error: 出错了!
//   at <anonymous>:3:9

上面代码中,try代码块抛出错误(上例用的是throw语句),JavaScript 引擎就立即把代码的执行,转到catch代码块,或者说错误被catch代码块捕获了。catch接受一个参数,表示try代码块抛出的值。如果你不确定某些代码是否会报错,就可以把它们放在try...catch代码块之中,便于进一步对错误进行处理

try {
  f();
} catch(e) {
  // 处理错误
}

上面代码中,如果函数f执行报错,就会进行catch代码块,接着对错误进行处理。catch代码块捕获错误之后,程序不会中断,会按照正常流程继续执行下去

try {
  throw "出错了";
} catch (e) {
  console.log(111);
}
console.log(222);
// 111
// 222

上面代码中,try代码块抛出的错误,被catch代码块捕获后,程序会继续向下执行。catch代码块之中,还可以再抛出错误,甚至使用嵌套的try...catch结构

try {
  foo.bar();
} catch (e) {
  if (e instanceof EvalError) {
    console.log(e.name + ": " + e.message);
  } else if (e instanceof RangeError) {
    console.log(e.name + ": " + e.message);
  }
  // ...
}

finally 代码块

try...catch结构允许在最后添加一个finally代码块,表示不管是否出现错误,都必需在最后运行的语句

function cleansUp() {
  try {
    throw new Error('出错了……');
    console.log('此行不会执行');
  } finally {
    console.log('完成清理工作');
  }
}
cleansUp()
// 完成清理工作
// Uncaught Error: 出错了……
//    at cleansUp (<anonymous>:3:11)
//    at <anonymous>:10:1

上面代码中,由于没有catch语句块,一旦发生错误,代码就会中断执行。中断执行前,会先执行finally代码块,然后再向用户提示报错信息

function idle(x) {
  try {
    console.log(x);
    return 'result';
  } finally {
    console.log('FINALLY');
  }
}
idle('hello')
// hello
// FINALLY

上面代码中,try代码块没有发生错误,而且里面还包括return语句,但是finally代码块依然会执行。而且,这个函数的返回值还是result

var count = 0;
function countUp() {
  try {
    return count;
  } finally {
    count++;
  }
}
countUp() // 0
count // 1

上面代码说明,return语句里面的count的值,是在finally代码块运行之前就获取了

下面是finally代码块用法的典型场景

openFile();
try {
  writeFile(Data);
} catch(e) {
  handleError(e);
} finally {
  closeFile();
}

上面代码首先打开一个文件,然后在try代码块中写入文件,如果没有发生错误,则运行finally代码块关闭文件;一旦发生错误,则先使用catch代码块处理错误,再使用finally代码块关闭文件。下面的例子充分反映了try...catch...finally这三者之间的执行顺序

function f() {
  try {
    console.log(0);
    throw 'bug';
  } catch(e) {
    console.log(1);
    return true; // 这句原本会延迟到 finally 代码块结束再执行
    console.log(2); // 不会运行
  } finally {
    console.log(3);
    return false; // 这句会覆盖掉前面那句 return
    console.log(4); // 不会运行
  }
  console.log(5); // 不会运行
}
var result = f();
// 0
// 1
// 3
result
// false

上面代码中,catch代码块结束执行之前,会先执行finally代码块。catch代码块之中,触发转入finally代码快的标志,不仅有return语句,还有throw语句

function f() {
  try {
    throw '出错了!';
  } catch(e) {
    console.log('捕捉到内部错误');
    throw e; // 这句原本会等到finally结束再执行
  } finally {
    return false; // 直接返回
  }
}
try {
  f();
} catch(e) {
  // 此处不会执行
  console.log('caught outer "bogus"');
} //  捕捉到内部错误

上面代码中,进入catch代码块之后,一遇到throw语句,就会去执行finally代码块,其中有return false语句,因此就直接返回了,不再会回去执行catch代码块剩下的部分了

try {
  try {
    console.log('Hello world!'); // 报错
  }
  finally {
    console.log('Finally');
  }
  console.log('Will I run?');
} catch(error) {
  console.error(error.message);
}
// Finally
// console is not defined

上面代码中,try里面还有一个try。内层的try报错,这时会执行内层的finally代码块,然后抛出错误,被外层的catch捕获

编程风格

概述

“编程风格”(programming style)指的是编写代码的样式规则。不同的程序员,往往有不同的编程风格。

有人说,编译器的规范叫做“语法规则”(grammar),这是程序员必须遵守的;而编译器忽略的部分,就叫“编程风格”(programming style),这是程序员可以自由选择的。这种说法不完全正确,程序员固然可以自由选择编程风格,但是好的编程风格有助于写出质量更高、错误更少、更易于维护的程序。所以,编程风格的选择不应该基于个人爱好、熟悉程度、打字量等因素,而要考虑如何尽量使代码清晰易读、减少出错。你选择的,不是你喜欢的风格,而是一种能够清晰表达你的意图的风格。这一点,对于 JavaScript 这种语法自由度很高的语言尤其重要。必须牢记的一点是,如果你选定了一种“编程风格”,就应该坚持遵守,切忌多种风格混用。如果你加入他人的项目,就应该遵守现有的风格

缩进

行首的空格和 Tab 键,都可以产生代码缩进效果(indent)。Tab 键可以节省击键次数,但不同的文本编辑器对 Tab 的显示不尽相同,有的显示四个空格,有的显示两个空格,所以有人觉得,空格键可以使得显示效果更统一。无论你选择哪一种方法,都是可以接受的,要做的就是始终坚持这一种选择。不要一会使用 Tab 键,一会使用空格键

区块

如果循环和判断的代码体只有一行,JavaScript 允许该区块(block)省略大括号;但是,建议总是使用大括号表示区块

JavaScript 要起首的大括号要跟在关键字的后面,因为 JavaScript 会自动添加句末的分号,导致一些难以察觉的错误

return
{
  key: value
};
// 相当于
return;
{
  key: value
};

圆括号

圆括号(parentheses)在 JavaScript 中有两种作用,一种表示函数的调用,另一种表示表达式的组合(grouping)

console.log('abc'); // 圆括号表示函数的调用
(1 + 2) * 3 // 圆括号表示表达式的组合

建议可以用空格,区分这两种不同的括号

1.表示函数调用时,函数名与左括号之间没有空格
2.表示函数定义时,函数名与左括号之间没有空格
3.其他情况时,前面位置的语法元素与左括号之间,都有一个空格

行尾的分号

分号表示一条语句的结束,JavaScript 允许省略行尾的分号。事实上,确实有一些开发者行尾从来不写分号。但是,由于下面要讨论的原因,建议还是不要省略这个分号

不使用分号的情况

首先,以下三种情况,语法规定本来就不需要在结尾添加分号

1.for 和 while 循环

for ( ; ; ) {
} // 没有分号
while (true) {
} // 没有分号

注意,do...while循环是有分号的

2.分支语句:if,switch,try

if (true) {
} // 没有分号
switch () {
} // 没有分号
try {
} catch {
} // 没有分号

3.函数的声明语句

function f() {
} // 没有分号

注意,函数表达式仍然要使用分号

var f = function f() {
};

以上三种情况,如果使用了分号,并不会出错。因为,解释引擎会把这个分号解释为空语句

分号的自动添加

除了上面三种情况,所有语句都应该使用分号。但是,如果没有使用分号,大多数情况下,JavaScript 会自动添加

var a = 1
// 等同于
var a = 1;

这种语法特性被称为“分号的自动添加”(Automatic Semicolon Insertion,简称 ASI)。因此,有人提倡省略句尾的分号。麻烦的是,如果下一行的开始可以与本行的结尾连在一起解释,JavaScript 就不会自动添加分号;另外,如果一行的起首是“自增”(++)或“自减”(--)运算符,则它们的前面会自动添加分号。如果continue、break、return和throw这四个语句后面直接跟换行符,则会自动添加分号。这意味着,如果return语句返回的是一个对象的字面量,起首的大括号一定要写在同一行,否则得不到预期结果

a = b = c = 1
a
++
b
--
c
// 等同于
a = b = c = 1;
a;
++b;
--c;

return
{ first: 'Jane' };
// 等同于
return;
{ first: 'Jane' };

由于解释引擎自动添加分号的行为难以预测,因此编写代码的时候不应该省略行尾的分号。有些 JavaScript 代码压缩器(uglifier)不会自动添加分号,因此遇到没有分号的结尾,就会让代码保持原状,而不是压缩成一行,使得压缩无法得到最优的结果。另外,不写结尾的分号,可能会导致脚本合并出错。所以,有的代码库在第一行语句开始前,会加上一个分号

全局变量

JavaScript 最大的语法缺点,可能就是全局变量对于任何一个代码块,都是可读可写。这对代码的模块化和重复使用非常不利,因此建议避免使用全局变量。如果不得不使用,可以考虑用大写字母表示变量名,这样更容易看出这是全局变量,比如UPPER_CASE

变量声明

JavaScript 会自动将变量声明“提升”(hoist)到代码块(block)的头部

if (!x) {
  var x = {};
}
// 等同于
var x;
if (!x) {
  x = {};
}

另外,所有函数都应该在使用之前定义。函数内部的变量声明,都应该放在函数的头部

with 语句

with可以减少代码的书写,但是会造成混淆

with (o) {
 foo = bar;
}

上面的代码,可以有四种运行结果

o.foo = bar;
o.foo = o.bar;
foo = bar;
foo = o.bar;

这四种结果都可能发生,取决于不同的变量是否有定义。因此,不要使用with语句

相等和严格相等

JavaScript 有两个表示相等的运算符:“相等”(==)和“严格相等”(===)。相等运算符会自动转换变量类型,造成很多意想不到的情况;因此,建议不要使用相等运算符(==),只使用严格相等运算符(===)

语句的合并

有些程序员追求简洁,喜欢合并不同目的的语句。虽然语句少了,但是可读性大打折扣,而且会造成误读,让别人误解代码的意思。建议不要将不同目的的语句,合并成一行

自增和自减运算符

自增(++)和自减(--)运算符,放在变量的前面或后面,返回的值不一样,很容易发生错误。事实上,所有的++运算符都可以用+= 1代替;改用+= 1,代码变得更清晰了

switch...case 结构

switch...case结构要求,在每一个case的最后一行必须是break语句,否则会接着运行下一个case。这样不仅容易忘记,还会造成代码的冗长。而且,switch...case不使用大括号,不利于代码形式的统一。此外,这种结构类似于goto语句,容易造成程序流程的混乱,使得代码结构混乱不堪,不符合面向对象编程的原则

function doAction(action) {
  switch (action) {
    case 'hack':
      return 'hack';
    case 'slash':
      return 'slash';
    case 'run':
      return 'run';
    default:
      throw new Error('Invalid action.');
  }
}

上面的代码建议改写成对象结构

function doAction(action) {
  var actions = {
    'hack': function () {
      return 'hack';
    },
    'slash': function () {
      return 'slash';
    },
    'run': function () {
      return 'run';
    }
  };
  if (typeof actions[action] !== 'function') {
    throw new Error('Invalid action.');
  }
  return actions[action]();
}

因此,建议switch...case结构可以用对象结构代替

console 对象与控制台

console 对象

console对象是 JavaScript 的原生对象,它有点像 Unix 系统的标准输出stdout和标准错误stderr,可以输出各种信息到控制台,并且还提供了很多有用的辅助方法

console的常见用途有两个:

1.调试程序,显示网页代码运行时的错误信息

2.提供了一个命令行接口,用来与网页代码互动

console对象的浏览器实现,包含在浏览器自带的开发工具之中。以 Chrome 浏览器的“开发者工具”(Developer Tools)为例,可以使用下面三种方法的打开它

1.按 F12 或者Control + Shift + i(PC)/ Command + Option + i(Mac)。

2.浏览器菜单选择“工具/开发者工具”。

3.在一个页面元素上,打开右键菜单,选择其中的“Inspect Element”

打开开发者工具以后,顶端有多个面板,包括:

Elements:查看网页的 HTML 源码和 CSS 代码

Resources:查看网页加载的各种资源文件(比如代码文件、字体文件 CSS 文件等),以及在硬盘上创建的各种内容(比如本地缓存、Cookie、Local Storage等)

Network:查看网页的 HTTP 通信情况

Sources:查看网页加载的脚本源码

Timeline:查看各种网页行为随时间变化的情况

Performance:查看网页的性能情况,比如 CPU 和内存消耗

Console:用来运行 JavaScript 命令

console 对象的静态方法

console对象提供的各种静态方法,用来与控制台窗口互动

console.log(),console.info(),console.debug()

console.log方法用于在控制台输出信息。它可以接受一个或多个参数,将它们连接起来输出;console.log方法会自动在每次输出的结尾,添加换行符

console.log('Hello World') // Hello World
console.log('a', 'b', 'c') // a b c

如果第一个参数是格式字符串(使用了格式占位符),console.log方法将依次用后面的参数替换占位符,然后再进行输出

console.log(' %s + %s = %s', 1, 1, 2) //  1 + 1 = 2

上面代码中,console.log方法的第一个参数有三个占位符(%s),第二、三、四个参数会在显示时,依次替换掉这个三个占位符

console.log方法支持以下占位符,不同类型的数据必须使用对应的占位符。

%s 字符串
%d 整数
%i 整数
%f 浮点数
%o 对象的链接
%c CSS格式字符串

使用%c占位符时,对应的参数必须是 CSS 代码,用来对输出内容进行 CSS 渲染

var number = 11 * 9;
var color = 'red';
console.log('%d %s balloons', number, color); // 99 red balloons

console.log(
  '%cThis text is styled!',
  'color: red; background: yellow; font-size: 24px;'
)

上面代码运行后,输出的内容将显示为黄底红字;如果参数是一个对象,console.log会显示该对象的值

console.log({foo: 'bar'}) // Object {foo: "bar"}
console.log(Date) // function Date() { [native code] }

上面代码输出Date对象的值,结果为一个构造函数

console.info是console.log方法的别名,用法完全一样。只不过console.info方法会在输出信息的前面,加上一个蓝色图标。console.debug方法与console.log方法类似,会在控制台输出调试信息。但是,默认情况下,console.debug输出的信息不会显示,只有在打开显示级别在verbose的情况下才会显示

console对象的所有方法,都可以被覆盖。因此,可以按照自己的需要,定义console.log方法

['log', 'info', 'warn', 'error'].forEach(function(method) {
  console[method] = console[method].bind(
    console,
    new Date().toISOString()
  );
});
console.log("出错了!"); // 2014-05-18T09:00.000Z 出错了!

上面代码表示,使用自定义的console.log方法,可以在显示结果添加当前时间

console.warn(),console.error()

warn方法和error方法也是在控制台输出信息,它们与log方法的不同之处在于,warn方法输出信息时,在最前面加一个黄色三角,表示警告;error方法输出信息时,在最前面加一个红色的叉,表示出错。同时,还会高亮显示输出文字和错误发生的堆栈。其他方面都一样

console.error('Error: %s (%i)', 'Server is not responding', 500) // Error: Server is not responding (500)
console.warn('Warning! Too few nodes (%d)', document.childNodes.length) // Warning! Too few nodes (1)

console.table()

对于某些复合类型的数据,console.table方法可以将其转为表格显示

var languages = [
  { name: "JavaScript", fileExtension: ".js" },
  { name: "TypeScript", fileExtension: ".ts" },
  { name: "CoffeeScript", fileExtension: ".coffee" }
];
console.table(languages);

上面代码的language变量,转为表格显示如下

(index) name fileExtension
0 "JavaScript" ".js"
1 "TypeScript" ".ts"
2 "CoffeeScript" ".coffee"

console.count()

count方法用于计数,输出它被调用了多少次

function greet(user) {
  console.count(user);
  return "hi " + user;
}
greet('bob')
// bob: 1
// "hi bob"
greet('alice')
// alice: 1
// "hi alice"
greet('bob')
// bob: 2
// "hi bob"

上面代码根据参数的不同,显示bob执行了两次,alice执行了一次

console.dir(),console.dirxml()

dir方法用来对一个对象进行检查(inspect),并以易于阅读和打印的格式显示

console.log({f1: 'foo', f2: 'bar'}) // Object {f1: "foo", f2: "bar"}
console.dir({f1: 'foo', f2: 'bar'})
// Object
//   f1: "foo"
//   f2: "bar"
//   __proto__: Object

上面代码显示dir方法的输出结果,比log方法更易读,信息也更丰富。该方法对于输出 DOM 对象非常有用,因为会显示 DOM 对象的所有属性

Node 环境之中,还可以指定以代码高亮的形式输出

console.dir(obj, {colors: true})

dirxml方法主要用于以目录树的形式,显示 DOM 节点

console.dirxml(document.body)

如果参数不是 DOM 节点,而是普通的 JavaScript 对象,console.dirxml等同于console.dir

console.assert()

console.assert方法主要用于程序运行过程中进行条件判断,如果不满足条件,就显示一个错误,但不会中断程序执行。这样就相当于提示用户,内部状态不正确。它接受两个参数,第一个参数是表达式,第二个参数是字符串。只有当第一个参数为false,才会提示有错误,在控制台输出第二个参数,否则不会有任何结果

console.assert(false, '判断条件不成立') // Assertion failed: 判断条件不成立
// 相当于
try {
  if (!false) {
    throw new Error('判断条件不成立');
  }
} catch(e) {
  console.error(e);
}

console.time(),console.timeEnd()

这两个方法用于计时,可以算出一个操作所花费的准确时间

console.time('Array initialize');
var array= new Array(1000000);
for (var i = array.length - 1; i >= 0; i--) {
  array[i] = new Object();
};
console.timeEnd('Array initialize');  // Array initialize: 1914.481ms

time方法表示计时开始,timeEnd方法表示计时结束。它们的参数是计时器的名称。调用timeEnd方法之后,控制台会显示“计时器名称: 所耗费的时间”

console.group(),console.groupEnd(),console.groupCollapsed()

console.group和console.groupEnd这两个方法用于将显示的信息分组。它只在输出大量信息时有用,分在一组的信息,可以用鼠标折叠/展开

console.group('一级分组');
console.log('一级分组的内容');
console.group('二级分组');
console.log('二级分组的内容');
console.groupEnd(); // 二级分组结束
console.groupEnd(); // 一级分组结束

上面代码会将“二级分组”显示在“一级分组”内部,并且“一级分组”和“二级分组”前面都有一个折叠符号,可以用来折叠本级的内容。console.groupCollapsed方法与console.group方法很类似,唯一的区别是该组的内容,在第一次显示时是收起的(collapsed),而不是展开的

console.groupCollapsed('Fetching Data');
console.log('Request Sent');
console.error('Error: Server not responding (500)');
console.groupEnd();

上面代码只显示一行”Fetching Data“,点击后才会展开,显示其中包含的两行

console.trace(),console.clear()

console.trace方法显示当前执行的代码在堆栈中的调用路径

console.trace()
// console.trace()
//   (anonymous function)
//   InjectedScript._evaluateOn
//   InjectedScript._evaluateAndWrap
//   InjectedScript.evaluate

console.clear方法用于清除当前控制台的所有输出,将光标回置到第一行。如果用户选中了控制台的“Preserve log”选项,console.clear方法将不起作用

控制台命令行 API

浏览器控制台中,除了使用console对象,还可以使用一些控制台自带的命令行方法

$_

$_属性返回上一个表达式的值

2 + 2 // 4
$_ // 4

$0 - $4

控制台保存了最近5个在 Elements 面板选中的 DOM 元素,$0代表倒数第一个(最近一个),$1代表倒数第二个,以此类推直到$4

$(selector)

$(selector)返回第一个匹配的元素,等同于document.querySelector()。注意,如果页面脚本对$有定义,则会覆盖原始的定义。比如,页面里面有 jQuery,控制台执行$(selector)就会采用 jQuery 的实现,返回一个数组

$$(selector)

$$(selector)返回选中的 DOM 对象,等同于document.querySelectorAll #### $x(path) $x(path)方法返回一个数组,包含匹配特定 XPath 表达式的所有 DOM 元素

$x("//p[a]")

上面代码返回所有包含a元素的p元素

inspect(object)

inspect(object)方法打开相关面板,并选中相应的元素,显示它的细节。DOM 元素在Elements面板中显示,比如inspect(document)会在 Elements 面板显示document元素。JavaScript 对象在控制台面板Profiles面板中显示,比如inspect(window)

getEventListeners(object)

getEventListeners(object)方法返回一个对象,该对象的成员为object登记了回调函数的各种事件(比如click或keydown),每个事件对应一个数组,数组的成员为该事件的回调函数

keys(object),values(object)

keys(object)方法返回一个数组,包含object的所有键名。values(object)方法返回一个数组,包含object的所有键值

var o = {'p1': 'a', 'p2': 'b'};
keys(o) // ["p1", "p2"]
values(o) // ["a", "b"]

monitorEvents(object[, events]) ,unmonitorEvents(object[, events])

monitorEvents(object[, events])方法监听特定对象上发生的特定事件。事件发生时,会返回一个Event对象,包含该事件的相关信息。unmonitorEvents方法用于停止监听

monitorEvents(window, "resize");
monitorEvents(window, ["resize", "scroll"])

上面代码分别表示单个事件和多个事件的监听方法

monitorEvents($0, 'mouse');
unmonitorEvents($0, 'mousemove');

上面代码表示如何停止监听。monitorEvents允许监听同一大类的事件。所有事件可以分成四个大类

1.mouse:"mousedown", "mouseup", "click", "dblclick", "mousemove", "mouseover", "mouseout", "mousewheel"

2.key:"keydown", "keyup", "keypress", "textInput"

3.touch:"touchstart", "touchmove", "touchend", "touchcancel"

4.control:"resize", "scroll", "zoom", "focus", "blur", "select", "change", "submit", "reset"

monitorEvents($("#msg"), "key");

上面代码表示监听所有key大类的事件

其他方法

命令行 API 还提供以下方法。

1.clear():清除控制台的历史

2.copy(object):复制特定 DOM 元素到剪贴板

3.dir(object):显示特定对象的所有属性,是console.dir方法的别名

4.dirxml(object):显示特定对象的 XML 形式,是console.dirxml方法的别名

debugger 语句

debugger语句主要用于除错,作用是设置断点。如果有正在运行的除错工具,程序运行到debugger语句时会自动停下。如果没有除错工具,debugger语句不会产生任何结果,JavaScript 引擎自动跳过这一句。Chrome 浏览器中,当代码运行到debugger语句时,就会暂停运行,自动打开脚本源码界面

for(var i = 0; i < 5; i++){
  console.log(i);
  if (i === 2) debugger;
}

上面代码打印出0,1,2以后,就会暂停,自动打开源码界面,等待进一步处理

标准库

Object 对象

概述

JavaScript 原生提供Object对象(注意起首的O是大写),现在介绍该对象原生的各种方法。JavaScript 的所有其他对象都继承自Object对象,即那些对象都是Object的实例。

Object对象的原生方法分成两类:Object本身的方法与Object的实例方法

Object对象本身的方法

所谓“本身的方法”就是直接定义在Object对象的方法

Object.print = function (o) { console.log(o) };

上面代码中,print方法就是直接定义在Object对象上

Object的实例方法

所谓实例方法就是定义在Object原型对象Object.prototype上的方法。它可以被Object实例直接使用

Object.prototype.print = function () {
  console.log(this);
};
var obj = new Object();
obj.print() // Object

上面代码中,Object.prototype定义了一个print方法,然后生成一个Object的实例obj。obj直接继承了Object.prototype的属性和方法,可以直接使用obj.print调用print方法。也就是说,obj对象的print方法实质上就是调用Object.prototype.print方法。凡是定义在Object.prototype对象上面的属性和方法,将被所有实例对象共享

以下先介绍Object作为函数的用法,然后再介绍Object对象的原生方法,分成对象自身的方法(又称为“静态方法”)和实例方法两部分

Object()

Object本身是一个函数,可以当作工具方法使用,将任意值转为对象。这个方法常用于保证某个值一定是对象。如果参数为空(或者为undefined和null),Object()返回一个空对象

var obj = Object();
// 等同于
var obj = Object(undefined);
var obj = Object(null);

obj instanceof Object // true

上面代码的含义,是将undefined和null转为对象,结果得到了一个空对象obj。instanceof运算符用来验证,一个对象是否为指定的构造函数的实例。obj instanceof Object返回true,就表示obj对象是Object的实例。如果参数是原始类型的值,Object方法将其转为对应的包装对象的实例

var obj = Object(1);
obj instanceof Object // true
obj instanceof Number // true
var obj = Object('foo');
obj instanceof Object // true
obj instanceof String // true
var obj = Object(true);
obj instanceof Object // true
obj instanceof Boolean // true

上面代码中,Object函数的参数是各种原始类型的值,转换成对象就是原始类型值对应的包装对象。如果Object方法的参数是一个对象,它总是返回该对象,即不用转换

var arr = [];
var obj = Object(arr); // 返回原数组
obj === arr // true
var value = {};
var obj = Object(value) // 返回原对象
obj === value // true
var fn = function () {};
var obj = Object(fn); // 返回原函数
obj === fn // true

利用这一点,可以写一个判断变量是否为对象的函数

function isObject(value) {
  return value === Object(value);
}
isObject([]) // true
isObject(true) // false

Object 构造函数

Object不仅可以当作工具函数使用,还可以当作构造函数使用,即前面可以使用new命令。Object构造函数的首要用途,是直接通过它来生成新对象

var obj = new Object();

注意:通过var obj = new Object()的写法生成新对象,与字面量的写法var obj = {}是等价的。或者说,后者只是前者的一种简便写法

Object构造函数的用法与工具方法很相似,使用时可以接受一个参数;如果该参数是一个对象,则直接返回这个对象;如果是一个原始类型的值,则返回该值对应的包装对象

var o1 = {a: 1};
var o2 = new Object(o1);
o1 === o2 // true
var obj = new Object(123);
obj instanceof Number // true

虽然用法相似,但是Object(value)与new Object(value)两者的语义是不同的,Object(value)表示将value转成一个对象,new Object(value)则表示新生成一个对象,它的值是value

Object 的静态方法

所谓“静态方法”,是指部署在Object对象自身的方法

Object.keys(),Object.getOwnPropertyNames()

Object.keys方法和Object.getOwnPropertyNames方法都用来遍历对象的属性。Object.keys方法的参数是一个对象,返回一个数组。该数组的成员都是该对象自身的(而不是继承的)所有属性名

Object.getOwnPropertyNames方法与Object.keys类似,也是接受一个对象作为参数,返回一个数组,包含了该对象自身的所有属性名

var obj = {
  p1: 123,
  p2: 456
};
Object.keys(obj) // ["p1", "p2"]
Object.getOwnPropertyNames(obj) // ["p1", "p2"]

对于一般的对象来说,Object.keys()和Object.getOwnPropertyNames()返回的结果是一样的。只有涉及不可枚举属性时,才会有不一样的结果。Object.keys方法只返回可枚举的属性,Object.getOwnPropertyNames方法还返回不可枚举的属性名

var a = ['Hello', 'World'];
Object.keys(a) // ["0", "1"]
Object.getOwnPropertyNames(a) // ["0", "1", "length"]

上面代码中,数组的length属性是不可枚举的属性,所以只出现在Object.getOwnPropertyNames方法的返回结果中。由于 JavaScript 没有提供计算对象属性个数的方法,所以可以用这两个方法代替

var obj = {
  p1: 123,
  p2: 456
};
Object.keys(obj).length // 2
Object.getOwnPropertyNames(obj).length // 2

一般情况下,几乎总是使用Object.keys方法,遍历对象的属性

其他方法

除了上面提到的两个方法,Object还有不少其他静态方法

对象属性模型的相关方法

Object.getOwnPropertyDescriptor():获取某个属性的描述对象

Object.defineProperty():通过描述对象,定义某个属性

Object.defineProperties():通过描述对象,定义多个属性

控制对象状态的方法

Object.preventExtensions():防止对象扩展

Object.isExtensible():判断对象是否可扩展

Object.seal():禁止对象配置

Object.isSealed():判断一个对象是否可配置

Object.freeze():冻结一个对象

Object.isFrozen():判断一个对象是否被冻结

原型链相关方法

Object.create():该方法可以指定原型对象和属性,返回一个新的对象

Object.getPrototypeOf():获取对象的Prototype对象

Object 的实例方法

除了静态方法,还有不少方法定义在Object.prototype对象。它们称为实例方法,所有Object的实例对象都继承了这些方法。Object实例对象的方法,主要有以下六个:

Object.prototype.valueOf():返回当前对象对应的值

Object.prototype.toString():返回当前对象对应的字符串形式

Object.prototype.toLocaleString():返回当前对象对应的本地字符串形式

Object.prototype.hasOwnProperty():判断某个属性是否为当前对象自身的属性,还是继承自原型对象的属性

Object.prototype.isPrototypeOf():判断当前对象是否为另一个对象的原型

Object.prototype.propertyIsEnumerable():判断某个属性是否可枚举

Object.prototype.valueOf()

valueOf方法的作用是返回一个对象的“值”,默认情况下返回对象本身

var obj = new Object();
obj.valueOf() === obj // true

valueOf方法的主要用途是,JavaScript 自动类型转换时会默认调用这个方法

var obj = new Object();
1 + obj // "1[object Object]"

上面代码将对象obj与数字1相加,这时 JavaScript 就会默认调用valueOf()方法,求出obj的值再与1相加。所以,如果自定义valueOf方法,就可以得到想要的结果

var obj = new Object();
obj.valueOf = function () {
  return 2;
};
1 + obj // 3

上面代码自定义了obj对象的valueOf方法,于是1 + obj就得到3。这种方法就相当于用自定义的obj.valueOf,覆盖Object.prototype.valueOf

Object.prototype.toString()

toString方法的作用是返回一个对象的字符串形式,默认情况下返回类型字符串

var o1 = new Object();
o1.toString() // "[object Object]"
var o2 = {a:1};
o2.toString() // "[object Object]"

上面代码表示,对于一个对象调用toString方法,会返回字符串[object Object],该字符串说明对象的类型。字符串[object Object]本身没有太大的用处,但是通过自定义toString方法,可以让对象在自动类型转换时,得到想要的字符串形式

var obj = new Object();
obj.toString = function () {
  return 'hello';
};
obj + ' ' + 'world' // "hello world"

上面代码表示,当对象用于字符串加法时,会自动调用toString方法。由于自定义了toString方法,所以返回字符串hello world。数组、字符串、函数、Date 对象都分别部署了自定义的toString方法,覆盖了Object.prototype.toString方法

[1, 2, 3].toString() // "1,2,3"
'123'.toString() // "123"
(function () {
  return 123;
}).toString()
// "function () {
//   return 123;
// }"
(new Date()).toString() // "Tue May 10 2016 09:11:31 GMT+0800 (CST)"

上面代码中,数组、字符串、函数、Date 对象调用toString方法,并不会返回[object Object],因为它们都自定义了toString方法,覆盖原始方法

toString() 的应用:判断数据类型

Object.prototype.toString方法返回对象的类型字符串,因此可以用来判断一个值的类型

var obj = {};
obj.toString() // "[object Object]"

上面代码调用空对象的toString方法,结果返回一个字符串object Object,其中第二个Object表示该值的构造函数。这是一个十分有用的判断数据类型的方法。由于实例对象可能会自定义toString方法,覆盖掉Object.prototype.toString方法,所以为了得到类型字符串,最好直接使用Object.prototype.toString方法。通过函数的call方法,可以在任意值上调用这个方法,帮助我们判断这个值的类型

Object.prototype.toString.call(value)

不同数据类型的Object.prototype.toString方法返回值如下;这就是说,Object.prototype.toString可以看出一个值到底是什么类型

Object.prototype.toString.call(2) // "[object Number]"
Object.prototype.toString.call('') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(Math) // "[object Math]"
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call([]) // "[object Array]"

利用这个特性,可以写出一个比typeof运算符更准确的类型判断函数

var type = function (o){
  var s = Object.prototype.toString.call(o);
  return s.match(/\[object (.*?)\]/)[1].toLowerCase();
};
type({}); // "object"
type([]); // "array"
type(5); // "number"
type(null); // "null"
type(); // "undefined"
type(/abcd/); // "regex"
type(new Date()); // "date"

在上面这个type函数的基础上,还可以加上专门判断某种类型数据的方法

var type = function (o){
  var s = Object.prototype.toString.call(o);
  return s.match(/\[object (.*?)\]/)[1].toLowerCase();
};
['Null', 'Undefined', 'Object', 'Array', 'String', 'Number', 'Boolean', 'Function', 'RegExp'].forEach(function (t) {
  type['is' + t] = function (o) {
    return type(o) === t.toLowerCase();
  };
});
type.isObject({}) // true
type.isNumber(NaN) // true
type.isRegExp(/abc/) // true

Object.prototype.toLocaleString()

Object.prototype.toLocaleString方法与toString的返回结果相同,也是返回一个值的字符串形式。这个方法的主要作用是留出一个接口,让各种不同的对象实现自己版本的toLocaleString,用来返回针对某些地域的特定的值

var person = {
  toString: function () {
    return 'Henry Norman Bethune';
  },
  toLocaleString: function () {
    return '白求恩';
  }
};
person.toString() // Henry Norman Bethune
person.toLocaleString() // 白求恩

目前,主要有三个对象自定义了toLocaleString方法。

1.Array.prototype.toLocaleString()

2.Number.prototype.toLocaleString()

3.Date.prototype.toLocaleString()

举例来说,日期的实例对象的toString和toLocaleString返回值就不一样,而且toLocaleString的返回值跟用户设定的所在地域相关

var date = new Date();
date.toString() // "Tue Jan 01 2018 12:01:33 GMT+0800 (CST)"
date.toLocaleString() // "1/01/2018, 12:01:33 PM"

Object.prototype.hasOwnProperty()

Object.prototype.hasOwnProperty方法接受一个字符串作为参数,返回一个布尔值,表示该实例对象自身是否具有该属性

var obj = { p: 123 };
obj.hasOwnProperty('p') // true
obj.hasOwnProperty('toString') // false

上面代码中,对象obj自身具有p属性,所以返回true。toString属性是继承的,所以返回false

属性描述对象

概述

JavaScript 提供了一个内部数据结构,用来描述对象的属性,控制它的行为,比如该属性是否可写、可遍历等等。这个内部数据结构称为“属性描述对象”(attributes object)。每个属性都有自己对应的属性描述对象,保存该属性的一些元信息。

{
  value: 123,
  writable: false,
  enumerable: true,
  configurable: false,
  get: undefined,
  set: undefined
}

属性描述对象提供6个元属性

1.value:value是该属性的属性值,默认为undefined

2.writable:writable是一个布尔值,表示属性值(value)是否可改变(即是否可写),默认为true

3.enumerable:enumerable是一个布尔值,表示该属性是否可遍历,默认为true。如果设为false,会使得某些操作(比如for...in循环、Object.keys())跳过该属性

4.configurable:configurable是一个布尔值,表示可配置性,默认为true。如果设为false,将阻止某些操作改写该属性,比如无法删除该属性,也不得改变该属性的属性描述对象(value属性除外)。也就是说,configurable属性控制了属性描述对象的可写性

5.get:get是一个函数,表示该属性的取值函数(getter),默认为undefined

6.set:set是一个函数,表示该属性的存值函数(setter),默认为undefined

Object.getOwnPropertyDescriptor()

Object.getOwnPropertyDescriptor()方法可以获取属性描述对象。它的第一个参数是目标对象,第二个参数是一个字符串,对应目标对象的某个属性名

var obj = { p: 'a' };
Object.getOwnPropertyDescriptor(obj, 'p') // Object { value: "a", writable: true, enumerable: true, configurable: true }

上面代码中,Object.getOwnPropertyDescriptor()方法获取obj.p的属性描述对象。

注意,Object.getOwnPropertyDescriptor()方法只能用于对象自身的属性,不能用于继承的属性

var obj = { p: 'a' };
Object.getOwnPropertyDescriptor(obj, 'toString') // undefined

上面代码中,toString是obj对象继承的属性,Object.getOwnPropertyDescriptor()无法获取

Object.getOwnPropertyNames()

Object.getOwnPropertyNames方法返回一个数组,成员是参数对象自身的全部属性的属性名,不管该属性是否可遍历

var obj = Object.defineProperties({}, {
  p1: { value: 1, enumerable: true },
  p2: { value: 2, enumerable: false }
});
Object.getOwnPropertyNames(obj) // ["p1", "p2"]

上面代码中,obj.p1是可遍历的,obj.p2是不可遍历的。Object.getOwnPropertyNames会将它们都返回。这跟Object.keys的行为不同,Object.keys只返回对象自身的可遍历属性的全部属性名

Object.keys([]) // []
Object.getOwnPropertyNames([]) // [ 'length' ]
Object.keys(Object.prototype) // []
Object.getOwnPropertyNames(Object.prototype) // ['hasOwnProperty', 'valueOf', 'constructor', 'toLocaleString', 'isPrototypeOf', 'propertyIsEnumerable', 'toString']

上面代码中,数组自身的length属性是不可遍历的,Object.keys不会返回该属性。第二个例子的Object.prototype也是一个对象,所有实例对象都会继承它,它自身的属性都是不可遍历的

Object.defineProperty(),Object.defineProperties()

Object.defineProperty()方法允许通过属性描述对象,定义或修改一个属性,然后返回修改后的对象,它的用法如下

Object.defineProperty(object, propertyName, attributesObject)

Object.defineProperty方法接受三个参数:

1.object:属性所在的对象

2.propertyName:字符串,表示属性名

3.attributesObject:属性描述对象

举例来说,定义obj.p可以写成下面这样

var obj = Object.defineProperty({}, 'p', {
  value: 123,
  writable: false,
  enumerable: true,
  configurable: false
});
obj.p // 123
obj.p = 246;
obj.p // 123

上面代码中,Object.defineProperty()方法定义了obj.p属性。由于属性描述对象的writable属性为false,所以obj.p属性不可写。注意,这里的Object.defineProperty方法的第一个参数是{}(一个新建的空对象),p属性直接定义在这个空对象上面,然后返回这个对象,这是Object.defineProperty()的常见用法。如果属性已经存在,Object.defineProperty()方法相当于更新该属性的属性描述对象。如果一次性定义或修改多个属性,可以使用Object.defineProperties()方法

var obj = Object.defineProperties({}, {
  p1: { value: 123, enumerable: true },
  p2: { value: 'abc', enumerable: true },
  p3: { get: function () { return this.p1 + this.p2 },
    enumerable:true,
    configurable:true
  }
});  
obj.p1 // 123
obj.p2 // "abc"
obj.p3 // "123abc"

上面代码中,Object.defineProperties()同时定义了obj对象的三个属性。其中,p3属性定义了取值函数get,即每次读取该属性,都会调用这个取值函数。注意,一旦定义了取值函数get(或存值函数set),就不能将writable属性设为true,或者同时定义value属性,否则会报错

Object.defineProperty()和Object.defineProperties()参数里面的属性描述对象,writable、configurable、enumerable这三个属性的默认值都为false

var obj = {};
Object.defineProperty(obj, 'foo', {});
Object.getOwnPropertyDescriptor(obj, 'foo') // { value: undefined, writable: false, enumerable: false, configurable: false }

上面代码中,定义obj.foo时用了一个空的属性描述对象,就可以看到各个元属性的默认值

Object.prototype.propertyIsEnumerable()

实例对象的propertyIsEnumerable()方法返回一个布尔值,用来判断某个属性是否可遍历。注意,这个方法只能用于判断对象自身的属性,对于继承的属性一律返回false

var obj = {};
obj.p = 123;
obj.propertyIsEnumerable('p') // true
obj.propertyIsEnumerable('toString') // false

元属性

属性描述对象的各个属性称为“元属性”,因为它们可以看作是控制属性的属性

value

value属性是目标属性的值

var obj = {};
obj.p = 123;
Object.getOwnPropertyDescriptor(obj, 'p').value // 123
Object.defineProperty(obj, 'p', { value: 246 }); obj.p // 246

上面代码是通过value属性,读取或改写obj.p的例子

writable

writable属性是一个布尔值,决定了目标属性的值(value)是否可以被改变

var obj = {};
Object.defineProperty(obj, 'a', {  value: 37,  writable: false});
obj.a // 37
obj.a = 25;
obj.a // 37

上面代码中,obj.a的writable属性是false。然后,改变obj.a的值,不会有任何效果。

注意,正常模式下,对writable为false的属性赋值不会报错,只会默默失败。但是,严格模式下会报错,即使对a属性重新赋予一个同样的值

如果原型对象的某个属性的writable为false,那么子对象将无法自定义这个属性

var proto = Object.defineProperty({}, 'foo', { value: 'a',  writable: false });
var obj = Object.create(proto);
obj.foo = 'b';
obj.foo // 'a'

上面代码中,proto是原型对象,它的foo属性不可写。obj对象继承proto,也不可以再自定义这个属性了。如果是严格模式,这样做还会抛出一个错误。

但是,有一个规避方法,就是通过覆盖属性描述对象,绕过这个限制。原因是这种情况下,原型链会被完全忽视

var proto = Object.defineProperty({}, 'foo', { value: 'a',  writable: false});
var obj = Object.create(proto);
Object.defineProperty(obj, 'foo', { value: 'b'});
obj.foo // "b"

enumerable

enumerable(可遍历性)返回一个布尔值,表示目标属性是否可遍历。

JavaScript 的早期版本,for...in循环是基于in运算符的。我们知道,in运算符不管某个属性是对象自身的还是继承的,都会返回true

var obj = {};
'toString' in obj // true

toString不是obj对象自身的属性,但是in运算符也返回true,这导致了toString属性也会被for...in循环遍历,这显然不太合理,后来就引入了“可遍历性”这个概念。只有可遍历的属性,才会被for...in循环遍历,同时还规定toString这一类实例对象继承的原生属性,都是不可遍历的,这样就保证了for...in循环的可用性。具体来说,如果一个属性的enumerable为false,下面三个操作不会取到该属性

1.for..in循环

2.Object.keys方法

3.JSON.stringify方法

因此,enumerable可以用来设置“秘密”属性

var obj = {};
Object.defineProperty(obj, 'x', { value: 123,  enumerable: false});
obj.x // 123
for (var key in obj) {
  console.log(key);
} // undefined
Object.keys(obj)  // []
JSON.stringify(obj) // "{}"

上面代码中,obj.x属性的enumerable为false,所以一般的遍历操作都无法获取该属性,使得它有点像“秘密”属性,但不是真正的私有属性,还是可以直接获取它的值。

注意,for...in循环包括继承的属性,Object.keys方法不包括继承的属性。如果需要获取对象自身的所有属性,不管是否可遍历,可以使用Object.getOwnPropertyNames方法。另外,JSON.stringify方法会排除enumerable为false的属性,有时可以利用这一点。如果对象的 JSON 格式输出要排除某些属性,就可以把这些属性的enumerable设为false

configurable

configurable(可配置性)返回一个布尔值,决定了是否可以修改属性描述对象。也就是说,configurable为false时,value、writable、enumerable和configurable都不能被修改了

var obj = Object.defineProperty({}, 'p', { writable: true,  configurable: false });
Object.defineProperty(obj, 'p', {writable: false}) // 修改成功

注意,writable只有在false改为true会报错,true改为false是允许的。至于value,只要writable和configurable有一个为true,就允许改动。另外,configurable为false时,直接目标属性赋值,不报错,但不会成功;如果是严格模式,还会报错

var obj = Object.defineProperty({}, 'p', {
  value: 1,
  configurable: false
});
obj.p = 2;
obj.p // 1

可配置性决定了目标属性是否可以被删除(delete)

var obj = Object.defineProperties({}, {
  p1: { value: 1, configurable: true },
  p2: { value: 2, configurable: false }
});
delete obj.p1 // true
delete obj.p2 // false
obj.p1 // undefined
obj.p2 // 2

上面代码中,obj.p1的configurable是true,所以可以被删除,obj.p2就无法删除

存取器

除了直接定义以外,属性还可以用存取器(accessor)定义。其中,存值函数称为setter,使用属性描述对象的set属性;取值函数称为getter,使用属性描述对象的get属性。一旦对目标属性定义了存取器,那么存取的时候,都将执行对应的函数。利用这个功能,可以实现许多高级特性,比如某个属性禁止赋值

var obj = Object.defineProperty({}, 'p', {
  get: function () {
    return 'getter';
  },
  set: function (value) {
    console.log('setter: ' + value);
  }
});
obj.p // "getter"
obj.p = 123 // "setter: 123"

上面代码中,obj.p定义了get和set属性。obj.p取值时,就会调用get;赋值时,就会调用set。JavaScript 还提供了存取器的另一种写法

var obj = {
  get p() {
    return 'getter';
  },
  set p(value) {
    console.log('setter: ' + value);
  }
}

上面的写法与定义属性描述对象是等价的,而且使用更广泛。

注意,取值函数get不能接受参数,存值函数set只能接受一个参数(即属性的值)。存取器往往用于属性的值依赖对象内部数据的场合

var obj ={
  $n : 5,
  get next() { return this.$n++ },
  set next(n) {
    if (n >= this.$n) this.$n = n;
    else throw new Error('新的值必须大于当前值');
  }
};
obj.next // 5
obj.next = 10;
obj.next // 10
obj.next = 5; // Uncaught Error: 新的值必须大于当前值

上面代码中,next属性的存值函数和取值函数,都依赖于内部属性$n

对象的拷贝

有时,我们需要将一个对象的所有属性,拷贝到另一个对象,可以用下面的方法实现

var extend = function (to, from) {
  for (var property in from) {
    to[property] = from[property];
  }
  return to;
}
extend({}, {
  a: 1
}) // {a: 1}

上面这个方法的问题在于,如果遇到存取器定义的属性,会只拷贝值

extend({}, {
  get a() { return 1 }
}) // {a: 1}

为了解决这个问题,我们可以通过Object.defineProperty方法来拷贝属性

var extend = function (to, from) {
  for (var property in from) {
    if (!from.hasOwnProperty(property)) continue;
    Object.defineProperty(
      to,
      property,
      Object.getOwnPropertyDescriptor(from, property)
    );
  }
  return to;
}
extend({}, { get a(){ return 1 } }) // { get a(){ return 1 } })

上面代码中,hasOwnProperty那一行用来过滤掉继承的属性,否则可能会报错,因为Object.getOwnPropertyDescriptor读不到继承属性的属性描述对象

控制对象状态

有时需要冻结对象的读写状态,防止对象被改变。JavaScript 提供了三种冻结方法,最弱的一种是Object.preventExtensions,其次是Object.seal,最强的是Object.freeze

Object.preventExtensions()

Object.preventExtensions方法可以使得一个对象无法再添加新的属性

var obj = new Object();
Object.preventExtensions(obj);
Object.defineProperty(obj, 'p', {
  value: 'hello'
}); // TypeError: Cannot define property:p, object is not extensible.
obj.p = 1;
obj.p // undefined

Object.isExtensible()

Object.isExtensible方法用于检查一个对象是否使用了Object.preventExtensions方法。也就是说,检查是否可以为一个对象添加属性

var obj = new Object();
Object.isExtensible(obj) // true
Object.preventExtensions(obj);
Object.isExtensible(obj) // false

上面代码中,对obj对象使用Object.preventExtensions方法以后,再使用Object.isExtensible方法,返回false,表示已经不能添加新属性了

Object.seal()

Object.seal方法使得一个对象既无法添加新属性,也无法删除旧属性

var obj = { p: 'hello' };
Object.seal(obj);
delete obj.p;
obj.p // "hello"
obj.x = 'world';
obj.x // undefined

Object.seal实质是把属性描述对象的configurable属性设为false,因此属性描述对象不再能改变了

var obj = { p: 'a' };
// seal方法之前
Object.getOwnPropertyDescriptor(obj, 'p') // Object { value: "a",  writable: true, enumerable: true, configurable: true }
Object.seal(obj);
// seal方法之后
Object.getOwnPropertyDescriptor(obj, 'p') // Object { value: "a", writable: true, enumerable: true, configurable: false }
Object.defineProperty(o, 'p', { enumerable: false }) // TypeError: Cannot redefine property: p

Object.seal只是禁止新增或删除属性,并不影响修改某个属性的值

var obj = { p: 'a' };
Object.seal(obj);
obj.p = 'b';
obj.p // 'b'

Object.isSealed()

Object.isSealed方法用于检查一个对象是否使用了Object.seal方法

var obj = { p: 'a' };
Object.seal(obj);
Object.isSealed(obj) // true

这时,Object.isExtensible方法也返回false

var obj = { p: 'a' };
Object.seal(obj);
Object.isExtensible(obj) // false

Object.freeze()

Object.freeze方法可以使得一个对象无法添加新属性、无法删除旧属性、也无法改变属性的值,使得这个对象实际上变成了常量

var obj = { p: 'hello' };
Object.freeze(obj);
obj.p = 'world';
obj.p // "hello"
obj.t = 'hello';
obj.t // undefined
delete obj.p // false
obj.p // "hello"

上面代码中,对obj对象进行Object.freeze()以后,修改属性、新增属性、删除属性都无效了。这些操作并不报错,只是默默地失败。如果在严格模式下,则会报错

Object.isFrozen()

Object.isFrozen方法用于检查一个对象是否使用了Object.freeze方法

var obj = { p: 'hello'};
Object.freeze(obj);
Object.isFrozen(obj) // true

使用Object.freeze方法以后,Object.isSealed将会返回true,Object.isExtensible返回false

var obj = { p: 'hello' };
Object.freeze(obj);
Object.isSealed(obj) // true
Object.isExtensible(obj) // false

Object.isFrozen的一个用途是,确认某个对象没有被冻结后,再对它的属性赋值

var obj = { p: 'hello' };
Object.freeze(obj);
if (!Object.isFrozen(obj)) {
  obj.p = 'world';
}

上面代码中,确认obj没有被冻结后,再对它的属性赋值,就不会报错了

局限性

上面的三个方法锁定对象的可写性有一个漏洞:可以通过改变原型对象,来为对象增加属性

var obj = new Object();
Object.preventExtensions(obj);
var proto = Object.getPrototypeOf(obj);
proto.t = 'hello';
obj.t // hello

上面代码中,对象obj本身不能新增属性,但是可以在它的原型对象上新增属性,就依然能够在obj上读到。一种解决方案是,把obj的原型也冻结住

var obj = new Object();
Object.preventExtensions(obj);
var proto = Object.getPrototypeOf(obj);
Object.preventExtensions(proto);
proto.t = 'hello';
obj.t // undefined

另外一个局限是,如果属性值是对象,上面这些方法只能冻结属性指向的对象,而不能冻结对象本身的内容

var obj = { foo: 1,  bar: ['a', 'b'] };
Object.freeze(obj);
obj.bar.push('c');
obj.bar // ["a", "b", "c"]

上面代码中,obj.bar属性指向一个数组,obj对象被冻结以后,这个指向无法改变,即无法指向其他值,但是所指向的数组是可以改变的

Array 对象

构造函数

Array是 JavaScript 的原生对象,同时也是一个构造函数,可以用它生成新的数组

var arr = new Array(2);
arr.length // 2
arr // [ empty x 2 ]

上面代码中,Array构造函数的参数2,表示生成一个两个成员的数组,每个位置都是空值。如果没有使用new,运行结果也是一样的

var arr = new Array(2);
// 等同于
var arr = Array(2);

Array构造函数有一个很大的缺陷,就是不同的参数,会导致它的行为不一致

// 无参数时,返回一个空数组
new Array() // []
// 单个正整数参数,表示返回的新数组的长度
new Array(1) // [ empty ]
new Array(2) // [ empty x 2 ]
// 非正整数的数值作为参数,会报错
new Array(3.2) // RangeError: Invalid array length
new Array(-3) // RangeError: Invalid array length
// 单个非数值(比如字符串、布尔值、对象等)作为参数,则该参数是返回的新数组的成员
new Array('abc') // ['abc']
new Array([1]) // [Array[1]]
// 多参数时,所有参数都是返回的新数组的成员
new Array(1, 2) // [1, 2]
new Array('a', 'b', 'c') // ['a', 'b', 'c']

可以看到,Array作为构造函数,行为很不一致。因此,不建议使用它生成新数组,直接使用数组字面量是更好的做法

// bad
var arr = new Array(1, 2);
// good
var arr = [1, 2];

注意,如果参数是一个正整数,返回数组的成员都是空位。虽然读取的时候返回undefined,但实际上该位置没有任何值。虽然可以取到length属性,但是取不到键名

var a = new Array(3);
var b = [undefined, undefined, undefined];
a.length // 3
b.length // 3
a[0] // undefined
b[0] // undefined
0 in a // false
0 in b // true

上面代码中,a是一个长度为3的空数组,b是一个三个成员都是undefined的数组。读取键值的时候,a和b都返回undefined,但是a的键位都是空的,b的键位是有值的

静态方法

Array.isArray()

Array.isArray方法返回一个布尔值,表示参数是否为数组。它可以弥补typeof运算符的不足

var arr = [1, 2, 3];
typeof arr // "object"
Array.isArray(arr) // true

实例方法

valueOf(),toString()

valueOf方法是一个所有对象都拥有的方法,表示对该对象求值。不同对象的valueOf方法不尽一致,数组的valueOf方法返回数组本身。toString方法也是对象的通用方法,数组的toString方法返回数组的字符串形式

var arr = [1, 2, 3, [4, 5, 6]];
arr.valueOf() // [1, 2, 3, [4, 5, 6]]
arr.toString() // "1,2,3,4,5,6"

push(),pop()

push方法用于在数组的末端添加一个或多个元素,并返回添加新元素后的数组长度。pop方法用于删除数组的最后一个元素,并返回该元素。注意,这两个方法都会改变原数组

var arr = [];
arr.push(1) // 1
arr.push('a') // 2
arr.push(true, {},'c') // 4
arr // [1, 'a', true, {}]  
arr.pop() // 'c'
arr // [1, 'a', true, {}]

对空数组使用pop方法,不会报错,而是返回undefined

[].pop() // undefined

push和pop结合使用,就构成了“后进先出”的栈结构(stack)

shift(),unshift()

shift()方法用于删除数组的第一个元素,并返回该元素。unshift()方法用于在数组的第一个位置添加元素,并返回添加新元素后的数组长度。注意,这两个方法都会改变原数组

var a = ['a', 'b', 'c'];
a.shift() // 'a'
a // ['b', 'c']
a.unshift('x','y'); // 3
a // ['x','y', 'b', 'c']

unshift()方法可以接受多个参数,这些参数都会添加到目标数组头部。shift()方法可以遍历并清空一个数组

var list = [1, 2, 3, 4];
var item;
while (item = list.shift()) {
  console.log(list);
}
list // []

上面代码通过list.shift()方法每次取出一个元素,从而遍历数组。它的前提是数组元素不能是0或任何布尔值等于false的元素,因此这样的遍历不是很可靠。push()和shift()结合使用,就构成了“先进先出”的队列结构(queue)

join()

join()方法以指定参数作为分隔符,将所有数组成员连接为一个字符串返回。如果不提供参数,默认用逗号分隔;如果数组成员是undefined或null或空位,会被转成空字符串

var a = [1,,2, 3, undefined, null, 4];
a.join(' ') // '1  2 3   4'
a.join(' | ') // '1 |  | 2 | 3 |  |  | 4'
a.join() // '1,,2,3,,,4'

通过call方法,这个方法也可以用于字符串或类似数组的对象

Array.prototype.join.call('hello', '-') // "h-e-l-l-o"
var obj = { 0: 'a', 1: 'b', length: 2 };
Array.prototype.join.call(obj, '-') // 'a-b'

concat()

concat方法用于多个数组的合并。它将新数组的成员,添加到原数组成员的后部,然后返回一个新数组,原数组不变

['hello'].concat(['world'], ['!']) // ["hello", "world", "!"]
[].concat({a: 1}, {b: 2}) // [{ a: 1 }, { b: 2 }]
[2].concat({a: 1}) // [2, {a: 1}]

除了数组作为参数,concat也接受其他类型的值作为参数,添加到目标数组尾部。如果数组成员包括对象,concat方法返回当前数组的一个浅拷贝。所谓“浅拷贝”,指的是新数组拷贝的是对象的引用

var obj = { a: 1 };
var oldArray = [obj];
var newArray = oldArray.concat();
obj.a = 2;
newArray[0].a // 2

上面代码中,原数组包含一个对象,concat方法生成的新数组包含这个对象的引用。所以,改变原对象以后,新数组跟着改变

reverse()

reverse方法用于颠倒排列数组元素,返回改变后的数组。注意,该方法将改变原数组

var a = ['a', 'b', 'c'];
a.reverse() // ["c", "b", "a"]
a // ["c", "b", "a"]

slice()

slice方法用于提取目标数组的一部分,返回一个新数组,原数组不变

arr.slice(start, end);

它的第一个参数为起始位置(从0开始),第二个参数为终止位置(但该位置的元素本身不包括在内)。如果省略第二个参数,则一直返回到原数组的最后一个成员

var a = ['a', 'b', 'c'];
a.slice(1) // ["b", "c"]
a.slice(2, 6) // ["c"]
a.slice() // ["a", "b", "c"]
a.slice(-2, -1) // ["b"]

slice没有参数则等于返回一个原数组的拷贝;如果slice方法的参数是负数,则表示倒数计算的位置;如果第一个参数大于等于数组长度或者第二个参数小于第一个参数,则返回空数组;slice方法的一个重要应用,是将类似数组的对象转为真正的数组

Array.prototype.slice.call({ 0: 'a', 1: 'b', length: 2 }) // ['a', 'b']
Array.prototype.slice.call(document.querySelectorAll("div"));
Array.prototype.slice.call(arguments);

上面代码的参数都不是数组,但是通过call方法,在它们上面调用slice方法,就可以把它们转为真正的数组

splice()

splice方法用于删除原数组的一部分成员,并可以在删除的位置添加新的数组成员,返回值是被删除的元素。注意,该方法会改变原数组

arr.splice(start, count, addElement1, addElement2, ...);

splice的第一个参数是删除的起始位置(从0开始),第二个参数是被删除的元素个数。如果后面还有更多参数,则表示这些就是要被插入数组的新元素;起始位置如果是负数,就表示从倒数位置开始删除

var a = ['a', 'b', 'c', 'd', 'e', 'f'];
a.splice(4, 2, 1, 2) // ["e", "f"]
a // ["a", "b", "c", "d", 1, 2]
a.splice(-4, 2) // ["c", "d"]
a // ["a", "b", 1, 2]

如果只是单纯地插入元素,splice方法的第二个参数可以设为0;如果只提供第一个参数,等同于将原数组在指定位置拆分成两个数组

var a = [1, 2, 3, 4];
a.splice(2) // [3, 4]
a // [1, 2]

sort()

sort方法对数组成员进行排序,默认是按照字典顺序排序。排序后,原数组将被改变

['d', 'c', 'b', 'a'].sort() // ['a', 'b', 'c', 'd']
[4, 3, 2, 1].sort() // [1, 2, 3, 4]
[11, 101].sort() // [101, 11]
[10111, 1101, 111].sort() // [10111, 1101, 111]

需要特殊注意:sort方法不是按照大小排序,而是按照字典顺序;也就是说,数值会被先转成字符串,再按照字典顺序进行比较,所以101排在11的前面。如果想让sort方法按照自定义方式排序,可以传入一个函数作为参数。sort的参数函数本身接受两个参数,表示进行比较的两个数组成员。如果该函数的返回值大于0,表示第一个成员排在第二个成员后面;其他情况下,都是第一个元素排在第二个元素前面

[
  { name: "张三", age: 30 },
  { name: "李四", age: 24 },
  { name: "王五", age: 28  }
].sort(function (o1, o2) {
  return o1.age - o2.age;
})
// [
//   { name: "李四", age: 24 },
//   { name: "王五", age: 28  },
//   { name: "张三", age: 30 }
// ]

map()

map方法将数组的所有成员依次传入参数函数,然后把每一次的执行结果组成一个新数组返回,原数组没有变化

var numbers = [1, 2, 3];
numbers.map(function (n) {
  return n + 1;
}); // [2, 3, 4]
numbers // [1, 2, 3]

map方法接受一个函数作为参数。该函数调用时,map方法向它传入三个参数:当前成员、当前位置和数组本身

[1, 2, 3].map(function(elem, index, arr) {
  return elem * index;
}); // [0, 2, 6]

map方法还可以接受第二个参数,用来绑定回调函数内部的this变量

var arr = ['a', 'b', 'c'];
[1, 2].map(function (e) {
  return this[e];
}, arr) // ['b', 'c']

上面代码通过map方法的第二个参数,将回调函数内部的this对象,指向arr数组。如果数组有空位,map方法的回调函数在这个位置不会执行,会跳过数组的空位

var f = function (n) { return 'a' };
[1, undefined, 2].map(f) // ["a", "a", "a"]
[1, null, 2].map(f) // ["a", "a", "a"]
[1, , 2].map(f) // ["a", , "a"]

上面代码中,map方法不会跳过undefined和null,但是会跳过空位

forEach()

forEach方法与map方法很相似,也是对数组的所有成员依次执行参数函数。但是,forEach方法不返回值,只用来操作数据。这就是说,如果数组遍历的目的是为了得到返回值,那么使用map方法,否则使用forEach方法。forEach的用法与map方法一致,参数是一个函数,该函数同样接受三个参数:当前值、当前位置、整个数组

function log(element, index, array) {
  console.log('[' + index + '] = ' + element);
}
[2, 5, 9].forEach(log);
// [0] = 2
// [1] = 5
// [2] = 9

上面代码中,forEach遍历数组不是为了得到返回值,而是为了在屏幕输出内容,所以不必使用map方法。forEach方法也可以接受第二个参数,绑定参数函数的this变量

var out = [];
[1, 2, 3].forEach(function(elem) {
  this.push(elem * elem);
}, out);
out // [1, 4, 9]

上面代码中,空数组out是forEach方法的第二个参数,结果,回调函数内部的this关键字就指向out。注意,forEach方法无法中断执行,总是会将所有成员遍历完。如果希望符合某种条件时就中断遍历要使用for循环

var arr = [1, 2, 3];
for (var i = 0; i < arr.length; i++) {
  if (arr[i] === 2) break;
  console.log(arr[i]);
} // 1

上面代码中,执行到数组的第二个成员时,就会中断执行,forEach方法做不到这一点。forEach方法不会跳过undefined和null,但会跳过空位

var log = function (n) {
  console.log(n + 1);
};
[1, undefined, 2].forEach(log) // 2 NaN 3
[1, null, 2].forEach(log) // 2 1 3
[1, , 2].forEach(log)// 2 3

filter()

filter方法用于过滤数组成员,满足条件的成员组成一个新数组返回。它的参数是一个函数,所有数组成员依次执行该函数,返回结果为true的成员组成一个新数组返回。该方法不会改变原数组

[1, 2, 3, 4, 5].filter(function (elem) {
  return (elem > 3);
}) // [4, 5]

filter方法的参数函数可以接受三个参数:当前成员,当前位置和整个数组

[1, 2, 3, 4, 5].filter(function (elem, index, arr) {
  return index % 2 === 0;
}); // [1, 3, 5]

filter方法还可以接受第二个参数,用来绑定参数函数内部的this变量

var obj = { MAX: 3 };
var myFilter = function (item) {
  if (item > this.MAX) return true;
};
var arr = [2, 8, 3, 4, 1, 3, 2, 9];
arr.filter(myFilter, obj) // [8, 4, 9]

上面代码中,过滤器myFilter内部有this变量,它可以被filter方法的第二个参数obj绑定,返回大于3的成员

some(),every()

这两个方法类似“断言”(assert),返回一个布尔值,表示判断数组成员是否符合某种条件。它们接受一个函数作为参数,所有数组成员依次执行该函数。该函数接受三个参数:当前成员、当前位置和整个数组,然后返回一个布尔值。

some方法是只要一个成员的返回值是true,则整个some方法的返回值就是true,否则返回false。every方法是所有成员的返回值都是true,整个every方法才返回true,否则返回false

var arr = [1, 2, 3, 4, 5];
arr.some(function (elem, index, arr) {
  return elem >= 3;
}); // true
arr.every(function (elem, index, arr) {
  return elem >= 3;
}); // false

some和every方法还可以接受第二个参数,用来绑定参数函数内部的this变量

function isEven(x) { return x % 2 === 0 }
[].some(isEven) // false
[].every(isEven) // true

reduce(),reduceRight()

reduce方法和reduceRight方法依次处理数组的每个成员,最终累计为一个值。它们的差别是,reduce是从左到右处理(从第一个成员到最后一个成员),reduceRight则是从右到左(从最后一个成员到第一个成员),其他完全一样

[1, 2, 3, 4, 5].reduce(function (a, b) {
  console.log(a, b);
  return a + b;
})
// 1 2
// 3 3
// 6 4
// 10 5
//最后结果:15

reduce方法和reduceRight方法的第一个参数都是一个函数。该函数接受以下四个参数。

1.累积变量,默认为数组的第一个成员

2.当前变量,默认为数组的第二个成员

3.当前位置(从0开始)

4.原数组

这四个参数之中,只有前两个是必须的,后两个则是可选的。如果要对累积变量指定初值,可以把它放在reduce方法和reduceRight方法的第二个参数

[1, 2, 3, 4, 5].reduce(function (a, b) {
  return a + b;
}, 10); // 25

上面的第二个参数相当于设定了默认值,处理空数组时尤其有用

function add(prev, cur) {
  return prev + cur;
}
[].reduce(add) // TypeError: Reduce of empty array with no initial value
[].reduce(add, 1) // 1

上面代码中,由于空数组取不到初始值,reduce方法会报错。这时,加上第二个参数,就能保证总是会返回一个值

function subtract(prev, cur) {
  return prev - cur;
}
[3, 2, 1].reduce(subtract) // 0
[3, 2, 1].reduceRight(subtract) // -4

由于这两个方法会遍历数组,所以实际上还可以用来做一些遍历相关的操作。比如,找出字符长度最长的数组成员

function findLongest(entries) {
  return entries.reduce(function (longest, entry) {
    return entry.length > longest.length ? entry : longest;
  }, '');
}
findLongest(['aaa', 'bb', 'c']) // "aaa"

上面代码中,reduce的参数函数会将字符长度较长的那个数组成员,作为累积值。这导致遍历所有成员之后,累积值就是字符长度最长的那个成员

indexOf(),lastIndexOf()

indexOf方法返回给定元素在数组中第一次出现的位置,如果没有出现则返回-1

var a = ['a', 'b', 'c'];
a.indexOf('b') // 1
a.indexOf('y') // -1

indexOf方法还可以接受第二个参数,表示搜索的开始位置

['a', 'b', 'c'].indexOf('a', 1) // -1

上面代码从1号位置开始搜索字符a,结果为-1,表示没有搜索到

lastIndexOf方法返回给定元素在数组中最后一次出现的位置,如果没有出现则返回-1

var a = [2, 5, 9, 2];
a.lastIndexOf(2) // 3
a.lastIndexOf(7) // -1

注意,这两个方法不能用来搜索NaN的位置,即它们无法确定数组成员是否包含NaN

[NaN].indexOf(NaN) // -1
[NaN].lastIndexOf(NaN) // -1

这是因为这两个方法内部,使用严格相等运算符(===)进行比较,而NaN是唯一一个不等于自身的值

链式使用

上面这些数组方法之中,有不少返回的还是数组,所以可以链式使用

var users = [
  {name: 'tom', email: 'tom@example.com'},
  {name: 'peter', email: 'peter@example.com'}
];
users.map(function (user) {
  return user.email;
}).filter(function (email) {
  return /^t/.test(email);
})
.forEach(function (email) {
  console.log(email);
}); // "tom@example.com"

上面代码中,先产生一个所有 Email 地址组成的数组,然后再过滤出以t开头的 Email 地址,最后将它打印出来

包装对象

定义

对象是 JavaScript 语言最主要的数据类型,三种原始类型的值——数值、字符串、布尔值——在一定条件下,也会自动转为对象,也就是原始类型的“包装对象”。所谓“包装对象”,就是分别与数值、字符串、布尔值相对应的Number、String、Boolean三个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象

var v1 = new Number(123);
var v2 = new String('abc');
var v3 = new Boolean(true);
typeof v1 // "object"
typeof v2 // "object"
typeof v3 // "object"
v1 === 123 // false
v2 === 'abc' // false
v3 === true // false

包装对象的最大目的,首先是使得 JavaScript 的对象涵盖所有的值,其次使得原始类型的值可以方便地调用某些方法。

Number、String和Boolean如果不作为构造函数调用(即调用时不加new),常常用于将任意类型的值转为数值、字符串和布尔值。即这三个对象作为构造函数使用(带有new)时,可以将原始类型的值转为对象;作为普通函数使用时(不带有new),可以将任意类型的值,转为原始类型的值

实例方法

三种包装对象各自提供了许多实例方法,这里介绍两种它们共同具有、从Object对象继承的方法:valueOf和toString

valueOf()

valueOf方法返回包装对象实例对应的原始类型的值

new Number(123).valueOf()  // 123
new String('abc').valueOf() // "abc"
new Boolean(true).valueOf() // true

toString()

toString方法返回对应的字符串形式

new Number(123).toString() // "123"
new String('abc').toString() // "abc"
new Boolean(true).toString() // "true"

原始类型与实例对象的自动转换

原始类型的值,可以自动当作包装对象调用,即调用包装对象的属性和方法。这时,JavaScript 引擎会自动将原始类型的值转为包装对象实例,在使用后立刻销毁实例。比如,字符串可以调用length属性,返回字符串的长度

var str = 'abc';
str.length // 3
// 等同于
var strObj = new String(str)
// String { 0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc" }
strObj.length // 3

上面代码中,abc是一个字符串,本身不是对象,不能调用length属性。JavaScript 引擎自动将其转为包装对象,在这个对象上调用length属性。调用结束后,这个临时对象就会被销毁。这就叫原始类型与实例对象的自动转换

自动转换生成的包装对象是只读的,无法修改。所以,字符串无法添加新属性

var s = 'Hello World';
s.x = 123;
s.x // undefined

另一方面,调用结束后,包装对象实例会自动销毁。这意味着,下一次调用字符串的属性时,实际是调用一个新生成的对象,而不是上一次调用时生成的那个对象,所以取不到赋值在上一个对象的属性。如果要为字符串添加属性,只有在它的原型对象String.prototype上定义

自定义方法

除了原生的实例方法,包装对象还可以自定义方法和属性供原始类型的值直接调用。比如,我们可以新增一个double方法,使得字符串和数字翻倍

String.prototype.double = function () {
  return this.valueOf() + this.valueOf();
};
'abc'.double() // abcabc
Number.prototype.double = function () {
  return this.valueOf() + this.valueOf();
};
(123).double() // 246

上面代码在123外面必须要加上圆括号,否则后面的点运算符(.)会被解释成小数点。但是,这种自定义方法和属性的机制,只能定义在包装对象的原型上,如果直接对原始类型的变量添加属性,则无效

Boolean 对象

概述

Boolean对象是 JavaScript 的三个包装对象之一。作为构造函数,它主要用于生成布尔值的包装对象实例

注意,false对应的包装对象实例,布尔运算结果也是true

if (new Boolean(false)) {
  console.log('true');
} // true
if (new Boolean(false).valueOf()) {
  console.log('true');
} // 无输出

上面代码的第一个例子之所以得到true,是因为false对应的包装对象实例是一个对象,进行逻辑运算时,被自动转化成布尔值true(因为所有对象对应的布尔值都是true)。而实例的valueOf方法,则返回实例对应的原始值,本例为false

Boolean 函数的类型转换作用

Boolean对象除了可以作为构造函数,还可以单独使用,将任意值转为布尔值。这时Boolean就是一个单纯的工具方法

Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean('') // false
Boolean(NaN) // false
Boolean(1) // true
Boolean('false') // true
Boolean([]) // true
Boolean({}) // true
Boolean(function () {}) // true
Boolean(/foo/) // true

使用双重的否运算符(!)也可以将任意值转为对应的布尔值

!!undefined // false
!!null // false
!!0 // false
!!'' // false
!!NaN // false
!!1 // true
!!'false' // true
!![] // true
!!{} // true
!!function(){} // true
!!/foo/ // true

对于一些特殊值,Boolean对象前面加不加new,会得到完全相反的结果,必须小心

if (Boolean(false)) {
  console.log('true');
} // 无输出
if (new Boolean(false)) {
  console.log('true');
} // true
if (Boolean(null)) {
  console.log('true');
} // 无输出
if (new Boolean(null)) {
  console.log('true');
} // true

Number 对象

概述

Number对象是数值对应的包装对象,可以作为构造函数使用,也可以作为工具函数使用。作为构造函数时,它用于生成值为数值的对象

var n = new Number(1);
typeof n // "object"

作为工具函数时,它可以将任何类型的值转为数值

Number(true) // 1

静态属性

Number对象拥有以下一些静态属性(即直接定义在Number对象上的属性,而不是定义在实例上的属性)。

//Number.POSITIVE_INFINITY:正的无限,指向Infinity
Number.POSITIVE_INFINITY // Infinity
//Number.NEGATIVE_INFINITY:负的无限,指向-Infinity
Number.NEGATIVE_INFINITY // -Infinity
//Number.NaN:表示非数值,指向NaN
Number.NaN // NaN
Number.MAX_VALUE // 1.7976931348623157e+308
Number.MAX_VALUE < Infinity // true
//Number.MIN_VALUE:表示最小的正数(即最接近0的正数,在64位浮点数体系中为5e-324),相应的,最接近0的负数为-Number.MIN_VALUE
Number.MIN_VALUE // 5e-324
Number.MIN_VALUE > 0 // true
//Number.MAX_SAFE_INTEGER:表示能够精确表示的最大整数,即9007199254740991
Number.MAX_SAFE_INTEGER // 9007199254740991
//Number.MIN_SAFE_INTEGER:表示能够精确表示的最小整数,即-9007199254740991
Number.MIN_SAFE_INTEGER // -9007199254740991

实例方法

Number对象有4个实例方法,都跟将数值转换成指定格式有关

Number.prototype.toString()

Number对象部署了自己的toString方法,用来将一个数值转为字符串形式。toString方法可以接受一个参数,表示输出的进制。如果省略这个参数,默认将数值先转为十进制,再输出字符串;否则,就根据参数指定的进制,将一个数字转化成某个进制的字符串

(10).toString() // "10"
(10).toString(2) // "1010"
(10).toString(8) // "12"
(10).toString(16) // "a"

只要能够让 JavaScript 引擎不混淆小数点和对象的点运算符,各种写法都能用。除了为10加上括号,还可以在10后面加两个点,JavaScript 会把第一个点理解成小数点(即10.0),把第二个点理解成调用对象属性,从而得到正确结果

10..toString(2) // "1010"
// 其他方法还包括
10 .toString(2) // "1010"
10.0.toString(2) // "1010"

这实际上意味着,可以直接对一个小数使用toString方法。通过方括号运算符也可以调用toString方法

10['toString'](2) // "1010"

toString方法只能将十进制的数,转为其他进制的字符串。如果要将其他进制的数,转回十进制,需要使用parseInt方法

Number.prototype.toFixed()

toFixed方法先将一个数转为指定位数的小数,然后返回这个小数对应的字符串

(10).toFixed(2) // "10.00"
10.005.toFixed(2) // "10.01"

toFixed方法的参数为小数位数,有效范围为0到20,超出这个范围将抛出 RangeError 错误

Number.prototype.toExponential()

toExponential方法用于将一个数转为科学计数法形式

(10).toExponential()  // "1e+1"
(10).toExponential(1) // "1.0e+1"
(10).toExponential(2) // "1.00e+1"
(1234).toExponential()  // "1.234e+3"
(1234).toExponential(1) // "1.2e+3"
(1234).toExponential(2) // "1.23e+3"

toExponential方法的参数是小数点后有效数字的位数,范围为0到20,超出这个范围,会抛出一个 RangeError 错误

Number.prototype.toPrecision()

toPrecision方法用于将一个数转为指定位数的有效数字

(12.35).toPrecision(1) // "1e+1"
(12.35).toPrecision(2) // "12"
(12.35).toPrecision(3) // "12.3"
(12.25).toPrecision(3) // "12.3"
(12.35).toPrecision(4) // "12.35"
(12.35).toPrecision(5) // "12.350"

toPrecision方法的参数为有效数字的位数,范围是1到21,超出这个范围会抛出 RangeError 错误。toPrecision方法用于四舍五入时不太可靠,跟浮点数不是精确储存有关

自定义方法

与其他对象一样,Number.prototype对象上面可以自定义方法,被Number的实例继承。由于add方法返回的还是数值,所以可以链式运算

Number.prototype.add = function (x) {
  return this + x;
};
Number.prototype.subtract = function (x) {
  return this - x;
};
(8).add(2).subtract(4) // 6

我们还可以部署更复杂的方法

Number.prototype.iterate = function () {
  var result = [];
  for (var i = 0; i <= this; i++) {
    result.push(i);
  }
  return result;
};
(8).iterate() // [0, 1, 2, 3, 4, 5, 6, 7, 8]

注意,数值的自定义方法,只能定义在它的原型对象Number.prototype上面,数值本身是无法自定义属性的

var n = 1;
n.x = 1;
n.x // undefined

上面代码中,n是一个原始类型的数值。直接在它上面新增一个属性x,不会报错,但毫无作用,总是返回undefined。这是因为一旦被调用属性,n就自动转为Number的实例对象,调用结束后,该对象自动销毁。所以,下一次调用n的属性时,实际取到的是另一个对象,属性x当然就读不出来

String 对象

概述

String对象是 JavaScript 原生提供的三个包装对象之一,用来生成字符串对象

var s1 = 'abc';
var s2 = new String('abc');
typeof s1 // "string"
typeof s2 // "object"
s2.valueOf() // "abc"

字符串对象是一个类似数组的对象(很像数组,但不是数组)

new String('abc') // String {0: "a", 1: "b", 2: "c", length: 3}
(new String('abc'))[1] // "b"

除了用作构造函数,String对象还可以当作工具方法使用,将任意类型的值转为字符串

静态方法

String.fromCharCode()

String对象提供的静态方法(即定义在对象本身,而不是定义在对象实例的方法),主要是String.fromCharCode()。该方法的参数是一个或多个数值,代表 Unicode 码点,返回值是这些码点组成的字符串

String.fromCharCode() // ""
String.fromCharCode(97) // "a"
String.fromCharCode(104, 101, 108, 108, 111) // "hello"

String.fromCharCode方法的参数为空,就返回空字符串;否则,返回参数对应的 Unicode 字符串。

注意,该方法不支持 Unicode 码点大于0xFFFF的字符,即传入的参数不能大于0xFFFF(即十进制的 65535)

String.fromCharCode(0x20BB7) // "ஷ"
String.fromCharCode(0x20BB7) === String.fromCharCode(0x0BB7) // true

上面代码中,String.fromCharCode参数0x20BB7大于0xFFFF,导致返回结果出错。0x20BB7对应的字符是汉字𠮷,但是返回结果却是另一个字符(码点0x0BB7)。这是因为String.fromCharCode发现参数值大于0xFFFF,就会忽略多出的位(即忽略0x20BB7里面的2)。这种现象的根本原因在于,码点大于0xFFFF的字符占用四个字节,而 JavaScript 默认支持两个字节的字符。这种情况下,必须把0x20BB7拆成两个字符表示

String.fromCharCode(0xD842, 0xDFB7) // "𠮷"

实例属性

String.prototype.length

字符串实例的length属性返回字符串的长度

'abc'.length // 3

实例方法

String.prototype.charAt()

charAt方法返回指定位置的字符,参数是从0开始编号的位置;这个方法也完全可以用数组下标替代;如果参数为负数,或大于等于字符串的长度,charAt返回空字符串

var s = new String('abc');
s.charAt(1) // "b"
s.charAt(s.length - 1) // "c"
'abc'[1] // "b"
'abc'.charAt(-1) // ""

String.prototype.charCodeAt()

charCodeAt方法返回字符串指定位置的 Unicode 码点(十进制表示),相当于String.fromCharCode()的逆操作;如果没有任何参数,charCodeAt返回首字符的 Unicode 码点;如果参数为负数,或大于等于字符串的长度,charCodeAt返回NaN

'abc'.charCodeAt(1) // 98
'abc'.charCodeAt() // 97
'abc'.charCodeAt(-1) // NaN

注意,charCodeAt方法返回的 Unicode 码点不会大于65536(0xFFFF),也就是说,只返回两个字节的字符的码点。如果遇到码点大于 65536 的字符(四个字节的字符),必需连续使用两次charCodeAt,不仅读入charCodeAt(i),还要读入charCodeAt(i+1),将两个值放在一起,才能得到准确的字符

String.prototype.concat()

concat方法用于连接两个字符串,返回一个新字符串,不改变原字符串;该方法可以接受多个参数

var s1 = 'abc';
var s2 = 'def';
s1.concat(s2) // "abcdef"
s1 // "abc"
'a'.concat('b', 'c') // "abc"

如果参数不是字符串,concat方法会将其先转为字符串,然后再连接

var one = 1;
var two = 2;
var three = '3';
''.concat(one, two, three) // "123"
one + two + three // "33"

String.prototype.slice()

slice方法用于从原字符串取出子字符串并返回,不改变原字符串。它的第一个参数是子字符串的开始位置,第二个参数是子字符串的结束位置(不含该位置);如果省略第二个参数,则表示子字符串一直到原字符串结束;如果参数是负值,表示从结尾开始倒数计算的位置,即该负值加上字符串长度;如果第一个参数大于第二个参数,slice方法返回一个空字符串

'JavaScript'.slice(0, 4) // "Java"
'JavaScript'.slice(4) // "Script"
'JavaScript'.slice(-6) // "Script"
'JavaScript'.slice(-2, -1) // "p"
'JavaScript'.slice(2, 1) // ""

String.prototype.substring()

substring方法用于从原字符串取出子字符串并返回,不改变原字符串,跟slice方法很相像。它的第一个参数表示子字符串的开始位置,第二个位置表示结束位置(返回结果不含该位置);如果省略第二个参数,则表示子字符串一直到原字符串的结束;如果第一个参数大于第二个参数,substring方法会自动更换两个参数的位置;如果参数是负数,substring方法会自动将负数转为0

'JavaScript'.substring(0, 4) // "Java"
'JavaScript'.substring(4) // "Script"
'JavaScript'.substring(10, 4) // "Script"
// 等同于
'JavaScript'.substring(4, 10) // "Script"
'JavaScript'.substring(4, -3) // "Java"

由于substring某些规则违反直觉,因此不建议使用substring方法,应该优先使用slice

String.prototype.substr()

substr方法用于从原字符串取出子字符串并返回,不改变原字符串,跟slice和substring方法的作用相同。substr方法的第一个参数是子字符串的开始位置(从0开始计算),第二个参数是子字符串的长度;如果省略第二个参数,则表示子字符串一直到原字符串的结束;如果第一个参数是负数,表示倒数计算的字符位置。如果第二个参数是负数,将被自动转为0,因此会返回空字符串

'JavaScript'.substr(4, 6) // "Script"
'JavaScript'.substr(4) // "Script"
'JavaScript'.substr(-6) // "Script"
'JavaScript'.substr(4, -1) // ""

String.prototype.indexOf(),String.prototype.lastIndexOf()

indexOf方法用于确定一个字符串在另一个字符串中第一次出现的位置,返回结果是匹配开始的位置。如果返回-1,就表示不匹配;indexOf方法还可以接受第二个参数,表示从该位置开始向后匹配

'hello world'.indexOf('o') // 4
'JavaScript'.indexOf('script') // -1
'hello world'.indexOf('o', 6) // 7

lastIndexOf方法的用法跟indexOf方法一致,主要的区别是lastIndexOf从尾部开始匹配,indexOf则是从头部开始匹配;另外,lastIndexOf的第二个参数表示从该位置起向前匹配

'hello world'.lastIndexOf('o') // 7
'hello world'.lastIndexOf('o', 6) // 4

String.prototype.trim()

trim方法用于去除字符串两端的空格,返回一个新字符串,不改变原字符串;该方法去除的不仅是空格,还包括制表符(t、v)、换行符(n)和回车符(r)

'  hello world  '.trim() // "hello world"
'\r\nabc \t'.trim() // 'abc'

String.prototype.toLowerCase(),String.prototype.toUpperCase()

toLowerCase方法用于将一个字符串全部转为小写,toUpperCase则是全部转为大写。它们都返回一个新字符串,不改变原字符串

'Hello World'.toLowerCase() // "hello world"
'Hello World'.toUpperCase() // "HELLO WORLD"

String.prototype.match()

match方法用于确定原字符串是否匹配某个子字符串,返回一个数组,成员为匹配的第一个字符串。如果没有找到匹配,则返回null;返回的数组还有index属性和input属性,分别表示匹配字符串开始的位置和原始字符串

var matches = 'cat, bat, sat, fat'.match('at');
'cat, bat, sat, fat'.match('at') // ["at"]
'cat, bat, sat, fat'.match('xt') // null
matches.index // 1
matches.input // "cat, bat, sat, fat"

String.prototype.search(),String.prototype.replace()

search方法的用法基本等同于match,但是返回值为匹配的第一个位置。如果没有找到匹配,则返回-1;search方法还可以使用正则表达式作为参数

'cat, bat, sat, fat'.search('at') // 1

replace方法用于替换匹配的子字符串,一般情况下只替换第一个匹配(除非使用带有g修饰符的正则表达式);replace方法还可以使用正则表达式作为参数

'aaa'.replace('a', 'b') // "baa"

String.prototype.split()

split方法按照给定规则分割字符串,返回一个由分割出来的子字符串组成的数组;如果分割规则为空字符串,则返回数组的成员是原字符串的每一个字符;如果省略参数,则返回数组的唯一成员就是原字符串;如果满足分割规则的两个部分紧邻着(即两个分割符中间没有其他字符),则返回数组之中会有一个空字符串;如果满足分割规则的部分处于字符串的开头或结尾(即它的前面或后面没有其他字符),则返回数组的第一个或最后一个成员是一个空字符串;split方法还可以接受第二个参数,限定返回数组的最大成员数;split方法还可以使用正则表达式作为参数

'a|b|c'.split('|') // ["a", "b", "c"]
'a|b|c'.split('') // ["a", "|", "b", "|", "c"]
'a|b|c'.split() // ["a|b|c"]
'a||c'.split('|') // ['a', '', 'c']
'|b|c'.split('|') // ["", "b", "c"]
'a|b|'.split('|') // ["a", "b", ""]
'a|b|c'.split('|', 2) // ["a", "b"]
'a|b|c'.split('|', 3) // ["a", "b", "c"]
'a|b|c'.split('|', 4) // ["a", "b", "c"]

String.prototype.localeCompare()

localeCompare方法用于比较两个字符串。它返回一个整数,如果小于0,表示第一个字符串小于第二个字符串;如果等于0,表示两者相等;如果大于0,表示第一个字符串大于第二个字符串;该方法的最大特点,就是会考虑自然语言的顺序。举例来说,正常情况下,大写的英文字母小于小写字母

'apple'.localeCompare('banana') // -1
'apple'.localeCompare('apple') // 0
'B' > 'a' // false

JavaScript 采用的是 Unicode 码点比较,B的码点是66,而a的码点是97。但是,localeCompare方法会考虑自然语言的排序情况,将B排在a的前面。

'B'.localeCompare('a') // 1

上面代码中,localeCompare方法返回整数1,表示B较大。localeCompare还可以有第二个参数,指定所使用的语言(默认是英语),然后根据该语言的规则进行比较

'ä'.localeCompare('z', 'de') // -1
'ä'.localeCompare('z', 'sv') // 1

上面代码中,de表示德语,sv表示瑞典语。德语中,ä小于z,所以返回-1;瑞典语中,ä大于z,所以返回1

Math 对象

Math是 JavaScript 的原生对象,提供各种数学功能。该对象不是构造函数,不能生成实例,所有的属性和方法都必须在Math对象上调用

静态属性

Math对象的静态属性,提供以下一些数学常数。

//Math.E:常数e
Math.E // 2.718281828459045
//Math.LN2:2 的自然对数
Math.LN2 // 0.6931471805599453
//Math.LN10:10 的自然对数
Math.LN10 // 2.302585092994046
//Math.LOG2E:以 2 为底的e的对数
Math.LOG2E // 1.4426950408889634
//Math.LOG10E:以 10 为底的e的对数
Math.LOG10E // 0.4342944819032518
//Math.PI:常数π
Math.PI // 3.141592653589793
//Math.SQRT1_2:0.5 的平方根
Math.SQRT1_2 // 0.7071067811865476
//Math.SQRT2:2 的平方根
Math.SQRT2 // 1.4142135623730951

这些属性都是只读的,不能修改

静态方法

Math对象提供以下一些静态方法。

Math.abs():绝对值
Math.ceil():向上取整
Math.floor():向下取整
Math.max():最大值
Math.min():最小值
Math.pow():指数运算
Math.sqrt():平方根
Math.log():自然对数
Math.exp():e的指数
Math.round():四舍五入
Math.random():随机数

Math.abs()

Math.abs方法返回参数值的绝对值

Math.max(),Math.min()

Math.max方法返回参数之中最大的那个值,Math.min返回最小的那个值。如果参数为空, Math.min返回Infinity, Math.max返回-Infinity

Math.max(2, -1, 5) // 5
Math.min(2, -1, 5) // -1
Math.min() // Infinity
Math.max() // -Infinity

Math.floor(),Math.ceil()

Math.floor方法返回小于参数值的最大整数(地板值);Math.ceil方法返回大于参数值的最小整数(天花板值)

Math.floor(3.2) // 3
Math.floor(-3.2) // -4
Math.ceil(3.2) // 4
Math.ceil(-3.2) // -3

这两个方法可以结合起来,实现一个总是返回数值的整数部分的函数

function ToInteger(x) {
  x = Number(x);
  return x < 0 ? Math.ceil(x) : Math.floor(x);
}
ToInteger(3.2) // 3
ToInteger(3.5) // 3
ToInteger(3.8) // 3
ToInteger(-3.2) // -3
ToInteger(-3.5) // -3
ToInteger(-3.8) // -3

Math.round()

Math.round方法用于四舍五入;要注意它对负数的处理(主要是对0.5的处理)

Math.round(0.1) // 0
Math.round(0.5) // 1
Math.round(0.6) // 1
Math.round(-1.1) // -1
Math.round(-1.5) // -1
Math.round(-1.6) // -2

Math.pow()

Math.pow方法返回以第一个参数为底数、第二个参数为幂的指数值

Math.pow(2, 3) // 8
var r = 20;
var area = Math.PI * Math.pow(r, 2) //计算圆面积

Math.sqrt()

Math.sqrt方法返回参数值的平方根。如果参数是一个负值,则返回NaN

Math.sqrt(4) // 2
Math.sqrt(-4) // NaN

Math.log()

Math.log方法返回以e为底的自然对数值。如果要计算以10为底的对数,可以先用Math.log求出自然对数,然后除以Math.LN10;求以2为底的对数,可以除以Math.LN2

Math.log(Math.E) // 1
Math.log(10) // 2.302585092994046
Math.log(100)/Math.LN10 // 2
Math.log(8)/Math.LN2 // 3

Math.exp()

Math.exp方法返回常数e的参数次方

Math.exp(1) // 2.718281828459045
Math.exp(3) // 20.085536923187668

Math.random()

Math.random()返回0到1之间的一个伪随机数,可能等于0,但是一定小于1

//任意范围的随机数生成函数
function getRandomArbitrary(min, max) {
  return Math.random() * (max - min) + min;
}
getRandomArbitrary(1.5, 6.5) // 2.4942810038223864
//任意范围的随机整数生成函数
function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}
getRandomInt(1, 6) // 5

三角函数方法

Math对象还提供一系列三角函数方法

//Math.sin():返回参数的正弦(参数为弧度值)
Math.sin(0) // 0
Math.sin(Math.PI / 2) // 1
//Math.cos():返回参数的余弦(参数为弧度值)
Math.cos(0) // 1
//Math.tan():返回参数的正切(参数为弧度值)
Math.tan(0) // 0
//Math.asin():返回参数的反正弦(返回值为弧度值)
Math.asin(1) // 1.5707963267948966
//Math.acos():返回参数的反余弦(返回值为弧度值)
Math.acos(1) // 0
//Math.atan():返回参数的反正切(返回值为弧度值)
Math.atan(1) // 0.7853981633974483

Date 对象

Date对象是 JavaScript 原生的时间库。它以国际标准时间(UTC)1970年1月1日00:00:00作为时间的零点,可以表示的时间范围是前后各1亿天(单位为毫秒)

普通函数的用法

Date对象可以作为普通函数直接调用,返回一个代表当前时间的字符串

注意,即使带有参数,Date作为普通函数使用时,返回的还是当前时间

Date() // "Tue Mar 12 2019 13:53:36 GMT+0800 (中国标准时间)"
Date(2000, 1, 1) // "Tue Mar 12 2019 13:53:36 GMT+0800 (中国标准时间)"

构造函数的用法

Date还可以当作构造函数使用。对它使用new命令,会返回一个Date对象的实例。如果不加参数,实例代表的就是当前时间

var today = new Date();

Date实例有一个独特的地方。其他对象求值的时候,都是默认调用.valueOf()方法,但是Date实例求值的时候,默认调用的是toString()方法。这导致对Date实例求值,返回的是一个字符串,代表该实例对应的时间

var today = new Date();
today // "Tue Mar 12 2019 13:56:06 GMT+0800 (中国标准时间)"
// 等同于
today.toString() // "Tue Mar 12 2019 13:56:06 GMT+0800 (中国标准时间)"

上面代码中,today是Date的实例,直接求值等同于调用toString方法。作为构造函数时,Date对象可以接受多种格式的参数,返回一个该参数对应的时间实例

// 参数为时间零点开始计算的毫秒数
new Date(1546704000000) // Sun Jan 06 2019 00:00:00 GMT+0800 (中国标准时间)
// 参数为日期字符串
new Date('January 6, 2019'); // Sun Jan 06 2019 00:00:00 GMT+0800 (中国标准时间)
// 参数为多个整数,代表年、月、日、小时、分钟、秒、毫秒
new Date(2019, 0, 1, 0, 0, 0, 0) // Tue Jan 01 2019 00:00:00 GMT+0800 (中国标准时间)

关于Date构造函数的参数,有几点说明:

1.参数可以是负整数,代表1970年元旦之前的时间

new Date(-1378218728000) // Fri Apr 30 1926 17:27:52 GMT+0800 (中国标准时间)

2.只要是能被Date.parse()方法解析的字符串,都可以当作参数

new Date('2019-2-15')
new Date('2019/2/15')
new Date('02/15/2019')
new Date('2019-FEB-15')
new Date('FEB, 15, 2019')
new Date('FEB 15, 2019')
new Date('February, 15, 2019')
new Date('February 15, 2019')
new Date('15 Feb 2019')
new Date('15, February, 2019') // Fri Feb 15 2019 00:00:00 GMT+0800 (中国标准时间)

上面多种日期字符串的写法,返回的都是同一个时间

3.参数为年、月、日等多个整数时,年和月是不能省略的,其他参数都可以省略的。也就是说,这时至少需要两个参数,因为如果只使用“年”这一个参数,Date会将其解释为毫秒数
2.只要是能被Date.parse()方法解析的字符串,都可以当作参数

new Date(2019) //Thu Jan 01 1970 08:00:02 GMT+0800 (中国标准时间)
new Date(2019, 0) // Tue Jan 01 2019 00:00:00 GMT+0800 (中国标准时间)
new Date(2019, 0, 1) // Tue Jan 01 2019 00:00:00 GMT+0800 (中国标准时间)
new Date(2019, 0, 1, 0) // Tue Jan 01 2019 00:00:00 GMT+0800 (中国标准时间)
new Date(2019, 0, 1, 0, 0, 0, 0) // Tue Jan 01 2019 00:00:00 GMT+0800 (中国标准时间)

上面代码中,不管有几个参数,返回的都是2019年1月1日零点

各个参数的取值范围如下:

1.年:使用四位数年份,比如2000。如果写成两位数或个位数,则加上1900,即10代表1910年。如果是负数,表示公元前。

2.月:0表示一月,依次类推,11表示12月。

3.日:1到31。

4.小时:0到23。

5.分钟:0到59。

6.秒:0到59

7.毫秒:0到999

注意:月份从0开始计算,天数从1开始计算。另外,除了日期的默认值为1,小时、分钟、秒钟和毫秒的默认值都是0。这些参数如果超出了正常范围,会被自动折算。比如,如果月设为15,就折算为下一年的4月;日期设为0,就代表上个月的最后一天;参数还可以使用负数,表示扣去的时间

new Date(2019, -1) // Sat Dec 01 2018 00:00:00 GMT+0800 (中国标准时间)
new Date(2019, 0, -1) // Sun Dec 30 2018 00:00:00 GMT+0800 (中国标准时间)

日期的运算

类型自动转换时,Date实例如果转为数值,则等于对应的毫秒数;如果转为字符串,则等于对应的日期字符串。所以,两个日期实例对象进行减法运算时,返回的是它们间隔的毫秒数;进行加法运算时,返回的是两个字符串连接而成的新字符串

var d1 = new Date(2000, 2, 1);
var d2 = new Date(2000, 3, 1);
d2 - d1 // 2678400000
d2 + d1 // "Sat Apr 01 2000 00:00:00 GMT+0800 (CST)Wed Mar 01 2000 00:00:00 GMT+0800 (CST)"

静态方法

Date.now()

Date.now方法返回当前时间距离时间零点(1970年1月1日 00:00:00 UTC)的毫秒数,相当于 Unix 时间戳乘以1000

Date.now() // 1364026285194

Date.parse()

Date.parse方法用来解析日期字符串,返回该时间距离时间零点(1970年1月1日 00:00:00)的毫秒数。日期字符串应该符合 RFC 2822 和 ISO 8061 这两个标准,即YYYY-MM-DDTHH:mm:ss.sssZ格式,其中最后的Z表示时区。但是,其他格式也可以被解析

Date.parse('Aug 9, 1995')
Date.parse('January 26, 2011 13:51:50')
Date.parse('Mon, 25 Dec 1995 13:30:00 GMT')
Date.parse('Mon, 25 Dec 1995 13:30:00 +0430')
Date.parse('2011-10-10')
Date.parse('2011-10-10T14:48:00')

上面的日期字符串都可以解析。如果解析失败,返回NaN

Date.UTC()

Date.UTC方法接受年、月、日等变量作为参数,返回该时间距离时间零点(1970年1月1日 00:00:00 UTC)的毫秒数

// 格式
Date.UTC(year, month[, date[, hrs[, min[, sec[, ms]]]]])
// 用法
Date.UTC(2011, 0, 1, 2, 3, 4, 567) // 1293847384567

该方法的参数用法与Date构造函数完全一致,比如月从0开始计算,日期从1开始计算。区别在于Date.UTC方法的参数,会被解释为 UTC 时间(世界标准时间),Date构造函数的参数会被解释为当前时区的时间

实例方法

Date的实例对象,有几十个自己的方法,除了valueOf和toString,可以分为以下三类

to类:从Date对象返回一个字符串,表示指定的时间。
get类:获取Date对象的日期和时间。
set类:设置Date对象的日期和时间

Date.prototype.valueOf()

valueOf方法返回实例对象距离时间零点(1970年1月1日00:00:00 UTC)对应的毫秒数,该方法等同于getTime方法

var d = new Date();
d.valueOf() // 1552374623595
d.getTime() // 1552374623595

预期为数值的场合,Date实例会自动调用该方法,所以可以用该方法计算时间的间隔

to 类方法

Date.prototype.toString()

toString方法返回一个完整的日期字符串。toString是默认的调用方法,所以如果直接读取Date实例,就相当于调用这个方法

var d = new Date(2019, 0, 1);
d.toString() // "Tue Jan 01 2019 00:00:00 GMT+0800 (中国标准时间)"
d // "Tue Jan 01 2019 00:00:00 GMT+0800 (中国标准时间)"
Date.prototype.toUTCString()

toUTCString方法返回对应的 UTC 时间,也就是比北京时间晚8个小时

var d = new Date(2019, 0, 1);
d.toUTCString() // "Mon, 31 Dec 2018 16:00:00 GMT"
Date.prototype.toISOString()

toISOString方法返回对应时间的 ISO8601 写法

var d = new Date(2019, 0, 1);
d.toISOString() // "2018-12-31T16:00:00.000Z"

注意,toISOString方法返回的总是 UTC 时区的时间

Date.prototype.toJSON()

toJSON方法返回一个符合 JSON 格式的 ISO 日期字符串,与toISOString方法的返回结果完全相同

var d = new Date(2019, 0, 1);
d.toJSON() // "2018-12-31T16:00:00.000Z"
Date.prototype.toDateString()

toDateString方法返回日期字符串(不含小时、分和秒)

var d = new Date(2019, 0, 1);
d.toDateString() // "Tue Jan 01 2019"
Date.prototype.toTimeString()

toTimeString方法返回时间字符串(不含年月日)

var d = new Date(2019, 0, 1);
d.toTimeString() // "00:00:00 GMT+0800 (中国标准时间)"
本地时间

以下三种方法,可以将 Date 实例转为表示本地时间的字符串。

Date.prototype.toLocaleString():完整的本地时间。

Date.prototype.toLocaleDateString():本地日期(不含小时、分和秒)。

Date.prototype.toLocaleTimeString():本地时间(不含年月日)。

这三个方法都有两个可选的参数

dateObj.toLocaleString([locales[, options]])
dateObj.toLocaleDateString([locales[, options]])
dateObj.toLocaleTimeString([locales[, options]])

这两个参数中,locales是一个指定所用语言的字符串,options是一个配置对象

var d = new Date(2019, 0, 1);
d.toLocaleString('en-US') // "1/1/2019, 12:00:00 AM"
d.toLocaleString('zh-CN') // "2019/1/1 上午12:00:00"
d.toLocaleDateString('en-US') // "1/1/2019"
d.toLocaleDateString('zh-CN') // "2019/1/1"
d.toLocaleTimeString('en-US') // "12:00:00 AM"
d.toLocaleTimeString('zh-CN') // "上午12:00:00"

get 类方法

1.Date对象提供了一系列get*方法,用来获取实例对象某个方面的值。

2.getTime():返回实例距离1970年1月1日00:00:00的毫秒数,等同于valueOf方法

3.getDate():返回实例对象对应每个月的几号(从1开始)

4.getDay():返回星期几,星期日为0,星期一为1,以此类推

5.getFullYear():返回四位的年份

6.getMonth():返回月份(0表示1月,11表示12月)

7.getHours():返回小时(0-23)

8.getMilliseconds():返回毫秒(0-999)

9.getMinutes():返回分钟(0-59)

10.getSeconds():返回秒(0-59)

11.getTimezoneOffset():返回当前时间与 UTC 的时区差异,以分钟表示,返回结果考虑到了夏令时因素

这些get*方法返回的都是整数,不同方法返回值的范围不一样。

分钟和秒:0 到 59

小时:0 到 23

星期:0(星期天)到 6(星期六)

日期:1 到 31

月份:0(一月)到 11(十二月)

var d = new Date('January 6, 2019');
d.getDate() // 6
d.getMonth() // 0
d.getFullYear() // 2019
d.getTimezoneOffset() // -480

-480表示 UTC 比当前时间少480分钟,即当前时区比 UTC 早8个小时

上面get*方法返回的都是当前时区的时间,Date对象还提供了这些方法对应的 UTC 版本,用来返回 UTC 时间。

getUTCDate()/getUTCFullYear()/getUTCMonth()/getUTCDay()/getUTCHours()/getUTCMinutes()/getUTCSeconds()/getUTCMilliseconds()

set 类方法

Date对象提供了一系列set*方法,用来设置实例对象的各个方面。

setDate(date):设置实例对象对应的每个月的几号(1-31),返回改变后毫秒时间戳
setFullYear(year [, month, date]):设置四位年份
setHours(hour [, min, sec, ms]):设置小时(0-23)
setMilliseconds():设置毫秒(0-999)
setMinutes(min [, sec, ms]):设置分钟(0-59)
setMonth(month [, date]):设置月份(0-11)
setSeconds(sec [, ms]):设置秒(0-59)
setTime(milliseconds):设置毫秒时间戳

这些方法基本是跟get*方法一一对应的,但是没有setDay方法,因为星期几是计算出来的,而不是设置的。另外,需要注意的是,凡是涉及到设置月份,都是从0开始算的,即0是1月,11是12月

var d = new Date ('January 6, 2019');
d // Sun Jan 06 2019 00:00:00 GMT+0800 (中国标准时间)
d.setDate(9) // 1546963200000
d // Wed Jan 09 2019 00:00:00 GMT+0800 (中国标准时间)

set类方法和get类方法,可以结合使用,得到相对时间

var d = new Date();
d.setDate(d.getDate() + 1000); // 将日期向后推1000天
d.setHours(d.getHours() + 6); // 将时间设为6小时后
d.setFullYear(d.getFullYear() - 1); // 将年份设为去年

set*系列方法除了setTime(),都有对应的 UTC 版本,即设置 UTC 时区的时间

setUTCDate()/setUTCFullYear()/setUTCHours()/setUTCMilliseconds()/setUTCMinutes()/setUTCMonth()/setUTCSeconds()

RegExp 对象

RegExp对象提供正则表示式的功能

概述

正则表达式(regular expression)是一种表达文本模式(即字符串结构)的方法,有点像字符串的模板,常常用来按照“给定模式”匹配文本。比如,正则表达式给出一个 Email 地址的模式,然后用它来确定一个字符串是否为 Email 地址。JavaScript 的正则表达式体系是参照 Perl 5 建立的。新建正则表达式有两种方法

1.使用字面量,以斜杠表示开始和结束

var regex = /xyz/;

2.使用RegExp构造函数

var regex = new RegExp('xyz');

两种写法是等价的,都新建了一个内容为xyz的正则表达式对象。它们的主要区别是,第一种方法在引擎编译代码时就会新建正则表达式,第二种方法在运行时新建正则表达式,所以前者的效率较高。而且,前者比较便利和直观,所以实际应用中,基本上都采用字面量定义正则表达式。RegExp构造函数还可以接受第二个参数,表示修饰符

var regex = new RegExp('xyz', 'i');
// 等价于
var regex = /xyz/i;

实例属性

正则对象的实例属性分成两类:

1.修饰符相关,返回一个布尔值,表示对应的修饰符是否设置

(1).RegExp.prototype.ignoreCase:返回一个布尔值,表示是否设置了i修饰符

(2).RegExp.prototype.global:返回一个布尔值,表示是否设置了g修饰符

(3).RegExp.prototype.multiline:返回一个布尔值,表示是否设置了m修饰符

上面三个属性都是只读的

var r = /abc/igm;
r.ignoreCase // true
r.global // true
r.multiline // true

2.与修饰符无关的属性

(1).RegExp.prototype.lastIndex:返回一个整数,表示下一次开始搜索的位置。该属性可读写,但是只在进行连续搜索时有意义

(2).RegExp.prototype.source:返回正则表达式的字符串形式(不包括反斜杠),该属性只读

var r = /abc/igm;
r.lastIndex // 0
r.source // "abc"

实例方法

RegExp.prototype.test()

正则实例对象的test方法返回一个布尔值,表示当前模式是否能匹配参数字符串

/cat/.test('cats and dogs') // true

如果正则表达式带有g修饰符,则每一次test方法都从上一次结束的位置开始向后匹配

var r = /x/g;
var s = '_x_x';
r.lastIndex // 0
r.test(s) // true
r.lastIndex // 2
r.test(s) // true
r.lastIndex // 4
r.test(s) // false

带有g修饰符时,可以通过正则对象的lastIndex属性指定开始搜索的位置

var r = /x/g;
var s = '_x_x';
r.lastIndex = 4;
r.test(s) // false
r.lastIndex // 0
r.test(s) //true

上面代码指定从字符串的第五个位置开始搜索,这个位置为空,所以返回false。同时,lastIndex属性重置为0,所以第二次执行r.test(s)会返回true。

注意,带有g修饰符时,正则表达式内部会记住上一次的lastIndex属性,这时不应该更换所要匹配的字符串,否则会有一些难以察觉的错误

var r = /bb/g;
r.test('bb') // true
r.test('-bb-') // false

lastIndex属性只对同一个正则表达式有效

var count = 0;
while (/a/g.test('babaa')) count++;

上面代码会导致无限循环,因为while循环的每次匹配条件都是一个新的正则表达式,导致lastIndex属性总是等于0。如果正则模式是一个空字符串,则匹配所有字符串

new RegExp('').test('abc') // true

RegExp.prototype.exec()

正则实例对象的exec方法,用来返回匹配结果。如果发现匹配,就返回一个数组,成员是匹配成功的子字符串,否则返回null

var s = '_x_x';
var r1 = /x/;
var r2 = /y/;
r1.exec(s) // ["x"]
r2.exec(s) // null

如果正则表示式包含圆括号(即含有“组匹配”),则返回的数组会包括多个成员。第一个成员是整个匹配成功的结果,后面的成员就是圆括号对应的匹配成功的组。也就是说,第二个成员对应第一个括号,第三个成员对应第二个括号,以此类推。整个数组的length属性等于组匹配的数量再加1

var s = '_x_x';
var r = /_(x)/;
r.exec(s) // ["_x", "x"]

exec方法的返回数组还包含以下两个属性:

1.input:整个原字符串

2.index:整个模式匹配成功的开始位置(从0开始计数)

var r = /a(b+)a/;
var arr = r.exec('_abbba_aba_');
arr // ["abbba", "bbb"]
arr.index // 1
arr.input // "_abbba_aba_"

上面代码中的index属性等于1,是因为从原字符串的第二个位置开始匹配成功。如果正则表达式加上g修饰符,则可以使用多次exec方法,下一次搜索的位置从上一次匹配成功结束的位置开始

var reg = /a/g;
var str = 'abc_abc_abc'
var r1 = reg.exec(str);
r1 // ["a"]
r1.index // 0
reg.lastIndex // 1
var r2 = reg.exec(str);
r2 // ["a"]
r2.index // 4
reg.lastIndex // 5
var r3 = reg.exec(str);
r3 // ["a"]
r3.index // 8
reg.lastIndex // 9
var r4 = reg.exec(str);
r4 // null
reg.lastIndex // 0

利用g修饰符允许多次匹配的特点,可以用一个循环完成全部匹配

var reg = /a/g;
var str = 'abc_abc_abc'
while(true) {
  var match = reg.exec(str);
  if (!match) break;
  console.log('#' + match.index + ':' + match[0]);
}
// #0:a
// #4:a
// #8:a

正则实例对象的lastIndex属性不仅可读,还可写。设置了g修饰符的时候,只要手动设置了lastIndex的值,就会从指定位置开始匹配

字符串的实例方法

字符串的实例方法之中,有4种与正则表达式有关

1.String.prototype.match():返回一个数组,成员是所有匹配的子字符串。

2.String.prototype.search():按照给定的正则表达式进行搜索,返回一个整数,表示匹配开始的位置。

3.String.prototype.replace():按照给定的正则表达式进行替换,返回替换后的字符串。

4.String.prototype.split():按照给定规则进行字符串分割,返回一个数组,包含分割后的各个成员

String.prototype.match()

字符串实例对象的match方法对字符串进行正则匹配,返回匹配结果

var s = '_x_x';
var r1 = /x/;
var r2 = /y/;
s.match(r1) // ["x"]
s.match(r2) // null

从上面代码可以看到,字符串的match方法与正则对象的exec方法非常类似:匹配成功返回一个数组,匹配失败返回null。如果正则表达式带有g修饰符,则该方法与正则对象的exec方法行为不同,会一次性返回所有匹配成功的结果

var s = 'abba';
var r = /a/g;
s.match(r) // ["a", "a"]
r.exec(s) // ["a"]

设置正则表达式的lastIndex属性,对match方法无效,匹配总是从字符串的第一个字符开始

String.prototype.search()

字符串对象的search方法,返回第一个满足条件的匹配结果在整个字符串中的位置。如果没有任何匹配,则返回-1

'_x_x'.search(/x/) // 1

String.prototype.replace()

字符串对象的replace方法可以替换匹配的值。它接受两个参数,第一个是正则表达式,表示搜索模式,第二个是替换的内容

str.replace(search, replacement)

正则表达式如果不加g修饰符,就替换第一个匹配成功的值,否则替换所有匹配成功的值

'aaa'.replace('a', 'b') // "baa"
'aaa'.replace(/a/, 'b') // "baa"
'aaa'.replace(/a/g, 'b') // "bbb"

replace方法的一个应用,就是消除字符串首尾两端的空格

var str = '  #id div.class  ';
str.replace(/^\s+|\s+$/g, '') // "#id div.class"

replace方法的第二个参数可以使用美元符号$,用来指代所替换的内容。

$&:匹配的子字符串

$`:匹配结果前面的文本

$':匹配结果后面的文本

$n:匹配成功的第n组内容,n是从1开始的自然数

$$:指代美元符号$

'hello world'.replace(/(\w+)\s(\w+)/, '$2 $1') // "world hello"
'abc'.replace('b', '[$`-$&-$\']') // "a[a-b-c]c"

上面代码中,第一个例子是将匹配的组互换位置,第二个例子是改写匹配的值。replace方法的第二个参数还可以是一个函数,将每一个匹配内容替换为函数返回值

'3 and 5'.replace(/[0-9]+/g, function (match) {
  return 2 * match;
}) // "6 and 10"
var a = 'The quick brown fox jumped over the lazy dog.';
var pattern = /quick|brown|lazy/ig;
a.replace(pattern, function replacer(match) {
  return match.toUpperCase();
}); // The QUICK BROWN fox jumped over the LAZY dog.

作为replace方法第二个参数的替换函数,可以接受多个参数。其中,第一个参数是捕捉到的内容,第二个参数是捕捉到的组匹配(有多少个组匹配,就有多少个对应的参数)。此外,最后还可以添加两个参数,倒数第二个参数是捕捉到的内容在整个字符串中的位置(比如从第五个位置开始),最后一个参数是原字符串。下面是一个网页模板替换的例子

var prices = {  'p1': '$1.99',  'p2': '$9.99',  'p3': '$5.00'};
var template = '<span id="p1"></span><span id="p2"></span><span id="p3"></span>';
template.replace(
  /(<span id=")(.*?)(">)(<\/span>)/g,
  function(match, $1, $2, $3, $4){
    return $1 + $2 + $3 + prices[$2] + $4;
  }
); // "<span id="p1">$1.99</span><span id="p2">$9.99</span><span id="p3">$5.00</span>"

上面代码的捕捉模式中,有四个括号,所以会产生四个组匹配,在匹配函数中用$1到$4表示。匹配函数的作用是将价格插入模板中

String.prototype.split()

字符串对象的split方法按照正则规则分割字符串,返回一个由分割后的各个部分组成的数组

str.split(separator, [limit])

该方法接受两个参数,第一个参数是正则表达式,表示分隔规则,第二个参数是返回数组的最大成员数

// 非正则分隔
'a,  b,c, d'.split(',') // [ 'a', '  b', 'c', ' d' ]
// 正则分隔,去除多余的空格
'a,  b,c, d'.split(/, */) // [ 'a', 'b', 'c', 'd' ]
// 指定返回数组的最大成员
'a,  b,c, d'.split(/, */, 2) [ 'a', 'b' ]

上面代码使用正则表达式,去除了子字符串的逗号后面的空格

'aaa*a*'.split(/a*/) // [ '', '*', '*' ]
'aaa**a*'.split(/a*/) // ["", "*", "*", "*"]

上面代码的分割规则是0次或多次的a,由于正则默认是贪婪匹配,所以例一的第一个分隔符是aaa,第二个分割符是a,将字符串分成三个部分,包含开始处的空字符串。例二的第一个分隔符是aaa,第二个分隔符是0个a(即空字符),第三个分隔符是a,所以将字符串分成四个部分。如果正则表达式带有括号,则括号匹配的部分也会作为数组成员返回

'aaa*a*'.split(/(a*)/) // [ '', 'aaa', '*', 'a', '*' ]

匹配规则

正则表达式的规则很复杂

字面量字符和元字符

大部分字符在正则表达式中就是字面的含义,比如/a/匹配a,/b/匹配b。如果在正则表达式中,某个字符只表示它字面的含义(就像前面的a和b),那么它们就叫做“字面量字符”(literal characters)

/dog/.test('old dog') // true

除了字面量字符以外,还有一部分字符有特殊含义,不代表字面的意思。它们叫做“元字符”(metacharacters),主要有以下几个

点字符(.)

点字符(.)匹配除回车(r)、换行(n) 、行分隔符(u2028)和段分隔符(u2029)以外的所有字符。注意,对于码点大于0xFFFF字符,点字符不能正确匹配,会认为这是两个字符

/c.t/

c.t匹配c和t之间包含任意一个字符的情况,只要这三个字符在同一行,比如cat、c2t、c-t等等,但是不匹配coot

位置字符

位置字符用来提示字符所处的位置,主要有两个字符:

1.^ 表示字符串的开始位置
2.$ 表示字符串的结束位置

// test必须出现在开始位置
/^test/.test('test123') // true
// test必须出现在结束位置
/test$/.test('new test') // true
// 从开始位置到结束位置只有test
/^test$/.test('test') // true
/^test$/.test('test test') // false
选择符(|)

竖线符号(|)在正则表达式中表示“或关系”(OR),即cat|dog表示匹配cat或dog

/11|22|33/.test('911') // true

选择符会包括它前后的多个字符,比如/ab|cd/指的是匹配ab或者cd,而不是指匹配b或者c。如果想修改这个行为,可以使用圆括号

/a( |\t)b/.test('a\tb') // true

上面代码指的是,a和b之间有一个空格或者一个制表符。其他的元字符还包括、*、+、?、()、[]、{}等

转义符

正则表达式中那些有特殊含义的元字符,如果要匹配它们本身,就需要在它们前面要加上反斜杠。比如要匹配+,就要写成+

/1+1/.test('1+1') // false
/1\+1/.test('1+1') // true

正则表达式中,需要反斜杠转义的,一共有12个字符:^、.、[、$、(、)、|、*、+、?、{和。需要特别注意的是,如果使用RegExp方法生成正则对象,转义需要使用两个斜杠,因为字符串内部会先转义一次

(new RegExp('1\+1')).test('1+1')  // false  
(new RegExp('1\\+1')).test('1+1')  // true

上面代码中,RegExp作为构造函数,参数是一个字符串。但是,在字符串内部,反斜杠也是转义字符,所以它会先被反斜杠转义一次,然后再被正则表达式转义一次,因此需要两个反斜杠转义

特殊字符

正则表达式对一些不能打印的特殊字符,提供了表达方法

cX 表示Ctrl-[X],其中的X是A-Z之中任一个英文字母,用来匹配控制字符

[b] 匹配退格键(U+0008),不要与b混淆

n 匹配换行键

r 匹配回车键

t 匹配制表符 tab(U+0009)

v 匹配垂直制表符(U+000B)

f 匹配换页符(U+000C)

0 匹配null字符(U+0000)

xhh 匹配一个以两位十六进制数(x00-xFF)表示的字符

uhhhh 匹配一个以四位十六进制数(u0000-uFFFF)表示的 Unicode 字符

字符类

字符类(class)表示有一系列字符可供选择,只要匹配其中一个就可以了。所有可供选择的字符都放在方括号内,比如[xyz] 表示x、y、z之中任选一个匹配

/[abc]/.test('hello world') // false
/[abc]/.test('apple') // true

有两个字符在字符类中有特殊含义:

脱字符(^)

如果方括号内的第一个字符是[^],则表示除了字符类之中的字符,其他字符都可以匹配。比如,1表示除了x、y、z之外都可以匹配

/[^abc]/.test('hello world') // true
/[^abc]/.test('bbc') // false

如果方括号内没有其他字符,即只有[^],就表示匹配一切字符,其中包括换行符。相比之下,点号作为元字符(.)是不包括换行符的

var s = 'Please yes\nmake my day!';
s.match(/yes.*day/) // null
s.match(/yes[^]*day/) // [ 'yes\nmake my day']

上面代码中,字符串s含有一个换行符,点号不包括换行符,所以第一个正则表达式匹配失败;第二个正则表达式[^]包含一切字符,所以匹配成功。注意,脱字符只有在字符类的第一个位置才有特殊含义,否则就是字面含义

连字符(-)

某些情况下,对于连续序列的字符,连字符(-)用来提供简写形式,表示字符的连续范围。比如,[abc]可以写成[a-c],[0123456789]可以写成[0-9],同理[A-Z]表示26个大写字母

/a-z/.test('b') // false
/[a-z]/.test('b') // true

上面代码中,当连字号(dash)不出现在方括号之中,就不具备简写的作用,只代表字面的含义,所以不匹配字符b。只有当连字号用在方括号之中,才表示连续的字符序列。以下都是合法的字符类简写形式

[0-9.,]
[0-9a-fA-F]
[a-zA-Z0-9-]
[1-31]

上面代码中最后一个字符类[1-31],不代表1到31,只代表1到3。连字符还可以用来指定 Unicode 字符的范围

var str = "\u0130\u0131\u0132";
/[\u0128-\uFFFF]/.test(str) // true

另外,不要过分使用连字符,设定一个很大的范围,否则很可能选中意料之外的字符。最典型的例子就是[A-z],表面上它是选中从大写的A到小写的z之间52个字母,但是由于在 ASCII 编码之中,大写字母与小写字母之间还有其他字符,结果就会出现意料之外的结果

/[A-z]/.test('\\') // true

预定义模式

预定义模式指的是某些常见模式的简写方式。

\d 匹配0-9之间的任一数字,相当于[0-9]。
\D 匹配所有0-9以外的字符,相当于[^0-9]。
\w 匹配任意的字母、数字和下划线,相当于[A-Za-z0-9_]。
\W 除所有字母、数字和下划线以外的字符,相当于[^A-Za-z0-9_]。
\s 匹配空格(包括换行符、制表符、空格符等),相等于[ \t\r\n\v\f]。
\S 匹配非空格的字符,相当于[^ \t\r\n\v\f]。
\b 匹配词的边界。
\B 匹配非词边界,即在词的内部

通常,正则表达式遇到换行符(n)就会停止匹配

var html = "<b>Hello</b>\n<i>world!</i>";
/.*/.exec(html)[0] // "<b>Hello</b>"
/[\S\s]*/.exec(html)[0] // "<b>Hello</b>\n<i>world!</i>"

字符串html包含一个换行符,结果点字符(.)不匹配换行符,导致匹配结果可能不符合原意。这时使用s字符类,就能包括换行符

重复类

模式的精确匹配次数,使用大括号({})表示。{n}表示恰好重复n次,{n,}表示至少重复n次,{n,m}表示重复不少于n次,不多于m次

/lo{2}k/.test('look') // true
/lo{2,5}k/.test('looook') // true

量词符

量词符用来设定某个模式出现的次数。

// ? 问号表示某个模式出现0次或1次,等同于{0, 1};t 出现0次或1次
/t?est/.test('test') // true
/t?est/.test('est') // true
//+ 加号表示某个模式出现1次或多次,等同于{1,}; t 出现1次或多次
/t+est/.test('ttest') // true
/t+est/.test('est') // false
//* 星号表示某个模式出现0次或多次,等同于{0,}; t 出现0次或多次
/t*est/.test('tttest') // true
/t*est/.test('est') // true

贪婪模式

上面的三个量词符,默认情况下都是最大可能匹配,即匹配直到下一个字符不满足匹配规则为止。这被称为贪婪模式

var s = 'aaa';
s.match(/a+/) // ["aaa"]

上面代码中,模式是/a+/,表示匹配1个或多个a,那么到底会匹配几个a呢?因为默认是贪婪模式,会一直匹配到字符a不出现为止,所以匹配结果是3个a。如果想将贪婪模式改为非贪婪模式,可以在量词符后面加一个问号

var s = 'aaa';
s.match(/a+?/) // ["a"]

上面代码中,模式结尾添加了一个问号/a+?/,这时就改为非贪婪模式,一旦条件满足,就不再往下匹配。除了非贪婪模式的加号,还有非贪婪模式的星号(*)和非贪婪模式的问号(?)

//+?:表示某个模式出现1次或多次,匹配时采用非贪婪模式;*?:表示某个模式出现0次或多次,匹配时采用非贪婪模式;??:表示某个模式出现0次或1次,匹配时采用非贪婪模式
'abb'.match(/ab*b/) // ["abb"]
'abb'.match(/ab*?b/) // ["ab"]
'abb'.match(/ab?b/) // ["abb"]
'abb'.match(/ab??b/) // ["ab"]

修饰符

修饰符(modifier)表示模式的附加规则,放在正则模式的最尾部。修饰符可以单个使用,也可以多个一起使用

g 修饰符

默认情况下,第一次匹配成功后,正则对象就停止向下匹配了。g修饰符表示全局匹配(global),加上它以后,正则对象将匹配全部符合条件的结果,主要用于搜索和替换

var regex = /b/;
var str = 'abba';
regex.test(str); // true
regex.test(str); // true
regex.test(str); // true

上面代码中,正则模式不含g修饰符,每次都是从字符串头部开始匹配。所以连续做了三次匹配都返回true

var regex = /b/g;
var str = 'abba';
regex.test(str); // true
regex.test(str); // true
regex.test(str); // false

上面代码中,正则模式含有g修饰符,每次都是从上一次匹配成功处,开始向后匹配。因为字符串abba只有两个b,所以前两次匹配结果为true,第三次匹配结果为false

i 修饰符

默认情况下,正则对象区分字母的大小写,加上i修饰符以后表示忽略大小写(ignoreCase)

/abc/.test('ABC') // false
/abc/i.test('ABC') // true
m 修饰符

m修饰符表示多行模式(multiline),会修改^和$的行为。默认情况下(即不加m修饰符时),^和$匹配字符串的开始处和结尾处,加上m修饰符以后,^和$还会匹配行首和行尾,即^和$会识别换行符(n)

/world$/.test('hello world\n') // false
/world$/m.test('hello world\n') // true

上面的代码中,字符串结尾处有一个换行符。如果不加m修饰符,匹配不成功,因为字符串的结尾不是world;加上以后,$可以匹配行尾

/^b/m.test('a\nb') // true

上面代码要求匹配行首的b,如果不加m修饰符,就相当于b只能处在字符串的开始处。加上b修饰符以后,换行符n也会被认为是一行的开始

组匹配

概述

正则表达式的括号表示分组匹配,括号中的模式可以用来匹配分组的内容

/fred+/.test('fredd') // true
/(fred)+/.test('fredfred') // true

上面代码中,第一个模式没有括号,结果+只表示重复字母d,第二个模式有括号,结果+就表示匹配fred这个词。

var m = 'abcabc'.match(/(.)b(.)/);
m // ['abc', 'a', 'c']

上面代码中,正则表达式/(.)b(.)/一共使用两个括号,第一个括号捕获a,第二个括号捕获c

注意,使用组匹配时,不宜同时使用g修饰符,否则match方法不会捕获分组的内容

var m = 'abcabc'.match(/(.)b(.)/g);
m // ['abc', 'abc']

上面代码使用带g修饰符的正则表达式,结果match方法只捕获了匹配整个表达式的部分。这时必须使用正则表达式的exec方法,配合循环,才能读到每一轮匹配的组捕获

var str = 'abcabc';
var reg = /(.)b(.)/g;
while (true) {
  var result = reg.exec(str);
  if (!result) break;
  console.log(result);
}
// ["abc", "a", "c"]
// ["abc", "a", "c"]

正则表达式内部,还可以用n引用括号匹配的内容,n是从1开始的自然数,表示对应顺序的括号

/(.)b(.)\1b\2/.test("abcabc") // true

上面的代码中,1表示第一个括号匹配的内容(即a),2表示第二个括号匹配的内容(即c)。括号还可以嵌套

/y((..)\2)\1/.test('yabababab') // true

上面代码中,1指向外层括号,2指向内层括号

var html = '<b class="hello">Hello</b><i>world</i>';
var tag = /<(\w+)([^>]*)>(.*?)<\/\1>/g;
var match = tag.exec(html);
match[1] // "b"
match[2] // " class="hello""
match[3] // "Hello"
match = tag.exec(html);
match[1] // "i"
match[2] // ""
match[3] // "world"
非捕获组

(?:x)称为非捕获组(Non-capturing group),表示不返回该组匹配的内容,即匹配的结果中不计入这个括号。

非捕获组的作用请考虑这样一个场景,假定需要匹配foo或者foofoo,正则表达式就应该写成/(foo){1, 2}/,但是这样会占用一个组匹配。这时,就可以使用非捕获组,将正则表达式改为/(?:foo){1, 2}/,它的作用与前一个正则是一样的,但是不会单独输出括号内部的内容

var m = 'abc'.match(/(?:.)b(.)/);
m // ["abc", "c"]

上面代码中的模式,一共使用了两个括号。其中第一个括号是非捕获组,所以最后返回的结果中没有第一个括号,只有第二个括号匹配的内容

// 正常匹配
var url = /(http|ftp):\/\/([^/\r\n]+)(\/[^\r\n]*)?/;
url.exec('http://google.com/'); // ["http://google.com/", "http", "google.com", "/"]
// 非捕获组匹配
var url = /(?:http|ftp):\/\/([^/\r\n]+)(\/[^\r\n]*)?/;
url.exec('http://google.com/'); // ["http://google.com/", "google.com", "/"]

上面的代码中,前一个正则表达式是正常匹配,第一个括号返回网络协议;后一个正则表达式是非捕获匹配,返回结果中不包括网络协议

先行断言

x(?=y)称为先行断言(Positive look-ahead),x只有在y前面才匹配,y不会被计入返回结果。比如,要匹配后面跟着百分号的数字,可以写成/d+(?=%)/。“先行断言”中,括号里的部分是不会返回的

var m = 'abc'.match(/b(?=c)/);
m // ["b"]
先行否定断言

x(?!y)称为先行否定断言(Negative look-ahead),x只有不在y前面才匹配,y不会被计入返回结果。比如,要匹配后面跟的不是百分号的数字,就要写成/d+(?!%)/

/\d+(?!\.)/.exec('3.14') // ["14"]

上面代码中,正则表达式指定,只有不在小数点前面的数字才会被匹配,因此返回的结果就是14。“先行否定断言”中,括号里的部分是不会返回的

var m = 'abd'.match(/b(?!c)/);
m // ['b']

JSON 对象

JSON 格式

JSON 格式(JavaScript Object Notation 的缩写)是一种用于数据交换的文本格式,2001年由 Douglas Crockford 提出,目的是取代繁琐笨重的 XML 格式。相比 XML 格式,JSON 格式有两个显著的优点:书写简单,一目了然;符合 JavaScript 原生语法,可以由解释引擎直接处理,不用另外添加解析代码。所以,JSON 迅速被接受,已经成为各大网站交换数据的标准格式,并被写入标准。每个 JSON 对象就是一个值,可能是一个数组或对象,也可能是一个原始类型的值。总之,只能是一个值,不能是两个或更多的值。JSON 对值的类型和格式有严格的规定

1.字符串必须使用双引号表示,不能使用单引号
2.数组或对象最后一个成员的后面,不能加逗号
3.对象的键名必须放在双引号里面
{ name: "张三", 'age': 32 }  
4.原始类型的值只有四种:字符串、数值(必须以十进制表示)、布尔值和null(不能使用NaN, Infinity, -Infinity和undefined)
[32, 64, 128, 0xFFF]
{ "name": "张三", "age": undefined }
5.复合类型的值只能是数组或对象,不能是函数、正则表达式对象、日期对象
{ "name": "张三",
  "birthday": new Date('Fri, 26 Aug 2011 07:13:10 GMT'),
  "getName": function () {
      return this.name;
  }
}

注意,null、空数组和空对象都是合法的 JSON 值

JSON 对象

JSON对象是 JavaScript 的原生对象,用来处理 JSON 格式数据。它有两个静态方法:JSON.stringify()和JSON.parse()

JSON.stringify()

基本用法

JSON.stringify方法用于将一个值转为 JSON 字符串。该字符串符合 JSON 格式,并且可以被JSON.parse方法还原

JSON.stringify('abc') // ""abc""
JSON.stringify([1, "false", false]) // '[1,"false",false]'
JSON.stringify({ name: "张三" }) // '{"name":"张三"}'

注意,对于原始类型的字符串,转换结果会带双引号

JSON.stringify('foo') === "foo" // false
JSON.stringify('foo') === "\"foo\"" // true

如果对象的属性是undefined、函数或 XML 对象,该属性会被JSON.stringify过滤

var obj = {
  a: undefined,
  b: function () {}
};
JSON.stringify(obj) // "{}"

如果数组的成员是undefined、函数或 XML 对象,则这些值被转成null

var arr = [undefined, function () {}];
JSON.stringify(arr) // "[null,null]"

正则对象会被转成空对象

JSON.stringify(/foo/) // "{}"

JSON.stringify方法会忽略对象的不可遍历的属性

var obj = {};
Object.defineProperties(obj, {
  'foo': { value: 1, enumerable: true },
  'bar': { value: 2, enumerable: false }
});
JSON.stringify(obj); // "{"foo":1}"

上面代码中,bar是obj对象的不可遍历属性,JSON.stringify方法会忽略这个属性

第二个参数

JSON.stringify方法还可以接受一个数组,作为第二个参数,指定需要转成字符串的属性

var obj = {
  'prop1': 'value1',
  'prop2': 'value2',
  'prop3': 'value3'
};
var selectedProperties = ['prop1', 'prop2'];
JSON.stringify(obj, selectedProperties) // "{"prop1":"value1","prop2":"value2"}"

上面代码中,JSON.stringify方法的第二个参数指定,只转prop1和prop2两个属性。这个类似白名单的数组,只对对象的属性有效,对数组无效

JSON.stringify(['a', 'b'], ['0']) // "["a","b"]"
JSON.stringify({0: 'a', 1: 'b'}, ['0']) // "{"0":"a"}"

第二个参数还可以是一个函数,用来更改JSON.stringify的返回值

function f(key, value) {
  if (typeof value === "number") {
    value = 2 * value;
  }
  return value;
}
JSON.stringify({ a: 1, b: 2 }, f) // '{"a": 2,"b": 4}'

注意,这个处理函数是递归处理所有的键

var o = {a: {b: 1}};
function f(key, value) {
  console.log("["+ key +"]:" + value);
  return value;
}
JSON.stringify(o, f)
// []:[object Object]
// [a]:[object Object]
// [b]:1
// '{"a":{"b":1}}'

上面代码中,对象o一共会被f函数处理三次,最后那行是JSON.stringify的输出。第一次键名为空,键值是整个对象o;第二次键名为a,键值是{b: 1};第三次键名为b,键值为1。递归处理中,每一次处理的对象,都是前一次返回的值

var o = {a: 1};
function f(key, value) {
  if (typeof value === 'object') {
    return {b: 2};
  }
  return value * 2;
}
JSON.stringify(o, f) // "{"b": 4}"

如果处理函数返回undefined或没有返回值,则该属性会被忽略

function f(key, value) {
  if (typeof(value) === "string") {
    return undefined;
  }
  return value;
}
JSON.stringify({ a: "abc", b: 123 }, f) // '{"b": 123}'
第三个参数

JSON.stringify还可以接受第三个参数,用于增加返回的 JSON 字符串的可读性。如果是数字,表示每个属性前面添加的空格(最多不超过10个);如果是字符串(不超过10个字符),则该字符串会添加在每行前面

JSON.stringify({ p1: 1, p2: 2 }, null, 2);
/*
"{
  "p1": 1,
  "p2": 2
}"
*/
JSON.stringify({ p1:1, p2:2 }, null, '|-');
/*
"{
|-"p1": 1,
|-"p2": 2
}"
*/
参数对象的 toJSON 方法

如果参数对象有自定义的toJSON方法,那么JSON.stringify会使用这个方法的返回值作为参数,而忽略原对象的其他属性

一个普通的对象

var user = {
  firstName: '三',
  lastName: '张',
  get fullName(){
    return this.lastName + this.firstName;
  }
};
JSON.stringify(user) // "{"firstName":"三","lastName":"张","fullName":"张三"}"

为这个对象加上toJSON方法

var user = {
  firstName: '三',
  lastName: '张',
  get fullName(){
    return this.lastName + this.firstName;
  },
  toJSON: function () {
    return {
      name: this.lastName + this.firstName
    };
  }
};
JSON.stringify(user) // "{"name":"张三"}"

上面代码中,JSON.stringify发现参数对象有toJSON方法,就直接使用这个方法的返回值作为参数,而忽略原对象的其他参数。Date对象就有一个自己的toJSON方法

var date = new Date('2015-01-01');
date.toJSON() // "2015-01-01T00:00:00.000Z"
JSON.stringify(date) // ""2015-01-01T00:00:00.000Z""

toJSON方法的一个应用是,将正则对象自动转为字符串。因为JSON.stringify默认不能转换正则对象,但是设置了toJSON方法以后,就可以转换正则对象了

var obj = {  reg: /foo/ };
// 不设置 toJSON 方法时
JSON.stringify(obj) // "{"reg":{}}"
// 设置 toJSON 方法时
RegExp.prototype.toJSON = RegExp.prototype.toString;
JSON.stringify(/foo/) // ""/foo/""

上面代码在正则对象的原型上面部署了toJSON方法,将其指向toString方法,因此遇到转换成JSON时,正则对象就先调用toJSON方法转为字符串,然后再被JSON.stingify方法处理

JSON.parse()

JSON.parse方法用于将 JSON 字符串转换成对应的值

JSON.parse('true') // true
JSON.parse('[1, 5, "false"]') // [1, 5, "false"]
JSON.parse('null') // null
var o = JSON.parse('{"name": "张三"}');
o.name // 张三

如果传入的字符串不是有效的 JSON 格式,JSON.parse方法将报错

JSON.parse("'String'") // SyntaxError: Unexpected token ILLEGAL

上面代码中,双引号字符串中是一个单引号字符串,因为单引号字符串不符合 JSON 格式,所以报错。为了处理解析错误,可以将JSON.parse方法放在try...catch代码块中

try {
  JSON.parse("'String'");
} catch(e) {
  console.log('parsing error');
}

JSON.parse方法可以接受一个处理函数,作为第二个参数,用法与JSON.stringify方法类似

function f(key, value) {
  if (key === 'a') {
    return value + 10;
  }
  return value;
}
JSON.parse('{"a": 1, "b": 2}', f) // {a: 11, b: 2}

面向对象编程

实例对象与 new 命令

JavaScript 语言具有很强的面向对象编程能力,这里介绍 JavaScript 面向对象编程的基础知识

对象是什么

面向对象编程(Object Oriented Programming,缩写为 OOP)是目前主流的编程范式。它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。对象可以复用,通过继承机制还可以定制。因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。那么,“对象”(object)到底是什么?我们从两个层次来理解

对象是单个实物的抽象

一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个与远程服务器的连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程

对象是一个容器,封装了属性(property)和方法(method)

属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal对象,使用“属性”记录具体是那一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)

构造函数

面向对象编程的第一步,就是要生成对象。前面说过,对象是单个实物的抽象。通常需要一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成。典型的面向对象编程语言(比如 C++ 和 Java),都有“类”(class)这个概念。所谓“类”就是对象的模板,对象就是“类”的实例。但是,JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)。JavaScript 语言使用构造函数(constructor)作为对象的模板。所谓”构造函数”,就是专门用来生成实例对象的函数。它就是对象的模板,描述实例对象的基本结构。一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。构造函数就是一个普通的函数,但是有自己的特征和用法

var Vehicle = function () {
  this.price = 1000;
};

上面代码中,Vehicle就是构造函数。为了与普通函数区别,构造函数名字的第一个字母通常大写

构造函数的特点有两个。

1.函数体内部使用了this关键字,代表了所要生成的对象实例

2.生成对象的时候,必须使用new命令

new 命令

基本用法

new命令的作用,就是执行构造函数,返回一个实例对象

var Vehicle = function () {
  this.price = 1000;
};
var v = new Vehicle();
v.price // 1000

上面代码通过new命令,让构造函数Vehicle生成一个实例对象,保存在变量v中。这个新生成的实例对象,从构造函数Vehicle得到了price属性。new命令执行时,构造函数内部的this,就代表了新生成的实例对象,this.price表示实例对象有一个price属性,值是1000。使用new命令时,根据需要构造函数也可以接受参数

var Vehicle = function (p) {
  this.price = p;
};
var v = new Vehicle(500);

new命令本身就可以执行构造函数,所以后面的构造函数可以带括号,也可以不带括号。下面两行代码是等价的,但是为了表示这里是函数调用,推荐使用括号

var v = new Vehicle(); // 推荐的写法
var v = new Vehicle; // 不推荐的写法

一个很自然的问题是,如果忘了使用new命令,直接调用构造函数会发生什么事?这种情况下,构造函数就变成了普通函数,并不会生成实例对象。而且由于后面会说到的原因,this这时代表全局对象,将造成一些意想不到的结果

var Vehicle = function (){
  this.price = 1000;
};
var v = Vehicle();
v // undefined
price // 1000

上面代码中,调用Vehicle构造函数时,忘了加上new命令。结果,变量v变成了undefined,而price属性变成了全局变量。因此,应该非常小心,避免不使用new命令、直接调用构造函数。为了保证构造函数必须与new命令一起使用,一个解决办法是,构造函数内部使用严格模式,即第一行加上use strict。这样的话,一旦忘了使用new命令,直接调用构造函数就会报错

function Fubar(foo, bar){
  'use strict';
  this._foo = foo;
  this._bar = bar;
}
Fubar() // TypeError: Cannot set property '_foo' of undefined

上面代码的Fubar为构造函数,use strict命令保证了该函数在严格模式下运行。由于严格模式中,函数内部的this不能指向全局对象,默认等于undefined,导致不加new调用会报错(JavaScript 不允许对undefined添加属性)。另一个解决办法,构造函数内部判断是否使用new命令,如果发现没有使用,则直接返回一个实例对象

function Fubar(foo, bar) {
  if (!(this instanceof Fubar)) {
    return new Fubar(foo, bar);
  }
  this._foo = foo;
  this._bar = bar;
}
Fubar(1, 2)._foo // 1
(new Fubar(1, 2))._foo // 1

上面代码中的构造函数,不管加不加new命令,都会得到同样的结果

new 命令的原理

使用new命令时,它后面的函数依次执行下面的步骤

1.创建一个空对象,作为将要返回的对象实例

2.将这个空对象的原型,指向构造函数的prototype属性

3.将这个空对象赋值给函数内部的this关键字

4.开始执行构造函数内部的代码

也就是说,构造函数内部,this指的是一个新生成的空对象,所有针对this的操作,都会发生在这个空对象上。构造函数之所以叫“构造函数”,就是说这个函数的目的,就是操作一个空对象(即this对象),将其“构造”为需要的样子。如果构造函数内部有return语句,而且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象

var Vehicle = function () {
  this.price = 1000;
  return 1000;
};
(new Vehicle()) === 1000 // false

上面代码中,构造函数Vehicle的return语句返回一个数值。这时,new命令就会忽略这个return语句,返回“构造”后的this对象。但是,如果return语句返回的是一个跟this无关的新对象,new命令会返回这个新对象,而不是this对象。这一点需要特别引起注意

var Vehicle = function (){
  this.price = 1000;
  return { price: 2000 };
};
(new Vehicle()).price // 2000

上面代码中,构造函数Vehicle的return语句,返回的是一个新对象。new命令会返回这个对象,而不是this对象。另一方面,如果对普通函数(内部没有this关键字的函数)使用new命令,则会返回一个空对象

function getMessage() {
  return 'this is a message';
}
var msg = new getMessage();
msg // {}
typeof msg // "object"

上面代码中,getMessage是一个普通函数,返回一个字符串。对它使用new命令,会得到一个空对象。这是因为new命令总是返回一个对象,要么是实例对象,要么是return语句指定的对象。本例中,return语句返回的是字符串,所以new命令就忽略了该语句。new命令简化的内部流程,可以用下面的代码表示

function _new(/* 构造函数 */ constructor, /* 构造函数参数 */ params) {  
  var args = [].slice.call(arguments); // 将 arguments 对象转为数组  
  var constructor = args.shift(); // 取出构造函数  
  var context = Object.create(constructor.prototype); // 创建一个空对象,继承构造函数的 prototype 属性  
  var result = constructor.apply(context, args); // 执行构造函数  
  return (typeof result === 'object' && result != null) ? result : context; // 如果返回结果是对象,就直接返回,否则返回 context 对象
}
var actor = _new(Person, '张三', 28); // 实例

new.target

函数内部可以使用new.target属性。如果当前函数是new命令调用,new.target指向当前函数,否则为undefined

function f() {
  console.log(new.target === f);
}
f() // false
new f() // true

使用这个属性,可以判断函数调用的时候,是否使用new命令

function f() {
  if (!new.target) {
    throw new Error('请使用 new 命令调用!');
  }
  // ...
}
f() // Uncaught Error: 请使用 new 命令调用!

Object.create() 创建实例对象

构造函数作为模板,可以生成实例对象。但是,有时拿不到构造函数,只能拿到一个现有的对象。我们希望以这个现有的对象作为模板,生成新的实例对象,这时就可以使用Object.create()方法

var person1 = {
  name: '张三',
  age: 38,
  greeting: function() {
    console.log('Hi! I\'m ' + this.name + '.');
  }
};
var person2 = Object.create(person1);
person2.name // 张三
person2.greeting() // Hi! I'm 张三.

上面代码中,对象person1是person2的模板,后者继承了前者的属性和方法

this 关键字

涵义

this关键字是一个非常重要的语法点。毫不夸张地说,不理解它的含义大部分开发任务都无法完成。this可以用在构造函数之中,表示实例对象;除此之外,this还可以用在别的场合。但不管是什么场合,this都有一个共同点:它总是返回一个对象;简单说,this就是属性或方法“当前”所在的对象

var person = {
  name: '张三',
  describe: function () { return '姓名:'+ this.name; }
};
person.describe() // "姓名:张三"

上面代码中,this.name表示name属性所在的那个对象。由于this.name是在describe方法中调用,而describe方法所在的当前对象是person,因此this指向person,this.name就是person.name。由于对象的属性可以赋给另一个对象,所以属性所在的当前对象是可变的,即this的指向是可变的

var A = {
  name: '张三',
  describe: function () { return '姓名:'+ this.name; }
};
var B = { name: '李四' };
B.describe = A.describe;
B.describe() // "姓名:李四"

上面代码中,A.describe属性被赋给B,于是B.describe就表示describe方法所在的当前对象是B,所以this.name就指向B.name。稍稍重构这个例子,this的动态指向就能看得更清楚

function f() { return '姓名:'+ this.name; }
var A = { name: '张三', describe: f };
var B = { name: '李四', describe: f };
A.describe() // "姓名:张三"
B.describe() // "姓名:李四"

只要函数被赋给另一个变量,this的指向就会变

var A = {
  name: '张三',
  describe: function () { return '姓名:'+ this.name; }
};
var name = '李四';
var f = A.describe;
f() // "姓名:李四"

上面代码中,A.describe被赋值给变量f,内部的this就会指向f运行时所在的对象(本例是顶层对象)。再看一个网页编程的例子

<input type="text" name="age" size=3 onChange="validate(this, 18, 99);">
<script>
function validate(obj, lowval, hival){
  if ((obj.value < lowval) || (obj.value > hival))
    console.log('Invalid Value!');
}
</script>

上面代码是一个文本输入框,每当用户输入一个值,就会调用onChange回调函数,验证这个值是否在指定范围。浏览器会向回调函数传入当前对象,因此this就代表传入当前对象(即文本框),然后就可以从this.value上面读到用户的输入值

总结一下,JavaScript 语言之中,一切皆对象,运行环境也是对象,所以函数都是在某个对象之中运行,this就是函数运行时所在的对象(环境)。这本来并不会让用户糊涂,但是 JavaScript 支持运行环境动态切换,也就是说,this的指向是动态的,没有办法事先确定到底指向哪个对象,这才是最让初学者感到困惑的地方

实质

JavaScript 语言之所以有 this 的设计,跟内存里面的数据结构有关系

var obj = { foo:  5 };

上面的代码将一个对象赋值给变量obj。JavaScript 引擎会先在内存里面,生成一个对象{ foo: 5 },然后把这个对象的内存地址赋值给变量obj。也就是说,变量obj是一个地址(reference)。后面如果要读取obj.foo,引擎先从obj拿到内存地址,然后再从该地址读出原始的对象,返回它的foo属性。原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来说,上面例子的foo属性,实际上是以下面的形式保存的

{
  foo: {
    [[value]]: 5
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  }
}

注意,foo属性的值保存在属性描述对象的value属性里面。这样的结构是很清晰的,问题在于属性的值可能是一个函数;这时,引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo属性的value属性

var obj = { foo: function () {} };

由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行

var f = function () {};
var obj = { f: f };
f() // 单独执行
obj.f() // obj 环境执行

JavaScript 允许在函数体内部,引用当前环境的其他变量

var f = function () {
  console.log(x);
};

上面代码中,函数体里面使用了变量x,该变量由运行环境提供。现在问题就来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境

var f = function () { console.log(this.x);}
var x = 1;
var obj = { f: f,  x: 2};
// 单独执行
f() // 1
// obj 环境执行
obj.f() // 2

上面代码中,函数f在全局环境执行,this.x指向全局环境的x;在obj环境执行,this.x指向obj.x

使用场合

this主要有以下几个使用场合

全局环境

全局环境使用this,它指的就是顶层对象window

this === window // true
function f() {
  console.log(this === window);
}
f() // true

上面代码说明,不管是不是在函数内部,只要是在全局环境下运行,this就是指顶层对象window

构造函数

构造函数中的this,指的是实例对象

var Obj = function (p) {
  this.p = p;
};
var o = new Obj('Hello World!');
o.p // "Hello World!"

上面代码定义了一个构造函数Obj。由于this指向实例对象,所以在构造函数内部定义this.p,就相当于定义实例对象有一个p属性

对象的方法

如果对象的方法里面包含this,this的指向就是方法运行时所在的对象。该方法赋值给另一个对象,就会改变this的指向。但是,这条规则很不容易把握。请看下面的代码

var obj ={
  foo: function () { console.log(this); }
};
obj.foo() // obj

上面代码中,obj.foo方法执行时,它内部的this指向obj。但是,下面这几种用法,都会改变this的指向

// 情况一
(obj.foo = obj.foo)() // window
// 情况二
(false || obj.foo)() // window
// 情况三
(1, obj.foo)() // window

上面代码中,obj.foo就是一个值。这个值真正调用的时候,运行环境已经不是obj了,而是全局环境,所以this不再指向obj。可以这样理解,JavaScript 引擎内部,obj和obj.foo储存在两个内存地址,称为地址一和地址二。obj.foo()这样调用时,是从地址一调用地址二,因此地址二的运行环境是地址一,this指向obj。但是,上面三种情况,都是直接取出地址二进行调用,这样的话,运行环境就是全局环境,因此this指向全局环境。上面三种情况等同于下面的代码

// 情况一
(obj.foo = function () { console.log(this);})()
// 等同于
(function () { console.log(this);})()
// 情况二
(false || function () { console.log(this);})()
// 情况三
(1, function () { console.log(this);})()

如果this所在的方法不在对象的第一层,这时this只是指向当前一层的对象,而不会继承更上面的层

var a = {
  p: 'Hello',
  b: {
    m: function() { console.log(this.p); }
  }
};
a.b.m() // undefined

上面代码中,a.b.m方法在a对象的第二层,该方法内部的this不是指向a,而是指向a.b,因为实际执行的是下面的代码

var b = {
  m: function() { console.log(this.p); }
};
var a = { p: 'Hello',  b: b };
(a.b).m() // 等同于 b.m()

如果要达到预期效果,只有写成下面这样

var a = {
  b: {
    m: function() { console.log(this.p); },
    p: 'Hello'
  }
};

如果这时将嵌套对象内部的方法赋值给一个变量,this依然会指向全局对象

var a = {
  b: {
    m: function() { console.log(this.p); },
    p: 'Hello'
  }
};
var hello = a.b.m;
hello() // undefined

上面代码中,m是多层对象内部的一个方法。为求简便,将其赋值给hello变量,结果调用时,this指向了顶层对象。为了避免这个问题,可以只将m所在的对象赋值给hello,这样调用时,this的指向就不会变

var hello = a.b;
hello.m() // Hello

  1. xyz

javascript 函数 LOG js 正则表达式 表达式 prototype 数组

作者

webmirror
TA的文章

相关文章