基础
简介
JavaScript 是一门高级、动态、解释型编程语言,非常适合面向对象和函数式编程风格
JavaScript: 名字、版本和模式
JavaScript 是 Netscape 在 Web 诞生初期创造的。
严格来讲,JavaScript 是经 Sun Microsystems(现 Oracle)授权使用的一个注册商标,用于描述 Netscape(现 Mozilla)对这门语言的实现。
Netscape 将这门语言提交给 Ecma International 进行标准化,由于商标问题,这门语言的标准版本沿用了别扭的名字“ECMAScript”。
在实践中,大家仍然称这门语言为 JavaScript。
2010 年以来,几乎所有的浏览器都支持 ECMAScript 标准第 5 版,缩写为 ES5。
ES6 发布于 2015 年,增加了重要的特性(包括类和模块语法)。
这些新特性把 JavaScript 从一门脚本语言转变为一门适合大规模软件工程的严肃、通用语言。
从 ES6 开始,ECMAScript 规范改为每年发布一次,语言的版本也以发布的年份来标识(ES2016、ES2017、ES2018、ES2019 和 ES2020)。
随着 JavaScript 的发展,语言设计者也在尝试纠正早期(ES5 之前)版本中的缺陷。
为了保证向后兼容,无论一个特性的问题有多严重,也不能把它删除。
但在 ES5 及之后,程序可以选择切换到 JavaScript 的严格模式。在这种模式下,一些早期的语言错误会得到纠正。
在 ES6 及之后,使用新语言特性经常会隐式触发严格模式。
例如,如果使用 ES6 的 class 关键字或者创建 ES6 模块,类和模块中的所有代码都会自动切换到严格模式。
在这些上下文中,不能使用老旧、有缺陷的特性。
为了好用,每种语言都必须有一个平台或标准库,用于执行包括基本输入和输出在内的基本操作。
核心 JavaScript 语言定义了最小限度的 API,可以操作数值、文本、数组、集合、映射等,但不包含任何输入和输出功能。
输入和输出(以及更复杂的特性,如联网、存储和图形处理)是内嵌 JavaScript 的 “宿主环境” 的责任。
浏览器是 JavaScript 最早的宿主环境,也是 JavaScript 代码最常见的运行环境。
浏览器环境允许 JavaScript 代码从用户的鼠标和键盘或者通过发送 HTTP 请求获取输入,也允许 JavaScript 代码通过 HTML 和 CSS 向用户显示输出
2010 年以后,JavaScript 代码又有了另一个宿主环境。
与限制 JavaScript 只能使用浏览器提供的 API 不同,Node 给予了 JavaScript 访问整个操作系统的权限,允许 JavaScript 程序读写文件、通过网络发送和接收数据,以及发送和处理 HTTP 请求。
Node 是实现 Web 服务器的一种流行方式,也是编写可以替代 shell 脚本的简单实用脚本的便捷工具
在人们普遍使用电话拨号上网的年代,能够在客户端完成一些基本的验证任务绝对是令人兴奋的。毕竟,拨号上网的速度之慢,导致了与服务器的每一次数据交换事实上都成了对人们耐心的一次考验
为完成简单的表单验证而频繁地与服务器交换数据只会加重用户的负担
一个完整的 JavaScript 实现应该由下列三个不同的部分组成:
核心(ECMAScript)
文档对象模型(DOM)
浏览器对象模型(BOM)
文档对象模型(DOM)
DOM 是针对 XML 但经过扩展用于 HTML 的应用程序编程接口(API)。DOM 把整个页面映射为一个多层节点结构。HTML 或 XML 页面中的每个部分都是某种类型的节点,这些节点又包含着不同类型的数据
通过 DOM 创建的表示文档的树形图,开发人员获得了控制页面内容和结构的主动权。借助 DOM 提供的 API,开发人员可以轻松自如地删除、添加、替换或修改任何节点
浏览器对象模型(BOM)
开发人员使用 BOM 可以控制浏览器显示的页面以外的部分。
从根本上讲,BOM 只处理浏览器窗口和框架;但人们习惯上也把所有针对浏览器的 JavaScript 扩展算作 BOM 的一部分。下面就是这样的扩展:
- 弹出新浏览器窗口的功能
- 移动、缩放和关闭浏览器窗口的功能
- 提供浏览器详细信息的 navigator 对象
- 提供浏览器所加载页面的详细信息的 location 对象
- 提供用户显示屏分辨率详细信息的 screen 对象
- 对 cookies 的支持
- 像 XMLHttpRequest 和 IE 的 ActiveXObject 这样的自定义对象
由于没有 BOM 标准可以遵循,因此每个浏览器都有自己的实现。虽然也存在一些事实标准,例如要有 window 对象和 navigator 对象等,但每个浏览器都会为这两个对象乃至其他对象定义自己的属性和方法。现在有了 HTML5,BOM 实现的细节有望朝着兼容性越来越高的方向发展
<script>
元素
在使用<script>
嵌入 JavaScript 代码时,记住不要在代码中的任何地方出现"</script>"
字符串。例如,浏览器在加载下面所示的代码时就会产生一个错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
因为按照解析嵌入式代码的规则,当浏览器遇到字符串"</script>"
时,就会认为那是结束的</script>
标签,而通过转义字符"\"
解决这个问题
1 2 3 4 |
|
标签的位置
按照传统的做法,所有<script>
元素都应该放在页面的<head>
元素中
这种做法的目的就是把所有外部文件(包括 CSS 文件和 JavaScript 文件)的引用都放在相同的地方。可是,在文档的<head>
元素中包含所有的 JavaScript 文件,意味着必须等到全部 JavaScript 代码都被下载、解析和执行完成以后,才能开始呈现页面的内容(浏览器遇到<body>
标签时才开始呈现内容)。对于那些需要很多 JavaScript 代码的页面来说,这无疑会导致浏览器在呈现页面时出现明显的延迟,而延迟期间的浏览器窗口中将是一片空白。为了避免这个问题,现代 Web 应用程序一般都会把全部 JavaScript 引用放在<body>
元素中页面内容的后面
这样,在解析包含的 JavaScript 代码之前,页面的内容将完全呈现在浏览器中。而用户也会因为浏览器窗口显示空白页面的时间缩短而感到打开页面的速度加快了
延迟脚本
HTML4.0.1 为<script>
标签定义了 defer 属性。这个属性的用途是表明脚本在执行时不会影响页面的构造。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,在<script>
元素中设置 defer 属性,相当于告诉浏览器立即下载,但延迟执行
就算我们把<script>
元素放在了文档的<head>
元素中,但其中包含的脚本将延迟到浏览器遇到</html>
标签后再执行。HTML5 规范要求脚本按照它们出现的先后顺序执行,因此第一个延迟脚本会先于第二个延迟脚本执行,而这两个脚本会先于DOMContentLoaded
事件触发前执行。在现实当中,延迟脚本并不一定会按照顺序执行,也不一定会在 DOMContentLoaded
事件触发前执行,因此最好只包含一个延迟脚本
defer 属性只适用于外部脚本文件。这一点在 HTML5 中已经明确规定,因此支持 HTML5 的实现会忽略给嵌入脚本设置的 defer 属性
异步脚本
HTML5 为 <script>
元素定义了 async 属性。这个属性与 defer 属性类似,都用于改变处理脚本的行为。同样与 defer 类似,async 只适用于外部脚本文件,并告诉浏览器立即下载文件。但与 defer 不同的是,标记为 async 的脚本并不保证按照指定它们的先后顺序执行
指定 async 属性的目的是不让页面等待脚本的下载和执行,从而异步加载页面其他内容。为此,建议异步脚本不要在加载期间修改 DOM
异步脚本一定会在页面的 load 事件前执行,但可能会在 DOMContentLoaded 事件触发之前或之后执行。
1 |
|
语法
ECMAScript 中的一切(变量、函数名和操作符)都区分大小写
标识符(变量、函数、属性的名字,或者函数的参数):第一个字符必须是一个字母、下划线或者一个美元符号;其他字符可以是字母、下划线、美元符号和数字
按照惯例,ESMAScript 标识符采用驼峰大小写格式,也就是第一个字母小写,剩下的每个单词的首字母大写
1 2 |
|
1 |
|
1 2 3 4 |
|
1 2 3 4 5 6 |
|
数据类型
- 基本数据类型:Undefined、Null、Boolean、Number 和 String,还有一种复杂的数据类型 Object
- 动态数据类型
- 弱类型
1 2 |
|
大小写敏感
1 2 3 4 5 6 7 |
|
- 声明
1 2 3 4 5 |
|
- 数组
1 2 3 4 |
|
1 |
|
1 |
|
- 对象
对象由花括号分隔。在括号内部,对象的属性以名称和值对的形式 (name : value) 来定义。属性由逗号分隔:
1 |
|
上面例子中的对象 (person) 有三个属性:firstname、lastname 以及 id。
空格和折行无关紧要。声明可横跨多行:
1 2 3 4 5 |
|
对象属性有两种寻址方式:
实例
1 2 |
|
-
undefined 和 null
Value = undefined
在计算机程序中,经常会声明无值的变量。未使用值来声明的变量,其值实际上是 undefined。
在执行过以下语句后,变量 carname 的值将是 undefined:
var carname;
Undefined 这个值表示变量不含有值。
可以通过将变量的值设置为 null 来清空变量。 -
重新声明 JavaScript 变量
如果重新声明 JavaScript 变量,该变量的值不会丢失:
在以下两条语句执行后,变量 carname 的值依然是 "Volvo":
1 2 |
|
- typeof 操作符:检测给定变量的数据类型
1 2 3 4 5 |
|
- 包含 undefined 的值的变量与尚未定义的变量不一样
1 2 3 4 5 |
|
即便未初始化的变量会自动被赋予undefined
值,但显式地初始化变量依然是明智的选择。
如果能做到这一点,那么当typeof
操作符返回undefined
值时,我们就知道被检测的变量还没有被声明,而不是尚未初始化
-
null
如果定义的变量准备在将来用于保存对象,那么最好将该变量初始化为 null 而不是其它值。
这样一来,只要直接检查null
值就可以知道相应的变量是否已经保存了一个对象的引用。 -
String 类型 字符串类型是不可变的,也就是说,字符串一旦创建,它们的值就不能改变。
要改变某个变量保存的字符串,首先要销毁原来的字符串,然后再用另一个包含新值的字符串填充该变量
NaN
NaN,即非数值(Not a Number)是一个特殊的数值,这个数值用于表示一个本来要返回数值的操作数未返回数值的情况(这样就不会抛出错误了)。在 ECMAScript 中,任何数值除以非数值会返回 NaN,因此不会影响其他代码的执行
NaN 本身有两个非同寻常的特点。首先,任何涉及 NaN 的操作(例如 NaN/10) 都会返回 NaN,这个特点在多步计算中有可能导致问题。其次,NaN 与任何值都不相等,包含 NaN 本身
1 |
|
针对 NaN 的这两个特点,ECMAScript 定义了 isNaN() 函数。这个函数接受一个参数,该参数可以是任何类型,而函数会帮我们确定这个参数是否“不是数值”。isNaN() 在接收到一个值之后,会尝试将这个值转换为数值。某些不是数值的值会直接转换为数值,例如字符串“10”或 Boolean 值。而任何不能被转换为数值的值都会导致这个函数返回 true
1 2 3 4 5 |
|
isNaN() 也适用于对象。在基于对象调用 isNaN() 函数时,会首先调用对象的 valueOf() 方法,然后确定该方法返回的值是否可以转换为数值。如果不能,则基于这个返回值再调用 toString() 方法,再测试返回值
数值转换
有 3 个函数可以把非数值转换为数值:Number()、parseInt() 和 parseFloat()。第一个函数,即转型函数 Number() 可以用于任何数据类型,而另外两个函数则专门用于把字符串转换成数值。这 3 个函数对于同样的输入会有返回不同的结果
1 2 3 4 |
|
1 2 3 4 5 6 7 |
|
parseInt 函数提供第二个参数:转换时使用的基数(即多少进制)。如果知道要解析的值是十六进制格式的字符串,那么指定基数 16 作为第二个参数,可以保证得到正确的结果
1 |
|
实际上,如果指定了 16 作为第二个参数,字符串可以不带前面的 "0x"
1 2 |
|
1 2 3 4 |
|
不指定基数意味着让 parseInt() 决定如何解析输入的字符串,因此为了避免错误的解析,建议无论在什么情况下都明确指定基数
多数情况下,我们要解析的都是十进制数值,因此始终将 10 作为第二个参数是非常必要的
1 2 3 4 5 6 |
|
String 类型
ECMAScript 中的字符串是不可变的,也就是说,字符串一旦创建,它们的值就不能改变。要改变某个变量保存的字符串,首先要销毁原来的字符串,然后再用另一个新值的字符串填充该变量
转换为字符串
1 2 3 4 |
|
数值、布尔值、对象和字符串值(没错,每个字符串也都有一个 toString() 方法,该方法返回字符串的一个副本)都有 toString() 方法。但 null 和 undefined 值没有这个方法
多数情况下,调用 toString() 方法不必传递参数。但是,在调用数值的 toString() 方法时,可以传递一个参数:输出数值的基数。默认情况下,toString() 方法以十进制格式返回数值的字符串表示。
在不知道要转换的值是不是 null 或 undefined 的情况下,还可以使用转型函数 String(),这个函数能够将任何类型的值转换为字符串。
1 2 3 4 5 6 7 8 |
|
Object 类型
ECMAScript 中的对象其实就是一组数据和功能的集合。对象可以通过执行 new 操作符后跟要创建的对象类型的名称来创建。而创建 Object 类型的实例并为其添加属性和(或)方法,就可以创建自定义对象
1 |
|
仅仅创建 Object 的实例并没有什么用处,但关键是要理解一个重要的思想:即在 ECMAScript 中,Object 类型是所有它的实例的基础。换句话说,Object 类型所具有的任何属性和方法也同样存在于具体的对象中
Object 的每个实例都具有下列属性和方法:
- constructor: 保存着用于创建当前对象的函数。对于前面的例子而言,构造函数(constructor)就是 Object()
- hasOwnProperty(propertyName): 用于检查给定的属性在当前对象实例中(而不是在实例的原型中)是否存在。其中,作为参数的属性名(propertyName)必须以字符串形式指定(例如:o.hasOwnProperty("name")
)
- isPrototypeOf(object): 用于检查传入的对象是否是当前对象的原型
- propertyIsEnumerable(propertyName): 用于检查给定的属性是否能够使用 for-in 语句来枚举。与 hasOwnProperty() 方法一样,作为参数的属性名必须以字符串形式指定
- toLocaleString(): 返回对象的字符串表示,该字符串与执行环境的地区对应
- toString(): 返回对象的字符串表示
- valueOf(): 返回对象的字符串、数值或布尔值表示。通常与 toString() 方法的返回值相同
由于在 ECMAScript 中 Object 是所有对象的基础,因此所有对象都具有这些基本的属性和方法
vue-cli4 中不能直接使用obj.hasOwnProperty
,
需要写成Object.prototype.hasOwnProperty.call(data, 'date')
删除对象的属性:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
操作符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
逻辑非
1 2 3 4 5 6 |
|
逗号操作符
使用逗号操作符可以在一条语句中执行多个操作
1 |
|
逗号操作符多用于声明多个变量;但除此之外,逗号操作符还可以用于赋值。在用于赋值时,逗号操作符总会返回表达式中的最后一项
1 2 |
|
语句
for-in 语句
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
ECMAScript 对象的属性没有顺序。因此,通过 for-in 循环输出的属性名的顺序是不可预测的。具体来讲,所有属性都会被返回一次,但返回的先后次序可能会因浏览器而异
建议在使用 for-in 循环之前,先检测确定该对象的值不是 null 或 undefined
label 语句
使用 label 语句可以在代码中添加标签,以便将来使用
1 2 3 |
|
这个例子定义的 start 标签可以在将来由 break 或 continue 语句引用。加标签的语句一般都要与 for 语句等循环语句配合使用
遍历对象的方法
for-in 遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
使用 Object.keys() 获取实例属性名组成的数组(ES6)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
1 2 3 4 5 6 7 8 9 |
|
with 语句
with 语句的作用是将代码的作用域设置到一个特定的对象中。with 语句的语法如下:
1 |
|
定义 with 语句的目的主要是为了简化多次编写同一个对象的工作,如下面的例子所示
1 2 3 |
|
上面的几行代码都包含 location 对象。如果使用 with 语句,可以把上面的代码改写成如下所示:
1 2 3 4 5 |
|
在这个重写后的例子中,使用 with 语句关联了 location 对象。这意味着在 with 语句的代码块内部,每个变量首先被认为是一个局部变量,而如果在局部环境中找不到变量的定义,就会查询 location 对象中是否由同名的属性。如果发现了同名属性,则以 location 对象属性的值作为变量的值。
严格模式下不允许使用 with 语句,否则将视为语句错误
由于大量使用 with 语句会导致性能下降,同时也会给调试代码造成困难,因此在开发大型应用程序时,不建议使用 with 语句
函数
1 2 3 |
|
- 匿名函数
1 2 3 4 5 6 7 8 9 |
|
script
标签需要写到button
标签下面
- 函数使用多个函数名
1 2 3 4 5 6 7 8 |
|
输出
1 2 |
|
ECMAScript 函数的参数与大多数其他语言中函数的参数有所不同。ECMAScript 函数不介意传递进来多少个参数,也不在乎传进来参数是什么数据类型。也就是说,即便你定义的函数只接收两个参数,在调用这个函数时也未必一定要传递两个参数。可以传递一个、三个甚至不传递参数,而解析器永远不会有什么怨言。
之所以会这样,原因是 ECMAScript 中的参数在内部是用一个数组来表示的。函数接收到的始终都是这个数组,而不关心数组中包含哪些参数(如果有参数的话)
如果这个数组中不包含任何元素,无所谓;如果包含多个元素,也没有问题。
实际上,在函数体内可以通过arguments
对象来访问这个参数数组,从而获取传递给函数的每一个参数
其实,arguments
对象只是与数组类似(它并不是 Array 的实例),因为可以使用方括号语法访问它的每一个元素(即第一个元素是arguments[0]
,第二个元素是arguments[1]
, 以此类推),使用length
属性来确定传递进来多少个参数
- 动态参数
1 2 3 4 5 6 7 8 9 |
|
- 预解析,函数声明不必须放在调用前面 预解析:浏览器在获得 js 文件的时候,不是立刻去执行代码,而是全篇快速扫描一遍, 把变量预先解析,把变量的声明提前, 函数里的局部变量也会预解析
1 2 3 4 5 6 |
|
1 2 |
|
ECMAScript 函数不能像传统意义上那样实现重载。而在其他语言(如 Java)中,可以为一个函数编写两个定义,只要这两个定义的签名(接受的参数的类型和数量)不同即可。如前所述,ECMAScript 函数没有签名,因为其参数是由包含零或多个值的数组来表示的。而没有函数签名,真正的重载是不可能做到的
全局和局部变量
1 2 3 4 5 |
|
1 2 3 4 5 |
|
虽然省略 var 操作可以定义全局变量,但这也不是推荐的做法。
因为在局部作用域中定义的全局变量很难维护,而且如果有意地忽略了var
操作符,也会由于相应变量
不会马上就有定义而导致不必要的混乱。
而未经声明的变量赋值在严格模式下会导致抛出ReferenceError
错误
- 当有局部变量的时候,使用的是局部变量 -> 就近原则
1 2 3 4 5 6 7 8 9 10 11 12 |
|
test 中没有使用全局变量,打印结果为
1 2 3 |
|
闭包
1 2 3 4 5 6 7 8 9 10 11 |
|
输出
1 2 |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
输出
1 2 |
|
1 2 3 4 5 6 7 8 9 10 11 |
|
输出
1 2 |
|
- 函数立即执行 函数声明和函数执行 放在一起
1 2 3 4 5 6 7 8 9 |
|
输出
1 2 |
|