From dc44bfca80da87ab518950eee44ba6b216203e3d Mon Sep 17 00:00:00 2001 From: kpsanmao Date: Wed, 25 Aug 2021 07:53:20 +0000 Subject: [PATCH 1/2] GitBook: [master] 8 pages modified --- README.md | 2 + SUMMARY.md | 10 + ru-men-pian/README.md | 2 + ru-men-pian/dao-lun.md | 165 ++++++++ ru-men-pian/ji-ben-yu-fa.md | 731 ++++++++++++++++++++++++++++++++++++ ru-men-pian/li-shi.md | 188 ++++++++++ shu-ju-lei-xing/README.md | 2 + shu-ju-lei-xing/gai-shu.md | 115 ++++++ 8 files changed, 1215 insertions(+) create mode 100644 SUMMARY.md create mode 100644 ru-men-pian/README.md create mode 100644 ru-men-pian/dao-lun.md create mode 100644 ru-men-pian/ji-ben-yu-fa.md create mode 100644 ru-men-pian/li-shi.md create mode 100644 shu-ju-lei-xing/README.md create mode 100644 shu-ju-lei-xing/gai-shu.md diff --git a/README.md b/README.md index b3f09b2..69f9360 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +# 前言 + 本教程全面介绍 JavaScript 核心语法,覆盖了 ES5 和 DOM 规范的所有内容。 内容上从最简单的讲起,循序渐进、由浅入深,力求清晰易懂。所有章节都带有大量的代码实例,便于理解和模仿,可以用到实际项目中,即学即用。 diff --git a/SUMMARY.md b/SUMMARY.md new file mode 100644 index 0000000..46be263 --- /dev/null +++ b/SUMMARY.md @@ -0,0 +1,10 @@ +# Table of contents + +* [前言](README.md) +* [入门篇](ru-men-pian/README.md) + * [导论](ru-men-pian/dao-lun.md) + * [历史](ru-men-pian/li-shi.md) + * [基本语法](ru-men-pian/ji-ben-yu-fa.md) +* [数据类型](shu-ju-lei-xing/README.md) + * [概述](shu-ju-lei-xing/gai-shu.md) + diff --git a/ru-men-pian/README.md b/ru-men-pian/README.md new file mode 100644 index 0000000..510319f --- /dev/null +++ b/ru-men-pian/README.md @@ -0,0 +1,2 @@ +# 入门篇 + diff --git a/ru-men-pian/dao-lun.md b/ru-men-pian/dao-lun.md new file mode 100644 index 0000000..463baf0 --- /dev/null +++ b/ru-men-pian/dao-lun.md @@ -0,0 +1,165 @@ +# 导论 + +## 什么是 JavaScript 语言? + +JavaScript 是一种轻量级的脚本语言。所谓“脚本语言”(script language),指的是它不具备开发操作系统的能力,而是只用来编写控制其他大型应用程序(比如浏览器)的“脚本”。 + +JavaScript 也是一种嵌入式(embedded)语言。它本身提供的核心语法不算很多,只能用来做一些数学和逻辑运算。JavaScript 本身不提供任何与 I/O(输入/输出)相关的 API,都要靠宿主环境(host)提供,所以 JavaScript 只合适嵌入更大型的应用程序环境,去调用宿主环境提供的底层 API。 + +目前,已经嵌入 JavaScript 的宿主环境有多种,最常见的环境就是浏览器,另外还有服务器环境,也就是 Node 项目。 + +从语法角度看,JavaScript 语言是一种“对象模型”语言。各种宿主环境通过这个模型,描述自己的功能和操作接口,从而通过 JavaScript 控制这些功能。但是,JavaScript 并不是纯粹的“面向对象语言”,还支持其他编程范式(比如函数式编程)。这导致几乎任何一个问题,JavaScript 都有多种解决方法。阅读本书的过程中,你会诧异于 JavaScript 语法的灵活性。 + +JavaScript 的核心语法部分相当精简,只包括两个部分:基本的语法构造(比如操作符、控制结构、语句)和标准库(就是一系列具有各种功能的对象比如`Array`、`Date`、`Math`等)。除此之外,各种宿主环境提供额外的 API(即只能在该环境使用的接口),以便 JavaScript 调用。以浏览器为例,它提供的额外 API 可以分成三大类。 + +* 浏览器控制类:操作浏览器 +* DOM 类:操作网页的各种元素 +* Web 类:实现互联网的各种功能 + +如果宿主环境是服务器,则会提供各种操作系统的 API,比如文件操作 API、网络通信 API等等。这些你都可以在 Node 环境中找到。 + +本书主要介绍 JavaScript 核心语法和浏览器网页开发的基本知识,不涉及 Node。全书可以分成以下四大部分。 + +* 基本语法 +* 标准库 +* 浏览器 API +* DOM + +JavaScript 语言有多个版本。本书的内容主要基于 ECMAScript 5.1 版本,这是学习 JavaScript 语法的基础。ES6 和更新的语法请参考我写的[《ECMAScript 6入门》](http://es6.ruanyifeng.com/)。 + +## 为什么学习 JavaScript? + +JavaScript 语言有一些显著特点,使得它非常值得学习。它既适合作为学习编程的入门语言,也适合当作日常开发的工作语言。它是目前最有希望、前途最光明的计算机语言之一。 + +### 操控浏览器的能力 + +JavaScript 的发明目的,就是作为浏览器的内置脚本语言,为网页开发者提供操控浏览器的能力。它是目前唯一一种通用的浏览器脚本语言,所有浏览器都支持。它可以让网页呈现各种特殊效果,为用户提供良好的互动体验。 + +目前,全世界几乎所有网页都使用 JavaScript。如果不用,网站的易用性和使用效率将大打折扣,无法成为操作便利、对用户友好的网站。 + +对于一个互联网开发者来说,如果你想提供漂亮的网页、令用户满意的上网体验、各种基于浏览器的便捷功能、前后端之间紧密高效的联系,JavaScript 是必不可少的工具。 + +### 广泛的使用领域 + +近年来,JavaScript 的使用范围,慢慢超越了浏览器,正在向通用的系统语言发展。 + +**(1)浏览器的平台化** + +随着 HTML5 的出现,浏览器本身的功能越来越强,不再仅仅能浏览网页,而是越来越像一个平台,JavaScript 因此得以调用许多系统功能,比如操作本地文件、操作图片、调用摄像头和麦克风等等。这使得 JavaScript 可以完成许多以前无法想象的事情。 + +**(2)Node** + +Node 项目使得 JavaScript 可以用于开发服务器端的大型项目,网站的前后端都用 JavaScript 开发已经成为了现实。有些嵌入式平台(Raspberry Pi)能够安装 Node,于是 JavaScript 就能为这些平台开发应用程序。 + +**(3)数据库操作** + +JavaScript 甚至也可以用来操作数据库。NoSQL 数据库这个概念,本身就是在 JSON(JavaScript Object Notation)格式的基础上诞生的,大部分 NoSQL 数据库允许 JavaScript 直接操作。基于 SQL 语言的开源数据库 PostgreSQL 支持 JavaScript 作为操作语言,可以部分取代 SQL 查询语言。 + +**(4)移动平台开发** + +JavaScript 也正在成为手机应用的开发语言。一般来说,安卓平台使用 Java 语言开发,iOS 平台使用 Objective-C 或 Swift 语言开发。许多人正在努力,让 JavaScript 成为各个平台的通用开发语言。 + +PhoneGap 项目就是将 JavaScript 和 HTML5 打包在一个容器之中,使得它能同时在 iOS 和安卓上运行。Facebook 公司的 React Native 项目则是将 JavaScript 写的组件,编译成原生组件,从而使它们具备优秀的性能。 + +Mozilla 基金会的手机操作系统 Firefox OS,更是直接将 JavaScript 作为操作系统的平台语言,但是很可惜这个项目没有成功。 + +**(5)内嵌脚本语言** + +越来越多的应用程序,将 JavaScript 作为内嵌的脚本语言,比如 Adobe 公司的著名 PDF 阅读器 Acrobat、Linux 桌面环境 GNOME 3。 + +**(6)跨平台的桌面应用程序** + +Chromium OS、Windows 8 等操作系统直接支持 JavaScript 编写应用程序。Mozilla 的 Open Web Apps 项目、Google 的 [Chrome App 项目](http://developer.chrome.com/apps/about_apps)、GitHub 的 [Electron 项目](http://electron.atom.io/)、以及 [TideSDK 项目](http://tidesdk.multipart.net/docs/user-dev/generated/),都可以用来编写运行于 Windows、Mac OS 和 Android 等多个桌面平台的程序,不依赖浏览器。 + +**(7)小结** + +可以预期,JavaScript 最终将能让你只用一种语言,就开发出适应不同平台(包括桌面端、服务器端、手机端)的程序。早在2013年9月的[统计](http://adambard.com/blog/top-github-languages-for-2013-so-far/)之中,JavaScript 就是当年 GitHub 上使用量排名第一的语言。 + +著名程序员 Jeff Atwood 甚至提出了一条 [“Atwood 定律”](http://www.codinghorror.com/blog/2007/07/the-principle-of-least-power.html): + +> “所有可以用 JavaScript 编写的程序,最终都会出现 JavaScript 的版本。”\(Any application that can be written in JavaScript will eventually be written in JavaScript.\) + +### 易学性 + +相比学习其他语言,学习 JavaScript 有一些有利条件。 + +**(1)学习环境无处不在** + +只要有浏览器,就能运行 JavaScript 程序;只要有文本编辑器,就能编写 JavaScript 程序。这意味着,几乎所有电脑都原生提供 JavaScript 学习环境,不用另行安装复杂的 IDE(集成开发环境)和编译器。 + +**(2)简单性** + +相比其他脚本语言(比如 Python 或 Ruby),JavaScript 的语法相对简单一些,本身的语法特性并不是特别多。而且,那些语法中的复杂部分,也不是必需要学会。你完全可以只用简单命令,完成大部分的操作。 + +**(3)与主流语言的相似性** + +JavaScript 的语法很类似 C/C++ 和 Java,如果学过这些语言(事实上大多数学校都教),JavaScript 的入门会非常容易。 + +必须说明的是,虽然核心语法不难,但是 JavaScript 的复杂性体现在另外两个方面。 + +首先,它涉及大量的外部 API。JavaScript 要发挥作用,必须与其他组件配合,这些外部组件五花八门,数量极其庞大,几乎涉及网络应用的各个方面,掌握它们绝非易事。 + +其次,JavaScript 语言有一些设计缺陷。某些地方相当不合理,另一些地方则会出现怪异的运行结果。学习 JavaScript,很大一部分时间是用来搞清楚哪些地方有陷阱。Douglas Crockford 写过一本有名的书,名字就叫[《JavaScript: The Good Parts》](http://javascript.crockford.com/),言下之意就是这门语言不好的地方很多,必须写一本书才能讲清楚。另外一些程序员则感到,为了更合理地编写 JavaScript 程序,就不能用 JavaScript 来写,而必须发明新的语言,比如 CoffeeScript、TypeScript、Dart 这些新语言的发明目的,多多少少都有这个因素。 + +尽管如此,目前看来,JavaScript 的地位还是无法动摇。加之,语言标准的快速进化,使得 JavaScript 功能日益增强,而语法缺陷和怪异之处得到了弥补。所以,JavaScript 还是值得学习,况且它的入门真的不难。 + +### 强大的性能 + +JavaScript 的性能优势体现在以下方面。 + +**(1)灵活的语法,表达力强。** + +JavaScript 既支持类似 C 语言清晰的过程式编程,也支持灵活的函数式编程,可以用来写并发处理(concurrent)。这些语法特性已经被证明非常强大,可以用于许多场合,尤其适用异步编程。 + +JavaScript 的所有值都是对象,这为程序员提供了灵活性和便利性。因为你可以很方便地、按照需要随时创造数据结构,不用进行麻烦的预定义。 + +JavaScript 的标准还在快速进化中,并不断合理化,添加更适用的语法特性。 + +**(2)支持编译运行。** + +JavaScript 语言本身,虽然是一种解释型语言,但是在现代浏览器中,JavaScript 都是编译后运行。程序会被高度优化,运行效率接近二进制程序。而且,JavaScript 引擎正在快速发展,性能将越来越好。 + +此外,还有一种 WebAssembly 格式,它是 JavaScript 引擎的中间码格式,全部都是二进制代码。由于跳过了编译步骤,可以达到接近原生二进制代码的运行速度。各种语言(主要是 C 和 C++)通过编译成 WebAssembly,就可以在浏览器里面运行。 + +**(3)事件驱动和非阻塞式设计。** + +JavaScript 程序可以采用事件驱动(event-driven)和非阻塞式(non-blocking)设计,在服务器端适合高并发环境,普通的硬件就可以承受很大的访问量。 + +### 开放性 + +JavaScript 是一种开放的语言。它的标准 ECMA-262 是 ISO 国际标准,写得非常详尽明确;该标准的主要实现(比如 V8 和 SpiderMonkey 引擎)都是开放的,而且质量很高。这保证了这门语言不属于任何公司或个人,不存在版权和专利的问题。 + +语言标准由 TC39 委员会负责制定,该委员会的运作是透明的,所有讨论都是开放的,会议记录都会对外公布。 + +不同公司的 JavaScript 运行环境,兼容性很好,程序不做调整或只做很小的调整,就能在所有浏览器上运行。 + +### 社区支持和就业机会 + +全世界程序员都在使用 JavaScript,它有着极大的社区、广泛的文献和图书、丰富的代码资源。绝大部分你需要用到的功能,都有多个开源函数库可供选用。 + +作为项目负责人,你不难招聘到数量众多的 JavaScript 程序员;作为开发者,你也不难找到一份 JavaScript 的工作。 + +## 实验环境 + +本教程包含大量的示例代码,只要电脑安装了浏览器,就可以用来实验了。读者可以一边读一边运行示例,加深理解。 + +推荐安装 Chrome 浏览器,它的“开发者工具”(Developer Tools)里面的“控制台”(console),就是运行 JavaScript 代码的理想环境。 + +进入 Chrome 浏览器的“控制台”,有两种方法。 + +* 直接进入:按下`Option + Command + J`(Mac)或者`Ctrl + Shift + J`(Windows / Linux) +* 开发者工具进入:开发者工具的快捷键是 F12,或者`Option + Command + I`(Mac)以及`Ctrl + Shift + I`(Windows / Linux),然后选择 Console 面板 + +进入控制台以后,就可以在提示符后输入代码,然后按`Enter`键,代码就会执行。如果按`Shift + Enter`键,就是代码换行,不会触发执行。建议阅读本教程时,将代码复制到控制台进行实验。 + +作为尝试,你可以将下面的程序复制到“控制台”,按下回车后,就可以看到运行结果。 + +```javascript +function greetMe(yourName) { + console.log('Hello ' + yourName); +} + +greetMe('World') +// Hello World +``` + diff --git a/ru-men-pian/ji-ben-yu-fa.md b/ru-men-pian/ji-ben-yu-fa.md new file mode 100644 index 0000000..3f716bd --- /dev/null +++ b/ru-men-pian/ji-ben-yu-fa.md @@ -0,0 +1,731 @@ +# 基本语法 + +## 语句 + +JavaScript 程序的执行单位为行(line),也就是一行一行地执行。一般情况下,每一行就是一个语句。 + +语句(statement)是为了完成某种任务而进行的操作,比如下面就是一行赋值语句。 + +```javascript +var a = 1 + 3; +``` + +这条语句先用`var`命令,声明了变量`a`,然后将`1 + 3`的运算结果赋值给变量`a`。 + +`1 + 3`叫做表达式(expression),指一个为了得到返回值的计算式。语句和表达式的区别在于,前者主要为了进行某种操作,一般情况下不需要返回值;后者则是为了得到返回值,一定会返回一个值。凡是 JavaScript 语言中预期为值的地方,都可以使用表达式。比如,赋值语句的等号右边,预期是一个值,因此可以放置各种表达式。 + +语句以分号结尾,一个分号就表示一个语句结束。多个语句可以写在一行内。 + +```javascript +var a = 1 + 3 ; var b = 'abc'; +``` + +分号前面可以没有任何内容,JavaScript 引擎将其视为空语句。 + +```javascript +;;; +``` + +上面的代码就表示3个空语句。 + +表达式不需要分号结尾。一旦在表达式后面添加分号,则 JavaScript 引擎就将表达式视为语句,这样会产生一些没有任何意义的语句。 + +```javascript +1 + 3; +'abc'; +``` + +上面两行语句只是单纯地产生一个值,并没有任何实际的意义。 + +## 变量 + +### 概念 + +变量是对“值”的具名引用。变量就是为“值”起名,然后引用这个名字,就等同于引用这个值。变量的名字就是变量名。 + +```javascript +var a = 1; +``` + +上面的代码先声明变量`a`,然后在变量`a`与数值1之间建立引用关系,称为将数值1“赋值”给变量`a`。以后,引用变量名`a`就会得到数值1。最前面的`var`,是变量声明命令。它表示通知解释引擎,要创建一个变量`a`。 + +注意,JavaScript 的变量名区分大小写,`A`和`a`是两个不同的变量。 + +变量的声明和赋值,是分开的两个步骤,上面的代码将它们合在了一起,实际的步骤是下面这样。 + +```javascript +var a; +a = 1; +``` + +如果只是声明变量而没有赋值,则该变量的值是`undefined`。`undefined`是一个特殊的值,表示“无定义”。 + +```javascript +var a; +a // undefined +``` + +如果变量赋值的时候,忘了写`var`命令,这条语句也是有效的。 + +```javascript +var a = 1; +// 基本等同 +a = 1; +``` + +但是,不写`var`的做法,不利于表达意图,而且容易不知不觉地创建全局变量,所以建议总是使用`var`命令声明变量。 + +如果一个变量没有声明就直接使用,JavaScript 会报错,告诉你变量未定义。 + +```javascript +x +// ReferenceError: x is not defined +``` + +上面代码直接使用变量`x`,系统就报错,告诉你变量`x`没有声明。 + +可以在同一条`var`命令中声明多个变量。 + +```javascript +var a, b; +``` + +JavaScript 是一种动态类型语言,也就是说,变量的类型没有限制,变量可以随时更改类型。 + +```javascript +var a = 1; +a = 'hello'; +``` + +上面代码中,变量`a`起先被赋值为一个数值,后来又被重新赋值为一个字符串。第二次赋值的时候,因为变量`a`已经存在,所以不需要使用`var`命令。 + +如果使用`var`重新声明一个已经存在的变量,是无效的。 + +```javascript +var x = 1; +var x; +x // 1 +``` + +上面代码中,变量`x`声明了两次,第二次声明是无效的。 + +但是,如果第二次声明的时候还进行了赋值,则会覆盖掉前面的值。 + +```javascript +var x = 1; +var x = 2; + +// 等同于 + +var x = 1; +var x; +x = 2; +``` + +### 变量提升 + +JavaScript 引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升(hoisting)。 + +```javascript +console.log(a); +var a = 1; +``` + +上面代码首先使用`console.log`方法,在控制台(console)显示变量`a`的值。这时变量`a`还没有声明和赋值,所以这是一种错误的做法,但是实际上不会报错。因为存在变量提升,真正运行的是下面的代码。 + +```javascript +var a; +console.log(a); +a = 1; +``` + +最后的结果是显示`undefined`,表示变量`a`已声明,但还未赋值。 + +## 标识符 + +标识符(identifier)指的是用来识别各种值的合法名称。最常见的标识符就是变量名,以及后面要提到的函数名。JavaScript 语言的标识符对大小写敏感,所以`a`和`A`是两个不同的标识符。 + +标识符有一套命名规则,不符合规则的就是非法标识符。JavaScript 引擎遇到非法标识符,就会报错。 + +简单说,标识符命名规则如下。 + +* 第一个字符,可以是任意 Unicode 字母(包括英文字母和其他语言的字母),以及美元符号(`$`)和下划线(`_`)。 +* 第二个字符及后面的字符,除了 Unicode 字母、美元符号和下划线,还可以用数字`0-9`。 + +下面这些都是合法的标识符。 + +```javascript +arg0 +_tmp +$elem +π +``` + +下面这些则是不合法的标识符。 + +```javascript +1a // 第一个字符不能是数字 +23 // 同上 +*** // 标识符不能包含星号 +a+b // 标识符不能包含加号 +-d // 标识符不能包含减号或连词线 +``` + +中文是合法的标识符,可以用作变量名。 + +```javascript +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 +// 这是单行注释 + +/* + 这是 + 多行 + 注释 +*/ +``` + +此外,由于历史上 JavaScript 可以兼容 HTML 代码的注释,所以``也被视为合法的单行注释。 + +```javascript +x = 1; x = 3; +``` + +上面代码中,只有`x = 1`会执行,其他的部分都被注释掉了。 + +需要注意的是,`-->`只有在行首,才会被当成单行注释,否则会当作正常的运算。 + +```javascript +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)。 + +```javascript +{ + var a = 1; +} + +a // 1 +``` + +上面代码在区块内部,使用`var`命令声明并赋值了变量`a`,然后在区块外部,变量`a`依然有效,区块对于`var`命令不构成单独的作用域,与不使用区块的情况没有任何区别。在 JavaScript 语言中,单独使用区块并不常见,区块往往用来构成其他更复杂的语法结构,比如`for`、`if`、`while`、`function`等。 + +## 条件语句 + +JavaScript 提供`if`结构和`switch`结构,完成条件判断,即只有满足预设的条件,才会执行相应的语句。 + +### if 结构 + +`if`结构先判断一个表达式的布尔值,然后根据布尔值的真伪,执行不同的语句。所谓布尔值,指的是 JavaScript 的两个特殊值,`true`表示“真”,`false`表示“伪”。 + +```javascript +if (布尔值) + 语句; + +// 或者 +if (布尔值) 语句; +``` + +上面是`if`结构的基本形式。需要注意的是,“布尔值”往往由一个条件表达式产生的,必须放在圆括号中,表示对表达式求值。如果表达式的求值结果为`true`,就执行紧跟在后面的语句;如果结果为`false`,则跳过紧跟在后面的语句。 + +```javascript +if (m === 3) + m = m + 1; +``` + +上面代码表示,只有在`m`等于3时,才会将其值加上1。 + +这种写法要求条件表达式后面只能有一个语句。如果想执行多个语句,必须在`if`的条件判断之后,加上大括号,表示代码块(多个语句合并成一个语句)。 + +```javascript +if (m === 3) { + m += 1; +} +``` + +建议总是在`if`语句中使用大括号,因为这样方便插入语句。 + +注意,`if`后面的表达式之中,不要混淆赋值表达式(`=`)、严格相等运算符(`===`)和相等运算符(`==`)。尤其是赋值表达式不具有比较作用。 + +```javascript +var x = 1; +var y = 2; +if (x = y) { + console.log(x); +} +// "2" +``` + +上面代码的原意是,当`x`等于`y`的时候,才执行相关语句。但是,不小心将严格相等运算符写成赋值表达式,结果变成了将`y`赋值给变量`x`,再判断变量`x`的值(等于2)的布尔值(结果为`true`)。 + +这种错误可以正常生成一个布尔值,因而不会报错。为了避免这种情况,有些开发者习惯将常量写在运算符的左边,这样的话,一旦不小心将相等运算符写成赋值运算符,就会报错,因为常量不能被赋值。 + +```javascript +if (x = 2) { // 不报错 +if (2 = x) { // 报错 +``` + +至于为什么优先采用“严格相等运算符”(`===`),而不是“相等运算符”(`==`),请参考《运算符》章节。 + +### if...else 结构 + +`if`代码块后面,还可以跟一个`else`代码块,表示不满足条件时,所要执行的代码。 + +```javascript +if (m === 3) { + // 满足条件时,执行的语句 +} else { + // 不满足条件时,执行的语句 +} +``` + +上面代码判断变量`m`是否等于3,如果等于就执行`if`代码块,否则执行`else`代码块。 + +对同一个变量进行多次判断时,多个`if...else`语句可以连写在一起。 + +```javascript +if (m === 0) { + // ... +} else if (m === 1) { + // ... +} else if (m === 2) { + // ... +} else { + // ... +} +``` + +`else`代码块总是与离自己最近的那个`if`语句配对。 + +```javascript +var m = 1; +var n = 2; + +if (m !== 1) +if (n === 2) console.log('hello'); +else console.log('world'); +``` + +上面代码不会有任何输出,`else`代码块不会得到执行,因为它跟着的是最近的那个`if`语句,相当于下面这样。 + +```javascript +if (m !== 1) { + if (n === 2) { + console.log('hello'); + } else { + console.log('world'); + } +} +``` + +如果想让`else`代码块跟随最上面的那个`if`语句,就要改变大括号的位置。 + +```javascript +if (m !== 1) { + if (n === 2) { + console.log('hello'); + } +} else { + console.log('world'); +} +// world +``` + +### switch 结构 + +多个`if...else`连在一起使用的时候,可以转为使用更方便的`switch`结构。 + +```javascript +switch (fruit) { + case "banana": + // ... + break; + case "apple": + // ... + break; + default: + // ... +} +``` + +上面代码根据变量`fruit`的值,选择执行相应的`case`。如果所有`case`都不符合,则执行最后的`default`部分。需要注意的是,每个`case`代码块内部的`break`语句不能少,否则会接下去执行下一个`case`代码块,而不是跳出`switch`结构。 + +```javascript +var x = 1; + +switch (x) { + case 1: + console.log('x 等于1'); + case 2: + console.log('x 等于2'); + default: + console.log('x 等于其他值'); +} +// x等于1 +// x等于2 +// x等于其他值 +``` + +上面代码中,`case`代码块之中没有`break`语句,导致不会跳出`switch`结构,而会一直执行下去。正确的写法是像下面这样。 + +```javascript +switch (x) { + case 1: + console.log('x 等于1'); + break; + case 2: + console.log('x 等于2'); + break; + default: + console.log('x 等于其他值'); +} +``` + +`switch`语句部分和`case`语句部分,都可以使用表达式。 + +```javascript +switch (1 + 3) { + case 2 + 2: + f(); + break; + default: + neverHappens(); +} +``` + +上面代码的`default`部分,是永远不会执行到的。 + +需要注意的是,`switch`语句后面的表达式,与`case`语句后面的表示式比较运行结果时,采用的是严格相等运算符(`===`),而不是相等运算符(`==`),这意味着比较时不会发生类型转换。 + +```javascript +var x = 1; + +switch (x) { + case true: + console.log('x 发生类型转换'); + break; + default: + console.log('x 没有发生类型转换'); +} +// x 没有发生类型转换 +``` + +上面代码中,由于变量`x`没有发生类型转换,所以不会执行`case true`的情况。这表明,`switch`语句内部采用的是“严格相等运算符”,详细解释请参考《运算符》一节。 + +### 三元运算符 ?: + +JavaScript 还有一个三元运算符(即该运算符需要三个运算子)`?:`,也可以用于逻辑判断。 + +```javascript +(条件) ? 表达式1 : 表达式2 +``` + +上面代码中,如果“条件”为`true`,则返回“表达式1”的值,否则返回“表达式2”的值。 + +```javascript +var even = (n % 2 === 0) ? true : false; +``` + +上面代码中,如果`n`可以被2整除,则`even`等于`true`,否则等于`false`。它等同于下面的形式。 + +```javascript +var even; +if (n % 2 === 0) { + even = true; +} else { + even = false; +} +``` + +这个三元运算符可以被视为`if...else...`的简写形式,因此可以用于多种场合。 + +```javascript +var myVar; +console.log( + myVar ? + 'myVar has a value' : + 'myVar does not have a value' +) +// myVar does not have a value +``` + +上面代码利用三元运算符,输出相应的提示。 + +```javascript +var msg = '数字' + n + '是' + (n % 2 === 0 ? '偶数' : '奇数'); +``` + +上面代码利用三元运算符,在字符串之中插入不同的值。 + +## 循环语句 + +循环语句用于重复执行某个操作,它有多种形式。 + +### while 循环 + +`While`语句包括一个循环条件和一段代码块,只要条件为真,就不断循环执行代码块。 + +```javascript +while (条件) + 语句; + +// 或者 +while (条件) 语句; +``` + +`while`语句的循环条件是一个表达式,必须放在圆括号中。代码块部分,如果只有一条语句,可以省略大括号,否则就必须加上大括号。 + +```javascript +while (条件) { + 语句; +} +``` + +下面是`while`语句的一个例子。 + +```javascript +var i = 0; + +while (i < 100) { + console.log('i 当前为:' + i); + i = i + 1; +} +``` + +上面的代码将循环100次,直到`i`等于100为止。 + +下面的例子是一个无限循环,因为循环条件总是为真。 + +```javascript +while (true) { + console.log('Hello, world'); +} +``` + +### for 循环 + +`for`语句是循环命令的另一种形式,可以指定循环的起点、终点和终止条件。它的格式如下。 + +```javascript +for (初始化表达式; 条件; 递增表达式) + 语句 + +// 或者 + +for (初始化表达式; 条件; 递增表达式) { + 语句 +} +``` + +`for`语句后面的括号里面,有三个表达式。 + +* 初始化表达式(initialize):确定循环变量的初始值,只在循环开始时执行一次。 +* 条件表达式(test):每轮循环开始时,都要执行这个条件表达式,只有值为真,才继续进行循环。 +* 递增表达式(increment):每轮循环的最后一个操作,通常用来递增循环变量。 + +下面是一个例子。 + +```javascript +var x = 3; +for (var i = 0; i < x; i++) { + console.log(i); +} +// 0 +// 1 +// 2 +``` + +上面代码中,初始化表达式是`var i = 0`,即初始化一个变量`i`;测试表达式是`i < x`,即只要`i`小于`x`,就会执行循环;递增表达式是`i++`,即每次循环结束后,`i`增大1。 + +所有`for`循环,都可以改写成`while`循环。上面的例子改为`while`循环,代码如下。 + +```javascript +var x = 3; +var i = 0; + +while (i < x) { + console.log(i); + i++; +} +``` + +`for`语句的三个部分(initialize、test、increment),可以省略任何一个,也可以全部省略。 + +```javascript +for ( ; ; ){ + console.log('Hello World'); +} +``` + +上面代码省略了`for`语句表达式的三个部分,结果就导致了一个无限循环。 + +### do...while 循环 + +`do...while`循环与`while`循环类似,唯一的区别就是先运行一次循环体,然后判断循环条件。 + +```javascript +do + 语句 +while (条件); + +// 或者 +do { + 语句 +} while (条件); +``` + +不管条件是否为真,`do...while`循环至少运行一次,这是这种结构最大的特点。另外,`while`语句后面的分号注意不要省略。 + +下面是一个例子。 + +```javascript +var x = 3; +var i = 0; + +do { + console.log(i); + i++; +} while(i < x); +``` + +### break 语句和 continue 语句 + +`break`语句和`continue`语句都具有跳转作用,可以让代码不按既有的顺序执行。 + +`break`语句用于跳出代码块或循环。 + +```javascript +var i = 0; + +while(i < 100) { + console.log('i 当前为:' + i); + i++; + if (i === 10) break; +} +``` + +上面代码只会执行10次循环,一旦`i`等于10,就会跳出循环。 + +`for`循环也可以使用`break`语句跳出循环。 + +```javascript +for (var i = 0; i < 5; i++) { + console.log(i); + if (i === 3) + break; +} +// 0 +// 1 +// 2 +// 3 +``` + +上面代码执行到`i`等于3,就会跳出循环。 + +`continue`语句用于立即终止本轮循环,返回循环结构的头部,开始下一轮循环。 + +```javascript +var i = 0; + +while (i < 100){ + i++; + if (i % 2 === 0) continue; + console.log('i 当前为:' + i); +} +``` + +上面代码只有在`i`为奇数时,才会输出`i`的值。如果`i`为偶数,则直接进入下一轮循环。 + +如果存在多重循环,不带参数的`break`语句和`continue`语句都只针对最内层循环。 + +### 标签(label) + +JavaScript 语言允许,语句的前面有标签(label),相当于定位符,用于跳转到程序的任意位置,标签的格式如下。 + +```javascript +label: + 语句 +``` + +标签可以是任意的标识符,但不能是保留字,语句部分可以是任意语句。 + +标签通常与`break`语句和`continue`语句配合使用,跳出特定的循环。 + +```javascript +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`语句后面不使用标签,则只能跳出内层循环,进入下一次的外层循环。 + +标签也可以用于跳出代码块。 + +```javascript +foo: { + console.log(1); + break foo; + console.log('本行不会输出'); +} +console.log(2); +// 1 +// 2 +``` + +上面代码执行到`break foo`,就会跳出区块。 + +`continue`语句也可以与标签配合使用。 + +```javascript +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`语句后面不使用标签,则只能进入下一轮的内层循环。 + +## 参考链接 + +* Axel Rauschmayer, [A quick overview of JavaScript](http://www.2ality.com/2011/10/javascript-overview.html) + diff --git a/ru-men-pian/li-shi.md b/ru-men-pian/li-shi.md new file mode 100644 index 0000000..abad5f4 --- /dev/null +++ b/ru-men-pian/li-shi.md @@ -0,0 +1,188 @@ +# 历史 + +## 诞生 + +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天,就设计完成了这种语言的第一版。它是一个大杂烩,语法有多个来源。 + +* 基本语法:借鉴 C 语言和 Java 语言。 +* 数据结构:借鉴 Java 语言,包括将值分成原始值和对象两大类。 +* 函数的用法:借鉴 Scheme 语言和 Awk 语言,将函数当作第一等公民,并引入闭包。 +* 原型继承模型:借鉴 Self 语言(Smalltalk 的一种变种)。 +* 正则表达式:借鉴 Perl 语言。 +* 字符串和数组处理:借鉴 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 与 ECMAScript 的关系 + +1996年8月,微软模仿 JavaScript 开发了一种相近的语言,取名为JScript(JavaScript 是 Netscape 的注册商标,微软不能用),首先内置于IE 3.0。Netscape 公司面临丧失浏览器脚本语言的主导权的局面。 + +1996年11月,Netscape 公司决定将 JavaScript 提交给国际标准化组织 ECMA(European Computer Manufacturers Association),希望 JavaScript 能够成为国际标准,以此抵抗微软。ECMA 的39号技术委员会(Technical Committee 39)负责制定和审核这个标准,成员由业内的大公司派出的工程师组成,目前共25个人。该委员会定期开会,所有的邮件讨论和会议记录,都是公开的。 + +1997年7月,ECMA 组织发布262号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript。这个版本就是 ECMAScript 1.0 版。之所以不叫 JavaScript,一方面是由于商标的关系,Java 是 Sun 公司的商标,根据一份授权协议,只有 Netscape 公司可以合法地使用 JavaScript 这个名字,且 JavaScript 已经被 Netscape 公司注册为商标,另一方面也是想体现这门语言的制定者是 ECMA,不是 Netscape,这样有利于保证这门语言的开放性和中立性。因此,ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现。在日常场合,这两个词是可以互换的。 + +ECMAScript 只用来标准化 JavaScript 这种语言的基本语法结构,与部署环境相关的标准都由其他标准规定,比如 DOM 的标准就是由 W3C组织(World Wide Web Consortium)制定的。 + +ECMA-262 标准后来也被另一个国际标准化组织 ISO(International Organization for Standardization)批准,标准号是 ISO-16262。 + +## JavaScript 的版本 + +1997年7月,ECMAScript 1.0发布。 + +1998年6月,ECMAScript 2.0版发布。 + +1999年12月,ECMAScript 3.0版发布,成为 JavaScript 的通行标准,得到了广泛支持。 + +2007年10月,ECMAScript 4.0版草案发布,对3.0版做了大幅升级,预计次年8月发布正式版本。草案发布后,由于4.0版的目标过于激进,各方对于是否通过这个标准,发生了严重分歧。以 Yahoo、Microsoft、Google 为首的大公司,反对 JavaScript 的大幅升级,主张小幅改动;以 JavaScript 创造者 Brendan Eich 为首的 Mozilla 公司,则坚持当前的草案。 + +2008年7月,由于对于下一个版本应该包括哪些功能,各方分歧太大,争论过于激进,ECMA 开会决定,中止 ECMAScript 4.0 的开发(即废除了这个版本),将其中涉及现有功能改善的一小部分,发布为 ECMAScript 3.1,而将其他激进的设想扩大范围,放入以后的版本,由于会议的气氛,该版本的项目代号起名为 Harmony(和谐)。会后不久,ECMAScript 3.1 就改名为 ECMAScript 5。 + +2009年12月,ECMAScript 5.0版 正式发布。Harmony 项目则一分为二,一些较为可行的设想定名为 JavaScript.next 继续开发,后来演变成 ECMAScript 6;一些不是很成熟的设想,则被视为 JavaScript.next.next,在更远的将来再考虑推出。TC39 的总体考虑是,ECMAScript 5 与 ECMAScript 3 基本保持兼容,较大的语法修正和新功能加入,将由 JavaScript.next 完成。当时,JavaScript.next 指的是ECMAScript 6。第六版发布以后,将指 ECMAScript 7。TC39 预计,ECMAScript 5 会在2013年的年中成为 JavaScript 开发的主流标准,并在此后五年中一直保持这个位置。 + +2011年6月,ECMAScript 5.1版发布,并且成为 ISO 国际标准(ISO/IEC 16262:2011)。到了2012年底,所有主要浏览器都支持 ECMAScript 5.1版的全部功能。 + +2013年3月,ECMAScript 6 草案冻结,不再添加新功能。新的功能设想将被放到 ECMAScript 7。 + +2013年12月,ECMAScript 6 草案发布。然后是12个月的讨论期,听取各方反馈。 + +2015年6月,ECMAScript 6 正式发布,并且更名为“ECMAScript 2015”。这是因为 TC39 委员会计划,以后每年发布一个 ECMAScript 的版本,下一个版本在2016年发布,称为“ECMAScript 2016”,2017年发布“ECMAScript 2017”,以此类推。 + +## 周边大事记 + +JavaScript 伴随着互联网的发展一起发展。互联网周边技术的快速发展,刺激和推动了 JavaScript 语言的发展。下面,回顾一下 JavaScript 的周边应用发展。 + +1996年,样式表标准 CSS 第一版发布。 + +1997年,DHTML(Dynamic HTML,动态 HTML)发布,允许动态改变网页内容。这标志着 DOM 模式(Document Object Model,文档对象模型)正式应用。 + +1998年,Netscape 公司开源了浏览器,这导致了 Mozilla 项目的诞生。几个月后,美国在线(AOL)宣布并购 Netscape。 + +1999年,IE 5部署了 XMLHttpRequest 接口,允许 JavaScript 发出 HTTP 请求,为后来大行其道的 Ajax 应用创造了条件。 + +2000年,KDE 项目重写了浏览器引擎 KHTML,为后来的 WebKit 和 Blink 引擎打下基础。这一年的10月23日,KDE 2.0发布,第一次将 KHTML 浏览器包括其中。 + +2001年,微软公司时隔5年之后,发布了 IE 浏览器的下一个版本 Internet Explorer 6。这是当时最先进的浏览器,它后来统治了浏览器市场多年。 + +2001年,Douglas Crockford 提出了 JSON 格式,用于取代 XML 格式,进行服务器和网页之间的数据交换。JavaScript 可以原生支持这种格式,不需要额外部署代码。 + +2002年,Mozilla 项目发布了它的浏览器的第一版,后来起名为 Firefox。 + +2003年,苹果公司发布了 Safari 浏览器的第一版。 + +2004年,Google 公司发布了 Gmail,促成了互联网应用程序(Web Application)这个概念的诞生。由于 Gmail 是在4月1日发布的,很多人起初以为这只是一个玩笑。 + +2004年,Dojo 框架诞生,为不同浏览器提供了同一接口,并为主要功能提供了便利的调用方法。这标志着 JavaScript 编程框架的时代开始来临。 + +2004年,WHATWG 组织成立,致力于加速 HTML 语言的标准化进程。 + +2005年,苹果公司在 KHTML 引擎基础上,建立了 WebKit 引擎。 + +2005年,Ajax 方法(Asynchronous JavaScript and XML)正式诞生,Jesse James Garrett 发明了这个词汇。它开始流行的标志是,2月份发布的 Google Maps 项目大量采用该方法。它几乎成了新一代网站的标准做法,促成了 Web 2.0时代的来临。 + +2005年,Apache 基金会发布了 CouchDB 数据库。这是一个基于 JSON 格式的数据库,可以用 JavaScript 函数定义视图和索引。它在本质上有别于传统的关系型数据库,标识着 NoSQL 类型的数据库诞生。 + +2006年,jQuery 函数库诞生,作者为John Resig。jQuery 为操作网页 DOM 结构提供了非常强大易用的接口,成为了使用最广泛的函数库,并且让 JavaScript 语言的应用难度大大降低,推动了这种语言的流行。 + +2006年,微软公司发布 IE 7,标志重新开始启动浏览器的开发。 + +2006年,Google推出 Google Web Toolkit 项目(缩写为 GWT),提供 Java 编译成 JavaScript 的功能,开创了将其他语言转为 JavaScript 的先河。 + +2007年,Webkit 引擎在 iPhone 手机中得到部署。它最初基于 KDE 项目,2003年苹果公司首先采用,2005年开源。这标志着 JavaScript 语言开始能在手机中使用了,意味着有可能写出在桌面电脑和手机中都能使用的程序。 + +2007年,Douglas Crockford 发表了名为《JavaScript: The good parts》的演讲,次年由 O'Reilly 出版社出版。这标志着软件行业开始严肃对待 JavaScript 语言,对它的语法开始重新认识。 + +2008年,V8 编译器诞生。这是 Google 公司为 Chrome 浏览器而开发的,它的特点是让 JavaScript 的运行变得非常快。它提高了 JavaScript 的性能,推动了语法的改进和标准化,改变外界对 JavaScript 的不佳印象。同时,V8 是开源的,任何人想要一种快速的嵌入式脚本语言,都可以采用 V8,这拓展了 JavaScript 的应用领域。 + +2009年,Node.js 项目诞生,创始人为 Ryan Dahl,它标志着 JavaScript 可以用于服务器端编程,从此网站的前端和后端可以使用同一种语言开发。并且,Node.js 可以承受很大的并发流量,使得开发某些互联网大规模的实时应用变得容易。 + +2009年,Jeremy Ashkenas 发布了 CoffeeScript 的最初版本。CoffeeScript 可以被转换为 JavaScript 运行,但是语法要比 JavaScript 简洁。这开启了其他语言转为 JavaScript 的风潮。 + +2009年,PhoneGap 项目诞生,它将 HTML5 和 JavaScript 引入移动设备的应用程序开发,主要针对 iOS 和 Android 平台,使得 JavaScript 可以用于跨平台的应用程序开发。 + +2009,Google 发布 Chrome OS,号称是以浏览器为基础发展成的操作系统,允许直接使用 JavaScript 编写应用程序。类似的项目还有 Mozilla 的 Firefox OS。 + +2010年,三个重要的项目诞生,分别是 NPM、BackboneJS 和 RequireJS,标志着 JavaScript 进入模块化开发的时代。 + +2011年,微软公司发布 Windows 8操作系统,将 JavaScript 作为应用程序的开发语言之一,直接提供系统支持。 + +2011年,Google 发布了 Dart 语言,目的是为了结束 JavaScript 语言在浏览器中的垄断,提供更合理、更强大的语法和功能。Chromium浏览器有内置的 Dart 虚拟机,可以运行 Dart 程序,但 Dart 程序也可以被编译成 JavaScript 程序运行。 + +2011年,微软工程师[Scott Hanselman](http://www.hanselman.com/blog/JavaScriptIsAssemblyLanguageForTheWebSematicMarkupIsDeadCleanVsMachinecodedHTML.aspx)提出,JavaScript 将是互联网的汇编语言。因为它无所不在,而且正在变得越来越快。其他语言的程序可以被转成 JavaScript 语言,然后在浏览器中运行。 + +2012年,单页面应用程序框架(single-page app framework)开始崛起,AngularJS 项目和 Ember 项目都发布了1.0版本。 + +2012年,微软发布 TypeScript 语言。该语言被设计成 JavaScript 的超集,这意味着所有 JavaScript 程序,都可以不经修改地在 TypeScript 中运行。同时,TypeScript 添加了很多新的语法特性,主要目的是为了开发大型程序,然后还可以被编译成 JavaScript 运行。 + +2012年,Mozilla 基金会提出 [asm.js](http://asmjs.org/) 规格。asm.js 是 JavaScript 的一个子集,所有符合 asm.js 的程序都可以在浏览器中运行,它的特殊之处在于语法有严格限定,可以被快速编译成性能良好的机器码。这样做的目的,是为了给其他语言提供一个编译规范,使其可以被编译成高效的 JavaScript 代码。同时,Mozilla 基金会还发起了 [Emscripten](https://github.com/kripken/emscripten/wiki) 项目,目标就是提供一个跨语言的编译器,能够将 LLVM 的位代码(bitcode)转为 JavaScript 代码,在浏览器中运行。因为大部分 LLVM 位代码都是从 C / C++ 语言生成的,这意味着 C / C++ 将可以在浏览器中运行。此外,Mozilla 旗下还有 [LLJS](http://mbebenita.github.io/LLJS/) (将 JavaScript 转为 C 代码)项目和 [River Trail](https://github.com/RiverTrail/RiverTrail/wiki) (一个用于多核心处理器的 ECMAScript 扩展)项目。目前,可以被编译成 JavaScript 的[语言列表](https://github.com/jashkenas/coffee-script/wiki/List-of-languages-that-compile-to-JS),共有将近40种语言。 + +2013年,Mozilla 基金会发布手机操作系统 Firefox OS,该操作系统的整个用户界面都使用 JavaScript。 + +2013年,ECMA 正式推出 JSON 的[国际标准](http://www.ecma-international.org/publications/standards/Ecma-404.htm),这意味着 JSON 格式已经变得与 XML 格式一样重要和正式了。 + +2013年5月,Facebook 发布 UI 框架库 React,引入了新的 JSX 语法,使得 UI 层可以用组件开发,同时引入了网页应用是状态机的概念。 + +2014年,微软推出 JavaScript 的 Windows 库 WinJS,标志微软公司全面支持 JavaScript 与 Windows 操作系统的融合。 + +2014年11月,由于对 Joyent 公司垄断 Node 项目、以及该项目进展缓慢的不满,一部分核心开发者离开了 Node.js,创造了 io.js 项目,这是一个更开放、更新更频繁的 Node.js 版本,很短时间内就发布到了2.0版。三个月后,Joyent 公司宣布放弃对 Node 项目的控制,将其转交给新成立的开放性质的 Node 基金会。随后,io.js 项目宣布回归 Node,两个版本将合并。 + +2015年3月,Facebook 公司发布了 React Native 项目,将 React 框架移植到了手机端,可以用来开发手机 App。它会将 JavaScript 代码转为 iOS 平台的 Objective-C 代码,或者 Android 平台的 Java 代码,从而为 JavaScript 语言开发高性能的原生 App 打开了一条道路。 + +2015年4月,Angular 框架宣布,2.0 版将基于微软公司的TypeScript语言开发,这等于为 JavaScript 语言引入了强类型。 + +2015年5月,Node 模块管理器 NPM 超越 CPAN,标志着 JavaScript 成为世界上软件模块最多的语言。 + +2015年5月,Google 公司的 Polymer 框架发布1.0版。该项目的目标是生产环境可以使用 WebComponent 组件,如果能够达到目标,Web 开发将进入一个全新的以组件为开发基础的阶段。 + +2015年6月,ECMA 标准化组织正式批准了 ECMAScript 6 语言标准,定名为《ECMAScript 2015 标准》。JavaScript 语言正式进入了下一个阶段,成为一种企业级的、开发大规模应用的语言。这个标准从提出到批准,历时10年,而 JavaScript 语言从诞生至今也已经20年了。 + +2015年6月,Mozilla 在 asm.js 的基础上发布 WebAssembly 项目。这是一种 JavaScript 引擎的中间码格式,全部都是二进制,类似于 Java 的字节码,有利于移动设备加载 JavaScript 脚本,执行速度提高了 20+ 倍。这意味着将来的软件,会发布 JavaScript 二进制包。 + +2016年6月,《ECMAScript 2016 标准》发布。与前一年发布的版本相比,它只增加了两个较小的特性。 + +2017年6月,《ECMAScript 2017 标准》发布,正式引入了 async 函数,使得异步操作的写法出现了根本的变化。 + +2017年11月,所有主流浏览器全部支持 WebAssembly,这意味着任何语言都可以编译成 JavaScript,在浏览器运行。 + +## 参考链接 + +* Axel Rauschmayer, [The Past, Present, and Future of JavaScript](http://oreilly.com/javascript/radarreports/past-present-future-javascript.csp) +* John Dalziel, [The race for speed part 4: The future for JavaScript](http://creativejs.com/2013/06/the-race-for-speed-part-4-the-future-for-javascript/) +* Axel Rauschmayer, [Basic JavaScript for the impatient programmer](http://www.2ality.com/2013/06/basic-javascript.html) +* resin.io, [Happy 18th Birthday JavaScript! A look at an unlikely past and bright future](http://resin.io/happy-18th-birthday-javascript/) + diff --git a/shu-ju-lei-xing/README.md b/shu-ju-lei-xing/README.md new file mode 100644 index 0000000..62a00c1 --- /dev/null +++ b/shu-ju-lei-xing/README.md @@ -0,0 +1,2 @@ +# 数据类型 + diff --git a/shu-ju-lei-xing/gai-shu.md b/shu-ju-lei-xing/gai-shu.md new file mode 100644 index 0000000..9aee624 --- /dev/null +++ b/shu-ju-lei-xing/gai-shu.md @@ -0,0 +1,115 @@ +# 概述 + +## 简介 + +JavaScript 语言的每一个值,都属于某一种数据类型。JavaScript 的数据类型,共有六种。(ES6 又新增了第七种 Symbol 类型的值,本教程不涉及。) + +* 数值(number):整数和小数(比如`1`和`3.14`)。 +* 字符串(string):文本(比如`Hello World`)。 +* 布尔值(boolean):表示真伪的两个特殊值,即`true`(真)和`false`(假)。 +* `undefined`:表示“未定义”或不存在,即由于目前没有定义,所以此处暂时没有任何值。 +* `null`:表示空值,即此处的值为空。 +* 对象(object):各种值组成的集合。 + +通常,数值、字符串、布尔值这三种类型,合称为原始类型(primitive type)的值,即它们是最基本的数据类型,不能再细分了。对象则称为合成类型(complex type)的值,因为一个对象往往是多个原始类型的值的合成,可以看作是一个存放各种值的容器。至于`undefined`和`null`,一般将它们看成两个特殊值。 + +对象是最复杂的数据类型,又可以分成三个子类型。 + +* 狭义的对象(object) +* 数组(array) +* 函数(function) + +狭义的对象和数组是两种不同的数据组合方式,除非特别声明,本教程的“对象”都特指狭义的对象。函数其实是处理数据的方法,JavaScript 把它当成一种数据类型,可以赋值给变量,这为编程带来了很大的灵活性,也为 JavaScript 的“函数式编程”奠定了基础。 + +## typeof 运算符 + +JavaScript 有三种方法,可以确定一个值到底是什么类型。 + +* `typeof`运算符 +* `instanceof`运算符 +* `Object.prototype.toString`方法 + +`instanceof`运算符和`Object.prototype.toString`方法,将在后文介绍。这里介绍`typeof`运算符。 + +`typeof`运算符可以返回一个值的数据类型。 + +数值、字符串、布尔值分别返回`number`、`string`、`boolean`。 + +```javascript +typeof 123 // "number" +typeof '123' // "string" +typeof false // "boolean" +``` + +函数返回`function`。 + +```javascript +function f() {} +typeof f +// "function" +``` + +`undefined`返回`undefined`。 + +```javascript +typeof undefined +// "undefined" +``` + +利用这一点,`typeof`可以用来检查一个没有声明的变量,而不报错。 + +```javascript +v +// ReferenceError: v is not defined + +typeof v +// "undefined" +``` + +上面代码中,变量`v`没有用`var`命令声明,直接使用就会报错。但是,放在`typeof`后面,就不报错了,而是返回`undefined`。 + +实际编程中,这个特点通常用在判断语句。 + +```javascript +// 错误的写法 +if (v) { + // ... +} +// ReferenceError: v is not defined + +// 正确的写法 +if (typeof v === "undefined") { + // ... +} +``` + +对象返回`object`。 + +```javascript +typeof window // "object" +typeof {} // "object" +typeof [] // "object" +``` + +上面代码中,空数组(`[]`)的类型也是`object`,这表示在 JavaScript 内部,数组本质上只是一种特殊的对象。这里顺便提一下,`instanceof`运算符可以区分数组和对象。`instanceof`运算符的详细解释,请见《面向对象编程》一章。 + +```javascript +var o = {}; +var a = []; + +o instanceof Array // false +a instanceof Array // true +``` + +`null`返回`object`。 + +```javascript +typeof null // "object" +``` + +`null`的类型是`object`,这是由于历史原因造成的。1995年的 JavaScript 语言第一版,只设计了五种数据类型(对象、整数、浮点数、字符串和布尔值),没考虑`null`,只把它当作`object`的一种特殊值。后来`null`独立出来,作为一种单独的数据类型,为了兼容以前的代码,`typeof null`返回`object`就没法改变了。 + +## 参考链接 + +* Axel Rauschmayer, [Improving the JavaScript typeof operator](http://www.2ality.com/2011/11/improving-typeof.html) + From 2ab2f8901e27f1cb7a1aac2d780756b80e549d3a Mon Sep 17 00:00:00 2001 From: kpsanmao Date: Wed, 25 Aug 2021 08:30:58 +0000 Subject: [PATCH 2/2] GitBook: [master] 26 pages modified --- SUMMARY.md | 22 +- features/README.md | 2 + features/console.md | 493 +++++++++++++++ features/conversion.md | 428 +++++++++++++ features/error.md | 456 ++++++++++++++ features/style.md | 491 +++++++++++++++ operators/README.md | 2 + operators/arithmetic.md | 314 ++++++++++ operators/bit.md | 359 +++++++++++ operators/boolean.md | 166 +++++ operators/comparison.md | 365 +++++++++++ operators/priority.md | 217 +++++++ stdlib.md | 2 + types/README.md | 2 + types/array.md | 503 ++++++++++++++++ types/function.md | 1000 +++++++++++++++++++++++++++++++ types/general.md | 115 ++++ types/null-undefined-boolean.md | 133 ++++ types/number.md | 655 ++++++++++++++++++++ types/object.md | 502 ++++++++++++++++ types/string.md | 286 +++++++++ 21 files changed, 6511 insertions(+), 2 deletions(-) create mode 100644 features/README.md create mode 100644 features/console.md create mode 100644 features/conversion.md create mode 100644 features/error.md create mode 100644 features/style.md create mode 100644 operators/README.md create mode 100644 operators/arithmetic.md create mode 100644 operators/bit.md create mode 100644 operators/boolean.md create mode 100644 operators/comparison.md create mode 100644 operators/priority.md create mode 100644 stdlib.md create mode 100644 types/README.md create mode 100644 types/array.md create mode 100644 types/function.md create mode 100644 types/general.md create mode 100644 types/null-undefined-boolean.md create mode 100644 types/number.md create mode 100644 types/object.md create mode 100644 types/string.md diff --git a/SUMMARY.md b/SUMMARY.md index 46be263..82b7373 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -5,6 +5,24 @@ * [导论](ru-men-pian/dao-lun.md) * [历史](ru-men-pian/li-shi.md) * [基本语法](ru-men-pian/ji-ben-yu-fa.md) -* [数据类型](shu-ju-lei-xing/README.md) - * [概述](shu-ju-lei-xing/gai-shu.md) +* [数据类型](types/README.md) + * [概述](types/general.md) + * [null, undefined 和布尔值](types/null-undefined-boolean.md) + * [数值](types/number.md) + * [字符串](types/string.md) + * [对象](types/object.md) + * [函数](types/function.md) + * [数组](types/array.md) +* [运算符](operators/README.md) + * [算术运算符](operators/arithmetic.md) + * [比较运算符](operators/comparison.md) + * [布尔运算符](operators/boolean.md) + * [二进制运算符](operators/bit.md) + * [其他运算符,运算顺序](operators/priority.md) +* [语法专题](features/README.md) + * [数据类型的转换](features/conversion.md) + * [错误处理机制](features/error.md) + * [编程风格](features/style.md) + * [console 对象与控制台](features/console.md) +* [标准库](stdlib.md) diff --git a/features/README.md b/features/README.md new file mode 100644 index 0000000..543bafa --- /dev/null +++ b/features/README.md @@ -0,0 +1,2 @@ +# 语法专题 + diff --git a/features/console.md b/features/console.md new file mode 100644 index 0000000..4368ade --- /dev/null +++ b/features/console.md @@ -0,0 +1,493 @@ +# console 对象与控制台 + +## console 对象 + +`console`对象是 JavaScript 的原生对象,它有点像 Unix 系统的标准输出`stdout`和标准错误`stderr`,可以输出各种信息到控制台,并且还提供了很多有用的辅助方法。 + +`console`的常见用途有两个。 + +* 调试程序,显示网页代码运行时的错误信息。 +* 提供了一个命令行接口,用来与网页代码互动。 + +`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 对象的静态方法 + +`console`对象提供的各种静态方法,用来与控制台窗口互动。 + +### console.log\(\),console.info\(\),console.debug\(\) + +`console.log`方法用于在控制台输出信息。它可以接受一个或多个参数,将它们连接起来输出。 + +```javascript +console.log('Hello World') +// Hello World +console.log('a', 'b', 'c') +// a b c +``` + +`console.log`方法会自动在每次输出的结尾,添加换行符。 + +```javascript +console.log(1); +console.log(2); +console.log(3); +// 1 +// 2 +// 3 +``` + +如果第一个参数是格式字符串(使用了格式占位符),`console.log`方法将依次用后面的参数替换占位符,然后再进行输出。 + +```javascript +console.log(' %s + %s = %s', 1, 1, 2) +// 1 + 1 = 2 +``` + +上面代码中,`console.log`方法的第一个参数有三个占位符(`%s`),第二、三、四个参数会在显示时,依次替换掉这个三个占位符。 + +`console.log`方法支持以下占位符,不同类型的数据必须使用对应的占位符。 + +* `%s` 字符串 +* `%d` 整数 +* `%i` 整数 +* `%f` 浮点数 +* `%o` 对象的链接 +* `%c` CSS 格式字符串 + +```javascript +var number = 11 * 9; +var color = 'red'; + +console.log('%d %s balloons', number, color); +// 99 red balloons +``` + +上面代码中,第二个参数是数值,对应的占位符是`%d`,第三个参数是字符串,对应的占位符是`%s`。 + +使用`%c`占位符时,对应的参数必须是 CSS 代码,用来对输出内容进行 CSS 渲染。 + +```javascript +console.log( + '%cThis text is styled!', + 'color: red; background: yellow; font-size: 24px;' +) +``` + +上面代码运行后,输出的内容将显示为黄底红字。 + +`console.log`方法的两种参数格式,可以结合在一起使用。 + +```javascript +console.log(' %s + %s ', 1, 1, '= 2') +// 1 + 1 = 2 +``` + +如果参数是一个对象,`console.log`会显示该对象的值。 + +```javascript +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`方法。 + +```javascript +['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`方法输出信息时,在最前面加一个红色的叉,表示出错。同时,还会高亮显示输出文字和错误发生的堆栈。其他方面都一样。 + +```javascript +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) +``` + +可以这样理解,`log`方法是写入标准输出(`stdout`),`warn`方法和`error`方法是写入标准错误(`stderr`)。 + +### console.table\(\) + +对于某些复合类型的数据,`console.table`方法可以将其转为表格显示。 + +```javascript +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" | + +下面是显示表格内容的例子。 + +```javascript +var languages = { + csharp: { name: "C#", paradigm: "object-oriented" }, + fsharp: { name: "F#", paradigm: "functional" } +}; + +console.table(languages); +``` + +上面代码的`language`,转为表格显示如下。 + +| \(index\) | name | paradigm | +| :--- | :--- | :--- | +| csharp | "C\#" | "object-oriented" | +| fsharp | "F\#" | "functional" | + +### console.count\(\) + +`count`方法用于计数,输出它被调用了多少次。 + +```javascript +function greet(user) { + console.count(); + return 'hi ' + user; +} + +greet('bob') +// : 1 +// "hi bob" + +greet('alice') +// : 2 +// "hi alice" + +greet('bob') +// : 3 +// "hi bob" +``` + +上面代码每次调用`greet`函数,内部的`console.count`方法就输出执行次数。 + +该方法可以接受一个字符串作为参数,作为标签,对执行次数进行分类。 + +```javascript +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),并以易于阅读和打印的格式显示。 + +```javascript +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 对象的所有属性。 + +```javascript +console.dir(document.body) +``` + +Node 环境之中,还可以指定以代码高亮的形式输出。 + +```javascript +console.dir(obj, {colors: true}) +``` + +`dirxml`方法主要用于以目录树的形式,显示 DOM 节点。 + +```javascript +console.dirxml(document.body) +``` + +如果参数不是 DOM 节点,而是普通的 JavaScript 对象,`console.dirxml`等同于`console.dir`。 + +```javascript +console.dirxml([1, 2, 3]) +// 等同于 +console.dir([1, 2, 3]) +``` + +### console.assert\(\) + +`console.assert`方法主要用于程序运行过程中,进行条件判断,如果不满足条件,就显示一个错误,但不会中断程序执行。这样就相当于提示用户,内部状态不正确。 + +它接受两个参数,第一个参数是表达式,第二个参数是字符串。只有当第一个参数为`false`,才会提示有错误,在控制台输出第二个参数,否则不会有任何结果。 + +```javascript +console.assert(false, '判断条件不成立') +// Assertion failed: 判断条件不成立 + +// 相当于 +try { + if (!false) { + throw new Error('判断条件不成立'); + } +} catch(e) { + console.error(e); +} +``` + +下面是一个例子,判断子节点的个数是否大于等于500。 + +```javascript +console.assert(list.childNodes.length < 500, '节点个数大于等于500') +``` + +上面代码中,如果符合条件的节点小于500个,不会有任何输出;只有大于等于500时,才会在控制台提示错误,并且显示指定文本。 + +### console.time\(\),console.timeEnd\(\) + +这两个方法用于计时,可以算出一个操作所花费的准确时间。 + +```javascript +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`这两个方法用于将显示的信息分组。它只在输出大量信息时有用,分在一组的信息,可以用鼠标折叠/展开。 + +```javascript +console.group('一级分组'); +console.log('一级分组的内容'); + +console.group('二级分组'); +console.log('二级分组的内容'); + +console.groupEnd(); // 二级分组结束 +console.groupEnd(); // 一级分组结束 +``` + +上面代码会将“二级分组”显示在“一级分组”内部,并且“一级分组”和“二级分组”前面都有一个折叠符号,可以用来折叠本级的内容。 + +`console.groupCollapsed`方法与`console.group`方法很类似,唯一的区别是该组的内容,在第一次显示时是收起的(collapsed),而不是展开的。 + +```javascript +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`方法显示当前执行的代码在堆栈中的调用路径。 + +```javascript +console.trace() +// console.trace() +// (anonymous function) +// InjectedScript._evaluateOn +// InjectedScript._evaluateAndWrap +// InjectedScript.evaluate +``` + +`console.clear`方法用于清除当前控制台的所有输出,将光标回置到第一行。如果用户选中了控制台的“Preserve log”选项,`console.clear`方法将不起作用。 + +## 控制台命令行 API + +浏览器控制台中,除了使用`console`对象,还可以使用一些控制台自带的命令行方法。 + +(1)`$_` + +`$_`属性返回上一个表达式的值。 + +```javascript +2 + 2 +// 4 +$_ +// 4 +``` + +(2)`$0` - `$4` + +控制台保存了最近5个在 Elements 面板选中的 DOM 元素,`$0`代表倒数第一个(最近一个),`$1`代表倒数第二个,以此类推直到`$4`。 + +(3)`$(selector)` + +`$(selector)`返回第一个匹配的元素,等同于`document.querySelector()`。注意,如果页面脚本对`$`有定义,则会覆盖原始的定义。比如,页面里面有 jQuery,控制台执行`$(selector)`就会采用 jQuery 的实现,返回一个数组。 + +(4)`$$(selector)` + +`$$(selector)`返回选中的 DOM 对象,等同于`document.querySelectorAll`。 + +(5)`$x(path)` + +`$x(path)`方法返回一个数组,包含匹配特定 XPath 表达式的所有 DOM 元素。 + +```javascript +$x("//p[a]") +``` + +上面代码返回所有包含`a`元素的`p`元素。 + +(6)`inspect(object)` + +`inspect(object)`方法打开相关面板,并选中相应的元素,显示它的细节。DOM 元素在`Elements`面板中显示,比如`inspect(document)`会在 Elements 面板显示`document`元素。JavaScript 对象在控制台面板`Profiles`面板中显示,比如`inspect(window)`。 + +(7)`getEventListeners(object)` + +`getEventListeners(object)`方法返回一个对象,该对象的成员为`object`登记了回调函数的各种事件(比如`click`或`keydown`),每个事件对应一个数组,数组的成员为该事件的回调函数。 + +(8)`keys(object)`,`values(object)` + +`keys(object)`方法返回一个数组,包含`object`的所有键名。 + +`values(object)`方法返回一个数组,包含`object`的所有键值。 + +```javascript +var o = {'p1': 'a', 'p2': 'b'}; + +keys(o) +// ["p1", "p2"] +values(o) +// ["a", "b"] +``` + +(9)`monitorEvents(object[, events]) ,unmonitorEvents(object[, events])` + +`monitorEvents(object[, events])`方法监听特定对象上发生的特定事件。事件发生时,会返回一个`Event`对象,包含该事件的相关信息。`unmonitorEvents`方法用于停止监听。 + +```javascript +monitorEvents(window, "resize"); +monitorEvents(window, ["resize", "scroll"]) +``` + +上面代码分别表示单个事件和多个事件的监听方法。 + +```javascript +monitorEvents($0, 'mouse'); +unmonitorEvents($0, 'mousemove'); +``` + +上面代码表示如何停止监听。 + +`monitorEvents`允许监听同一大类的事件。所有事件可以分成四个大类。 + +* mouse:"mousedown", "mouseup", "click", "dblclick", "mousemove", "mouseover", "mouseout", "mousewheel" +* key:"keydown", "keyup", "keypress", "textInput" +* touch:"touchstart", "touchmove", "touchend", "touchcancel" +* control:"resize", "scroll", "zoom", "focus", "blur", "select", "change", "submit", "reset" + +```javascript +monitorEvents($("#msg"), "key"); +``` + +上面代码表示监听所有`key`大类的事件。 + +(10)其他方法 + +命令行 API 还提供以下方法。 + +* `clear()`:清除控制台的历史。 +* `copy(object)`:复制特定 DOM 元素到剪贴板。 +* `dir(object)`:显示特定对象的所有属性,是`console.dir`方法的别名。 +* `dirxml(object)`:显示特定对象的 XML 形式,是`console.dirxml`方法的别名。 + +## debugger 语句 + +`debugger`语句主要用于除错,作用是设置断点。如果有正在运行的除错工具,程序运行到`debugger`语句时会自动停下。如果没有除错工具,`debugger`语句不会产生任何结果,JavaScript 引擎自动跳过这一句。 + +Chrome 浏览器中,当代码运行到`debugger`语句时,就会暂停运行,自动打开脚本源码界面。 + +```javascript +for(var i = 0; i < 5; i++){ + console.log(i); + if (i === 2) debugger; +} +``` + +上面代码打印出0,1,2以后,就会暂停,自动打开源码界面,等待进一步处理。 + +## 参考链接 + +* Chrome Developer Tools, [Using the Console](https://developers.google.com/chrome-developer-tools/docs/console) +* Matt West, [Mastering The Developer Tools Console](http://blog.teamtreehouse.com/mastering-developer-tools-console) +* Firebug Wiki, [Console API](https://getfirebug.com/wiki/index.php/Console_API) +* Axel Rauschmayer, [The JavaScript console API](http://www.2ality.com/2013/10/console-api.html) +* Marius Schulz, [Advanced JavaScript Debugging with console.table\(\)](http://blog.mariusschulz.com/2013/11/13/advanced-javascript-debugging-with-consoletable) +* Google Developer, [Command Line API Reference](https://developers.google.com/chrome-developer-tools/docs/commandline-api) + diff --git a/features/conversion.md b/features/conversion.md new file mode 100644 index 0000000..9f50062 --- /dev/null +++ b/features/conversion.md @@ -0,0 +1,428 @@ +# 数据类型的转换 + +## 概述 + +JavaScript 是一种动态类型语言,变量没有类型限制,可以随时赋予任意值。 + +```javascript +var x = y ? 1 : 'a'; +``` + +上面代码中,变量`x`到底是数值还是字符串,取决于另一个变量`y`的值。`y`为`true`时,`x`是一个数值;`y`为`false`时,`x`是一个字符串。这意味着,`x`的类型没法在编译阶段就知道,必须等到运行时才能知道。 + +虽然变量的数据类型是不确定的,但是各种运算符对数据类型是有要求的。如果运算符发现,运算子的类型与预期不符,就会自动转换类型。比如,减法运算符预期左右两侧的运算子应该是数值,如果不是,就会自动将它们转为数值。 + +```javascript +'4' - '3' // 1 +``` + +上面代码中,虽然是两个字符串相减,但是依然会得到结果数值`1`,原因就在于 JavaScript 将运算子自动转为了数值。 + +本章讲解数据类型自动转换的规则。在此之前,先讲解如何手动强制转换数据类型。 + +## 强制转换 + +强制转换主要指使用`Number()`、`String()`和`Boolean()`三个函数,手动将各种类型的值,分别转换成数字、字符串或者布尔值。 + +### Number\(\) + +使用`Number`函数,可以将任意类型的值转化成数值。 + +下面分成两种情况讨论,一种是参数是原始类型的值,另一种是参数是对象。 + +**(1)原始类型值** + +原始类型值的转换规则如下。 + +```javascript +// 数值:转换后还是原来的值 +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`。 + +```javascript +parseInt('42 cats') // 42 +Number('42 cats') // NaN +``` + +上面代码中,`parseInt`逐个解析字符,而`Number`函数整体转换字符串的类型。 + +另外,`parseInt`和`Number`函数都会自动过滤一个字符串前导和后缀的空格。 + +```javascript +parseInt('\t\v\r12.34\n') // 12 +Number('\t\v\r12.34\n') // 12.34 +``` + +**(2)对象** + +简单的规则是,`Number`方法的参数是对象时,将返回`NaN`,除非是包含单个数值的数组。 + +```javascript +Number({a: 1}) // NaN +Number([1, 2, 3]) // NaN +Number([5]) // 5 +``` + +之所以会这样,是因为`Number`背后的转换规则比较复杂。 + +第一步,调用对象自身的`valueOf`方法。如果返回原始类型的值,则直接对该值使用`Number`函数,不再进行后续步骤。 + +第二步,如果`valueOf`方法返回的还是对象,则改为调用对象自身的`toString`方法。如果`toString`方法返回原始类型的值,则对该值使用`Number`函数,不再进行后续步骤。 + +第三步,如果`toString`方法返回的是对象,就报错。 + +请看下面的例子。 + +```javascript +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]`)。所以,会有下面的结果。 + +```javascript +Number({}) // NaN +``` + +如果`toString`方法返回的不是原始类型的值,结果就会报错。 + +```javascript +var obj = { + valueOf: function () { + return {}; + }, + toString: function () { + return {}; + } +}; + +Number(obj) +// TypeError: Cannot convert object to primitive value +``` + +上面代码的`valueOf`和`toString`方法,返回的都是对象,所以转成数值时会报错。 + +从上例还可以看到,`valueOf`和`toString`方法,都是可以自定义的。 + +```javascript +Number({ + valueOf: function () { + return 2; + } +}) +// 2 + +Number({ + toString: function () { + return 3; + } +}) +// 3 + +Number({ + valueOf: function () { + return 2; + }, + toString: function () { + return 3; + } +}) +// 2 +``` + +上面代码对三个对象使用`Number`函数。第一个对象返回`valueOf`方法的值,第二个对象返回`toString`方法的值,第三个对象表示`valueOf`方法先于`toString`方法执行。 + +### String\(\) + +`String`函数可以将任意类型的值转化成字符串,转换规则如下。 + +**(1)原始类型值** + +* **数值**:转为相应的字符串。 +* **字符串**:转换后还是原来的值。 +* **布尔值**:`true`转为字符串`"true"`,`false`转为字符串`"false"`。 +* **undefined**:转为字符串`"undefined"`。 +* **null**:转为字符串`"null"`。 + +```javascript +String(123) // "123" +String('abc') // "abc" +String(true) // "true" +String(undefined) // "undefined" +String(null) // "null" +``` + +**(2)对象** + +`String`方法的参数如果是对象,返回一个类型字符串;如果是数组,返回该数组的字符串形式。 + +```javascript +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`方法返回的是对象,就报错。 + +下面是一个例子。 + +```javascript +String({a: 1}) +// "[object Object]" + +// 等同于 +String({a: 1}.toString()) +// "[object Object]" +``` + +上面代码先调用对象的`toString`方法,发现返回的是字符串`[object Object]`,就不再调用`valueOf`方法了。 + +如果`toString`法和`valueOf`方法,返回的都是对象,就会报错。 + +```javascript +var obj = { + valueOf: function () { + return {}; + }, + toString: function () { + return {}; + } +}; + +String(obj) +// TypeError: Cannot convert object to primitive value +``` + +下面是通过自定义`toString`方法,改变返回值的例子。 + +```javascript +String({ + toString: function () { + return 3; + } +}) +// "3" + +String({ + valueOf: function () { + return 2; + } +}) +// "[object Object]" + +String({ + valueOf: function () { + return 2; + }, + toString: function () { + return 3; + } +}) +// "3" +``` + +上面代码对三个对象使用`String`函数。第一个对象返回`toString`方法的值(数值3),第二个对象返回的还是`toString`方法的值(`[object Object]`),第三个对象表示`toString`方法先于`valueOf`方法执行。 + +### Boolean\(\) + +`Boolean()`函数可以将任意类型的值转为布尔值。 + +它的转换规则相对简单:除了以下五个值的转换结果为`false`,其他的值全部为`true`。 + +* `undefined` +* `null` +* `0`(包含`-0`和`+0`) +* `NaN` +* `''`(空字符串) + +```javascript +Boolean(undefined) // false +Boolean(null) // false +Boolean(0) // false +Boolean(NaN) // false +Boolean('') // false +``` + +当然,`true`和`false`这两个布尔值不会发生变化。 + +```javascript +Boolean(true) // true +Boolean(false) // false +``` + +注意,所有对象(包括空对象)的转换结果都是`true`,甚至连`false`对应的布尔对象`new Boolean(false)`也是`true`(详见《原始类型值的包装对象》一章)。 + +```javascript +Boolean({}) // true +Boolean([]) // true +Boolean(new Boolean(false)) // true +``` + +所有对象的布尔值都是`true`,这是因为 JavaScript 语言设计的时候,出于性能的考虑,如果对象需要计算才能得到布尔值,对于`obj1 && obj2`这样的场景,可能会需要较多的计算。为了保证性能,就统一规定,对象的布尔值为`true`。 + +## 自动转换 + +下面介绍自动转换,它是以强制转换为基础的。 + +遇到以下三种情况时,JavaScript 会自动转换数据类型,即转换是自动完成的,用户不可见。 + +第一种情况,不同类型的数据互相运算。 + +```javascript +123 + 'abc' // "123abc" +``` + +第二种情况,对非布尔值类型的数据求布尔值。 + +```javascript +if ('abc') { + console.log('hello') +} // "hello" +``` + +第三种情况,对非数值类型的值使用一元运算符(即`+`和`-`)。 + +```javascript ++ {foo: 'bar'} // NaN +- [1, 2, 3] // NaN +``` + +自动转换的规则是这样的:预期什么类型的值,就调用该类型的转换函数。比如,某个位置预期为字符串,就调用`String()`函数进行转换。如果该位置既可以是字符串,也可能是数值,那么默认转为数值。 + +由于自动转换具有不确定性,而且不易除错,建议在预期为布尔值、数值、字符串的地方,全部使用`Boolean()`、`Number()`和`String()`函数进行显式转换。 + +### 自动转换为布尔值 + +JavaScript 遇到预期为布尔值的地方(比如`if`语句的条件部分),就会将非布尔值的参数自动转换为布尔值。系统内部会自动调用`Boolean()`函数。 + +因此除了以下五个值,其他都是自动转为`true`。 + +* `undefined` +* `null` +* `+0`或`-0` +* `NaN` +* `''`(空字符串) + +下面这个例子中,条件部分的每个值都相当于`false`,使用否定运算符后,就变成了`true`。 + +```javascript +if ( !undefined + && !null + && !0 + && !NaN + && !'' +) { + console.log('true'); +} // true +``` + +下面两种写法,有时也用于将一个表达式转为布尔值。它们内部调用的也是`Boolean()`函数。 + +```javascript +// 写法一 +expression ? true : false + +// 写法二 +!! expression +``` + +### 自动转换为字符串 + +JavaScript 遇到预期为字符串的地方,就会将非字符串的值自动转为字符串。具体规则是,先将复合类型的值转为原始类型的值,再将原始类型的值转为字符串。 + +字符串的自动转换,主要发生在字符串的加法运算时。当一个值为字符串,另一个值为非字符串,则后者转为字符串。 + +```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 +var obj = { + width: '100' +}; + +obj.width + 20 // "10020" +``` + +上面代码中,开发者可能期望返回`120`,但是由于自动转换,实际上返回了一个字符`10020`。 + +### 自动转换为数值 + +JavaScript 遇到预期为数值的地方,就会将参数值自动转换为数值。系统内部会自动调用`Number()`函数。 + +除了加法运算符(`+`)有可能把运算子转为字符串,其他运算符都会把运算子自动转成数值。 + +```javascript +'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`。 + +一元运算符也会把运算子转成数值。 + +```javascript ++'abc' // NaN +-'abc' // NaN ++true // 1 +-false // 0 +``` + +## 参考链接 + +* Axel Rauschmayer, [What is {} + {} in JavaScript?](http://www.2ality.com/2012/01/object-plus-object.html) +* Axel Rauschmayer, [JavaScript quirk 1: implicit conversion of values](http://www.2ality.com/2013/04/quirk-implicit-conversion.html) +* Benjie Gillam, [Quantum JavaScript?](http://www.benjiegillam.com/2013/06/quantum-javascript/) + diff --git a/features/error.md b/features/error.md new file mode 100644 index 0000000..2f0cd72 --- /dev/null +++ b/features/error.md @@ -0,0 +1,456 @@ +# 错误处理机制 + +## Error 实例对象 + +JavaScript 解析或运行时,一旦发生错误,引擎就会抛出一个错误对象。JavaScript 原生提供`Error`构造函数,所有抛出的错误都是这个构造函数的实例。 + +```javascript +var err = new Error('出错了'); +err.message // "出错了" +``` + +上面代码中,我们调用`Error()`构造函数,生成一个实例对象`err`。`Error()`构造函数接受一个参数,表示错误提示,可以从实例的`message`属性读到这个参数。抛出`Error`实例对象以后,整个程序就中断在发生错误的地方,不再往下执行。 + +JavaScript 语言标准只提到,`Error`实例对象必须有`message`属性,表示出错时的提示信息,没有提到其他属性。大多数 JavaScript 引擎,对`Error`实例还提供`name`和`stack`属性,分别表示错误的名称和错误的堆栈,但它们是非标准的,不是每种实现都有。 + +* **message**:错误提示信息 +* **name**:错误名称(非标准属性) +* **stack**:错误的堆栈(非标准属性) + +使用`name`和`message`这两个属性,可以对发生什么错误有一个大概的了解。 + +```javascript +if (error.name) { + console.log(error.name + ': ' + error.message); +} +``` + +`stack`属性用来查看错误发生时的堆栈。 + +```javascript +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`对象是解析代码时发生的语法错误。 + +```javascript +// 变量名错误 +var 1a; +// Uncaught SyntaxError: Invalid or unexpected token + +// 缺少括号 +console.log 'hello'); +// Uncaught SyntaxError: Unexpected string +``` + +上面代码的错误,都是在语法解析阶段就可以发现,所以会抛出`SyntaxError`。第一个错误提示是“token 非法”,第二个错误提示是“字符串不符合要求”。 + +### ReferenceError 对象 + +`ReferenceError`对象是引用一个不存在的变量时发生的错误。 + +```javascript +// 使用一个不存在的变量 +unknownVariable +// Uncaught ReferenceError: unknownVariable is not defined +``` + +另一种触发场景是,将一个值分配给无法分配的对象,比如对函数的运行结果赋值。 + +```javascript +// 等号左侧不是变量 +console.log() = 1 +// Uncaught ReferenceError: Invalid left-hand side in assignment +``` + +上面代码对函数`console.log`的运行结果赋值,结果引发了`ReferenceError`错误。 + +### RangeError 对象 + +`RangeError`对象是一个值超出有效范围时发生的错误。主要有几种情况,一是数组长度为负数,二是`Number`对象的方法参数超出范围,以及函数堆栈超过最大值。 + +```javascript +// 数组长度不得为负数 +new Array(-1) +// Uncaught RangeError: Invalid array length +``` + +### TypeError 对象 + +`TypeError`对象是变量或参数不是预期类型时发生的错误。比如,对字符串、布尔值、数值等原始类型的值使用`new`命令,就会抛出这种错误,因为`new`命令的参数应该是一个构造函数。 + +```javascript +new 123 +// Uncaught TypeError: 123 is not a constructor + +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()`这六个函数。 + +```javascript +decodeURI('%2') +// URIError: URI malformed +``` + +### EvalError 对象 + +`eval`函数没有被正确执行时,会抛出`EvalError`错误。该错误类型已经不再使用了,只是为了保证与以前代码兼容,才继续保留。 + +### 总结 + +以上这6种派生错误,连同原始的`Error`对象,都是构造函数。开发者可以使用它们,手动生成错误对象的实例。这些构造函数都接受一个参数,代表错误提示信息(message)。 + +```javascript +var err1 = new Error('出错了!'); +var err2 = new RangeError('出错了,变量超出有效范围!'); +var err3 = new TypeError('出错了,变量类型无效!'); + +err1.message // "出错了!" +err2.message // "出错了,变量超出有效范围!" +err3.message // "出错了,变量类型无效!" +``` + +## 自定义错误 + +除了 JavaScript 原生提供的七种错误对象,还可以定义自己的错误对象。 + +```javascript +function UserError(message) { + this.message = message || '默认信息'; + this.name = 'UserError'; +} + +UserError.prototype = new Error(); +UserError.prototype.constructor = UserError; +``` + +上面代码自定义一个错误对象`UserError`,让它继承`Error`对象。然后,就可以生成这种自定义类型的错误了。 + +```javascript +new UserError('这是自定义的错误!'); +``` + +## throw 语句 + +`throw`语句的作用是手动中断程序执行,抛出一个错误。 + +```javascript +var x = -1; + +if (x <= 0) { + throw new Error('x 必须为正数'); +} +// Uncaught Error: x 必须为正数 +``` + +上面代码中,如果变量`x`小于等于`0`,就手动抛出一个错误,告诉用户`x`的值不正确,整个程序就会在这里中断执行。可以看到,`throw`抛出的错误就是它的参数,这里是一个`Error`对象的实例。 + +`throw`也可以抛出自定义错误。 + +```javascript +function UserError(message) { + this.message = message || '默认信息'; + this.name = 'UserError'; +} + +throw new UserError('出错了!'); +// Uncaught UserError {message: "出错了!", name: "UserError"} +``` + +上面代码中,`throw`抛出的是一个`UserError`实例。 + +实际上,`throw`可以抛出任何类型的值。也就是说,它的参数可以是任何值。 + +```javascript +// 抛出一个字符串 +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`结构,允许对错误进行处理,选择是否往下执行。 + +```javascript +try { + throw new Error('出错了!'); +} catch (e) { + console.log(e.name + ": " + e.message); + console.log(e.stack); +} +// Error: 出错了! +// at :3:9 +// ... +``` + +上面代码中,`try`代码块抛出错误(上例用的是`throw`语句),JavaScript 引擎就立即把代码的执行,转到`catch`代码块,或者说错误被`catch`代码块捕获了。`catch`接受一个参数,表示`try`代码块抛出的值。 + +如果你不确定某些代码是否会报错,就可以把它们放在`try...catch`代码块之中,便于进一步对错误进行处理。 + +```javascript +try { + f(); +} catch(e) { + // 处理错误 +} +``` + +上面代码中,如果函数`f`执行报错,就会进行`catch`代码块,接着对错误进行处理。 + +`catch`代码块捕获错误之后,程序不会中断,会按照正常流程继续执行下去。 + +```javascript +try { + throw "出错了"; +} catch (e) { + console.log(111); +} +console.log(222); +// 111 +// 222 +``` + +上面代码中,`try`代码块抛出的错误,被`catch`代码块捕获后,程序会继续向下执行。 + +`catch`代码块之中,还可以再抛出错误,甚至使用嵌套的`try...catch`结构。 + +```javascript +var n = 100; + +try { + throw n; +} catch (e) { + if (e <= 50) { + // ... + } else { + throw e; + } +} +// Uncaught 100 +``` + +上面代码中,`catch`代码之中又抛出了一个错误。 + +为了捕捉不同类型的错误,`catch`代码块之中可以加入判断语句。 + +```javascript +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); + } + // ... +} +``` + +上面代码中,`catch`捕获错误之后,会判断错误类型(`EvalError`还是`RangeError`),进行不同的处理。 + +## finally 代码块 + +`try...catch`结构允许在最后添加一个`finally`代码块,表示不管是否出现错误,都必需在最后运行的语句。 + +```javascript +function cleansUp() { + try { + throw new Error('出错了……'); + console.log('此行不会执行'); + } finally { + console.log('完成清理工作'); + } +} + +cleansUp() +// 完成清理工作 +// Uncaught Error: 出错了…… +// at cleansUp (:3:11) +// at :10:1 +``` + +上面代码中,由于没有`catch`语句块,一旦发生错误,代码就会中断执行。中断执行之前,会先执行`finally`代码块,然后再向用户提示报错信息。 + +```javascript +function idle(x) { + try { + console.log(x); + return 'result'; + } finally { + console.log('FINALLY'); + } +} + +idle('hello') +// hello +// FINALLY +``` + +上面代码中,`try`代码块没有发生错误,而且里面还包括`return`语句,但是`finally`代码块依然会执行。而且,这个函数的返回值还是`result`。 + +下面的例子说明,`return`语句的执行是排在`finally`代码之前,只是等`finally`代码执行完毕后才返回。 + +```javascript +var count = 0; +function countUp() { + try { + return count; + } finally { + count++; + } +} + +countUp() +// 0 +count +// 1 +``` + +上面代码说明,`return`语句里面的`count`的值,是在`finally`代码块运行之前就获取了。 + +下面是`finally`代码块用法的典型场景。 + +```javascript +openFile(); + +try { + writeFile(Data); +} catch(e) { + handleError(e); +} finally { + closeFile(); +} +``` + +上面代码首先打开一个文件,然后在`try`代码块中写入文件,如果没有发生错误,则运行`finally`代码块关闭文件;一旦发生错误,则先使用`catch`代码块处理错误,再使用`finally`代码块关闭文件。 + +下面的例子充分反映了`try...catch...finally`这三者之间的执行顺序。 + +```javascript +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`语句。 + +```javascript +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`代码块。 + +```javascript +try { + try { + consle.log('Hello world!'); // 报错 + } + finally { + console.log('Finally'); + } + console.log('Will I run?'); +} catch(error) { + console.error(error.message); +} +// Finally +// consle is not defined +``` + +上面代码中,`try`里面还有一个`try`。内层的`try`报错(`console`拼错了),这时会执行内层的`finally`代码块,然后抛出错误,被外层的`catch`捕获。 + +## 参考连接 + +* Jani Hartikainen, [JavaScript Errors and How to Fix Them](http://davidwalsh.name/fix-javascript-errors) + diff --git a/features/style.md b/features/style.md new file mode 100644 index 0000000..1a9be53 --- /dev/null +++ b/features/style.md @@ -0,0 +1,491 @@ +# 编程风格 + +## 概述 + +“编程风格”(programming style)指的是编写代码的样式规则。不同的程序员,往往有不同的编程风格。 + +有人说,编译器的规范叫做“语法规则”(grammar),这是程序员必须遵守的;而编译器忽略的部分,就叫“编程风格”(programming style),这是程序员可以自由选择的。这种说法不完全正确,程序员固然可以自由选择编程风格,但是好的编程风格有助于写出质量更高、错误更少、更易于维护的程序。 + +所以,编程风格的选择不应该基于个人爱好、熟悉程度、打字量等因素,而要考虑如何尽量使代码清晰易读、减少出错。你选择的,不是你喜欢的风格,而是一种能够清晰表达你的意图的风格。这一点,对于 JavaScript 这种语法自由度很高的语言尤其重要。 + +必须牢记的一点是,如果你选定了一种“编程风格”,就应该坚持遵守,切忌多种风格混用。如果你加入他人的项目,就应该遵守现有的风格。 + +## 缩进 + +行首的空格和 Tab 键,都可以产生代码缩进效果(indent)。 + +Tab 键可以节省击键次数,但不同的文本编辑器对 Tab 的显示不尽相同,有的显示四个空格,有的显示两个空格,所以有人觉得,空格键可以使得显示效果更统一。 + +无论你选择哪一种方法,都是可以接受的,要做的就是始终坚持这一种选择。不要一会使用 Tab 键,一会使用空格键。 + +## 区块 + +如果循环和判断的代码体只有一行,JavaScript 允许该区块(block)省略大括号。 + +```javascript +if (a) + b(); + c(); +``` + +上面代码的原意可能是下面这样。 + +```javascript +if (a) { + b(); + c(); +} +``` + +但是,实际效果却是下面这样。 + +```javascript +if (a) { + b(); +} + c(); +``` + +因此,建议总是使用大括号表示区块。 + +另外,区块起首的大括号的位置,有许多不同的写法。最流行的有两种,一种是起首的大括号另起一行。 + +```javascript +block +{ + // ... +} +``` + +另一种是起首的大括号跟在关键字的后面。 + +```javascript +block { + // ... +} +``` + +一般来说,这两种写法都可以接受。但是,JavaScript 要使用后一种,因为 JavaScript 会自动添加句末的分号,导致一些难以察觉的错误。 + +```javascript +return +{ + key: value +}; + +// 相当于 +return; +{ + key: value +}; +``` + +上面的代码的原意,是要返回一个对象,但实际上返回的是`undefined`,因为 JavaScript 自动在`return`语句后面添加了分号。为了避免这一类错误,需要写成下面这样。 + +```javascript +return { + key : value +}; +``` + +因此,表示区块起首的大括号,不要另起一行。 + +## 圆括号 + +圆括号(parentheses)在 JavaScript 中有两种作用,一种表示函数的调用,另一种表示表达式的组合(grouping)。 + +```javascript +// 圆括号表示函数的调用 +console.log('abc'); + +// 圆括号表示表达式的组合 +(1 + 2) * 3 +``` + +建议可以用空格,区分这两种不同的括号。 + +> 1. 表示函数调用时,函数名与左括号之间没有空格。 +> 2. 表示函数定义时,函数名与左括号之间没有空格。 +> 3. 其他情况时,前面位置的语法元素与左括号之间,都有一个空格。 + +按照上面的规则,下面的写法都是不规范的。 + +```javascript +foo (bar) +return(a+b); +if(a === 0) {...} +function foo (b) {...} +function(x) {...} +``` + +上面代码的最后一行是一个匿名函数,`function`是语法关键字,不是函数名,所以与左括号之间应该要有一个空格。 + +## 行尾的分号 + +分号表示一条语句的结束。JavaScript 允许省略行尾的分号。事实上,确实有一些开发者行尾从来不写分号。但是,由于下面要讨论的原因,建议还是不要省略这个分号。 + +### 不使用分号的情况 + +首先,以下三种情况,语法规定本来就不需要在结尾添加分号。 + +**(1)for 和 while 循环** + +```javascript +for ( ; ; ) { +} // 没有分号 + +while (true) { +} // 没有分号 +``` + +注意,`do...while`循环是有分号的。 + +```javascript +do { + a--; +} while(a > 0); // 分号不能省略 +``` + +**(2)分支语句:if,switch,try** + +```javascript +if (true) { +} // 没有分号 + +switch () { +} // 没有分号 + +try { +} catch { +} // 没有分号 +``` + +**(3)函数的声明语句** + +```javascript +function f() { +} // 没有分号 +``` + +注意,函数表达式仍然要使用分号。 + +```javascript +var f = function f() { +}; +``` + +以上三种情况,如果使用了分号,并不会出错。因为,解释引擎会把这个分号解释为空语句。 + +### 分号的自动添加 + +除了上一节的三种情况,所有语句都应该使用分号。但是,如果没有使用分号,大多数情况下,JavaScript 会自动添加。 + +```javascript +var a = 1 +// 等同于 +var a = 1; +``` + +这种语法特性被称为“分号的自动添加”(Automatic Semicolon Insertion,简称 ASI)。 + +因此,有人提倡省略句尾的分号。麻烦的是,如果下一行的开始可以与本行的结尾连在一起解释,JavaScript 就不会自动添加分号。 + +```javascript +// 等同于 var a = 3 +var +a += +3 + +// 等同于 'abc'.length +'abc' +.length + +// 等同于 return a + b; +return a + +b; + +// 等同于 obj.foo(arg1, arg2); +obj.foo(arg1, +arg2); + +// 等同于 3 * 2 + 10 * (27 / 6) +3 * 2 ++ +10 * (27 / 6) +``` + +上面代码都会多行放在一起解释,不会每一行自动添加分号。这些例子还是比较容易看出来的,但是下面这个例子就不那么容易看出来了。 + +```javascript +x = y +(function () { + // ... +})(); + +// 等同于 +x = y(function () {...})(); +``` + +下面是更多不会自动添加分号的例子。 + +```javascript +// 引擎解释为 c(d+e) +var a = b + c +(d+e).toString(); + +// 引擎解释为 a = b/hi/g.exec(c).map(d) +// 正则表达式的斜杠,会当作除法运算符 +a = b +/hi/g.exec(c).map(d); + +// 解释为'b'['red', 'green'], +// 即把字符串当作一个数组,按索引取值 +var a = 'b' +['red', 'green'].forEach(function (c) { + console.log(c); +}) + +// 解释为 function (x) { return x }(a++) +// 即调用匿名函数,结果f等于0 +var a = 0; +var f = function (x) { return x } +(a++) +``` + +只有下一行的开始与本行的结尾,无法放在一起解释,JavaScript 引擎才会自动添加分号。 + +```javascript +if (a < 0) a = 0 +console.log(a) + +// 等同于下面的代码, +// 因为 0console 没有意义 +if (a < 0) a = 0; +console.log(a) +``` + +另外,如果一行的起首是“自增”(`++`)或“自减”(`--`)运算符,则它们的前面会自动添加分号。 + +```javascript +a = b = c = 1 + +a +++ +b +-- +c + +console.log(a, b, c) +// 1 2 0 +``` + +上面代码之所以会得到`1 2 0`的结果,原因是自增和自减运算符前,自动加上了分号。上面的代码实际上等同于下面的形式。 + +```javascript +a = b = c = 1; +a; +++b; +--c; +``` + +如果`continue`、`break`、`return`和`throw`这四个语句后面,直接跟换行符,则会自动添加分号。这意味着,如果`return`语句返回的是一个对象的字面量,起首的大括号一定要写在同一行,否则得不到预期结果。 + +```javascript +return +{ first: 'Jane' }; + +// 解释成 +return; +{ first: 'Jane' }; +``` + +由于解释引擎自动添加分号的行为难以预测,因此编写代码的时候不应该省略行尾的分号。 + +不应该省略结尾的分号,还有一个原因。有些 JavaScript 代码压缩器(uglifier)不会自动添加分号,因此遇到没有分号的结尾,就会让代码保持原状,而不是压缩成一行,使得压缩无法得到最优的结果。 + +另外,不写结尾的分号,可能会导致脚本合并出错。所以,有的代码库在第一行语句开始前,会加上一个分号。 + +```javascript +;var a = 1; +// ... +``` + +上面这种写法就可以避免与其他脚本合并时,排在前面的脚本最后一行语句没有分号,导致运行出错的问题。 + +## 全局变量 + +JavaScript 最大的语法缺点,可能就是全局变量对于任何一个代码块,都是可读可写。这对代码的模块化和重复使用,非常不利。 + +因此,建议避免使用全局变量。如果不得不使用,可以考虑用大写字母表示变量名,这样更容易看出这是全局变量,比如`UPPER_CASE`。 + +## 变量声明 + +JavaScript 会自动将变量声明“提升”(hoist)到代码块(block)的头部。 + +```javascript +if (!x) { + var x = {}; +} + +// 等同于 +var x; +if (!x) { + x = {}; +} +``` + +这意味着,变量`x`是`if`代码块之前就存在了。为了避免可能出现的问题,最好把变量声明都放在代码块的头部。 + +```javascript +for (var i = 0; i < 10; i++) { + // ... +} + +// 写成 +var i; +for (i = 0; i < 10; i++) { + // ... +} +``` + +上面这样的写法,就容易看出存在一个全局的循环变量`i`。 + +另外,所有函数都应该在使用之前定义。函数内部的变量声明,都应该放在函数的头部。 + +## with 语句 + +`with`可以减少代码的书写,但是会造成混淆。 + +```javascript +with (o) { + foo = bar; +} +``` + +上面的代码,可以有四种运行结果: + +```javascript +o.foo = bar; +o.foo = o.bar; +foo = bar; +foo = o.bar; +``` + +这四种结果都可能发生,取决于不同的变量是否有定义。因此,不要使用`with`语句。 + +## 相等和严格相等 + +JavaScript 有两个表示相等的运算符:“相等”(`==`)和“严格相等”(`===`)。 + +相等运算符会自动转换变量类型,造成很多意想不到的情况。 + +```javascript +0 == ''// true +1 == true // true +2 == true // false +0 == '0' // true +false == 'false' // false +false == '0' // true +' \t\r\n ' == 0 // true +``` + +因此,建议不要使用相等运算符(`==`),只使用严格相等运算符(`===`)。 + +## 语句的合并 + +有些程序员追求简洁,喜欢合并不同目的的语句。比如,原来的语句是 + +```javascript +a = b; +if (a) { + // ... +} +``` + +他喜欢写成下面这样。 + +```javascript +if (a = b) { + // ... +} +``` + +虽然语句少了一行,但是可读性大打折扣,而且会造成误读,让别人误解这行代码的意思是下面这样。 + +```javascript +if (a === b){ + // ... +} +``` + +建议不要将不同目的的语句,合并成一行。 + +## 自增和自减运算符 + +自增(`++`)和自减(`--`)运算符,放在变量的前面或后面,返回的值不一样,很容易发生错误。事实上,所有的`++`运算符都可以用`+= 1`代替。 + +```javascript +++x +// 等同于 +x += 1; +``` + +改用`+= 1`,代码变得更清晰了。 + +建议自增(`++`)和自减(`--`)运算符尽量使用`+=`和`-=`代替。 + +## switch...case 结构 + +`switch...case`结构要求,在每一个`case`的最后一行必须是`break`语句,否则会接着运行下一个`case`。这样不仅容易忘记,还会造成代码的冗长。 + +而且,`switch...case`不使用大括号,不利于代码形式的统一。此外,这种结构类似于`goto`语句,容易造成程序流程的混乱,使得代码结构混乱不堪,不符合面向对象编程的原则。 + +```javascript +function doAction(action) { + switch (action) { + case 'hack': + return 'hack'; + case 'slash': + return 'slash'; + case 'run': + return 'run'; + default: + throw new Error('Invalid action.'); + } +} +``` + +上面的代码建议改写成对象结构。 + +```javascript +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`结构可以用对象结构代替。 + +## 参考链接 + +* Eric Elliott, Programming JavaScript Applications, [Chapter 2. JavaScript Style Guide](http://chimera.labs.oreilly.com/books/1234000000262/ch02.html), O'Reilly, 2013 +* Axel Rauschmayer, [A meta style guide for JavaScript](http://www.2ality.com/2013/07/meta-style-guide.html) +* Axel Rauschmayer, [Automatic semicolon insertion in JavaScript](http://www.2ality.com/2011/05/semicolon-insertion.html) +* Rod Vagg, [JavaScript and Semicolons](http://dailyjs.com/2012/04/19/semicolons/) + diff --git a/operators/README.md b/operators/README.md new file mode 100644 index 0000000..395fdc3 --- /dev/null +++ b/operators/README.md @@ -0,0 +1,2 @@ +# 运算符 + diff --git a/operators/arithmetic.md b/operators/arithmetic.md new file mode 100644 index 0000000..085df1d --- /dev/null +++ b/operators/arithmetic.md @@ -0,0 +1,314 @@ +# 算术运算符 + +运算符是处理数据的基本方法,用来从现有的值得到新的值。JavaScript 提供了多种运算符,覆盖了所有主要的运算。 + +## 概述 + +JavaScript 共提供10个算术运算符,用来完成基本的算术运算。 + +* **加法运算符**:`x + y` +* **减法运算符**: `x - y` +* **乘法运算符**: `x * y` +* **除法运算符**:`x / y` +* **指数运算符**:`x ** y` +* **余数运算符**:`x % y` +* **自增运算符**:`++x` 或者 `x++` +* **自减运算符**:`--x` 或者 `x--` +* **数值运算符**: `+x` +* **负数值运算符**:`-x` + +减法、乘法、除法运算法比较单纯,就是执行相应的数学运算。下面介绍其他几个算术运算符,重点是加法运算符。 + +## 加法运算符 + +### 基本规则 + +加法运算符(`+`)是最常见的运算符,用来求两个数值的和。 + +```javascript +1 + 1 // 2 +``` + +JavaScript 允许非数值的相加。 + +```javascript +true + true // 2 +1 + true // 2 +``` + +上面代码中,第一行是两个布尔值相加,第二行是数值与布尔值相加。这两种情况,布尔值都会自动转成数值,然后再相加。 + +比较特殊的是,如果是两个字符串相加,这时加法运算符会变成连接运算符,返回一个新的字符串,将两个原字符串连接在一起。 + +```javascript +'a' + 'bc' // "abc" +``` + +如果一个运算子是字符串,另一个运算子是非字符串,这时非字符串会转成字符串,再连接在一起。 + +```javascript +1 + 'a' // "1a" +false + 'a' // "falsea" +``` + +加法运算符是在运行时决定,到底是执行相加,还是执行连接。也就是说,运算子的不同,导致了不同的语法行为,这种现象称为“重载”(overload)。由于加法运算符存在重载,可能执行两种运算,使用的时候必须很小心。 + +```javascript +'3' + 4 + 5 // "345" +3 + 4 + '5' // "75" +``` + +上面代码中,由于从左到右的运算次序,字符串的位置不同会导致不同的结果。 + +除了加法运算符,其他算术运算符(比如减法、除法和乘法)都不会发生重载。它们的规则是:所有运算子一律转为数值,再进行相应的数学运算。 + +```javascript +1 - '2' // -1 +1 * '2' // 2 +1 / '2' // 0.5 +``` + +上面代码中,减法、除法和乘法运算符,都是将字符串自动转为数值,然后再运算。 + +### 对象的相加 + +如果运算子是对象,必须先转成原始类型的值,然后再相加。 + +```javascript +var obj = { p: 1 }; +obj + 2 // "[object Object]2" +``` + +上面代码中,对象`obj`转成原始类型的值是`[object Object]`,再加`2`就得到了上面的结果。 + +对象转成原始类型的值,规则如下。 + +首先,自动调用对象的`valueOf`方法。 + +```javascript +var obj = { p: 1 }; +obj.valueOf() // { p: 1 } +``` + +一般来说,对象的`valueOf`方法总是返回对象自身,这时再自动调用对象的`toString`方法,将其转为字符串。 + +```javascript +var obj = { p: 1 }; +obj.valueOf().toString() // "[object Object]" +``` + +对象的`toString`方法默认返回`[object Object]`,所以就得到了最前面那个例子的结果。 + +知道了这个规则以后,就可以自己定义`valueOf`方法或`toString`方法,得到想要的结果。 + +```javascript +var obj = { + valueOf: function () { + return 1; + } +}; + +obj + 2 // 3 +``` + +上面代码中,我们定义`obj`对象的`valueOf`方法返回`1`,于是`obj + 2`就得到了`3`。这个例子中,由于`valueOf`方法直接返回一个原始类型的值,所以不再调用`toString`方法。 + +下面是自定义`toString`方法的例子。 + +```javascript +var obj = { + toString: function () { + return 'hello'; + } +}; + +obj + 2 // "hello2" +``` + +上面代码中,对象`obj`的`toString`方法返回字符串`hello`。前面说过,只要有一个运算子是字符串,加法运算符就变成连接运算符,返回连接后的字符串。 + +这里有一个特例,如果运算子是一个`Date`对象的实例,那么会优先执行`toString`方法。 + +```javascript +var obj = new Date(); +obj.valueOf = function () { return 1 }; +obj.toString = function () { return 'hello' }; + +obj + 2 // "hello2" +``` + +上面代码中,对象`obj`是一个`Date`对象的实例,并且自定义了`valueOf`方法和`toString`方法,结果`toString`方法优先执行。 + +## 余数运算符 + +余数运算符(`%`)返回前一个运算子被后一个运算子除,所得的余数。 + +```javascript +12 % 5 // 2 +``` + +需要注意的是,运算结果的正负号由第一个运算子的正负号决定。 + +```javascript +-1 % 2 // -1 +1 % -2 // 1 +``` + +所以,为了得到负数的正确余数值,可以先使用绝对值函数。 + +```javascript +// 错误的写法 +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 +``` + +余数运算符还可以用于浮点数的运算。但是,由于浮点数不是精确的值,无法得到完全准确的结果。 + +```javascript +6.5 % 2.1 +// 0.19999999999999973 +``` + +## 自增和自减运算符 + +自增和自减运算符,是一元运算符,只需要一个运算子。它们的作用是将运算子首先转为数值,然后加上1或者减去1。它们会修改原始变量。 + +```javascript +var x = 1; +++x // 2 +x // 2 + +--x // 1 +x // 1 +``` + +上面代码的变量`x`自增后,返回`2`,再进行自减,返回`1`。这两种情况都会使得,原始变量`x`的值发生改变。 + +运算之后,变量的值发生变化,这种效应叫做运算的副作用(side effect)。自增和自减运算符是仅有的两个具有副作用的运算符,其他运算符都不会改变变量的值。 + +自增和自减运算符有一个需要注意的地方,就是放在变量之后,会先返回变量操作前的值,再进行自增/自减操作;放在变量之前,会先进行自增/自减操作,再返回变量操作后的值。 + +```javascript +var x = 1; +var y = 1; + +x++ // 1 +++y // 2 +``` + +上面代码中,`x`是先返回当前值,然后自增,所以得到`1`;`y`是先自增,然后返回新的值,所以得到`2`。 + +## 数值运算符,负数值运算符 + +数值运算符(`+`)同样使用加号,但它是一元运算符(只需要一个操作数),而加法运算符是二元运算符(需要两个操作数)。 + +数值运算符的作用在于可以将任何值转为数值(与`Number`函数的作用相同)。 + +```javascript ++true // 1 ++[] // 0 ++{} // NaN +``` + +上面代码表示,非数值经过数值运算符以后,都变成了数值(最后一行`NaN`也是数值)。具体的类型转换规则,参见《数据类型转换》一章。 + +负数值运算符(`-`),也同样具有将一个值转为数值的功能,只不过得到的值正负相反。连用两个负数值运算符,等同于数值运算符。 + +```javascript +var x = 1; +-x // -1 +-(-x) // 1 +``` + +上面代码最后一行的圆括号不可少,否则会变成自减运算符。 + +数值运算符号和负数值运算符,都会返回一个新的值,而不会改变原始变量的值。 + +## 指数运算符 + +指数运算符(`**`)完成指数运算,前一个运算子是底数,后一个运算子是指数。 + +```javascript +2 ** 4 // 16 +``` + +注意,指数运算符是右结合,而不是左结合。即多个指数运算符连用时,先进行最右边的计算。 + +```javascript +// 相当于 2 ** (3 ** 2) +2 ** 3 ** 2 +// 512 +``` + +上面代码中,由于指数运算符是右结合,所以先计算第二个指数运算符,而不是第一个。 + +## 赋值运算符 + +赋值运算符(Assignment Operators)用于给变量赋值。 + +最常见的赋值运算符,当然就是等号(`=`)。 + +```javascript +// 将 1 赋值给变量 x +var x = 1; + +// 将变量 y 的值赋值给变量 x +var x = y; +``` + +赋值运算符还可以与其他运算符结合,形成变体。下面是与算术运算符的结合。 + +```javascript +// 等同于 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 +``` + +下面是与位运算符的结合(关于位运算符,请见后文的介绍)。 + +```javascript +// 等同于 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 +``` + +这些复合的赋值运算符,都是先进行指定运算,然后将得到值返回给左边的变量。 + diff --git a/operators/bit.md b/operators/bit.md new file mode 100644 index 0000000..49948dc --- /dev/null +++ b/operators/bit.md @@ -0,0 +1,359 @@ +# 二进制运算符 + +## 概述 + +二进制位运算符用于直接对二进制位进行计算,一共有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位带符号的整数。 + +```javascript +i = i | 0; +``` + +上面这行代码的意思,就是将`i`(不管是整数或小数)转为32位整数。 + +利用这个特性,可以写出一个函数,将任意数值转为32位整数。 + +```javascript +function toInt32(x) { + return x | 0; +} +``` + +上面这个函数将任意值与`0`进行一次或运算,这个位运算会自动将一个值转为32位整数。下面是这个函数的用法。 + +```javascript +toInt32(1.001) // 1 +toInt32(1.999) // 1 +toInt32(1) // 1 +toInt32(-1) // -1 +toInt32(Math.pow(2, 32) + 1) // 1 +toInt32(Math.pow(2, 32) - 1) // -1 +``` + +上面代码中,`toInt32`可以将小数转为整数。对于一般的整数,返回值不会有任何变化。对于大于或等于2的32次方的整数,大于32位的数位都会被舍去。 + +## 二进制或运算符 + +二进制或运算符(`|`)逐位比较两个运算子,两个二进制位之中只要有一个为`1`,就返回`1`,否则返回`0`。 + +```javascript +0 | 3 // 3 +``` + +上面代码中,`0`和`3`的二进制形式分别是`00`和`11`,所以进行二进制或运算会得到`11`(即`3`)。 + +位运算只对整数有效,遇到小数时,会将小数部分舍去,只保留整数部分。所以,将一个小数与`0`进行二进制或运算,等同于对该数去除小数部分,即取整数位。 + +```javascript +2.9 | 0 // 2 +-2.9 | 0 // -2 +``` + +需要注意的是,这种取整方法不适用超过32位整数最大值`2147483647`的数。 + +```javascript +2147483649.4 | 0; +// -2147483647 +``` + +## 二进制与运算符 + +二进制与运算符(`&`)的规则是逐位比较两个运算子,两个二进制位之中只要有一个位为`0`,就返回`0`,否则返回`1`。 + +```javascript +0 & 3 // 0 +``` + +上面代码中,0(二进制`00`)和3(二进制`11`)进行二进制与运算会得到`00`(即`0`)。 + +## 二进制否运算符 + +二进制否运算符(`~`)将每个二进制位都变为相反值(`0`变为`1`,`1`变为`0`)。它的返回结果有时比较难理解,因为涉及到计算机内部的数值表示机制。 + +```javascript +~ 3 // -4 +``` + +上面表达式对`3`进行二进制否运算,得到`-4`。之所以会有这样的结果,是因为位运算时,JavaScript 内部将所有的运算子都转为32位的二进制整数再进行运算。 + +`3`的32位整数形式是`00000000000000000000000000000011`,二进制否运算以后得到`11111111111111111111111111111100`。由于第一位(符号位)是1,所以这个数是一个负数。JavaScript 内部采用补码形式表示负数,即需要将这个数减去1,再取一次反,然后加上负号,才能得到这个负数对应的10进制值。这个数减去1等于`11111111111111111111111111111011`,再取一次反得到`00000000000000000000000000000100`,再加上负号就是`-4`。考虑到这样的过程比较麻烦,可以简单记忆成,一个数与自身的取反值相加,等于-1。 + +```javascript +~ -3 // 2 +``` + +上面表达式可以这样算,`-3`的取反值等于`-1`减去`-3`,结果为`2`。 + +对一个整数连续两次二进制否运算,得到它自身。 + +```javascript +~~3 // 3 +``` + +所有的位运算都只对整数有效。二进制否运算遇到小数时,也会将小数部分舍去,只保留整数部分。所以,对一个小数连续进行两次二进制否运算,能达到取整效果。 + +```javascript +~~2.9 // 2 +~~47.11 // 47 +~~1.9999 // 1 +~~3 // 3 +``` + +使用二进制否运算取整,是所有取整方法中最快的一种。 + +对字符串进行二进制否运算,JavaScript 引擎会先调用`Number`函数,将字符串转为数值。 + +```javascript +// 相当于~Number('011') +~'011' // -12 + +// 相当于~Number('42 cats') +~'42 cats' // -1 + +// 相当于~Number('0xcafebabe') +~'0xcafebabe' // 889275713 + +// 相当于~Number('deadbeef') +~'deadbeef' // -1 +``` + +`Number`函数将字符串转为数值的规则,参见《数据的类型转换》一章。 + +对于其他类型的值,二进制否运算也是先用`Number`转为数值,然后再进行处理。 + +```javascript +// 相当于 ~Number([]) +~[] // -1 + +// 相当于 ~Number(NaN) +~NaN // -1 + +// 相当于 ~Number(null) +~null // -1 +``` + +## 异或运算符 + +异或运算(`^`)在两个二进制位不同时返回`1`,相同时返回`0`。 + +```javascript +0 ^ 3 // 3 +``` + +上面表达式中,`0`(二进制`00`)与`3`(二进制`11`)进行异或运算,它们每一个二进制位都不同,所以得到`11`(即`3`)。 + +“异或运算”有一个特殊运用,连续对两个数`a`和`b`进行三次异或运算,`a^=b; b^=a; a^=b;`,可以[互换](http://en.wikipedia.org/wiki/XOR_swap_algorithm)它们的值。这意味着,使用“异或运算”可以在不引入临时变量的前提下,互换两个变量的值。 + +```javascript +var a = 10; +var b = 99; + +a ^= b, b ^= a, a ^= b; + +a // 99 +b // 10 +``` + +这是互换两个变量的值的最快方法。 + +异或运算也可以用来取整。 + +```javascript +12.9 ^ 0 // 12 +``` + +## 左移运算符 + +左移运算符(`<<`)表示将一个数的二进制值向左移动指定的位数,尾部补`0`,即乘以`2`的指定次方。向左移动的时候,最高位的符号位是一起移动的。 + +```javascript +// 4 的二进制形式为100, +// 左移一位为1000(即十进制的8) +// 相当于乘以2的1次方 +4 << 1 +// 8 + +-4 << 1 +// -8 +``` + +上面代码中,`-4`左移一位得到`-8`,是因为`-4`的二进制形式是`11111111111111111111111111111100`,左移一位后得到`11111111111111111111111111111000`,该数转为十进制(减去1后取反,再加上负号)即为`-8`。 + +如果左移0位,就相当于将该数值转为32位整数,等同于取整,对于正数和负数都有效。 + +```javascript +13.5 << 0 +// 13 + +-13.5 << 0 +// -13 +``` + +左移运算符用于二进制数值非常方便。 + +```javascript +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`;如果是负数,头部全部补`1`。右移运算符基本上相当于除以`2`的指定次方(最高位即符号位参与移动)。 + +```javascript +4 >> 1 +// 2 +/* +// 因为4的二进制形式为 00000000000000000000000000000100, +// 右移一位得到 00000000000000000000000000000010, +// 即为十进制的2 +*/ + +-4 >> 1 +// -2 +/* +// 因为-4的二进制形式为 11111111111111111111111111111100, +// 右移一位,头部补1,得到 11111111111111111111111111111110, +// 即为十进制的-2 +*/ +``` + +右移运算可以模拟 2 的整除运算。 + +```javascript +5 >> 1 +// 2 +// 相当于 5 / 2 = 2 + +21 >> 2 +// 5 +// 相当于 21 / 4 = 5 + +21 >> 3 +// 2 +// 相当于 21 / 8 = 2 + +21 >> 4 +// 1 +// 相当于 21 / 16 = 1 +``` + +## 头部补零的右移运算符 + +头部补零的右移运算符(`>>>`)与右移运算符(`>>`)只有一个差别,就是一个数的二进制形式向右移动时,头部一律补零,而不考虑符号位。所以,该运算总是得到正值。对于正数,该运算的结果与右移运算符(`>>`)完全一致,区别主要在于负数。 + +```javascript +4 >>> 1 +// 2 + +-4 >>> 1 +// 2147483646 +/* +// 因为-4的二进制形式为11111111111111111111111111111100, +// 带符号位的右移一位,得到01111111111111111111111111111110, +// 即为十进制的2147483646。 +*/ +``` + +这个运算实际上将一个值转为32位无符号整数。 + +查看一个负整数在计算机内部的储存形式,最快的方法就是使用这个运算符。 + +```javascript +-1 >>> 0 // 4294967295 +``` + +上面代码表示,`-1`作为32位整数时,内部的储存形式使用无符号整数格式解读,值为 4294967295(即`(2^32)-1`,等于`11111111111111111111111111111111`)。 + +## 开关作用 + +位运算符可以用作设置对象属性的开关。 + +假定某个对象有四个开关,每个开关都是一个变量。那么,可以设置一个四位的二进制数,它的每个位对应一个开关。 + +```javascript +var FLAG_A = 1; // 0001 +var FLAG_B = 2; // 0010 +var FLAG_C = 4; // 0100 +var FLAG_D = 8; // 1000 +``` + +上面代码设置 A、B、C、D 四个开关,每个开关分别占有一个二进制位。 + +然后,就可以用二进制与运算,检查当前设置是否打开了指定开关。 + +```javascript +var flags = 5; // 二进制的0101 + +if (flags & FLAG_C) { + // ... +} +// 0101 & 0100 => 0100 => true +``` + +上面代码检验是否打开了开关`C`。如果打开,会返回`true`,否则返回`false`。 + +现在假设需要打开`A`、`B`、`D`三个开关,我们可以构造一个掩码变量。 + +```javascript +var mask = FLAG_A | FLAG_B | FLAG_D; +// 0001 | 0010 | 1000 => 1011 +``` + +上面代码对`A`、`B`、`D`三个变量进行二进制或运算,得到掩码值为二进制的`1011`。 + +有了掩码,二进制或运算可以确保打开指定的开关。 + +```javascript +flags = flags | mask; +``` + +上面代码中,计算后得到的`flags`变量,代表三个开关的二进制位都打开了。 + +二进制与运算可以将当前设置中凡是与开关设置不一样的项,全部关闭。 + +```javascript +flags = flags & mask; +``` + +异或运算可以切换(toggle)当前设置,即第一次执行可以得到当前设置的相反值,再执行一次又得到原来的值。 + +```javascript +flags = flags ^ mask; +``` + +二进制否运算可以翻转当前设置,即原设置为`0`,运算后变为`1`;原设置为`1`,运算后变为`0`。 + +```javascript +flags = ~flags; +``` + +## 参考链接 + +* Michal Budzynski, [JavaScript: The less known parts. Bitwise Operators](http://michalbe.blogspot.co.uk/2013/03/javascript-less-known-parts-bitwise.html) +* Axel Rauschmayer, [Basic JavaScript for the impatient programmer](http://www.2ality.com/2013/06/basic-javascript.html) +* Mozilla Developer Network, [Bitwise Operators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators) + diff --git a/operators/boolean.md b/operators/boolean.md new file mode 100644 index 0000000..4ffebc3 --- /dev/null +++ b/operators/boolean.md @@ -0,0 +1,166 @@ +# 布尔运算符 + +## 概述 + +布尔运算符用于将表达式转为布尔值,一共包含四个运算符。 + +* 取反运算符:`!` +* 且运算符:`&&` +* 或运算符:`||` +* 三元运算符:`?:` + +## 取反运算符(!) + +取反运算符是一个感叹号,用于将布尔值变为相反值,即`true`变成`false`,`false`变成`true`。 + +```javascript +!true // false +!false // true +``` + +对于非布尔值,取反运算符会将其转为布尔值。可以这样记忆,以下六个值取反后为`true`,其他值都为`false`。 + +* `undefined` +* `null` +* `false` +* `0` +* `NaN` +* 空字符串(`''`) + +```javascript +!undefined // true +!null // true +!0 // true +!NaN // true +!"" // true + +!54 // false +!'hello' // false +![] // false +!{} // false +``` + +上面代码中,不管什么类型的值,经过取反运算后,都变成了布尔值。 + +如果对一个值连续做两次取反运算,等于将其转为对应的布尔值,与`Boolean`函数的作用相同。这是一种常用的类型转换的写法。 + +```javascript +!!x +// 等同于 +Boolean(x) +``` + +上面代码中,不管`x`是什么类型的值,经过两次取反运算后,变成了与`Boolean`函数结果相同的布尔值。所以,两次取反就是将一个值转为布尔值的简便写法。 + +## 且运算符(&&) + +且运算符(`&&`)往往用于多个表达式的求值。 + +它的运算规则是:如果第一个运算子的布尔值为`true`,则返回第二个运算子的值(注意是值,不是布尔值);如果第一个运算子的布尔值为`false`,则直接返回第一个运算子的值,且不再对第二个运算子求值。 + +```javascript +'t' && '' // "" +'t' && 'f' // "f" +'t' && (1 + 2) // 3 +'' && 'f' // "" +'' && '' // "" + +var x = 1; +(1 - 1) && ( x += 1) // 0 +x // 1 +``` + +上面代码的最后一个例子,由于且运算符的第一个运算子的布尔值为`false`,则直接返回它的值`0`,而不再对第二个运算子求值,所以变量`x`的值没变。 + +这种跳过第二个运算子的机制,被称为“短路”。有些程序员喜欢用它取代`if`结构,比如下面是一段`if`结构的代码,就可以用且运算符改写。 + +```javascript +if (i) { + doSomething(); +} + +// 等价于 + +i && doSomething(); +``` + +上面代码的两种写法是等价的,但是后一种不容易看出目的,也不容易除错,建议谨慎使用。 + +且运算符可以多个连用,这时返回第一个布尔值为`false`的表达式的值。如果所有表达式的布尔值都为`true`,则返回最后一个表达式的值。 + +```javascript +true && 'foo' && '' && 4 && 'foo' && true +// '' + +1 && 2 && 3 +// 3 +``` + +上面代码中,例一里面,第一个布尔值为`false`的表达式为第三个表达式,所以得到一个空字符串。例二里面,所有表达式的布尔值都是`true`,所以返回最后一个表达式的值`3`。 + +## 或运算符(\|\|) + +或运算符(`||`)也用于多个表达式的求值。它的运算规则是:如果第一个运算子的布尔值为`true`,则返回第一个运算子的值,且不再对第二个运算子求值;如果第一个运算子的布尔值为`false`,则返回第二个运算子的值。 + +```javascript +'t' || '' // "t" +'t' || 'f' // "t" +'' || 'f' // "f" +'' || '' // "" +``` + +短路规则对这个运算符也适用。 + +```javascript +var x = 1; +true || (x = 2) // true +x // 1 +``` + +上面代码中,或运算符的第一个运算子为`true`,所以直接返回`true`,不再运行第二个运算子。所以,`x`的值没有改变。这种只通过第一个表达式的值,控制是否运行第二个表达式的机制,就称为“短路”(short-cut)。 + +或运算符可以多个连用,这时返回第一个布尔值为`true`的表达式的值。如果所有表达式都为`false`,则返回最后一个表达式的值。 + +```javascript +false || 0 || '' || 4 || 'foo' || true +// 4 + +false || 0 || '' +// '' +``` + +上面代码中,例一里面,第一个布尔值为`true`的表达式是第四个表达式,所以得到数值4。例二里面,所有表达式的布尔值都为`false`,所以返回最后一个表达式的值。 + +或运算符常用于为一个变量设置默认值。 + +```javascript +function saveText(text) { + text = text || ''; + // ... +} + +// 或者写成 +saveText(this.text || '') +``` + +上面代码表示,如果函数调用时,没有提供参数,则该参数默认设置为空字符串。 + +## 三元条件运算符(?:) + +三元条件运算符由问号(?)和冒号(:)组成,分隔三个表达式。它是 JavaScript 语言唯一一个需要三个运算子的运算符。如果第一个表达式的布尔值为`true`,则返回第二个表达式的值,否则返回第三个表达式的值。 + +```javascript +'t' ? 'hello' : 'world' // "hello" +0 ? 'hello' : 'world' // "world" +``` + +上面代码的`t`和`0`的布尔值分别为`true`和`false`,所以分别返回第二个和第三个表达式的值。 + +通常来说,三元条件表达式与`if...else`语句具有同样表达效果,前者可以表达的,后者也能表达。但是两者具有一个重大差别,`if...else`是语句,没有返回值;三元条件表达式是表达式,具有返回值。所以,在需要返回值的场合,只能使用三元条件表达式,而不能使用`if..else`。 + +```javascript +console.log(true ? 'T' : 'F'); +``` + +上面代码中,`console.log`方法的参数必须是一个表达式,这时就只能使用三元条件表达式。如果要用`if...else`语句,就必须改变整个代码写法了。 + diff --git a/operators/comparison.md b/operators/comparison.md new file mode 100644 index 0000000..4eedf67 --- /dev/null +++ b/operators/comparison.md @@ -0,0 +1,365 @@ +# 比较运算符 + +## 概述 + +比较运算符用于比较两个值的大小,然后返回一个布尔值,表示是否满足指定的条件。 + +```javascript +2 > 1 // true +``` + +上面代码比较`2`是否大于`1`,返回`true`。 + +> 注意,比较运算符可以比较各种类型的值,不仅仅是数值。 + +JavaScript 一共提供了8个比较运算符。 + +* `>` 大于运算符 +* `<` 小于运算符 +* `<=` 小于或等于运算符 +* `>=` 大于或等于运算符 +* `==` 相等运算符 +* `===` 严格相等运算符 +* `!=` 不相等运算符 +* `!==` 严格不相等运算符 + +这八个比较运算符分成两类:相等比较和非相等比较。两者的规则是不一样的,对于非相等的比较,算法是先看两个运算子是否都是字符串,如果是的,就按照字典顺序比较(实际上是比较 Unicode 码点);否则,将两个运算子都转成数值,再比较数值的大小。 + +## 非相等运算符:字符串的比较 + +字符串按照字典顺序进行比较。 + +```javascript +'cat' > 'dog' // false +'cat' > 'catalog' // false +``` + +JavaScript 引擎内部首先比较首字符的 Unicode 码点。如果相等,再比较第二个字符的 Unicode 码点,以此类推。 + +```javascript +'cat' > 'Cat' // true' +``` + +上面代码中,小写的`c`的 Unicode 码点(`99`)大于大写的`C`的 Unicode 码点(`67`),所以返回`true`。 + +由于所有字符都有 Unicode 码点,因此汉字也可以比较。 + +```javascript +'大' > '小' // false +``` + +上面代码中,“大”的 Unicode 码点是22823,“小”是23567,因此返回`false`。 + +## 非相等运算符:非字符串的比较 + +如果两个运算子之中,至少有一个不是字符串,需要分成以下两种情况。 + +**(1)原始类型值** + +如果两个运算子都是原始类型的值,则是先转成数值再比较。 + +```javascript +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`本身)与`NaN`使用非相等运算符进行比较,返回的都是`false`。 + +```javascript +1 > NaN // false +1 <= NaN // false +'1' > NaN // false +'1' <= NaN // false +NaN > NaN // false +NaN <= NaN // false +``` + +**(2)对象** + +如果运算子是对象,会转为原始类型的值,再进行比较。 + +对象转换成原始类型的值,算法是先调用`valueOf`方法;如果返回的还是对象,再接着调用`toString`方法,详细解释参见《数据类型的转换》一章。 + +```javascript +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' +``` + +两个对象之间的比较也是如此。 + +```javascript +[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`,而相等运算符(`==`)会将它们转换成同一个类型,再用严格相等运算符进行比较。 + +本节介绍严格相等运算符的算法。 + +**(1)不同类型的值** + +如果两个值的类型不同,直接返回`false`。 + +```javascript +1 === "1" // false +true === "true" // false +``` + +上面代码比较数值的`1`与字符串的“1”、布尔值的`true`与字符串`"true"`,因为类型不同,结果都是`false`。 + +**(2)同一类的原始类型值** + +同一类型的原始类型的值(数值、字符串、布尔值)比较时,值相同就返回`true`,值不同就返回`false`。 + +```javascript +1 === 0x1 // true +``` + +上面代码比较十进制的`1`与十六进制的`1`,因为类型和值都相同,返回`true`。 + +需要注意的是,`NaN`与任何值都不相等(包括自身)。另外,正`0`等于负`0`。 + +```javascript +NaN === NaN // false ++0 === -0 // true +``` + +**(3)复合类型值** + +两个复合类型(对象、数组、函数)的数据比较时,不是比较它们的值是否相等,而是比较它们是否指向同一个地址。 + +```javascript +{} === {} // false +[] === [] // false +(function () {} === function () {}) // false +``` + +上面代码分别比较两个空对象、两个空数组、两个空函数,结果都是不相等。原因是对于复合类型的值,严格相等运算比较的是,它们是否引用同一个内存地址,而运算符两边的空对象、空数组、空函数的值,都存放在不同的内存地址,结果当然是`false`。 + +如果两个变量引用同一个对象,则它们相等。 + +```javascript +var v1 = {}; +var v2 = v1; +v1 === v2 // true +``` + +注意,对于两个对象的比较,严格相等运算符比较的是地址,而大于或小于运算符比较的是值。 + +```javascript +var obj1 = {}; +var obj2 = {}; + +obj1 > obj2 // false +obj1 < obj2 // false +obj1 === obj2 // false +``` + +上面的三个比较,前两个比较的是值,最后一个比较的是地址,所以都返回`false`。 + +**(4)undefined 和 null** + +`undefined`和`null`与自身严格相等。 + +```javascript +undefined === undefined // true +null === null // true +``` + +由于变量声明后默认值是`undefined`,因此两个只声明未赋值的变量是相等的。 + +```javascript +var v1; +var v2; +v1 === v2 // true +``` + +## 严格不相等运算符 + +严格相等运算符有一个对应的“严格不相等运算符”(`!==`),它的算法就是先求严格相等运算符的结果,然后返回相反值。 + +```javascript +1 !== '1' // true +// 等同于 +!(1 === '1') +``` + +上面代码中,感叹号`!`是求出后面表达式的相反值。 + +## 相等运算符 + +相等运算符用来比较相同类型的数据时,与严格相等运算符完全一样。 + +```javascript +1 == 1.0 +// 等同于 +1 === 1.0 +``` + +比较不同类型的数据时,相等运算符会先将数据进行类型转换,然后再用严格相等运算符比较。下面分成几种情况,讨论不同类型的值互相比较的规则。 + +**(1)原始类型值** + +原始类型的值会转换成数值再进行比较。 + +```javascript +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 +// 因为字符串转为数字时,省略前置和后置的空格 +``` + +上面代码将字符串和布尔值都转为数值,然后再进行比较。具体的字符串与布尔值的类型转换规则,参见《数据类型转换》一章。 + +**(2)对象与原始类型值比较** + +对象(这里指广义的对象,包括数组和函数)与原始类型的值比较时,对象转换成原始类型的值,再进行比较。 + +具体来说,先调用对象的`valueOf()`方法,如果得到原始类型的值,就按照上一小节的规则,互相比较;如果得到的还是对象,则再调用`toString()`方法,得到字符串形式,再进行比较。 + +下面是数组与原始类型值比较的例子。 + +```javascript +// 数组与数值的比较 +[1] == 1 // true + +// 数组与字符串的比较 +[1] == '1' // true +[1, 2] == '1,2' // true + +// 对象与布尔值的比较 +[1] == true // true +[2] == true // false +``` + +上面例子中,JavaScript 引擎会先对数组`[1]`调用数组的`valueOf()`方法,由于返回的还是一个数组,所以会接着调用数组的`toString()`方法,得到字符串形式,再按照上一小节的规则进行比较。 + +下面是一个更直接的例子。 + +```javascript +const obj = { + valueOf: function () { + console.log('执行 valueOf()'); + return obj; + }, + toString: function () { + console.log('执行 toString()'); + return 'foo'; + } +}; + +obj == 'foo' +// 执行 valueOf() +// 执行 toString() +// true +``` + +上面例子中,`obj`是一个自定义了`valueOf()`和`toString()`方法的对象。这个对象与字符串`'foo'`进行比较时,会依次调用`valueOf()`和`toString()`方法,最后返回`'foo'`,所以比较结果是`true`。 + +**(3)undefined 和 null** + +`undefined`和`null`只有与自身比较,或者互相比较时,才会返回`true`;与其他类型的值比较时,结果都为`false`。 + +```javascript +undefined == undefined // true +null == null // true +undefined == null // true + +false == null // false +false == undefined // false + +0 == null // false +0 == undefined // false +``` + +**(4)相等运算符的缺点** + +相等运算符隐藏的类型转换,会带来一些违反直觉的结果。 + +```javascript +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 +``` + +上面这些表达式都不同于直觉,很容易出错。因此建议不要使用相等运算符(`==`),最好只使用严格相等运算符(`===`)。 + +## 不相等运算符 + +相等运算符有一个对应的“不相等运算符”(`!=`),它的算法就是先求相等运算符的结果,然后返回相反值。 + +```javascript +1 != '1' // false + +// 等同于 +!(1 == '1') +``` + diff --git a/operators/priority.md b/operators/priority.md new file mode 100644 index 0000000..fa5101c --- /dev/null +++ b/operators/priority.md @@ -0,0 +1,217 @@ +# 其他运算符,运算顺序 + +## void 运算符 + +`void`运算符的作用是执行一个表达式,然后不返回任何值,或者说返回`undefined`。 + +```javascript +void 0 // undefined +void(0) // undefined +``` + +上面是`void`运算符的两种写法,都正确。建议采用后一种形式,即总是使用圆括号。因为`void`运算符的优先性很高,如果不使用括号,容易造成错误的结果。比如,`void 4 + 7`实际上等同于`(void 4) + 7`。 + +下面是`void`运算符的一个例子。 + +```javascript +var x = 3; +void (x = 5) //undefined +x // 5 +``` + +这个运算符的主要用途是浏览器的书签工具(Bookmarklet),以及在超级链接中插入代码防止网页跳转。 + +请看下面的代码。 + +```markup + +点击 +``` + +上面代码中,点击链接后,会先执行`onclick`的代码,由于`onclick`返回`false`,所以浏览器不会跳转到 example.com。 + +`void`运算符可以取代上面的写法。 + +```markup +文字 +``` + +下面是一个更实际的例子,用户点击链接提交表单,但是不产生页面跳转。 + +```markup + + 提交 + +``` + +## 逗号运算符 + +逗号运算符用于对两个表达式求值,并返回后一个表达式的值。 + +```javascript +'a', 'b' // "b" + +var x = 0; +var y = (x++, 10); +x // 1 +y // 10 +``` + +上面代码中,逗号运算符返回后一个表达式的值。 + +逗号运算符的一个用途是,在返回一个值之前,进行一些辅助操作。 + +```javascript +var value = (console.log('Hi!'), true); +// Hi! + +value // true +``` + +上面代码中,先执行逗号之前的操作,然后返回逗号后面的值。 + +## 运算顺序 + +### 优先级 + +JavaScript 各种运算符的优先级别(Operator Precedence)是不一样的。优先级高的运算符先执行,优先级低的运算符后执行。 + +```javascript +4 + 5 * 6 // 34 +``` + +上面的代码中,乘法运算符(`*`)的优先性高于加法运算符(`+`),所以先执行乘法,再执行加法,相当于下面这样。 + +```javascript +4 + (5 * 6) // 34 +``` + +如果多个运算符混写在一起,常常会导致令人困惑的代码。 + +```javascript +var x = 1; +var arr = []; + +var y = arr.length <= 0 || arr[0] === undefined ? x : arr[0]; +``` + +上面代码中,变量`y`的值就很难看出来,因为这个表达式涉及5个运算符,到底谁的优先级最高,实在不容易记住。 + +根据语言规格,这五个运算符的优先级从高到低依次为:小于等于(`<=`\)、严格相等(`===`)、或(`||`)、三元(`?:`)、等号(`=`)。因此上面的表达式,实际的运算顺序如下。 + +```javascript +var y = ((arr.length <= 0) || (arr[0] === undefined)) ? x : arr[0]; +``` + +记住所有运算符的优先级,是非常难的,也是没有必要的。 + +### 圆括号的作用 + +圆括号(`()`)可以用来提高运算的优先级,因为它的优先级是最高的,即圆括号中的表达式会第一个运算。 + +```javascript +(4 + 5) * 6 // 54 +``` + +上面代码中,由于使用了圆括号,加法会先于乘法执行。 + +运算符的优先级别十分繁杂,且都是硬性规定,因此建议总是使用圆括号,保证运算顺序清晰可读,这对代码的维护和除错至关重要。 + +顺便说一下,圆括号不是运算符,而是一种语法结构。它一共有两种用法:一种是把表达式放在圆括号之中,提升运算的优先级;另一种是跟在函数的后面,作用是调用函数。 + +注意,因为圆括号不是运算符,所以不具有求值作用,只改变运算的优先级。 + +```javascript +var x = 1; +(x) = 2; +``` + +上面代码的第二行,如果圆括号具有求值作用,那么就会变成`1 = 2`,这是会报错了。但是,上面的代码可以运行,这验证了圆括号只改变优先级,不会求值。 + +这也意味着,如果整个表达式都放在圆括号之中,那么不会有任何效果。 + +```javascript +(expression) +// 等同于 +expression +``` + +函数放在圆括号中,会返回函数本身。如果圆括号紧跟在函数的后面,就表示调用函数。 + +```javascript +function f() { + return 1; +} + +(f) // function f(){return 1;} +f() // 1 +``` + +上面代码中,函数放在圆括号之中会返回函数本身,圆括号跟在函数后面则是调用函数。 + +圆括号之中,只能放置表达式,如果将语句放在圆括号之中,就会报错。 + +```javascript +(var a = 1) +// SyntaxError: Unexpected token var +``` + +### 左结合与右结合 + +对于优先级别相同的运算符,同时出现的时候,就会有计算顺序的问题。 + +```javascript +a OP b OP c +``` + +上面代码中,`OP`表示运算符。它可以有两种解释方式。 + +```javascript +// 方式一 +(a OP b) OP c + +// 方式二 +a OP (b OP c) +``` + +上面的两种方式,得到的计算结果往往是不一样的。方式一是将左侧两个运算数结合在一起,采用这种解释方式的运算符,称为“左结合”(left-to-right associativity)运算符;方式二是将右侧两个运算数结合在一起,这样的运算符称为“右结合”运算符(right-to-left associativity)。 + +JavaScript 语言的大多数运算符是“左结合”,请看下面加法运算符的例子。 + +```javascript +x + y + z + +// 引擎解释如下 +(x + y) + z +``` + +上面代码中,`x`与`y`结合在一起,它们的预算结果再与`z`进行运算。 + +少数运算符是“右结合”,其中最主要的是赋值运算符(`=`)和三元条件运算符(`?:`)。 + +```javascript +w = x = y = z; +q = a ? b : c ? d : e ? f : g; +``` + +上面代码的解释方式如下。 + +```javascript +w = (x = (y = z)); +q = a ? b : (c ? d : (e ? f : g)); +``` + +上面的两行代码,都是右侧的运算数结合在一起。 + +另外,指数运算符(`**`)也是右结合。 + +```javascript +2 ** 3 ** 2 +// 相当于 2 ** (3 ** 2) +// 512 +``` + diff --git a/stdlib.md b/stdlib.md new file mode 100644 index 0000000..60bda40 --- /dev/null +++ b/stdlib.md @@ -0,0 +1,2 @@ +# 标准库 + diff --git a/types/README.md b/types/README.md new file mode 100644 index 0000000..62a00c1 --- /dev/null +++ b/types/README.md @@ -0,0 +1,2 @@ +# 数据类型 + diff --git a/types/array.md b/types/array.md new file mode 100644 index 0000000..ddc56b0 --- /dev/null +++ b/types/array.md @@ -0,0 +1,503 @@ +# 数组 + +## 定义 + +数组(array)是按次序排列的一组值。每个值的位置都有编号(从0开始),整个数组用方括号表示。 + +```javascript +var arr = ['a', 'b', 'c']; +``` + +上面代码中的`a`、`b`、`c`就构成一个数组,两端的方括号是数组的标志。`a`是0号位置,`b`是1号位置,`c`是2号位置。 + +除了在定义时赋值,数组也可以先定义后赋值。 + +```javascript +var arr = []; + +arr[0] = 'a'; +arr[1] = 'b'; +arr[2] = 'c'; +``` + +任何类型的数据,都可以放入数组。 + +```javascript +var arr = [ + {a: 1}, + [1, 2, 3], + function() {return true;} +]; + +arr[0] // Object {a: 1} +arr[1] // [1, 2, 3] +arr[2] // function (){return true;} +``` + +上面数组`arr`的3个成员依次是对象、数组、函数。 + +如果数组的元素还是数组,就形成了多维数组。 + +```javascript +var a = [[1, 2], [3, 4]]; +a[0][1] // 2 +a[1][1] // 4 +``` + +## 数组的本质 + +本质上,数组属于一种特殊的对象。`typeof`运算符会返回数组的类型是`object`。 + +```javascript +typeof [1, 2, 3] // "object" +``` + +上面代码表明,`typeof`运算符认为数组的类型就是对象。 + +数组的特殊性体现在,它的键名是按次序排列的一组整数(0,1,2...)。 + +```javascript +var arr = ['a', 'b', 'c']; + +Object.keys(arr) +// ["0", "1", "2"] +``` + +上面代码中,`Object.keys`方法返回数组的所有键名。可以看到数组的键名就是整数0、1、2。 + +由于数组成员的键名是固定的(默认总是0、1、2...),因此数组不用为每个元素指定键名,而对象的每个成员都必须指定键名。JavaScript 语言规定,对象的键名一律为字符串,所以,数组的键名其实也是字符串。之所以可以用数值读取,是因为非字符串的键名会被转为字符串。 + +```javascript +var arr = ['a', 'b', 'c']; + +arr['0'] // 'a' +arr[0] // 'a' +``` + +上面代码分别用数值和字符串作为键名,结果都能读取数组。原因是数值键名被自动转为了字符串。 + +注意,这点在赋值时也成立。一个值总是先转成字符串,再作为键名进行赋值。 + +```javascript +var a = []; + +a[1.00] = 6; +a[1] // 6 +``` + +上面代码中,由于`1.00`转成字符串是`1`,所以通过数字键`1`可以读取值。 + +上一章说过,对象有两种读取成员的方法:点结构(`object.key`)和方括号结构(`object[key]`)。但是,对于数值的键名,不能使用点结构。 + +```javascript +var arr = [1, 2, 3]; +arr.0 // SyntaxError +``` + +上面代码中,`arr.0`的写法不合法,因为单独的数值不能作为标识符(identifier)。所以,数组成员只能用方括号`arr[0]`表示(方括号是运算符,可以接受数值)。 + +## length 属性 + +数组的`length`属性,返回数组的成员数量。 + +```javascript +['a', 'b', 'c'].length // 3 +``` + +JavaScript 使用一个32位整数,保存数组的元素个数。这意味着,数组成员最多只有 4294967295 个(232 - 1)个,也就是说`length`属性的最大值就是 4294967295。 + +只要是数组,就一定有`length`属性。该属性是一个动态的值,等于键名中的最大整数加上`1`。 + +```javascript +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`设置的值。 + +```javascript +var arr = [ 'a', 'b', 'c' ]; +arr.length // 3 + +arr.length = 2; +arr // ["a", "b"] +``` + +上面代码表示,当数组的`length`属性设为2(即最大的整数键只能是1)那么整数键2(值为`c`)就已经不在数组中了,被自动删除了。 + +清空数组的一个有效方法,就是将`length`属性设为0。 + +```javascript +var arr = [ 'a', 'b', 'c' ]; + +arr.length = 0; +arr // [] +``` + +如果人为设置`length`大于当前元素个数,则数组的成员数量会增加到这个值,新增的位置都是空位。 + +```javascript +var a = ['a']; + +a.length = 3; +a[1] // undefined +``` + +上面代码表示,当`length`属性设为大于数组个数时,读取新增的位置都会返回`undefined`。 + +如果人为设置`length`为不合法的值,JavaScript 会报错。 + +```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`属性的值。 + +```javascript +var a = []; + +a['p'] = 'abc'; +a.length // 0 + +a[2.1] = 'abc'; +a.length // 0 +``` + +上面代码将数组的键分别设为字符串和小数,结果都不影响`length`属性。因为,`length`属性的值就是等于最大的数字键加1,而这个数组没有整数键,所以`length`属性保持为`0`。 + +如果数组的键名是添加超出范围的数值,该键名会自动转为字符串。 + +```javascript +var arr = []; +arr[-1] = 'a'; +arr[Math.pow(2, 32)] = 'b'; + +arr.length // 0 +arr[-1] // "a" +arr[4294967296] // "b" +``` + +上面代码中,我们为数组`arr`添加了两个不合法的数字键,结果`length`属性没有发生变化。这些数字键都变成了字符串键名。最后两行之所以会取到值,是因为取键值时,数字键名会默认转为字符串。 + +## in 运算符 + +检查某个键名是否存在的运算符`in`,适用于对象,也适用于数组。 + +```javascript +var arr = [ 'a', 'b', 'c' ]; +2 in arr // true +'2' in arr // true +4 in arr // false +``` + +上面代码表明,数组存在键名为`2`的键。由于键名都是字符串,所以数值`2`会自动转成字符串。 + +注意,如果数组的某个位置是空位,`in`运算符返回`false`。 + +```javascript +var arr = []; +arr[100] = 'a'; + +100 in arr // true +1 in arr // false +``` + +上面代码中,数组`arr`只有一个成员`arr[100]`,其他位置的键名都会返回`false`。 + +## for...in 循环和数组的遍历 + +`for...in`循环不仅可以遍历对象,也可以遍历数组,毕竟数组只是一种特殊对象。 + +```javascript +var a = [1, 2, 3]; + +for (var i in a) { + console.log(a[i]); +} +// 1 +// 2 +// 3 +``` + +但是,`for...in`不仅会遍历数组所有的数字键,还会遍历非数字键。 + +```javascript +var a = [1, 2, 3]; +a.foo = true; + +for (var key in a) { + console.log(key); +} +// 0 +// 1 +// 2 +// foo +``` + +上面代码在遍历数组时,也遍历到了非整数键`foo`。所以,不推荐使用`for...in`遍历数组。 + +数组的遍历可以考虑使用`for`循环或`while`循环。 + +```javascript +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`方法,也可以用来遍历数组,详见《标准库》的 Array 对象一章。 + +```javascript +var colors = ['red', 'green', 'blue']; +colors.forEach(function (color) { + console.log(color); +}); +// red +// green +// blue +``` + +## 数组的空位 + +当数组的某个位置是空元素,即两个逗号之间没有任何值,我们称该数组存在空位(hole)。 + +```javascript +var a = [1, , 1]; +a.length // 3 +``` + +上面代码表明,数组的空位不影响`length`属性。 + +需要注意的是,如果最后一个元素后面有逗号,并不会产生空位。也就是说,有没有这个逗号,结果都是一样的。 + +```javascript +var a = [1, 2, 3,]; + +a.length // 3 +a // [1, 2, 3] +``` + +上面代码中,数组最后一个成员后面有一个逗号,这不影响`length`属性的值,与没有这个逗号时效果一样。 + +数组的空位是可以读取的,返回`undefined`。 + +```javascript +var a = [, , ,]; +a[1] // undefined +``` + +使用`delete`命令删除一个数组成员,会形成空位,并且不会影响`length`属性。 + +```javascript +var a = [1, 2, 3]; +delete a[1]; + +a[1] // undefined +a.length // 3 +``` + +上面代码用`delete`命令删除了数组的第二个元素,这个位置就形成了空位,但是对`length`属性没有影响。也就是说,`length`属性不过滤空位。所以,使用`length`属性进行数组遍历,一定要非常小心。 + +数组的某个位置是空位,与某个位置是`undefined`,是不一样的。如果是空位,使用数组的`forEach`方法、`for...in`结构、以及`Object.keys`方法进行遍历,空位都会被跳过。 + +```javascript +var a = [, , ,]; + +a.forEach(function (x, i) { + console.log(i + '. ' + x); +}) +// 不产生任何输出 + +for (var i in a) { + console.log(i); +} +// 不产生任何输出 + +Object.keys(a) +// [] +``` + +如果某个位置是`undefined`,遍历的时候就不会被跳过。 + +```javascript +var a = [undefined, undefined, undefined]; + +a.forEach(function (x, i) { + console.log(i + '. ' + x); +}); +// 0. undefined +// 1. undefined +// 2. undefined + +for (var i in a) { + console.log(i); +} +// 0 +// 1 +// 2 + +Object.keys(a) +// ['0', '1', '2'] +``` + +这就是说,空位就是数组没有这个元素,所以不会被遍历到,而`undefined`则表示数组有这个元素,值是`undefined`,所以遍历不会跳过。 + +## 类似数组的对象 + +如果一个对象的所有键名都是正整数或零,并且有`length`属性,那么这个对象就很像数组,语法上称为“类似数组的对象”(array-like object)。 + +```javascript +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`属性不是动态值,不会随着成员的变化而变化。 + +```javascript +var obj = { + length: 0 +}; +obj[3] = 'd'; +obj.length // 0 +``` + +上面代码为对象`obj`添加了一个数字键,但是`length`属性没变。这就说明了`obj`不是数组。 + +典型的“类似数组的对象”是函数的`arguments`对象,以及大多数 DOM 元素集,还有字符串。 + +```javascript +// 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 +``` + +上面代码包含三个例子,它们都不是数组(`instanceof`运算符返回`false`),但是看上去都非常像数组。 + +数组的`slice`方法可以将“类似数组的对象”变成真正的数组。 + +```javascript +var arr = Array.prototype.slice.call(arrayLike); +``` + +除了转为真正的数组,“类似数组的对象”还有一个办法可以使用数组的方法,就是通过`call()`把数组的方法放到对象上面。 + +```javascript +function print(value, index) { + console.log(index + ' : ' + value); +} + +Array.prototype.forEach.call(arrayLike, print); +``` + +上面代码中,`arrayLike`代表一个类似数组的对象,本来是不可以使用数组的`forEach()`方法的,但是通过`call()`,可以把`forEach()`嫁接到`arrayLike`上面调用。 + +下面的例子就是通过这种方法,在`arguments`对象上面调用`forEach`方法。 + +```javascript +// 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`遍历。 + +```javascript +Array.prototype.forEach.call('abc', function (chr) { + console.log(chr); +}); +// a +// b +// c +``` + +注意,这种方法比直接使用数组原生的`forEach`要慢,所以最好还是先将“类似数组的对象”转为真正的数组,然后再直接调用数组的`forEach`方法。 + +```javascript +var arr = Array.prototype.slice.call('abc'); +arr.forEach(function (chr) { + console.log(chr); +}); +// a +// b +// c +``` + +## 参考链接 + +* Axel Rauschmayer, [Arrays in JavaScript](http://www.2ality.com/2012/12/arrays.html) +* Axel Rauschmayer, [JavaScript: sparse arrays vs. dense arrays](http://www.2ality.com/2012/06/dense-arrays.html) +* Felix Bohm, [What They Didn’t Tell You About ES5′s Array Extras](http://net.tutsplus.com/tutorials/javascript-ajax/what-they-didnt-tell-you-about-es5s-array-extras/) +* Juriy Zaytsev, [How ECMAScript 5 still does not allow to subclass an array](http://perfectionkills.com/how-ecmascript-5-still-does-not-allow-to-subclass-an-array/) + diff --git a/types/function.md b/types/function.md new file mode 100644 index 0000000..fccc4c0 --- /dev/null +++ b/types/function.md @@ -0,0 +1,1000 @@ +# 函数 + +函数是一段可以反复调用的代码块。函数还能接受输入的参数,不同的参数会返回不同的值。 + +## 概述 + +### 函数的声明 + +JavaScript 有三种声明函数的方法。 + +**(1)function 命令** + +`function`命令声明的代码区块,就是一个函数。`function`命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面。 + +```javascript +function print(s) { + console.log(s); +} +``` + +上面的代码命名了一个`print`函数,以后使用`print()`这种形式,就可以调用相应的代码。这叫做函数的声明(Function Declaration)。 + +**(2)函数表达式** + +除了用`function`命令声明函数,还可以采用变量赋值的写法。 + +```javascript +var print = function(s) { + console.log(s); +}; +``` + +这种写法将一个匿名函数赋值给变量。这时,这个匿名函数又称函数表达式(Function Expression),因为赋值语句的等号右侧只能放表达式。 + +采用函数表达式声明函数时,`function`命令后面不带有函数名。如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效。 + +```javascript +var print = function x(){ + console.log(typeof x); +}; + +x +// ReferenceError: x is not defined + +print() +// function +``` + +上面代码在函数表达式中,加入了函数名`x`。这个`x`只在函数体内部可用,指代函数表达式本身,其他地方都不可用。这种写法的用处有两个,一是可以在函数体内部调用自身,二是方便除错(除错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数)。因此,下面的形式声明函数也非常常见。 + +```javascript +var f = function f() {}; +``` + +需要注意的是,函数的表达式需要在语句的结尾加上分号,表示语句结束。而函数的声明在结尾的大括号后面不用加分号。总的来说,这两种声明函数的方式,差别很细微,可以近似认为是等价的。 + +**(3)Function 构造函数** + +第三种声明函数的方式是`Function`构造函数。 + +```javascript +var add = new Function( + 'x', + 'y', + 'return x + y' +); + +// 等同于 +function add(x, y) { + return x + y; +} +``` + +上面代码中,`Function`构造函数接受三个参数,除了最后一个参数是`add`函数的“函数体”,其他参数都是`add`函数的参数。 + +你可以传递任意数量的参数给`Function`构造函数,只有最后一个参数会被当做函数体,如果只有一个参数,该参数就是函数体。 + +```javascript +var foo = new Function( + 'return "hello world";' +); + +// 等同于 +function foo() { + return 'hello world'; +} +``` + +`Function`构造函数可以不使用`new`命令,返回结果完全一样。 + +总的来说,这种声明函数的方式非常不直观,几乎无人使用。 + +### 函数的重复声明 + +如果同一个函数被多次声明,后面的声明就会覆盖前面的声明。 + +```javascript +function f() { + console.log(1); +} +f() // 2 + +function f() { + console.log(2); +} +f() // 2 +``` + +上面代码中,后一次的函数声明覆盖了前面一次。而且,由于函数名的提升(参见下文),前一次声明在任何时候都是无效的,这一点要特别注意。 + +### 圆括号运算符,return 语句和递归 + +调用函数时,要使用圆括号运算符。圆括号之中,可以加入函数的参数。 + +```javascript +function add(x, y) { + return x + y; +} + +add(1, 1) // 2 +``` + +上面代码中,函数名后面紧跟一对圆括号,就会调用这个函数。 + +函数体内部的`return`语句,表示返回。JavaScript 引擎遇到`return`语句,就直接返回`return`后面的那个表达式的值,后面即使还有语句,也不会得到执行。也就是说,`return`语句所带的那个表达式,就是函数的返回值。`return`语句不是必需的,如果没有的话,该函数就不返回任何值,或者说返回`undefined`。 + +函数可以调用自身,这就是递归(recursion)。下面就是通过递归,计算斐波那契数列的代码。 + +```javascript +function fib(num) { + if (num === 0) return 0; + if (num === 1) return 1; + return fib(num - 2) + fib(num - 1); +} + +fib(6) // 8 +``` + +上面代码中,`fib`函数内部又调用了`fib`,计算得到斐波那契数列的第6个元素是8。 + +### 第一等公民 + +JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。函数只是一个可以执行的值,此外并无特殊之处。 + +由于函数与其他数据类型地位平等,所以在 JavaScript 语言中又称函数为第一等公民。 + +```javascript +function add(x, y) { + return x + y; +} + +// 将函数赋值给一个变量 +var operator = add; + +// 将函数作为参数和返回值 +function a(op){ + return op; +} +a(add)(1, 1) +// 2 +``` + +### 函数名的提升 + +JavaScript 引擎将函数名视同变量名,所以采用`function`命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。所以,下面的代码不会报错。 + +```javascript +f(); + +function f() {} +``` + +表面上,上面代码好像在声明之前就调用了函数`f`。但是实际上,由于“变量提升”,函数`f`被提升到了代码头部,也就是在调用之前已经声明了。但是,如果采用赋值语句定义函数,JavaScript 就会报错。 + +```javascript +f(); +var f = function (){}; +// TypeError: undefined is not a function +``` + +上面的代码等同于下面的形式。 + +```javascript +var f; +f(); +f = function () {}; +``` + +上面代码第二行,调用`f`的时候,`f`只是被声明了,还没有被赋值,等于`undefined`,所以会报错。 + +注意,如果像下面例子那样,采用`function`命令和`var`赋值语句声明同一个函数,由于存在函数提升,最后会采用`var`赋值语句的定义。 + +```javascript +var f = function () { + console.log('1'); +} + +function f() { + console.log('2'); +} + +f() // 1 +``` + +上面例子中,表面上后面声明的函数`f`,应该覆盖前面的`var`赋值语句,但是由于存在函数提升,实际上正好反过来。 + +## 函数的属性和方法 + +### name 属性 + +函数的`name`属性返回函数的名字。 + +```javascript +function f1() {} +f1.name // "f1" +``` + +如果是通过变量赋值定义的函数,那么`name`属性返回变量名。 + +```javascript +var f2 = function () {}; +f2.name // "f2" +``` + +但是,上面这种情况,只有在变量的值是一个匿名函数时才是如此。如果变量的值是一个具名函数,那么`name`属性返回`function`关键字之后的那个函数名。 + +```javascript +var f3 = function myName() {}; +f3.name // 'myName' +``` + +上面代码中,`f3.name`返回函数表达式的名字。注意,真正的函数名还是`f3`,而`myName`这个名字只在函数体内部可用。 + +`name`属性的一个用处,就是获取参数函数的名字。 + +```javascript +var myFunc = function () {}; + +function test(f) { + console.log(f.name); +} + +test(myFunc) // myFunc +``` + +上面代码中,函数`test`内部通过`name`属性,就可以知道传入的参数是什么函数。 + +### length 属性 + +函数的`length`属性返回函数预期传入的参数个数,即函数定义之中的参数个数。 + +```javascript +function f(a, b) {} +f.length // 2 +``` + +上面代码定义了空函数`f`,它的`length`属性就是定义时的参数个数。不管调用时输入了多少个参数,`length`属性始终等于2。 + +`length`属性提供了一种机制,判断定义时和调用时参数的差异,以便实现面向对象编程的“方法重载”(overload)。 + +### toString\(\) + +函数的`toString()`方法返回一个字符串,内容是函数的源码。 + +```javascript +function f() { + a(); + b(); + c(); +} + +f.toString() +// function f() { +// a(); +// b(); +// c(); +// } +``` + +上面示例中,函数`f`的`toString()`方法返回了`f`的源码,包含换行符在内。 + +对于那些原生的函数,`toString()`方法返回`function (){[native code]}`。 + +```javascript +Math.sqrt.toString() +// "function sqrt() { [native code] }" +``` + +上面代码中,`Math.sqrt()`是 JavaScript 引擎提供的原生函数,`toString()`方法就返回原生代码的提示。 + +函数内部的注释也可以返回。 + +```javascript +function f() {/* + 这是一个 + 多行注释 +*/} + +f.toString() +// "function f(){/* +// 这是一个 +// 多行注释 +// */}" +``` + +利用这一点,可以变相实现多行字符串。 + +```javascript +var multiline = function (fn) { + var arr = fn.toString().split('\n'); + return arr.slice(1, arr.length - 1).join('\n'); +}; + +function f() {/* + 这是一个 + 多行注释 +*/} + +multiline(f); +// " 这是一个 +// 多行注释" +``` + +上面示例中,函数`f`内部有一个多行注释,`toString()`方法拿到`f`的源码后,去掉首尾两行,就得到了一个多行字符串。 + +## 函数作用域 + +### 定义 + +作用域(scope)指的是变量存在的范围。在 ES5 的规范中,JavaScript 只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;另一种是函数作用域,变量只在函数内部存在。ES6 又新增了块级作用域,本教程不涉及。 + +对于顶层函数来说,函数外部声明的变量就是全局变量(global variable),它可以在函数内部读取。 + +```javascript +var v = 1; + +function f() { + console.log(v); +} + +f() +// 1 +``` + +上面的代码表明,函数`f`内部可以读取全局变量`v`。 + +在函数内部定义的变量,外部无法读取,称为“局部变量”(local variable)。 + +```javascript +function f(){ + var v = 1; +} + +v // ReferenceError: v is not defined +``` + +上面代码中,变量`v`在函数内部定义,所以是一个局部变量,函数之外就无法读取。 + +函数内部定义的变量,会在该作用域内覆盖同名全局变量。 + +```javascript +var v = 1; + +function f(){ + var v = 2; + console.log(v); +} + +f() // 2 +v // 1 +``` + +上面代码中,变量`v`同时在函数的外部和内部有定义。结果,在函数内部定义,局部变量`v`覆盖了全局变量`v`。 + +注意,对于`var`命令来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量。 + +```javascript +if (true) { + var x = 5; +} +console.log(x); // 5 +``` + +上面代码中,变量`x`在条件判断区块之中声明,结果就是一个全局变量,可以在区块之外读取。 + +### 函数内部的变量提升 + +与全局作用域一样,函数作用域内部也会产生“变量提升”现象。`var`命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。 + +```javascript +function foo(x) { + if (x > 100) { + var tmp = x - 100; + } +} + +// 等同于 +function foo(x) { + var tmp; + if (x > 100) { + tmp = x - 100; + }; +} +``` + +### 函数本身的作用域 + +函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关。 + +```javascript +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`的内部变量。 + +```javascript +var x = function () { + console.log(a); +}; + +function y(f) { + var a = 2; + f(); +} + +y(x) +// ReferenceError: a is not defined +``` + +上面代码将函数`x`作为参数,传入函数`y`。但是,函数`x`是在函数`y`体外声明的,作用域绑定外层,因此找不到函数`y`的内部变量`a`,导致报错。 + +同样的,函数体内部声明的函数,作用域绑定函数体内部。 + +```javascript +function foo() { + var x = 1; + function bar() { + console.log(x); + } + return bar; +} + +var x = 2; +var f = foo(); +f() // 1 +``` + +上面代码中,函数`foo`内部声明了一个函数`bar`,`bar`的作用域绑定`foo`。当我们在`foo`外部取出`bar`执行时,变量`x`指向的是`foo`内部的`x`,而不是`foo`外部的`x`。正是这种机制,构成了下文要讲解的“闭包”现象。 + +## 参数 + +### 概述 + +函数运行的时候,有时需要提供外部数据,不同的外部数据会得到不同的结果,这种外部数据就叫参数。 + +```javascript +function square(x) { + return x * x; +} + +square(2) // 4 +square(3) // 9 +``` + +上式的`x`就是`square`函数的参数。每次运行的时候,需要提供这个值,否则得不到结果。 + +### 参数的省略 + +函数参数不是必需的,JavaScript 允许省略参数。 + +```javascript +function f(a, b) { + return a; +} + +f(1, 2, 3) // 1 +f(1) // 1 +f() // undefined + +f.length // 2 +``` + +上面代码的函数`f`定义了两个参数,但是运行时无论提供多少个参数(或者不提供参数),JavaScript 都不会报错。省略的参数的值就变为`undefined`。需要注意的是,函数的`length`属性与实际传入的参数个数无关,只反映函数预期传入的参数个数。 + +但是,没有办法只省略靠前的参数,而保留靠后的参数。如果一定要省略靠前的参数,只有显式传入`undefined`。 + +```javascript +function f(a, b) { + return a; +} + +f( , 1) // SyntaxError: Unexpected token ,(…) +f(undefined, 1) // undefined +``` + +上面代码中,如果省略第一个参数,就会报错。 + +### 传递方式 + +函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)。这意味着,在函数体内修改参数值,不会影响到函数外部。 + +```javascript +var p = 2; + +function f(p) { + p = 3; +} +f(p); + +p // 2 +``` + +上面代码中,变量`p`是一个原始类型的值,传入函数`f`的方式是传值传递。因此,在函数内部,`p`的值是原始值的拷贝,无论怎么修改,都不会影响到原始值。 + +但是,如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。 + +```javascript +var obj = { p: 1 }; + +function f(o) { + o.p = 2; +} +f(obj); + +obj.p // 2 +``` + +上面代码中,传入函数`f`的是参数对象`obj`的地址。因此,在函数内部修改`obj`的属性`p`,会影响到原始值。 + +注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。 + +```javascript +var obj = [1, 2, 3]; + +function f(o) { + o = [2, 3, 4]; +} +f(obj); + +obj // [1, 2, 3] +``` + +上面代码中,在函数`f()`内部,参数对象`obj`被整个替换成另一个值。这时不会影响到原始值。这是因为,形式参数(`o`)的值实际是参数`obj`的地址,重新对`o`赋值导致`o`指向另一个地址,保存在原地址上的值当然不受影响。 + +### 同名参数 + +如果有同名的参数,则取最后出现的那个值。 + +```javascript +function f(a, a) { + console.log(a); +} + +f(1, 2) // 2 +``` + +上面代码中,函数`f()`有两个参数,且参数名都是`a`。取值的时候,以后面的`a`为准,即使后面的`a`没有值或被省略,也是以其为准。 + +```javascript +function f(a, a) { + console.log(a); +} + +f(1) // undefined +``` + +调用函数`f()`的时候,没有提供第二个参数,`a`的取值就变成了`undefined`。这时,如果要获得第一个`a`的值,可以使用`arguments`对象。 + +```javascript +function f(a, a) { + console.log(arguments[0]); +} + +f(1) // 1 +``` + +### arguments 对象 + +**(1)定义** + +由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是`arguments`对象的由来。 + +`arguments`对象包含了函数运行时的所有参数,`arguments[0]`就是第一个参数,`arguments[1]`就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。 + +```javascript +var f = function (one) { + console.log(arguments[0]); + console.log(arguments[1]); + console.log(arguments[2]); +} + +f(1, 2, 3) +// 1 +// 2 +// 3 +``` + +正常模式下,`arguments`对象可以在运行时修改。 + +```javascript +var f = function(a, b) { + arguments[0] = 3; + arguments[1] = 2; + return a + b; +} + +f(1, 1) // 5 +``` + +上面代码中,函数`f()`调用时传入的参数,在函数内部被修改成`3`和`2`。 + +严格模式下,`arguments`对象与函数参数不具有联动关系。也就是说,修改`arguments`对象不会影响到实际的函数参数。 + +```javascript +var f = function(a, b) { + 'use strict'; // 开启严格模式 + arguments[0] = 3; + arguments[1] = 2; + return a + b; +} + +f(1, 1) // 2 +``` + +上面代码中,函数体内是严格模式,这时修改`arguments`对象,不会影响到真实参数`a`和`b`。 + +通过`arguments`对象的`length`属性,可以判断函数调用时到底带几个参数。 + +```javascript +function f() { + return arguments.length; +} + +f(1, 2, 3) // 3 +f(1) // 1 +f() // 0 +``` + +**(2)与数组的关系** + +需要注意的是,虽然`arguments`很像数组,但它是一个对象。数组专有的方法(比如`slice`和`forEach`),不能在`arguments`对象上直接使用。 + +如果要让`arguments`对象使用数组方法,真正的解决方法是将`arguments`转为真正的数组。下面是两种常用的转换方法:`slice`方法和逐一填入新数组。 + +```javascript +var args = Array.prototype.slice.call(arguments); + +// 或者 +var args = []; +for (var i = 0; i < arguments.length; i++) { + args.push(arguments[i]); +} +``` + +**(3)callee 属性** + +`arguments`对象带有一个`callee`属性,返回它所对应的原函数。 + +```javascript +var f = function () { + console.log(arguments.callee === f); +} + +f() // true +``` + +可以通过`arguments.callee`,达到调用函数自身的目的。这个属性在严格模式里面是禁用的,因此不建议使用。 + +## 函数的其他知识点 + +### 闭包 + +闭包(closure)是 JavaScript 语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。 + +理解闭包,首先必须理解变量作用域。前面提到,JavaScript 有两种作用域:全局作用域和函数作用域。函数内部可以直接读取全局变量。 + +```javascript +var n = 999; + +function f1() { + console.log(n); +} +f1() // 999 +``` + +上面代码中,函数`f1`可以读取全局变量`n`。 + +但是,正常情况下,函数外部无法读取函数内部声明的变量。 + +```javascript +function f1() { + var n = 999; +} + +console.log(n) +// Uncaught ReferenceError: n is not defined( +``` + +上面代码中,函数`f1`内部声明的变量`n`,函数外是无法读取的。 + +如果出于种种原因,需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数。 + +```javascript +function f1() { + var n = 999; + function f2() { +  console.log(n); // 999 + } +} +``` + +上面代码中,函数`f2`就在函数`f1`内部,这时`f1`内部的所有局部变量,对`f2`都是可见的。但是反过来就不行,`f2`内部的局部变量,对`f1`就是不可见的。这就是 JavaScript 语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。 + +既然`f2`可以读取`f1`的局部变量,那么只要把`f2`作为返回值,我们不就可以在`f1`外部读取它的内部变量了吗! + +```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`的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。 + +闭包的最大用处有两个,一个是可以读取外层函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。 + +```javascript +function createIncrementor(start) { + return function () { + return start++; + }; +} + +var inc = createIncrementor(5); + +inc() // 5 +inc() // 6 +inc() // 7 +``` + +上面代码中,`start`是函数`createIncrementor`的内部变量。通过闭包,`start`的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包`inc`使得函数`createIncrementor`的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。 + +为什么闭包能够返回外层函数的内部变量?原因是闭包(上例的`inc`)用到了外层变量(`start`),导致外层函数(`createIncrementor`)不能从内存释放。只要闭包没有被垃圾回收机制清除,外层函数提供的运行环境也不会被清除,它的内部变量就始终保存着当前值,供闭包读取。 + +闭包的另一个用处,是封装对象的私有属性和私有方法。 + +```javascript +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`函数。 + +有时,我们需要在定义函数之后,立即调用该函数。这时,你不能在函数的定义之后加上圆括号,这会产生语法错误。 + +```javascript +function(){ /* code */ }(); +// SyntaxError: Unexpected token ( +``` + +产生这个错误的原因是,`function`这个关键字既可以当作语句,也可以当作表达式。 + +```javascript +// 语句 +function f() {} + +// 表达式 +var f = function f() {} +``` + +当作表达式时,函数可以定义后直接加圆括号调用。 + +```javascript +var f = function f(){ return 1}(); +f // 1 +``` + +上面的代码中,函数定义后直接加圆括号调用,没有报错。原因就是`function`作为表达式,引擎就把函数定义当作一个值。这种情况下,就不会报错。 + +为了避免解析的歧义,JavaScript 规定,如果`function`关键字出现在行首,一律解释成语句。因此,引擎看到行首是`function`关键字之后,认为这一段都是函数的定义,不应该以圆括号结尾,所以就报错了。 + +函数定义后立即调用的解决方法,就是不要让`function`出现在行首,让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面。 + +```javascript +(function(){ /* code */ }()); +// 或者 +(function(){ /* code */ })(); +``` + +上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表达式,而不是函数定义语句,所以就避免了错误。这就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称 IIFE。 + +注意,上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个 IIFE,可能就会报错。 + +```javascript +// 报错 +(function(){ /* code */ }()) +(function(){ /* code */ }()) +``` + +上面代码的两行之间没有分号,JavaScript 会将它们连在一起解释,将第二行解释为第一行的参数。 + +推而广之,任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果,比如下面三种写法。 + +```javascript +var i = function(){ return 10; }(); +true && function(){ /* code */ }(); +0, function(){ /* code */ }(); +``` + +甚至像下面这样写,也是可以的。 + +```javascript +!function () { /* code */ }(); +~function () { /* code */ }(); +-function () { /* code */ }(); ++function () { /* code */ }(); +``` + +通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:一是不必为函数命名,避免了污染全局变量;二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。 + +```javascript +// 写法一 +var tmp = newData; +processData(tmp); +storeData(tmp); + +// 写法二 +(function () { + var tmp = newData; + processData(tmp); + storeData(tmp); +}()); +``` + +上面代码中,写法二比写法一更好,因为完全避免了污染全局变量。 + +## eval 命令 + +### 基本用法 + +`eval`命令接受一个字符串作为参数,并将这个字符串当作语句执行。 + +```javascript +eval('var a = 1;'); +a // 1 +``` + +上面代码将字符串当作语句运行,生成了变量`a`。 + +如果参数字符串无法当作语句运行,那么就会报错。 + +```javascript +eval('3x') // Uncaught SyntaxError: Invalid or unexpected token +``` + +放在`eval`中的字符串,应该有独自存在的意义,不能用来与`eval`以外的命令配合使用。举例来说,下面的代码将会报错。 + +```javascript +eval('return;'); // Uncaught SyntaxError: Illegal return statement +``` + +上面代码会报错,因为`return`不能单独使用,必须在函数中使用。 + +如果`eval`的参数不是字符串,那么会原样返回。 + +```javascript +eval(123) // 123 +``` + +`eval`没有自己的作用域,都在当前作用域内执行,因此可能会修改当前作用域的变量的值,造成安全问题。 + +```javascript +var a = 1; +eval('a = 2'); + +a // 2 +``` + +上面代码中,`eval`命令修改了外部变量`a`的值。由于这个原因,`eval`有安全风险。 + +为了防止这种风险,JavaScript 规定,如果使用严格模式,`eval`内部声明的变量,不会影响到外部作用域。 + +```javascript +(function f() { + 'use strict'; + eval('var foo = 123'); + console.log(foo); // ReferenceError: foo is not defined +})() +``` + +上面代码中,函数`f`内部是严格模式,这时`eval`内部声明的`foo`变量,就不会影响到外部。 + +不过,即使在严格模式下,`eval`依然可以读写当前作用域的变量。 + +```javascript +(function f() { + 'use strict'; + var foo = 1; + eval('foo = 2'); + console.log(foo); // 2 +})() +``` + +上面代码中,严格模式下,`eval`内部还是改写了外部变量,可见安全风险依然存在。 + +总之,`eval`的本质是在当前作用域之中,注入代码。由于安全风险和不利于 JavaScript 引擎优化执行速度,一般不推荐使用。通常情况下,`eval`最常见的场合是解析 JSON 数据的字符串,不过正确的做法应该是使用原生的`JSON.parse`方法。 + +### eval 的别名调用 + +前面说过`eval`不利于引擎优化执行速度。更麻烦的是,还有下面这种情况,引擎在静态代码分析的阶段,根本无法分辨执行的是`eval`。 + +```javascript +var m = eval; +m('var x = 1'); +x // 1 +``` + +上面代码中,变量`m`是`eval`的别名。静态代码分析阶段,引擎分辨不出`m('var x = 1')`执行的是`eval`命令。 + +为了保证`eval`的别名不影响代码优化,JavaScript 的标准规定,凡是使用别名执行`eval`,`eval`内部一律是全局作用域。 + +```javascript +var a = 1; + +function f() { + var a = 2; + var e = eval; + e('console.log(a)'); +} + +f() // 1 +``` + +上面代码中,`eval`是别名调用,所以即使它是在函数中,它的作用域还是全局作用域,因此输出的`a`为全局变量。这样的话,引擎就能确认`e()`不会对当前的函数作用域产生影响,优化的时候就可以把这一行排除掉。 + +`eval`的别名调用的形式五花八门,只要不是直接调用,都属于别名调用,因为引擎只能分辨`eval()`这一种形式是直接调用。 + +```javascript +eval.call(null, '...') +window.eval('...') +(1, eval)('...') +(eval, eval)('...') +``` + +上面这些形式都是`eval`的别名调用,作用域都是全局作用域。 + +## 参考链接 + +* Ben Alman, [Immediately-Invoked Function Expression \(IIFE\)](http://benalman.com/news/2010/11/immediately-invoked-function-expression/) +* Mark Daggett, [Functions Explained](http://markdaggett.com/blog/2013/02/15/functions-explained/) +* Juriy Zaytsev, [Named function expressions demystified](http://kangax.github.com/nfe/) +* Marco Rogers polotek, [What is the arguments object?](http://docs.nodejitsu.com/articles/javascript-conventions/what-is-the-arguments-object) +* Juriy Zaytsev, [Global eval. What are the options?](http://perfectionkills.com/global-eval-what-are-the-options/) +* Axel Rauschmayer, [Evaluating JavaScript code via eval\(\) and new Function\(\)](http://www.2ality.com/2014/01/eval.html) + diff --git a/types/general.md b/types/general.md new file mode 100644 index 0000000..9aee624 --- /dev/null +++ b/types/general.md @@ -0,0 +1,115 @@ +# 概述 + +## 简介 + +JavaScript 语言的每一个值,都属于某一种数据类型。JavaScript 的数据类型,共有六种。(ES6 又新增了第七种 Symbol 类型的值,本教程不涉及。) + +* 数值(number):整数和小数(比如`1`和`3.14`)。 +* 字符串(string):文本(比如`Hello World`)。 +* 布尔值(boolean):表示真伪的两个特殊值,即`true`(真)和`false`(假)。 +* `undefined`:表示“未定义”或不存在,即由于目前没有定义,所以此处暂时没有任何值。 +* `null`:表示空值,即此处的值为空。 +* 对象(object):各种值组成的集合。 + +通常,数值、字符串、布尔值这三种类型,合称为原始类型(primitive type)的值,即它们是最基本的数据类型,不能再细分了。对象则称为合成类型(complex type)的值,因为一个对象往往是多个原始类型的值的合成,可以看作是一个存放各种值的容器。至于`undefined`和`null`,一般将它们看成两个特殊值。 + +对象是最复杂的数据类型,又可以分成三个子类型。 + +* 狭义的对象(object) +* 数组(array) +* 函数(function) + +狭义的对象和数组是两种不同的数据组合方式,除非特别声明,本教程的“对象”都特指狭义的对象。函数其实是处理数据的方法,JavaScript 把它当成一种数据类型,可以赋值给变量,这为编程带来了很大的灵活性,也为 JavaScript 的“函数式编程”奠定了基础。 + +## typeof 运算符 + +JavaScript 有三种方法,可以确定一个值到底是什么类型。 + +* `typeof`运算符 +* `instanceof`运算符 +* `Object.prototype.toString`方法 + +`instanceof`运算符和`Object.prototype.toString`方法,将在后文介绍。这里介绍`typeof`运算符。 + +`typeof`运算符可以返回一个值的数据类型。 + +数值、字符串、布尔值分别返回`number`、`string`、`boolean`。 + +```javascript +typeof 123 // "number" +typeof '123' // "string" +typeof false // "boolean" +``` + +函数返回`function`。 + +```javascript +function f() {} +typeof f +// "function" +``` + +`undefined`返回`undefined`。 + +```javascript +typeof undefined +// "undefined" +``` + +利用这一点,`typeof`可以用来检查一个没有声明的变量,而不报错。 + +```javascript +v +// ReferenceError: v is not defined + +typeof v +// "undefined" +``` + +上面代码中,变量`v`没有用`var`命令声明,直接使用就会报错。但是,放在`typeof`后面,就不报错了,而是返回`undefined`。 + +实际编程中,这个特点通常用在判断语句。 + +```javascript +// 错误的写法 +if (v) { + // ... +} +// ReferenceError: v is not defined + +// 正确的写法 +if (typeof v === "undefined") { + // ... +} +``` + +对象返回`object`。 + +```javascript +typeof window // "object" +typeof {} // "object" +typeof [] // "object" +``` + +上面代码中,空数组(`[]`)的类型也是`object`,这表示在 JavaScript 内部,数组本质上只是一种特殊的对象。这里顺便提一下,`instanceof`运算符可以区分数组和对象。`instanceof`运算符的详细解释,请见《面向对象编程》一章。 + +```javascript +var o = {}; +var a = []; + +o instanceof Array // false +a instanceof Array // true +``` + +`null`返回`object`。 + +```javascript +typeof null // "object" +``` + +`null`的类型是`object`,这是由于历史原因造成的。1995年的 JavaScript 语言第一版,只设计了五种数据类型(对象、整数、浮点数、字符串和布尔值),没考虑`null`,只把它当作`object`的一种特殊值。后来`null`独立出来,作为一种单独的数据类型,为了兼容以前的代码,`typeof null`返回`object`就没法改变了。 + +## 参考链接 + +* Axel Rauschmayer, [Improving the JavaScript typeof operator](http://www.2ality.com/2011/11/improving-typeof.html) + diff --git a/types/null-undefined-boolean.md b/types/null-undefined-boolean.md new file mode 100644 index 0000000..8298097 --- /dev/null +++ b/types/null-undefined-boolean.md @@ -0,0 +1,133 @@ +# null, undefined 和布尔值 + +## null 和 undefined + +### 概述 + +`null`与`undefined`都可以表示“没有”,含义非常相似。将一个变量赋值为`undefined`或`null`,老实说,语法效果几乎没区别。 + +```javascript +var a = undefined; +// 或者 +var a = null; +``` + +上面代码中,变量`a`分别被赋值为`undefined`和`null`,这两种写法的效果几乎等价。 + +在`if`语句中,它们都会被自动转为`false`,相等运算符(`==`)甚至直接报告两者相等。 + +```javascript +if (!undefined) { + console.log('undefined is false'); +} +// undefined is false + +if (!null) { + console.log('null is false'); +} +// null is false + +undefined == null +// true +``` + +从上面代码可见,两者的行为是何等相似!谷歌公司开发的 JavaScript 语言的替代品 Dart 语言,就明确规定只有`null`,没有`undefined`! + +既然含义与用法都差不多,为什么要同时设置两个这样的值,这不是无端增加复杂度,令初学者困扰吗?这与历史原因有关。 + +1995年 JavaScript 诞生时,最初像 Java 一样,只设置了`null`表示"无"。根据 C 语言的传统,`null`可以自动转为`0`。 + +```javascript +Number(null) // 0 +5 + null // 5 +``` + +上面代码中,`null`转为数字时,自动变成0。 + +但是,JavaScript 的设计者 Brendan Eich,觉得这样做还不够。首先,第一版的 JavaScript 里面,`null`就像在 Java 里一样,被当成一个对象,Brendan Eich 觉得表示“无”的值最好不是对象。其次,那时的 JavaScript 不包括错误处理机制,Brendan Eich 觉得,如果`null`自动转为0,很不容易发现错误。 + +因此,他又设计了一个`undefined`。区别是这样的:`null`是一个表示“空”的对象,转为数值时为`0`;`undefined`是一个表示"此处无定义"的原始值,转为数值时为`NaN`。 + +```javascript +Number(undefined) // NaN +5 + undefined // NaN +``` + +### 用法和含义 + +对于`null`和`undefined`,大致可以像下面这样理解。 + +`null`表示空值,即该处的值现在为空。调用函数时,某个参数未设置任何值,这时就可以传入`null`,表示该参数为空。比如,某个函数接受引擎抛出的错误作为参数,如果运行过程中未出错,那么这个参数就会传入`null`,表示未发生错误。 + +`undefined`表示“未定义”,下面是返回`undefined`的典型场景。 + +```javascript +// 变量声明了,但没有赋值 +var i; +i // undefined + +// 调用函数时,应该提供的参数没有提供,该参数等于 undefined +function f(x) { + return x; +} +f() // undefined + +// 对象没有赋值的属性 +var o = new Object(); +o.p // undefined + +// 函数没有返回值时,默认返回 undefined +function f() {} +f() // undefined +``` + +## 布尔值 + +布尔值代表“真”和“假”两个状态。“真”用关键字`true`表示,“假”用关键字`false`表示。布尔值只有这两个值。 + +下列运算符会返回布尔值: + +* 前置逻辑运算符: `!` \(Not\) +* 相等运算符:`===`,`!==`,`==`,`!=` +* 比较运算符:`>`,`>=`,`<`,`<=` + +如果 JavaScript 预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是除了下面六个值被转为`false`,其他值都视为`true`。 + +* `undefined` +* `null` +* `false` +* `0` +* `NaN` +* `""`或`''`(空字符串) + +布尔值往往用于程序流程的控制,请看一个例子。 + +```javascript +if ('') { + console.log('true'); +} +// 没有任何输出 +``` + +上面代码中,`if`命令后面的判断条件,预期应该是一个布尔值,所以 JavaScript 自动将空字符串,转为布尔值`false`,导致程序不会进入代码块,所以没有任何输出。 + +注意,空数组(`[]`)和空对象(`{}`)对应的布尔值,都是`true`。 + +```javascript +if ([]) { + console.log('true'); +} +// true + +if ({}) { + console.log('true'); +} +// true +``` + +更多关于数据类型转换的介绍,参见《数据类型转换》一章。 + +## 参考链接 + +* Axel Rauschmayer, [Categorizing values in JavaScript](http://www.2ality.com/2013/01/categorizing-values.html) + diff --git a/types/number.md b/types/number.md new file mode 100644 index 0000000..4a50578 --- /dev/null +++ b/types/number.md @@ -0,0 +1,655 @@ +# 数值 + +## 概述 + +### 整数和浮点数 + +JavaScript 内部,所有数字都是以64位浮点数形式储存,即使整数也是如此。所以,`1`与`1.0`是相同的,是同一个数。 + +```javascript +1 === 1.0 // true +``` + +这就是说,JavaScript 语言的底层根本没有整数,所有数字都是小数(64位浮点数)。容易造成混淆的是,某些运算只有整数才能完成,此时 JavaScript 会自动把64位浮点数,转成32位整数,然后再进行运算,参见《运算符》一章的“位运算”部分。 + +由于浮点数不是精确的值,所以涉及小数的比较和运算要特别小心。 + +```javascript +0.1 + 0.2 === 0.3 +// false + +0.3 / 0.1 +// 2.9999999999999996 + +(0.3 - 0.2) === (0.2 - 0.1) +// false +``` + +### 数值精度 + +根据国际标准 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个二进制位。 + +```text +(-1)^符号位 * 1.xx...xx * 2^指数部分 +``` + +上面公式是正常情况下(指数部分在0到2047之间),一个数在 JavaScript 内部实际的表示形式。 + +精度最多只能到53个二进制位,这意味着,绝对值小于2的53次方的整数,即-253到253,都可以精确表示。 + +```javascript +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位的十进制数都可以精确处理。 + +```javascript +Math.pow(2, 53) +// 9007199254740992 + +// 多出的三个有效数字,将无法保存 +9007199254740992111 +// 9007199254740992000 +``` + +上面示例表明,大于2的53次方以后,多出来的有效数字(最后三位的`111`)都会无法保存,变成0。 + +### 数值范围 + +根据标准,64位浮点数的指数部分的长度是11个二进制位,意味着指数部分的最大值是2047(2的11次方减1)。也就是说,64位浮点数的指数部分的值最大为2047,分出一半表示负数,则 JavaScript 能够表示的数值范围为21024到2-1023(开区间),超出这个范围的数无法表示。 + +如果一个数大于等于2的1024次方,那么就会发生“正向溢出”,即 JavaScript 无法表示这么大的数,这时就会返回`Infinity`。 + +```javascript +Math.pow(2, 1024) // Infinity +``` + +如果一个数小于等于2的-1075次方(指数部分最小值-1023,再加上小数部分的52位),那么就会发生为“负向溢出”,即 JavaScript 无法表示这么小的数,这时会直接返回0。 + +```javascript +Math.pow(2, -1075) // 0 +``` + +下面是一个实际的例子。 + +```javascript +var x = 0.5; + +for(var i = 0; i < 25; i++) { + x = x * x; +} + +x // 0 +``` + +上面代码中,对`0.5`连续做25次平方,由于最后结果太接近0,超出了可表示的范围,JavaScript 就直接将其转为0。 + +JavaScript 提供`Number`对象的`MAX_VALUE`和`MIN_VALUE`属性,返回可以表示的具体的最大值和最小值。 + +```javascript +Number.MAX_VALUE // 1.7976931348623157e+308 +Number.MIN_VALUE // 5e-324 +``` + +## 数值的表示法 + +JavaScript 的数值有多种表示方法,可以用字面形式直接表示,比如`35`(十进制)和`0xFF`(十六进制)。 + +数值也可以采用科学计数法表示,下面是几个科学计数法的例子。 + +```javascript +123e3 // 123000 +123e-3 // 0.123 +-3.1E+12 +.1e-23 +``` + +科学计数法允许字母`e`或`E`的后面,跟着一个整数,表示这个数值的指数部分。 + +以下两种情况,JavaScript 会自动将数值转为科学计数法表示,其他情况都采用字面形式直接表示。 + +**(1)小数点前的数字多于21位。** + +```javascript +1234567890123456789012 +// 1.2345678901234568e+21 + +123456789012345678901 +// 123456789012345680000 +``` + +**(2)小数点后的零多于5个。** + +```javascript +// 小数点后紧跟5个以上的零, +// 就自动转为科学计数法 +0.0000003 // 3e-7 + +// 否则,就保持原来的字面形式 +0.000003 // 0.000003 +``` + +## 数值的进制 + +使用字面量(literal)直接表示一个数值时,JavaScript 对整数提供四种进制的表示方法:十进制、十六进制、八进制、二进制。 + +* 十进制:没有前导0的数值。 +* 八进制:有前缀`0o`或`0O`的数值,或者有前导0、且只用到0-7的八个阿拉伯数字的数值。 +* 十六进制:有前缀`0x`或`0X`的数值。 +* 二进制:有前缀`0b`或`0B`的数值。 + +默认情况下,JavaScript 内部会自动将八进制、十六进制、二进制转为十进制。下面是一些例子。 + +```javascript +0xff // 255 +0o377 // 255 +0b11 // 3 +``` + +如果八进制、十六进制、二进制的数值里面,出现不属于该进制的数字,就会报错。 + +```javascript +0xzz // 报错 +0o88 // 报错 +0b22 // 报错 +``` + +上面代码中,十六进制出现了字母`z`、八进制出现数字`8`、二进制出现数字`2`,因此报错。 + +通常来说,有前导0的数值会被视为八进制,但是如果前导0后面有数字`8`和`9`,则该数值被视为十进制。 + +```javascript +0888 // 888 +0777 // 511 +``` + +前导0表示八进制,处理时很容易造成混乱。ES5 的严格模式和 ES6,已经废除了这种表示法,但是浏览器为了兼容以前的代码,目前还继续支持这种表示法。 + +## 特殊数值 + +JavaScript 提供了几个特殊的数值。 + +### 正零和负零 + +前面说过,JavaScript 的64位浮点数之中,有一个二进制位是符号位。这意味着,任何一个数都有一个对应的负值,就连`0`也不例外。 + +JavaScript 内部实际上存在2个`0`:一个是`+0`,一个是`-0`,区别就是64位浮点数表示法的符号位不同。它们是等价的。 + +```javascript +-0 === +0 // true +0 === -0 // true +0 === +0 // true +``` + +几乎所有场合,正零和负零都会被当作正常的`0`。 + +```javascript ++0 // 0 +-0 // 0 +(-0).toString() // '0' +(+0).toString() // '0' +``` + +唯一有区别的场合是,`+0`或`-0`当作分母,返回的值是不相等的。 + +```javascript +(1 / +0) === (1 / -0) // false +``` + +上面的代码之所以出现这样结果,是因为除以正零得到`+Infinity`,除以负零得到`-Infinity`,这两者是不相等的(关于`Infinity`详见下文)。 + +### NaN + +**(1)含义** + +`NaN`是 JavaScript 的特殊值,表示“非数字”(Not a Number),主要出现在将字符串解析成数字出错的场合。 + +```javascript +5 - 'x' // NaN +``` + +上面代码运行时,会自动将字符串`x`转为数值,但是由于`x`不是数值,所以最后得到结果为`NaN`,表示它是“非数字”(`NaN`)。 + +另外,一些数学函数的运算结果会出现`NaN`。 + +```javascript +Math.acos(2) // NaN +Math.log(-1) // NaN +Math.sqrt(-1) // NaN +``` + +`0`除以`0`也会得到`NaN`。 + +```javascript +0 / 0 // NaN +``` + +需要注意的是,`NaN`不是独立的数据类型,而是一个特殊数值,它的数据类型依然属于`Number`,使用`typeof`运算符可以看得很清楚。 + +```javascript +typeof NaN // 'number' +``` + +**(2)运算规则** + +`NaN`不等于任何值,包括它本身。 + +```javascript +NaN === NaN // false +``` + +数组的`indexOf`方法内部使用的是严格相等运算符,所以该方法对`NaN`不成立。 + +```javascript +[NaN].indexOf(NaN) // -1 +``` + +`NaN`在布尔运算时被当作`false`。 + +```javascript +Boolean(NaN) // false +``` + +`NaN`与任何数(包括它自己)的运算,得到的都是`NaN`。 + +```javascript +NaN + 32 // NaN +NaN - 32 // NaN +NaN * 32 // NaN +NaN / 32 // NaN +``` + +### Infinity + +**(1)含义** + +`Infinity`表示“无穷”,用来表示两种场景。一种是一个正的数值太大,或一个负的数值太小,无法表示;另一种是非0数值除以0,得到`Infinity`。 + +```javascript +// 场景一 +Math.pow(2, 1024) +// Infinity + +// 场景二 +0 / 0 // NaN +1 / 0 // Infinity +``` + +上面代码中,第一个场景是一个表达式的计算结果太大,超出了能够表示的范围,因此返回`Infinity`。第二个场景是`0`除以`0`会得到`NaN`,而非0数值除以`0`,会返回`Infinity`。 + +`Infinity`有正负之分,`Infinity`表示正的无穷,`-Infinity`表示负的无穷。 + +```javascript +Infinity === -Infinity // false + +1 / -0 // -Infinity +-1 / -0 // Infinity +``` + +上面代码中,非零正数除以`-0`,会得到`-Infinity`,负数除以`-0`,会得到`Infinity`。 + +由于数值正向溢出(overflow)、负向溢出(underflow)和被`0`除,JavaScript 都不报错,所以单纯的数学运算几乎没有可能抛出错误。 + +`Infinity`大于一切数值(除了`NaN`),`-Infinity`小于一切数值(除了`NaN`)。 + +```javascript +Infinity > 1000 // true +-Infinity < -1000 // true +``` + +`Infinity`与`NaN`比较,总是返回`false`。 + +```javascript +Infinity > NaN // false +-Infinity > NaN // false + +Infinity < NaN // false +-Infinity < NaN // false +``` + +**(2)运算规则** + +`Infinity`的四则运算,符合无穷的数学计算规则。 + +```javascript +5 * Infinity // Infinity +5 - Infinity // -Infinity +Infinity / 5 // Infinity +5 / Infinity // 0 +``` + +0乘以`Infinity`,返回`NaN`;0除以`Infinity`,返回`0`;`Infinity`除以0,返回`Infinity`。 + +```javascript +0 * Infinity // NaN +0 / Infinity // 0 +Infinity / 0 // Infinity +``` + +`Infinity`加上或乘以`Infinity`,返回的还是`Infinity`。 + +```javascript +Infinity + Infinity // Infinity +Infinity * Infinity // Infinity +``` + +`Infinity`减去或除以`Infinity`,得到`NaN`。 + +```javascript +Infinity - Infinity // NaN +Infinity / Infinity // NaN +``` + +`Infinity`与`null`计算时,`null`会转成0,等同于与`0`的计算。 + +```javascript +null * Infinity // NaN +null / Infinity // 0 +Infinity / null // Infinity +``` + +`Infinity`与`undefined`计算,返回的都是`NaN`。 + +```javascript +undefined + Infinity // NaN +undefined - Infinity // NaN +undefined * Infinity // NaN +undefined / Infinity // NaN +Infinity / undefined // NaN +``` + +## 与数值相关的全局方法 + +### parseInt\(\) + +**(1)基本用法** + +`parseInt`方法用于将字符串转为整数。 + +```javascript +parseInt('123') // 123 +``` + +如果字符串头部有空格,空格会被自动去除。 + +```javascript +parseInt(' 81') // 81 +``` + +如果`parseInt`的参数不是字符串,则会先转为字符串再转换。 + +```javascript +parseInt(1.23) // 1 +// 等同于 +parseInt('1.23') // 1 +``` + +字符串转为整数的时候,是一个个字符依次转换,如果遇到不能转为数字的字符,就不再进行下去,返回已经转好的部分。 + +```javascript +parseInt('8a') // 8 +parseInt('12**') // 12 +parseInt('12.34') // 12 +parseInt('15e2') // 15 +parseInt('15px') // 15 +``` + +上面代码中,`parseInt`的参数都是字符串,结果只返回字符串头部可以转为数字的部分。 + +如果字符串的第一个字符不能转化为数字(后面跟着数字的正负号除外),返回`NaN`。 + +```javascript +parseInt('abc') // NaN +parseInt('.3') // NaN +parseInt('') // NaN +parseInt('+') // NaN +parseInt('+1') // 1 +``` + +所以,`parseInt`的返回值只有两种可能,要么是一个十进制整数,要么是`NaN`。 + +如果字符串以`0x`或`0X`开头,`parseInt`会将其按照十六进制数解析。 + +```javascript +parseInt('0x10') // 16 +``` + +如果字符串以`0`开头,将其按照10进制解析。 + +```javascript +parseInt('011') // 11 +``` + +对于那些会自动转为科学计数法的数字,`parseInt`会将科学计数法的表示方法视为字符串,因此导致一些奇怪的结果。 + +```javascript +parseInt(1000000000000000000000.5) // 1 +// 等同于 +parseInt('1e+21') // 1 + +parseInt(0.0000008) // 8 +// 等同于 +parseInt('8e-7') // 8 +``` + +**(2)进制转换** + +`parseInt`方法还可以接受第二个参数(2到36之间),表示被解析的值的进制,返回该值对应的十进制数。默认情况下,`parseInt`的第二个参数为10,即默认是十进制转十进制。 + +```javascript +parseInt('1000') // 1000 +// 等同于 +parseInt('1000', 10) // 1000 +``` + +下面是转换指定进制的数的例子。 + +```javascript +parseInt('1000', 2) // 8 +parseInt('1000', 6) // 216 +parseInt('1000', 8) // 512 +``` + +上面代码中,二进制、六进制、八进制的`1000`,分别等于十进制的8、216和512。这意味着,可以用`parseInt`方法进行进制的转换。 + +如果第二个参数不是数值,会被自动转为一个整数。这个整数只有在2到36之间,才能得到有意义的结果,超出这个范围,则返回`NaN`。如果第二个参数是`0`、`undefined`和`null`,则直接忽略。 + +```javascript +parseInt('10', 37) // NaN +parseInt('10', 1) // NaN +parseInt('10', 0) // 10 +parseInt('10', null) // 10 +parseInt('10', undefined) // 10 +``` + +如果字符串包含对于指定进制无意义的字符,则从最高位开始,只返回可以转换的数值。如果最高位无法转换,则直接返回`NaN`。 + +```javascript +parseInt('1546', 2) // 1 +parseInt('546', 2) // NaN +``` + +上面代码中,对于二进制来说,`1`是有意义的字符,`5`、`4`、`6`都是无意义的字符,所以第一行返回1,第二行返回`NaN`。 + +前面说过,如果`parseInt`的第一个参数不是字符串,会被先转为字符串。这会导致一些令人意外的结果。 + +```javascript +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,尤其需要注意。 + +```javascript +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`方法用于将一个字符串转为浮点数。 + +```javascript +parseFloat('3.14') // 3.14 +``` + +如果字符串符合科学计数法,则会进行相应的转换。 + +```javascript +parseFloat('314e-2') // 3.14 +parseFloat('0.0314E+2') // 3.14 +``` + +如果字符串包含不能转为浮点数的字符,则不再进行往后转换,返回已经转好的部分。 + +```javascript +parseFloat('3.14more non-digit characters') // 3.14 +``` + +`parseFloat`方法会自动过滤字符串前导的空格。 + +```javascript +parseFloat('\t\v\r12.34\n ') // 12.34 +``` + +如果参数不是字符串,或者字符串的第一个字符不能转化为浮点数,则返回`NaN`。 + +```javascript +parseFloat([]) // NaN +parseFloat('FF2') // NaN +parseFloat('') // NaN +``` + +上面代码中,尤其值得注意,`parseFloat`会将空字符串转为`NaN`。 + +这些特点使得`parseFloat`的转换结果不同于`Number`函数。 + +```javascript +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`。 + +```javascript +isNaN(NaN) // true +isNaN(123) // false +``` + +但是,`isNaN`只对数值有效,如果传入其他值,会被先转成数值。比如,传入字符串的时候,字符串会被先转成`NaN`,所以最后返回`true`,这一点要特别引起注意。也就是说,`isNaN`为`true`的值,有可能不是`NaN`,而是一个字符串。 + +```javascript +isNaN('Hello') // true +// 相当于 +isNaN(Number('Hello')) // true +``` + +出于同样的原因,对于对象和数组,`isNaN`也返回`true`。 + +```javascript +isNaN({}) // true +// 等同于 +isNaN(Number({})) // true + +isNaN(['xzy']) // true +// 等同于 +isNaN(Number(['xzy'])) // true +``` + +但是,对于空数组和只有一个数值成员的数组,`isNaN`返回`false`。 + +```javascript +isNaN([]) // false +isNaN([123]) // false +isNaN(['123']) // false +``` + +上面代码之所以返回`false`,原因是这些数组能被`Number`函数转成数值,请参见《数据类型转换》一章。 + +因此,使用`isNaN`之前,最好判断一下数据类型。 + +```javascript +function myIsNaN(value) { + return typeof value === 'number' && isNaN(value); +} +``` + +判断`NaN`更可靠的方法是,利用`NaN`为唯一不等于自身的值的这个特点,进行判断。 + +```javascript +function myIsNaN(value) { + return value !== value; +} +``` + +### isFinite\(\) + +`isFinite`方法返回一个布尔值,表示某个值是否为正常的数值。 + +```javascript +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`。 + +## 参考链接 + +* Dr. Axel Rauschmayer, [How numbers are encoded in JavaScript](http://www.2ality.com/2012/04/number-encoding.html) +* Humphry, [JavaScript 中 Number 的一些表示上/下限](http://blog.segmentfault.com/humphry/1190000000407658) + diff --git a/types/object.md b/types/object.md new file mode 100644 index 0000000..0299d65 --- /dev/null +++ b/types/object.md @@ -0,0 +1,502 @@ +# 对象 + +## 概述 + +### 生成方法 + +对象(object)是 JavaScript 语言的核心概念,也是最重要的数据类型。 + +什么是对象?简单说,对象就是一组“键值对”(key-value)的集合,是一种无序的复合数据集合。 + +```javascript +var obj = { + foo: 'Hello', + bar: 'World' +}; +``` + +上面代码中,大括号就定义了一个对象,它被赋值给变量`obj`,所以变量`obj`就指向一个对象。该对象内部包含两个键值对(又称为两个“成员”),第一个键值对是`foo: 'Hello'`,其中`foo`是“键名”(成员的名称),字符串`Hello`是“键值”(成员的值)。键名与键值之间用冒号分隔。第二个键值对是`bar: 'World'`,`bar`是键名,`World`是键值。两个键值对之间用逗号分隔。 + +### 键名 + +对象的所有键名都是字符串(ES6 又引入了 Symbol 值也可以作为键名),所以加不加引号都可以。上面的代码也可以写成下面这样。 + +```javascript +var obj = { + 'foo': 'Hello', + 'bar': 'World' +}; +``` + +如果键名是数值,会被自动转为字符串。 + +```javascript +var obj = { + 1: 'a', + 3.2: 'b', + 1e2: true, + 1e-2: true, + .234: true, + 0xFF: true +}; + +obj +// Object { +// 1: "a", +// 3.2: "b", +// 100: true, +// 0.01: true, +// 0.234: true, +// 255: true +// } + +obj['100'] // true +``` + +上面代码中,对象`obj`的所有键名虽然看上去像数值,实际上都被自动转成了字符串。 + +如果键名不符合标识名的条件(比如第一个字符为数字,或者含有空格或运算符),且也不是数字,则必须加上引号,否则会报错。 + +```javascript +// 报错 +var obj = { + 1p: 'Hello World' +}; + +// 不报错 +var obj = { + '1p': 'Hello World', + 'h w': 'Hello World', + 'p+q': 'Hello World' +}; +``` + +上面对象的三个键名,都不符合标识名的条件,所以必须加上引号。 + +对象的每一个键名又称为“属性”(property),它的“键值”可以是任何数据类型。如果一个属性的值为函数,通常把这个属性称为“方法”,它可以像函数那样调用。 + +```javascript +var obj = { + p: function (x) { + return 2 * x; + } +}; + +obj.p(1) // 2 +``` + +上面代码中,对象`obj`的属性`p`,就指向一个函数。 + +如果属性的值还是一个对象,就形成了链式引用。 + +```javascript +var o1 = {}; +var o2 = { bar: 'hello' }; + +o1.foo = o2; +o1.foo.bar // "hello" +``` + +上面代码中,对象`o1`的属性`foo`指向对象`o2`,就可以链式引用`o2`的属性。 + +对象的属性之间用逗号分隔,最后一个属性后面可以加逗号(trailing comma),也可以不加。 + +```javascript +var obj = { + p: 123, + m: function () { ... }, +} +``` + +上面的代码中,`m`属性后面的那个逗号,有没有都可以。 + +属性可以动态创建,不必在对象声明时就指定。 + +```javascript +var obj = {}; +obj.foo = 123; +obj.foo // 123 +``` + +上面代码中,直接对`obj`对象的`foo`属性赋值,结果就在运行时创建了`foo`属性。 + +### 对象的引用 + +如果不同的变量名指向同一个对象,那么它们都是这个对象的引用,也就是说指向同一个内存地址。修改其中一个变量,会影响到其他所有变量。 + +```javascript +var o1 = {}; +var o2 = o1; + +o1.a = 1; +o2.a // 1 + +o2.b = 2; +o1.b // 2 +``` + +上面代码中,`o1`和`o2`指向同一个对象,因此为其中任何一个变量添加属性,另一个变量都可以读写该属性。 + +此时,如果取消某一个变量对于原对象的引用,不会影响到另一个变量。 + +```javascript +var o1 = {}; +var o2 = o1; + +o1 = 1; +o2 // {} +``` + +上面代码中,`o1`和`o2`指向同一个对象,然后`o1`的值变为1,这时不会对`o2`产生影响,`o2`还是指向原来的那个对象。 + +但是,这种引用只局限于对象,如果两个变量指向同一个原始类型的值。那么,变量这时都是值的拷贝。 + +```javascript +var x = 1; +var y = x; + +x = 2; +y // 1 +``` + +上面的代码中,当`x`的值发生变化后,`y`的值并不变,这就表示`y`和`x`并不是指向同一个内存地址。 + +### 表达式还是语句? + +对象采用大括号表示,这导致了一个问题:如果行首是一个大括号,它到底是表达式还是语句? + +```javascript +{ foo: 123 } +``` + +JavaScript 引擎读到上面这行代码,会发现可能有两种含义。第一种可能是,这是一个表达式,表示一个包含`foo`属性的对象;第二种可能是,这是一个语句,表示一个代码区块,里面有一个标签`foo`,指向表达式`123`。 + +为了避免这种歧义,JavaScript 引擎的做法是,如果遇到这种情况,无法确定是对象还是代码块,一律解释为代码块。 + +```javascript +{ console.log(123) } // 123 +``` + +上面的语句是一个代码块,而且只有解释为代码块,才能执行。 + +如果要解释为对象,最好在大括号前加上圆括号。因为圆括号的里面,只能是表达式,所以确保大括号只能解释为对象。 + +```javascript +({ foo: 123 }) // 正确 +({ console.log(123) }) // 报错 +``` + +这种差异在`eval`语句(作用是对字符串求值)中反映得最明显。 + +```javascript +eval('{foo: 123}') // 123 +eval('({foo: 123})') // {foo: 123} +``` + +上面代码中,如果没有圆括号,`eval`将其理解为一个代码块;加上圆括号以后,就理解成一个对象。 + +## 属性的操作 + +### 属性的读取 + +读取对象的属性,有两种方法,一种是使用点运算符,还有一种是使用方括号运算符。 + +```javascript +var obj = { + p: 'Hello World' +}; + +obj.p // "Hello World" +obj['p'] // "Hello World" +``` + +上面代码分别采用点运算符和方括号运算符,读取属性`p`。 + +请注意,如果使用方括号运算符,键名必须放在引号里面,否则会被当作变量处理。 + +```javascript +var foo = 'bar'; + +var obj = { + foo: 1, + bar: 2 +}; + +obj.foo // 1 +obj[foo] // 2 +``` + +上面代码中,引用对象`obj`的`foo`属性时,如果使用点运算符,`foo`就是字符串;如果使用方括号运算符,但是不使用引号,那么`foo`就是一个变量,指向字符串`bar`。 + +方括号运算符内部还可以使用表达式。 + +```javascript +obj['hello' + ' world'] +obj[3 + 3] +``` + +数字键可以不加引号,因为会自动转成字符串。 + +```javascript +var obj = { + 0.7: 'Hello World' +}; + +obj['0.7'] // "Hello World" +obj[0.7] // "Hello World" +``` + +上面代码中,对象`obj`的数字键`0.7`,加不加引号都可以,因为会被自动转为字符串。 + +注意,数值键名不能使用点运算符(因为会被当成小数点),只能使用方括号运算符。 + +```javascript +var obj = { + 123: 'hello world' +}; + +obj.123 // 报错 +obj[123] // "hello world" +``` + +上面代码的第一个表达式,对数值键名`123`使用点运算符,结果报错。第二个表达式使用方括号运算符,结果就是正确的。 + +### 属性的赋值 + +点运算符和方括号运算符,不仅可以用来读取值,还可以用来赋值。 + +```javascript +var obj = {}; + +obj.foo = 'Hello'; +obj['bar'] = 'World'; +``` + +上面代码中,分别使用点运算符和方括号运算符,对属性赋值。 + +JavaScript 允许属性的“后绑定”,也就是说,你可以在任意时刻新增属性,没必要在定义对象的时候,就定义好属性。 + +```javascript +var obj = { p: 1 }; + +// 等价于 + +var obj = {}; +obj.p = 1; +``` + +### 属性的查看 + +查看一个对象本身的所有属性,可以使用`Object.keys`方法。 + +```javascript +var obj = { + key1: 1, + key2: 2 +}; + +Object.keys(obj); +// ['key1', 'key2'] +``` + +### 属性的删除:delete 命令 + +`delete`命令用于删除对象的属性,删除成功后返回`true`。 + +```javascript +var obj = { p: 1 }; +Object.keys(obj) // ["p"] + +delete obj.p // true +obj.p // undefined +Object.keys(obj) // [] +``` + +上面代码中,`delete`命令删除对象`obj`的`p`属性。删除后,再读取`p`属性就会返回`undefined`,而且`Object.keys`方法的返回值也不再包括该属性。 + +注意,删除一个不存在的属性,`delete`不报错,而且返回`true`。 + +```javascript +var obj = {}; +delete obj.p // true +``` + +上面代码中,对象`obj`并没有`p`属性,但是`delete`命令照样返回`true`。因此,不能根据`delete`命令的结果,认定某个属性是存在的。 + +只有一种情况,`delete`命令会返回`false`,那就是该属性存在,且不得删除。 + +```javascript +var obj = Object.defineProperty({}, 'p', { + value: 123, + configurable: false +}); + +obj.p // 123 +delete obj.p // false +``` + +上面代码之中,对象`obj`的`p`属性是不能删除的,所以`delete`命令返回`false`(关于`Object.defineProperty`方法的介绍,请看《标准库》的 Object 对象一章)。 + +另外,需要注意的是,`delete`命令只能删除对象本身的属性,无法删除继承的属性(关于继承参见《面向对象编程》章节)。 + +```javascript +var obj = {}; +delete obj.toString // true +obj.toString // function toString() { [native code] } +``` + +上面代码中,`toString`是对象`obj`继承的属性,虽然`delete`命令返回`true`,但该属性并没有被删除,依然存在。这个例子还说明,即使`delete`返回`true`,该属性依然可能读取到值。 + +### 属性是否存在:in 运算符 + +`in`运算符用于检查对象是否包含某个属性(注意,检查的是键名,不是键值),如果包含就返回`true`,否则返回`false`。它的左边是一个字符串,表示属性名,右边是一个对象。 + +```javascript +var obj = { p: 1 }; +'p' in obj // true +'toString' in obj // true +``` + +`in`运算符的一个问题是,它不能识别哪些属性是对象自身的,哪些属性是继承的。就像上面代码中,对象`obj`本身并没有`toString`属性,但是`in`运算符会返回`true`,因为这个属性是继承的。 + +这时,可以使用对象的`hasOwnProperty`方法判断一下,是否为对象自身的属性。 + +```javascript +var obj = {}; +if ('toString' in obj) { + console.log(obj.hasOwnProperty('toString')) // false +} +``` + +### 属性的遍历:for...in 循环 + +`for...in`循环用来遍历一个对象的全部属性。 + +```javascript +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`循环有两个使用注意点。 + +* 它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性。 +* 它不仅遍历对象自身的属性,还遍历继承的属性。 + +举例来说,对象都继承了`toString`属性,但是`for...in`循环不会遍历到这个属性。 + +```javascript +var obj = {}; + +// toString 属性是存在的 +obj.toString // toString() { [native code] } + +for (var p in obj) { + console.log(p); +} // 没有任何输出 +``` + +上面代码中,对象`obj`继承了`toString`属性,该属性不会被`for...in`循环遍历到,因为它默认是“不可遍历”的。关于对象属性的可遍历性,参见《标准库》章节中 Object 一章的介绍。 + +如果继承的属性是可遍历的,那么就会被`for...in`循环遍历到。但是,一般情况下,都是只想遍历对象自身的属性,所以使用`for...in`的时候,应该结合使用`hasOwnProperty`方法,在循环内部判断一下,某个属性是否为对象自身的属性。 + +```javascript +var person = { name: '老张' }; + +for (var key in person) { + if (person.hasOwnProperty(key)) { + console.log(key); + } +} +// name +``` + +## with 语句 + +`with`语句的格式如下: + +```javascript +with (对象) { + 语句; +} +``` + +它的作用是操作同一个对象的多个属性时,提供一些书写的方便。 + +```javascript +// 例一 +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`区块内部有变量的赋值操作,必须是当前对象已经存在的属性,否则会创造一个当前作用域的全局变量。 + +```javascript +var obj = {}; +with (obj) { + p1 = 4; + p2 = 5; +} + +obj.p1 // undefined +p1 // 4 +``` + +上面代码中,对象`obj`并没有`p1`属性,对`p1`赋值等于创造了一个全局变量`p1`。正确的写法应该是,先定义对象`obj`的属性`p1`,然后在`with`区块内操作它。 + +这是因为`with`区块没有改变作用域,它的内部依然是当前作用域。这造成了`with`语句的一个很大的弊病,就是绑定对象不明确。 + +```javascript +with (obj) { + console.log(x); +} +``` + +单纯从上面的代码块,根本无法判断`x`到底是全局变量,还是对象`obj`的一个属性。这非常不利于代码的除错和模块化,编译器也无法对这段代码进行优化,只能留到运行时判断,这就拖慢了运行速度。因此,建议不要使用`with`语句,可以考虑用一个临时变量代替`with`。 + +```javascript +with(obj1.obj2.obj3) { + console.log(p1 + p2); +} + +// 可以写成 +var temp = obj1.obj2.obj3; +console.log(temp.p1 + temp.p2); +``` + +## 参考链接 + +* Dr. Axel Rauschmayer,[Object properties in JavaScript](http://www.2ality.com/2012/10/javascript-properties.html) +* Lakshan Perera, [Revisiting JavaScript Objects](http://www.laktek.com/2012/12/29/revisiting-javascript-objects/) +* Angus Croll, [The Secret Life of JavaScript Primitives](http://javascriptweblog.wordpress.com/2010/09/27/the-secret-life-of-javascript-primitives/)i +* Dr. Axel Rauschmayer, [JavaScript’s with statement and why it’s deprecated](http://www.2ality.com/2011/06/with-statement.html) + diff --git a/types/string.md b/types/string.md new file mode 100644 index 0000000..e40bf47 --- /dev/null +++ b/types/string.md @@ -0,0 +1,286 @@ +# 字符串 + +## 概述 + +### 定义 + +字符串就是零个或多个排在一起的字符,放在单引号或双引号之中。 + +```javascript +'abc' +"abc" +``` + +单引号字符串的内部,可以使用双引号。双引号字符串的内部,可以使用单引号。 + +```javascript +'key = "value"' +"It's a long journey" +``` + +上面两个都是合法的字符串。 + +如果要在单引号字符串的内部,使用单引号,就必须在内部的单引号前面加上反斜杠,用来转义。双引号字符串内部使用双引号,也是如此。 + +```javascript +'Did she say \'Hello\'?' +// "Did she say 'Hello'?" + +"Did she say \"Hello\"?" +// "Did she say "Hello"?" +``` + +由于 HTML 语言的属性值使用双引号,所以很多项目约定 JavaScript 语言的字符串只使用单引号,本教程遵守这个约定。当然,只使用双引号也完全可以。重要的是坚持使用一种风格,不要一会使用单引号表示字符串,一会又使用双引号表示。 + +字符串默认只能写在一行内,分成多行将会报错。 + +```javascript +'a +b +c' +// SyntaxError: Unexpected token ILLEGAL +``` + +上面代码将一个字符串分成三行,JavaScript 就会报错。 + +如果长字符串必须分成多行,可以在每一行的尾部使用反斜杠。 + +```javascript +var longString = 'Long \ +long \ +long \ +string'; + +longString +// "Long long long string" +``` + +上面代码表示,加了反斜杠以后,原来写在一行的字符串,可以分成多行书写。但是,输出的时候还是单行,效果与写在同一行完全一样。注意,反斜杠的后面必须是换行符,而不能有其他字符(比如空格),否则会报错。 + +连接运算符(`+`)可以连接多个单行字符串,将长字符串拆成多行书写,输出的时候也是单行。 + +```javascript +var longString = 'Long ' + + 'long ' + + 'long ' + + 'string'; +``` + +如果想输出多行字符串,有一种利用多行注释的变通方法。 + +```javascript +(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`) + +上面这些字符前面加上反斜杠,都表示特殊含义。 + +```javascript +console.log('1\n2') +// 1 +// 2 +``` + +上面代码中,`\n`表示换行,输出的时候就分成了两行。 + +反斜杠还有三种特殊用法。 + +(1)`\HHH` + +反斜杠后面紧跟三个八进制数(`000`到`377`),代表一个字符。`HHH`对应该字符的 Unicode 码点,比如`\251`表示版权符号。显然,这种方法只能输出256种字符。 + +(2)`\xHH` + +`\x`后面紧跟两个十六进制数(`00`到`FF`),代表一个字符。`HH`对应该字符的 Unicode 码点,比如`\xA9`表示版权符号。这种方法也只能输出256种字符。 + +(3)`\uXXXX` + +`\u`后面紧跟四个十六进制数(`0000`到`FFFF`),代表一个字符。`XXXX`对应该字符的 Unicode 码点,比如`\u00A9`表示版权符号。 + +下面是这三种字符特殊写法的例子。 + +```javascript +'\251' // "©" +'\xA9' // "©" +'\u00A9' // "©" + +'\172' === 'z' // true +'\x7A' === 'z' // true +'\u007A' === 'z' // true +``` + +如果在非特殊字符前面使用反斜杠,则反斜杠会被省略。 + +```javascript +'\a' +// "a" +``` + +上面代码中,`a`是一个正常字符,前面加反斜杠没有特殊含义,反斜杠会被自动省略。 + +如果字符串的正常内容之中,需要包含反斜杠,则反斜杠前面需要再加一个反斜杠,用来对自身转义。 + +```javascript +"Prev \\ Next" +// "Prev \ Next" +``` + +### 字符串与数组 + +字符串可以被视为字符数组,因此可以使用数组的方括号运算符,用来返回某个位置的字符(位置编号从0开始)。 + +```javascript +var s = 'hello'; +s[0] // "h" +s[1] // "e" +s[4] // "o" + +// 直接对字符串使用方括号运算符 +'hello'[1] // "e" +``` + +如果方括号中的数字超过字符串的长度,或者方括号中根本不是数字,则返回`undefined`。 + +```javascript +'abc'[3] // undefined +'abc'[-1] // undefined +'abc'['x'] // undefined +``` + +但是,字符串与数组的相似性仅此而已。实际上,无法改变字符串之中的单个字符。 + +```javascript +var s = 'hello'; + +delete s[0]; +s // "hello" + +s[1] = 'a'; +s // "hello" + +s[5] = '!'; +s // "hello" +``` + +上面代码表示,字符串内部的单个字符无法改变和增删,这些操作会默默地失败。 + +### length 属性 + +`length`属性返回字符串的长度,该属性也是无法改变的。 + +```javascript +var s = 'hello'; +s.length // 5 + +s.length = 3; +s.length // 5 + +s.length = 7; +s.length // 5 +``` + +上面代码表示字符串的`length`属性无法改变,但是不会报错。 + +## 字符集 + +JavaScript 使用 Unicode 字符集。JavaScript 引擎内部,所有字符都用 Unicode 表示。 + +JavaScript 不仅以 Unicode 储存字符,还允许直接在程序中使用 Unicode 码点表示字符,即将字符写成`\uxxxx`的形式,其中`xxxx`代表该字符的 Unicode 码点。比如,`\u00A9`代表版权符号。 + +```javascript +var s = '\u00A9'; +s // "©" +``` + +解析代码的时候,JavaScript 会自动识别一个字符是字面形式表示,还是 Unicode 形式表示。输出给用户的时候,所有字符都会转成字面形式。 + +```javascript +var f\u006F\u006F = 'abc'; +foo // "abc" +``` + +上面代码中,第一行的变量名`foo`是 Unicode 形式表示,第二行是字面形式表示。JavaScript 会自动识别。 + +我们还需要知道,每个字符在 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 无法识别,会认为这是两个字符。 + +```javascript +'𝌆'.length // 2 +``` + +上面代码中,JavaScript 认为`𝌆`的长度为2,而不是1。 + +总结一下,对于码点在`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 编码转为原来的值 + +```javascript +var string = 'Hello World!'; +btoa(string) // "SGVsbG8gV29ybGQh" +atob('SGVsbG8gV29ybGQh') // "Hello World!" +``` + +注意,这两个方法不适合非 ASCII 码的字符,会报错。 + +```javascript +btoa('你好') // 报错 +``` + +要将非 ASCII 码字符转为 Base64 编码,必须中间插入一个转码环节,再使用这两个方法。 + +```javascript +function b64Encode(str) { + return btoa(encodeURIComponent(str)); +} + +function b64Decode(str) { + return decodeURIComponent(atob(str)); +} + +b64Encode('你好') // "JUU0JUJEJUEwJUU1JUE1JUJE" +b64Decode('JUU0JUJEJUEwJUU1JUE1JUJE') // "你好" +``` + +## 参考链接 + +* Mathias Bynens, [JavaScript’s internal character encoding: UCS-2 or UTF-16?](http://mathiasbynens.be/notes/javascript-encoding) +* Mathias Bynens, [JavaScript has a Unicode problem](http://mathiasbynens.be/notes/javascript-unicode) +* Mozilla Developer Network, [Window.btoa](https://developer.mozilla.org/en-US/docs/Web/API/Window.btoa) +