JavaScript学习笔记(1):语言基础
基本语法
标识符
作为变量、函数、属性或参数的名称。
规则:
- 区分大小写;
- 第一个字符只能是字母、下划线(_)或美元符号($);
- 其余字符还可以是数字;
- 上述“字母”不只是英文字母,可以是扩展ASCII字符和Unicode字符(但不推荐)。
注释
单行注释:
1 |
|
多行注释:
1 |
|
严格模式
严格模式是ES5添加的一种执行模式,ES3的一些不规范的写法在这种模式下会被处理,对于不安全的操作将会抛出错误。启用严格模式就在脚本开头加上一个裸的字符串:
1 |
|
也可以单独指定一个函数使用严格模式执行,只需要在函数作用域开头加上这个字符串。
语句
语句建议以分号结尾,虽然不加分号在绝大多数情况下也不会有任何问题。
像if之类的语句后跟的语句内容如果只有一行语句,可以不写成语句块,但是建议写成语句块。
关键字和保留字
关键字是有特殊用途的标识符,不能用来当作变量或函数等名称的。ES5关键字有:
- break
- do
- in
- typeof
- case
- else
- instanceof
- var
- catch
- export
- new
- void
- class
- extends
- return
- while
- const
- finally
- super
- with
- continue
- for
- switch
- yield
- debugger
- function
- this
- default
- if
- throw
- delete
- import
- try
保留字是ES标准为将来可能支持的功能保留的关键字,这些关键字也不要作为变量或函数的标识符来用。ES5规定的保留字有:
- 始终保留:
- enum
- 严格模式下保留:
- implements
- package
- public
- interface
- protected
- static
- let
- private
- 模块代码中保留:
- await
变量
JS变量是弱类型的,变量只是个名称,它可以用于保存任意类型的数据。声明变量有var、let、const三种方式,其中let和const在ES6开始才可以使用,是ES6标准新增的内容。
变量应当在声明时初始化,或者确保会在之后使用它之前让它拥有一个可用的值,否则未经初始化的变量值都是undefined。
var声明
var的作用域是函数作用域,也就是说只有函数作用域(和全局作用域)能够关住var声明的变量,函数内使用var声明的变量无法在函数外使用。
全局作用域内声明的var变量会成为window的属性。
var声明的变量会进行变量提升,所谓提升实际就是默认把声明拉到函数作用域(全局作用域)的顶部,也就是无论你在哪里第一次声明一个变量,都好像是在函数作用域的开始声明的一样。声明提升的特点有:
- 无论在哪里的声明都会提升到函数作用域顶部,也就是在声明之前使用它不会报错,但是赋值的行为还是在原来的代码位置,因此在声明之前使用它虽然不会报错,但是它的值是undefined;
- 多次声明一个变量没有问题,如果每次赋值都初始化了值,都把它们当成普通的赋值语句来处理。
有以下例子:
1 |
|
let声明
let声明和var的区别有:
- let声明的作用域是块作用域,块作用域就可以关住let声明;
- let声明不会被提升;
- 在全局作用域使用let声明的变量不会成为window对象的属性。
有以下例子:
1 |
|
可以看到块作用域中的var变量可以被外面访问,let就不行。
有以下例子:
1 |
|
可以看出let变量声明不会被提升,当然也不能重复定义。在let变量声明前的区域被称为暂时性死区。
在没有let声明时,for循环中的var声明会渗透到循环外,因为for循环的块作用域关不住var声明,比如这个例子:
1 |
|
每次循环会往任务队列推一个延迟打印的任务,使用var循环的话循环完了,i也变成了5,延迟打印任务打印的全是5,而let的行为就好像是为每次循环创建了一个新的j,使得每次打印的值都不同。
const声明
const变量和let大部分行为都相同,唯一不同的就是它必须在声明时初始化,因为稍后就无法修改它的值了,唯一给它赋值的机会就是初始化它的时候。
如果它指向引用值,那么这个引用的对象的内容还是可以修改的,只是它不能再引用别的对象了。
const变量适合用于for循环,用于迭代一个不准备改变的量。
无声明
无声明的变量就是不使用关键字直接使用的变量,这样的变量直接就成为全局变量了,无论在哪里直接用,哪怕是在函数内直接拿来赋值,也会直接成为全局的变量,这样的行为是不推荐的。
为了规范代码,在使用时能够用const就不用let,能用let就不用var,而无声明的全局变量最好永远不要用。
数据类型
简单数据类型
JS有七种简单数据类型(原始类型):
- Undefined
- Null
- Boolean
- Number
- String
- Symbol
- BigInt
其中Symbol(符号类型)是ES6新增的,BigInt(大整数类型)是ES10新增的。
Undefined类型
只有一个可能的值,即undefined。
它会被解释为一个假值,因此在使用判断检测时要想好自己是否只希望检测undefined值,不要把其他假值错当成了undefined。
变量声明了但没赋值就会默认是这个值,但注意:
- 未声明的变量和声明了但未赋值的变量是不同的,前者使用会报错,后者使用不会报错只是值为undefined;
- 未声明的变量和声明了但未赋值的变量在使用typeof测试时都会返回
"undefined"
。
如以下例子:
1 |
|
Null类型
它也只有一个可能的值null,也是个假值,表示一个空的对象指针。如果在初始化一个未来用来保存对象的变量时,建议使用null初始化。
Boolean类型
有两个可能的值:true和false。
Boolean()函数用于将其他值转换为布尔值,规则如下:
- 布尔值:原样返回。
- 字符串:空字符串转为false,其他为true。
- 数值:0和NaN转为false,其他为true(包括无穷值)。
- 对象:null转为fasle,其他任意对象都会为true。
- undefined:始终为false。
Number类型
Number类型使用IEEE754标准来保存整数和小数。
字面量:
- 十进制整数:直接书写;
- 八进制整数:(严格模式下不支持八进制)以0开头,但如果任意位数上出现了大于或等于8的数就仍看作是十进制整数;
- 十六进制整数:使用0x前缀;
- 浮点数:带小数点的数就是浮点数,整数部分和小数部分为0的话都可以省略不写,科学计数法也是支持的。
避免直接比较小数是否相等,因为IEEE754的浮点数不能表示所有的小数。
Number类型可表示的值的范围存储在Number.MAX_VALUE
和Number.MIN_VALUE
中,如果计算的结果超过了这个表示范围,结果会存储为无穷值(Infinity)。
无穷值分为正无穷(Infinity)和负无穷(-Infinity),要判断一个值是不是无穷值,使用isFinite()
函数,返回false表示它为无穷值,返回true表示它还在表示范围之内。
此外还有一个特殊的数值NaN:
- 使用0值除以0值得到的是NaN,而使用其他数值除以0值得到的是无穷值;
- NaN参与运算得到的结果也是NaN;
- NaN不与包括它自身在内的任何值相等,要判断一个值是不是NaN,使用
isNaN()
函数,除了NaN值本身外,任何不能转换为数值的值都会使它返回true。
String类型
字符串可以以单引号、双引号、反引号包含。
字符串中可以使用转义字符:
\n
:换行。\t
:制表。\b
:退格。\r
:回车。\f
:换页。\\
:反斜杠。\'
:单引号。\"
:双引号。- \` :反引号。
\xnn
:以十六进制编码的字符。\unnnn
:以十六进制编码的Unicode字符。
注意:
- 字符串是不可变的类型。
- 其他值转换为字符串需要用
toString()
方法,一般不需要传参,只有在对整数调用时,希望获得整数的非十进制字符串表示,才需要传入一个底数作为参数。 - 字符串的
toString()
方法返回自己的一个副本。 - null和undefined没有
toString()
方法,但可以使用String()
转型函数,它的规则是:- 如果值有
toString()
方法,就调用它的该方法; - 如果值为null,就返回
"null"
; - 如果值为undefined,就返回
"undefined"
。
- 如果值有
模板字面量:
- 使用反引号表示;
- 保留字符串中的换行,可以跨行定义;
- 支持插值语法,使用${内容}
的格式进行字符串内插值,插值的内容可以是任意JS表达式,并最终会将运算结果使用String()
转型函数转换为字符串拼接进字符串。
模板字符串的插值语法支持标签函数,标签函数看起来只是一个普通的函数,但它可以接收到模板字符串形成的参数,通过返回的字符串来定义模板字符串最终形成的结果字符串,实现自定义行为,使用时只需要把标签函数作为模板字符串的修饰符:
1 |
|
模板字符串还支持原始字符串,实际上就是使用了内置的标签函数,使得各种转义字符不进行转义,保持它原始的样子:
1 |
|
Symbol类型
符号是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
使用符号通过调用Symbol()
函数来创建一个新符号,这个符号就是独一无二的:
1 |
|
Symbol()
函数不能当作构造函数通过new来调用,因为符号类型是一个原始值,通过new就创建成了对象,这与初衷不符。如果你确实想用对象来包装符号,可以使用Object()
转型函数。
在创建符号时可以传入一个字符串作为参数,但是这个字符串对符号本身没有任何作用,只能作为调试参数:
1 |
|
可以看到传入的字符串对符号没什么影响,字符串一样的符号照样不会有关联,每次调用Symbol()
返回的必定是不同的、独一无二的符号。
符号全局注册表:
为了更好地管理和重用符号,可以使用全局符号注册表,将符号注册到注册表。
注册通过Symbol.for()
静态函数进行,它需要一个字符串作为键,如果该键已存在,说明已注册该符号,就返回已存在的该键对应的符号,如果键不存在,就创建一个新符号并在全局注册表中注册该符号,同时我们可以使用Symbol.keyFor()
静态方法查询某个符号在注册表中对应的键:
1 |
|
符号的主要用途就是作为对象属性的键,在使用符号作为字面量对象的属性名时只能用计算属性语法:
1 |
|
类似于Object.getOwnPropertyNames()
方法只能返回对象的普通属性,Object.getOwnPropertySymbols()
是专为符号属性准备的,将返回所有的符号属性构成的数值,此外还有Object.getOwnPropertyDescriptors()
则会返回两种属性都包含的描述对象。注意前两者是返回的数组,最后一个返回对象各个属性的描述对象(含四个属性配置项)构成的对象。
常用内置符号:
Symbol.asyncIterator
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.iterator
Symbol.match
Symbol.replace
Symbol.search
Symbol.species
Symbol.split
Symbol.toPrimitive
Symbol.toStringTag
Symbol.unscopables
数值转换
有三个函数可以将非数值类型转换为数值类型,分别是Number()
、parseInt()
和parseFloat()
。
Number()
可以用于任何类型,后两个用于字符串转数值。
Number()
的转换规则:
- 数值:直接返回。
- 布尔值:true转换为1,false转换为0。
- null:转换为0。
- undefined:转换为NaN。
- 字符串:按照以下规则转换:
- 如果收到的字符串是一个完美的数值字面量表示,就把它转换为对应的数值,要求最多在字符串前后含有若干空白字符,此外不能含有任何不合法的字符。
- 如果收到的是空字符串,转换为0。
- 如果是其他任何情况,都返回NaN。
- 对象:先调用对象的
valueOf()
方法,尝试将获得的值应用上述规则转换为数值,如果得到的是NaN,就再调用它的toString()
方法按照字符串的规则重新转换,返回转换的结果。
而parseInt()
和parseFloat()
是由字符串转换为数值,规则就不一样了,它会尽可能返回一个数值:
- 会忽略开头的空白符,从遇到的第一个非空白字符开始转换。
- 如果遇到的第一个非空白字符不是数字,直接返回NaN。
- 对于合法的数值字面量字符进行转换,当遇到了第一个不合法的字符,转换终止,但已转换的部分会保留下来,返回已转换的部分。
parseInt()
可以按照某个进制来解析整数,通过第二个参数来传递底数,如想把字符串中的数值当作n进制数,就在第二个参数中传入n,返回的始终是十进制的整数,没有第二个参数就会按照解析到的数值字面量来决定(十进制或十六进制,不支持八进制字面量)。parseFloat()
则不能指定底数,它只能解析十进制数,在能返回整数的情况下它不会返回小数。- 对于空字符串返回NaN。
复杂数据类型
复杂数据类型(引用类型)只有Object一种,是一个无序键值对的集合。
Object类型
Object类型是一组数据(属性)和功能(方法)的集合。
可以通过创建Object类型的实例来创建自己的对象,然后再给对象添加属性和方法。创建对象:
1 |
|
或者在没有参数时省略括号:
1 |
|
Object上的方法有:
constructor()
:用于创建当前对象的函数。hasOwnProperty()
:用于判断当前对象实例上是否存在给定的属性。isPrototypeOf()
:用于判断当前对象是否为另一个对象的原型。propertyIsEnumerable()
:用于判断给定的属性是否可以使用for-in语句枚举。toLocaleString()
:返回对象的字符串表示,该字符串反映对象所在的本地化执行环境。toString()
:返回对象的字符串表示。valueOf()
:返回对象对应的字符串、数值或布尔值表示。通常与toString()
的返回值相同。
由于Object是所有对象的基类,因此所有对象都会有这些方法。
关于对象的更多内容,见“面向对象”章节。
typeof操作符
typeof操作符用来确定一个值的类型,对一个名称或值使用该操作符会返回以下字符串之一:
"undefined"
"boolean"
"string"
"number"
"symbol"
"bigint"
"object"
"function"
注意:typeof操作符对null返回"object"
,因为null值被认为是一个类似于C语言中空指针的东西。
操作符
一元操作符
一元操作符包含自增自减操作符和一元加减操作符。
自增自减操作符与C语言中的自增自减操作符的行为完全相同,但对于非数值类型会进行隐式转换为数值之后再自增自减操作。
一元加减(正负)操作符作用于一个值时,如果它是数值,则直接生效,如果它是其他值,则进行隐式类型转换之后再运算。
也就是自增自减操作符和一元加减操作符返回的永远是个数值类型的结果。
隐式类型转换都是使用转型函数来进行的,即Number()
。
位操作符
ECMAScript中的所有数值都以IEEE 754 64位格式存储,但位操作并不直接应用到64位表示,而是先把值转换为32位整数,再进行位操作,之后再把结果转换为64位。对开发者而言,就好像只有32位整数一样,因为64位整数存储格式是不可见的。
当负数被以二进制字符串输出时,输出的并不是完全的在内存中的表示方式(补码),而是保留了负号的二进制原码:
1 |
|
特殊值NaN和Infinity在位操作中都会被当成0处理。
同样的,位操作符对非数值使用时也会进行隐式类型转换转为数值类型。
按位与或非
和号(&)来表示按位与,管道符(|)来表示按位或,它们的计算过程都包含了符号位。
波浪符(~)来表示按位非,返回数值每一位取反后的结果(包括符号位)。因此在宏观上,看起来就是对数值取相反数再减一,如25按位非得到的就是-26。
按位异或
相同的二进制位运算得到0,相异得到1。
左移和右移
左移(<<)会将全部的二进制位(包含符号位)向左移动,末尾补零。
右移分为有符号右移(>>)和无符号右移(>>>),区别是有符号右移会使用符号位填补左侧空位,无符号右移始终使用0填补。
布尔操作符
逻辑非(!)、逻辑与(&&)、逻辑或(||)。
逻辑非先将值转换为布尔值,再取反。逻辑非始终得到的是布尔值。同时使用两个感叹号,可以实现将任意值转换为布尔值。
由于短路逻辑的存在,逻辑与和逻辑或并不一定会返回布尔值。
短路逻辑:
- 逻辑与:如果第一个操作数解释为“假”值,那么直接返回第一个操作数,否则返回第二个操作数。
- 逻辑或:如果第一个操作数解释为“真”值,那么直接返回第一个操作数,否则返回第二个操作数。
当第二个操作数被短路时,则根本不会对它运行求值。
算术操作符
乘法操作符
操作数会被隐式类型转换为数值,对特殊值有以下处理:
- 如果乘积超出了表示范围,会返回±Infinity;
- 有NaN参与运算,结果也是NaN;
- Infinity乘以0的结果是NaN,乘以其他数值的结果是Infinity,但正负号根据两操作数的正负决定。
有以下例子:
1 |
|
除法操作符
操作数会被隐式类型转换为数值,对特殊值有以下处理:
- 如果乘积超出了表示范围,会返回±Infinity;
- 有NaN参与运算,结果也是NaN;
- Infinity除以Infinity和0除以0的结果都是NaN;
- 非0数值除以0,根据两操作数的符号返回±Infinity;
- Infinity除以非Infinity的任何数值,根据两操作数的符号返回±Infinity。
有以下例子:
1 |
|
取模操作符
操作数会被隐式类型转换为数值,对特殊值有以下处理:
- 如果被除数是Infinity,除数是有限值,则返回NaN。
- 如果被除数是有限值,除数是0,则返回NaN。
- 如果是Infinity除以Infinity,则返回NaN。
- 如果被除数是有限值,除数是Infinity,则返回被除数。
- 如果被除数是0,除数不是0,则返回0。
指数操作符
指数操作符使用两个乘号表示,是ES7新增的内容,等同于Math.pow()
的功能,同样会进行操作数的隐式类型转换。
同时也有自己的指数赋值操作符(**=)。
加法操作符
和前面的操作符不同,它返回的不一定是数值:
- 如果两个操作数中没有字符串,则将两个操作数都隐式转换为数值再进行数值的运算;
- 如果两个操作数中有字符串,则会将两个操作数都隐式转换为字符串然后进行字符串的拼接操作。
对于两个数值的运算:
- 如果有任一操作数是NaN,则返回NaN;
- 如果是Infinity加Infinity,则返回Infinity;
- 如果是-Infinity加-Infinity,则返回-Infinity;
- 如果是Infinity加-Infinity,则返回NaN;
- 如果是+0加+0,则返回+0;
- 如果是-0加+0,则返回+0;
- 如果是-0加-0,则返回-0。
减法运算符
减法的各种行为都如同把第二个操作数取相反数再和第一个数相加一样,因此可类比加法的规则,但是唯一的不同是加法中有一操作数为字符串则结果是字符串的拼接,而减法始终会把两个操作数隐式转换为数值进行运算。
关系操作符
大小比较
包括小于、大于、小于等于和大于等于四个关系运算符,它们总是返回布尔值,比较时进行隐式类型转换的规则为:
- 如果两个操作数都是数值,那么直接比较大小(涉及到NaN的比较都会直接返回false);
- 如果两个操作数都是字符串,那么比较两个操作数的字典序(以ASCII大小为依据);
- 如果其中一个操作数为数值,则将另一操作数转为数值进行比较;
- 布尔值始终转换为数值参与比较;
- 对象作为操作数将调用其valueOf方法使用取得的值进行比较,如果valueOf得不到合适的值将调用其toString方法参与比较。
相等比较
相等比较的操作符分为两组,分别为相等(==)和不等(!=)、全等(===)和不全等(!==)。
区别在于相等和不等会在比较时进行类型转换,全等和不全等必须比较同类型的值,类型不同的值直接会判定为不等。
相等和不等判断时的规则为:
- 有NaN参与比较,相等操作符直接返回false,不等操作符直接返回true,两个NaN比较也是如此;
- null与undefined判断为相等;
- 布尔值将转换为数值来参与比较;
- 字符串和数值比较时,将字符串转为数值再比较;
- 两个对象比较则判断它们是不是同一个对象,如果是同一个对象的引用则相等,否则不等;
- 如果只有一个操作数是对象,那么调用它的
valueOf()
方法用取得的值进行比较。
赋值操作符
赋值包括简单赋值操作符(=)和复合赋值操作符(*=、/=、%=、+=、-=、<<=、>>=、>>>=)。
条件操作符
条件操作符的行为与C语言的三元条件运算符的行为完全一致。
逗号操作符
逗号操作符的行为与C语言的逗号运算符的行为完全一致,可以将多个表达式放在一个语句中执行,最终整个逗号表达式的值是最后一个子表达式的值。
语句
if语句
if语句接受一个表达式,并将其进行隐式类型转换,得到一个布尔值,根据布尔值的真假决定是否执行后面的代码块。
可以使用if-else、if-else if-else构成分支语句。
switch语句
switch语句类似于C语言中的switch语句,但是不同的是JS增强了它。
JS的switch语句可以使用任何类型的变量作判断,同时case的值也可以使用任意的变量和表达式,而不需要一定是常量。switch语句在判断变量和每个case时使用的是全等操作符比较。
while循环
与C语言中的while循环的表现完全一致,先判断后循环,最少循环零次。
do-while循环
与C语言中的do-while循环的表现完全一致,先循环后判断,至少循环一次。
三种for循环
传统for循环
传统for循环语句与C语言中的for循环的表现类似,需要提供初始化语句、判断表达式和状态转移表达式,且三者都可以省略。
for-in循环
for-in语句用于枚举对象中的非符号键属性。
ECMAScript中对象的属性是无序的,所有可枚举的属性都会返回一次,但返回的顺序可能会因浏览器而异。
如果for-in循环要迭代的变量是null或undefined,则不执行循环体。
for-of循环
for-of循环用于遍历可迭代对象的元素。
for-of循环会按照可迭代对象返回的默认迭代器的next()
方法产生值的顺序迭代元素。具体见后续迭代器与生成器的章节。
如果尝试迭代的变量不支持迭代,则for-of语句会抛出错误。
ES9对for-of语句进行了扩展,增加了for-await-of循环,以支持生成期约的异步可迭代对象。
标签语句
标签语句的格式是使用一个名称加一个冒号标记某一语句,后面可通过continue和break语句引用。
流程控制语句
流程控制语句包括break语句和continue语句,主要用来控制循环的流程。
它们的行为与C语言中类似,唯一不同的是它们可以使用标签,从而一次性跳出多层嵌套的循环,有以下例子:
1 |
|
with语句
with语句将某个变量作为执行的上下文,当使用一个名称时,首先会判断这个名称是否是块内的局部变量,如果不是,则向外层的with作用域中找,即查找with指定的对象上是否有该属性,如果有,就使用该属性:
1 |
|
严格模式下不能使用with语句,否则会抛出错误。因为with语句影响性能且难以调试其中的代码。
函数
ECMAScript中的函数使用function关键字声明,后跟一组参数,然后是函数体。
ECMAScript中的函数不需要指定是否返回值。任何函数在任何时间都可以使用return语句来返回函数的值。
return语句也可以不带返回值,这时函数会立即停止执行并返回undefined。
严格模式对函数的限制:
- 函数不能以eval或arguments作为名称;
- 函数的参数不能叫eval或arguments;
- 两个命名参数不能拥有同一个名称。
更多关于函数的内容见后面章节。