JavaScript: 基本使用
JavaScript语法
简介
JS实现了html文档与用户的更丰富的交互,实现在客户端处理部分信息而不是每次交互都向服务器报告。本身的一套标准为ECMAScript,除此以外还和DOM、BOM紧密相连,是这三个部分实现了JS的功能
DOM和BOM都是API(可以理解为一个封装好的类),分别定义了对html文档、对浏览器的接口和方法,在今后将会涉及,在这里着重讲述语法
JS的设计有许多C++及类C++语言的影子,理解起来不难
与HTML的接口
<script>
想在HTML文档中运行js脚本,只需要添加<script></script>元素,虽然可以直接在元素内添加行内脚本,但更好的做法是引用外部文件,好处有:
- 方便管理、修改
js代码 - 一份多用,方便浏览器缓存使用
也有坏处,如果引用了其它域的文件,则有可能被传入恶意脚本;所以引用外部文件应该确保安全可信
<script>元素含有诸多属性:
src:类似<img>的src,用于指示外部文件来源,可配合<base>使用相对地址;需要注意如果一条<script>元素既有src又有内部代码,那么浏览器会忽视内部,只执行外部文件integrity:接收资源时先对比数字签名,确保其来源可信,是防范恶意脚本的武器async:立即下载这条元素指示的外部文件,但不影响其它页面或脚本的加载,即异步脚本;异步脚本脱离了执行顺序,所以不应该在里面进行DOM操作,以免导致异常defer:立即下载这条元素指示的外部文件,但可被推迟到所有其它页面或脚本加载完再执行;这样的脚本没有脱离顺序而只是延后执行了
js本身并不包含DOM等,所以单独运行访问window等对象的程序是有错误的,只有在html文档中才可以正常运行
脚本位置
以前,脚本都放在<head>内,以集中js和css的外部文件;但这将使浏览器在加载完所有脚本后才开始渲染<body>的内容,所以现在都选择将<script>部分放在<body>最后
数据类型
基本数据类型
js的数据类型管理是松散的,它的关键字不决定数据类型而是作用域等;有六种基本数据类型是原始类型,其中五种类型含有字面量(一个常量):
Number:数值类型,字面量包括全体数字、NaN、Infinity与-InfinityString:字符串类型,字面量包括所有字符串Boolean:布尔值类型,字面量有true与falseUndefined:未定义类型,字面量只有undefined,没有相应函数Null:空类型,字面量只有null,它是一个空指针,所以用typeof null返回的是object类型,没有相应函数
剩下的一个类型为Symbol(符号类型),它没有字面量语法,要用特殊的函数来声明;设计如此特殊的Symbol是为了提供一个全局唯一的标识符,以避免属性键冲突问题
原始值与引用值
以上六种数据类型都是原始值,其对象没有属性和方法,且互相赋值时进行的操作是完全复制;这些类型和Object类型可以作为Object类型对象的属性,对象类型的对象实质上是引用值,互相赋值时进行的操作是起别名,而不是创建副本
要创建一个原始值,可以直接使用字面量语法,或是调用相应类型的函数:
1 | let val_1 = val; // 使用字面量,自动分析val的类型. |
要创建一个对象类型的值,可以使用字面量,也可以使用new加上一个类型,这相当于一个构造函数:
1 | let obj = {}; // 使用字面量,创建空对象. |
如果提供了指定的类型(obj_2),或是使用Object()时提供的是原始值(obj_1,obj_3),那么这种方式得到的对象称为包装对象,可以简单理解为被允许含有属性和方法的原始值,在之后会提到
创建好一个对象之后,可以随时为它添加属性,也可以在初始化时赋予:
1 | let obj = { // 初始化时添加属性. |
一个数组也是Object类型的,它是一种特殊的对象,其中的成员都是属性,每个属性都有一个索引(即下标,从0开始)
Symbol
Symbol类型的值不能通过字面量声明,而且,虽然它可以作为属性,但不能是包装对象:
1 | let sym_0 = Symbol(); // 声明一个唯一的标识符. |
括号内的字符串(如果不是则调用String())只是一个解释,不影响这个值的唯一性;所以就算两个值使用同样的解释,它们也是不一样的
如果想重复用同一个Symbol,除了直接赋值,还可以用注册表,能更好地管理符号:
1 | let sym_1 = Symbol.for('key'); // 注册一个为'key'的键,同时它也作为符号的解释. |
同样,虽然使用相同的解释,由注册表创建的符号和一般形式创建的符号是不同的:
1 | let sym_1=Symbol.for('k'), sym_2=Symbol.for('k'), sym_3=Symbol('k'); |
数据间的转换
数据类型管理松散虽然方便,但也因此带来一些麻烦,为此需要熟悉数据间的转换规则,可以调用转类型函数:
Number():- 以下为合法数值:如果是小数,则必须含
'.',且小数点前后至少存在一边;如果是假值(例如Infinity),则返回假值;如果以0开头,则为八进制数;如果以0x或0X开头,则为十六进制数 - 对字符串,如果它只是合法数值加上了引号(空字符串为0),则原封不动变成数值,注意它检测不出八进制数,因为它会忽略开头的0;否则返回
NaN - 对布尔值,
true返回1,false返回0 - 对
undefined,返回NaN - 对
null,返回0 - 对对象,如果是包装对象、或是只含一个元素的数组,则以上述规则转换;如果是空对象,则返回
NaN - 不能将符号类型转换成数值
- 以下为合法数值:如果是小数,则必须含
parseInt()/parseFloat():如果只想将字符串转换成整数或小数,可以选择它们,它们会取字符串的第一个数值(直到遇到非整数/小数字符时停止,如果没有则为NaN)。它与Number()逻辑上有些不同,例如空字符串返回NaN而不是0,而"12abc"返回12而不是NaN。parseInt()还接受第二个参数,以确定数值的进制(默认为自动识别)String():- 所有字符串都是合法的,可以用
''、""、``(反引号)包围,用'\'转义,但只有用反引号包围的字符串能够解析插值符 - 对所有假值和数值,一般是原封不动;如果数值是十六进制或八进制,则先转换成十进制
- 对符号类型,返回
Symbol(description) - 对对象,如果是包装对象或数组,一般原封不动;其它会返回
"[object Object]"
- 所有字符串都是合法的,可以用
String.raw``:将反引号包围的字符串视作原始字符串,不会解析转义字符、插值等Boolean():- 对数值,非0或
Infinity等假值返回true,0或NaN返回false - 对字符串,非空返回
true,否则返回false - 对对象和符号类型,都返回
true - 对未定义与
null,返回false
- 对数值,非0或
Symbol():调用String()undefined与null:只能转换成其它类型,不能由其它类型转换而来Object():将其它类型转换成对象
除未定义与空类型都有toString()方法,相当于调用String()
引用类型
介绍
由基本数据类型可组成对象类型,将这个设计好的对象类型称为一个引用类型;类似面向对象中的类(但js中的引用类型不具备类的许多特征,是不完整的类)
创建一个引用类型与数据类型相似,不带new则是创建一个与基本数据类型对应的值(隐式调用这个引用类型的类型转换方法),带new则是使用构造函数,创建一个对象类型的引用值,一个新实例
基本引用类型
标准提供了一些自带的引用类型,例如之前所说的五种含有构造函数的基本数据类型:
Date:时间类型,继承String,重写toString()(返回一个字符串,表示所在时区、时间)和valueOf()(返回一个数值,表示距离UTC:1970.1.1.00:00:00的毫秒)RegExp:正则表达式类型,字面量由两个’/’夹住的匹配文本、和跟在后面的标记字母组成。例如/abc/g匹配整个字符串的abc,标记可以一起使用,标记有:g:查找全部而非第一个字符串i:不区分大小写m:不因达到行末而停止
Boolean,Number,String:之前提到的包装对象就是用这些引用类型创建的,原始值没有属性和方法,但为了让原始值有着对象的行为,js保留了这些引用类型。当通过原始值调用方法时,系统会调用这些引用类型的构造函数和方法(这个临时对象生存周期在句内,这是不能给一个原始值添加属性或方法的原因)。不建议显式创建包装对象Error,ReferenceError等:用于处理错误的类型,例如访问未声明对象会抛出ReferenceError
一些内置对象
不详细展开:
Global:它无法被访问,所有在全局范围内定义的对象、函数都将成为它的成员,包括标准已经定义的parseInt()等。它还有其它的重要的成员函数:encodeURIComponent():编码URI,方便传递给浏览器。与encodeURI()(编码所有非标准字符)相比,它会编码所有非字母字符,更常用decodeURIComponent():解码URI,替换所有特殊字符eval():解释提供的字符串,相当于运行所提供的字符串,方便处理用户输入。使用它应慎重,因为会遭到恶意攻击
window:它是Window引用类型的一个内置实例,是DOM展示出来的接口,被视为Global的代理Math:存放数学常量和方法的对象,含有E,PI等常量,max(),min(),ceil(),floor(),random()等方法
集合引用类型
Array:数组,它的每个槽位可以放不同数据类型的值,构造一个Array有多种方法:1
2
3let arr = Array(Number); // 构造含Number个元素的数组.
let arr2 = Array(...); // 构造内容为...的数组.
let arr3 = new Array(); // 与let arr3 = Array();等价.Map:映射,专门用于存储多个键值对,虽然很多方法可以由Object实现,但也有不同:它可以用任何数据类型作为键,且会维护顺序方便迭代WeakMap:弱映射,与Map的区别在于它的键的数据类型只能是Object,它的弱在于存储的键是弱实例,不属于正式的引用,不会阻止垃圾回收Set:数据的集合,在功能上像是加强的Map,也支持顺序迭代WeakSet:弱集合,与Set的区别在于它的数据只能是Object类型的,同样不会阻止垃圾回收
运算符
不同类型的数据可以相互转换,还存在一些假值,这使运算符较为琐碎,也因此要注意表达式的运算顺序:
Symbol类型不能参与运算;双目运算中只有一边是对象类型时,将调用valueOf()方法- 单目
!:调用Boolean(),再取反;两次!相当于一次Boolean() - 单目
+/-:对数字和Infinity则是变为正/负值(0也分正负,但+0 === -0),NaN则还是NaN;字符串类型会先调用Number();其它类型会先调用String(),再调用Number() - 双目
+/-:- 两边均为数值:存在一边为
NaN,则为NaN;无穷减无穷(或负无穷加无穷)也为NaN;无穷加无穷为无穷;具体数和无穷相加减得到这个无穷 - 其它情况相加:都调用
String(),然后拼接 - 其它情况相减:都调用
Number(),然后运算
- 两边均为数值:存在一边为
- 双目
*:- 两边均为数值:存在一边为
NaN,则为NaN;无穷与其它值相乘还是无穷,同号正异号负 - 只有数值和字符串:先调用
Number(),再相乘 - 其它情况:先调用
String(),再调用Number()
- 两边均为数值:存在一边为
- 双目
/:- 两边均为数值:一般数字都视作小数运算;如果存在一边为
NaN、或0/0、无穷除无穷,则为NaN;非0除以0得无穷;非无穷除以无穷得0 - 只有数值和字符串:先调用
Number(),再相除 - 其它情况:先调用
String(),再调用Number()
- 两边均为数值:一般数字都视作小数运算;如果存在一边为
- 双目
**:类似python,求幂,与数学逻辑类似;1的无穷为NaN - 比较操作符:
- 两边均为数值:任一为
NaN时,小、大、等于均返回false(包括NaN==NaN);Infinity等于Infinity;null等于undefined - 两边均为字符串:逐个比较字符编码
- 其中一边为数值或布尔值:将两边转换成数值
- 两边都是对象:查看是否指向同一对象
- 两边均为数值:任一为
- 全等(
===)、不全等(!==):上面的比较操作符比较反直觉,而这两个运算符类似强类型比较,它们不会转换两边的类型;对全等来说,只有在两边类型、值完全一致时才会返回true - 三目运算符:
exp ? T_val : F_val
关键字
定义变量
关键字并不指定变量的数据类型,而是指定其作用域:
1 | val_0; // 全局变量,第一次声明时必须初始化. |
使用var时,声明语句会提升至函数头部,且只保留一次声明:
1 | var val = 1; |
所以同名的var变量在函数体内,即使被块分隔,却始终是一个变量;为了防止不经意的重名,不推荐使用var
而let和const则更讨喜,它们的声明语句不会提升,且作用域为块内,所以块内外的量是不同的;但因为不会提升也不会合并,所以不能在一个块内用let和const声明同名变量;由于一切对象都以引用值传递,所以优先考虑使用const,其次为let,最后是var
1 | let a; |
一个量可以通过插值符顺畅地在字符串中解析:
1 | let a = 1, b = 2; |
对于全局变量,如果已确保不再使用,可以赋null,这有利于浏览器进行内存回收
循环语句
除了C的if、while、do-while,还提供了几种语句:
for (variable in object)(for-in):遍历对象的非符号键属性,在每一轮循环中赋值给variable,顺序未知for (variable of iterable)(for-of):遍历可迭代对象(例如字符串、数组)的元素,在每一轮循环中赋值给variable,以迭代顺序输出for await (variable of iterable)(for-await-of):for-of的扩展,用于遍历异步可迭代对象labelName : 循环:给循环加标签,结合continue与break在嵌套循环中使用with(object):创建一个属于object的块,在块内使用变量时首先检查是否已声明,若无则检查是否为这个object的成员方法。因此块内调用这个object的方法时有时不用显式写出它,但它影响性能、难以调试维护,在严格模式下禁用也不推荐使用
实际上每轮循环中的variable都是临时创建并初始化的,在一轮结束后会丢弃。关键字let,const都可使用,只是在块内会有区别,多次强调的是应该多用const!
Js的switch语句在C的基础上,可以对任何数据类型进行switch,case后也可以接表达式或变量;在比较时使用全等,不会进行类型转换
定义函数
只需要关键字function,就可以定义一个函数,在不接参数的return后,跳出函数并返回undefined
函数对返回类型管理不严格,但不建议将返回undefined和返回实际值混合使用
迭代器
迭代器让使用者不需要事先得知其数据结构就可以顺序访问其值,迭代器关注的只有”如何访问下一个值”这个问题。可迭代对象(iterable)通常是集合引用类型,即包含元素有限、含有顺序,具体是看是否实现了[Symbol.iterator]()方法,它会返回一个可迭代对象,调用第一次next()后它将指向第一个元素
标准原生的可迭代对象拥有如下方法:
next():无论是否存在下一元素,都会顺序迭代一次,返回一个iteratorResult对象
iteratorResult对象必然含有两个属性:
done:判断现在的iterable是否可以继续调用next()value:如果可以调用,则输出现在iterable指向的值