走进 JavaScript 世界

JavaScript 是一门基于对象事件驱动弱类型脚本语言

  1. 机器语言
  2. 汇编语言
  3. 高级语言
    • 面向过程 C
    • 面向对象 C++
    • 函数式
    • 声明式
  • 脚本语言动态语言

    • 相较于编译语言,不需要编译器,它需要的是解释器
    • 编译器面向的是计算机
    • 解释器面向的是某个特定的软件或者计算机某一个部分
  1. HTML 结构
  2. CSS 美化
  3. JavaScript 行为

浏览器内核

  • 渲染引擎
    JS引擎越来越独立,内核就倾向于只指渲染引擎
  • JS 引擎

    解析和执行 javascript 来实现网页的动态效果

组成

  • ECMAScript ECMA262 标准
    1. JS语法和基本对象
  • JS-Web-API W3C 标准
    1. DOM文档对象模型
    2. BOM浏览器对象模型
    3. 事件绑定
    4. Ajax
    5. 存储

引用方式

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
<!-- 行间 -->
<a
onclick="alert('Hello World!')"
id="fuck"
href="javascript:void(0);"
>
Hello World!
</a>

<!-- 内部 -->
<script type="text/javascript">
/*
document.getElementById('ID')
document / parent.querySelector('选择器')
*/
const elem = document.getElementById('fuck')

elem.onclick = function() {} // DOM0 级
elem.addEventListener('click', function() {}, false) // DOM2 级
elem.addEventListener('keyup', function() {}, false) // DOM3 级
</script>

<!-- 外部 -->
<script src="main.js" type="text/javascript"></script>

变量命名

  • 不允许数字开头
  • 不允许使用关键字和保留字
  • 使用字母,数字,下划线(_),美元符($)任意组合而成

属性操作

  • .点运算符
  • []方括号运算符

常用属性

  • id
  • className
  • value
  • style
    • width
    • height
    • color
    • fontSize
    • background
    • cssText
  • innerText
  • innerHTML
  • href
  • src
  • tagName
  • classList
    • add()
    • remove()
    • contains()
    • toggle()
  • href 值和 src 值获取到的是绝对路径
  • style 是行间属性
  • cssText 会替换掉当前所有的行间属性
  • class 是保留字,改成 className
  • tagName 获取到的是大写字母
  • 属性中不允许出现-,font-size 改成 fontSize

数据类型

  • 基本类型 string number boolean null undefined symbol
    • 传值值类型
    • 储存在内存中 线性表
  • 复杂类型 object
    • 传址引用类型
      • 浅拷贝
        • Array.prototype.slice
        • Object.assign()
        • ...展开运算符
      • 深拷贝
        • JSON.parse(JSON.stringify(object)) 👎

          • 循环引用会报错
          • 对象的原 constructor 丢失,全指向 Object
          • function RegExp undefined symbol 会被忽略
          • Map Set
        • MessageChannel

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          function structuralClone(obj) {
          return new Promise(resolve => {
          const { port1, port2 } = new MessageChannel()
          port2.onmessage = ev => resolve(ev.data)
          port1.postMessage(obj)
          })
          }

          var obj = {
          a: 1,
          b: {
          c: 2
          }
          }
          obj.b.d = obj.b

          // 解决 undefined 和 循环引用 问题
          const test = async () => {
          const clone = await structuralClone(obj)
          console.log(clone)
          }
          test()
        • jquery.extend()

        • lodash 的 deepClone()

    • 储存在内存中 树形结构

typeof 运算符

function object
string number boolean undefined symbol bigint

  • typeof只能检测出普通变量类型, 除function外, 都是object
  • typeof + instanceof
1
2
3
4
5
6
7
8
9
10
typeof 'abc' // string
typeof 123 // number
typeof true // boolean
typeof {} // object
typeof [] // object
typeof null // object (JS Bug)
typeof undefined // undefined
typeof console.log // function
typeof Symbol() // symbol
typeof 1n // bigint

强制数据类型转换

  • String()

    • 数字类型返回对应的字符串数字
    • 布尔值返回字符串 true/false
    • null返回字符串 null
    • undefined返回字符串 undefined
    • 数组[1, 2, , 3, [4, [5]], { a: ‘b’ }]
      • ‘1,2,,3,4,5,[object Object]’
    • 函数把整个函数变成字符串返回
    • 对象调用对象的 toString 方法 ‘[object Object]’
  • Number()

    • 字符串

      • 字符串数字, 返回对应的数字
      • 空字符串, 返回 0
      • 其余情况, 返回 NaN
    • 布尔值 true 返回 1,false 返回 0

    • null 返回 0

    • undefined 返回 NaN

    • parseInt() parseFloat()

      • 二进制
      • 十进制
      • 八进制 第一位是0o
      • 十六进制 第一位是0x
      1
      2
      3
      4
      5
      6
      7
      parseInt('0x1F') === parseInt('1F', 16)

      ['1', '2', '3'].map(parseInt) // [1, NaN, NaN]

      parseInt('1', 0) // 1 ,radix 为 0 按十进制处理
      parseInt('2', 1) // NaN ,radix 为 1 非法(不在 2-36 之内)
      parseInt('3', 2) // NaN ,二进制中没有 3
    • 对象先调用对象的 valueOf 方法进行检测,检测结果是 NaN,然后调用对象的 toString 方法,再根据上述规则进行转换

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      let a = {
      valueOf() {
      return 0
      },
      toString() {
      return '1'
      },
      /* [Symbol.toPrimitive]() {
      return 'overwrite'
      } */
      }
      console.log(1 + a)
  • Boolean()👈👈

运算符

数学运算符

+加、-减、*乘、/除、%取模、++递增、--递减、+正号、-负号

加性操作符

  • +加法

    • 当左右两侧的操作数,任何一侧为字符串,另一侧非字符串的类型则会转为字符串,进行字符串的拼接
    • 当左右两侧都为 Number 类型的情况下,按照如下规则返回结果:
      • 左右两侧均为数值,执行常规的加法计算
      • 如果有一个操作数是 NaN,则结果返回 NaN
    • 如果操作数是 Boolean、null、undefined,则会(根据对应的规则 Number())转为数字类型,再进行计算
    • 4 + [1, 2, 3] 👉 ‘41,2,3’
    • 加法运算符
    • 字符串拼接
    • 正号(一元加运算符)
  • -减法

乘性操作符

  • *乘法
  • /除法
  • %取模

一元操作符

只能操作一个值的操作符叫做一元操作符

  • +一元加运算符
    • 如果操作数是 Number 类型的时候,正号放在数值前面,对其完全没有任何影响
    • 如果操作数是 String、Boolean、null、undefined,则会(根据对应的规则 Number())转为数字类型
  • -一元减运算符

递增、递减运算符

  • 前置型
    • ++a
    • --b
  • 后置型
    • b++
    • a--

浮点数精度问题

1
2
console.log(0.1 + 0.2) // 0.30000000000000004
console.log((0.1 * 10 + 0.2 * 10) / 10) // 0.3

赋值运算符

=等号、+=加等、-=减等、*=乘等、/=除等、%=模等

比较运算符

关系操作符

<小于、>大于、<=小于等于、>= 大于等于

相等操作符

==相等、!=不等、===全等、!==全不等

=== 全等比较

  1. 数据类型
    • Object 比较地址是否一致

== 隐式类型转换

  • 如果两边都是 String 类型,比较Unicode编码
  • 如果两边都是 Number 类型,比较数值是否相同
  • null == undefined
  • 如果有一侧是 NaN,则结果得到 false
    • NaN 不等于任何类型的数值,包括自己本身NaN != NaN
  • 如果一侧是 String、Boolean、null、undefined、Object,则会(根据对应的规则 Number())转为数字类型,再进行比较

何时使用 == 何时使用 ===

1
2
3
4
5
6
7
if (obj.a == null) {
// 这里相当于 obj.a === null || obj.a === undefined
// 这是 jQuery 源码中推荐的写法

其它情况使用尽量都使用 ===

}

逻辑运算符

短路

  • && 与
  • || 或

作用在一个条件上,所以也是一元运算符的成员之一,表示对该操作数进行取反操作,会对该数据进行隐式类型转换为布尔值

  • 如果操作数是非空字符串,返回 !true
  • 如果操作数是字符串0,返回 !true
  • 如果操作数是数字0,返回 !false
  • 如果操作数是 NaN,返回 !false
  • 如果操作数是 null 或 undefined,返回 !false
  • 如果操作数是 Object 类型,返回 !true
    • 所有对象均为 true,包括 空数组 []、空对象 {}
    • [] == false
  • !!a等同于Boolean(a)

运算符优先级

流程控制

顺序结构

if 分支

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
if () {

} else if () {

} else {

}

switch (表达式/变量) {
// 全等比较
case value1:

break
case value2:

break
default:
break
}

switch (true) {
case score >= 90 && score <= 100:
console.log('优秀')
break
case score >= 60 && score < 90:
console.log('良好')
break
case score < 60 && score >= 0:
console.log('不及格')
break
default:
console.log('whoops!')
break
}

for 循环

1
2
3
4
5
6
7
8
9
10
11
12
for (let i = 0; index <= 9; i++) {

}

while () {

}

do {

} while ()

  • break

    终止当前循环,包括 break 后面的代码也会被停止执行,并且跳出该循环

  • continue

    终止当前循环,包括 break 后面的代码也会被停止执行,并进入下次循环

万物皆“对象”

  • 内置函数String Number Boolean Symbol Array Object Function Date RegExp Error
  • 内置对象Math JSON

String 字符串

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
charAt(i) // 获取指定下标的字符
charCodeAt(i) // 获取指定下标的 Unicode 编码, 可用来比较大小
String.fromCharCode(25105, 29233, 20320) // '我爱你'

indexOf(searchValue[, fromIndex]) // 查找对应字符首次出现的下标
lastIndexOf()
function indexOf(str, searchValue, fromIndex = 0) {
let i = fromIndex < 0 ? 0 : fromIndex,
index = -1
for (; i < str.length; i++) {
if (str.charAt(i) === searchValue) {
index = i
break
}
}
return index
}

slice(start[, end]) // 返回截取的字符串, 包含 start 不含 end
substr(start[, length]) // 待废弃
substring(start[, end]) // 推荐 👍 start/end 小于 0 则等于 0, 包含 start 不含 end

split(separator[, limit]) // 分割成数组, 字符串首尾有分隔符时会出现空格

trim()

includes(searchValue[, fromIndex])
'abc'.startsWith('a') // true
'abc'.endsWith('c') // true

toLowerCase() // 小写字符
toUpperCase() // 大写字符

'a'.repeat(3) // 'aaa'
/* =+ 性能更好 */
'a'.concat('b', 'c') // 'abc'

Number 数字

1
2
3
// 四舍六入五成双 银行家算法
let number = 123
number.toFixed(2) // '123.00'

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
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
Array.from() // 类数组转数组
Array.of('a', 'b', 'c') // ['a', 'b', 'c']
Array.isArray() // 判断是否为数组

push() // 返回数组长度
pop() // 返回弹出元素, 空数组返回 undefined
unshift() // 返回数组长度
shift() // 返回弹出元素, 空数组返回 undefined

slice(start[, end]) // 不会改变原数组, 返回截取后的新数组
splice(start[, num, item1, item2, ...]) // 添加或删除数组中的元素

join(separator = ',') // 拼接成字符串

reverse() // 会改变原数组, 返回一个位置颠倒的数组
concat(arr1, arr2, ...) // 不会改变原数组, 返回一个新数组
indexOf(searchValue[, fromIndex])
lastIndexOf()
includes(searchValue[, fromIndex])

sort((a, b) => {
return a - b // 从小到大
return b - a // 从大到小
return Math.random() - 0.5 // 随机排序[0, 1)
})

['B', 'a', 'C', 'c', 'b', 'A'].sort((a, b) => {
a = a.toLowerCase()
b = b.toLowerCase()

if (a >= b) {
return 1
} else {
return -1
}

// return a.charCodeAt() - b.charCodeAt()

})

forEach(function(item, index, arr) {}, bindThis)
filter(function(item, index, arr) {}, bindThis)
map(function(item, index, arr) {}, bindThis)
flatMap(function(item, index, arr) {}, bindThis)

reduce(function(total, item, index, arr) {
return total + item
}, initialValue)

some(function(item, index, arr) {}, bindThis)
every(function(item, index, arr) {}, bindThis)

find(function(item, index, arr) {}, bindThis)
findIndex(function(item, index, arr) {}, bindThis)

flat(depth) // 扁平化多维数组 Infinity

Object 对象

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
keys()
values()

delete obj.key

Object.create()
Object.assign()

Object.freeze()
Object.isFrozen()

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

new Set([...document.querySelectorAll('*')].map(item => item.tagName)).size

/*
{HTML: 1, HEAD: 1, BODY: 1}
*/
[...document.querySelectorAll('*')].map(item => item.tagName).reduce((res, item) => {
res[item] = (res[item] || 0) + 1
return res
}, {})

/*
[
['HTML', 1],
['HEAD', 1],
['BODY', 1]
]
*/
Object.entries(
[...document.querySelectorAll('*')].map(item => item.tagName).reduce((res, item) => {
res[item] = (res[item] || 0) + 1
return res
}, {})
).sort((a, b) => b[1] - a[1]).slice(0, 3)

Date 日期时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typeof Date() // string

let date = new Date() // 参数为空时为当前系统时间
let year = date.getFullYear()
let month = date.getMonth() + 1
let day = date.getDate()
let week = date.getDay()
let hour = date.getHours()
let minute = date.getMinutes()
let second = date.getSeconds()
let milliSecond = date.getMilliseconds()
let currentMilliSecond = date.getTime() // Date.now()

new Date(1000) // Thu Jan 01 1970 08:00:01 GMT+0800 (中国标准时间)
new Date(-1000) // Thu Jan 01 1970 07:59:59 GMT+0800 (中国标准时间)
new Date('2023-01-02 00:02:01:999') // Mon Jan 02 2023 00:02:01 GMT+0800 (中国标准时间)
new Date(2023, 00, 02, 00, 01, 61, 999) // Mon Jan 02 2023 00:02:01 GMT+0800 (中国标准时间)

Math 数学

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Math.PI

Math.min()
Math.max()

Math.ceil() // 向上取整
Math.floor() // 向下取整
Math.round() // 四舍五入

Math.abs() // 绝对值

Math.random()
/* n 到 m 之间的随机整数 */
Math.floor(Math.random() * (m - n + 1) + n) // 👍 推荐
Math.ceil(Math.random() * (m - n) + n) // 👎
Math.round(Math.random() * (m - n) + n) // 👎


/* 获取长度一致的随机数 */
let random = Math.random()
random = random + '0000000000'
random = random.slice(0, 10)

JSON

1
2
JSON.parse()
JSON.stringify()

globalThis 全局

全局属性

  • Infinity
  • NaN
  • undefined

全局函数

  • encodeURI() 👈

    不会对 ASCII 字母、数字、-_.!~*'();,/?:@&=+$#编码

  • decodeURI()
  • encodeURIComponent()

    不会对 ASCII 字母、数字、-_.!~*'()编码
    会对;,/?:@&=+$#这些用于分隔 URI 组件的 ASCII 标点符号编码

  • decodeURIComponent()
  1. 非转义字符
    • ASCII字母(a-zA-Z)
    • 十进制数字(0-9)
    • uri标记符 -_.!~*’
    • 括号 ()
  2. 保留字符
    • ;,/?:@&=+$
  3. 特殊字符
    • #
  4. 其他

总结:

  • encodeURI 只对 4.其它 编码
  • encodeURIComponent 除了非转义字符(保留字符、特殊字符、其它)
  • escape()废弃

    编码普通字符串

  • unescape()废弃

    解码普通字符串

  • isFinite()

  • isNaN()

  • eval()

requestAnimationFrame 动画帧

  • requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来, 在一次重排重绘中就完成, 与计算机系统重绘频率保持一致
  • 屏幕画面的渲染帧频一般是60Hz/s, 相当于每秒重绘60次
  • 隐藏或不可见的元素中, requestAnimationFrame 将不会进行重排重绘
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
/* 返回顶部 缓冲动画 */
function moveScroll(target = 0) {
cancelAnimationFrame(window.scrollTimer)
window.scrollTimer = requestAnimationFrame(move)
function move() {
let nowScroll = window.scrollY
// let nowScroll = document.documentElement.scrollTop || document.body.scrollTop
if (Math.abs(target - nowScroll) < 2) {
window.scrollTo(0, target)
// document.documentElement.scrollTop = document.body.scrollTop = target
} else {
let speed = (target - nowScroll) / 20
window.scrollTo(0, nowScroll + speed)
window.scrollTimer = requestAnimationFrame(move)
}
}
}

/* 兼容 */
if (!window.requestAnimationFrame) {
let lastTime = 0
window.requestAnimationFrame = function(callback) {
const now = new Date().getTime()
const nextTime = Math.max(lastTime + 16.7, now)
return window.setTimeout(function() { // 定时器 最小执行时间 >= 4ms
lastTime = nextTime
callback()
}, nextTime - now)
}
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = clearTimeout
}

DOM 文档对象模型

  • 针对HTML文档的一个应用程序编程接口
  • 是 JS 和 HTML 的桥梁

DOM 关系

  • childNodes 子节点

    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
    elem.childNodes.forEach(item => {
    console.log(item.nodeType, item.nodeName)
    /*
    1 ELEMENT_NODE
    2 ATTRIBUTE_NODE
    3 TEXT_NODE
    4 CDATA_SECTION_NODE
    5 ENTITY_REFERENCE_NODE
    6 ENTITY_NODE
    7 PROCESSING_INSTRUCTION_NODE
    8 COMMENT_NODE
    9 DOCUMENT_NODE
    10 DOCUMENT_TYPE_NODE
    11 DOCUMENT_FRAGMENT_NODE
    12 NOTATION_NODE
    */
    })

    class Node {}

    /* document */
    class Document extends Node {}
    class DocumentFragment extends Node {}

    /* 文本和注释 */
    class CharacterData extends Node {}
    class Text extends CharacterData {}
    class Comment extends CharacterData {}

    /* elem */
    class Element extends Node {}
    class HTMLElement extends Element {}
    class HTMLDivElement extends HTMLElement {}
    class HTMLInputElement extends HTMLElement {}
    • children子元素
  • firstChild 第 0 个子节点

    • firstElementChild第 0 个子元素
  • lastChild 最后一个子节点

    • lastElementChild最后一个子元素
  • nextSibling 下一个兄弟节点

    • nextElementSibling下一个兄弟元素
  • previousSibling 上一个兄弟节点

    • previousElementSibling上一个兄弟元素
  • parentNode 父节点

    • parentElement父元素

节点操作

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
/* document fragment */
let frag = document.createDocumentFragment()

let osList = ['mac', 'win', 'linux']
for (let i = 0, len = osList.length; i < len; i++) {
let li = document.createElement('li')
let liText = document.createTextNode(osList[i])
li.appendChild(liText)
// li.textContent = liText
frag.appendChild(li)
}

ul.appendChild(frag)

/* 查找节点 */
document.getElementById('id')
document.getElementsByName('name')
document.getElementsByClassName('class')
document.getElementsByTagName('div')
document.querySelector('selector')
document.querySelectorAll('selector')

/* 创建节点 */
document.createElement('div')
document.createTextNode('文本')

/* 添加节点 如果是页面上已经存在的节点,则相当于 移动 操作 */
parentEl.appendChild(node) // 在元素的末尾添加
parentEl.insertBefore(node, existNode) // 在 existNode 前添加

/* 替换节点 */
parentEl.replaceChild(node, oldNode)

/* 删除节点 */
parentEl.removeChild(node)
node.remove()

/* 克隆节点 */
node.cloneNode(!false)

属性操作

1
2
3
4
5
6
7
8
9
elem.attributes
elem.getAttribute('class')
elem.setAttribute('class', 'className')
elem.removeAttribute('class')
elem.hasAttribute('class')

/* data 自定义属性 data-key="value" */
elem.dataset.key = 'value'

属性property(各种数据类型) 特性attribute(字符串)

公认的 特性attribute 会映射到 属性property
属性是node节点, 特性会显示到html结构

  • elem.className
  • elem.getAttribute(‘class’)
  • elem.getAttributeNode(‘class’).nodeValue
1
2
3
4
5
6
7
8
9
// jQuery 源码: 在IE6、7下, 浏览器的 getAttribute 和 setAttribute 不能正常获取和设置 attribute 的值
let className = oDiv.getAttributeNode('class').nodeValue

let attr = oDiv.getAttributeNode('class')
if (!attr) {
attr = document.createAttribute('class')
oDiv.setAttributeNode(attr)
}
attr.nodeValue = 'className'

长度宽度

强制渲染 触发回流

  • offset client scroll 相关属性
  • width height
  • getComputedStyle()
  • getBoundingClientRect()

offset

  • offsetWidthwidth+padding+border 包含滚动条

  • offsetHeightheight+padding+border 包含滚动条

  • offsetTop 距离定位父级的top坐标

  • offsetLeft 距离定位父级的left坐标

  • offsetParent定位父级

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function getPageOffset(elem) {
    let top = elem.offsetTop
    let left = elem.offsetLeft
    while (elem = elem.offsetParent) {
    top += elem.offsetTop + elem.clientTop
    left += elem.offsetLeft + elem.clientLeft
    }
    return { top, left }
    }

client

  • clientWidthwidth+padding 不含滚动条
  • clientHeightheight+padding 不含滚动条
  • clientTop 上边框宽度
  • clientLeft 左边框宽度

scroll

  • scrollWidth无滚动条 则和 client 一样
  • scrollHeight
  • scrollTop 上下滚动距离
  • scrollLeft 左右滚动距离
  • scrollIntoView

getBoundingClientRect()

  • left 元素左侧距离可视区左侧距离
  • top 元素顶部距离可视区顶部距离
  • right 元素右侧距离可视区左侧距离
  • bottom 元素底部距离可视区顶部距离
  • widthwidth+padding+border
  • heightwidth+padding+border

兼容性问题: IE 会多出 2px

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
function getPageOffset(elem) {
let elRect = elem.getBoundingClientRect()
let scrollL = window.scrollX
let scrollT = window.scrollY
return {
left: scrollL + elRect.left,
top: scrollT + elRect.top
}
}

/* 碰撞检测 */
function rectCollisionDetect(elem, elem2) {
let elRect = elem.getBoundingClientRect()
let el2Rect = ele2.getBoundingClientRect()
if (
elRect.right < el2Rect.left
|| el2Rect.right < elRect.left
|| elRect.bottom < el2Rect.top
|| el2Rect.bottom < elRect.top
) {
console.log("矩形碰撞检测: 没有碰撞")
}
}

function circleCollisionDetect(elem, elem2) {
let elRect = elem.getBoundingClientRect()
let elCenter = {
x: elRect.left + elRect.width / 2,
y: elRect.top + elRect.width / 2
}

let el2Rect = ele2.getBoundingClientRect()
let el2Center = {
x: el2Rect.left + el2Rect.width / 2,
y: el2Rect.top + el2Rect.width / 2
}

// 圆心距离 <= 半径和
let dis = (elCenter.x - el2Center.x) * (elCenter.x - el2Center.x) + (elCenter.y - el2Center.y) * (elCenter.y - el2Center.y)
dis = Math.sqrt(dis)

return dis <= elRect.width / 2 + el2Rect.width / 2
}

NodeList

Node 静态集合但并不都是静态的

  • childNodes动态
  • querySelectorAll静态
  • 可以使用 forEach

HTMLCollection

Element 动态集合

  • children
  • getElementsByName getElementsByTagName getElementsByClassName
  • 没有 forEach 方法, 使用 for 循环

for 和 forEach 哪个更快?
for 更快

  • for 直接在当前函数中执行,forEach 每次都要新创建一个函数
  • 函数有单独的作用域和上下文,所以耗时更久
  • 开发中不仅要考虑性能,还要考虑代码的可读性,forEach 可读性更好

表格相关

tBodies rows cells tHead tFoot

BOM 浏览器对象模型

window

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
/* 可视区尺寸 */
window.innerWidth`包含滚动条`
window.innerHeight
document.documentElement.clientHeight`不含滚动条`

/* 页面尺寸 */
document.documentElement.offsetHeight
document.documentElement.scrollHeight

document.body

scrollX / pageXOffset
scrollY / pageYOffset
scrollTo(x, y)

document.documentElement.scrollTop
document.documentElement.offsetLeft
document.documentElement.clientTop

/* 上滑加载 */
let clientHeight = document.documentElement.clientHeight
let offsetHeight = document.body.offsetHeight
let scrollTop = document.documentElement.scrollTop

let distance = 100

// if ((window.innerHeight + window.pageYOffset) >= (offsetHeight - distance)) {}
if ((clientHeight + scrollTop) >= (offsetHeight - distance)) {
loadMore()
}

screen 屏幕信息

1
2
3
/* 屏幕尺寸 */
window.screen.width
window.screen.height

location 地址栏信息

1
2
3
4
5
window.location
document.location

window.onhashchange = () => {}

history 历史记录

  • state
  • pushState(state, title[, url])
  • replaceState
  • userAgent
  • onLine

Event 事件

事件绑定

elem.onclick = fn
elem.onclick = null

事件绑定添加多个事件会被后面覆盖
事件监听可以添加多个事件处理函数

事件监听

elem.addEventListener(type, listener[, options|useCapture])
elem.removeEventListener(type, listener[, options|useCapture])

可选参数

  • options<Object>
    • capture<Boolean>
    • once<Boolean>
    • passive<Boolean>表示永远不会调用 preventDefault; 如果 listener 仍然调用, 客户端将会忽略它并抛出一个控制台警告
  • useCapture<Boolean>默认 false 冒泡
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
class BindEvent {
constructor(elem) {
this.elem = elem
}

addEventListener(type, handler) {
const elem = this.elem
if (elem.addEventListener) {
elem.addEventListener(type, handler, false)
} else if (elem.attachEvent) {
elem.attachEvent(`on${type}`, (e) => {
e = BindEvent.fixEvent(e)
handler.call(elem, e)
})
} else {
elem[`on${type}`] = () => {
const e = BindEvent.fixEvent(window.event)
handler.call(elem, e)
}
}
}

removeEventListener(type, handler) {
const elem = this.elem
if (removeEventListener) {
elem.removeEventListener(type, handler, false)
} else if (attachEvent) {
elem.detachEvent(`on${type}`, handler)
} else {
elem[`on${type}`] = null
}
}

static stopPropagation(e) {
if (e.stopPropagation) {
e.stopPropagation()
} else {
e.cancelBubble = true
}
}

static stopImmediatePropagation(e) {
if (e.stopImmediatePropagation) {
e.stopImmediatePropagation()
}
}

static preventDefault(e) {
if (e.preventDefault) {
e.preventDefault()
} else {
e.returnValue = true
}
}

static fixEvent(e) {
e.target = e.target || e.srcElement // 触发事件的目标源元素/触发事件的子元素
e.currentTarget // 绑定事件的父元素 this

e.preventDefault = e.preventDefault || function() {
e.returnValue = false
}
e.stopPropagation = e.stopPropagation || function() {
e.cancelBubble = true
}

e.charCode = typeof e.charCode === 'number' ? e.charCode : e.keyCode

return e
}
}

事件流

DOM2级事件流事件捕获capture(先父后子👎) -> 处于目标target -> 事件冒泡bubbling(先子后父👍)

  • window

    • document document.domain
      • html 👈 document.documentElement
        • body 👈 document.body
            • 目标元素
  • 事件捕获当事件发生时,最先得到通知的是 window,然后是 document,由上至下逐级依次而入,直到真正触发事件的那个元素(目标元素)为止,这个过程就是捕获

  • 事件冒泡接下来,事件会从目标元素开始冒泡,由下至上逐级依次传播,直到 window 对象为止,这个过程就是冒泡

    事件代理利用冒泡机制将事件统一委托在父级上执行, 再通过事件源获取到相关元素
    减少事件注册 节省内存
    适合动态添加的元素

    focus blur 等事件没有冒泡机制
    mousemove mouseout 等事件不适合,需要不断通过位置去计算定位

自定义事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 长按: 当鼠标在一个元素上长按超过 750 毫秒触发 */
const longTap = new Event('longTap', {
bubbles: true,
cancelable: false
})

let timer = 0

elem.addEventListener('mousedown', () => {
clearTimeout(timer)
timer = setTimeout(() => {
elem.dispatchEvent(longTap)
// elem.dispatchEvent(new CustomEvent('longTap', {}))
}, 750)
})
elem.addEventListener('mouseup', () => {
clearTimeout(timer)
})
document.addEventListener('longTap', (e) => {
if (e.target === elem) {
console.log('自定义事件')
}
}, false)

鼠标事件

1
2
3
4
5
6
7
8
elem.addEventListener('mousedown', (e) => {
document.addEventListener('mousemove', moveFn)
document.addEventListener('mouseup', () => {
document.removeEventListener('mousemove', moveFn)
}, { once: true })
/* 阻止文本选中 */
e.preventDefault()
})
  • mouseenter mouseleave👍
    • 和子元素无关 不会在鼠标移动父子级切换过程中触发
  • mouseover mouseout👎
    • 受子元素影响 冒泡带来的问题

键盘事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 组合键 */
elem.addEventListener('keydown', (e) => {
if (e.keyCode === 13 && e.ctrlKey) {
// ctrl + enter
}
})

/* 自定义组合键 */
let lastCode = ''
elem.addEventListener('keydown', (e) => {
if ((e.keyCode === 38 && lastCode === 40) || (e.keyCode === 40 && lastCode === 38)) {
// 上 38 下 40
} else {
lastCode = e.keyCode
}
})
elem.addEventListener('keyup', () => {
lastCode = ''
})

移动端事件

touch 事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
屏幕不能滑动
不能双指缩放
不能触发菜单
输入类型表单控件不能输入
不能长按选中文字
阻止所有的鼠标事件执行(包括a标签的默认跳转)
*/
document.addEventListener('touchstart', (e) => {
e.preventDefault()
})

/*
屏幕不能滑动
不能双指缩放
*/
document.addEventListener('touchmove', (e) => {
e.preventDefault()
})
点击穿透

在移动端中,当触发一个事件时,JS 会记录当前触发的坐标,300ms之后,在该坐标查找元素,如果元素有鼠标事件,就把该鼠标事件执行了鼠标事件的执行在移动端会有 300ms 左右的延迟

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
<style>
#box {
position: absolute;
left: 0;
top: 0;
width: 100px;
height: 100px;
background: rgba(255, 0, 0, .3);
}
</style>

<a href="https://www.5207.fun/">点击穿透</a>
<div id="box"></div>

<script>
{
let box = document.querySelector('#box')
// box.addEventListener('touchend', () => {
// setTimeout(() => {
// box.style.display = 'none'
// }, 320)
// })
box.addEventListener('touchend', (e) => {
box.style.display = 'none'
})
document.addEventListener('touchend', (e) => {
e.preventDefault()
}, {
passive: false
})
}
</script>

原因:双击缩放(double tap to zoom)网页

1
2
3
4
/* 早期解决方案 FastClick */
window.addEventListener('load', function() {
FastClick.attach( document.body )
}, false )
1
2
<!-- 现代浏览器可使用 width=device-width 表示已经是响应式处理,不需要缩放,就不会双击延迟 -->
<meta name="viewport" content="width=device-width" />
touchEvent
  • touches 屏幕中的手指列表
  • targetTouches 元素上的手指列表
  • changedTouches触发事件的手指列表
1
2
3
elem.addEventListener('touchmove', (e) => {
console.log(e.touches.length)
})
禁止缩放
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
// 禁止双指缩放
document.documentElement.addEventListener(
'touchstart',
function (event) {
if (event.touches.length > 1) {
event.preventDefault()
}
},
false
)
document.addEventListener('gesturestart', function (event) {
event.preventDefault()
}, false)

// 禁止双击缩放
var lastTouchEnd = 0
document.documentElement.addEventListener(
'touchend',
function (event) {
var now = Date.now()
if (now - lastTouchEnd <= 300) {
event.preventDefault()
}
lastTouchEnd = now
},
false
)

orientation 横竖屏切换

1
2
3
4
5
6
7
8
9
10
11
12
window.addEventListener('orientationchange', () => {
switch (orientation) {
case 0:
case 180:
console.log('竖屏')
break;
case -90:
case 90:
console.log('横屏')
break;
}
})

devicemotion 加速度

1
2
3
4
5
6
7
8
9
10
window.addEventListener('devicemotion', (e) => {
const acceleration = e.acceleration // 加速度
const accelerationIncludingGravity = e.accelerationIncludingGravity // 重力加速度
/*
x: 手机宽度方向
y: 手机高度方向
z: 手机厚度方向
*/
console.log(accelerationIncludingGravity.x - acceleration.x) // 注意 IOS 和 安卓 取值相反, IOS 是正数时 安卓 是负数
})
手机摇一摇
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const MAX_RANGE = 60 // 当两次之间的晃动幅度超过 MAX_RANGE 认为用户进行了摇一摇
const MIN_RANGE = 5 // 当两次之间的晃动幅度小于 MIN_RANGE 执行摇一摇要进行的操作
let lastX = 0
let lastY = 0
let lastZ = 0
let isShake = false
window.addEventListener('devicemotion', throttle((e) => {
let motion = e.acceleration
let x = motion.x
let y = motion.y
let z = motion.z
let range = Math.abs(x - lastX) + Math.abs(y - lastY) + Math.abs(z - lastZ)
if (range >= MAX_RANGE) {
isShake = true
}
if (range <= MIN_RANGE && isShake) {
isShake = false
// do something...
}
lastX = x
lastY = y
lastZ = z
}))

deviceorientation 倾斜角度

  • e.beta
  • e.gamma
  • e.alpha

拖放操作

元素拖拽

  • draggable设置元素为可拖放
    • 给默认可拖拽的元素(选中的文本、链接、图像)设置 draggable 为 false, 可以阻止拖拽
    • firefox浏览器如果拖放的元素没有设置拖放内容 dataTransfer.setData() 还是不能拖放
  • dataTransfer.setData() 设置拖拽时传递的信息
  • dataTransfer.getData() 获取拖拽时传递的信息
  • 相关事件
    • ondragstart 拖动开始时触发
    • ondrag 拖动过程中触发
    • ondragend 拖动结束时触发
    • ---------------------------------
    • ondragenter 拖动元素进入放置目标时触发
    • ondragleave 拖动元素从放置目标中移出时触发
    • ondragover 元素或文本选择在放置目标中移动时触发
    • ondrop 在有效放置目标上放置元素或文本选择时触发
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
/*
拖动元素
<div id="box1" draggable="true" style="width: 100px; height: 100px; background: red;"></div>

放置目标
<div id="box2" draggable="true" style="width: 200px; height: 200px; background: green; position: absolute; left: 400px; top: 200px;"></div>

*/

let box1 = document.querySelector('#box1')
let box2 = document.querySelector('#box2')

let img = new Image() // document.createElement('img')
img.src = 'https://www.5207.fun/img/loading.gif'

box1.ondragstart = function(e) {
e.dataTransfer.setData('text/plain', '#box1')

// dropEffect 白名单
// e.dataTransfer.effectAllowed = 'copyLink'

// 设置拖放图片
e.dataTransfer.setDragImage(img, 100, 100)

console.log('dragstart')
}

box2.ondragover = function(e) {
// 每次触发都会重置默认行为
e.preventDefault()

console.log(e.dataTransfer.dropEffect)
e.dataTransfer.dropEffect = 'move'

console.log('dragover')
}
box2.ondrop = function(e) {
e.preventDefault()
e.stopPropagation()

let id = e.dataTransfer.getData('text/plain')
let el = document.querySelector(id)
this.appendChild(el)
}

系统文件拖放

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
/*
html, body {
margin: 0;
width: 100%;
height: 100%;
}
*/
// 设置body为可放置元素
document.body.ondragover = function(e) {
e.preventDefault()
}
// 处理放置后的行为
document.body.ondrop = function(e) {
e.preventDefault()
e.stopPropagation()

console.log(e.dataTransfer.files)

let file = e.dataTransfer.files[0]

let fr = new FileReader()
fr.onload = function() {
// console.log(this.result)
let img = new Image()
img.src = this.result
document.body.appendChild(img)
}
// fr.readAsText(file, 'gbk')
fr.readAsDataURL(file)

/* fr.onload = function() {
let int8Array = new Int8Array(this.result)
console.log(int8Array)

int8Array.forEach(data => {
// console.log(data)
console.log(String.fromCharCode(data))
})
}
fr.readAsArrayBuffer(file) */
}

系统文件读取

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
document.body.ondragover = function(e) {
e.preventDefault()
}

document.body.ondrop = function(e) {
e.preventDefault()
e.stopPropagation()

// console.log(e.dataTransfer.files)
// console.log(e.dataTransfer.items)

[...e.dataTransfer.items].forEach(item => {
console.log(item.kind)

// let file = item.getAsFile()
// console.log(file)

let entry = item.webkitGetAsEntry()
getFiles(entry)
})
}

function getFiles(entry) {
console.log(entry)

if (entry.isFile) {
entry.file(file => {
console.log(file)
}, error => {
// file://
console.error(error)
})
}

if (entry.isDirectory) {
let dirReader = entry.createReader()
dirReader.readEntries(entries => {
entries.forEach(entry => {
getFiles(entry)
})
})
}
}

严格模式

代码(或一个函数)开始行插入'use strict'开启严格模式

一般情况下,开发环境用 ES 或者 Typescript,打包出的 js 代码使用严格模式

  1. 全局变量必须声明
  2. 禁止使用 with
  3. 创建 eval 作用域
    • 正常模式下,JS 只有两种变量作用域:全局作用域 + 函数作用域。严格模式下,JS 增加了 eval 作用域
  4. 禁止 this 指向全局作用域
  5. 函数参数不能重名
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
'use strict' // 全局开启

function fn() {
'use strict' // 某个函数开启
}

n = 10 // ReferenceError: n is not defined

let obj = { x: 10 }
with (obj) {
// Uncaught SyntaxError: Strict mode code may not include a with statement
console.log(x)
}

var x = 10
eval('var x = 20; console.log(x)')
console.log(x)

function fn() {
console.log('this', this) // undefined
}
fn()

function fn(x, x, y) {
// Uncaught SyntaxError: Duplicate parameter name not allowed in this context
}