JavaScript高级程序设计
这是我对《JavaScript高级程序设计》的详细学习笔记,主要内容包括html中的JavaScript,变量基础,变量、作用域与内存,基本引用类型、集合引用类型,迭代器与生成器,对象、类与面向对象编程,函数,期约与异步函数,BOM,DOM,DOM扩展,DOM2和DOM3,事件......
什么是JavaScript
- JavaScript是一门用来与网页交互的脚本语言,包含以下三个组成部分:
- ECMAScript:由ECMA-262定义并提供其核心功能
- DOM(文档对象模型):提供与网页内容交互的方法的接口
- BOM(浏览器对象模型):提供与浏览器交互的方法和接口
HTML中的JavaScript
< script >元素
属性
- async:可选,表示应立即开始下载脚本,但不能阻止其他页面动作(异步执行)。只对外部脚本文件有效。
- charset:可选,字符集。
- crossorign:可选,配置相关请求的CORS(跨源资源共享)设置。默认不使用CORS
- defer:可选,脚本立即下载,但推迟执行。仅对外部脚本文件有效。
- integrity:可选,允许比对接收到的资源和指定的加密签名以验证子资源的完整性。
- src:可选。
- type:可选,代表代码块中脚本语言的内容类型(也称MIME类型)
执行顺序
- 在不使用defer或者async的情况下,包含在
注意点
- < script >中的代码尽量不要出现< /script >,浏览器会将其视为结束标志;如果一定要使用,使用转义字符,例:
- 使用src属性的< script >标签元素不应该在< script >< /script >中再包含其他代码(也就是一个< script >标签,行内式和外部文件式只能选一个)
跨域
- < script >元素可以包含来自外部域的JavaScript文件
- 若src属性是一个指向不同于的url,则浏览器会向此指定路径发送一个GET请求,此初始请求不受浏览器同源策略的限制,但返回的JavaScript仍受限制
- 好处:通过不同的域分发JavaScript(就是我们引入外部包的过程)
- 可使用integrity属性进行防范
位置
- 通常将所有的JavaScript引用放在< body >元素中的页面内容后面
动态加载脚本
< noscript >元素
- < noscript >可以是一种出错提示手段
- 在以下任一条件被满足时,< noscript >中的内容就会被渲染
- 浏览器不支持脚本
- 浏览器对脚本的支持被关闭
语言基础
语法
标识符
- 标识符是变量、函数、属性或函数参数的名称
- 标识符的组成如下:
- 第一个字符必须是一个字母、下划线、或美元符号
- 剩下的其他字符可以是字母、下划线、美元符号或数字
- 推荐使用驼峰大小写形式
- 关键字、保留字、true、false和null不能作为标识符
关键字
break | do | in | typeof |
---|---|---|---|
case | else | instanceof | var |
catch | export | new | void |
class | extends | return | while |
const | finally | super | with |
continue | for | switch | yield |
default | if | throw | this |
function | debugger | delete | import |
try |
保留字
始终保留 | 严格模式下保留 | 模块代码中保留 | |
---|---|---|---|
enum | implements | package | await |
public | interface | ||
protected | private | ||
static | let |
变量
- 可以保存任意类型的数据,每个变量都是一个保存任意值的占位符
- 变量有三个:var、let和const
- const优先,let次之,不使用var
var
- 不初始化的情况下,变量会保存一个特殊值undefined
- 使用var操作符定义的变量会成为包含它的函数的局部变量
- 在函数内部定义变量时省略var,可以创建一个全局变量(严格模式下会报错,且不推荐这么做)
- var声明提升:
- 使用var声明变量时,变量会发生变量提升
- 所谓提升,是把所有变量声明提升到函数作用域的顶部
- 可以使用var声明同一个变量
- 用var在全局作用域中声明的变量会成为window对象的属性
- 具体用例:
// var的作用域
// var声明提升
let
- let声明的范围是块作用域
- let不允许在同一个块作用域中出现冗余声明
- let声明的变量不会在作用域中提升
- 用let在全局作用域中声明的变量不会成为window对象的属性
- 具体用例:
// 混用var与let
// for循环中的let声明
const
- const的行为与let基本相同
- 用const声明变量的同时必须初始化变量,且该变量不允许进行修改
- 如果const变量引用的是一个对象,修改该对象内部的属性方法是允许的
- 如果想让整个对象(包括属性方法)不能修改,可以使用Object.freeze()
- 具体用例:
// for-of 和 for-in
// Object.freeze() 再给属性赋值时不会报错 但会赋值失败
const o1 = {
age: 13,
};
const o2 = Object.freeze({
age: 14,
});
o1.age = 0;
o2.age = 0;
o2.name = `xiaoming`;
console.log(`${o1.age} ${o2.age} ${02.name}`); // 0 14 undefined
数据类型
- 6种简单数据类型:Undefined、Null、Boolean、Number、String、Symbol
- 1种复杂数据类型:Obiect
typeof操作符
-
使用typeof返回的字符串及其意义
| 字符串 | 意义 |
| — | — |
| “undefined” | 值未定义 |
| “boolean” | 值为布尔值 |
| “string” | 值为字符串 |
| “number” | 值为数值 |
| “object” | 值为对象或null |
| “function” | 值为函数 |
| “symbol” | 值为符号 | -
具体用例:
// typeof 操作符
undefined类型
- undefine类型只有一个值,就是特殊值undefined
- 变量声明了但是没有赋值是,变量的值为undefined(明确是空对象指针null和为初始变量的区别)
- 对未声明的变量,只能执行一个有用的操作,就是对它调用typeof,返回值为undefined
- 具体事例:
// 包含undefined值的变量与未定义变量的区别
// 明确检测undefined这个字面值
Null类型
- Null类型只有一个值,即特殊值null
- 在定义将来要保存对象值的变量时,建议用null初始化
- 具体事例:
// null与undefined表面相等
// 明确检测null这个字面值
Boolean类型
- Boolean类型有两个字面值,true和false
- 所有其他ECMAScript类型的值都有相应的布尔值的等价形式
- 可使用Boolean([任意类型的数据])转型函数将其他值转换为布尔值
- 不同类型与布尔值的转换规则
| 数据类型 | 转换为true的值 | 转换为false的值 |
| — | — | — |
| Boolean | true | false |
| String | 非空字符串 | “”(空字符串) |
| Number | 非零数值(包括无穷值) | 0、NaN |
| Object | 任意对象 | null |
| Undefined | N/A(不存在) | undefined |
Number类型
进制
- 八进制需要前缀0(零),但如果字面量中的数字超出了应有的范围,会将整个数字视为十进制数(如079)
- 十六进制需要前缀0x
- 使用八进制和十六进制的格式创建的数值在所有数学操作中均视为十进制数值
浮点值
- 小数点前可以没有整数
- 若小数点后面没有数字,数值自动转化为整数
- 若数值本身就是整数。只是小数点后面跟着零,自动转化为整数
- 永远不要测试某个特定的浮点值,理由如下:
值的范围
- Number.MIN_VALUE 最小数值
- Number.MAX_VALUE 最大数值
- 超过了最大值,会被转换为+Infinity
- 超过了最小值,会被转化为-Infinity
- isInfinity():确定一个值是不是有限大
NaN(Not a Number)
- 用来表示本来要返回数值的操作失败了(而不是抛出错误)
- 0、-0、+0相除会返回NaN
- 若分子是非0值,分母是有符号0或者无符号0,则会返回Infinity或-Infinity
- isNaN()函数:
- 参数:接受一个任意数据类型的参数
- 功能:判断参数是否“不是数值”
- 返回值:布尔值
- 原理:任何不能转换为数值的值都会导致这个函数返回true
- 具体用例:
// 0、+0、-0 相除
// infinity情况
// isNaN() 函数
数值转换
Number()函数
- 具体用例:
// 布尔值
// 数值
// null
// undefined
// 字符串
// 对象
parseInt()函数
- 需要得到整数时优先使用parseInt()函数
- 参数:
- 第一个参数:需要被转换的数据
- 第二个参数:可选,指定进制数,默认为10
- 具体用例:
// 空字符串
// 数字+其他字符 组成的字符串
// 其他进制数
parseFloat()函数
- 工作方式与parseInt()类似
- 具体用例:
// 第一次出现的小数点是有效的,第二次出现的小数点是无效的
// 只解析十进制数,不能指定底数
// 始终忽略字符串开头的0
// 若字符串表示整数(没有小数点或者小数点后面只有0,则返回整数)
string类型
- 可以使用单引号(‘’)、双引号(“”)、反引号(``)表示
字符字面量
\n | 换行 |
---|---|
\t | 制表 |
\b | 退格 |
\r | 回车 |
\f | 换页 |
\\ | 反斜杠 |
\’ | 单引号 |
\" | 双引号 |
\` | 反引号 |
\xnn | 以十六进制编码nn表示的字符(n是十六进制数字0~F) |
\unnnn | 以十六进制编码nnnn表示的Unicode字符 |
- 注意:转义序列表示一个字符(在计算的时候算一个)
- string.length :length属性用于获取字符串的长度
字符串的特点
- 这里的字符串是不可变的,要修改必须某个变量的字符串值必须先销毁原字符串,再保存新变量
转换为字符串:
- toString()方法:
- 可用于数值、布尔值、对象和字符串
- 不可用于null和undefined
- 参数:仅在对数值进行转换时接受参数,参数为转换的进制数
- String()转型函数,规则如下:
- 若值有toString()方法,则调用此方法并返回结果
- 若值为null,返回“null”
- 若值为undefined,返回“undefined”
- 具体用例:
// toString()
// String()
模板字面量(反引号)
- 模板字面量在定义模板时特别有用(???)
- 模板字面量会保持反引号内部的空格
- 具体用例:
// 定义模板 HTML模板 可以安全地插入到HTML中
// 保持内部空格
字符串插值
- 通过
${[JavaScript表达式]}
来实现 - 插入值会使用toString()强制转换为字符串
- 插值表达式中可以调用函数和方法
- 插值表达式中可以插入自己之前的值
- 具体用例:
// toString()转换
// 表达式中调用函数和方法
// 表达式中插入自己之前的值
模板字面量标签函数
- 模板字面量支持定义标签函数(???)
- 通过标签函数可以自定义插值行为
- 标签函数会接收被插值记号分隔后的模板和对每个表达式求值的结果
- 具体用例:
// 标签函数
原始字符串
- 使用 String.Raw
字符串内容
可以直接获取原始的模板字面量,而不是被转移后的字符表示
Symbol类型(符号)
- 符号是原始值,且符号实例是唯一、不可变的。
- 用途:确保对象属性使用唯一标识符,不会发生属性冲突的危险
- 具体用例:
// typeof操作符 // 创建Symbol(`字符串参数`)实例并将其用作对象的新属性 即使参数相同,Symbol也是不同的 // 全局符号注册表 Symbol.for(`字符串参数`)方法
- 常用内置符号:
- 这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为
- 所有内置符号属性都是不可写、不可枚举、不可配置的
// Symbol.asyncIterator
// 一个方法,该方法返回对象默认的AsyncIterator。由for-await-of使用。实现了异步迭代器API的函数
// Symbol.hasInstance
// 一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由操作符instanceof操作符使用。
// instanceof操作符可以用来确定一个对象实例的原型链上是否有原型
// Symbol.isConcatSpreadable
// 一个布尔值,如果是true或真值,类数组对象会被打平到数组实例,用Array.prototype.concat()打平
// 如果是false或假值,整个对象被追加到数组末尾
// Symbol.iterator
// 一个方法,该方法返回对象默认的迭代器。由for-of语句使用。这个符号实现了迭代器API的函数
// Symbolmatch
// 一个正则表达式方法,该方法用正则表达式去匹配字符串。由String.prototype.match()方法使用
// Symbol.replace
// 一个正则表达式方法,该方法替换一个字符串中的匹配字符串。由String.prototype.replace()方法使用
// Symbol.search
// 一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引。由String.prototype.search()使用
// Symbol.species
// 一个函数值,该函数作为创建派生对象的构造函数
// Symbol.split
// 一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由String.prototype.split()使用
// Symbol.toPrimitive
// 一个方法,该方法将对象转换为相应的原始值。由ToPrimitive抽象对象使用
// Symbol.toStringTag
// 一个字符串,该字符串用于创建对象默认字符串描述。由内置方法Object.prototype.toString()使用
Object类型
- 对象是一组数据和功能的集合体
- 对象通过new操作符后跟对象类型的名称来创建
- Object是派生其他对象的基类,Object类型的所有属性和方法在派生的对象上同样存在
- Object实例的属性和方法:
| constructor | 用于创建当前对象的函数 |
| — | — |
| hasOwnProperty(PropertyName) | 用于判断当前对象实例上是否存在给定的属性。PropertyName是字符串 |
| isPrototypeOf(object) | 用于判断当前对象是否为另一个对象的原型 |
| propertyIsEnumerable(PropertyName) | 用于判断给定的属性是否可以使用for-in语句枚举。PropertyName是字符串 |
| toLocaleString() | 返回对象的字符串表示,该字符串反映对象所在的本地化执行环境 |
| toString() | 返回对象的字符串表示 |
| valueOf() | 返回对象对应的字符串、数值或布尔值表示 |
操作符
- 在应用给对象时,操作符通常会调用valueOf()和\或toString()方法来取的可以计算的值
一元操作符
- 递增/递减操作符(++/–):类比于c语言的递增递减操作符
- 一元加和减:若应用于非数值,则相当于执行了Number()转型函数
- 具体用例:
let s = 1.1;
console.log(--s); // 0.10000000000000009
位操作符
- 位操作作用于32位的整数,但ECMAScript中数值以IEEE 754 64位格式存储
- 前32位表示整数值,第32位表示符号(从左到右,为从后到前)
- 正值以真正的二进制格式存储,负值以补码的形式存储
- NaN和Infinity在位操作中会被当成0处理
- 位操作符应用于非数值,自动使用Number()函数转换该值
按位非(~)
- 一个操作数
- 作用:返回数值的一补数(二进制数直接取反),在十进制中相当于取反并加1
按位与(&)
- 两个操作数
- 作用:将两个数的二进制表示对齐,执行”与“操作进行合并
按位或(|)
- 两个操作数
- 作用:将两个数的二进制表示对齐,执行“或”操作进行合并
按位异或(^)
- 两个操作数
- 作用:将两个数的二进制表示对齐,执行“异或”操作进行合并
左移(<<)
- a<<b:a是被左移的数,b是左移的位数
- 作用:按照指定的位数将数值的所有位向左移动
- 因左移而在右边空出来的位置用0填充
有符号右移(>>)
- a>>b:a是被右移的数,b是右移的位数
- 作用:按照指定的位数将数值的所有位向右移动,同时保留符号位
- 因右移而在左边空出来的位置用0填充
无符号右移(>>>)
- a>>>b:a是被右移的数,b是右移的位数
- 作用:按照指定的位数将数值的所有位向右移动,不管是不是符号位(因此负数左移后结果很大)
- 因右移而在左边空出来的位置用0填充
布尔操作符
逻辑非(!)
- 返回值:布尔值
逻辑与(&&)
- 可应用于任何类型的操作数
- &&是一个短路操作符:a&&b,若a对应的布尔值为true,则a&&b的结果是b;若a对应的布尔值为false,则a&&b的结果是false,第二个值就不管了
- 如果有一个操作数是null,返回null
- 如果有一个操作数是NaN,返回NaN
- 如果有一个操作数是undefined,返回undefined
逻辑或(||)
- 可应用于任何类型的操作数
- ||是一个短路操作符:a||b,若a对应的布尔值为false,则a||b的结果是b;若a对应的布尔值为true,则a||b的结果是true,第二个值就不管了
- 如果有一个操作数是null,返回null
- 如果有一个操作数是NaN,返回NaN
- 如果有一个操作数是undefined,返回undefined
- 典型应用:
// 第一个不是null或者undefined 第二个值就不管了
let myObject = preferredObject || backupObject
乘性操作符
乘法操作符(*)
- 可应用于任何类型的操作数
- 任一操作符是NaN,返回NaN
- Infinity*0=NaN
- Infinity*Infinity=Infinity
除法操作符(/)
- 可应用于任何类型的操作数
- 任一操作符是NaN,返回NaN
- Infinity/Infinity=NaN
- 0/0=NaN
取模操作符(%)
- 可应用于任何类型的操作数
- 任一操作符是NaN,返回NaN
- Infinity%c=NaN
- c%0=NaN
- Infinity%Infinity=NaN
指数操作符(**)
加性操作符
加法操作符(+)
- 用于数值求和:
- 任一操作符是NaN,返回NaN
- Infinity+(-Infinity)=NaN
- +0+(+0)=+0
- -0+(+0)=+0
- -0+(-0)=-0
- 字符串拼接
- 只要有一个操作数是字符串,就会将另一个操作数转换为字符串,并将两者拼接
- 若任一操作数是对象、布尔值或数值,则调用toString()转换
- 对于undefined和null,调用String()转换为“undefined”和“null”
减法操作符(-)
- 任一操作符是NaN,返回NaN
- Infinity-Infinity=NaN
- +0-(+0)=+0
- -0-(+0)=-0
- -0-(-0)=+0(无论是加还是减,都只有-0-0=-0)
关系操作符(< 、>、<=、>=)
- 任一操作符是NaN,返回false
- 结果为布尔值
- 若有任一操作数是字符串、对象或者是布尔值,最终都是数值的比较
- 若两个对象都是字符串,会逐个比较它们的字符编码
相等操作符
等于和不等于(==、!=)
- 任一操作符是NaN,相等操作返回false,不相等操作返回true(NaN==NaN 是false)
- 结果为布尔值
- 任一操作数是数值、布尔值,或者一个操作数是对象,另一个不是,都会转换为数值进行比较
- 两个操作数都是对象,比较它俩是否都指向同一个对象
- null==undefined 是true
- null和undefined不能转换成其他类型再比较
全等和不全等(=、!)
- 任一操作符是NaN,相等操作返回false,不相等操作返回true(NaN===NaN 是false)
- 结果为布尔值
- 在比较是不转换操作数
- null===undefined是false
条件操作符
- 具体用例:
let max = (num > num2) ? num1 : num2;
// 若(num > num2)为true (num > num2) ? num1 : num2===num1
// 若(num > num2)为false (num > num2) ? num1 : num2===num2
赋值操作符
逗号操作符
- 可以用于在一条语句中执行多个操作
- 在赋值时用逗号操作符分隔值,最终会返回表达式的最后一个值
语句
- if语句、do-while语句、while语句、for语句、break语句、continue语句与c语言几乎一样
for-in语句
- 一种严格的迭代语句,用于枚举对象中的非符号键属性
- 因为对象的属性是无序的,所以for-in语句不能保证返回对象属性的顺序
- 具体用例:
// 语法:for(property in expression) statement
let o = {
name: `xiaoming`,
age: 11,
height: 180,
weight: 150,
};
for (const propName in o) {
console.log(`${propName}`);
}
// name
// age
// height
// weight
for-of语句
- 一种严格的迭代语句,用于遍历可迭代对象的元素
- for-of循环会按照可迭代对象的next()方法产生值的顺序迭代元素
- 具体用例:
// 语法:for(property of expression) statement
for (const el of[1, 4, 3, 2]) {
console.log(el);
}
// 1
// 4
// 3
// 2
标签语句
- break语句和continue语句都可以与标签语句一起使用,返回代码中的特定位置(可以很方便地退出多层循环)
- 具体用例:
// break
let num = 0;
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (i == 5 && j == 5) {
break;
}
num++;
}
}
console.log(num);// 95
// break+标签 如果不用标签 会退出一层循环 这个标签在循环最外层 因此退出到了最外层循环
num = 0;
outermost:
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (i == 5 && j == 5) {
break outermost;
}
num++;
}
}
console.log(num);// 55
// continue
num = 0;
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (i == 5 && j == 5) {
continue;
}
num++;
}
}
console.log(num);
// continue+标签 如果不用标签,会结束本轮j循环进入j+1循环
// 用了标签,结束本轮i循环,进入i+1循环
num = 0;
outermost2:
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (i == 5 && j == 5) {
continue outermost2;
}
num++;
}
}
console.log(num);
switch语句
- switch语句与c语言的类似
- 判断条件:switch的参数与条件相等的情况下进入该语句
- 但是,switch语句可以用于所有的变量类型(字符串、变量都是可以的)
- 条件的值可以是常量,变量或者是表达式
- switch语句在比较条件的值时会使用全等操作符
- 具体用例:
// 案例中switch语句是布尔值 条件是表达式 若条件为true 与switch参数相等 就进入语句
let num = 25;
switch (true) {
case num < 0:
console.log(`num<0`);
breakl;
case num >= 0 && num < 10:
console.log(`0<=num<10`);
break;
case num >= 10 && num < 20:
console.log(`10<=num<20`);
break;
default:
console.log(`num>=20`);
}
函数
- 不需要指定是否返回值
- 碰到return语句,函数会立即停止执行并退出
- 推荐函数要么返回值,要么不返回值
- 不指定返回值的函数实际上会返回特殊值undefined
变量、作用域与内存
原始值与引用值
- 原始值:
- Undefined,Null,Boolean,Number,String,Symbol
- 保存原始值的变量是按值访问的,我们操作的就是存储在变量中的实际值
- 引用值:
- 保存在内存中的对象
- 保存引用值的变量是按引用访问的,实际操作的是对该对象的引用
动态属性
- 原始值:
- 原始值不能有属性(给其添属性不会报错,显示为undefined)
- 原始类型的初始化:
- 只使用原始字面量形式
- 使用new关键字,会创建一个Object类型的实例但其行为类似原始值
- 引用值:可以随时增添、修改、删除其属性和方法
- 具体用例:
// 原始类型使用new的初始化
let name = new String(`xiaoming`);
console.log(typeof name);//Object
复制值
- 原始值:通过变量把原始值赋值给另一个变量时,原始值复制了一份,放到了新变量的位置(两者完全独立)
- 引用值:复制的是对象的地址,两者实际上指向同一个对象
传递参数
- 无论是原始值还是引用值,都是按值传递的,只是引用值的值是一个地址,因此指向同一个地址
确定类型(instanceof)
- typeof适用于判断一个变量是否是原始类型,是什么原始类型
- instanceof适用于确定一个对象是什么类型的对象
- 语法:
- result = variable instanceof constructor
- result:布尔值
- variable:实例对象
- constructor:某个构造函数(Object/Array/RegExp等)
- 用instanceof检测原始值会返回false
执行上下文和作用域
执行上下文
- 执行上下文是一个名词概念,表示一个变量或者函数的关联区域
- 每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上面
- 上下文在其所有代码执行完毕后会被销毁,包括定义在其上面的变量和函数
- 全局上下文是最外层的上下文 (window对象)
- var声明的全局变量会成为window的属性和方法
- 每个函数都有自己的上下文
- 通过上下文栈来控制程序的执行流
- 代码执行流进入函数->函数上下文入栈->函数执行完毕->弹出函数上下文->控制权还给之前的执行上下文
作用域链
- 上下文中的代码在执行的时候,会创建变量对象的一个作用域链。
- 代码正在执行的上下文的变量对象始终位于作用域链的最前端
- 如果上下文是函数,则其活动对象用作变量对象
- 作用域链的下一个变量对象来自包含上下文(指包含自己这个上下文的上下文),再下一个对象来自再下一个包含上下文,依次类推至全局上下文。(实际上就是由大到小一条链,越小的越往链的前端跑)
- 代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的
- 内部上下文可以沿作用域链访问外部上下文的一切
- 外部上下文无法访问内部上下文的一切
- 函数参数被认为是当前上下文中的变量
作用域链增强
- try/catch语句,with语句会导致作用域链增强
- 指在作用域前端临时添加一个上下文,这个上下文在代码执行后会被删除
变量声明
- var声明的变量会被自动添加到最接近的上下文
- let和const的作用域是块级的
- 标识符查找
- 搜索开始于作用域链前端,以给定的名称搜索对应的的标识符
- 作用域中的对象也有一个原型链,因此搜索可能会涉及每个对象的原型链
- 搜索到了就不会再搜索下去了
垃圾回收
- 执行环境负责在代码执行期间管理内存
- 最常用的策略是标记清理
- 引用计数也是一种回收策略,但是在循环引用等有很大的问题
- 为避免循环引用,应在确保不使用的情况下切断原生JavaScript对象和DOM元素之间的联系
内存管理
- 如果数据不必要,那就把它设置为null,从而释放其引用(解除引用)
- 使用const和let而不是var有助于提升性能
- 尽量避免动态属性赋值或者动态添加属性,并在构造函数中一次性声明所有属性(这样多个对象共同使用一个隐藏类)
- 意外声明全局变量会造成内存泄漏
- 定时器也会导致内存泄露
- 使用闭包会造成内存泄露(闭包是指在函数里面声明函数,闭包也有许多优点)
基本引用类型
- 引用值(对象)是某个特定引用类型的实例
- 引用类型:描述了自己的对象应有的属性和方法
- 新对象通过new操作符后加一个构造函数(constructor)来创建
- 构造函数就是用来创建新对象的函数
- 函数也是一种引用类型
- 所有的引用类型都是基于Object类型的
Date
- 日期/时间组件方法,具体用例:
// 创建一个日期对象
let now = new Date(); // Mon Jul 13 1987 20:29:48 GMT+0900 (中国夏令时间)
// Date.now()方法返回执行时日期和时间的毫秒数 可用于代码分析(获得时间差)
console.log(Date.now()); // 1657715845530
let t1 = now.getTime(); // 1657715388159
// 返回四位数年
let y1 = now.getFullYear(); // 2022
// 返回UTC日期的四位数年
let y2 = now.getUTCFullYear(); // 2022
// 设置日期的年
let y3 = now.setFullYear(1987); // 553174258602
// 设置日期的年后 now对应的年改变了
let y4 = now.getFullYear(); // 1987
- 使用Date类型可以构建倒计时:
//定义倒数函数
function countDown(time) {
var nowTime = +new Date(); //当前时间
var inputTime = +new Date(time); //用户输入的时间
var times = (inputTime - nowTime) / 1000; //由毫秒得到秒
var d = parseInt(times / 60 / 60 / 24); //天
d = d < 10 ? '0' + d : d;
var h = parseInt(times / 60 / 60 % 24); //时
h = h < 10 ? '0' + h : h;
var m = parseInt(times / 60 % 60); //分
m = m < 10 ? '0' + m : m;
var s = parseInt(times % 60); //秒
s = s < 10 ? '0' + s : s;
return d + '天' + h + '时' + m + '分' + s + '秒'; //返回剩余时间
}
- 其他的方法类似,具体参考MDN
RegExp
- 通过RegExp支持正则表达式
创建正则表达式:
- 方法1:let expression = /pattern/flags
- 方法2:let pattern2 = new RegExp(“pattern”,“flags”)
- 方法3:基于已有的正则表达式实例,并选择性地修改标记
// 方法1
let pattern1 = /[bc]at/i;
// 方法2
let pattern2 = new RegExp("[bc]at","i");
// 方法3
const re1 = /cat/g;
const re2 = new RegExp(re1,"i");
正则表达式
标记(flags)
g | 全局模式,表示查找字符串的全部内容,而不是匹配一个就结束 |
---|---|
i | 不区分大小写 |
m | 多行模式,查到一行末会继续查找 |
y | 粘附模式,只查找从lastIndex开始及之后的字符串(最后一个索引对应的值为首) |
u | Unicode模式,启用Unicode匹配 |
s | dotAll模式,表示元字符,匹配任何字符串(包括\n和\r) |
转义
- 元字符:( { [ \ ^ $ | ) ] } ? * + .
- 要匹配上面这些字符本身,必须使用反斜杠(\)来转义
二次转义
- 在RegExp构造正则表达式时,在需要一次转义的地方进行二次转义(元字符前面要加两条斜杠)
RegExp实例的方法
RegExp.exec(string)
- 作用:配合捕获组使用(捕获组是指正则表达式中用小括号()包起来的那一部分
- 参数:一个,必选,是要应用模式的字符串
- 返回值:包含匹配信息的数组,若没找到匹配项,则返回null
- 返回值(数组)的属性:
- index:字符串中匹配模式的其实位置
- input:要查找的字符串
- 注意:如果正则表达式没有设置全局标记,对同一个字符串调用多少次exec(),也只会返回第一个匹配信息;若设置了全局模式(g标记),则每次调用exec()方法会返回一个匹配信息
RegExp.test(string)
- 作用:测试模式是否匹配
- 参数:一个,必选,用于测试的字符串
- 返回值:若输入的文本域模式匹配,则返回true,否则返回false
原始值包装类型
- 我们调用某个原始值的属性或者方法的时候,后台自动创建相应的原始包装类型对象,暴露属性和方法,用完之后,销毁该对象
Boolean
- Boolean类型的对象,本身是一个对象,而不是布尔值,在a&&b这种情况下要注意
Number
Number.toFixed(参数)
- 作用:返回包括小数点位数的数值字符串
- 参数:一个参数,表示返回的数值字符串小数点的位数(范围:0-20)
Number.toExponential(参数)
- 作用:返回以科学计数法表示的数值字符串
- 参数:一个参数,表示结果中小数的位数
Number.toPrecision(参数)
- 作用:根据情况返回最合理的结果,可能是固定长度,也可能是科学计数法
- 参数:一个参数,表示结果中数字的总位数(不包括指数)(小数位范围:1-21)
Number.isInteger(参数)
- 作用:用于辨别一个数值是否保存为整数
- 参数:一个参数,被辨别的数值
Number.isSafeInteger(参数)
- 作用:鉴别一个整数是否在安全范围内
- 参数:一个参数,被鉴别的整数
- 安全范围:Number.MIN_SAFE_INTEGER(-2(53)+1)~~Number.MAX_SAFE_INTEGER(2(53)-1)
String
String.length
- 表示字符串中字符的数量
String.concat()
- 作用:将一个或多个字符串拼接成一个新字符串
- 参数:可以接收任一多个参数
- 返回值:一个新字符串
String.slice(start,end)
- 作用:提取字符串,范围[start,end)
- 参数为负数的情况:范围以字符串长度加上负值参数
String.substr(start,number)
- 作用:提取字符串,范围为从start开始,数量为number
- 参数为负数的情况:第一个负值参数变成字符串长度加负值参数,第二个参数变0
String.substring(start,end)
- 作用:提取字符串,范围[start,end)
- 参数为负数的情况:负值参数全部变0
String.indexOf(string,start) 与 String.lastIndexOf(string,start)
- 作用:从字符串中搜索传入的字符串,前者从前往后,后者从后往前
- 参数:string为传入的字符串,start为搜索的开始位置
- 返回值:目标字符串所在位置的索引,若没找到,返回-1
- 具体用例:
let stringValue = `today is a sunny day,it's good to have a travel`;
// 存放目标字符串的位置
let position = new Array();
let pos = stringValue.indexOf(`a`);
// 找不到就停止
while (pos > -1) {
console.log(pos);
position.push(pos);
pos = stringValue.indexOf(`a`, pos + 1);// 往后一位
}
console.log(position); // [ 3, 9, 18, 35, 39, 43 ]
字符串迭代
- 具体用例:
for (const c of `abcd`){
console.log(c);
}
// a
// b
// c
// d
大小写转换
- String.toLowerCase()
- String.toLocaleLowerCase()
- String.toUpperCase()
- String.toLocaleCase()
字符串模式匹配
- String.match():根据参数匹配字符串并返回数组
- String.replace():参数1表示匹配模式,参数2表示替换字符串
- String.split():参数1是分隔符,参数2是数组大小,按照参数1将字符串分隔并存放到数组中
单例内置对象
Global
- 在全局作用域中定义的变量和函数都会变成Global对象的属性
Math
数学特殊值
Math.E | 自然对数的基数e的值 |
---|---|
Math.LN10 | 10为底的自然对数 |
Math.LN2 | 2为底的自然对数 |
Math.LOG2E | 以2为底e的对数 |
Math.LOG10E | 以10为底e的对数 |
Math.PI | pi的值 |
Math.SQRT1_2 | 1/2的平方根 |
Math.SQRT2 | 2的平方根 |
min()和max()方法
- 均可以接受任意多的参数,确定这组数的最小值或者最大值
舍入方法
- Math.ceil():向上舍入
- Math.floor():向下舍入
- Math.round():四舍五入
- Math.fround():返回数值最接近的单精度(32位)浮点值表示
random()方法
- random()方法返回一个0~1范围内的随机数[0,1}
- 随机数生成:
// 函数 功能:生成指定范围的随机数
function selectFrom(lowerValue, upperValue) {
let choices = upperValue - lowerValue + 1;
return Math.floor(Math.random() * choices + lowerValue);
}
- 若为了加密需要而生成随机数,使用window.crypto.getGandomValues()
其他方法
Math.abs(x) | 绝对值 |
---|---|
Math.exp(x) | e的x次幂 |
Math.expm1(x) | e的x次幂-1 |
Math.log(x) | x的自然对数 |
Math.log1p(x) | x的自然对数+1 |
Math.pow(x,power) | x的power次幂 |
Math.hypot(…nums) | 每个数平方和的平方根 |
Math.clz32(x) | 32位整数x的前置0的数量 |
Math.sign(x) | x的符号 |
Math.trunc(x) | x的整数部分 |
Math.sqrt(x) | 平方根 |
Math.cbrt(x) | 立方根 |
Math.acos(x) | 反余弦 |
Math.acosh(x) | 反双曲余弦 |
Math.asin(x) | 反正弦 |
Math.asinh(x) | 反双曲正弦 |
Math.atan(x) | 反正切 |
Math.atanh(x) | 反双曲正切 |
Math.atan2(x) | y/x的反正切 |
Math.cos(x) | 余弦 |
Math.sin(x) | 正弦 |
Math.tan(x) | 正切 |
集合引用类型
Object
- 在对象字面量中,属性名可以是字符串或者数值(数值会自动转换为字符串)
- 对象字面量很适合用于给函数传递大量参数,可选参数用对象来封装,必选参数使用命名参数
- 一般通过点语法使用对象的参数和方法
- 使用中括号访问属性的场景:
- 想要通过变量访问属性的时候
- 属性命中包含非字母数字字符的时候
Array
- 数组中每个槽位都可以存储任意类型的数据
- 数组是动态大小的,会随着数据添加而自动增长
- 在实践中要避免使用数组空位。如果确实需要空位,可以显式地使用undefined替代
- 使用Array.length属性可以很方便地向数组末尾添加元素
- Array.isArray()用于确定一个值是否为数组
创建数组
- 使用new
- 使用数组字面量
- Array.from():
- 将类数组结构转换为数组实例
- 第一个参数是一个类数组对象,即任何可迭代结构(字符串,集合,映射,数组,arguments,某些自定义对象)
- 第二个参数可选,为映射函数参数,用于直接增强新数组的值(不必再创建中间数组,新数组元素与可迭代结构相应元素一一对应)
- 第三个参数可选:用于指定映射函数中this的值,但这个重写的this在箭头函数中不适用
- Array.of():将一组参数转换为数组实例
- 具体用例:
const a1 = [1, 2, 3, 4];
// 使用映射函数参数,直接增强新数组的值
const a2 = Array.from(a1, x => x - Math.pow(x, 2));
// 指定this的值
const a3 = Array.from(a1, function(x) {
return x ** this.exponent;
}, { exponent: 2 });
console.log(a2); // [ 0, -2, -6, -12 ]
console.log(a3); // [ 1, 4, 9, 16 ]
迭代器方法
- Array.keys():返回数组索引的迭代器
- Array.values():返回数组元素的迭代器
- Array.entries():返回索引/值对的迭代器
let a = [1, 2, 3, 4, 5];
let aEntries = Array.from(a.entries());
let aKeys = Array.from(a.keys());
let aValues = Array.from(a.values());
console.log(aEntries); // [ [ 0, 1 ], [ 1, 2 ], [ 2, 3 ], [ 3, 4 ], [ 4, 5 ] ]
console.log(aKeys); // [ 0, 1, 2, 3, 4 ]
console.log(aValues); // [ 1, 2, 3, 4, 5 ]
// 利用解构拆分键值对
for (const [idx, element] of a.entries()) {
console.log(`${idx}:${element}`);
}
// 0:1
// 1:2
// 2:3
// 3:4
// 4:5
复制和填充
Array.fill(参数1,[start],[end])
- 参数1是填充物
- 填充范围为[start,end)(注意start和end都可以是负数,若是负值,则用数组长度加上负值)
- 静默忽略超出数组边界、零长度及方向相反的索引范围
Array.copyWithin(insert,start,end)
- 从一个数组中[start,end)的范围进行复制,插入到另一个数组的insert位置
- 静默忽略超出数组边界、零长度及方向相反的索引范围
转换方法
Array.toString() Array.toLocaleString()
- 对每个字符调用Array.toString()或者Array.toLocaleString()方法,拼接而成新字符串
Array.join(参数)
- 接受一个参数,参数表示字符串分隔符;若不传入参数或者传入undefined,默认逗号进行分隔
- 返回包含所有项的字符串
栈方法与队列方法
- 栈:先进后出(LIFO,Last-In-First-Out),最先添加的项先被删除
- 队列:先进先出(FIFO,First-In-First-Out),最后添加的先被删除
- Array.push():接受任意数量的参数,添加到数组末尾,返回数组的最新长度
- Array.pop():删除数组的最后一项,返回被删除的项
- Array.shift():删除数组的第一项并返回它
- Array.unshift():在数组开头增添任意多元素,返回数组的新长度
- 以上四个方法均改变了原数组
排序方法
Array.reverse()
- 将数组反向排列
Array.sort()
- sort()方法对数组中元素调用String()方法转型后再进行比较(比较个位数可以,但是多位数不行)
- sort()方法可以接受一个比较参数,用于判断哪个值应该排在前面(如果第一个参数应该排在第二个参数前面,就返回负值)
let values = [0, 4, 33, -9, -9, 0];
// 反向排序 使用箭头函数+条件操作符简化代码
values.sort((a, b) => a < b ? 1 : a > b ? -1 : 0);
console.log(values); // [ 33, 4, 0, 0, -9, -9 ]
// 正向排序
values.sort((a, b) => a > b ? 1 : a < b ? -1 : 0);
console.log(values); // [ -9, -9, 0, 0, 4, 33 ]
操作方法
Array.concat()
- 不影响原数组
- 在现有数组全部元素的基础上创建一个副本(新数组),再把参数接到副本末尾
- 如果参数是数组,默认会将数组打平再接到副本后面,此行为由Symbol.isConcatSpreadable控制,此值默认为true,将其设置为false就可以阻止concat()打平参数数组
const a1 = [1, 2, 3, 4];
const a2 = [5, 6];
a2[Symbol.isConcatSpreadable] = false;
const a3 = {
[Symbol.isConcatSpreadable]: true,
length: 2,
0: 5,
1: 6
};
console.log(a1.concat(a2));
console.log(a1.concat(a3));
Array.slice(start,[end])
- 创建一个原数组[start,end)的元素组成的新数组
Array.splice(start,number,[insert])
- start:要删除的第一个元素的位置
- number:要删除元素的数量
- insert:要插入的元素,元素插入到删除第一个删除元素的位置,插入元素的数量不限
- 返回值:返回被删除的元素组成的数组,若没有则返回空数组
- 通过灵活使用三个参数,可以实现删除,插入和替换
搜索和位置方法
严格相等
- 以下三个方法在比较的时候会使用全等(===)比较
Array.indexOf(target,start)
- 参数:要查找的元素以及可选的起始搜索位置
- 返回值:目标元素位置,没找到则返回-1
- 方向:从前往后
Array.lastIndexOf(target,start)
- 参数:要查找的元素以及可选的起始搜索位置
- 返回值:目标元素位置,没找到则返回-1
- 方向:从后往前
Array.includes(target,start)
- 参数:要查找的元素以及可选的起始搜索位置
- 返回值:找到返回true,找不到返回false
- 方向:从前往后
断言函数
- 断言函数接受三个参数:元素、索引和数组本身
- Array.find()返回第一个匹配的元素 Array.findIndex()返回第一个匹配元素的索引
- 这两个断言函数都可以接收第二个可选参数,用于指定断言函数内部this的值
const people = [{
name: `xiaoming`,
age: 11,
},
{
name: `xiaowang`,
age: 27,
},
];
console.log(people.findIndex((element, index, array) => element.age < 17));
// 0
console.log(people.find((element, index, array) => element.age < 17));
// { name: 'xiaoming', age: 11 }
迭代方法
- 每个方法接收两个参数,以每一项为参数运行的函数,以及可选的作为函数运行上下文的作用域对象(影响函数中this的值)。
- 传给每个方法的函数接收3个参数:数组元素,元素索引,数组本身
- 这些方法都不改变原数组
- Array.every():对数组的每一项都运行传入的函数,如果对每一项函数都返回true,则这个方法返回true(适合用于判断数组是否符合某一个条件)
- Array.filter():对数组的每一项都运行传入的参数,函数返回true的项会组成数组之后返回(适合用于从数组中筛选满足给定条件的元素)
- Array.forEach():对数组的每一项都运行传入的函数,没有返回值(相当于使用for循环遍历数组)
- Array.map():对数组的每一项都运行传入的函数,返回由每次函数调用的结果构成的数组(适用于获得一个与原始数组元素一一对应的新数组)
- Array.some():对数组的每一项都运行传入的参数,如果有一项返回true,则这个方法返回true(适合用于判断数组是否符合某一个条件)
// every()方法
const a = [1, 2, 3, 4];
console.log(a.every((item, index, array) => item < 5));// true
归并方法
- Array.reduce() Array.reduceRight()
- 这两个方法都会迭代数组的所有项,并在此基础上构建一个返回值
- 两个方法的区别点在于reduce()从前往后,reduceRight()从后往前
- 接收两个参数,第一个参数是对每一项都运行的归并函数,第二个参数是可选的以之为归并起点的初始值
const a1 = [1, 2, 3, 4];
// 归并函数接收4个参数
// prev:上一个归并值 cur:当前项 index当前项的索引 array:数组本身
console.log(a1.reduce((prev, cur, index, array) => prev + cur));
// 10
定型数组
ArrayBuffer()
- ArrayBuffer是一个构造函数,可用于在内存中分配特定数量的内存空间,ArrayBuffer已经创建就不能调整大小
- 参数,一个参数,为创建的arrayBuffer的大小
- ArrayBuffer会将所有的二进制位初始化为0
- 要读取或者写入ArrayBuffer,必须通过视图
定型数组
- 定型数组是一种ArrayBuffer视图,但并不是唯一一种
- 定型数组的类型以Array 来进行划分(如Int8Array是一种类型)
ElementType
ElementType | 字节 | 说明 | 范围 |
---|---|---|---|
Int8 | 1 | 8位有符号整数 | -128-127 |
Uint8 | 1 | 8位无符号整数 | 0-255 |
Int16 | 2 | 16位有符号整数 | -32768-32767 |
Uint16 | 2 | 16位无符号整数 | 0-65535 |
Int32 | 4 | 32位有符号整数 | -2147483648-2147483647 |
Uint32 | 4 | 32位无符号整数 | 0-4294967295 |
Float32 | 4 | 32位IEEE-754浮点数 | -3.4e+38~+3.4e+38 |
Float64 | 8 | 64位IEEE-754浮点数 | -1.7e+308~+1.7e+308 |
定型数组行为
- 定型数组大部分的方法都可以照搬普通数组,但要记住,直接修改原数组的方法不可以用到定型数组上面
新增方法
- set()
- 从提供的数组或定型数组中把值复制到当前定型数组中指定的索引位置
- 参数:第一个参数必选,为提供的数组或者定型数组;第二个参数可选,偏移量,默认为0
- subarray():
- 基于从原始定型数组中复制的值返回一个新的定型数组
- 复制时开始和结束的索引都是可选的
下溢和上溢
- 定型数组的下溢和上溢不会影响到其他索引
- 可以理解为每一个位置都是固定的,溢出的部分按照二进制固定位切除
Map
- 一种新的集合类型
创建,增添,查询,删除
- Map.set():增添键值对,参数为一个键值对,返回新的集合
- Map.get():通过键名进行查询,返回对应的值(严格对象相等)
- Map.has():通过键名进行查询,返回布尔值(严格对象相等)
- Map.delete:通过键名进行对应键值对的删除(严格对象相等)
- Map.clear():清除这个映射中的所有键值对
- Map.size:获取该映射中键值对的数量
顺序与迭代
- entries()方法,keys()方法,values()方法:分别返回以插入顺序生成的键值对,键,值的迭代器
- 通过for-of进行迭代就好了
优点
- Map的内存占用更小
- Map插入速度较Object快一点
- Map的删除操作较快
WeakMap
- 弱映射
- 行为模式方法都与Map差不多
- WeakMap的键只能是Object类型或者是继承自Object的类型,值的类型没有限制
- 若没有指向键这个对象的引用,这个对象键会被自动回收
- WeakMap的键值对不可迭代
- 应用场景:保存关联元数据
- 保存DOM节点元数据,当对应的DOM 节点被删除后,若没有其他的关联,该键就会被销毁
Set
- Set在大部分方面与Map类似(就把它当做键值都是同一个东西的Map好了)
- 与Map不同的是,使用add()方法增添元素
- 定义一个实用的Set函数库(函数定义书上有,但是看不懂)
WeakSet
- WeakSet与Set的关系跟WeakMap与Map的关系是一样的
迭代与扩展操作
- 扩展操作符(…),扩展操作符对可迭代对象实行的是浅复制(意味着只会复制对象的引用)
迭代器与生成器
迭代
- 迭代器是一个可以由任意对象实现的接口,支持连续获取对象产出的每一个值
- 任何实现Iterable接口的对象都有一个Symbol.iterator属性,这个属性引用默认迭代器
- 默认迭代器是一个工厂函数,调用之后会返回一个实现Iterator接口的对象
- 迭代器必须通过连续调用next()方法才可以连续获取值,这个方法返回一个IteratorObject。
- 这个对象包含一个done属性和一个value属性。
- 前者是一个布尔值,表示是否还有更多的值可以访问
- 后者包含迭代器返回的当前值
- 这个方法可以通过手动调用next()方法进行消费,也可以通过原生消费,比如for-of循环来自动消费
- 提前终止迭代器:
- 可选的return()方法可以提前终止迭代器
- for-of循环通过break,continue,return,或throw提前退出迭代
- 解构操作并未消费所有值的时候提前退出迭代
- 后面两个是表层的实现,a是迭代器内部的方法,是实现b,c的前提
const a = [1, 2, 3, "4", { b: 5, c: 6 }];
const a1 = [1];
// 调用数组的[Symbol.iterator]属性这个函数 返回一个迭代器
let iter = a[Symbol.iterator]();
let iter2 = iter[Symbol.iterator]();
// 迭代器与迭代器的迭代器全等
console.log(iter2 === iter); // true
// 通过break停止了迭代 但迭代器本身是没有停止的
for (const i of iter) {
console.log(i);
if (i > 2) {
console.log(`stop or not?`);
break;
}
}
// 1
// 2
// 3
// stop or not?
for (const i of iter) {
console.log(i);
}
// 4
// { b: 5, c: 6 }
生成器
- 生成器是一种特殊的函数,调用之后返回一个生成器对象
- 生成器对象实现了Iterator接口,可以用在任何消费可迭代对象的地方
- 生成器的独特之处在于支持yield关键字,这个关键字能够暂停生成器函数
- 使用yield关键字之后还可以通过next()方法接收输入和产生输出(next是生成器函数继续运转)
// 创建一个生成器
function* generatorFn(initial) {
console.log(initial);
console.log(yield);
}
// 实例化一个生成器对象
const generatorObject = generatorFn('foo');
// 第一个next()方法启动生成器对象,此传入值无效
generatorObject.next(`zro`);// foo
generatorObject.next(`abb`);// abb
// 因为最后没有代码了,再调用next()也没有用了
generatorObject.next(`cdd`);
- yield*可以将跟在它后面的可迭代对象序列化为一连串值
function* generatorFn() {
// 使用yield可以很方便地进行迭代
yield*[1, 2, 3];
}
const g = generatorFn();
for (const k of g) {
console.log(k);
}
// 1
// 2
// 3
- 使用yield*实现递归算法
function* generatorFn(n) {
if (n > 0) {
yield* generatorFn(n - 1);
yield n - 1;
}
}
const g = generatorFn(3);
for (const k of g) {
console.log(k);
}
// 0
// 1
// 2
- value属性是生成器的返回值,默认为undefined,可以通过生成器的返回指定
function* generatorFn() {
// 注意这个是生成器的返回 后面还有一个生成器的方法return() 是用来终止生成器的
return `foo`;
}
const g = generatorFn();
console.log(g.next()); // { value: 'foo', done: true }
- 提前终止生成器
- return()方法用于提前终止生成器,进入了关闭状态之后就无法恢复了
- throw()方法在暂停的时候会将错误注入到生成器对象中。
- 如果错误未被处理,生成器就会被关闭。
- 如果错误被处理了,生成器会跳过对应的yield,但可以恢复执行
function* generatorFn() {
for (const x of[1, 2, 3]) {
try {
yield x;
} catch (e) {}
}
}
const g = generatorFn();
console.log(g.next());
// 这个结果与我想象中不太一样 调用throw()方法后2还是被返回了 我不理解
// 并且如果调用throw()方法 生成器内部不处理的话 编辑器是会报错的
// 如果再第一次调用next()方法之前就使用throw()方法的话 也会报错 因为此时生成器还没有启动
console.log(g.throw());
console.log(g.next());
// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
对象、类与面向对象编程
对象
属性的类型
数据属性
- 数据属性包含一个保存数据值的位置
- 数据属性有四个特性:
- [[Configurable]]:属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它修改为访问器属性。默认为true
- [[Enumerable]]:表示属性是否可枚举(是否可以通过for-in循环返回)。默认为true
- [[Writable]]:表示属性值([[Value]])是否可修改。默认为true
- [[Value]]:属性值。默认为undefined
let person = {};
Object.defineProperty(person, `age`, {
configurable: false,
enumerable: true,
writable: true,
value: 3,
});
console.log(person.age);
Object.defineProperty(person, `name`, {
value: 2,
});
console.log(person.age);
访问器属性
- 访问器属性不包含数据值
- 访问器属性有四个特性:
- [[Configurable]]:属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它修改为访问器属性。默认为true
- [[Enumerable]]:表示属性是否可枚举(是否可以通过for-in循环返回)。默认为true
- [[Get]]:获取函数,在读取访问器属性时,会调用获取函数,并返回一个有效值。默认为undefined
- [[Set]]:设置函数,在写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改。默认为undefined
- 获取函数和设置函数不一定都要定义。只定义获取函数意味着属性是只读的,尝试修改属性会被忽略
对属性的操作
定义和修改属性
- Object.defineProperty()方法用于一次设置一个属性
- Object.defineProperties()方法可用于一次设置多个属性
- 三个参数:要给其添加属性的对象、属性的名称、一个描述符对象(描述符对象上的属性可以包括四个特性,根据有没有数据位置来分辨数据属性和访问器属性)
读取属性特性
- Object.getOwnPropertyDescriptor()方法
- 获取指定属性的属性描述符
- 两个参数:属性所在的对象、属性名
- 返回值:一个对象
- Object.getOwnPropertyDescriptor()方法
- 获取某个对象全部属性的描述符
- 一个参数:某个对象
- 返回值:一个对象
合并对象
- Object.assign()方法
- 参数:一个目标对象、一个或多个源对象
- 原理:将源对象中可枚举属性浅复制到目标对象(从源对象的[[Get]]取得属性的值,使用目标函数对象上的[[Set]]设置属性的值。
对象标识及相等判定
- Object.is()方法
- 参数:两个用于比较的对象
- 若想检查超过两个值,函数如下:
// 函数 用于比较一个或多个值是否相等
function recursivelyCheckEqual(x, ...rest) {
return Object.is(x, rest[0]) && (rest.length < 2 || recursivelyCheckEqual(...rest));
}
console.log(recursivelyCheckEqual(1, ...[1, 1, 1])); // true
console.log(recursivelyCheckEqual(...[1, 1, 1, 1])); // true
console.log(recursivelyCheckEqual([1, 1, 1, 1])); // false
console.log(recursivelyCheckEqual(1)); // false rest[0]是undefined,即没有第二个参数的情况下,默认为undefined
console.log(recursivelyCheckEqual(1, 1, 1, 1, 1, 1)); // true
增强对象的语法
- 属性值简写:简写属性名只要使用变量名(不用冒号)就会被自动解释为同名的属性键(这是有同名变量的情况下)
- 可计算属性:可以在对象字面量中完成动态属性赋值。中括号包围的对象属性键本身可以是复杂的表达式。
- 简写方法名
const methodKey = `sayName`;
let person = {
name_: ``,
// 简写了方法名
get name() {
return this.name_;
},
// 由name属性的get函数进行简单的获取 没有这个get函数并不影响设置this.name_的值
// 再由name属性的set函数设置this.name_
// 这样调用sayName()的时候this.name_就是`Matt`了
set name(name) {
this.name_ = name;
},
// 这里使用可计算属性
[methodKey]() {
console.log(`My name is ${this.name_}`);
}
};
person.name = `Matt`; // get和set都是访问器属性name的函数
person.sayName(); // My name is Matt
对象解构
- 对象解构是使用与对象匹配的结构来实现对象属性赋值
- 使用解构,可以在一个类似对象字面量的结构中,声明多个变量,同时执行多个赋值操作,如果想让变量直接使用属性的名称,可以使用简写语法(类似于属性值简写,调换过来了)
- 如果引用的属性不存在,则该变量的值是undefined
- 可以在解构赋值的同时定义默认值
- 解构会把源数据结构转换为对象(导致原始值会被当成对象,也就是原始包装类型的属性和方法是可以使用的)
嵌套解构
- 解构对于嵌套的属性或赋值目标没有限制
let person = {
name: `Matt`,
age: 16,
job: {
title: `software engineer`,
}
};
let personCopy = {};
// 注意:这里是把一个对象的引用直接赋值给personCopy了 也就是只传了地址 一改全改
// 个人认为这里本质上是用一个对象作为过渡
// 但是我不明白为什么personCopy.name可以使用 不是没有定义吗
// 还是说 这一整块放进去括号里面 就默认声明了?
({
name: personCopy.name,
age: personCopy.age,
job: personCopy.job
} = person);
console.log(personCopy);
创建对象
构造函数模式
- 使用new操作符调用构造函数创建对象实例
- 在内存中创建一个新对象
- 在这个对象内部的[[Prototype]]特性被赋值为构造函数的prototype属性
- 构造函数内部的this指向新对象
- 执行构造函数内部代码(给新对象加属性和方法)
- 若构造函数返回非空对象O,则返回非空对象O;否则,返回刚创建的新对象
- 构造函数的问题:
- 若方法函数在构造函数内部声明,则不同实例上的函数虽然同名但不相等
- 若方法函数统一放在外面声明,则自定义类型引用的代码不能很好地聚在一起
// 构造器函数
function Person(name, age, job) {
console.log(this);
this.name = name;
this.age = age;
this.job = job;
this.sayNmae = function() {
console.log(this.name);
}
}
// 实例化对象 类型为Person
let person1 = new Person(`kitty`, `22`, `worker`); // Person {}
// 若直接调用函数 内部this指向为window
Person(`hello`, `3`, `play game`); // 指向window
原型模式
- 每个函数都会创建一个prototype属性,这个属性是一个对象,包含由特定引用类型的实例共享的属性和方法(这个就是原型)
- 默认情况下,所有原型对象自动获得一个名为constructor的属性,指向与之关联的构造函数
- 每次调用构造函数创建一个新实例,这个实例内部的[[Prototype]]指针就会被赋值为构造函数的原型对象
- 实例与构造函数原型之间有直接联系(prototype指向原型),原型与构造器之间有直接联系(constructor指向构造函数,prototype指向原型)
- 方法:
- Function.prototype.isPrototypeOf(Object)
- 确定两个对象时间是否是对应的原型与实例
- Function.prototype是原型
- Object是对象实例
- Object.getPrototypeOf(Object)
- 获取对象实例的原型
- 这是一个Object类型的方法
- 参数Object是对象实例
- Object.create(Function.prototype)
- 创建一个新对象,同时为其指定原型
- 参数为指定的原型,是一个对象
- 返回值:对象
- Function.prototype.isPrototypeOf(Object)
属性查找机制
- 在通过对象访问属性的时候,会按照属性名进行查找
- 搜索开始于对象本身,找到返回,找不到下一步
- 搜索进入原型(进入prototype属性)进行查找,找到返回
遮蔽效果
- 给对象实例添加一个属性,这个属性会遮蔽原型上的同名属性(即你引用这个属性名,得到的是你自己给对象实例设置的那个属性数据)
- 使用delete操作符可以完全删除实例上的属性,取消遮蔽效果
in操作符
- 单独使用:
- in操作符会在可以通过对象访问指定属性时返回true
- hasOwnPrototype()方法会在属性存在于调用它的对象实例上时返回true
// 函数 作用 确定某个属性是否存在于实例对象原型上
// Object是实例对象 name是属性名 为字符串
function hasPrototypeProperty(Object, name) {
return !Object.hasOwnProperty(name) && (name in Object);
}
function Person() {
Person.prototype.name = 'Mit';
Person.prototype.age = 13;
}
const person = new Person;
console.log(hasPrototypeProperty(person, `name`)); // true
person.name = `Code`;
console.log(hasPrototypeProperty(person, `name`)); // false
for (k in person) {
console.log(k);
}
- for-in
- 可以通过对象访问且可以被枚举的属性会全部返回,遮蔽原型中不可枚举属性的实例属性也会被返回
属性枚举
- for-in
- Object.keys()方法:接受一个对象作为参数,返回包含该对象所有可枚举属性名称的字符串数组
- Object.getOwnPrototypeNames():接受一个对象作为参数,列出所有实例属性,无论是否可枚举
- Object.getOwnPrototypeStmbol():针对符号,与上面类似
- 2、3、4的枚举顺序是确定的,先以升序枚举数值键,再插入顺序枚举字符串和符号建
对象迭代
- Object.values():接收一个对象,返回对象值的数组
- Object.entries():接收一个对象,返回键值对的数组
- 注意:
- 非字符串属性会被转换为字符串输出
- 对执行对象进行的是浅复制
- 符号属性会被忽略
其他原型语法
- 直接通过一个包含所有属性和方法的对象字面量来重写原型
原型的动态性
- 任何时候对原型所做的修改也会在实例上面反映出来
- 前提是你这个实例确确实实是指向这个原型(就是说你不是先创建实例再重写原型)
原型的问题
- 原型的问题主要是共享性
- 大家都是可以通过属性名访问原型的,数据一个改了就相当于全部改了
继承
- 原型链是主要继承方式
原型链
- 任何函数的默认原型都是一个Object的实例
- 属性可以沿着原型链一直向上查找直到Object
- 原型与继承关系:
- instanceof操作符:如果一个实例的原型链中出现过相应的构造函数,则返回true
- isPrototypeOf()方法,只要原型链中包含这个原型,就会返回true
- 以对象字面量创建原型的方法会破坏之前的原型链,因为这相当于重写了原型链
- 原型链的问题:原型链中包含引用值的时候,数据就共享了,大家都可以改
- 原型链本链:
- 具体实例:
// 4. 在SuperType的方法上面寻找 找不到
function SuperType() {
this.property = true;
}
// 5. 在SuperType的prototype的方法上面寻找 找到了 返回值是this.prototype 此值为true
SuperType.prototype.getSuperValue = function() {
return this.property;
};
// 1. 在SubType的方法上面寻找 找不到
function SubType() {
this.subProperty = false;
}
// 3. SubType的prototype定义在SuperType上面
SubType.prototype = new SuperType();
// 2. 在SubType的prototype的方法上面寻找 找不到
SubType.prototype.getSubValue = function() {
return this.subProperty;
};
let instance = new SubType();
// 目的:寻找getSuperValue方法
console.log(instance.getSuperValue()); // true
盗用构造函数
- 在子类构造函数中调用父类构造函数
- 优点:可以在子类构造函数中向父类构造函数传参
- 缺陷:必须在构造函数中定义方法,因此函数不能重用
组合继承
- 综合了原型链和盗用构造函数,通过原型链实现方法,且通过盗用构造函数让每实例都有自己的属性
- 具体用例:
// 组合继承实现了数据的私有以及方法的共享
function SuperType(name) {
this.name = name;
this.color = [`black`, `blue`, `white`];
}
// 这是原型链 定义方法
SuperType.prototype.sayName = function() {
console.log(this.name);
};
// 这是盗用构造函数 定义属性
function SubType(name, age) {
SuperType.call(this, name);
// 增添新数据
this.age = age;
}
SubType.prototype = new SuperType();
// 向自己的原型链增添新方法
SubType.prototype.sayAge = function() {
console.log(this.age);
}
// 创建SubType的实例1
const instance1 = new SubType(`Marry`, 19);
instance1.color.shift();
console.log(instance1.color); // [ 'blue', 'white' ]
instance1.sayAge(); // 19
instance1.sayName(); // Marry
// 创建SubType的实例2
const instance2 = new SubType(`Tracy`, 23);
instance2.color.push(`gray`);
// 实例1与实例2的数据并不会互相影响
console.log(instance2.color); // [ 'black', 'blue', 'white', 'gray' ]
// 但实例1、实例2和原型链上的方法是共享的
instance2.sayAge(); // 23
instance2.sayName(); // Tracy
原型式继承
- 原型式继承适用于不需要单独创建构造函数,但仍需要在对象间共享信息的场合
- 原理:通过原型链来共享信息
- Object.create()方法:
- 此方法将原型式继承概念化
- 两个参数:作为新对象原型的对象、给新对象定义额外属性的对象(可选)
- 以同一原型创建的对象实例间可以信息共享
- 具体用例
// 这个object()函数会创建一个临时的构造函数,将传入的对象赋值给这个构造函数的原型,
// 然后返回这个临时类型的一个实例
// 本质上,object()是对传入的对象进行了一次浅复制
function object(o) {
function F() {}
F.prototype = o;
return new F;
}
寄生式继承
- 创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象
寄生式组合继承
- 组合继承存在效率问题:父类构造函数始终会被调用两次
- 寄生式组合继承可以算是目前引用类型继承的最佳模式
- 具体用例:
// 寄生式的核心函数
function inheritPrototype(SubType, SuperType) {
const prototype = new Object(SuperType.prototype); // 创建对象
prototype.constructor = SubType; // 增强对象
SubType.prototype = prototype; // 赋值对象
}
function SuperType(name) {
this.name = name;
this.color = [`black`, `blue`, `white`];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
// SubType.prototype = new SuperType(); 这一行被取代了
// 原本是直接创建一个SuperType实例作为SubType.prototype
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
}
const instance1 = new SubType(`Marry`, 19);
instance1.color.shift();
console.log(instance1.color); // [ 'blue', 'white' ]
instance1.sayAge(); // 19
instance1.sayName(); // Marry
const instance2 = new SubType(`Tracy`, 23);
instance2.color.push(`gray`);
console.log(instance2.color); // [ 'black', 'blue', 'white', 'gray' ]
instance2.sayAge(); // 23
instance2.sayName(); // Tracy
类
类定义
- 类的背后使用的是原型和构造函数的概念
- 类受块作用域限制
- 默认情况下,类中的代码都在严格模式下执行
类构造函数
实例化
- constructor关键字用于在类定义内部创建类的构造函数
- 在使用new操作符创建类的新实例时,会调用constructor方法
- 类实例化时传入的参数会用作类构造函数的参数
- 默认情况下,类构造函数在执行后会返回this对象(就是类本身);但是如果让类构造函数返回别的对象,那实例的类型就改变了
- 调用类构造函数必须使用new
把类当成特殊函数
- 类标识符有prototype属性,这个原型也有一个constructor属性指向类自身
- 类中定义的constructor方法不会被当成构造函数(new的是谁谁就是构造函数,new 类,类是构造函数;new constructor,constructor是构造函数
- 类可以作为参数传递
实例、原型和类成员
实例成员
- 每个实例都对应一个唯一的成员对象,这意味着所有的成员都不会在原型上共享
原型方法与访问器
- 添加到this的所有内容都会存在于不同的实例上面,在类块中定义的所有内容都会定义在类的原型上
- 不能再类块中给原型添加原始值或对象作为成员数据,但是可以添加方法
- 类定义支持获取和设置访问器
- 具体用例:
class Person {
constructor() {
this.locate = () => console.log(`instance`);
}
locate() {
console.log(`prototype`);
}
}
const p = new Person();
p.locate(); // instance
Person.prototype.locate(); // prototype
静态类方法
- 通常用于执行不特定于实例的操作,也不要求存在类的实例
- 静态成员每个类上只能有一个
- 使用static关键字作为前缀
非函数原型和类成员
- 类定义不显式支持在原型或类上添加成员数据
- 但在类定义外部,可以手动添加
迭代器与生成器方法
- 类定义语法支持在原型和类本身上定义生成器语法
- 可以通过添加一个默认的迭代器([Symbol.iterator]),将类实例变成可迭代对象
继承
继承基础
- 类继承背后是原型链
- 使用extends关键字,可以继承任何拥有[[Construct]]和原型的对象
- 派生类(继承者)都会通过原型链访问到类和原型上定义的方法,this的值指向调用相应方法的实例或类
super关键字
- 派生类的方法可以通过super关键字引用它们的原型
- 且仅限于类构造函数、实例方法和静态方法内部
- 在类构造函数中使用super可以调用父类构造函数
- 在静态方法中可以通过super调用继承的类上定义的静态方法
- 注意:
- super只能在派生类构造函数和静态方法中使用
- 不能单独引用super关键字
- 调用super()会调用父类构造函数,并将返回的实例赋值给this
- 在类构造函数中,不能在调用super之前引用this
- 如果再派生类中显式定义了构造函数,则要么必须在其中调用super(),要么必须在其中返回一个对象
抽象基类
- 抽象基类是指可供其他类继承,但本身不会被实例化的类
// 抽象基类
class Vehicle {
constructor() {
console.log(new.target);
if (new.target === Vehicle) {
throw new Error(`Vehicle cannot be directly instantiated`);
}
}
}
// 派生类
class Bus extends Vehicle {};
new Bus(); // [class Bus extends Vehicle]
// new Vehicle(); // Error: Vehicle cannot be directly instantiated
继承内置类型
- 有些内置类型的方法会返回新实例。默认情况下,返回实例的类型与原始实例的类型是一致的
- 如果想覆盖这个行为,可以覆盖Symbol.species访问器,这个访问器决定在创建返回的实例时使用的类
类混入
- 如果只需要混入多个对象的属性,可以使用Object.assign()方法
- 混入模式可以通过在一个表示式中连缀多个混入元素来实现,这个表达式最终会解析为一个可以被继承的类
- 具体用例:
class Vehicle {}
let FooMixin = (Superclass) => class extends Superclass {
// 这里面放自己需要的代码
foo() {
console.log(`foo`);
}
};
let BarMixin = (Superclass) => class extends Superclass {
bar() {
console.log(`bar`);
}
};
let BazMixin = (Superclass) => class extends Superclass {
baz() {
console.log(`baz`);
}
};
// 通过这个辅助函数 将嵌套调用展开 不过这个函数我看不懂
function mix(BaseClass, ...Mixins) {
return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass);
}
class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {}
let b = new Bus();
b.foo(); // foo
b.bar(); // bar
b.baz(); // baz
- 现在更提倡组合模式
- 把方法提取到独立的类和辅助对象中,然后把它们组合起来,但不使用继承
- 这样在代码设计中可以提供很大的灵活性
函数
箭头函数
- 箭头函数不能使用argements、super和new.target
- 箭头函数不能用作构造函数
- 箭头函数没有prototype属性
函数名
- 函数名是指向函数的指针,所以一个函数可以有多个名称
- 使用不带括号的函数名会访问函数指针,而不会执行函数
参数
理解参数
- JS中函数既不关心传入参数的个数,也不关心这些参数的数据类型
- JS的函数参数在内部表现为一个数组
- argements
- argements对象是一个类数组对象,存储着传进来的每个参数值
- argements.length是它的长度
- argements对象的值会自动同步到对应的命名参数;但是argements跟命名参数的值在内存中是分开的,只是会保持同步而已
默认参数
- 在ES6之后,可以显式定义默认参数,只要在函数定义中的参数后面用=就可以为参数赋一个默认值
- 给函数传undefined相当于没有传值,不过这样可以利用多个独立的默认值
- argements对象的值不反映参数的默认值,只反映传给函数的参数
- 默认参数值可以是调用函数返回的值
- 函数的默认参数只有在函数被调用的时候才会求值,不会在函数定义时求值
- 默认参数会按照定义它们的顺序依次被初始化
- 后定义的默认值可以引用先定义的参数,前面定义的参数不能引用后面定义的参数
参数扩展与收集
- 扩展操作符既可以用于调用函数时传参,也可以用于定义函数参数
- 扩展参数:对可迭代对象应用扩展操作符,并且将其作为一个参数传入,可以将可迭代对象拆分,并将迭代返回的每个值单独传入
- 收集参数:
- 在构思函数定义时,可以使用扩展操作符把不同长度的独立参数组合为一个数组
- 收集参数的操作只能放到形参列表的最后
函数声明与函数表达式
- 函数声明会被提升,但是函数表达式不会
函数作为值
- 函数名在JavaScript中就是变量,所以函数可以用在任何可以使用变量的地方
- 函数可以作为参数传给另一个函数,也可以在一个函数中返回另一个函数
函数内部
argements
- callee:是argements对象的一个属性,是一个指向argements对象所在函数的指针
- 使用argements.callee可以很方便地将函数逻辑与函数名解耦
- 但是,在严格模式下访问argements.callee会报错,因此不推荐使用,可以使用命名函数表达式解决耦合问题
this
- 在标准函数中,this指向把函数当成方法调用的上下文对象
- 在箭头函数中,this指向定义箭头函数的上下文,可以使用箭头函数解决事件回调或定时器回调函数的问题
function King() {
console.log(this); // King {} 这是一个普通函数 且不是以方法的形式调用 this就指向函数本身
this.name = 'xiaoming';
setTimeout(() => {
console.log(this); // King { name: 'xiaoming' } 这是一个箭头函数 定义在定时器里面
// 但上下文据此推测是该函数
console.log(this.name); // xiaoming
}, 5000);
}
function Queen() {
console.log(this); // Queen {}
this.name = `xiaohong`;
setTimeout(function() {
// 这是一个普通函数 调用该函数的是定时器 因此该函数内部的this指向定时器
console.log(this);
// Timeout {
// _idleTimeout: 10000,
// _idlePrev: null,
// _idleNext: null,
// _idleStart: 51,
// _onTimeout: [Function (anonymous)],
// _timerArgs: undefined,
// _repeat: null,
// _destroyed: false,
// [Symbol(refed)]: true,
// [Symbol(kHasPrimitive)]: false,
// [Symbol(asyncId)]: 9,
// [Symbol(triggerId)]: 1
// }
console.log(this.name); // undefined
}, 10000)
}
new King();
new Queen();
new.target
- 用于检测函数是否使用了new关键字
- 如果函数正常调用,new.target的值是undefined;入股使用new关键字调用,则new.target将指向被调用的构造函数
函数的属性和方法
属性
- length:保存函数定义的命名参数的个数
- prototype:保存了引用类型所有实例方法,此属性是不可枚举的
方法
- apply():
- 作用:以指定的this值来调用函数(设置函数调用是函数体内的this对象的值)
- 两个参数:函数内的this值,一个参数数组(可以是Array实例、argements对象)
- call():
- 作用:以指定的this值来调用函数(设置函数调用是函数体内的this对象的值)
- 两个参数:函数内的this值,第二个参数是逐个传递给该方法的参数
- bind():
- 创建一个新的函数实例,其this值会被绑定到传给bind()对象的值
递归
- 善于使用命名函数表达式将函数逻辑与函数名解耦
// 这里是先执行函数命名表达式 因为它在括号里面
// 执行函数命名表达式的结果是返回一个函数的地址 跟函数表达式声明一样
// 其实这里括号不要也可以
const factorial = (function f(num) {
if (num <= 1) {
return 1;
} else {
return num * f(num - 1);
}
});
// 首先 factorial是动不了的 其次动了也没有意义
// 要减少耦合 只是说赋值给别人后 别人也可以用
const anotherFactorial = factorial;
console.log(factorial); // [Function: f]
console.log(anotherFactorial(3));
尾调用优化
尾调用优化的条件
- 代码在严格模式下执行
- 外部函数的返回值是对尾调用函数的调用
- 尾调用函数返回后不需要执行额外的逻辑
- 尾调用函数不是引用外部函数作用域中自由变量的闭包
- 斐波那契数列用例:
// 优化后
`use strict`;
// 基础框架
function fib(n) {
return fibImpl(0, 1, n);
}
function fibImpl(a, b, n) {
if (n === 0) {
return a;
}
return fibImpl(b, a + b, n - 1);
}
console.log(fib(1000));
// 优化前
function fib(n) {
if (n < 2) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
console.log(fib(1000));
闭包
闭包概念与原理
- 闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的
- 外部函数的活动对象是内部函数作用域上的第二个对象
- 在定义内部函数的时候,就会为它创建作用域链,预装载全局变量对象,并保存在内部的[[Scope]]中。在调用这个函数时,会创建相应的执行上下文,然后通过复制函数的[[Scope]]来创建其作用域链
- 作用域链其实是一个包含指针的列表,每个指针分别指向一个变量对象
- 在A函数内部定义的B函数的活动对象会把A函数的活动对象添加到自己的(A的)作用域链中
- 外部函数的活动对象不能在自己活动完毕后销毁,因为内部函数的作用域链中仍有对它的引用,知道内部函数被销毁后外部函数的活动对象才会被销毁
this对象
- 内部的匿名函数不会绑定到特定对象,所以其this指向window(严格模式下是undefined)
内存泄漏
- 使用闭包还有内存泄漏的问题
- 要注意对象在不使用时候要解绑
私有变量
- 任何定义在函数或块中的变量,都可以认为是私有的,因为在这个函数或块的外部无法访问其中的变量
- 私有变量包括函数参数、局部变量、含税内部定义的其他函数
特权方法
- 特权方法:指能够访问函数私有变量(及私有函数)的公有方法
- 构造函数实现(其实就是使用this对象,创建实例的时候this是指向实例的,用this弄一个方法出来供实例访问私有变量)
- 静态私有变量(这个是用prototype来共享方法,以供实例访问私有变量)
function MyObject() {
// 私有变量和私有函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 公有方法 此函数是与MyObject的活动对象关联的
// 在实例上调用此方法 使privateVariable自增 且调用privateFunction函数
// 实现了 公有方法访问私有数据
this.publicMethod = function() {
privateVariable++;
return privateFunction();
};
}
// 这是一个匿名函数 且此函数表达式立刻执行
(function() {
// 私有变量和私有函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 创建对象
MyObject = function() {};
// 通过prototype共享方法
MyObject.prototype.publicMethod = function() {
privateVariable++;
return privateFunction();
};
})();
模块模式
- 在模块模式中,单例对象作为一个模块,经过初始化可以包含某些私有数据,而这些数据又可以通过其暴露的公共方法来访问
- 实质上是通过直接将公共接口分派给实例来实现共享
模块增强模式
- 适用于单例对象需要某个特定类型的实例,但又必须给它添加额外属性或方法的场景
// 此函数是立即调用并返回函数表达式的值给singleton
// 通过singlrton就可以调用公有方法 进而使用私有数据 私有方法
let singleton = function() {
// 私有变量 私有函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 创建对象
let object = new CustomType();
// 增添特权/公有属性和方法
// 定义了一个匿名函数 这是一个闭包函数 与singleton的活动对象关联
object.publicMethod = function() {
privateVariable++;
return privateFunction();
};
// 返回对象 这个就是哈数表达式的返回值
return object;
}();
期约与异步函数
同步与异步
- 同步行为对应内存中顺序执行的处理器指令
- 异步执行类似于系统中断,即当前进程外部的实体可以触发代码执行
期约(Promise)
- 期约的主要功能是为异步代码提供了清晰的抽象。
- 可以用期约表示异步执行的代码块,也可以用期约表示异步计算的值。
- 在需要串行异步代码时,期约的价值最突出
期约基础
期约实例化
- Promise(期约)是一种引用类型,可以通过new操作符来实例化
- 创建期约时需要传入执行器(executor)函数作为参数
期约状态机
- 期约时有状态的对象,可能状态如下:
- 待定(pending)
- 兑现/解决(fulfilled/resolves)
- 拒绝(rejected)
- 无论落定为哪种状态,都是不可逆的
- 期约状态切换为兑现,会产生一个私有的内部值(Value);期约的状态切换为拒绝,就会产生一个私有的内部理由(reason)
执行函数控制期约状态
- 执行器函数的职责:
- 初始化期约的异步行为
- 控制状态的最终转换
- 通过执行函数的两个参数控制状态装换
- resolve():切换状态为兑现
- reject():切换状态为拒绝
- 执行器函数是同步执行的
- 定时退出功能:
// 为避免期约卡在特定状态 可以设置定时退出功能
// 但是这玩意儿是会报错的 不好用
let p = new Promise((resolve, reject) => {
setTimeout(reject, 5000);
});
setTimeout(console.log, 0, p);
setTimeout(console.log, 6000, p);
Promise.resolve()
- 通过调用Promise.resolve()方法,可以实例化一个解决的期约
- 实际上,使用这个方法,可以把任何一个值转换为一个期约
- 解决期约的值是这个方法的第一个参数
- 如果传入值本身是一个期约,那它的行为相当于一个空包装(什么都不做,也不改变状态)
// 这两种行为是等价的
let p1 = new Promise((resolve, reject) => resolve());
let p2 = Promise.resolve();
Promise.reject()
- 会实例化一个拒绝的期约并抛出一个异步错误,这个拒绝的期约的理由就是传给Promise.reject()的第一个参数
- 拒绝期约的错误是通过浏览器异步信息队列来进行处理的
期约的实例方法
Promise.prototype.then()
- 这个方法是为期约实例添加处理程序的主要方法
- 参数:
- 最多两个,且两个都输可选的,onResolevd处理程序和onRejected处理程序,会在期约分别进入“兑现”和“拒绝”状态时执行
- 传给then()的任何非函数类型参数都会被静默忽略
- 如果只提供一个参数,另一个参数的位置上最好传入null
- 返回值:
- 一个新实例,该实例基于onResolved处理程序的返回值构建(用Promise.resolve()包装返回值并返回)
- 若onResolved处理程序没有提供,则包装上一个期约解决之后的值
- 抛出异常会返回拒绝的期约
- 返回的错误对象会被包装在一个解决期约中返回
Promise.prototype.catch()
- 该方法用于给期约添加拒绝处理程序
- 参数:一个,onRejected处理程序 7
- 返回值:新的期约实例,返回情况与Promise.prototype.then()一样
Promise.prototype.finally()
-
该方法用于给期约添加onFinally处理程序,这个处理程序在期约状态改变时(转为解决或者拒绝)执行
-
此方法在大多数情况下表现为父期约的传递
非重入期约方法
- 当期约进入落定状态时(也就是生成实例的时候),与该状态相关的处理程序(就是这个期约的代码)仅仅会被排期,而非立刻执行
- 处理程序会被推进信息队列,在当前线程上的同步代码执行完成后,处理程序才会执行
传递解决值和拒绝理由
- 在执行函数中,解决的值和拒绝的理由是分别作为resolve()和reject()的第一个参数往后传的
- 然后,这些值又会传给它们·各自的处理程序,作为onResolved和onRejected的唯一参数
拒绝期约与拒绝状态处理
- 在期约的执行程序或处理程序中抛出错误会导致拒绝,对应的错误对象会成为拒绝的理由。
- 期约可以以任何理由被拒绝,但最好使用统一的错误对象,这样可以让浏览器捕获错误对象中的栈追踪信息
- 所有错误都是异步抛出且未处理的,并不会阻止运行时继续执行同步指令
期约连锁与期约合成
期约连锁
- 一个期约接一个期约地拼接
- 让每个后续期约都等待之前的期约,也就是串行化异步任务
- 期约连锁可以构建有向非循环图的结构:
- 实例方法添加的处理程序是有向定点,图中的每个节点都会等待前一个结点落定,图的方向就是期约的解决或拒绝顺序
- 由于期约的处理程序是先添加到信息队列,然后才逐个执行,因此构成了层次遍历
- 具体事例:
let p1 = new Promise((resolve, reject) => {
console.log(`p1 executor`);
setTimeout(resolve, 1000);
});
p1.then(() => new Promise((resolve, reject) => {
console.log(`p2 executor`);
setTimeout(resolve, 1000);
}))
.then(() => new Promise((resolve, reject) => {
console.log(`p3 executor`);
setTimeout(resolve, 1000);
}))
.then(() => new Promise((resolve, reject) => {
console.log(`p4 executor`);
setTimeout(resolve, 1000);
}));
// 将生成期约的代码提取到一个工厂函数中
// 使用的时候只要修改工厂函数的名称与功能代码就可以了
function delayedResolve(str) {
return new Promise((resolve, reject) => {
console.log(str);
setTimeout(resolve, 1000);
});
}
delayedResolve(`p1 executor`)
.then(() => delayedResolve(`p2 executor`))
.then(() => delayedResolve(`p3 executor`))
.then(() => delayedResolve(`p4 executor`));
期约合成
- 将多个期约组合为一个期约
Promise.all()
- 此静态方法创建的期约会在一组期约全部解决之后再解决(就是说只有一组期约全部解决了,返回的新期约才是解决的)
- 接受一个可迭代对象,返回一个新期约
- 如果所有期约全部解决了,则合成期约的解决值就是包含期约解决值的数组,按照迭代器顺序
- 如果有期约拒绝,则第一个拒绝的期约的理由就是新合成期约拒绝的理由,之后再拒绝的期约不会影响合成期约拒绝的理由(但是拒绝期约该处理的还是会处理的,只是理由不拿出来用而已)
- 具体实例:
// Promise.all()
let p1 = Promise.all([
Promise.resolve(3),
Promise.resolve(4),
Promise.resolve(5)
]).then((values) => setTimeout(console.log, 0, values)); // [ 3, 4, 5 ]
let p2 = Promise.all([
Promise.resolve(3),
Promise.resolve(4),
Promise.reject(5)
]).then(null, (reason) => console.log(reason)); // 5
Promise.race()
- 此静态方法返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像(无论是解决还是拒绝,只要是第一个落定的期约,Promise.race()就会包装其解决值或拒绝理由并返回新期约(也是该解决的解决,该拒绝的拒绝,只是不显示出来而已)
- 接受一个可迭代对象,返回一个新期约值
- 具体示例:
// Promise.race()
let p3 = Promise.race([
Promise.resolve(3),
Promise.resolve(4),
Promise.resolve(5)
]).then((values) => setTimeout(console.log, 0, values)); // 3
let p4 = Promise.race([
Promise.reject(3),
Promise.resolve(4),
Promise.resolve(5)
]).catch((reason) => setTimeout(console.log, 0, reason)); // 3
串行期约合成
- 基于后续期约使用之前期约的返回值来串联期约是期约的基本功能(将期约合成起来,渐进地消费一个值)
- 具体示例:
function addTwo(x) { return x + 2; }
function addThree(x) { return x + 3; }
function addFive(x) { return x + 5; }
// 利用扩展操作符进行参数收集
function compose(...fns) {
// 返回一个箭头函数 箭头函数参数为x
// 利用Array.reduce()方法进行归并
// 利用期约进行参数传递
return (x) => fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(x))
}
let addTen = compose(addTwo, addThree, addFive);
addTen(8).then(console.log); // 18
异步函数
async
- async关键字用在函数声明、函数表达式、箭头函数和方法上,使普通函数具有异步特征
- 异步函数如果使用return返回了值(没有return则返回undefined),这个值会被Promise.resolve()包装成一个期约对象。异步函数始终返回期约对象
await
- 使用await可以暂停异步函数的执行,等待任务的解决。
- await会尝试“解包”对象的值,然后将这个值传给表达式,再异步恢复异步函数的执行
- await只能直接出现在异步函数的定义中(不能跟异步函数有隔阂了)
- await可以直接传递函数的返回值
- 具体用例:
async function foo() {
console.log(2);
console.log(await Promise.resolve(8));
console.log(9);
}
async function bar() {
console.log(4);
console.log(await Promise.resolve(6));
console.log(7);
}
console.log(1);
foo();
console.log(3);
bar();
// 1
// 2
// 3
// 4
// 8
// 9
// 6
// 7
异步函数策略
- 非阻塞暂停sleep
// 非阻塞暂停sleep
async function sleep(delay) {
return new Promise((resolve) => setTimeout(resolve, delay));
}
async function foo() {
const t0 = Date.now();
// 暂停执行1500ms
await sleep(1500);
console.log(Date.now() - t0);;
}
foo(); // 1509
- 如果顺序不是必须保证的,可以先一次性初始化所有的期约,然后分别等待它们的结果
BOM
- BOM提供了与网页无关的浏览器功能对象
window对象
视口位置
- 用户可以通过滚动在有限视口中查看文档
- 质量文档相对于视口滚动距离的属性有两对,返回相等的值:window.pageXoffset/window.scrollX和window,pageYoffset/window.scrollY(分别是x轴方向上的滚动距离和y轴方向上的滚动距离)
- scroll()、scrollTo()、scrollBy()
- 接受表示相对视口距离的x和y坐标
- 在前两个中表示要滚动到的坐标,在后一个表示滚动的距离(可以用来做返回顶部效果)
- 这三个方法有一个属性behavior:auto表示正常滚动,smooth表示平滑滚动
window.scrollTo({
left: 0,
top: 300,
behavior: 'smooth'
});
// 有一个问题是我在调试的时候 behavior设置为auto它就不移动了
导航与弹出窗口
- window.open()用于导航到指定的URL,也可用于打开新的浏览器窗口
- 四个参数:要加载的URL,目标窗口,特性字符串,表示新窗口在浏览器历史记录中是否替代当前加载页面的布尔值
定时器
- setTimeout():
- 用于指定在一定时间后执行某些代码
- 两个参数:要执行的代码(字符串或函数),在实行回调前等待的时间(毫秒)
- 返回值:一个表示该超时排期的数值ID
- clearTimeout(ID):根据ID来取消等待中的排期任务
- setInterval():
- 用于指定每个一段时间执行相应的代码
- 两个参数:要执行的代码(字符串或函数),把下一次执行定时代码的任务添加到队列要等待的时间(毫秒)
- 注意:这里的间隔时间指的是向队列添加新任务之前等待的时间,浏览器是不关心这个任务什么时候执行或者执行要花多长时间的,因此,执行时间短,非阻塞的回调函数比较适合setInterval
- 返回值:一个循环定时ID
- clearInterval(ID):根据ID来取消循环定时
- 循环任务也是可以通过setTimeout()来设置的,并且最好通过setTimeout()来设置。因为若使用setInterval(),一个任务结束和下一个任务开始之间的时间间隔是无法保证的
let num = 0;
let max = 10;
let incrementNumber = function() {
num++;
// 如果还没有达到最大值,再设置一个超时任务
if (num < max) {
// 但是吧 这里也是有缺陷的
// 里面的代码就得根据外面的数据来进行 挺不方便的
// 可以进行优化
setTimeout(incrementNumber, 500);
} else {
console.log(`Done`);
}
}
setTimeout(incrementNumber, 500);
系统对话框
- alert():警示框,接受一个参数
- confirm():
- 确认框
- 接收一个参数,用户提示文本
- 返回值:true表示用户点击了确认,false表示用户点击了撤销
- prompt():
- 提示框
- 接收两个参数,要提示给用户的文本,文本框的默认值
- 返回值:若用户单击OK,返回值是文本框的值;若用户单击Cancel,返回值是null
alert(`alert`);
confirm(`confirm`);
prompt(`prompt`, `prompt`);

location对象
- 提供当前窗口中加载文档的信息,以及通用的导航功能
- 它既是window属性,也是document属性
属性
location 对象属性 | 返回值 |
---|---|
location.herf | 获取或者设置 整个URL |
location.host | 返回主机(域名) |
location.port | 返回端口号 如果未写返回 空字符串 |
location.pathname | 返回路径 |
location.search | 返回参数 |
location.hash | 返回片段 #后面的内容 常见于链接 锚点 |
URLSearchParams
- URLSearchParams提供了一组标准API方法
- 通过它们可以检查和修改查询字符串
- 参数:一个查询字符串
- 返回值:一个URLSearchParams类型的实例
- 具体用例:
// 这就是查询字符串
let qs = `?q=javascript&num=10`;
// 通过new进行实例化
let searchParams = new URLSearchParams(qs);
// toString()
console.log(searchParams.toString()); // q=javascript&num=10
// has()
console.log(searchParams.has(`num`)); // true
// get()
console.log(searchParams.get(`num`)); // 10
// set()
searchParams.set(`page`, `3`); // q=javascript&num=10&page=3
console.log(searchParams.toString());
// delete()
searchParams.delete(`q`);
console.log(searchParams.toString()); // num=10&page=3
// 支持迭代
for (const param of searchParams) {
console.log(param);
// [ 'num', '10' ]
// [ 'page', '3' ]
}
location.herf
- 可用于修改浏览器地址
navigator
- navigator对象的属性通常用于确定浏览器的类型
检测插件
- plugins是navigator的一个属性
- 返回值:
- name:插件名称
- description:插件介绍
- filename:插件的文件名
- length:由当前插件处理的MIME类型数量
处理注册程序
- registerProtocolHandler()方法
- 这个方法可以把一个网站注册为处理某种特定类型信息应用程序
- 三个参数:要处理的协议,处理该协议的URL,应用名称
history对象
- go():
- 可以在用户历史记录中验任意方向导航,前进后退都可以
- 一个参数,整数,表示前进后退多少步
- back():后退一部
- forward():前进一步
DOM
- DOM(文档对象模型)是HTML和XML文档的编程接口。DOM表示有多层节点构成的文档,通过它开发者可以添加、删除和修改页面的各个部分
- DOM中总共有12种节点类型,这些类型都继承一种基本类型
节点层级
Node类型
- 所有界定啊类型都继承Node类型,因此所有类型都共享相同的基本属性和方法
节点关系
- 每个节点都有一个children属性,其中包含一个NodeList的实例
- NodeList是一个类数组对象,NodeList是实时的活动对象,DOM 结构的变化会自动地在NodeList中反映出来(直接用NodeList赋值的对象是实时的,但是Nodelist.length赋值的变量不是实时的)
- 每个节点都有一个parentNode属性,指向其DOM树中的父节点
- previousSibling和nextSibling属性用于在同胞节点间移动
- firstChild和lastChild属性分别指向父节点中第一个和最后一个节点
- hasChildNode()方法用于确认父节点是否有子节点
- ownerDocument属性指向代表整个文档的文档节点
操纵节点
- appendChild()方法,用于在childNode列表末尾添加节点(添加的节点会成为childNode中的最后一个,若添加的节点是原本的第一个节点,添加了之后就变成最后一个了)
- insertBefore()方法,两个参数,要出入的节点与参照节点
- replaceChild()方法,两个参数,要插入的节点和要替换的节点
- removeChild()方法,一个参数,要删除的节点
- cloneNode()方法,一个参数,true为深复制,默认为false,浅复制(深复制会复制节点及其整个子DOM树
- normalize()方法:检测这个节点的所有后代,删除空文本节点,合并同胞文本节点
Document类型
文档子节点与文档信息
- 文档节点
- document.documentElement指向< html >元素
- document.body指向body
- document.doctype指向< !doctype >标签
- document.title指向< title >
- document.URL包含当前页面的完整URL
- document.domain包含包含页面的域名
- document.referee包含链接到当前页面的那个页面的URL
定位元素
- getElementById()方法
- getElementsByTagName()方法:返回包含零个后多个元素的NodeList(实时更新的哦)
特殊集合
- 以下特殊集合都是HTMLCollection的实例,因此都是实时更新的
- document.anchors:带name的< a >元素
- document.forms:< form >元素
- document.images:< img >元素
- document.links:带href的< a >元素
<a href="javascript:;" name="aaa"></a>
<script>
// 文档中所有a标签
let as = document.anchors;
let length = document.anchors.length;
console.log(as, length); // HTMLCollection [a, aaa: a
// 复制并添加a标签在文档中
let a2 = document.querySelectorAll(`a`);
let a3 = a2[0].cloneNode();
let script = document.querySelector(`script`);
script.appendChild(a3);
console.log(as, length); // HTMLCollection(2) [a, a, aaa: a] 1
</script>
<a href="javascript:;" name="aaa"></a>
<script>
console.log(as, length); // HTMLCollection(3) [a, a, a, aaa: a] 1
// 确实会实时更新的 不必手动再获取一次 就连被赋值的变量都是如此
// 直接被HTMLCollection赋值的变量是会实时更新的
// 但是HTMLCollection的长度赋值的变量并不会实时更新
</script>
文档写入
- document.write():接受一个字符串写入到文档中
- document.eriteln():接受一个字符串写入到文档中,并在字符串末尾追加一个换行符
- document.open():打开网页输出流
- document.close():关闭网页输出流
Element类型
取得属性与设置属性
- getAttribute():主要用于取得自定义属性的值
- setAttribute():两个参数,属性名与属性值
- attributes属性:实时集合,保存节点的属性(键值对形式)
- removeAttribute():删除指定名字的属性
创建元素
- document.createElement()方法:一个参数,标签名,创建该标签名的元素
Text类型
操作文本
- appendData(text):向节点末尾添加文本text
- deleteData(offset,count):从位置offset开始删除count个字符
- insertData(offset,text):在位置offset插入text
- replaceData(offset,count,text)
- spliceText(offset):在位置offset拆分文本节点
- substringData(offset,count):提取文本
<script>
const div = document.createElement(`div`);
const node1 = document.createTextNode(`hello `);
const node2 = document.createTextNode(`world!`);
const body = document.body;
div.appendChild(node1);
div.appendChild(node2);
body.appendChild(div);
// hello world!
</script>
Comment类型
- 注释
DOM编程
动态脚本
<div>hello world!</div>
<script>
// 传两个参数没什么意思的 因为script标签是插入到body的最后面才是最好的
// 或者插入到head中 然后在script代码中使用onload就好了
function loadScript(url, element) {
let script = document.createElement(`script`);
script.src = url;
element.appendChild(script);
}
loadScript('./67-动态脚本.js', document.body);
</script>
MutationObserver接口
- 使用MutationObserver可以观察整个文档、DOM树的一部分、或某个元素,此外还可以观察元素属性、子节点、文本,或者三者任意组合变化
基本用法
-
调用MutationObserver构造函数并传入一个回调函数来创建
-
Observer()方法,两个参数,要观察变化的DOM节点,一个MutationObserver对象(MutationObserver对象是一个键值对形式配置选项的字典,用于控制观察哪些方面的变化)
-
每个回调都会收到一个MUtationObserver实例的数组,表示哪里发生了变化,发生了什么变化
-
MutationObserver实例的属性:
| target | 被修改影响的节点 |
| — | — |
| type | 字符串,变化的类型 |
| oldValue | 变化之前的值 |
| attributeName | 被修改属性的名字 |
| attributeNamespace | 被修改属性的名字 |
| addedNodes | 变化中添加的节点,默认为空NodeList |
| removeNodes | 变化中删除的节点,默认为空NodeList |
| previousSibling | 变化节点的前一个同胞节点 |
| nextSibling | 变化节点的后一个同胞节点 | -
disconnect()方法:提前终止执行回调,并且也会抛弃已经加入任务队列要异步执行的回调
-
多次调用observer()方法,可以复用一个MutationObserver对象观察多个不同的目标节点
MutationObserverInit与观察范围
-
MutationObserverInit对象用于控制对目标节点的观察范围,观察者可以观察的事件包括属性变化,文本变化和子节点变化
-
MutationserverInit对象的属性:
| subtree | 布尔值,是否观察目标节点的子树 |
| — | — |
| attributes | 布尔值,是否观察目标节点的属性变化 |
| atributeFilter | 字符串数组,要观察哪些属性变化 |
| attributeOldValue | 布尔值,是否记录变化之前的属性值 |
| charactertData | 布尔值,修改字符数据是否触发变化事件 |
| characterDataOldValue | 布尔值,是否记录变化之前的字符数据 |
| childList | 布尔值,修改目标节点的子节点是否触发变化事件 | -
具体用例:
<body>
<div>
<p>MutationObserver</p>
<p>MutationObserver</p>
</div>
<script>
// 实例化MutationObserver对象
let observer = new MutationObserver((mutationRecords) => {
console.log(mutationRecords);
})
const div = document.querySelector(`div`);
const p = document.querySelectorAll(`p`)[0];
const text = p.firstChild;
observer.observe(div, {
attributes: true,
childList: true
});
observer.observe(text, {
characterData: true
});
// 添加属性
div.setAttribute(`foo`, `bar`);
// 增添节点
let p1 = p.cloneNode(true);
div.appendChild(p1);
// 改变文本
text.textContent = `delete`;
// (3) [MutationRecord, MutationRecord, MutationRecord]
</script>
</body>
异步回调与记录队列
- 其核心是异步回调与记录队列模型
- takeRecords()方法可以清空队列,取出并返回其中的所有的MutationObserver实例
性能、内存与垃圾回收
- MutationObserver实例对于目标节点之间的引用关系是非对称的。MutationObserver用于对要观察节点的弱引用,但是目标节点拥有对MutationObserver的强引用(节点没了MutationObserver没了,MutationObserver还在不阻止垃圾回收程序回收节点)
DOM扩展
Selectors API
- querySelector()方法
- querySelectorAll()方法
- match()方法,接收一个CSS选择符参数,如果元素匹配则返回true
元素遍历
let parentElement = document.getElementById(`parent`);
// firstElementChild 指向第一个element类型的子元素
let currentChildElement = document.parentElement.firstElementChild;
// 若没有子元素 firstElementChild返回null 退出循环
while (currentChildElement) {
// 这就是元素节点 做相应处理
processChild(currentChildElement);
if (currentChildElement === parentElement.lastElementChild) {
break;
} else {
currentChildElement = currentChildElement.nextElementSibling;
}
}
HTML5
- getElementsByClassName():返回值是NodeList
- classList属性:
- add(value)方法
- contains(value)方法:表明指定value是否存在
- remove(value)方法:
- toggle(value)方法:如果类名列表中已经存在指定value,则删除;若不存在,则添加(这个属性可以挺方便地制造出切换效果)
焦点管理
- document.activeElement:始终包含当前拥有焦点的DOM元素
- document.hasFocus()方法:表示文档是否用于焦点
HTMLDocument扩展
readyState属性
- 可以用于判断文档是否加载完毕
- 值:
- loading:文档正在加载
- complete:文档加载完成
自定义数据属性
- 自定义属性要前缀data-
- 元素的dataset属性可用于访问属性
插入标记
- innerHTML
<body>
<div class="content">
<p>today is a beautiful day</p>
<ul>
<li>play a game</li>
<li>have a sleep</li>
<li>make a study</li>
</ul>
<button>插入</button>
</div>
<script>
const content = document.querySelector(`.content`);
const btn = document.querySelector(`button`);
btn.addEventListener('click', () => {
content.innerHTML = `<p>tomorrow is a beautiful day<p>`;
})
</script>
</body>

- outerHTML:与innerHTML类似,不同点是outerHTML会连自己一起展示出来,改变的时候自己也会改变
- insertAdjacentHTML() 和 insertAdjacentText()
- 两个参数:要插入的标记位置和要插入的HTML或文本
- 第一个参数的选择:
- beforebegin
- afterbegin
- beforeend
- afterend
- 内存与性能问题:
- 如果被移除的子树元素中之前有关联的事件处理程序或其他JavaScript对象(作为元素属性),那它们之间的绑定关系会滞留在内存中
- 因此,最好手动删除要被替换的元素上关联的事件处理程序和JavaScript对象
- 在循环使用innerHtml或者outerHTML的时候,最好不要在循环中替换元素,而是循环创建好要使用的HTML,再一次性插入到文本中,像这样:
<body>
<ul></ul>
<script>
const ul = document.querySelector(`ul`);
const values = [1, 2, 3, 4, 5];
let itemsHtml = ``;
for (const value of values) {
itemsHtml += `<li>${value}</li>`;
}
ul.innerHTML = itemsHtml;
</script>
</body>
scrollIntoView()
- 存在于所有HTML元素上
- 可以滚动浏览器窗口或容器元素以便包含元素进入视口
- 参数如下:
- alignToTop:布尔值。true表示窗口滚动后元素的顶部与视口顶部对齐,false表示窗口滚动后元素的底部与视口底部对齐
- scrollIntoViewOptions是一个选项对象
- behavior:定义过渡动画,可取值smooth、auto
- block:定义垂直方向的对齐,可取值start、center、end、nearest
- inline:定义水平方向的对齐,可取值start、center、end、nearest
- 这个方法可用于在页面上发生某个事件时引起用户的关注
专有扩展
- children:只包含Element类型的子节点
DOM2和DOM3
Node的变化
- isSameNode()和isEqualNode()
- isSameNode()是相同节点;isEqualNode是相等节点
- 相同是引用同一个对象,相等是类型、属性,子树等全部相等
- setUserData()用于给节点追加数据
- 三个参数,键、值、处理函数
- 处理函数会在包含数据的节点被复制、删除、重命名或导入其他文档的时候执行,可以在这个时候呀决定如何处理用户数据
- 处理函数接收五个数据:
- 表示操作类型的数值(1:复制,2:导入,3:删除)
- 数据的键
- 数据的值
- 源节点
- 目标节点
元素尺寸
- 偏移尺寸:包含边框

- 客户端尺寸:不包含边框

- 滚动尺寸:不包含边框

- 元素尺寸:

遍历
NodeIterator
- 从某个起点开始执行对DOM结构的深度优先遍历(从左到右,从上到下)
- 四个参数:
- root:作为遍历根节点的节点
- whatToShow:数值代码,表示应该访问哪些节点
- filter:NodeFilter对象或函数,表示应该是否接收或跳过特定节点
- entityReferenceExpansion:布尔值
- NodeIterator方法:
- nextNode()
- previousNode()
<body>
<div class="div1">
<p><b>Hello</b> world!</p>
<ul>
<li>List item 1</li>
<li>List item 2</li>
<li>List item 3</li>
</ul>
</div>
<script>
const div = document.querySelector(`div`);
// 设置过滤器 仅过滤li标签1
const filter = function(node) {
return node.tagName.toLowerCase() == `li` ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_SKIP;
};
// 创建实例 指定数值代码为SHOW_ELEMENT 过滤器filter
const iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, filter, false);
let node = iterator.nextNode();
while (node != null) {
// 打印标签名
console.log(node.tagName);
node = iterator.nextNode();
}
</script>
</body>
TreeWalker
- TreeWalker方法:
- nextNode()
- previousNode()
- parentNode()
- firstNode()
- lastNode()
- nextSibling()
- previousSibling()
- 参数与NodeIterator一样
- TreeWalker可以在DOM数中任意游走
范围
- 使用范围可以非常细腻地对DOM树进行操作
- 范围的主要价值在于它可以在不破坏文档结构的情况下添加、修改、删除内容
- 范围可用于设置高亮
<body>
<!-- 如果是没有这么准确地包含着要高亮关键词的标签
可以使用正则表达式 将所有内容遍历一次 筛选出关键词
将每一个关键词赋给变量 添加到范围中
使用surroundContents将高亮span插入就好了
至于如何遍历内容 可以用NodeIterator从根节点开始遍历 稍后自己试一试 -->
<p id="p1"><b>hello</b>world!</p>
<script>
let p1 = document.getElementById(`p1`),
helloNode = p1.firstChild.firstChild,
worldNode = p1.lastChild,
// 实例化一个范围
range = document.createRange();
// 使range包含helloNode
range.selectNode(helloNode);
const span = document.createElement(`span`);
span.style.backgroundColor = `yellow`;
// 提取出范围的内容
// 在原始文档中范围之前所在的位置插入给定的节点
// 将范围对应文档片段的内容添加给定节点
range.surroundContents(span);
console.log(p1.innerHTML);
// world!
</script>
</body>

事件
- JavaScript和HTML的交互是通过事件实现的。
- 事件代表文档或浏览器窗口中某个有意义的时刻,可以使用仅在事件发生时执行的监听器(处理程序)订阅事件
事件流
- 当你点击一个按钮时,你不仅点击了这个按钮,还点击了它的容器以及整个页面
- 事件流描述了页面接收事件的顺序
- 事件冒泡:
- 事件被定义为从最具体的元素(文档树中最深的节点)开始触发,然后向上传播至没有那么具体的元素(文档)
- 现代浏览器中的事件会一直冒泡到window对象
- 事件捕获:
- 最不具体的节点最先收到事件,最具体的节点在最后收到事件
- 所有的浏览器都是从window对象开始捕获事件
- 事件捕获是为了在事件到达最终目标前拦截事件
- DOM事件流:
- 事件流的三个阶段:事件捕获、到达目标、事件冒泡
- 事件捕获最先发生,为提前拦截事件提供了可能
- 最后一个阶段是冒泡,最迟要在这个阶段响应事件
- 现代浏览器会在捕获阶段在目标事件上触发事件,最终结果是事件目标上有两个机会来处理事件
事件处理程序
- 为响应事件而调用的函数被称为事件处理函数
- 事件处理函数的名称以on开头
DOM2
- DOM0和DOM2事件处理程序都是在冒泡阶段调用事件处理程序
- DOM2中对同一个目标多个事件处理程序以添加顺序来触发
- addEventListener()
- removeEventListener()
- 注意:想要移除事件,必须知道事件处理程序的名字(即函数名),因此,使用匿名函数注册的事件处理程序是无法移除的
事件对象
- 在DOM中发生事件时,所有相关信息会被收集并存储在一个名为event的对象中
- event对象是传给事件处理函数的唯一参数
- bubbles
- cancelable
- currentTarget
- defaultPrevented
- datail
- eventPhase
- preventDefault()
- stopImmediatePropagation()
- stopPropagation()
- target
- trusted
- type
- View
<body>
<button>click</button>
<script>
const btn = document.querySelector(`button`);
btn.onclick = function() {
console.log(window.event);
};
btn.addEventListener(`click`, (event) => {
console.log(event.preventDefault);
console.log(event.stopImmediatePropagation);
console.log(event.stopPropagation);
});
</script>
</body>
事件类型
用户界面事件
- load
- unload
焦点事件
- focus
- blur
鼠标和滚轮事件
- click
- dblclick:双击主键
- mousedown
- mouseup
- mouseenter:鼠标光标从元素外部转移到元素内部
- mouseleave
- mousemove
- mouseout:鼠标光标从元素外部转移到元素内部时触发
- mouseover:鼠标光标从元素内部转移到元素外部
客户端坐标
- clientY
- clientX
页面坐标
- pageX
- pageY
屏幕坐标
- screenX
- screenY
修饰键
- DOM规定了4个属性来表示这几个修饰键的状态:shiftKey、ctrlKey、altKey、metaKey
- 在各自的修饰键按下时包含布尔值true
相关元素
- event对象的realtedTarget属性提供相关元素的信息
- 这个属性只有在mouseover和mouseout事件发生时才包含值
额外事件信息
- event对象的detail属性,用以给出关于事件的更多信息
键盘与键盘输入事件
- keydown:按下键盘上的某个键
- textInput:按下键盘上的某个键并产生字符时触发
- keyup,释放键盘上的某个键时触发
键码
- event的keyCode属性中的一个键码,对应键盘上特定的一个键
HTML5事件
- contextmenu事件:用于表示何时该显示上下文菜单,从而允许开发者取消默认的上下文菜单并提供自定义菜单
- beforeunload事件:会在window上触发,用意是给开发者提供阻止页面被卸载的机会
<body>
<style>
#myMenu {
position: absolute;
visibility: hidden;
border: 1px solid rgba(0, 0, 0, .3);
}
#myMenu li {
list-style: none;
}
</style>
<div id="myDiv">right click or ctrl+click me to get a custom context menu.</div>
<ul id="myMenu">
<li>
<a href="https://www.baidu.com/">百度</a>
</li>
<li>
<a href="https://www.bilibili.com/">b站</a>
</li>
<li>
<a href="https://juejin.cn/">掘金</a>
</li>
<li>
<a href="https://developer.mozilla.org/zh-CN/">mdn</a>
</li>
</ul>
<script>
window.addEventListener(`load`, (event) => {
const div = document.querySelector(`#myDiv`);
// 给指定元素注册contextmenu事件 这是本来的菜单事件
div.addEventListener(`contextmenu`, (event) => {
// 取消默认的菜单事件冒泡
event.preventDefault();
// 显示自定义的菜单
const menu = document.querySelector(`#myMenu`);
// 使菜单跟随鼠标
menu.style.left = `${event.clientX}px`;
menu.style.top = `${event.clientY}px`;
menu.style.visibility = `visible`;
});
// 给文档注册点击事件 使左键单击时自定义菜单消失
document.addEventListener(`click`, (event) => {
document.querySelector(`#myMenu`).style.visibility = `hidden`;
});
});
</script>
</body>
内存与性能
- 页面中事件处理程序的数量与页面的整体性能直接相关。
- 每个函数都是对象,都占用内存空间,对象越多,性能越差
- 为指定事件处理程序所需访问DOM的次数会先期造成整个页面交互的延迟
事件委托
- 使用一个事件处理程序来管理一种类型的事件
- 使用事件委托,只要给所有元素的共同祖先节点添加一个事件处理程序,就可以解决问题
- 优点如下:
- document对象随时可用
- 节省花在设置页面处理程序上的时间
- 减少页面所需的内存
- 最适合事件委托的事件包括:click、mousedown、mouseup、keydown、keypress
删除事件处理程序
- 如果知道某个元素会被删除,那么最好在删除它之前手动删除它的事件处理程序
更多推荐
所有评论(0)