变量、作用域与内存
javascript变量是松散类型的,而且变量不过就是特定时间点一个特定值的名称而已。
ecma-262规定,任何实现内部[[Call]]方法的对象都应该在typeof检测时返回“function”
执行上下文与作用域
执行环境又叫执行上下文(下面简称上下文)。变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上。无法通过代码访问变量对象,但后台处理数据会用到它。
全局上下文是最外层的上下文。根据ECMAScript实现的宿主环境,表示全局上下文的对象可能不一样。浏览器中,全局上下文就是window对象,因此所有通过var定义的全局变量和函数都会成为window对象的属性和方法。使用let和 const的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)。
每个函数调用都有自己的上下文。当代码执行流进入函数时,函数上下文被推到一个上下文栈上。在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。ECMAScript程序的执行流就是通过这个上下文栈进行控制的。
上下文中的代码在执行的时候,会创建变量对象的一个作用域链。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域链的最前端。如果上下文是函数,则其活动对象用作变量对象。活动对象最初只有一个定义变量:arguments。(全局上下文中没有这个变量)。作用域链中的下一个变量对象来自包含上下文,再下一个对象来自再下一个包含上下文。依次类推直至全局上下文;全局上下文的变量对象始终是作用域链的最后一个变量对象。
代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。搜索过程始终从作用域链的最前端开始,然后逐级往后,直到找到标识符。
作用域链中的对象也有一个原型链,因此搜索可能涉及每个对象的原型链
局部作用域中定义的变量可用于在局部上下文中替换全局变量。
函数参数被认为是当前上下文中的变量。
作用域链增强
执行上下文主要包括全局上下文和函数上下文。(eval() 调用内部存在第三种上下文)。
某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除。
- try/catch语句的catch块
- with语句
对with语句来说,会向作用域链前端添加指定的对象;对catch语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明。
变量声明
使用var的函数作用域声明
使用var声明的变量会自动提升到函数作用域顶部,而由于var声明的变量会被提升,javascript引擎会自动将多余的声明在作用域顶部合并为一个声明。
在全局作用域中使用var声明的变量会被添加成为window对象的属性。
在使用var声明变量时,变量会被自动添加到最接近的上下文。
如果变量未经声明就被初始化了,那么他就会自动被添加到全局上下文
使用let的块级作用域声明
let声明的范围是块作用域,而var声明的范围是函数作用域。
暂时性死区:let声明的变量不会在作用域中被提升
let在同一个作用域内不能声明两次,重复的var声明会被忽略,而重复的let声明会抛出SyntaxError
在使用let声明迭代变量时,javascript引擎在后台会为每个迭代循环声明一个新的迭代变量。每个setTimeout引用的都是不同的变量实例。
for(let i = 0; i < 6; i++) {
// 下面的形式无论let 还是 var 输出都是 0 1 2 3 4 5
setTimeout(console.log, 0, i)
// 此时如果是var声明i,则输出是 6 6 6 6 6 6
setTimeout(() => console.log(i), 0)
}
使用const的常量声明
使用const声明的变量必须同时初始化为某个值。一经声明,在其声明周期的任何时候都不能再重新赋予新值。
由于const声明暗示变量的值是单一类型且不可修改,javascript运行时编译器可以将其所有实例都替换成实际的值,而不会通过查询表进行变量查找。谷歌的V8引擎就执行这种优化。
垃圾回收
JavaScript是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。基本思路:确定哪个变量不会再使用,然后释放它占用的内存。浏览器发展史上,用到过两种主要的标记策略:标记清理和引用计数。
标记清理
JavaScript最常用的垃圾回收策略是标记清理
垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并回收它们的内存。
引用计数
思路是对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为 1。如果同一个值又被赋给另个一变量,那么引用数加 1。类似的,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。当一个值的引用数为 0 时,就说明没办法访问到这个值了,垃圾回收程序下次运行的时候就会释放引用数为0的值的内存。但引用计数有一个严重的问题:循环引用;对象A有一个指针指向B,同样对象B有一个指针指向对象A
内存泄露
意外声明全局变量是最常见也最容易修复的内存泄露问题
function setName() {
name = 'jack'
}
定时器也可能会悄悄导致内存泄露,下面代码中,定时器的回调通过闭包引用了外部变量
let name = 'jack'
setTimeout(() => {
console.log(name)
}, 100)
使用闭包很容易在不知不觉中造成内存泄露
const outer = function () {
let name = 'jack'
return function () {
return name
}
}
以上代码创建了一个内部闭包,只要outer函数存在就不能清理name,因为闭包一直引用着它。
静态分配与对象池
开发者无法直接控制什么时候开始收集垃圾,但可以间接控制触发垃圾回收的条件。浏览器决定何时运行垃圾回收程序的一个标准就是对象更替的速度。如果有很多对象被初始化,然后一下子又都超出了作用域,那么浏览器就会采用更激进的方式调度垃圾回收程序运行,从而影响性能。
解决策略:对象池
基本引用类型
Data
RegExp
原始值包装类型
Boolean
Number
String
单例内置对象
任何由ECMAScript实现提供、与宿主环境无关,并在ECMAScript程序开始执行时就存在的对象
Global
isNaN()、isFinine()、parseInt()、和parseFloat(),都是Global对象的方法。出了这些Global还有另外一些方法
URL编码方法
encodeURI() 和 encodeURIComponent() 方法用于编码统一资源标识符(URI)
encodeURI() 不会编码属于URL组件的特殊字符,如冒号、斜杠、问号、井号
encodeURIComponent() 会编码它发现的所有非标准字符
let uri = "http://www.wrox.com/illegal value.js#start";
// "http://www.wrox.com/illegal%20value.js#start"
console.log(encodeURI(uri));
// "http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.js%23start"
console.log(encodeURIComponent(uri));
eval() 方法
这个方法是一个完整的ECMAScript解释器,它接收一个参数,即一个要执行的JavaScript字符串
Math
集合引用类型
Object
ECMA-262将对象定义为一组属性的无序集合;一组数据和功能的集合
使用构造函数创建和使用对象字面量
Array
ECMAScript数组是一组有序的数据
与对象一样,在使用数组字面量表示法创建数组不会调用Array构造函数
Array.from() 将类数组结构转换为数组实例
Array.of() 将一组参数转化为数组实例
keys() values() entries() 迭代器方法
concat()
let colors = ["red", "green", "blue"];
let colors2 = colors.concat("yellow", ["black", "brown"]);
console.log(colors); // ["red", "green","blue"]
console.log(colors2); // ["red", "green", "blue", "yellow", "black", "brown"]
slice()
let colors = ["red", "green", "blue", "yellow", "purple"];
let colors2 = colors.slice(1);
let colors3 = colors.slice(1, 4);
alert(colors2); // green,blue,yellow,purple
alert(colors3); // green,blue,yellow
splice()
let colors = ["red", "green", "blue"];
let removed = colors.splice(0,1); // 删除第一项
alert(colors); // green,blue
alert(removed); // red,只有一个元素的数组
removed = colors.splice(1, 0, "yellow", "orange"); // 在位置 1 插入两个元素
alert(colors); // green,yellow,orange,blue
alert(removed); // 空数组
removed = colors.splice(1, 1, "red", "purple"); // 插入两个值,删除一个元素
alert(colors); // green,red,purple,orange,blue
alert(removed);
indexOf() lastIndexOf() includes() find() findIndex() every() some() filter() map() reduce() forEach()
定型数组
目的是提升向原生库传输数据的效率。
ArrayBuffer 对固定长度的连续内存的引用
Map
has()
get()
set()
delete()
clear()
size
WeakMap
WeakMap实例不会妨碍垃圾回收
Set
has()
add()
delete()
WeakSet
WeakSet实例中的元素不会妨碍垃圾回收
迭代器与生成器
理解迭代
迭代就是按顺序反复多次执行一段程序,通常会有明确的终止条件。如果一个数据结构不提供迭代方法,那么循环该数据的的时候需要熟悉数据的使用方法,还有就是循环的顺序不是数据固有的
迭代器模式
迭代器模式描述了一个方案,即可以把有些结构称为"可迭代对象"(iterable),因为它们实现了正式的Iterable接口,而且可以通过迭代器Iterator消费。可迭代对象是一种抽象说法,可以理解成数组或集合这样的集合类型的对象。它们元素有限,且具有无歧义的遍历顺序。
let obj = {
from: 1,
to: 5,
[Symbol.iterator]() {
return {
current: this.from,
last: this.to,
next() {
if(this.current <= this.last) {
return {value: this.current++, done: false}
} else {
return {done: true}
}
}
}
}
}
任何实现Iterable接口的数据结构都可以被实现Iterator接口的结构“消费”。迭代器是按需创建的一次性对象。每个迭代器都会关联一个可迭代对象,而迭代器会暴露迭代其关联可迭代对象的API。上面代码obj对象即是一个可迭代对象,它实现了Iterable接口,返回的迭代器是[Symbol.iterator]工厂函数返回的对象。
可迭代协议
实现Iterable接口(可迭代协议)要求同时具备两种能力:支持迭代的自我识别能力和创建实现Iterator接口的对象的能力。必须有一个默认迭代器属性,即以[Symbol.iterator]作为键的迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。
很多内置类型都实现了Iterable接口
- 字符串
- 数组
- 集合
- arguments对象
- NodeList等Dom集合类型
不需要显式的调用工厂函数来生成迭代器。实现可迭代协议的所有类型都会自动兼容接收可迭代对象的任何语言特性。接收可迭代对象的原生语言特性包括:
- for-of 循环
- 数组解构 spread
- 扩展操作符号 rest
- Array.from()
- Yield * 操作符,在生成器中使用
迭代器协议
迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器API使用next() 方法在可迭代对象中遍历数据。每次成功调用next() ,都会返回一个IteratorResult对象。该对象包含两个属性:done和value
迭代器并不与可迭代对象某个时刻的快照绑定,而仅仅是使用游标来记录遍历可迭代对象的历程。如果可迭代对象在迭代期间被修改了,那么迭代器也会反应相应的变化。
迭代器维护者一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可迭代对象
总结:实现了可迭代接口(Iterable)的对象在使用的时候调用默认的迭代器工厂函数会返回一个实现了迭代器接口(Iterator)的迭代器对象
自定义迭代器
任何实现Iterator接口的对象都可以作为迭代器使用
class Test {
next() {}
[Symbol.itertor](){
return this
}
}
提前终止迭代
可选的return() 方法用于指定在迭代器提前关闭时执行的逻辑。
for-of循环通过break、continue、return或throw提前退出
如果迭代器没有关闭,还可以继续从上次离开的地方继续迭代。比如数组的迭代器就是不能关闭的。
生成器
生成器是ECMAScript6 新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的能力。可以与Iterable配合使用,能轻松的创建数据流。
生成器是一种特殊的函数,调用之后会生成一个生成器对象。生成器对象实现了Iterable接口,因此可以用在任何消费可迭代对象的地方。
生成器基础
箭头函数不能用来定义生成器函数
调用生成器函数会产生一个生成器对象。生成器对象一开始处于暂停执行(suspended)状态。与迭代器相似,生成器对象也实现了Iterable接口和Iterator接口,因此具有next()方法。调用这个方法会让生成器开始或恢复执行。只有第一次调用next()方法的时候生成器才会开始执行。
临时性可迭代对象可以实现为生成器
通过yield中断执行
yield关键字可以让生成器停止和开始执行,是生成器最有用的地方。生成器在遇到yield关键字之前会正常执行。遇到这个关键字后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用next()方法来恢复执行。通过yield关键字退出的生成器函数会处在{done: false}的状态;通过return退出生成器函数的是{done: true}
生成器对象作为可迭代对象
function * nTime(n) {
while(n-- > 0) {
yield
}
}
for (const iterator of nTime(-3)) {
console.log(6789);
}
使用yield实现输入输出
function * generatorFn(){
return yield 'bar'
}
let g = generatorFn()
g.next() // bar
g.next('foo') // foo
function* gen() {
let ask1 = yield "2 + 2 = ?";
alert(ask1); // 4
let ask2 = yield "3 * 3 = ?"
alert(ask2); // 9
}
let generator = gen();
alert( generator.next().value ); // "2 + 2 = ?"
alert( generator.next(4).value ); // "3 * 3 = ?"
alert( generator.next(9).done );
产生可迭代对象
function* generatorFn() {
yield * [1,2]
yield * [3,4]
yield * [5,6]
}
// 等同于下面的
function* generatorFn2() {
for (let i of [1,2]) {
yield i
}
for (let i of [3,4]) {
yield i
}
for (let i of [5,6]) {
yield i
}
}
yield*实际上只是将一个可迭代对象序列化为一连串可以单独产出的值,所以这跟把yield放到一个循环里没有什么不同。
使用yield*实现递归算法
暂时不做讨论
生成器作为默认迭代器
可以通过提供一个generator函数作为Symbol.Iterator,来使用generator进行迭代
class Foo {
constructor() {
this.value = [1,2,3,5,6]
}
*[Symbol.iterator]() {
yield * this.value
}
}
for (const iterator of new Foo()) {
console.log(iterator);
}
let range = {
from: 1,
to: 5,
*[Symbol.iterator]() { // [Symbol.iterator]: function*() 的简写形式
for(let value = this.from; value <= this.to; value++) {
yield value;
}
}
};
alert( [...range] ); // 1,2,3,4,5
提前终止生成器
一个实现Iterator接口的对象一定有next() 方法,还有一个可选的return() 方法用于提前终止迭代器。生成器对象除了有这两个方法,还有第三个方法:throw()
function* generatorFn() {}
const g = generatorFn()
g.next()
g.return()
g.throw()
对象、类与面向对象编程
ECMA-262将对象定义为一组属性的无序集合;一组数据和功能的集合
Tips: 浏览器环境中的BOM和DOM对象,都是由宿主环境定义和提供的宿主对象,宿主对象不受ECMA-262约束,所以它们可能会也可能不会集成Object
理解对象
属性的类型
-
数据属性
- [[configurable]] 是否可以通过delete删除、修改特性、以及是否可以修改为访问器属性,默认情况下,所有直接定义在对象上的属性这个特性都为true
- [[enumberable]] 是否可以通过for-in循环返回,默认情况下,所有直接定义在对象上的属性这个特性都为true
- [[writable]] 表示属性的值是否可以被修改,默认情况下,所有直接定义在对象上的属性这个特性都为true
- [[value]] 包含属性实际的值,默认为undefined
要改变默认特性,通过Object.defineProperty() 方法
let person = {}
Object.defineProperty(person, 'name', {
//
}
-
访问器属性
- [[configurable]] 是否可以通过delete删除、修改特性、以及是否可以修改为数据属性,默认情况下,所有直接定义在对象上的属性这个特性都为true
- [[enumberable]] 是否可以通过for-in循环返回,默认情况下,所有直接定义在对象上的属性这个特性都为true
- [[get]] 获取函数
- [[set]] 设置函数
let person = {
year_: 2022,
edition: 1
}
Object.defineProperty(person, 'year_', {
get() {
return this.year_
},
set(newVal) {
if (newValue > 2022) {
this.year_ = newVal
this.edition += newVal - 2022
}
}
})
定义多个属性
Object.defineProperties() 方法
let book = {}
Object.defineProperties(book, {
year_: {
value: 2022
},
year: {
get(){}
set(value){}
}
}
读取属性的特性
Object.getOwnPropertyDescriptor() 方法可以获得指定属性的属性描述符
let book = {
year: 2022
}
let descriptor = Object.getOwnPropertyDescriptor(book, 'year')
descriptor.value // 2022
合并对象
Object.assign() 方法,对每个对象源执行的是浅复制。
对象标识及相等判定
Object.is() 方法
Object.is(NaN, NaN) // true
// 递归的判断多个值是否相等
function recursivelyCheckEqual(x, ...rest){
return Object.is(x, rest[0]) && (rest.length < 2 || recursivelyCheckEqual(...rest)
}
创建对象
工厂模式
工厂模式就是一个简单的函数,这个函数可以创建对象,为它添加属性和方法,然后返回这个对象。
function createPerson(name, age) {
let o = new Object()
o.name = name
o.age = age
o.sayName = function () {
return this.name
}
return o
}
let p = createPerson('jack', 22)
构造函数模式
function Persion(name, age){
this.name = name
this.age = age
this.sayName = function () {
return this.name
}
}
let p = new Person('jack', 22)
构造函数也是函数
function Test() {
this.test = 'fuck6789'
}
let book = {
fuck() {
(function (){
Test()
})()
}
}
book.fuck()
console.log(global.test); // 'fuck6789'
如果没有使用new操作符调用一个构造函数,如Test(), 结果会将属性和方法添加到window对象。在调用一个函数而没有明确设置this值的情况下(即没有作为对象的方法调用,或者没有使用call()/apply() 调用),this始终指向Global对象(在浏览器中就是window对象)
构造函数的问题
定义的方法会在每个实例上都创建一遍。
每个实例都会重新创建一遍新方法
原型模式
每个函数都会创建一个prototype属性,这个属性是一个对象,这个对象就是通过调用构造函数创建的对象的原型,使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。
正常的原型链都会终止于Object的原型对象
function Person(){}
/**
* 正常的原型链都会终止于 Object 的原型对象
* Object 原型的原型是 null
*/
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Person.prototype.__proto__.constructor === Object); // true
console.log(Person.prototype.__proto__.__proto__ === null); // true
console.log(Person.prototype.__proto__);
// {
// constructor: f Object(),
// toString: ...
// hasOwnProperty: ...
// isPrototypeOf: ...
// ...
// }
可以通过Object.create() 方法来创建一个新对象,同时为其指定原型
原型层级
通过对象访问属性时,搜索开始于对象实例本身,如果没找到,搜索会沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应的值。
hasOwnProperty() 方法用于确定某个属性是在实例上还是在原型上。
原型和in操作符
in 操作符会在可以通过对象访问属性时返回true,无论该属性是在实例上还是原型上。
属性枚举顺序
for-in循环和Object.keys() 的枚举顺序是不确定的,取决于JavaScript引擎
对象迭代
ECMAScript2017新增两个对象迭代方法
- Object.values()
- Object.entries()
重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型
继承
原型链
ECMA-262把原型链定义为ECMAScript的主要继承方式
每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针([[prototype]])指向原型。如果原型是另一个类型的实例,则意味着这个原型本身有一个内部指针([[prototype]])指向另一个原型,则相应的另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。
原型链的问题
在使用原型链实现继承时,原型实际上变成另一个类型的实例。这意味着原先的实例摇身一变成为了原型属性。
盗用构造函数
function SuperType (){
this.colors = ['red', 'blue', 'green']
}
function SubType(){
SuperType.call(this)
}
let sub = new SubType()
console.log(sub.colors); // ['red', 'blue', 'green']
缺点
必须在构造函数中定义方法,函数不能重用。子类也不能访问父类原型上定义的方法。因此盗用构造函数基本上不能单独使用
组合继承
综合了原型链和盗用构造函数
function SuperType (name){
this.name = name
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
console.log(this.name);
}
function SubType(){
SuperType.call(this, 'Jack')
}
SubType.prototype = new SuperType()
let sub = new SubType()
sub.sayName()
类
类是ECMAScript中新的基础性语法糖结构,实际上背后使用的仍然是原型和构造函数的概念
类定义
函数声明可以提升,类定义不能
函数受函数作用域限制,类受块作用域限制
{
function FunctionDeclaration() {}
class ClassDeclaration {}
}
console.log(FunctionDeclaration); // FunctionDeclaration() {}
console.log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined
类构成
类包含:构造函数方法、实例方法、获取函数、设置函数和静态类方法
类构造函数
实例化
使用new操作符实例化类的操作等于使用new调用其构造函数。
使用new调用类的构造函数会执行如下操作
- 在内存中创建一个新对象
- 这个新对象内部的[[prototyoe]]指针被赋值为构造函数的prototype属性
- 构造函数内部的this被赋值为这个新对象(即this指向新对象)
- 执行构造函数内部的代码(给新对象添加属性)
- 如果构造函数返回非空对象,则返回该对象;否则返回刚创建的新对象
类构造函数与构造函数的主要区别是,调用类构造函数必须使用new操作符。而普通构造函数如果不使用new调用,那么就会以全局的this(通常是window)作为内部对象
function Person(){
return this
}
let p = Person()
let p2 = new Person()
console.log(p); // global
console.log(p2);// Person {}
实例、原型和类成员
类的语法可以非常方便的定义应该存在于实例上的成员、应该存在于原型上的成员,以及应该存在于类本身的成员
实例成员
在constructor() 方法里面的
class Person {
constructor(){
this.name = 'jack'
}
}
// 也可以把实例属性列在类最上方,给予默认值
class Person {
name = 'jack'
constructor(){
}
}
原型方法与访问器
class Person {
// 在类块中定义的所有内容都会定义在类的原型上
method(){}
}
类定义也支持获取和设置访问器,语法与行为跟普通对象一样
class Person {
set name (newName) {
this._name = newName + '-test'
}
get name () {
return this._name
}
}
let p = new Person()
p.name = 'fuck'
console.log(p.name);// fuck-test
静态类方法
静态方法用来执行不特定于实例的操作,也不要求存在类的实例。
class Person {
static sayName() {
console.log('hi bitch');
}
}
let p = new Person()
Person.sayName() // hi bitch
p.sayName() // TypeError: p.sayName is not a function
class Person {
constructor() {
// 添加到 this 的所有内容都会存在于不同的实例上
this.locate = () => console.log('instance', this);
}
// 定义在类的原型对象上
locate() {
console.log('prototype', this);
}
// 定义在类本身上
static locate() {
console.log('class', this);
}
}
let p = new Person();
p.locate(); // instance, Person {}
Person.prototype.locate(); // prototype, {constructor: ... }
Person.locate(); // class, class Person {}
// 静态类方法非常适合作为实例工厂:
class Person {
constructor(age) {
this.age_ = age;
}
sayAge() {
console.log(this.age_);
}
static create() {
// 使用随机年龄创建并返回一个 Person 实例
return new Person(Math.floor(Math.random()*100));
}
}
console.log(Person.create()); // Person { age_: ... }
迭代器与生成器方法
class Person {
constructor() {
this.nicknames = ['Jack', 'Jake', 'J-Dog'];
}
*[Symbol.iterator]() {
yield *this.nicknames.entries();
}
}
let p = new Person();
for (let [idx, nickname] of p) {
console.log(nickname);
}
继承
使用extends关键字,背后依旧使用的是原型链
继承基础
不仅可以继承类,也可以继承普通的构造函数
构造函数、HomeObject和super()
派生类的方法可以通过super关键字引用他们的原型。这个关键字自能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部
在派生类构造函数中是,不要在调用super() 方法之前引用this
在静态方法中可以通过super调用继承的类上定义的静态方法
class Vehicle {
static identify(){
console.log('vehicle');
}
}
class Bus extends Vehicle {
static identify(){
super.identify()
}
}
Bus.identify() // vehicle
注意事项:
- super 只能在派生类构造函数和静态方法中使用
- 不能单独引用super 关键字,要么用它调用构造函数,要么用它引用静态方法
- 调用super() 会调用父类构造函数,并将返回的实例赋值给this
- super() 的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入
- 如果没有定义类构造函数,在实例化派生类时会调用super() ,而且会传入所有传给派生类的参数
class Vehicle {
constructor(licensePlate) {
this.licensePlate = licensePlate;
}
}
class Bus extends Vehicle {}
console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' }
- 在类构造函数中,不能在调用super() 之前引用this
- 如果派生类中显示定义了构造函数,则要么必须在其中调用super() ,要么必须在其中返回一个对象
class Vehicle {}
class Car extends Vehicle {}
class Bus extends Vehicle {
constructor() {
super();
}
}
class Van extends Vehicle {
constructor() {
return {};
}
}
console.log(new Car()); // Car {}
console.log(new Bus()); // Bus {}
console.log(new Van()); // {}
抽象基类
类似abstract,ECMAScript没有专门支持这种类的语法,通过new.target也很容易实现。
new.target 保存通过new关键字调用的类或函数。通过在实例化时检测new.target 是不是抽象基类,可以阻止对抽象基类的实例化
// 抽象基类
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 {}
new Vehicle(); // class Vehicle {}
// Error: Vehicle cannot be directly instantiated
如果要求派生类必须实现某个方法
// 抽象基类
class Vehicle {
constructor() {
if (new.target === Vehicle) {
throw new Error('Vehicle cannot be directly instantiated');
}
if (!this.foo) {
throw new Error('Inheriting class must define foo()');
}
console.log('success!');
}
}
// 派生类
class Bus extends Vehicle {
foo() {}
}
// 派生类
class Van extends Vehicle {}
new Bus(); // success!
new Van(); // Error: Inheriting class must define foo()
继承内置类型
class SuperArray extends Array {
shuffle() {
// 洗牌算法
for (let i = this.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this[i], this[j]] = [this[j], this[i]];
}
}
}
let a = new SuperArray(1, 2, 3, 4, 5);
console.log(a instanceof Array); // true
console.log(a instanceof SuperArray); // true
“复合胜过继承”
函数
函数实际上是对象,所以同样有属性和方法,函数名就是指向函数对象的指针。
箭头函数
箭头函数不能使用arguments、super和new.target,也不能用作构造函数。此外,箭头函数也没有prototype属性
函数名
如果函数是一个获取函数、设置函数,或者使用bind() 实例化,那么标识符前会加上一个前缀
function foo() {}
console.log(foo.bind(null).name); // bound foo
let dog = {
years: 1,
get age() {
return this.years;
},
set age(newAge) {
this.years = newAge;
}
}
let propertyDescriptor = Object.getOwnPropertyDescriptor(dog, 'age');
console.log(propertyDescriptor.get.name); // get age
console.log(propertyDescriptor.set.name); // set age
理解参数
定义函数时要接收两个参数,并不意味着调用时就传两个参数,可以传一个、两个、三个,抑或是一个都不传。这样修改arguments[0] = 'change', 会修改函数传入的第一个参数值。但是这种同步是单向的。arguments的长度是根据传入参数的个数来确定的
函数声明与函数表达式
JavaScript引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。
函数声明会在任何代码执行之前先被读取并添加到执行上下文,这个过程叫做函数声明提升。在执行代码时,JavaScript引擎会先执行一遍扫描,把发现的函数声明提升到源代码树顶部。
函数内部
函数内部存在两个特殊的对象:arguments和this。es6又新增了new.target 属性
arguments
arguments是一个类数组对象,包含调用函数时传入的所有参数。
arguments还有一个callee属性,是一个指向arguments对象所在函数的指针
this
this在标准函数和箭头函数中有不同的行为
在标准函数中,this引用的是把函数当成方法调用的上下文对象,这时候通常称其为this值
在箭头函数中,this引用的是定义箭头函数的上下文,箭头函数中的this会保留定义该函数时的上下文
caller
这个属性引用的是调用当前函数的函数
new.target
es6新增的,new.target属性用来检测函数是否使用new关键字调用,如果是正常调用的,new.target的值是undefined;如果是使用new关键字调用的,则new.target将引用被调用的构造函数
function King() {
if (!new.target) {
throw 'King must be instantiated using "new"'
}
console.log('King instantiated using "new"');
}
new King(); // King instantiated using "new"
King(); // Error: King must be instantiated using "new"
函数属性与方法
length属性:保存函数定义的命名参数的个数
prototype属性:保存引用类型所有实例方法的地方
apply()和call() 方法。这两个方法都会以指定的this值来调用函数,即会设置调用函数时函数体内this对象的值。有控制函数调用上下文即函数体内this值的能力,可以将任意对象设置为任意函数的作用域。
apply() 方法接收两个参数:函数内this的值和一个参数数组-可以使Array的实例也可以是arguments对象
bind() 方法:创建一个新的函数实例,其this值会被绑定到传给bind() 的对象
递归
递归函数通常的形式是一个函数通过名称调用自己
递归代码最容易在栈内存中迅速产生大量栈帧
尾调用优化
es6新增了一项内存管理优化机制,让JavaScript引擎在满足条件时可以重用帧栈。这项优化非常适合“尾调用”,即外部函数的返回值是一个内部函数的返回值。尾调用优化要求必须在严格模式下有效。
function outerFunction() {
return innerFunction(); // 尾调用
}
es6优化前,内存中发生的操作:
- 执行到 outerFunction 函数体,第一个栈帧被推到栈上。
- 执行 outerFunction 函数体,到 return 语句。计算返回值必须先计算 innerFunction。
- 执行到 innerFunction 函数体,第二个栈帧被推到栈上。
- 执行 innerFunction 函数体,计算其返回值。
- 将返回值传回 outerFunction,然后 outerFunction 再返回值。
- 将栈帧弹出栈外。
es6优化之后,内存中发生的操作:
- 执行到 outerFunction 函数体,第一个栈帧被推到栈上。
- 执行 outerFunction 函数体,到达 return 语句。为求值返回语句,必须先求值 innerFunction。
- 引擎发现把第一个栈帧弹出栈外也没问题,因为 innerFunction 的返回值也是 outerFunction的值。
- 弹出 outerFunction 的栈帧。
- 执行到 innerFunction 函数体,栈帧被推到栈上。
- 执行 innerFunction 函数体,计算其返回值。
- 将 innerFunction 的栈帧弹出栈外。
第一种情况下每多调用一次嵌套函数,就会多增加一个栈帧。而第二种情况下无论调用多少次嵌套函数,都只有一个栈帧。这就是 ES6 尾调用优化的关键:如果函数的逻辑允许基于尾调用将其销毁,则引擎就会那么做。
闭包
闭包指的是那些引用了另一个函数作用域中变量的函数。
理解作用域链创建和使用的细节对理解闭包非常重要。在调用一个函数时,会为这个函数调用创建一个执行上下文,并创建一个作用域链。然后用arguments和其它命名参数来初始化这个函数的活动对象。外部函数的活动对象是内部函数作用域链上的第二个对象。这个作用域链一直向外串起了所有包含函数的活动对象,直到全局执行上下文才终止。
函数执行时,每个执行上下文中都会有一个包含其中变量的对象。全局上下文中的叫变量对象,它会在代码执行期间始终存在。而函数局部上下文中的叫活动对象,只在函数执行期间存在。在定义一个函数时,就会为它创建作用域链,预装载全局变量对象,并保存在[[Scope]]中。在调用这个函数时,会创建相应的执行上下文,然后通过复制函数的[[Scope]]来创建其作用域链。接着会创建函数的活动对象(用作变量对象)并将其推入作用域链前端。作用域链其实是一个包含指针的列表,每个指针分别指向一个变量对象,但物理上并不会包含相应的对象。
this对象
在闭包中使用this会让代码变复杂。如果内部函数没有使用箭头函数定义,则this对象会在运行时绑定到执行函数上下文。如果在全局函数中调用,则this在非严格模式下等于window,在严格模式下等于undefined。如果作为某个对象的方法调用,则this等于这个对象。匿名函数在这种情况下不会绑定到某个对象,这就意味着this会指向window,除非在严格模式下this是undefined。
window.identity = 'The Window';
let object = {
identity: 'My Object',
getIdentityFunc() {
return function() {
return this.identity;
};
}
};
console.log(object.getIdentityFunc()()); // 'The Window'
每个函数在被调用的时候都会自动创建两个特殊变量:this和arguments。内部函数永远不可能直接访问外部函数的这两个变量。
window.identity = 'The Window';
let object = {
identity: 'My Object',
getIdentityFunc() {
let that = this;
return function() {
return that.identity;
};
}
};
console.log(object.getIdentityFunc()()); // 'My Object'
window.identity = 'The Window';
let object = {
identity: 'My Object',
getIdentity () {
return this.identity;
}
};
object.getIdentity(); // 'My Object'
(object.getIdentity)(); // 'My Object'
// 第三行执行了一次赋值,然后再调用赋值后的结果。因为赋值表
// 达式的值是函数本身,this 值不再与任何对象绑定,所以返回的是"The Window"。
(object.getIdentity = object.getIdentity)(); // 'The Window'
小结
闭包的作用域链中包含自己的一个变量对象,然后是包含函数的变量对象,直到全局上下文的变量对象
通常,函数作用域及其中的所有变量在函数执行完毕后都会被销毁
闭包在被函数返回之后,其作用域会一直保存在内存中,直到闭包被销毁
BOM
浏览器对象模型是以window对象为基础的,window对象也被复用为ECMAScript的Global对象,所有全局变量和函数都是它的属性,而且所有原生类型的构造函数和普通函数也都从一开始就存在于这个对象之上。能够支持访问和操作浏览器的窗口
location对象
location对象既是window的属性,也是document的属性
URLSearchParams
const urlSearch = '?test=2&fuck=3'
const params = new URLSearchParams(urlSearch)
// params.entires()
// ['test', '2']
// ['fuck', '3']
DOM
文档对象模型是HTML和XML文档的编程接口。DOM将整个页面抽象为一组分层节点,使用DOM API开发者可以添加、删除和修改页面的各个部分
Node类型
DOM Level1描述了名为Node的接口,这个接口是所有DOM节点类型都必须实现的。一共有12种节点类型
// 节点类型可以通过与常量比较来确定
const someNode = document.getElementById('app')
if(someNode.nodeType === Node.ELEMENT_NODE){
// DO SOMETHING
}
开发最常用的就是元素节点和文本节点
nodeName
nodeName始终等于元素的标签名
节点关系
每个节点都有一个childNodes属性,其中包含一个NodeList(类数组对象)的实例。NodeList是一个对DOM结构的查询,DOM结构的变化会实时在NodeList中反映出来,并不是第一次访问时所获得的内容快照。
每个节点都有一个parentNode属性,指向当前节点的父节点
preventSibling和nextSibling可以访问临近节点
父节点快速访问首尾节点,firstChild和lastChild
有一个hasChildNodes的方法能快速确认是否有子节点
所有节点都有一个指向document的属性 owerDocument
操纵节点
appendChild()可以在childNodes列表末尾添加节点,如果append 的一个节点是已存在的,那么该节点将会从原来的位置被转移到新位置
insertBefore(要插入的节点,参照节点 || null) 当参照节点为null的时候,insertBefore与appendChild效果相同
replaceChild(要插入的节点,要替换的节点)
removeChild(要移除的节点)
其他方法
所有的节点类型还共享两个方法
cloneNode()
会返回与调用它的节点一模一样的节点,可以传入true来复制子节点,复制出来可以通过appendChild、insertBefore或者replaceChild添加到文档中去
normalize() 处理文档子树中的文本节点
由于解析器实现的差异或 DOM 操作等原因,可能会出现并不包含文本的文本节点,或者文本节点之间互为同胞关系。在节点上调用 normalize()方法会检测这个节点的所有后代,从中搜索上述两种情形。如果发现空文本节点,则将其删除;如果两个同胞节点是相邻的,则将其合并为一个文本节点。
Document类型
表示文档节点的类型 ,document是HTMLDocument的实例。document是window对象的属性,因此是一个全局对象
文档子节点
document.documentElement 指向 节点
document.body 指向节点
document.doctype 指向节点
一般来说,appendChild、insertBefore和replaceChild方法不会用在document对象上。因为文档类型是只读的,而且只能有一个Element类型的子节点,即
文档信息
document.title
document.URL
document.domain
如果一个页面的iframe url不一致,可以通过设置父子页面domain一致来实现通信,一旦domain放松就不可以收紧了
document.domain = 'baidu.com' // 放松 成功
document.domain = 'porn.baidu.com' // 收紧,失败
document.referrer
定位元素
document.getElementById
document.getElementsByTagName
特殊集合
均为HTMLCollection的实例,
HTMLCollection和NodeList的差别,HMTL只能含有元素节点,而NodeList可以包含所有节点类型
document.anchors
document.forms
document.images
document.links
Element 类型
除document类型之外,Element类型是Web开发中最常用的类型。element表示xml或html元素,对外暴露出访问元素标签名、子节点和属性的能力
HTML元素
所有的HTML元素都通过HTMLElement类型表示,HTMLElement直接继承自Element。
id,title, lang, dir, className 这些属性可以直接通过element获取
取得属性
getAttribute('id', 'app')
setAttribute('id', 'app')
removeAttribute('id')
attributes 属性
Element类型是唯一使用attributes属性的DOM节点类型。attributes属性包含一个NameNodeMap实例,是一个类似NodeList的“实时”集合。
getNamedItem
removeNamedItem
setNamedItem
Item
四个方法,用的不多。多用于迭代元素上所有属性的场景
创建元素
document.createElement
元素后代
childNodes 属性包含元素的所有子节点,而children是获取元素的子元素,是HTMLCollection集合
其他节点
Text 类型,text节点由Text类型表示
创建文本节点 document.createTextNode()
Comment 类型
DocumentType类型
DocumentFragment 类型
Attr 类型
使用NodeList
NodeList 和 NamedNodeMap、HTMLCollection 这3个集合类型都是实时的
const divs = document.getElementsByTagName('div')
for(let i = 0; i < divs.length; i++) {
const div = document.createElement('div')
document.body.appendChild(div)
}
// 这会陷入死循环中
最好限制操作NodeList的次数,每次查询都会搜索整个文档,最好把查询到的NodeList缓存起来。
MutationObserver 接口
let observer = new MutationObserver(() => console.log('this dom changed'))
// 第二个参数是可选的
observer.observe(document.body, {attributes: true}}
// 终止监听
observer.disconnect()
小结
DOM由一系列节点类型构成,主要包括以下几种
- Node是基准节点类型,所有其他类型都继承Node
- Document 类型表示整个文档,对应树形结构的根节点
- Element 节点表示文档中所有HTML和XML元素,可以用来操作他们的内容和属性
- 其他节点类型分别表示文本内容、注释、文档类型。。。
DOM扩展
Selectors API
Selectors API Level 1 的核心是两个方法 querySelector() 和 querySelectorAll() ,document类型和element 类型的实例上都会暴露这两个方法
Selectors API Level2 在Element类型上新增了方法,比如matches()
querySelector()
// 取得<body> 元素
let body = document.querySelector('body')
// 取得ID为 “myDiv”的元素
let myDiv = document.querySelector('#myDiv')
// 取得类名为“selected”的第一个元素
let selected = document.querySelector('.selected')
// 取得类名为“button”的图片
let img = document.body.querySelector('img.button')
querySelectorAll()
querySelectorAll() 方法和querySelector方法一样,但返回的是NodeList的静态实例,而非快照
matches()
matches() 接收一个CSS选择符参数,如果元素元素匹配该选择符则返回true
元素遍历
childElementCount
firstElementChild
lastElementChild
preventElementSibling
nextElementSibling
HTML5
HTML5代表着与以前的HTML截然不同的方向。在所有以前的HTML规范中,从未出现过描述JavaScript接口的情形,HTML就是一个纯标记语言。JavaScript绑定的事,一概交给Dom规范去定义。然而HTML5规范包含了与标记相关的大量JavaScript API定义。其中有的API与DOM重合,定义了浏览器应该提供的DOM扩展。
CSS类扩展
getElementsByClassName
// 取得所有类名中包含“username”和“current”元素
// 类名的顺序无关紧要
let allCurrentUsernames = document.getElementsByClassName('current username')
// 返回的是NodeList
classList
classList是一个新的集合类型 DOMTokenList的实例,所有Element类型就具有的一个属性。具有以下几个方法(IE10及以上)
- add(增加的类名):void
- contains(判断是否包含的类名): boolean
- remove(需要删除的类名) :void
- toggle(需要切换的类名) :void
let div = document.querySelect('#app')
// div.classList.add
// div.classList.contains
// div.classList.remove
// div.classList.toggle
自定义数据属性
在元素上加一个data-自定义属性名,该元素的dataset中可以访问到该属性
// <h1 class='test' data-Fuck-Y-y-hai="baobei">hello world!</h1>
let h = document.querySelector('test')
JSON.stringify(h.dataset) // '{"fuckYYHai":"baobei"}'
// 自定义属性名以-为分隔符自动转为小驼峰
插入标记
innerHTML属性
innerHTML会返回元素所有后代的HTML字符串,包括元素、文本节点和注释,在写入innerHTML时,会根据提供的字符串值以新的DOM子树替代元素中原来包含的所有节点
outerHTML
outerHTML会包含该元素
insertAdjacentHTML
insertAdjacentText
这两个方法都接收两个参数:要插入标记的位置和要插入的HTML或文本
位置参数:
| beforebegin | 当前元素前面,作为上一个同胞节点 |
|---|---|
| afterbegin | 当前元素内部,新的子节点或第一个子节点 |
| beforeend | 当前元素内部,新的子节点或最后一个子节点 |
| afterend | 当前元素后面,作为下一个同胞节点 |
scrollIntoView()
该方法存在于所有的HTML元素上,滚动方式
专有扩展
未来有可能被标准化,被纳入HTML5
children属性
children属性是一个HTMLCollection,只包含元素的Element类型的子节点
contains() 方法
document.documentElement.contains(document.body) // true
插入标记
innerText
outerText
DOM2和DOM3
元素尺寸
偏移尺寸
客户端尺寸
滚动尺寸
确定元素尺寸
元素有一个getBoundingClientRect() 方法,获取元素在视口中的位置
遍历
NodeIterator和TreeWalker可以对DOM树执行深度优先的遍历
事件
js和html的交互是通过事件实现的,事件代表文档或者浏览器窗口中某个有意义的时刻。
事件流
事件流描述了页面接收事件的顺序。
事件冒泡
从最具体的元素(文档树中最深的节点)开始触发,然后向上传播至没有那么具体的元素(文档)。
现代浏览器中的事件会一直冒泡到window对象
事件捕获
最不具体的节点最先收到事件,最具体的节点最后收到事件
DOM事件流
事件流分3个阶段:事件捕获、到达目标和事件冒泡
在 DOM 事件流中,实际的目标(
事件处理程序
事件以为这用户或浏览器执行的某种动作。比如,click,load,mouseover。为响应事件而调用的函数被称为事件处理程序(或事件监听)。事件处理程序的名字以“on”开头,因此click事件的处理程序叫做onclick,有很多方式可以指定事件处理程序。
HTML 事件处理程序
// <button value='valueStr' onclick="test('fu', event, this, value)">test</button>
function test(a, e, dom, value) {
// a 'fu'
// e event对象
// dom 事件的目标元素
// value = dom.value
}
上面的这个test函数实际上是被包装了一层,作用域链被拓展了。
function () {
with(document){
with(this){
// 属性值
}
}
}
DOM0 事件处理程序
// <input id="d3" value="fuck"/>
let input = document.getElementById('d3')
input.onclick = function () {
console.log(this.id) // d3
}
DOM2 事件处理程序
addEventListener() 和 removeEventListener() 这两个方法暴露在所有DOM节点上,他们接收3个参数:事件名、事件处理函数和一个布尔值,true表示在捕获阶段调用事件处理程序,false(默认值)表示在冒泡阶段调用事件处理程序
let btn = document.getElementById('btn')
function handler(){}
btn.addEventListener('click', handler, false)
btn.removeEventListener('click', handler, false)
大多数情况下,事件处理程序会被添加到事件流的冒泡阶段,这样兼容性好。把事件处理程序注册到捕获阶段通常用于在事件到达其指定目标之前拦截事件。
事件对象
在DOM中发生事件时,所有相关信息都会被收集并存储在一个名为evnet的对象中
DOM 事件对象
preventDefault() 用于阻止特定事件的默认动作
stopPropagation() 阻止冒泡
eventPhase 属性确定事件流当前所处的阶段
1代表捕获阶段被调用,2代表在目标上被调用,3代表捕获阶段被调用
事件类型
事件委托
事件委托利用事件冒泡,可以只使用一个事件处理程序来管理一种类型的事件。
模拟事件
createEvent方法可以模拟事件
动画与Canvas图形
使用requestAnimationFrame
这个方法会告诉浏览器要执行动画了(让JavaScript跟进浏览器渲染周期),于是浏览器可以通过最优方式确定重绘的时序。
无论setTimeout和setInterval都不能保证时间精度,作为第二个参数的延时只能保证何时会把代码添加到浏览器的任务队列,不能保证添加到队列就会立即执行
requestAnimationFrame接收一个参数,此参数是一个要在重绘屏幕前调用的函数,解决了浏览器不知道js动画何时开始的问题,以及最佳间隔是多少的问题
function updateScreen() {
// do something
}
let requestId = window.requestAnimatinoFrame(() => updateScreen())
window.cancelAnimationFram(requestId)
使用requestAnimationFrame节流
let enabled = true
function someOperate() {
// do some operate
}
window.addEventListener('scroll', () => {
if(enabled) {
enabled = false
window.requestAnimationFrame(someOperate())
setTimeout(() => enabled = true, 50)
}
})
JavaScript API
影子DOM
他可以将一个完整的DOM树作为节点添加到父DOM树
let colorList = ['red', 'blue', 'pink']
for (const color of colorList) {
const div = document.createElement('div')
const attachDOM = div.attachDOMShadow({mode: 'open'})
div.appendChild(attachDOM)
attachDOM.innerHTML = `
<p>this is ${color} mode</p>
<style>
p {
color: ${color};
}
</style>
`
document.body.appendChild(div)
}
错误处理
只要代码中包含了finally子句,try块或catch块中的return语句就会被忽略
JSON
json是一种轻量级的数据格式,可以方便的表示复杂的数据结构,ECMAscript5定义了远程JSON对象,用于把JavaScript对象序列化为JSON字符串以及将jSON字符串反序列化为js对象
网络请求与远程资源
ajax是无需刷新从服务器获取数据的一种方法
跨源资源共享
CORS的基本思路就是使用自定义的HTTP头部允许服务器和浏览器进行相互了解,以确定请求或响应应该成功还是失败
替代性跨源技术
依赖能够执行跨源请求的DOM特性,在不适用xhr对象情况下发送某种类型的请求。
图片探测
任何页面都可以跨域加载图片
JSONP
jsonp包含两个部分:回调和数据。jsonp调用是通过动态创建
function handleResponse(response) {
//do something
}
let script = document.createElement("script");
script.src = "http://freegeoip.net/json/?callback=handleResponse";
document.body.insertBefore(script, document.body.firstChild);
工作者线程
JavaScript环境实际上是运行在托管操作系统中的虚拟环境,在浏览器每打开一个页面,就会分配它一个自己的环境。每个页面都有自己的内存、事件循环、DOM等等,使用工作者线程,浏览器可以在原始页面环境之外再分配一个完全独立的二级子环境,这个子环境不能与依赖单线程交互的API(如DOM)互操作,但可以与父环境并行执行代码。
工作者线程主要定义了三种: 1. 专用工作者线程。2. 共享工作者线程。3.服务工作者线程
杂篇
MIME:多用途互联网邮件拓展类型
是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开。多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式。
ECMAScript
浏览器只是ECMAScript实现可能存在的一种宿主环境。宿主环境提供ECMAScript的基准实现和与环境自身交互必须的拓展
Symbol
symbol是原始值,且符号实例是唯一、不可变的。用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
符号的基本用法
Symbol()
// 也可以传入一个字符串参数作为对符号的描述
Symbol('foo')
全局符号注册表
let a = Symbol.for('foo') // 创建新符号
let b = Symbol.for('foo') // 重用已有符号
a === b // true
使用符号作为属性
let s1 = Symbol('foo'),
s2 = Symbol('bar');
let o = {
[s1]: 'foo val',
[s2]: 'bar val',
baz: 'baz val',
qux: 'qux val'
};
console.log(Object.getOwnPropertySymbols(o));
// [Symbol(foo), Symbol(bar)]
console.log(Object.getOwnPropertyNames(o));
// ["baz", "qux"]
console.log(Object.getOwnPropertyDescriptors(o));
// {baz: {...}, qux: {...}, Symbol(foo): {...}, Symbol(bar): {...}}
console.log(Reflect.ownKeys(o));
// ["baz", "qux", Symbol(foo), Symbol(bar)]
常用内置符号
ECMAScript 6 也引入了一批常用内置符号,用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。这些内置符号都以 Symbol 工厂函数字符串属性的形式存在。
Symbol.iterator
Symbol.asyncIterator
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.match
Symbol.replace
Symbol.search
Symbol.species
Symbol.split
Symbol.toPrimitive
Symbol.toStringTag
Symbol.unscopables
For-in
for-in语句是一种严格的迭代语句,用于枚举对象中的非symbol键属性
For-of
for-of语句是一种严格的迭代语句,用于遍历可迭代对象的元素
With
with语句的用途是将代码作用域设置为特定的对象
事件循环:宏任务和微任务
他是一个在JavaScript引擎等待任务,执行任务和进入休眠状态等待更多任务这几个状态之间转换的无限循环。
引擎的一般算法是:1. 当有任务时,从最先进入的任务开始执行。2. 休眠直到出现任务,然后转到第一步。
一个任务来时,引擎可能正处于繁忙状态,那么这个任务就会被排入队列。多个任务组成一个队列,即所谓的“宏任务队列”。
两个细节:
- 引擎执行任务时永远不会渲染(render)。如果任务需要执行很长一段时间也没关系。仅在任务完成后才会绘制对DOM的更改。
- 如果一项任务执行花费的时间过长,浏览器将无法执行其他任务,例如处理用户事件。在一定时间后,浏览器会抛出一个如“页面未响应”之类的警报。
微任务
微任务仅来自于代码,通常是promise创建的
queueMicroTask(func), 这个函数帮助func在微任务队列中排队执行。
每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后再执行其他宏任务,或渲染,或进行其他任何操作。
console.log(1);
setTimeout(() => console.log(2));
Promise.resolve().then(() => console.log(3));
Promise.resolve().then(() => setTimeout(() => console.log(4)));
Promise.resolve().then(() => console.log(5));
setTimeout(() => console.log(6));
console.log(7);
// 输出顺序
1
7
3
5
2
6
4