前言
原文地址 或掘金yeyan1996 好记性不如烂笔头。本人为了消化,对原文逐行敲一遍以加深印象。
JavaScript 简介
一个完整的 JavaScript 由 3 个部分组成,核心(ECMAScript 语法),DOM,BOM,后两者目前已经是可选项了,或者可以抽象为宿主,因为 JS 已经 不仅限运行于浏览器
在 HTML 中使用 JavaScript
在浏览器中使用 JS 可以通过 script 标签来执行 JS 文件,进一步可以分为 3 种方式,
-
内嵌 JS 代码,
-
通过 src 指向本地 JS 文件
-
通过 src 指向某个静态服务器的 JS 文件(域名)
推荐的是使用 src 的形式,相比于内嵌可以利用缓存提高页面加载速度和解析 DOM 的速度,并且,因为 JS 和 HTML 解耦了可维护性更强
当 script 标签是 src 形式的外部脚本中可以设置 defer,async 属性,前者可以让页面解析完毕后再运行脚本,后者则是异步下载脚本并执行,同时会异步的执行 JS 代码,这 2 个属性都是为了解决浏览器必须要等到 script 标签中的 JS 代码下载并执行后才会解析之后的元素从而导致的白屏时间久的问题
<script src="xxx" async></script>
JavaScript 基本概念
标识符
标识符指的是变量,函数,属性的名字,主流的名字以驼峰命名为主,或者 $, _
第一个字符不能是数字(但从第二个字符开始就是合法的)
数据类型
截至今日,JavaScript 有 7 种简单数据类型,1种复杂数据类型
简单数据类型:
- Undefined
- Null
- Boolean
- Number
- String
- Symbol
- BigInt (ES10 草案)
复杂数据类型:
- Object
Function 是 Object 的子类,即继承于 Object
Undefined 类型
Undefined 类型只有一个值,即 undefined,它和 not defined 很容易混淆,它们的异同在于
- 使用 typeof 操作符都会返回 'undefined'
- 使用 undefined 变量是安全的,使用 not defined 的变量会抛出错误
Null 类型
Null 类型也只有一个值,即 null,null 表示一个空对象指针,如果使用 typeof 操作符,返回的类型是 'object',但这只是语言上的 BUG,目前几乎不可能修复,如果使用 instanceof 操作符判断是否是 Object 的实例,会返回 false,证明 null 和 Object 并没有什么关系
undefined 值是派生自 null 值的,所以它们宽松相等
Number 类型
JS 的 Number 类型使用 IEEE754 格式来表示整数和浮点数值,它会导致一些小问题,例如 JS 的 0.1 其实并不是真正的 0.1,它的二进制为 0.001100110011...,无限循环(小数十进制转二进制的规则是乘 2 取整),内部是这样存储的
isNaN 来判断参数是否是 NaN,但是它有个缺陷在于会先将参数转为 Number 类型(同样是隐式转换),所以会出现 isNaN('foo') 返回 true 的情况,ES6 的 Number.isNaN 弥补了这一个缺陷,它会返回 false,证明 'foo' 字符串并不是 NaN
parseInt 和 Number 函数的区别在于,前者是逐个字符解析参数,而后者是直接转换
parseInt 会逐个解析参数 '123foo',当遇到非数字字符或者小数点则停止(这里是字符串 f),会返回之前转换成功的数字,而 Number 则是将整个参数转为数字
(值得一提的是 parseFloat 遇到非数字字符或者第二个小数点,会返回之前转换成功的数字)
操作符
一元操作符
只能操作一个值的操作符叫做一元操作符,后置递增/递减操作符与前置递增/递减有一个重要的区别,后置是在包含它们的语句被求值之后执行的
前者先让 num1 减1,再执行和 num2 累加,后者是先和 num2 累加,再让 num 减1 ,另外一元操作符会先尝试将变量转换为数字
布尔操作符
逻辑与和逻辑非这两个操作符都是短路操作,即第一个操作数能决定结果,就不会对第二个操作数求值
以下常用的逻辑与判断结果
| 第一个操作数 | 操作符 | 第二个操作数 | 结果 |
|---|---|---|---|
| null | && | 任何 | 第一个操作数 |
| undefined | && | 任何 | 第一个操作数 |
| NaN | && | 任何 | 第一个操作数 |
| false | && | 任何 | 第一个操作数 |
| "" | && | 任何 | 第一个操作数 |
| 0 | && | 任何 | 第一个操作数 |
| 对象 | && | 任何 | 第二个操作数 |
| true | && | 任何 | 第二个操作数 |
当第一个参数是假值时,逻辑与返回第一个操作数,反之返回第二个操作数
以下是所有假值的列表:false,null,undefined,0,NaN,""
逻辑或与逻辑与相反,以下常用的逻辑或与判断结果
| 第一个操作数 | 操作符 | 第二个操作数 | 结果 |
|---|---|---|---|
| null | || | 任何 | 第二个操作数 |
| undefined | || | 任何 | 第二个操作数 |
| NaN | || | 任何 | 第二个操作数 |
| false | || | 任何 | 第二个操作数 |
| "" | || | 任何 | 第二个操作数 |
| 0 | || | 任何 | 第二个操作数 |
| 对象 | || | 任何 | 第一个操作数 |
| true | || | 任何 | 第一个操作数 |
当第一个参数是假值时,逻辑或返回第二个操作数,反之返回第一个操作数
加性操作符
在 ECMAScript 中,加性操作符有一些特殊的行为,这里分为操作数中有字符串和没有字符串的情况
有字符串一律视为字符串拼接,如果其中一个是字符串,另一个不是字符串,则会将它转为字符串再拼接,接着会遇到两种情况
- 第二个操作数是对象,则会调用 [[toPrimitive]] 将其转为原始值,如果原始值是字符串那仍会执行字符串拼接
- 操作数不是对象,则直接视为字符串拼接
关系操作符
和加性操作符一样,JS 中的关系操作符(>,<,>=,<=)也会有一些反常的行为
- 两个操作数都是数值,则执行数值比较(如果其中一个是 NaN,则始终返回 false)
- 两个操作数都是字符串,逐个比较字符串的编码值
- 其中一个操作数是对象,则调用 [[toPrimitive]] 转为原始值,按照之前规则比较
- 其中一个操作数是布尔值,会转为 Number 类型,再执行比较
对于第二条,举个例子
console.log('abc' < 'abd') // true
复制代码
内部是这么判断的,由于两个都是字符串,先判断字符串的第一位,发现都是 "a",接着比较第二个,发现也是相同的,接着比较第三个,由于 "c" 的编码比 "d" 小(前者是 99 后者是 100),所以字符串 abc "小于" 字符串 abd
相等操作符
相等操作符和加性,关系操作符一样师承一脉,也有很多奇怪的特点,以至于十几年后的今天还被人诟病,先看一下网上的一些例子
具体我不想展开讲,英语不错的朋友可以直接查看规范,我说一下个人的记忆技巧
- 如果类型相同,直接判断是否相等,不同类型才会发生隐式转换
- 涉及到对象,则会调用 [[toPrimitive]]
- NaN 和任何都不想等,包括自身
- 等式两边都会尽可能转为 Number 类型,如果在转为数字的途中,已经是同一类型则不会进一步转换
- null 和 undefined 有些特殊行为,首先它们两个是宽松相等(==),但不是严格相等(===),除此之外任何值都不会和 null / undefined 宽松/严格相等
综合来说,为了避免隐式转换的坑,尽量使用严格相等(===)
for 语句
for 语句其实是 while 语句衍变而来的,for 语句包含 3 个表达式,通过分号分隔,第一个表达式一般为声明或者赋值语句,第二个表达式为循环终止条件,第三个语句为一次循环后执行的表达式
let i = 0
for (;;i++){
//...
}
复制代码
上述代码会陷入死循环,让浏览器崩溃,原因是第二个表达式没有设置,会被视为始终为 true,即永远不会退出循环,并且每次循环变量 i 都会 +1,同时没有初始语句,代码本身无任何意义,只是说明 for 循环的 3 个表达式都是可选的
如果按照执行顺序来给 for 语句的执行顺序进行排序的话,是这样的
for (/* 1 */let i = 0;/* 2 */i < 10;/* 3 */i++) {
/* 4 */ console.log(i)
}
顺序为 1 -> 2 -> 4 -> 3 -> 4 -> 3 -> 4 -> ... -> 退出
for in 语句
for in 语句会返回对象的属性,返回的顺序可能会因浏览器而异,因为没有规范,所以不要依赖它返回的顺序,而 Reflect.ownKeys ,Object.getOwnPropertyNames,Object.getOwnPropertySymbols 是由 ES6 规范 [[OwnPropertyKeys]] 算法定义的,其内容如下
- 首先顺序返回整数的属性(数组的属性)
- 依次按照创建顺序返回字符串属性
- 最后返回所有符号属性
label 语句
使用 label 语句可以为 for 语句添加标签的功能,当 for 语句内部通过 break,continue 语句退出时,可以额外指定标签名来退出到更外层的循环,这会用在多层 for 循环中
当 i 和 j 都是 5 的时候,会跳过 5 次遍历(55,56,57,58,59),最终结果为 95,即循环执行了 95 次
switch 语句
在 switch 语句中,如果每个条件不写 break 关键字退出判断的话,会发生条件穿透
i 满足第一个 case,所以打印了字符串 25,但是由于没有 break,会无视第二个判断条件直接执行第二个 case 的语句,如果第二个条件也没有 break 还会继续穿透到 default 中
switch 语句中 case 的判断条件是严格相等,字符串 10 不等于数字 10
函数
在 ES6 以前,函数的参数会被保存在一个叫 arguments 的对象中在函数执行的时候被创建,它是一个类数组,它有 length 属性代表参数个数,这里的参数个数是执行函数时传入的参数个数,而不是函数定义的参数个数
即使定义了 3 个参数, arguments 反映的只是函数运行时候的参数个数,另外 arguments 还有一些比较特殊的特性,非严格模式下它和函数运行时的参数会建立一个链接,当参数被修改时会反映到 arguments 上,反之同理
而严格模式不会建立这种链接,两者完全分离,虽然 ES6 仍可以使用 arguments,但是它已经被废弃,推荐使用剩余运算符(...)
函数的参数是按值传递,不是按引用传递,即如果参数是一个对象,则在函数内部,通过形参修改这个对象,会反映到所有指向这个参数的变量
而严格模式不会建立这种链接,两者完全分离,虽然 ES6 仍可以使用 arguments,但是它已经被废弃,推荐使用剩余运算符(...)
函数的参数是按值传递,不是按引用传递,即如果参数是一个对象,则在函数内部,通过形参修改这个对象,会反映到所有指向这个参数的变量
由于按值传递,所以这里变量 obj 和形参 o 都指向同一个堆内存的对象,在 func 内部通过形参 o 往这个对象中添加了 a 属性,会同时反映到变量 obj