JavaScript红宝书读书笔记

2022-04-14  本文已影响0人  Zindex

前言

辅助阅读

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

拓展知识

  1. 在过去 <script> 元素放在 <head> 标签内,会导致页面阻塞问题。现代基本放在 <body> 其他元素后面。
  2. defer 属性添加后会在解析到 </html> 之后再执行。在 DOMContentLoaded 事件之前执行。多个defer则按出现顺序执行。前面都是理论,实际还有其他复杂情况,因此defer最好只用一次。
  3. async 与 defer 类似,但 async 不能保证按照出现顺序执行。但保证会在load事件前执行。好的web应用不推荐使用。
  4. 动态加载脚本例如使用 DOM API。其中涉及async问题和预加载器问题详见第15页。
let script = document.createElement('script');
script.src = 'gibberish.js' ;
document.head.appendChild(script)
  1. XHTML问题以及废弃语法详见16-17页,DOCTYPE 与 noscript 详见18-19页
  2. 外部 script 比行内更好。

第3章 语言基础

3.1 语法

3.1.1 大小写:

JavaScript无论变量,函数名,操作符都严格区分大小写。比如var是关键字,VaR可以作为变量名

3.1.2 标识符:

也就是变量名,函数名,属性名,参数名等。遵守

  1. 第一个字符必须是(一个字母,下划线_,美元符号) 三选一。
  2. 剩下的其他字符可以是(字母,下划线,美元符号,数字)

3.1.3 注释

//单行注释

/*多行
注释*/

3.1.4 严格模式

严格模式可以在脚本开头加上这一行。

"use strict"

或者在单独指定一个函数在严格模式下执行

function doSomething(){
    "use strict";
    //函数体
}

3.1.5 语句

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 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 操作符会返回下列字符串之一:

let message = "some string";
console.log(typeof message);        //输出 "string"
console.log(typeof(message));       //输出“string”
console.log(typeof 97);     //输出 "number"

3.4.2 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 类型

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 值的范围

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 数值转换

有三个函数可以将放数字转换为数值:

  1. Number()
  2. parseInt()
  3. parseFloat()
    Number() 是转型函数,可用于任何数据类型。
    parseInt() 和 parseFloat() 主要作用是小数和整数互相转换,也可用于字符串类型转数值类型。
Number()

一元操作符与 Number() 函数遵循以下规则执行转换:

parseInt()

考虑到用 Number() 函数转换字符串时相对复杂且有点反常规,通常在需要得到整数时可以优先使用 parseInt() 函数。parseInt() 函数更专注于字符串是否包含数值模式。
字符串使用 parseInt() 进行数值转换,从第一个非空格字符开始:

  1. 如果第一个字符不是数值字符、加号减号,parseInt() 立即返回 NaN。如果是单纯的加号减号也返回 NaN。
  2. 如果是空字符串也返回NaN,这点与 Number() 函数不一样。
  3. 如果第一个字符是数值字符、非单独出现的加号减号,则继续一次检测每个字符,直到字符串末尾,或出现非数值字符。比如 '1234blue' 会被转换为1234。
  4. 如果字符串以0开头,在非严格模式下会被某些实现解释为八进制数。也可转其他进制数。

不同进制的数值格式很容易混淆,因此 parseInt() 也接收第二个参数用于指定进制数(底数),例如:

let num = parseInt('0xAF',16);      //175
let num = parseInt('AF',16);      //175
let num = parseInt('AF');      //NaN
parseFloat()
  1. 解析到字符串末尾或解析到一个无效的浮点数值字符为止。意味着第一次出现的小数点是有效的,但第二次出现的小数点及后面的字符就无效了,例如 '22.34.5' 将转换成22.34。
  2. parseFloat() 只解析十进制数。始终忽略字符串除开头以外的零0,意味着像16进制数始终返回0。
  3. 科学计数法的小数点不解析,直接转为整数。'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()
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() 函数遵循以下规则:

  1. 如果值有 toString() 方法,则调用该方法并返回结果。
  2. 如果值是null返回 "null",如果值是 undefined 返回 "undefined"
加号操作符拼串

用加号操作符给一个值加上一个空字符串 "" 也可以将其转换为字符串。

console.log(null+"")

3.4.6.4 模板字面量

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
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
上一篇下一篇

猜你喜欢

热点阅读