阅读 6743

三刷红宝书之 JavaScript 基本概念

前言

这是我第三次翻开红宝书也就是《 JavaScript 高级程序设计第三版》,不得不说,虽然书有一些年份,很多知识点也不适合现代的前端开发,但是对于想要掌握 JavaScript 基础的前端新手,亦或是像我一样想找回曾经遗忘在记忆角落的那些碎片知识,这本书依旧非常的适合,不愧被成为 "JavaScript 圣经"

本文是读书笔记,之所以又一次选择读这本书还有一个理由,之前都是记的纸质笔记,这次想把它作为电子版,也算是对之前知识的整理

本文篇幅较长,目的是作为我的电子版学习笔记,我会尽可能去其糟粕,取其精华,同时我会添加一些书上未记载但很重要的知识点补充

let's go

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 基本概念

标识符

标识符指的是变量,函数,属性的名字,主流的名字以驼峰命名为主,或者 $, _

第一个字符不能是数字(但从第二个字符开始就是合法的)

// illegal
let 123hello = '123'

// legitimate
let $123hello = '123'
let helloWorld = '123'
let hello123World = '123'
let _$hello = '123'
复制代码

数据类型

截至今日,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 的变量会抛出错误
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,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
复制代码

Number 类型

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 遇到非数字字符或者第二个小数点,会返回之前转换成功的数字)

String

ECMAScript 中的字符串是一旦创建,它们的就不可改变,如果需要改变某个变量保存的字符串,需要销毁原来的字符串,再用另一个新值字符串填充该变量

Object

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 中,加性操作符有一些特殊的行为,这里分为操作数中有字符串和没有字符串的情况

有字符串一律视为字符串拼接,如果其中一个是字符串,另一个不是字符串,则会将它转为字符串再拼接,接着会遇到两种情况

  • 第二个操作数是对象,则会调用 [[toPrimitive]] 将其转为原始值,如果原始值是字符串那仍会执行字符串拼接
  • 操作数不是对象,则直接视为字符串拼接
console.log("123" + 123) // "123123"
console.log('123' + NaN) // "123NaN"
console.log("123" + {}) // "123[object Object]"
console.log("123" + undefined) // "123undefined"
复制代码

如果两个操作数都不是字符串,又会有两种情况

  • 操作数是对象,则会调用 [[toPrimitive]] 将其转为原始值,如果原始值是字符串那仍会执行字符串拼接
  • 操作数不是对象,则会转为 Number 类型再计算

值得一提的是,涉及到 NaN 的四则运算最终结果都是 NaN(另一个操作数为字符串仍视为字符串拼接)

console.log(123 + true) // 124
console.log(123 + undefined) // NaN 因为 undefined 被转为 NaN
console.log(NaN + {}) // "NaN[object Object]" 含有对象会转为原始值,因为是字符串所以视为拼接
复制代码

关系操作符

和加性操作符一样,JS 中的关系操作符(>,<,>=,<=)也会有一些反常的行为

  • 两个操作数都是数值,则执行数值比较(如果其中一个是 NaN,则始终返回 false)
  • 两个操作数都是字符串,逐个比较字符串的编码值
  • 其中一个操作数是对象,则调用 [[toPrimitive]] 转为原始值,按照之前规则比较
  • 其中一个操作数是布尔值,会转为 Number 类型,再执行比较

对于第二条,举个例子

console.log('abc' < 'abd') // true
复制代码

内部是这么判断的,由于两个都是字符串,先判断字符串的第一位,发现都是 "a",接着比较第二个,发现也是相同的,接着比较第三个,由于 "c" 的编码比 "d" 小(前者是 99 后者是 100),所以字符串 abc "小于" 字符串 abd

相等操作符

相等操作符和加性,关系操作符一样师承一脉,也有很多奇怪的特点,以至于十几年后的今天还被人诟病,先看一下网上的一些例子

undefined==null //true
[]==[] //false
[]==![] //true
{}==!{} //false
![]=={} //false
[]==!{} //true
[1,2]==![1] //false
复制代码

具体我不想展开讲,英语不错的朋友可以直接查看规范,我说一下个人的记忆技巧

  • 如果类型相同,直接判断是否相等,不同类型才会发生隐式转换
  • 涉及到对象,则会调用 [[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 循环中

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 语句

在 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》

ECMA-262

MDN

关注下面的标签,发现更多相似文章
评论