JavaScript红宝书读书笔记
前言
辅助阅读
MDN文档:JavaScript | MDN (mozilla.org)
现代JavaScript教程:JavaScript Info
第1章 什么是JavaScript
1.1 简史回顾
1995年,网景公司的 Brendan Eich 开发Mocha脚本语言(后来改名LiveScript),后网景与sun结盟共研 LiveScript。在 Netscape Navigator 2发布前蹭 Java 热度改名 JavaScript 。后面就是微软与网景大战,为统一标准由 TC39 制定 ECMA-262 。
1.2 JavaScript 实现
完整的 JavaScript 包含 ECMAScript、文档对象模型DOM、浏览器对象模型BOM。
第二章 HTML 中的 JavaScript
2.1 <script> 元素
由网景最初实现的 <script> 元素插入HTML被加入到 HTML 规范。该元素有八个属性,八个属性全是可选以及一个废弃。
属性名 | 作用 |
---|---|
src | 表示包含要执行的代码的外部文件 |
type | 默认值 "text/javascript",第二个常用值 "module"开启ES6模块允许import和export出现 |
async | 表示异步加载此脚本,只对外部脚本文件有效 |
defer | 脚本延迟到文档完全被解析和显示之后再执行,只对外部脚本文件有效 |
charset | 使用src属性指定的代码字符集,大多数浏览器不在乎它的值 |
crossorigin | 配置相关请求的CORS设置,默认不使用CORS |
integrity | 允许对比接收到的资源和指定的加密签名以验证子资源完整性。可用于确保CDN不会提供恶意内容 |
language | 废弃值,用于说明此元素内的脚本语言例如VBScript |
- 行内 JavaScript 代码中不能出现 </script> 防止浏览器读到结束标签出现错误,解决方法是使用转义字符 \
- 使用了src属性的 script 元素里不要再写任何内容,因为写的内容不会执行。
- script 元素不受同源策略限制,但返回的内容执行里如果包含限制的内容还是会限制。
- 没有使用 async 和 defer 的 script 元素会依次执行
拓展知识
- 在过去 <script> 元素放在 <head> 标签内,会导致页面阻塞问题。现代基本放在 <body> 其他元素后面。
- defer 属性添加后会在解析到 </html> 之后再执行。在 DOMContentLoaded 事件之前执行。多个defer则按出现顺序执行。前面都是理论,实际还有其他复杂情况,因此defer最好只用一次。
- async 与 defer 类似,但 async 不能保证按照出现顺序执行。但保证会在load事件前执行。好的web应用不推荐使用。
- 动态加载脚本例如使用 DOM API。其中涉及async问题和预加载器问题详见第15页。
let script = document.createElement('script');
script.src = 'gibberish.js' ;
document.head.appendChild(script)
- XHTML问题以及废弃语法详见16-17页,DOCTYPE 与 noscript 详见18-19页
- 外部 script 比行内更好。
第3章 语言基础
3.1 语法
3.1.1 大小写:
JavaScript无论变量,函数名,操作符都严格区分大小写。比如var是关键字,VaR可以作为变量名
3.1.2 标识符:
也就是变量名,函数名,属性名,参数名等。遵守
- 第一个字符必须是(一个字母,下划线_,美元符号) 三选一。
- 剩下的其他字符可以是(字母,下划线,美元符号,数字)
- 推荐使用小驼峰写法
- 注意:关键字,保留字,true,false,null,不能作为标识符。
3.1.3 注释
//单行注释
/*多行
注释*/
3.1.4 严格模式
严格模式可以在脚本开头加上这一行。
"use strict"
或者在单独指定一个函数在严格模式下执行
function doSomething(){
"use strict";
//函数体
}
3.1.5 语句
- 分号:ECMAScript 语句以分号结尾,可以省略但不推荐。特别是立即执行函数很容出错。解析器会自动补上分号会影响性能。但有些编辑器会自动补上分号。
- 代码块:多条语句可以合并到一个代码块中,代码块以左花括号开始,右花括号结束。
if(test){
test = false;
console.log(test)
}
if 语句如果只执行一条语句可以不使用代码块,但红宝书不推荐。
if(test) console.log(test) //不推荐
3.2 关键字与保留字
详见 第 23 页
3.3 变量
ECMAScript 变量是松散类型,可以保存任何类型的数据。声明变量的三个关键字(var,let,const)
3.3.1 var
var message
上面的代码定义了一个名为 message 的变量,可以用它保存任何类型的值,在不初始化的情况下会保存一个特殊值 undefined。当然也可以在定义时就就设置值。并在后面修改该变量的值,虽然可以改变变量值的类型,但不推荐
var message = 'hi';
message = 100;
var声明作用域
使用var定义的变量会成为 包含它的函数 的 局部变量,意味着函数退出时被销毁。
function test(){
var message = "hi";
}
test();
console.log(message)
省略 var 直接声明变量,会成为 window 全局下的一个变量。严格模式下禁止这么做,会抛出错误。
function test(){
message = "hi";
//相当于 window.message = "hi"
}
test()
console.log(message)
如果需要定义多个变量,可以在一条语句中用逗号分隔每个变量。像下面那样换行不是必须的,但有利于阅读。
var message = "hi",
found = false,
age = 29
var 声明提升(hoist)
使用 var 声明的变量会自动提升到函数作用域顶部。
function foo(){
console.log(age);
var age = 26;
}
foo(); //不会报错,会输出 undefined
因为 ECMAScript 运行时会把它看成等价于如下代码
function foo(){
var age;
console.log(age);
age = 26;
}
foo();
此外,可以使用 var 重复声明同一个变量
function foo(){
var age = 18;
var age = 25;
var age = 35;
console.log(age)
}
foo() //输出35
3.3.2 let声明
let 声明的范围是块作用域,对比var声明的是函数作用域。
if(true){
var name = 'Mart';
console.log(name); //输出 Mart
}
console.log(name); //输出Mart
if(true){
let age = 25;
console.log(age); //输出25
}
console.log(age); //报错:ReferenceError,age未定义
let不允许同一个块作用域中出现冗余声明。混用 let 和 var 一样报错。
var name;
var name;
let name; //报错:SyntaxError,name已经声明过了
let age;
let age; //报错:SyntaxError,age已经声明过了
var age; //报错:SyntaxError,age已经声明过了
JavaScript引擎会记录用于变量声明的标识符及其所在的块作用域,因此嵌套使用相同的标识符不会报错,因为同一个块中没有重复声明。
let name = 'Nicholas';
console.log(name) //输出 Nicholas
if(true){
console.log(name) //输出Nicholas
}
if(true){
let name = 'Mart';
console.log(name) //输出Mart
}
3.3.2.1 let暂时性死区
let 声明的变量不会再作用域中被提升,在解析代码时,任何方式在 let 声明之前引用 let 后面会声明的变量,都会抛出ReferenceError。在let声明致歉的执行瞬间被成为“暂时性死区”temporal dead zone。
let name = 'Nicholas';
if(true){
console.log(name) //报错,初始化之前无法访问“name”
let name = 'Mart';
}
3.3.2.2 全局声明
使用 let 在全局作用域中声明的变量不会成为window对象的属性,而 var 会。
//全局作用域中,指这个JS模块的<script>标签内,不在任何块中
var name = 'Matt';
console.log(window.name) //输出 'Matt'
let age = 26;
console.log(window.age) //undefined
不过,let 声明如果发生在全局作用域中,相应的变量会在页面的生命周期内存续,因此为了避免SyntaxError,必须确保页面不会重复声明同一个变量。
let name = 'Nicholas';
console.log(name) //输出 Nicholas
if (true) {
var name = 'mart' //报错,SyntaxError
}
3.3.2.3 条件声明
在使用 var 声明变量时,由于声明会被提升,JavaScript引擎会自动将多余的声明在作用域顶部合并为一个声明。因为 let 的作用域是块,所以不可能检查前面是否已经使用 let 声明过同名变量,同时也就不可能在没有声明的情况下声明它。(这里表示看不懂,说人话?但后面那句注意挺有用的,因为开发时遇到过条件声明然后看不懂的情况)
对于 let 这个新的ES6 声明关键字,不能依赖条件声明模式
注意:不能使用let进行条件式声明是件好事,因为条件声明是一种反模式,他让程序变得更难理解。如果你发现自己在使用这个模式,那一定有更好的替代方式
3.3.2.4 for循环中的let声明
for循环中用var定义的迭代变量会渗透到循环体外部
for(var i=0; i<5; ++i){
}
console.log(i) //输出5
使用let声明的 迭代变量 的作用域仅限于for循环块内部。
for(let i=0; i<5; ++i){
}
consolo.log(i); //报错ReferenceError:i没有定义
在使用var的时候,最常见的问题就是对迭代变量的奇特声明和修改。下面的例子之所以是这样,因为退出循环时,迭代变量保存的是导致循环退出的值5。执行异步函数时,等循环走完了再走回调,所有的i都是同一个变量。
for(var i=0; i<5; i++){
setTimeout(()=>{
console.log(i)
},0)
}
//会输出5,5,5,5,5
而在使用 let 声明迭代变量时,JavaScript引擎会在后台为每个 迭代循环 声明一个 新的迭代变量,每个 setTimeout 应用的都是不同的变量实例,所以输出的是循环执行过程中每个迭代变量的值
for(let i=0; i<5; i++){
setTimeout(()=>{
console.log(i)
},0)
}
//会输出0,1,2,3,4
3.3.3 const 声明
- const 声明变量时必须同时初始化变量,且尝试修改const声明的变量会导致运行时报错。
- 也具有“不允许重复声明”,“块级作用域”的特点。
- const 声明的限制只适用于他只想的变量的引用,也就是说const变量应用的是一个对象或数组,修改里面的内容不违反const的限制。
const age = 26;
age = 36; //TypeError:给常量赋值
const 声明的变量不能用于 for 循环的 “迭代变量”,但可以在循环体内声明一个 不会被修改的变量,利用块级作用域的特点。这对for-of 和 for-in 循环特别有意义。
let i = 0;
for(const j=7; i<5; i++){
console.log(j) //7,7,7,7,7
}
for(const key in {a:1,b:2}){
console.log(key) //a,b
}
for(const value of [1,2,3,4,5]){
console.log(value); //1,2,3,4,5
}
3.3.4 声明风格及最佳实践
开发时声明变量,const 优先,let 其次,尽量不使用var
3.4 数据类型
6种简单数据类型也称原始类型:undefined,null,boolean,number,string,symbol
1种复杂数据类型:object
typeof 操作符
对一个值使用 typeof 操作符会返回下列字符串之一:
- “undefined”表示未定义
- “boolean”表示为布尔值
- “string” 表示为字符串
- “number”表示为数值
- “object”表示为对象(而不是函数)或 null
- “function”表示为函数
- “symbol”表示为符号
let message = "some string";
console.log(typeof message); //输出 "string"
console.log(typeof(message)); //输出“string”
console.log(typeof 97); //输出 "number"
- typeof 是一个操作符不是一个函数,但可以使用参数方式
- typeof null 返回的是 object,因为特殊值 null 被认为是一个空对象的引用。
- 严格来说,函数在ECMAScript 种被认为是对象,并不代表一种数据类型,可是函数也有自己特殊的属性,所以需要通过 typeof 操作符来区分函数和其他对象
3.4.2 undefined 类型
- undefined 类型只有一个值,就是特殊值 undefined,当使用 var 或 let 声明了变量但没有初始化时,就相当于给变量赋予了 undefined 值。
- 增加 undefined 这个特殊值的目的就是为了正式明确空对象指针(null)和未初始化变量的区别。
- 包含 undefined 值的变量跟未定义变量是有区别的。
let message;
console.log(message); //输出 undefined
console.log(age); //报错
对于未声明的变量,只能执行一个有用操作,就是对他调用 typeof (或非严格模式下的delete),所以用typeof无法区分变量未声明和变量未初始化。
undefined 是一个假值即
let a = Boolean(undefined)
console.log(a) //输出 false
3.4.3 null 类型
- null 类型只有 null 一个特殊值,表示空指针对象, typeof null 返回的是一个object。
- 任何时候,只要变量要保存对象,而当时又没对象可保存,就要用 null 来填充变量。
- null 是一个假值,即
Boolean(null)
返回false,所以 null == undefined。
3.4.4 boolean 类型
boolean 类型有两个字面值:true 和 false。虽然布尔值只有两个,但所有其他ECMAScript类型的值都有相应布尔值的等价形式。可用 Boolean()
转型函数将其他类型的值转换为布尔值。
数据类型 | 转换为true的值 | 转换为false的值 |
---|---|---|
boolean | true | false |
undefined | N/A(不存在) | undefined |
number | 非零数值 | 0、NaN |
string | 非空字符串 | ""空字符串 |
object | 任意对象 | null |
3.4.5 number 类型
number 类型使用 IEEE754 格式表示整数和浮点数(某些语言也叫双精度值)。
基本的数值字面量是十进制整数。也可以声明前缀为 0o 的八进制数或 0x 前缀的十六进制数。
let intNum = 55; //整数
let octalNum1 = 0o70; //八进制的56
let octalNum2 = 0o79; //无效八进制值,当成79处理
let hexNum = 0*1f; //十六进制31
3.4.5.1 浮点值
浮点值必须包含小数点且小数点后面必须至少有一个数字。虽然小数点前面不是必须有整数,但推荐加上。
let floatNum1 = 1.1;
let floatNum2 = .1; //有效但不推荐
因为存储浮点值使用的内除空间是存储整数值的两倍,所以 ECMAScript 总是想方设法把值转换为整数。因此在小数点后面没有数字的情况下就会变成整数。
1. === 1; //返回true
10.0 === 10; //返回true
对于非常大或非常小的数值,浮点值可以用科学记数法来表示。(科学计数法:用于表示一个应该乘以10的给定次幂的数值)
ECMAScript 中科学记数法的格式要求是一个数值(整数或浮点数)后面各一个 大写或小写的字母e,再接上一个要乘的10的多少次幂。例如:
let floatNum = 3.125e7; //等于31250000
科学计数法也可以用于表示非常小的数值,甚至 ECMAScript 会将小数点后至少包含6个零的浮点值转换为科学计数法表示。
console.log(0.00000000000000003); //输出3e-17
console.log(0.0000003); //输出3e-7
浮点值的精确度最高可达17位小数,但在算术计算中远不如证书精确,因此永远不要浮点值来做测试例如判断条件。
3.4.5.2 值的范围
- 由于内存限制,ECMAScript 并不支持表示这个世界上所有的数值。
- 最小值 Number.MIN_VALUE = 5e-324,低于此值显示 -Infinity,即
Number.NEGATIVE_INFINITY
- 最大值 Number.MAX_VALUE = 1.7976931348623157e+308,高于此值显示 Infinity,即
Number.POSITIVE_INFINITY
- 如果要测算是有限大的值可以用 isFinite() 函数,返回的如果是false则是超范围的值。
let result = Number.MIN_VALUE + Number.MAX_VALUE;
isFinite(result); //这里返回true,书上写错了
3.5.5.3
NaN
其他偏冷门知识 第35页。
Not a Number ,不是数值,用于表示本来要返回数值的操作失败了。
特性1:任何涉及NaN的操作始终返回 NaN,
特性2:NaN不等于包括NaN在内的任何值。为此ECMAScript提供了 isNaN() 函数,该函数的参数无论什么类型都会先进行一次数值型强制类型转换。
console.log(isNaN(NaN)); //true
console.log(isNaN('string')); //true
console.log(isNaN('10')); //false
console.log(isNaN(true)); //false
//甚至可以传入一个对象。遵循 ECMAScript 内置函数和操作符的工作方式
ECMAScript内置函数和操作符的工作方式:调用 valueOf() 方法,再调用 toString() 方法
3.4.5.4 数值转换
有三个函数可以将放数字转换为数值:
- Number()
- parseInt()
- parseFloat()
Number() 是转型函数,可用于任何数据类型。
parseInt() 和 parseFloat() 主要作用是小数和整数互相转换,也可用于字符串类型转数值类型。
Number()
一元操作符与 Number() 函数遵循以下规则执行转换:
- 布尔值:true 转为 1,false 转为 0
- null:返回 0
- undefined:返回 NaN
- 对象:调用 valueOf() 方法, 并按照上述规则转换返回的值,如果转换结果是NaN,则调用 toString() 方法,再按照转换字符串的规则转换。
- 字符串:
- 如果字符串包含数值字符,包括数值字符前面带加减号的情况,则转换为一个十进制数值。前面的0全部省略。
- 如果字符串包含有效的浮点值格式,则会转换为相应的浮点值。前面多余的0也全部省略。
- 如果字符串包含有效的二进制、八进制、十六进制等的数则转为对应的十进制数,如果是无效的进制数则为 NaN。
- 如果是空字符串,则返回 0 。
- 如果字符串包含除上述情况之外的其他字符,则返回 NaN
parseInt()
考虑到用 Number() 函数转换字符串时相对复杂且有点反常规,通常在需要得到整数时可以优先使用 parseInt() 函数。parseInt() 函数更专注于字符串是否包含数值模式。
字符串使用 parseInt() 进行数值转换,从第一个非空格字符开始:
- 如果第一个字符不是数值字符、加号减号,parseInt() 立即返回 NaN。如果是单纯的加号减号也返回 NaN。
- 如果是空字符串也返回NaN,这点与 Number() 函数不一样。
- 如果第一个字符是数值字符、非单独出现的加号减号,则继续一次检测每个字符,直到字符串末尾,或出现非数值字符。比如 '1234blue' 会被转换为1234。
- 如果字符串以0开头,在非严格模式下会被某些实现解释为八进制数。也可转其他进制数。
不同进制的数值格式很容易混淆,因此 parseInt() 也接收第二个参数用于指定进制数(底数),例如:
let num = parseInt('0xAF',16); //175
let num = parseInt('AF',16); //175
let num = parseInt('AF'); //NaN
parseFloat()
- 解析到字符串末尾或解析到一个无效的浮点数值字符为止。意味着第一次出现的小数点是有效的,但第二次出现的小数点及后面的字符就无效了,例如 '22.34.5' 将转换成22.34。
- parseFloat() 只解析十进制数。始终忽略字符串除开头以外的零0,意味着像16进制数始终返回0。
- 科学计数法的小数点不解析,直接转为整数。'3.125e7' 直接转为 '31250000'
3.4.6 String 类型
String 字符串类型表示零个或多个16位Unicode字符序列。字符串可以用双引号、单引号或反引号标示。开头和结尾的引号必须是同一种
let firstName = 'John';
let lastName = "Jacob";
let fullName = `JoyCoy`;
3.4.6.1 字符字面量 length 属性
例如 \n 换行, \t 制表。见第38页。
字符串的长度可以通过 length 属性获取。如果字符串中包含双字节字符,则 length 属性返回的值可能不准确
let text = "This is the letter sigma: \u03a3.";
console.log(text.length); //输出 28,\u03a3 表示一个 Unicode字符
3.4.6.2 字符串的特点
ECMAScript 中的字符串时不可变的(immutable),意味着如果一个变量的值为字符串类型,必须先销毁原始的字符串,然后再将新的字符串保存到这个变量。
3.4.6.3 转为字符串
XXX.toString()
- 使用 XXX.toString() 方法,可用于数值、布尔值、对象和字符串,但不能用于 null 和 undefined ,因为null和undefined没有原型链指向 Object.prototype。
- toString() 在数值类型调用这个方法的时候可以传入一个底数参数,输出的时候就会返回这个数值的二进制八进制十六进制等。
let num = 10;
console.log(num.toString(2)); //"1010"
console.log(num.toString(8)); //"12"
console.log(num.toString(16)); //"a"
String(XXX)
因为 toString() 方法无法被 null 和 undefined 使用,要想转换所有类型为字符串,可以使用 String() 转型函数。String() 函数遵循以下规则:
- 如果值有 toString() 方法,则调用该方法并返回结果。
- 如果值是null返回 "null",如果值是 undefined 返回 "undefined"
加号操作符拼串
用加号操作符给一个值加上一个空字符串 "" 也可以将其转换为字符串。
console.log(null+"")
3.4.6.4 模板字面量
- ES6 新增了模板字面量定义字符串,特性一是模板字面量保留换行字符及多个空格。
- 特性二是模板字面量支持字符串插值。插值里面可以调用函数和方法,得到的返回值会进行一次 toString() 调用。
3.4.6.6 模板字面量标签函数 tag function
通过标签函数可以自定义插值行为,标签函数会接收:被,插值记号,分隔后的模板,和,对每个表达式求值的结果。
let a = 6;
let b = 9;
function simpleTag(strings, aValExpression, bValExpression, sumExpression) {
console.log(strings);
console.log(aValExpression);
console.log(bValExpression);
console.log(sumExpression);
return 'foobar'
}
let untagedResult = simpleTag`${a}+${b}=${a + b}`;
console.log(untagedResult);
//['', '+', '=', '']
//6
//9
//15
//foobar
- 上面的例子中,调用 simpleTag 函数时,传入的参数不是一个括号,而是一个模板字面量。
可以看到输出的第一个值即第一个参数arguments[0]的strings是一个被插值符号分割后的字符串组成的数组['', '+', '=', '']
- 第二个参数开始是插值符号里面的内容,而且这些内容没有进行 toString() 转换为字符串。因为表达式参数?的数量是可变的,通常应该使用剩余操作符 rest operator 将他们收集到一个数组中。下面的函数执行结果与上面一样。
function simpleTag(strings,...expressions){
console.log(strings);
for(const expression of expressions){
console.log(expression)
}
return 'foobar';
}
对于有 n 个插值的模板字面量,就意味着除了第一个参数以外后面就有n个参数,而传给标签函数第一个参数所包含的字符串则始终是n+1.因此如果你想把这些字符串和所有插值结果按顺序拼接起来作为默认返回的字符串,可以这样做
function zipTag(strings, ...expressions) {
return strings[0]
+
expressions.map((e, i) => {
return `${e}${strings[i + 1]}`
}).join('')
}
//运用上面的函数
let a = 6;
let b = 9;
let taggedResult = zipTag`${a}+${b}=${a + b}`
console.log(taggedResult);
//运行相当于console.log("" + ["6+", "9=", "15"].join(''));
3.4.6.7 原始字符串
模板字面量里写字符字面量(比如换行符或unicode字符),得到的是经过转换后得到的而不是原封不动的输出。
console.log(`\u00A9`); //输出©
console.log(`first line\nsecond line`);
//输出
//first line
//second line
为此,可以使用默认的 String.raw 标签函数让他们原封不动的输出,当然直接敲回车键的换行不会被转义。
console.log(String.raw`\u00A9`); //输入\u00A9
想获得原封不动的值还有一个方法,就是通过标签函数的第一个参数的raw属性获取,44页的printRaw方法,或下面的简便方法。
function tagFunction() {
return originValue = arguments[0].raw
}
let result = tagFunction`\u00A9`;
console.log(result); //输出 \u00A9