这是我第三次翻开红宝书也就是《 JavaScript 高级程序设计第三版》,不得不说,虽然书有一些年份,不少知识点也不适合现代的前端开发,可是对于想要掌握 JavaScript 基础的前端新手,亦或是像我同样想找回曾经遗忘在记忆角落的那些碎片知识,这本书依旧很是的适合,不愧被成为 "JavaScript 圣经"javascript
本文是读书笔记,之因此又一次选择读这本书还有一个理由,以前都是记的纸质笔记,此次想把它做为电子版,也算是对以前知识的整理html
本文篇幅较长,目的是做为个人电子版学习笔记,我会尽量去其糟粕,取其精华,同时我会添加一些书上未记载但很重要的知识点补充
前端
let's gojava
一个完整的 JavaScript 由 3 个部分组成,核心(ECMAScript 语法),DOM,BOM,后二者目前已是可选项了,或者能够抽象为宿主,由于 JS 已经不只限运行于浏览器node
在浏览器中使用 JS 能够经过 script
标签来执行 JS 文件,进一步能够分为 3 种方式,内嵌 JS 代码,经过 src 指向本地 JS 文件,经过 src 指向某个静态服务器的 JS 文件(域名),推荐的是使用 src 的形式,相比于内嵌能够利用缓存提升页面加载速度和解析 DOM 的速度,而且,由于 JS 和 HTML 解耦了可维护性更强git
当 script 标签是 src 形式的外部脚本,中能够设置 defer
,async
属性,前者可让页面解析完毕后再运行脚本,后者则是异步下载脚本并执行,同时会异步的执行 JS 代码,这 2 个属性都是为了解决浏览器必需要等到 script 标签中的 JS 代码下载并执行后才会解析以后的元素从而致使的白屏时间久的问题算法
<script src="xxx" async></script>
复制代码
标识符指的是变量,函数,属性的名字,主流的名字以驼峰命名为主,或者 $, _数组
第一个字符不能是数字(但从第二个字符开始就是合法的)浏览器
// illegal
let 123hello = '123'
// legitimate
let $123hello = '123'
let helloWorld = '123'
let hello123World = '123'
let _$hello = '123'
复制代码
截至今日,JavaScript 有 7 种简单数据类型,1种复杂数据类型缓存
简单数据类型:
复杂数据类型:
Function 是 Object 的子类,即继承于 Object
Undefined 类型只有一个值,即 undefined,它和 not defined 很容易混淆,它们的异同在于
let foo
console.log(typeof foo) // 'undefined'
console.log(typeof bar) // 'undefined'
console.log(foo) // undefined
console.log(bar) // Uncaught ReferenceError: bar is not defined
复制代码
Null 类型也只有一个值,即 null,null 表示一个空对象指针,若是使用 typeof 操做符,返回的类型是 'object',但这只是语言上的 BUG,目前几乎不可能修复,若是使用 instanceof 操做符判断是不是 Object 的实例,会返回 false,证实 null 和 Object 并无什么关系
console.log(typeof null) // 'object'
console.log(null instanceof Object) // false
复制代码
undefined 值是派生自 null 值的,因此它们宽松相等
console.log(undefined == null) // true
复制代码
JS 的 Number 类型使用 IEEE754 格式来表示整数和浮点数值,它会致使一些小问题,例如 JS 的 0.1 其实并非真正的 0.1,它的二进制为 0.001100110011...,无限循环(小数十进制转二进制的规则是乘 2 取整),内部是这样存储的
能够经过 Number 函数,将传入的参数转为相应的 Number 类型(注意隐式转换的坑)
console.log(Number('123')) // 123
console.log(Number(null)) // 0
console.log(Number(undefined)) // NaN
console.log(Number('false')) // NaN
console.log(Number(true)) // 1
复制代码
NaN 属于 Number 类型,且 NaN 不等于自身,能够经过 window 对象的 isNaN
来判断参数是不是 NaN,可是它有个缺陷在于会先将参数转为 Number 类型(一样是隐式转换),因此会出现 isNaN('foo')
返回 true 的状况,ES6 的 Number.isNaN
弥补了这一个缺陷,它会返回 false,证实 'foo' 字符串并非 NaN
console.log(NaN === NaN) // false
console.log(isNaN(NaN)) // true
console.log(isNaN('foo')) // true 但这是不合理的,由于 'foo' 并非 NaN
console.log(Number.isNaN('foo')) // false
复制代码
window.isNaN 是用来判断参数是否是一个数字,Number.isNaN 是用来判断参数是否是 NaN
parseInt 和 Number 函数的区别在于,前者是逐个字符解析参数,然后者是直接转换
console.log(parseInt('123.456')) // 123
console.log(parseInt('123foo')) // 123
console.log(Number('123foo')) // NaN
复制代码
parseInt 会逐个解析参数 '123foo',当遇到非数字字符或者小数点则中止(这里是字符串 f),会返回以前转换成功的数字,而 Number 则是将整个参数转为数字
(值得一提的是 parseFloat 遇到非数字字符或者第二个小数点,会返回以前转换成功的数字)
ECMAScript 中的字符串是一旦建立,它们的值就不可改变,若是须要改变某个变量保存的字符串,须要销毁原来的字符串,再用另外一个新值字符串填充该变量
DOM 和 BOM 对象都是由宿主提供的宿主对象,这里的宿主即浏览器,换句话非浏览器环境可能会没有浏览器上的一些全局变量和方法,例如 node 中就没有 alert 方法
只能操做一个值的操做符叫作一元操做符,后置递增/递减操做符与前置递增/递减有一个重要的区别,后置是在包含它们的语句被求值以后执行的
let num1 = 2
let num2 = 20
let num3 = --num1 + num2 // 21
let num4 = num1 + num2 // 21
复制代码
let num1 = 2
let num2 = 20
let num3 = num1-- + num2 // 22
let num4 = num1 + num2 // 21
复制代码
前者先让 num1 减1,再执行和 num2 累加,后者是先和 num2 累加,再让 num 减1 ,另一元操做符会先尝试将变量转换为数字
逻辑与和逻辑非这两个操做符都是短路操做,即第一个操做数能决定结果,就不会对第二个操做数求值
let num = 0
true || num++
console.log(num) //0
复制代码
如下经常使用的逻辑与判断结果
第一个操做数 | 操做符 | 第二个操做数 | 结果 |
---|---|---|---|
null | && | 任何 | 第一个操做数 |
undefined | && | 任何 | 第一个操做数 |
NaN | && | 任何 | 第一个操做数 |
false | && | 任何 | 第一个操做数 |
"" | && | 任何 | 第一个操做数 |
0 | && | 任何 | 第一个操做数 |
对象 | && | 任何 | 第二个操做数 |
true | && | 任何 | 第二个操做数 |
当第一个参数是假值时,逻辑与返回第一个操做数,反之返回第二个操做数
如下是全部假值的列表:false,null,undefined,0,NaN,""
逻辑或与逻辑与相反,如下经常使用的逻辑或与判断结果
第一个操做数 | 操做符 | 第二个操做数 | 结果 |
---|---|---|---|
null | || | 任何 | 第二个操做数 |
undefined | || | 任何 | 第二个操做数 |
NaN | || | 任何 | 第二个操做数 |
false | || | 任何 | 第二个操做数 |
"" | || | 任何 | 第二个操做数 |
0 | || | 任何 | 第二个操做数 |
对象 | || | 任何 | 第一个操做数 |
true | || | 任何 | 第一个操做数 |
当第一个参数是假值时,逻辑或返回第二个操做数,反之返回第一个操做数
在 ECMAScript 中,加性操做符有一些特殊的行为,这里分为操做数中有字符串和没有字符串的状况
有字符串一概视为字符串拼接,若是其中一个是字符串,另外一个不是字符串,则会将它转为字符串再拼接,接着会遇到两种状况
console.log("123" + 123) // "123123"
console.log('123' + NaN) // "123NaN"
console.log("123" + {}) // "123[object Object]"
console.log("123" + undefined) // "123undefined"
复制代码
若是两个操做数都不是字符串,又会有两种状况
值得一提的是,涉及到 NaN 的四则运算最终结果都是 NaN(另外一个操做数为字符串仍视为字符串拼接)
console.log(123 + true) // 124
console.log(123 + undefined) // NaN 由于 undefined 被转为 NaN
console.log(NaN + {}) // "NaN[object Object]" 含有对象会转为原始值,由于是字符串因此视为拼接
复制代码
和加性操做符同样,JS 中的关系操做符(>,<,>=,<=)也会有一些反常的行为
对于第二条,举个例子
console.log('abc' < 'abd') // true
复制代码
内部是这么判断的,因为两个都是字符串,先判断字符串的第一位,发现都是 "a",接着比较第二个,发现也是相同的,接着比较第三个,因为 "c" 的编码比 "d" 小(前者是 99 后者是 100),因此字符串 abc "小于" 字符串 abd
相等操做符和加性,关系操做符同样师承一脉,也有不少奇怪的特色,以致于十几年后的今天还被人诟病,先看一下网上的一些例子
undefined==null //true
[]==[] //false
[]==![] //true
{}==!{} //false
![]=={} //false
[]==!{} //true
[1,2]==![1] //false
复制代码
具体我不想展开讲,英语不错的朋友能够直接查看规范,我说一下我的的记忆技巧
综合来讲,为了不隐式转换的坑,尽可能使用严格相等(===)
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 语句会返回对象的属性,返回的顺序可能会因浏览器而异,由于没有规范,因此不要依赖它返回的顺序,而 Reflect.ownKeys ,Object.getOwnPropertyNames,Object.getOwnPropertySymbols 是由 ES6 规范 [[OwnPropertyKeys]] 算法定义的,其内容以下
使用 label 语句能够为 for 语句添加标签的功能,当 for 语句内部经过 break,continue 语句退出时,能够额外指定标签名来退出到更外层的循环,这会用在多层 for 循环中
let num = 0
outer: for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (i === 5 && j === 5) {
continue outer
}
num++
}
}
console.log(num) // 95
复制代码
当 i 和 j 都是 5 的时候,会跳过 5 次遍历(55,56,57,58,59),最终结果为 95,即循环执行了 95 次
在 switch 语句中,若是每一个条件不写 break 关键字退出判断的话,会发生条件穿透
let i = 25
switch (i) {
case 25:
console.log('25')
case 35:
console.log('35')
break;
default:
console.log('default')
}
// "25"
// "35"
复制代码
i 知足第一个 case,因此打印了字符串 25,可是因为没有 break,会无视第二个判断条件直接执行第二个 case 的语句,若是第二个条件也没有 break 还会继续穿透到 default 中
switch 语句中 case 的判断条件是严格相等,字符串 10 不等于数字 10
在 ES6 之前,函数的参数会被保存在一个叫 arguments
的对象中在函数执行的时候被建立,它是一个类数组,它有 length 属性表明参数个数,这里的参数个数是执行函数时传入的参数个数,而不是函数定义的参数个数
function func(a,b,c) {
console.log(arguments)
}
func(1,2) // Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ, length:2]
复制代码
即便定义了 3 个参数, arguments 反映的只是函数运行时候的参数个数,另外 arguments 还有一些比较特殊的特性,非严格模式下它和函数运行时的参数会创建一个连接,当参数被修改时会反映到 arguments 上,反之同理
function func(a,b,c) {
console.log(arguments)
a = 123
console.log(arguments)
}
func(1,2)
// Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ, length:2]
// Arguments(2) [123, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ, length:2]
复制代码
function func(a,b,c) {
console.log(a)
arguments[0] = 123
console.log(a)
}
func(1,2)
// 1
// 123
复制代码
而严格模式不会创建这种连接,二者彻底分离,虽然 ES6 仍可使用 arguments,可是它已经被废弃,推荐使用剩余运算符(...)
函数的参数是按值传递,不是按引用传递,即若是参数是一个对象,则在函数内部,经过形参修改这个对象,会反映到全部指向这个参数的变量
let obj = {}
function func(o) {
o.a = '123'
}
console.log(obj) // {}
func(obj)
console.log(obj) // {a:"123"}
复制代码
因为按值传递,因此这里变量 obj 和形参 o 都指向同一个堆内存的对象,在 func 内部经过形参 o 往这个对象中添加了 a 属性,会同时反映到变量 obj
《JavaScript 高级程序设计第三版》
《你不知道的JavaScript》