漫长的从零到一的JS调试之路

漫长的从零到一的JS调试之路

关于变量

var

声明的作用域是函数作用域

使用var定义的变量会成为包含它的函数的局部变量

使用var定义的变量会自动提升到函数作用域顶部(声明提升)

let

let声明的是块作用域,需要注意的是块作用域函数作用域的子集

let定义的变量不会进行声明提升,这称为“暂时性死区”

const

可以理解为java中的声明一个常量

关于数据类型

Undefined(定义了但未赋予初始值)

它的值就是undefined,使用var或let声明变量没赋初值,就相当于赋予了undefined

Null

它的值为null,逻辑上它的值表示一个空对象指针

Boolean

Number

有个特殊的值:NaN(不是数值“Not a Number”),用来表示本来要返回数值的操作失败了

String

可以用单(‘’)、双引号(“”)或者反引号(`)标示

Symbol

确保对象属性使用唯一标识符,防止属性冲突的危险

Object

关于正则表达式

用于定义一些字符串的规则

语法:

var 变量 = new RegExp(“正则表达式”,”匹配模式”);

完成上述new后得到一个正则表达式对象,随后我们就能用这个对象去检查其他字符串是否符合该正则表达式的规范

此外还能使用字面量来创建正则表达式:

var 变量 = /正则表达式/匹配模式


正则表达式规则:

/[abc]/:表示含有a或b或c的字符串


/[ ^ abc]/:表示匹配除了[abc]的所有字符


/a{n}/:花括号的n表示对它前面一个内容起作用,{n}表示正好出现n次

扩展:/(abcd){n}/:表示abcd这一个整体正好出现n次

另外也可以这样写,/a{x,y}/,这表示a可以出现x到y次(出现次数”区间化“)

具体规则还有好多,可参考官方文档…

关于DOM

  • 全称Document Object Model
  • js通过DOM来对html文档进行操作。
  • Document:指的是整个html页面
  • Object:将网页的每一部分都转换为一个对象(每一个标签)
  • Model:使用模型表示对象之间的关系,方便我们获取对象

onload事件会在整个页面加载完成之后才触发

为window绑定一个onload事件,该事件对应的响应函数将会在页面加载完成之后执行,这样就可以确保我们的代码执行时所有的DOM对象已经加载完毕

关于BOM

  • 全称浏览器对象模型

  • 可以使我们通过js来操作对象

  • 在BOM中为我们提供了一组对象,用来完成对浏览器的操作

  • BOM对象

    • Window

      • 代表整个浏览器的窗口,同时window也是网页的全局对象
    • Navigator

      • 代表当前浏览器的信息,通过该对象可以识别不同的浏览器
    • History

      • 代表浏览器的历史记录,可以通过该对象来操作浏览器的历史记录,由于隐私原因,该对象不能获取到具体的历史记录,只能操纵浏览器向前或者向后翻页,且该操作只在当次访问时有效
    • Screen

      • 代表用户的屏幕的信息,通过该对象可以获取到用户的显示器的相关的信息

事件句柄

  • onclick…

关于typeof、instanceof、===(这个不算高级吧)

  • typeof

    • 可以判断undefined、数值、字符串、布尔值、function(且判断显示的数据类型以字符串的形式返回)

    • 但无法判断(null和object)、(object和array)

  • instanceof

    • 判断一个对象是否是一个类的实例
  • ===

    • 可以判断null和undefined

关于区分数据类型和变量类型

  • 数据类型

    • 基本类型
    • 对象类型
  • 变量类型

    • 基本类型
    • 引用类型

乍一看很像,说简单点,变量类型就是一个装数据类型的容器,比如变量类型中的基本类型保存的就是基本类型的数据,例子有var a = 3

关于IIFE(Immediately Invoked Function Expression)

立刻执行函数

类似于函数声明,但由于包含在括号内,所以会被解释成函数式表达式,紧跟在第一组括号后面的第二组括号会立即调用前面的函数表达式,具体如下所示:

1
2
3
(function(){
//块级作用域
})();

集合引用类型

Array

创建数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<script>
//普通创建方式,使用构造函数
let colors = new Array(20)
console.log(colors)
//使用数组字面量
let color = ['red','green','blue']
console.log(color)

/*
创建数组的静态方法(两种)from(),of(),其中from()用于将类数组结构转换为数组,类数组是指有length属性和可索引元素的结构(字符串,集合,映射等)。of()用于将一组参数转换为数组
*/
const strArr = Array.from('kitholt')
console.log(strArr)

const m = new Map().set(1,2).set(3,4)
console.log(Array.from(m))

const s = new Set().add(1).add(2).add(3)
console.log(Array.from(s))

//from()方法还能对数组进行 浅复制
const a1 = [1,[6,66],3,4]
const a2 = Array.from(a1)
a1[1][0] = 99
console.log(a2 === a1);//false
console.log(a2[1][0])
</script>

数组复制和填充

1
2
3
4
5
6
7
8
9
10
11
<script>
/* copyWithin()用于批量复制数组。fill()用于向一个数组插入全部或者部分的值,通常需要指定范围,包含开始索引,不包含结束索引 */
const zeros = [0,0,0,0,0]
zeros.fill(1,1,4)//用1,填充索引大于等于1且小于4的元素,预期结果[0, 1, 1, 1, 0]
console.log(zeros)
//值得一提的是,对于超出数组边界或者0长度再或者是索引方向相反的情况,fill()都不会报错,而是忽略

const ints = [0,1,2,3,4,5,6,7,8,9]
ints.copyWithin(4,0,3)//结果为[0,1,2,3,0,1,2,7,8,9]
console.log(ints)
</script>

数组排序方法

1
2
3
4
5
6
7
8
<script>
/* 这里有两种方法,分别是reverse()和 sort(),其中reverse()能够接受一个比较函数,我们能够自定义这个 比较函数来决定是升序还是降序 */
const val = [1,4,2,5,41,32,7,6]
val.sort((a, b) => a > b ? 1 : a < b ? -1 : 0)//升序排列
console.log(val)
console.log(val.reverse())
//需要注意的是,这两种方法都返回调用它们数组的引用(会改变原数组)
</script>

数组的操作方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script>
/*
关于数组操作的方法,有concat(),它能创建一个数组的副本,然后将参数添加到这个副本的末尾。
slice()用于切割数组,需要注意的是这个方法不会影响原来的数组。
强大的splice(),说它强大是因为这一个方法,能够实现删除,插入,替换的操作。
*/
const arr = ['fran','伟鸿','陈部','志坚']
const arrConcat = arr.concat('胖子','仕豪',['浩彬','陈尹'])
console.log(arrConcat)//['fran', '伟鸿', '陈部', '志坚', '胖子', '仕豪', '浩彬', '陈尹']

const arrSlice = arrConcat.slice(2)//从第二个位置开始拆分数组,还有一种方式dddd
console.log(arrSlice)

arr.splice(0,2)
console.log('***',arr)//['陈部', '志坚']

arr.splice(1,0,'胖子','仕豪')
console.log(arr)//['陈部', '胖子', '仕豪', '志坚']
</script>

数组的迭代器方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
/* 对于一个数组,它有对应的索引,值,以及索引/值对。我们可以用下面三种方法来分别获取... */
const colors = ['red','green','blue']
const colorsIndex = Array.from(colors.keys())
console.log(colorsIndex)
for(let i of colors.values()){
console.log(i)
}

const colorVal = Array.from(colors.values())
console.log(colorVal)

const colorEntries = Array.from(colors.entries())
console.log(colorEntries)
</script>

数组索引

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
/*
对于数组的length属性,它不仅只读还可修改,通过修改length的属性,可以从数组末尾删除或者添加元素
*/
const colors = ['red','green','blue']
console.log('原数组',colors)
//添加一个black
colors[colors.length] = 'black'
console.log('插入black',colors)
//将数组长度变为2
colors.length = 2
console.log('将数组长度变为2',colors)
</script>

Map

基本API

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
//创建一个map
const myMap = new Map()
myMap.set('k1','v1')
.set('k2','v2')
.set('k3','v3')
/* get(),has(),delete()以及keys(),values(),entries()三个返回迭代器的方法 */
console.log(myMap)
console.log(myMap.get('k1'))//根据键获取对应的值
console.log(myMap.has('k2'))
myMap.delete('k2')
console.log(myMap)
</script>

注意点

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
/* 键和值在迭代器中是可以修改的,但是映射内部的引用无法修改 */
const m1 = new Map([['k1','v1']])
for(let key of m1.keys()){
key = 'newKey'
console.log(key)//newKey
console.log(m1.get('k1'))//结果为v1
}
/* 选择map还是obj?
插入操作看map
查询操作用obj
*/
</script>

Set

基本API

1
2
3
4
5
6
7
8
<script>
//set的方法包括add(),delete(),has(),size()
const mySet = new Set();
mySet.add('v1').add('v2').add('v3').add('v3')
console.log(mySet)//{'v1', 'v2', 'v3'}
const result = mySet.has('v1')
console.log(result)
</script>

对象,类与面向对象编程

对象

合并对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
/*
合并对象,顾名思义就是将两个对象进行合并。有时候也叫作“混入”,对象A(源对象)混入对象B(目标对象),注意该操作执行的【浅复制】9
*/
let src, dest, result
//定义目标对象
dest = {}
//定义源对象
src = {id: 'src'}
//增强后的对象,与此同时dest对象也会被修改
result = Object.assign(dest, src)
console.log("目标对象dest和增强后的对象result是否相等(同一个指针指向)?",result === dest)
result.id = "null"
console.log(result.id)
console.log(dest.id)
</script>

增强的对象语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<script>
/*
什么是语法糖,就是能让程序猴写代码写起来更舒服的语法结构
1.属性值简写(属性名和变量名一样时,即可触发对象属性的简写形式)
2.简写方法名
3.可计算属性 (可以在对象字面量中动态完成属性赋值,中括号包围的对象属性键表示这个属性会随着中括号里面的变量值的改变而改变
具体操作如下)
*/

const name = 'fran_name'
const age = 'fran_age'
const major = 'fran_major'
const say = 'fran_say'
let person = {
[name]: 'fran',
[age]: 21,
[major]: 'Software engineer',
[say](){
console.log(`my name is ${this.fran_name}`)
}
}
console.log(person)//{fran_name: 'fran', fran_age: 21, fran_major: 'Software engineer'}
/*
这种操作有点像,先挖一个萝卜坑(中括号),给每个坑一个编号(变量名)然后再种萝卜(变量的值)
*/

/*
另外可计算属性和简写方法名能够“联动”产生一些“节目”效果,方法名也是对象的一个属性,它也是可以计算的,在方法名外面也可以加[]
*/
person.fran_say()//my name is fran
</script>

对象属性的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<script>
/* 属性分为:
数据属性(就是我们平时定义的属性,包含一个保存数据值的位置。值会从这个位置读取,同时修改后也会写入到这个位置)
另外需要注意的是:我们可以对数据属性进行进一步地“约束”,这种“约束”叫作属性的特性。有四个特性configurable【属性是否能够删除】,enumerable【属性是否可以循环】,writable【属性的值是否能够修改】,value
利用Object.defineProperty()时,这四个特性默认都是false,当然可以人为设置

访问器属性(不含数据值,包含一个setter和getter函数)----------------->响应式数据的底层实现!!!!!!!
get函数(当访问访问器属性时,会被调用,用于返回一个有效值)
set函数(当写入 访问器属性时,会被调用,接收传入的新值,对数据进行修改)
*/
let person = {}
person.name = 'jack'
console.log(person.name)
Object.defineProperty(person,'age',{
writable: false,
configurable: false,
value: 18
})
console.log(person.age)
person.age = 21
console.log(person.age)//结果仍为18,因为writable设置了false,也就是说属性的值无法修改
delete person.age//删除属性无效

//下面演示一下“响应式数据”---摘(改)(抄)自js高级程序设计
let book = {
year_: 2021,
edition: 1,
}
Object.defineProperty(book,'year',{
get(){
return this.year_
},
set(newVal){
if(newVal > 2021){
this.year_ = newVal
this.edition = newVal - 2021
}
}
})
book.year = 2025
console.log(book.edition)// 4
//现在想想,vue里的data配置项中的数据(每一个vc的属性)都会为每个数据匹配一个专有的访问器属性,当数据被修改的时候,会调用其对应的setter方法
</script>

对象解构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<script>
/*
对象解构?官方说法就是 使用与 对象匹配的结构 来实现 对象属性赋值
大白话就是,如果你像得到某个对象的一些属性的值并用一些变量保存起来,最常规的做法就是 变量 = 对象.属性
*/
let person = {
name: 'fran',
age: 21,
shoes:{
nike: 'nike',
anta: 'anta',
}
}
//常规赋值
let name = person.name
let age = person.age
console.log('常规赋值')
console.log(name,age)
//使用解构赋值
let {name: myName, age: myAge} = person
console.log('解构赋值')
console.log(myName, myAge)
//值得注意的是,如果有对象以参数传入某个函数,我们也能对这个对象参数进行解构赋值,比如下面这个例子
let showShoes = function({shoes:{nike, anta}}){
console.log(nike, anta)
}
//传入的是一个person对象,但是会将该对象的shoes属性结构出来
showShoes(person)
</script>

构造函数

构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
/*
在js里,函数和构造函数也是属于Object这个祖先类,引入构造函数是为了更进一步确定对象类型
比如obj1 = {},这是Object的实例,而person = new Person(),它既是Object的实例,也是Person的实例

如何区分普通函数和构造函数?
使用new操作符调用的函数就是构造函数

一般构造函数的函数名第一个字母是大写,普通函数的则是小写
*/
//开始定义构造函数, ??应该能理解为创建一个类??
function Person(name, age) {
this.name = name
this.age = age
this.sayName = function(){
console.log(`halo, my name is ${this.name}. I am ${this.age}.`)
}
}
let fran = new Person('fran',21)//halo, my name is fran. I am 21.
fran.sayName()
</script>

继承(原型)

原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
/*
原型是构造函数的一个属性,这个属性是一个对象,上面有特定引用类型对象的共享方法和属性,从而实现所有属性和方法在所有实例之间是共享的
*/
function Person(name, age) {
this.name = name
this.age = age
//定义一个原型上的方法.
Person.prototype.sayName = function(){
console.log(`halo, my name is ${this.name}. I am ${this.age}.`)
}
}
let jack = new Person('jack',19)
let fran = new Person('fran',21)
jack.sayName()
fran.sayName()
</script>

组合继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<script>
/*
组合继承,综合了原型链和盗用构造函数,将两者的优点结合了。在原型链的原型上继承属性和方法,而子类实例的属性则通过盗用构造函数(调用父类的构造方法)来进行赋值
*/
function Person(name){
this.name = name
this.nationality = []
}
Person.prototype.sayName = function(){
console.log(`my name is ${this.name}`)
}

function YellowGuy(name, age){
Person.call(this,name)
this.age = age
}
YellowGuy.prototype = new Person()

YellowGuy.prototype.sayAge = function(){
console.log(`i am ${this.age}`)
}

let fran = new YellowGuy('fran',21)
fran.sayName()
fran.sayAge()
fran.nationality.push('China')
console.log(fran.nationality[0])
</script>

原型式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
/*
使用背景: 你有一个对象,想在它的基础上再创建一个新的对象。你可以采取前面讲到的组合继承,但是组合继承有的时候是需要事先“约定”好的,不能很好的做到随时使用。我们可以封装一个函数Object(),传入要在此基础上修改的对象,返回一个新的对象,这个新的对象的原型指向传入的这个对象
但是需要注意的是:属性中包含引用值始终会在相关对象之间共享
该方式最大的有点就是减少了构造函数的创建。想在那个对象基础上创建一个新的对象,只需要将这个对象传入Object函数
*/
function object(o){
function NewObj() {}
NewObj.prototype = o
return new NewObj()
}
let person = {
name: ''
}
let fran = object(person)
fran.name = 'fran'
console.log(fran.name)

/*
寄生式继承和它类似,只不过多了个增强对象环节(增加新的方法),然后再将这个新对象返回...
*/
</script>

寄生式组合继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<script>
/*
听名字就是组合继承的改进版,它是为了解决组合继承的效率问题,最主要的效率问题是父类构造函数会被调用两次,一次是在子类原型创建时调用,另外一次是在子类的构造函数中调用。

其实子类原型只要包含父类对象的实例属性即可,子类构造函数只要再执行的时候重写自己的原型即可

具体实现步骤就是:①利用寄生式继承来继承父类原型②执行完①会得到一个新对象,再将这个对象赋值给子类原型

仔细想想,组合继承的子类原型是父类构造函数的一个实例,而寄生式组合继承的则是一个“过渡对象”(这个对象的原型就是父类原型)
*/
function object(o){
function NewObj() {}
NewObj.prototype = o
return new NewObj()
}

let Person = function(name){
this.name = name
}

let ChinesePerson = function(name, age){
Person.call(this, name)//重点!!!!!!!!!!!!!!这里调用父类的构造方法
this.age = age
}

//核心方法,这一步代替了 ChinesePerson.prototype = new Person(),减少一次调用父类构造方法的次数
function inheritPrototype(subType, superType){
//利用寄生式继承里面的object方法来得到“过渡对象”
let prototype = object(superType.prototype)
//执行完①会得到一个新对象,再将这个对象赋值给子类原型
subType.prototype = prototype
}
inheritPrototype(ChinesePerson, Person)
let fran = new ChinesePerson('fran',21)
console.log(fran)
</script>

类构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
/*
什么是类构造函数?简单说就是在 类创建实例的时候 要调用的函数,此外要调用类构造函数一定要用new操作符!!!
*/
class Person{
//下面这就是Person的类构造函数- -,构造函数用于对实例进行相关属性方法的赋值操作
constructor(name){
this.name = name
this.sayName = function(){
console.log(`my name is ${this.name}`)
}
}
}
let p1 = new Person('fran')
p1.sayName()

//let p2 = Person()
//报错 Uncaught TypeError: Class constructor Person cannot be invoked without 'new'

let p3 = new Person.constructor()
console.log(p3 instanceof Person.constructor)
</script>

抽象基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<script>
/*
抽象基类???不就是抽象类嘛,在java中定义抽象类一个abstract关键字就好了,但是在js中要在需要定义成抽象类的constructor中使用new.target来检测,new.target会保存 通过new关键字调用的类或函数,通过比较可以阻止对抽象基类的实例化
想想,因为js里没有abstract这个关键字,所以默认 所有的类都是可以实例化的。现在又引入了抽象基类这个概念,只好将识别 抽象基类的环节 推迟到constructor中执行,new.target作为一个检查点,如果new的是一个 抽象基类 就会抛出错误
*/
class Vehicle{
constructor(price){
this.price = price
//设置“检查点,如果发现new的是Vehicle就抛出错误”
if(new.target === Vehicle){
throw new Error('不好意思,Vehicle是一个抽象基类,无法被实例化- -,请实例化它的子类,不用谢')
}
/* if(!this.foo){
throw new Error('这个子类必须实现foo方法')
}
console.log('创建Vehicle的子类实例成功') */
}
showPrice(){
console.log(`这辆车的价格是${this.price}w`)
}
/* foo(){
console.log('foo')
} */
}
Vehicle.prototype.foo = function(){
console.log('foo')
}

class Car extends Vehicle{
constructor(brand,price){
//注意子类的类构造函数内一定要先执行父类的类构造方法!!!不然会报错
super(price)
this.brand = brand
this.showBrand = function(){
console.log(`这辆车是${this.brand}`)
}
}
/* foo(){
console.log('foo')
} */
}

//下面尝试new一下这个抽象基类(必抛错)
//let car = new Vehicle()//Uncaught Error: 不好意思,Vehicle是一个抽象基类,无法被实例化- -,请实例化它的子类,不用谢 at new Vehicle (抽象基类.html:19)
let toyota = new Car('丰田',20)
let auto = new Car('大众',10)
toyota.showBrand()
toyota.foo()
console.log(toyota.brand === auto.brand)
</script>

原型方法与访问器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<script>
/*
为了能在实例之间能够共享方法,类定义语法把在 类块定义的方法作为 原型方法
另外,类定义也支持设置访问器
*/
class Person{
//添加到this的所有内容都会存在于不同的实例上
constructor(name,age){
this.name = name

/* 这是我之前犯的错误
this.age = age
*/

this.age_ = age
this.locate = () => {
console.log('instance')
}
/* this.sayAge = function(){
console.log(this.age)
} */
}
set age(age){
console.log('修改age')

/* 同上
this.age = age
*/

this.age_ = age
}
get age(){
console.log('读取age')
return this.age_
}
//这个就是定义在类的原型的方法,从这里可以看出,该方法位于类块里,constructor之外
locate(){
console.log('prototype')
}
}

let p1 = new Person('fran',21)
console.log(p1.name)
p1.locate()//instance
Person.prototype.locate()//prototype

p1.age
p1.age = 22

/*
在设置访问器的时候遇到了栈溢出的问题:产生这个问题的原因很简单但是我一开始并没有发现:我把访问器的属性和实例属性写成一样的了。访问器属性的工作原理是:当访问到该属性,该属性本身是不会存放数据的,而是会调用set或者get函数,里面才是要处理的数据
就拿上面的age来说,实例属性和访问器属性都是age,乍一看合情合理:我访问age属性,自然就会调用相关函数改变age的值。大错特错,来看看set的内部有条this.age语句,这条语句相当于又访问了age这个访问属性,然后由调用set,然后.........就死循环最后就栈溢出了
解决方法很简单:属性命名不一样即可,一般真正用来存放数据的属性我们都会把它们“隐藏起来”(这样命名xxx_),而访问器属性则直接命名xxx,有点代理的味道了
*/
</script>

不是小结的小结


顶不住了,别人画的是思维导图,我画的是些什么玩意er,可能我的思维比较混乱吧- -至少它在我脑海是这个布局

DOOOOOM

节点关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script>
/*
什么是节点,简单讲就是html标签(有点不严谨,确实不严谨),每个文档(document)是一个网页的根节点,这个根节点有一个唯一的子节点---<html>,也叫文档元素,所有的文档元素都在这个元素之内
节点之间两种关系:父子关系、同胞关系
每个节点都有一个childNodes和parentNodes属性,其中childNodes属性的值是一个NodeList类数组对象,存放的是一个有序节点(按照html标签的出现顺序),因为是一个类数组,我们可以用中括号或者item()的方法访问里面的元素
另外在childNodes里,每个childNode还有一个nextSibling和previousSibling属性,分别指向自己的后一个、前一个同胞元素
还有一点需要补充的是,父节点还有firstchild和lastChild两个属性,专门来访问第一个节点和最后一个节点
下面是简单的调试环节
*/

//获取文档元素html
let html = document.documentElement
let htmlChildren = html.childNodes
console.log(htmlChildren)//NodeList(3) [head, text, body]

let head = html.firstChild
console.log(head)

let body = html.lastChild
console.log(body)

let headChildren = htmlChildren[0].childNodes
console.log(headChildren)
</script>

操作节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 
操纵节点的一些方法
appendChild(), insertBefore(), replaceChild(), removeChild(),其中appendChild()是将元素插入到末尾,insertBefore()可以指定插入节点的位置, 剩下俩个不用过多解释- -
此外还有cloneNode()方法,通过传一个布尔参数来决定是否开启深复制,若为true则复制该节点以及其整个子DOM树,反之只复制该节点

normalize()方法,后续会进行补充...
*/

let ul = document.documentElement.lastChild.childNodes[1]
console.log(ul)

let li = document.createElement('li')
li.innerHTML = 5
ul.appendChild(li)

ul.removeChild(li)

//偷懒,就不演示了
</script>

定位元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 
getElementById(), getElementByTagName()等返回的都是一个HTMLCollection对象,它和前面的NodeList很像,唯一的区别就是前者有一个额外的方法namedItem(),通过标签的name属性取得某一项的引用

通过查阅相关资料,NodeList包含的是一个节点的所有节点(包括了元素节点),而HTMLCollection只包含其元素节点,通过下面的一个小例子就能看出它们之间的差别
*/
let htmlNodeList = document.documentElement.childNodes
console.log(htmlNodeList)//NodeList(3) [head, text, body]

let htmlHTMLCollection = document.documentElement.children
console.log(htmlHTMLCollection)//HTMLCollection(2) [head, body]

/*
我们能看到多出了个TEXT_NODE
*/
</script>

MutationObserver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script>
/*
这个强大的接口能够在DOM被修改时异步执行回调,使用这个接口能够观察文档,DOM树的一部分或者某个元素,或者元素属性、子节点、文本的变化

基本用法:
1.创建实例,并传入一个回调处理函数
2.使用observe()为该实例绑定观察对象,传两个参数,一个参数是指定要观察的DOM节点,第二个参数是一个配置对象,用于指定观察哪些方面的变化

每个回调都会收到一个MutationRecord实例的数组,这个数组记录了发生变化的相关信息

想想,貌似vue的$nextTick函数的实现原理好像就是这个...
*/

let observer = new MutationObserver(() => console.log('<body> 标签的属性被改变了!!!'))
observer.observe(document.body, {attributes: true})

document.body.className = 'foo'
console.log('改变body的css类名')

/* 我们来观察一下回调执行的时机
结果:
改变body的css类名
<body> 标签的属性被改变了!!! */
</script>

Selectors_API

1
2
3
4
5
6
7
8
9
10
11
12
<script>
/*
可以替代之前的getElementById()和getElementByTagName()
querySelector():接收CSS选择符参数,返回匹配的节点
querySelectorAll():接收CSS选择符参数,返回所有匹配的节点,所有实例包装在NodeList中
matches(): 接收CSS选择符参数,如果元素匹配则该选择符返回true,反之返回false
*/

console.log(document.body.querySelectorAll('p'))//NodeList(6) [p, p, p, p, p, p], 需要注意的是,这个返回的NodeList是一个“静态”的,并非“实时”的

console.log(document.body.matches('body.p'))
</script>

CSS类扩展

1
2
3
4
5
6
7
8
9
10
11
<script>
/*
classList属性,要操作类名,可通过此属性实现,HTML5提供了一下方法:
add(),contains(),remove(),toggle()
*/
let div = document.querySelector('.red')
div.classList.remove('red')

div.classList.add('green')
console.log(div.style)
</script>

插入标记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
/*
两个属性:
innerHTML:dddd
outerHTML:调用它的元素会被传入的HTML字符串解析生成的HTML DOM子树给取代
两个方法,都接收两个参数(插入标记的位置,要插入的HTML或者文本):
插入标记位置的可选参数:
1. beforebegin:元素之前,相当于在前面插入一个同胞节点
2. afterbegin:元素开头内部,相当于插入一个子节点
3. beforeend: 元素末尾内部,相当于插入一个子节点(末尾)
4. afterend: 元素之后,相当于在后面插入一个同胞节点
insertAdjacentHTML(),insertAdjacentText()
*/

</script>

遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<script>
/*
所谓遍历就是遍历DOM结构,有两种类型来遍历
1. NodeIterator
使用document.createNodeIterator()创建,该方法需要接收四个参数([要遍历根节点的节点],[要访问哪些节点],[过滤对象 || 过滤函数],[扩展实体引用])
(whatToShow)要访问哪些节点,这个参数对应的常量是在NodeFilter中定义的
关于过滤对象的使用:
过滤对象NodeFilter只有一个acceptNode()方法,具体实现下面会有
节点迭代器有两个主要的方法:nextNode(),previousNode()
2. TreeWalker,这可以说是前者的升级版,额外添加了不同方向遍历的方法:
parentNode()
firstChild()
lastChild()
nextSibling()
previousSibling()
*/

//定义一个过滤对象NodeFilter,该过滤对象表示只接收<p>元素的节点
let filter = {
acceptNode(node){
return node.tagName.toLowerCase() == 'p' ? NodeFilter.FILTER_ACCEPT: NodeFilter.FILTER_REJECT
}
}

let div = document.querySelector('.div1')
let iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, filter, false);
let node = iterator.nextNode()
while(node != null){
console.log(node.tagName)
node = iterator.nextNode()
}
</script>

待续…

函数

没有重载、有默认参数值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<script>
/*
js函数并不能像传统编程那样重载,因为没有签名(接收参数的类型和数量),因此如果定义了两个同名方法,则后面定义的会覆盖掉先定义的

在es6之后,能够支持显式定义默认参数,只要在函数定义中的参数后面用=就可以为参数赋值
值得一提的是,默认参数值不限于原始值和对象类型,也可以使用调用函数返回的值
*/

function addNum(num){
return num + 100
}
function addNum(num){
return num + 200
}
let result = addNum(30)
console.log(result)//230

function giveName(name = 'foo'){
return `halo, my name is ${name}`
}
let sentence = giveName()
let sentence2 = giveName('kitholt')
console.log(sentence)//halo, my name is foo
console.log(sentence2)//halo, my name is kitholt

//参数也存在于自己的作用域,它们不能引用函数体的作用域,下面这种操作如果不传第二个参数的话会报错
function makeKing(name = 'jack', numerals = defaultNumeral){
let defaultNumeral = '2'
return `King ${name} ${numerals}`
}
console.log(makeKing(undefined,2))//King jack 2
console.log(makeKing())//报错
</script>

函数声明与函数表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
/*
在前面我们知道,函数声明和函数表达式都可以定义一个函数,但是他们有个很重要的区别。
这个区别就是js引擎在执行任何代码之前,会先读取函数声明,然后再生成函数定义,而对于以函数表达式定义的函数只能等到代码执行到它那一行才会生成函数定义
*/

//使用函数声明定义函数
console.log(sayName())
function sayName(){
return 'my name is kitholt'
}
//使用函数表达式声明函数
console.log(sayAge())//Uncaught ReferenceError: Cannot access 'sayAge' before initialization at 函数声明与函数表达式.html:22
let sayAge = function(){
return '21'
}
</script>

扩展运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script>
/*
扩展运算符
1. 扩展参数:对可迭代对象应用扩展运算符,并将其作为一个参数传入,可以将可迭代对象拆分,并将迭代返回的每个值单独传入
2. 收集参数(我愿称之为“捡漏”),【...】收集参数的结果是一个Array实例,它只能作为最后一个参数传入,因为收集参数的结果是可变的,假如作为第一个参数传入,
那么所有的传入的参数都会被“打包”成Array实例,反之如果放在最后一个,它会“打包”剩余的参数

*/
function getSum(){
let num = 0;
for(let i = 0; i < arguments.length; i++){
num += arguments[i]
}
return num
}
let values = [1, 2, 3, 4]
console.log(getSum(...values))//对参数进行扩展运算

function ignoreFirst(firstValue, ...values){
console.log(values)
}
ignoreFirst()//[]
ignoreFirst(233)//[]
ignoreFirst(1,2,3)//[2, 3]
</script>

函数的this对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<script>
/*
在标准函数中,this引用的是把函数当成方法调用的上下文对象
关于this的指向问题,简单说就是谁是调用者,这个this就指向它

getId这个方法返回的是一个匿名函数,在匿名函数中的this是不会绑定到某个对象的,也就是说this会指向window
*/
window.id = 'Window'
let obj = {
id: 'obj',
getId(){
return function(){
return this.id
}
}
}
console.log(obj.getId()())//Window

/*
那如果得到obj的id呢?在内部函数的外面 将指向obj的this保存起来再在内部函数里引用就好了
*/
let obj1 = {
id: 'obj1',
getId(){
let that = this
return function(){
return that.id
}
}
}
console.log(obj1.getId()())//obj1
</script>

私有变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<script>
/*
任何定义在函数或者块中的变量,都可以认为是私有的
可以利用闭包实现私有变量的定义
*/
function MyObj(){
let privateVal = 10
function privateFun(){
return false
}

this.publicMethod = function(){
privateVal++
return privateFun()
}
}

let myObj = new MyObj()
console.log(myObj.privateVal)//undefined
myObj.privateFun()//私有变量.html:28 Uncaught TypeError: myObj.privateFun is not a function

/*
静态的私有变量,需要用匿名函数表达式 创建一个包含 构造函数 及其方法 的私有作用域
公有方法需要定义在原型上
*/

</script>

模块模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<script>
/*
模块模式使用一个匿名函数返回一个对象,在匿名函数内部,先定义私有变量和私有函数,之后,创建一个匿名函数要返回的对象字面量,这个对象只包含公开访问的属性和方法

比如在web开发中,经常要使用单例对象管理应用程序级的消息
*/

function Component(id){
this.id = id
}
//单例模式创建app
let app = function(){
let components = new Array()

//返回一个对象字面量
return {
getComponentCount(){
return components.length
},
registerComponent(component){
if(typeof component == 'object'){
components.push(component)
}
}
}
}()
console.log(app)
let c1 = new Component('1')
let c2 = new Component('2')
let c3 = new Component('3')
app.registerComponent(c1)
app.registerComponent(c2)
app.registerComponent(c3)
console.log(app)
console.log(app.getComponentCount())
</script>

结?(不完整)

事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<script>
/*
js函数并不能像传统编程那样重载,因为没有签名(接收参数的类型和数量),因此如果定义了两个同名方法,则后面定义的会覆盖掉先定义的

在es6之后,能够支持显式定义默认参数,只要在函数定义中的参数后面用=就可以为参数赋值
值得一提的是,默认参数值不限于原始值和对象类型,也可以使用调用函数返回的值
*/

function addNum(num){
return num + 100
}
function addNum(num){
return num + 200
}
let result = addNum(30)
console.log(result)//230

function giveName(name = 'foo'){
return `halo, my name is ${name}`
}
let sentence = giveName()
let sentence2 = giveName('kitholt')
console.log(sentence)//halo, my name is foo
console.log(sentence2)//halo, my name is kitholt

//参数也存在于自己的作用域,它们不能引用函数体的作用域,下面这种操作如果不传第二个参数的话会报错
function makeKing(name = 'jack', numerals = defaultNumeral){
let defaultNumeral = '2'
return `King ${name} ${numerals}`
}
console.log(makeKing(undefined,2))//King jack 2
console.log(makeKing())//报错
</script>

函数声明与函数表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
/*
在前面我们知道,函数声明和函数表达式都可以定义一个函数,但是他们有个很重要的区别。
这个区别就是js引擎在执行任何代码之前,会先读取函数声明,然后再生成函数定义,而对于以函数表达式定义的函数只能等到代码执行到它那一行才会生成函数定义
*/

//使用函数声明定义函数
console.log(sayName())
function sayName(){
return 'my name is kitholt'
}
//使用函数表达式声明函数
console.log(sayAge())//Uncaught ReferenceError: Cannot access 'sayAge' before initialization at 函数声明与函数表达式.html:22
let sayAge = function(){
return '21'
}
</script>

动画与Canvas图形

requestAnimationFrame

(下面介绍摘自阮一峰老师的博客)

设置这个API的目的是为了让各种网页动画效果(DOM动画、Canvas动画、SVG动画、WebGL动画)能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。代码中使用这个API,就是告诉浏览器希望执行一个动画,让浏览器在下一个动画帧安排一次网页重绘。

**requestAnimationFrame的优势,在于充分利用显示器的刷新机制,比较节省系统资源。**显示器有固定的刷新频率(60Hz或75Hz),也就是说,每秒最多只能重绘60次或75次,requestAnimationFrame的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。此外,使用这个API,一旦页面不处于浏览器的当前标签,就会自动停止刷新。这就节省了CPU、GPU和电力。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
/*
requestAnimationFrame,瞎翻译:请求动画帧函数
该方法接收一个参数:是要在重绘屏幕前调用的函数
值得注意的是这个参数函数还能接收一个参数,该参数表示下次重绘的时间(待补充...)

额外注意:dom元素的style对象和dom元素的css样式表两者是独立的,没有任何关系,JS只能操作或修改行内样式!!!
*/

var a = 0;
function step(){
a++;
console.log(a)
var g = requestAnimationFrame(step)
if(a>=100){
cancelAnimationFrame(g)
}
}
step()
</script>

该方法也会返回一个请求ID,可以用于通过另外一个方法cancelAnimationFrame()来取消任务,这有点像setTimeout()


补充要点:什么是回流和重绘?

  1. 回流:页面中元素尺寸、布局或者隐藏而需要重新构建页面

    以下操作会引起回流

    1. 添加或者删除可见的DOM元素;
    2. 元素尺寸改变——边距、填充、边框、宽度和高度
    3. 内容变化,比如用户在input框中输入文字
    4. 浏览器窗口尺寸改变——resize事件发生时
    5. 计算 offsetWidth 和 offsetHeight 属性
  2. 重绘:元素的外观发生改变,但是布局没有发生改变

在页面设计中药尽量减少回流和重绘的次数,这就是所谓的性能优化(高阶操作)

  • 改变样式尽量集中改变,可以使用class属性给多个元素同时添加
  • position属性为absolute或fixed的元素,重排开销比较小,不用考虑它对其他元素的影响
  • 优化动画- -

绘制矩形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<canvas class="drawing" width="800" height="300" style="background-color: honeydew;"></canvas>
<script>
/*
创建画布:<canvas></canvas>
获取画布的上下文:getContext(),根据接收的参数来决定获取的是什么类型的画布上下文对象,比如传入'2d'---一定要传参,不然会报错

两个基本属性:填充(fillStyle)和描边(strokeStyle)

矩形是唯一一个可以直接在2D画布中直接绘制的形状
相关方法(都接受四个参数:x坐标,y坐标,矩形宽度,矩形高度):
1. fillRect()
2. strokeRect()
3. clearRect() 清除画布某个区域,可以用于“打孔”

注意:
1. lineWidth:描边宽度
2. lineCap控制线条端点['butt', 'round', 'square']
3. lineJoin控制线条交点的形状['round', 'bevel(取平?)', 'miter(出尖)']

*/

let drawing = document.querySelector('.drawing')
let context = drawing.getContext('2d')

//画一个红色矩形
context.fillStyle = 'red'
context.fillRect(10, 10, 50, 50)
//给这个红色的矩形描一下边
context.strokeStyle = 'black'
//设置描边宽度
context.lineWidth = 10

context.strokeRect(10, 10, 50, 50)
//再画一个透明的蓝色矩形
context.fillStyle = 'rgba(0, 0, 255, 0.5)'
context.fillRect(30, 30, 50, 50)

//在这两个矩形重叠的地方打个“孔”
context.clearRect(40, 40, 10, 10)
</script>

绘制路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<script>
/*
Path路径,通过路径可以创建复杂的形状和线条。绘制路径的一般流程是
1. 创建路径beginPath()
2. 开始操作arc(), arc(), lineTo(), moveTo()...
3. 结束路径closePath()
4. 描画路径

绘制文本(接收参数:要绘制的字符串,x坐标,y坐标,可选的最大像素宽度):
1. fillText()
2. strokeText()
*/

let drawing = document.querySelector('.drawing')
let context = drawing.getContext('2d')
//现在来尝试画一个不带数字的表盘
//先将“画笔”移到画布中央,此时原点(0,0)被移动此处
context.beginPath()
context.translate(400, 400)
//半径为400px,画一个圆
context.arc(0, 0, 400, 0, 2 * Math.PI, false)
//半径为390px,画一个内圆
context.arc(0, 0, 390, 0, 2 * Math.PI, false)

context.rotate(1)

//画一个时针
context.moveTo(0, 0)
context.lineTo(0, -360)
//画一个分针
context.moveTo(0, 0)
context.lineTo(-330, 0)
//结束路径
context.closePath()
//描画路径
context.stroke()

//给表盘顶部添加数字12
context.font = 'bold 14px Arial'
context.textAlign = 'center'
context.textBaseline = 'middle'
context.fillText('12', 0, -375)

</script>

阴影

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
/*
context这个笔可以根据以下属性自动为已有形状或路径生成阴影
shadowColor,shadowOffsetX,shadowOffsetY,shadowBlur
如何使用:只要在绘制图形或者路径前设置好适当的值即可
*/

let drawing = document.querySelector('.drawing')
let context = drawing.getContext('2d')

context.shadowOffsetX = 5
context.shadowOffsetY = 5
context.shadowColor = 'green'

//画一个红色矩形
context.fillStyle = 'red'
context.fillRect(10, 10, 50, 50)
</script>

渐变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<script>
/*
标准画布的兄弟---线性渐变画布
let gradient = context.createLinearGradient(x1, y1, x2, y2)
四个参数:起始xy坐标和终点xy坐标

调用addColorStop()为渐变指定颜色,接收两个参数,第一个表示色标位置,范围是0-1,第二个是css颜色字符串

之后将这个对象赋值给fillStyle或者strokeStyle

忘了说了,有线性渐变就自然有径向渐变createRadialGradient(),接收六个参数,前三个指定起点圆形的xy坐标和半径,后三个指定终点圆形的...
*/
let drawing = document.querySelector('.drawing')
let context = drawing.getContext('2d')

//创建渐变画布
let gradient = context.createLinearGradient(0, 0, 800, 300)
gradient.addColorStop(0,'red')
gradient.addColorStop(0.5,'yellow')
gradient.addColorStop(0.6,'pink')
gradient.addColorStop(1,'blue')
//线性渐变填充,这里会占画布的1/2
context.fillStyle = gradient
context.fillRect(0, 0, 400, 300)


//创建径向渐变
let radialGradient = context.createRadialGradient(600, 150, 90, 490, 150, 200)
radialGradient.addColorStop(0,'white')
radialGradient.addColorStop(0.5,'purple')
radialGradient.addColorStop(1,'black')
//径向渐变填充
context.fillStyle = radialGradient
context.fillRect(400, 0, 400, 300)
</script>

图案

1
2
3
4
5
6
7
8
<script>
/*
如何使用:
1. createPattern(),接收两个参数:一个HTML<img>元素和如何重复图像的字符串
2. 将填充样式设置为图案,context.fillStyle = pattern
3. 调用fillRect方法
*/
</script>

图像数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<canvas class="drawing" width="2000" height="2000"></canvas>
<img src="img/img1.jpg" alt="" width="500" height="300" id="img1">
<script>

/*
使用getImageData()获取画布上原始图像数据,该方法返回一个ImageData实例,有三个属性:width,height和data,data是一个包含图像所有的原始像素信息的数组
每个像素在数组中都有4个值表示分别表示红绿蓝透明,比如说第一个像素的信息是在数组第0到第3个值中,后面的以此类推

一个小应用:灰阶过滤器
*/

let drawing = document.querySelector('.drawing')
let ctx = drawing.getContext('2d')
let image, imageData, red, green, blue, len, average
//console.log(ctx)
image = document.getElementById('img1')




//在画布上绘制图像
window.onload = function(){
ctx.drawImage(image, 0, 0, 500, 300)
//取得图像数据
imageData = ctx.getImageData(0, 0, image.width, image.height)
data = imageData.data
len = data.length
for(let i = 0; i < len; i += 4){
red = data[i]
green = data[i+1]
blue = data[i+2]
//三颜色求平均值
average = Math.floor((red + green + blue) / 3)

//相当于过滤掉了颜色信息,只留下灰度信息(我不太知道这是啥原理...)
data[i] = average
data[i+1] = average
data[i+2] = average
}
//将修改的数据写回,并在画布上重新显示
imageData.data = data
ctx.putImageData(imageData, 0, 0)
}




/*
在完成这个应用的过程中遇到一些小坑
一般图片的获取都是需要一定时间的,而drawImage()这个方法是同步执行的,如果图片还没加载完毕,画布就显示不出来图片
解决方法:
1. 对图片的操作全都放在window.onload里面
2. 使用promise异步完成,当图片加载完毕时在调用drawImage()--------------------------------------出错了,暂时找不到- -
下面使用promise来尝试解决
*/

/* let promise = new Promise((resolve, reject) =>{
if(image){
resolve()
}else{
console.log('图片还未加载完成...')
}
})
promise.then(() => {
//绘制图像
ctx.drawImage(image, 0, 0, 500, 300)
console.log(image.width)
//取得图像数据
imageData = ctx.getImageData(0, 0, image.width, image.height)
data = imageData.data
console.log("23333",data)
len = data.length
for(let i = 0; i < len; i += 4){
red = data[i]
green = data[i+1]
blue = data[i+2]
//三颜色求平均值
average = Math.floor((red + green + blue) / 3)

//相当于过滤掉了颜色信息,只留下灰度信息(我不太知道这是啥原理...)
data[i] = average
data[i+1] = average
data[i+2] = average
}
//将修改的数据写回,并在画布上重新显示
imageData.data = data
ctx.putImageData(imageData, 0, 0)
}) */
</script>

合成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<script>
/*
globalAlpha:设置所有绘制内容的透明度,取值0-1
globalCompositeOperation:表示新绘制的形状如何与画布中已有的形状融合
*/

let drawing = document.querySelector('.drawing')
let ctx = drawing.getContext('2d')
//globalAlpha的例子
ctx.fillStyle = 'red'
ctx.fillRect(0, 0, 80, 80)

//改变全局透明度
ctx.globalAlpha = 0.5

ctx.fillStyle = 'blue'
ctx.fillRect(60, 60, 60, 60)

//恢复全局透明度
ctx.globalAlpha = 1

ctx.fillStyle = 'red'
ctx.fillRect(500, 500, 80, 80)

//红色矩形会出现在蓝色矩形上面
ctx.globalCompositeOperation = 'destination-over'

ctx.fillStyle = 'blue'
ctx.fillRect(560, 560, 60, 60)

</script>

小demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<script>
/*
实现动画的三个基本步骤:清除画布---更新状态(位置)---渲染
目的:实现一个鼠标移动特效,鼠标在移动的过程中周围会有许多彩色小球在鼠标周围
*/
//获取画布,对画布进行初始化操作,比如将画布的宽高设为和浏览器窗口的一样
let drawing = document.querySelector('.drawing')
let ctx = drawing.getContext('2d')
drawing.width = window.innerWidth
drawing.height = window.innerHeight
/*
需要用到的全局变量
1. 用于存放小球的数组ballArr
2. 圆周率π
*/
let ballArr = []
let P = Math.PI
/*
创建小球:
小球有自己的圆心(xy坐标),以及用于模拟小球随机偏移的偏移量(dxdy),半径(radius),颜色(color),其中颜色属性使用一个随机函数getRandomColor()获取
更新自身位置的函数updatePosition(),以及一个用于显示小球的渲染显示函数renderBall()
*/
function Ball(x, y, r){
this.positionX = x
this.positionY = y
this.dx = parseInt(Math.random()*8 -4)
this.dy = parseInt(Math.random()*8 -4)
this.radius = r
this.color = this.getRandomColor()

//每创建一个小球,都将其加入小球数组,便于管理
ballArr.push(this)
}
//赋予每个小球不同的颜色,颜色字符串的六个数字通过随机数生成然后进行拼串操作得到的
Ball.prototype.getRandomColor = function(){
let color = '#'
for(let i = 0; i < 6; i++){
let randomColorNum = Math.floor(Math.random() * 9)
color += randomColorNum
}
return color
}
/*
更新每个小球的位置状态, 每次移动小球都会逐渐变小,直至“消失”,小球消失的依据就是它的半径是否为负数
如果为负数就将其从小球数组中删除, 需要一个removeBall()函数
*/
Ball.prototype.updatePosition = function(){
this.positionX += this.dx
this.positionY += this.dy
this.radius -= 0.3
if(this.radius < 0){
this.removeBall()
}
}
Ball.prototype.removeBall = function(){
for(let i = 0; i < ballArr.length; i++){
if(ballArr[i] === this){
ballArr.splice(i,1)
}
}
}
//渲染显示函数renderBall(), 开始使用ctx对象
Ball.prototype.renderBall = function(){
ctx.beginPath()
ctx.fillStyle = this.color
ctx.arc(this.positionX, this.positionY, this.radius, 0, 2 * P, false)
ctx.fillRect(this.positionX + 10, this.positionY + 10, this.radius, this.radius)
ctx.fill()
}

/*
给鼠标绑定相关事件,以及执行动画操作
在这里需要注意的是,在更新每个小球的位置时,小球有可能会因为半径缩减为负数而从数组中被移除,因此在renderBall之前需要判断当前小球是否还在数组中

我之前就是直接updatePosition()后就调用renderBall(),结果就是动画效果只能执行一次
*/
drawing.addEventListener('mousemove', function(event){
new Ball(event.offsetX, event.offsetY, 15)
})
drawing.addEventListener('click', function(event){
for(let i = 0; i < 15; i++){
new Ball(event.offsetX, event.offsetY, 15)
}
})
function animation(){
ctx.clearRect(0, 0, drawing.width, drawing.height)
for(let i = 0; i < ballArr.length; i++){
ballArr[i].updatePosition()
if(ballArr[i]){
ballArr[i].renderBall()
}
}
requestAnimationFrame(animation)
}
animation()
</script>

ES67891011(特性

解构赋值

允许按照一定模式从数组和对象中提取值,对变量进行赋值

1
2
3
4
5
6
7
8
9
10
11
const group = ['张三','李四','王五',‘赵六];
let [zhang, li, wang, zhao] = group

const pepople = {
name: 'Lee',
age: 18;
say: function(){
console.log('hello, my name is Lee');
}
};
let {say} = people;

模板字符串

(`)反引号

  1. 可直接出现换行符
  2. 在模板字符串里,变量使用${变量名}进行拼接

简化对象写法

允许在大括号里面直接写入变量和函数,作为对象的属性和方法

箭头函数

1
let add = n => n+n
  1. 箭头函数的this是静态的,始终指向函数声明所在的作用域下的this的值,并不会随着调用者的不用而不同
  2. 不能作为构造函数实例化对象
  3. 不能使用argument
  4. 当只有一个形参时,可以省略小括号
  5. 此外,当代码只有一句时,可以省略return

扩展运算符(…)

能将数组转换为逗号分隔的参数序列

  1. 数组合并
  2. 数组克隆
  3. 将伪数组转为真数组

Symbol(符号)

符号实例是唯一的,用于标识对象的唯一属性

Symbol不能new,只能使用Symbol()初始化

Symbol没有字面量语法


注意区分let a = Symbol()和let a = Symbol.for();

全局注册表中定义的符号和使用Symbol()定义的符号并不相同

迭代器(Iterator)

通俗说就是遍历,可迭代对象通过实现iterable接口,而且可以通过迭代器Iterator来消费

工作原理:每个可迭代对象都有一个Symbol.Iterator的属性,这个属性是一个方法

  1. 它会创建一个指针对象,指向当前数据结构的起始位置
  2. 第一次调用对象的next方法,指针会后移指向数据结构的第一个成员
  3. 不断调用next,指针不断后移,直到到头
  4. 每调用next方法返回一个包含value和done的属性对象

生成器(generator)

1
2
3
4
5
6
function* generatorFn(){
//code1
yield param1;
//code2
yield param2
}

function*表示这是一个生成器函数

值得一提的是,调用这个函数并不会立即执行,它会返回一个生成器对象

1
2
3
4
5
6
7
8
9
function* generatorFn(){
//code1
yield param1;
//code2
yield param2
}

//得到生成器函数
let gen = generatorFn();

yield的作用

yield有产出、产量、返回的意思,因此不难理解yield就相当于生成器里的return关键字。也就是说生成器函数能够返回多次(执行多次)

next()

它实现了iterator接口,因此也有next()方法,调用该方法能执行每个yield之前的代码,并且next()也可传入参数,该参数会覆盖上一个yield返回的值

promise(一个对象)

用于封装异步操作

promise的出现是为了解决回调地狱

Promise是一个构造函数,通过new关键字实例化对象

语法

1
new Promise((resolve, reject) => {});
  • Promise接受一个函数作为参数(参数函数)
  • 在参数函数中接受两个参数:
    • resolve
    • reject

promise实例

  • state:状态

    1. pending(准备)

    2. fulfilled(成功)

    3. rejected(失败)

      通过调用resolve()和reject()来改变promise对象的状态(分别对应fulfilled和rejected)

  • result:结果

promise状态的改变是一次性的

promise的结果

1
2
3
4
5
6
7
8
9
10
11
12
//创建promise对象
const p = new Promise((resolve,reject)=>{
setTimeout(() => {
resolve('用户数据');
}, 1000);
});

p.then((value) =>{
console.log(value+'访问成功');
}).catch(()=>{
console.log('访问失败');
});

promise的方法

  • then方法

    • 参数是两个函数

    • 它的返回值是一个新的promise对象(且状态为pending)

1
2
3
4
5
6
7
8
9
<script>
const p = new Promise((resolve,reject) => {
resolve('yes');
// reject();
});
//当p的状态是fulfilled时,执行第一个参数函数,打印‘成功调用’
p.then(() => {console.log('成功调用')}, () => {console.log('调用失败...')});
console.log(p);
</script>

注意:如果promise的状态不改变,then里的方法不会执行

解决:使用return可以将实例的状态改成fulfilled

1
2
3
4
5
6
7
8
9
10
11
12
const p = new Promise((resolve,reject) => {
resolve();
});

const t = p.then(() =>{
console.log('success');
return 233;
});
//值得一提的是,return不仅能够将新的promise的状态改变成fulfilled,而且还能够将return后面的值传递出去,作为链式调用的下一个then方法的参数
t.then((value) => {//此时的value为上面的233
//code
});
  • catch方法

    1
    2
    3
    4
    5
    6
    7
    const p = new Promise((resolve,reject) => {
    rejected();
    });

    const t = p.catch((reason) =>{
    console.log('reason:'+reason);
    });
    1. 当promise的状态改为rejected时,会执行
    2. 当promise的执行体中出现代码错误,会执行

数值扩展

Number.EPSILON是js表示的最小精度(其值为2.2204460……)【小数点后十六位】

在两个数比较大小时,如果这两个数的绝对值小于Number.EPSILON,则认为这两个数是相等的(有点像高数里的极限)

1
2
3
4
5
6
7
8
function equal(a, b){
if(Math.abs(a-b) < Number.EPSILON){
return true;
}
else{
return false;
}
}
  • Number.isFinite

    用于判断一个数值是否为有限数

  • Number.isNaN

    检测一个数值是否为NaN

  • Number.parseInt Number.parseFloat

    将字符串转为整型或者浮点数

  • Number.isInteger

  • Math.trunc

    将数字的小数部分去掉

    值得一提的是trunc是truncate(截断)的缩写

  • Math.sign

    判断一个数是正数 负数 还是零

    正数返回1,负数返回-1,零返回0

对象扩展

  • Object.assign(target, source)

两个对象target,source,source会覆盖target的对象属性

  • Object.setPrototypeOf()

    设置对象的原型

模块化

模块化是指将一个大的文件拆分成许多小文件,然后将小文件结合起来

优点:

  1. 防止命名冲突
  2. 代码复用
  3. 高维护性

模块化语法

export:向外暴露接口

import:引入其他模块提供的功能

多种暴露的方式

  • 分别暴露

    1
    2
    export let param = 0
    export function fun(){};
  • 统一暴露

    1
    2
    3
     let param = 0
    function fun(){};
    export {param, fun}
  • 默认暴露

    1
    2
    3
    export default{
    //你要暴露的数据,一般为对象
    }

多种导入方式

  • 通用导入方式
1
2
3
<script type="module">
import * as m1 from "js文件路径";
</script>
  • 解构赋值形式

    1
    2
    3
    4
    5
    6
    7
    //第一种解构赋值
    import {想要从模块获取的变量或方法} from 'js路径'
    //第二种解构赋值,如果出现了导入的变量重名的情况,可以想数据库的select操作一样,用as给变量重命名
    import {a} from 'js路径'
    import {a as bb} from 'js路径'
    //第三种,当暴露方式为默认暴露时,default必须起别名,因为default是一个关键字
    import {default as m} from ‘./m.js
  • 简便形式(只针对默认暴露)

    1
    import mm from ‘./mm.js

ES8

async && await

async函数

  1. async函数的返回值为promise对象
  2. promise对象的结果有async函数执行的返回值决定

await表达式

  1. await必须写在async函数中
  2. 其右侧的表达式一般为promise对象
  3. await返回的是promise成功的值
  4. await的promise失败了,就会抛出异常,需要通过try-catch捕获处理

对象方法扩展

  • Object.values()

    返回一个给定对象的所有可枚举属性值的数组

  • Object.entries()

    返回一个给定对象自身可遍历属性[key, value]的数组

  • Object.getOwnPropertyDescriptor()

    返回指定对象所有自身属性的描述对象

手撕Promise源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* 手写一个promise(前端面试可能会要求手撕promise)
* 关于自定义一个promise,首先promise有两个属性一个是PromiseState,PromiseResult
* resolve,reject方法
* 以及十分重要的then方法
* ①
* 并且为了保证每一个promise的状态只能改变一次,需要在调用resolve和reject方法时进行判断,如果不为pending则改变状态
* ②
* 在异步任务执行回调的过程中,会出现then的方法执行不了的问题,这是因为同步代码会按照代码的书写顺序执行,而异步代码还没执行,此时的PromiseSate的状态还未改变(pending),因此在执行then方法的时候无法判断
* 此时需要在then方法中增加判断是否为pending的语句,并且将成功或者失败才调用的回调函数绑定在Promise对象上
*
* 通过对Promise源码的复盘,我明白了回调函数执行真正的位置是在resolve和reject中,而then方法的作用仅仅是接受并且绑定回调函数
* (未完)
*/

function Promise(executor){
//设置两个属性
this.PromiseState = 'pending';
this.PromiseResult = null;

//保存成功或者失败的回调函数
this.callback = {};
//保存Promise的this
const _self = this;

//设置resolve函数
function resolve(data){
if(_self.PromiseState != 'pending') return;
_self.PromiseState = 'fulfilled';
_self.PromiseResult = data;
if(_self.callback.onResolved){//如果已经绑定了 成功状态后才执行的回调函数
_self.callback.onResolved(data);
}
}

//设置reject函数
function reject(data){
if(_self.PromiseState != 'pending') return;
_self.PromiseState = 'rejected';
_self.PromiseResult = data;
if(_self.callback.onRejected){//如果绑定了 失败状态后才执行的回调函数
_self.callback.onRejected(data);
}
}

//设置并调用执行器函数
executor(resolve, reject);

//定义then函数,将它绑定在promise的原型上
Promise.prototype.then = function(onResolved, onRejected){
if(this.PromiseState === 'fulfilled'){//如果同步事件,且状态为成功,对应的回调函数会立刻在then里执行
onResolved(this.PromiseResult);
}
if(this.PromiseState === 'rejected'){//如果同步事件,且状态为失败,对应的回调函数会立刻在then里执行
onRejected(this.PromiseResult);
}
if(this.PromiseState === 'pending'){//如果为异步事件,在执行then方法时的状态必为pending,因此需要将回调函数保存起来,并在resolve或者reject中执行
this.callback = {
onRejected,onResolved
}
}
}

}

改进一

但是,我们发现使用上述代码时,如果调用多个then方法,会出现只执行最后一个then方法的结果,这是因为上述代码保存callback是以对象的形式保存的,每执行一次then,后一个then保存的callback会覆盖掉前一个保存的。因此代码可以进一步改进,就是将callback保存的形式改为数组,在调用时直接遍历,下面放上改进代码

1
2
3
4
5
6
7
8
9
10
11
12
//将callback改为数组
this.callbacks = [];

//在resolve最后一行改为
_self.callbacks.forEach((item) => {item.onResolved(data)});
//在reject最后一行改为
_self.callbacks.forEach((item) => {item.onRejected(data)});

//then方法的判断pending状态的代码改为
this.callbacks.push({
onRejected,onResolved
})

改进二

同步修改状态then方法结果返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Promise.prototype.then = function(onResolved, onRejected){
return new Promise((resolve, reject) =>{
if(this.PromiseState === 'fulfilled'){//如果同步事件,且状态为成功,对应的回调函数会立刻在then里执行
let result = onResolved(this.PromiseResult);
if(result instanceof Promise){
result.then(v =>{
resolve(v);
},r =>{
reject(r);
});
}
else{
resolve(result);
}
}
if(this.PromiseState === 'rejected'){//如果同步事件,且状态为失败,对应的回调函数会立刻在then里执行
onRejected(this.PromiseResult);
}
if(this.PromiseState === 'pending'){//如果为异步事件,在执行then方法时的状态必为pending,因此需要将回调函数保存起来,并在resolve或者reject中执行
this.callbacks.push({
onRejected,onResolved
})
}
})
}

then方法返回的结果是一个promise对象,而这个promise对象的结果是由其指定的回调函数决定的

  1. 如果抛出异常,返回的promise变为rejected,reason为抛出的异常
  2. 如果返回的是非promise的任意值,返回的promise的值变为resolved,value为返回的值
  3. 如果返回的是另一个新的promise,则这个的promise的结果会变成返回的promise的结果

异步修改状态then方法结果返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
Promise.prototype.then = function(onResolved, onRejected){
const self = this;
return new Promise((resolve, reject) =>{
if(this.PromiseState === 'fulfilled'){//如果同步事件,且状态为成功,对应的回调函数会立刻在then里执行
let result = onResolved(this.PromiseResult);
if(result instanceof Promise){
result.then(v =>{
resolve(v);
},r =>{
reject(r);
});
}
else{
resolve(result);
}
}
if(this.PromiseState === 'rejected'){//如果同步事件,且状态为失败,对应的回调函数会立刻在then里执行
let result = onRejected(this.PromiseResult);
if(result instanceof Promise){
result.then(v =>{
resolve(v);
},r =>{
reject(r);
});
}
else{
reject(result);
}

}
if(this.PromiseState === 'pending'){//如果为异步事件,在执行then方法时的状态必为pending,因此需要将回调函数保存起来,并在resolve或者reject中执行
this.callbacks.push({
onResolved: function(){
let result = onResolved(self.PromiseResult);
if(result instanceof Promise){
result.then(v =>{
resolve(v);
},r =>{
reject(r);
});
}
else{
resolve(result);
}
},
onRejected: function(){
let result = onRejected(self.PromiseResult);
if(result instanceof Promise){
result.then(v =>{
resolve(v);
},r =>{
reject(r);
});
}
else{
resolve(result);
}
}
})
}
})
}

注:未完待续,回头再研究其他方法的源码

axios

定义:Promise based HTTP client for the browser and node.js

用于发送ajax请求(browser)

用于发送http请求(node.js)

可以使用 函数axios()发送ajax请求

1
2
3
4
5
6
7
8
9
//为按钮绑定发送get请求的事件
btns[0].onclick = function(){
axios({//该函数接收一个对象作为参数
method: 'GET',
url: 'http://localhost:3000/comments/1'
}).then(repsonse => {
console.log(repsonse);
})
};

除了发送get请求,还可发送post,put,delete请求

另外,除了使用上述的方法发送请求,还可以使用axios对象发送请求

axios.request(config)

axios.post(url, data, config)

需要注意的是config指的是一个配置对象,我们可以设置一些属性,比如:请求方式,baseURL,设置超时等

创建实例对象发送请求

我们可以使用axios的create()方法,该方法接收一个配置对象config,并返回一个与axios相近的对象

Q:为什么要创建实例对象发送请求呢?

A:在实际项目中,页面一般会从不同的服务器发送请求,axios的配置对象就会随着服务器的不同而变化。因此创建不同的实例对象,每个实例对象负责一个服务器,各司其职

axios拦截器

axios取消请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let cancel = null;
btns[4].onclick = function(){
if(cancel != null){//检测上一次的请求是否已经完成
cancel();//取消上一次的请求
};
axios({
method: 'GET',
url: 'http://localhost:3000/comments/1',
//给配置对象添加属性,该属性用于取消请求
cancelToken: new axios.CancelToken(function(c){//通过该回调函数能拿到一个cancel对象
cancel = c;
})
}).then(repsonse => {
console.log(repsonse);
console.log(cancel);
cancel = null;//cancel的值初始化
});
};