devTools
- 仅当开启
DevTools
时,右键刷新网页按钮才可以弹出上下文菜单。来使用清空缓存并硬性加载来清除页面缓存 - 当使用移动端设备视图时,不支持访问浏览器自带的 pdf 查看器。将会显示警告注释:
"no enabled plugin supports this MIME type"
CSS
一句话整理
inset
属性是top、right、bottom、left
的缩写,使用方法同margin
max-height
会覆盖height
, 而min-height
会覆盖max-height
- 浮动元素会脱离网页文档,与其他元素发生重叠,但是,不会与文字内容发生重叠。所以可以实现文字环绕效果
- 当元素设置了
border-width
与border-style
但并未设置border-color
时,自动使用元素的color
属性 - 当需要点击页面元素不会自动清除文字选中状态时,需要该元素拥有
user-select: none;
样式。 - flex 容器中,设置
flex: 1
时,会计算剩余内容宽(高)度。但当计算出来的总宽(高)度超过父容器时,则flex: 1
失效,其他元素被挤压。此时可以同时对元素设置width: 0
或height: 0
来避免计算剩余空间时溢出。
可替换元素
css 中存在一种元素叫做可替换元素,又称为置换元素。
这种元素是一种外部对象,其渲染独立于 css,css 可以影响其的位置,但不会影响它的内容
可替换元素有如下几种:
- 典型可替换元素:
<iframe>
、<video>
、<embed>
、<img>
、标准元素的content
属性插入的为标准可替换元素(目前仅支持url()
)等 - 仅特殊情况下时被认为可替换元素:
<option>
、<audio>
、<canvas>
、<object>
等 - 匿名可替换元素: 伪元素 css
content
属性所插入的元素
匿名元素
在 CSS 中,没有明确的 HTML 标签的东西称之为匿名元素,例如下面这段 HTML:
1 | <p> |
其中:Asuka 为匿名内联元素,而 FEE 因为有明确的 <span>
包裹,为标准内联盒子
除此外,伪元素 css content
属性所插入的元素也同样为匿名可替换元素。
匿名元素与标准元素的区别
由于匿名元素没有显示的标签包裹,其的宽高也无法被修改,这是与标准元素之间最显著的不同。
例如如下情况:
1 | .app-container::before { |
其中伪元素的宽高被定义为 100px
,但伪元素内部由 content
属性注入的图片匿名元素扔按照自身原本宽高展示,不受伪元素宽高影响
这比较类似于如下的 html 结构,但无法修改 img 的宽高,只能修改 div 的宽高
1 | <div> |
JavaScript
一句话整理
- 函数参数只有传递的参数值为
undefined
时,才会使用默认值,null
和''
不会采用默认值 NaN
与任何值都不相等,包括NaN
本身es5
中的parserInt()
默认不具备解析8
进制的能力,需要传入第二个参数来解析parserFloat()
只能转换10
进制,16
和8
进制格式字符串始终会被转换为0
- 几乎所有类型都有
tostring()
方法,但null
和undefined
没有。因此在不知道目标类型时,可以使用转型方法String()
,String()
会自动调用toString
方法(如果有),对于null
和undefined
则会返回"null"
和"undefined"
offsetTop
不是距离父元素的距离,而是指向最近的 offsetParent 元素的内边距边界。th
、td
的min-height
无效,直接使用height
即可,等同于min-height
,超出会被自动撑高flex-direction
反向时,justify-content
也一并反向reverse()
和sort()
会变更原始数组- 使用
calc
对scss
变量进行处理时,需要使用#{}
包裹,如:height: calc(100% - #{$header-height});
switch
语句中的对比为严格相等运算符(===
)typeof
可以检查一个未定义的变量而不报错,如typeof marry
结果为undefined
echart -- legend.itemWidth
设置为"15"
而非15
时会出现文字跑开现象vue3 - setup
外使用router
对象时可以直接import
引入createRouter
生成的对象- 当元素为绝对定位,且不指定宽高时(如内含图片),若
left
的值使其贴至父容器右壁或right
的值使其贴近父元素的左壁,便会自动缩小自身宽度,使其永远不会跑出父容器。 vue2
的响应式系统针对对象时,不会监听一开始不存在的属性。- 全局匹配模式下对同一个正则连续调用
test()
,第二次会在第一次匹配的位置后面开始查找 Array.prototype.find
遇到返回的结果为false
、""
、等会被自动转换为false
的情况,可能会出现误判,建议查找元素存不存在时使用Array.prototype.findIndex
- 页面存在
iframe
时,f12 选择元素会自动跳转到 iframe 内部,就会出现同样的选择代码本来选不中f12点了一下就选中了的诡异情况 - 可以使用
Response.clone()
来对Fetch
的response
对象进行克隆,解决response
对象仅能读取一次的问题 - 后端向前端设置 cookie 时,若设置了
HttpOnly
为 true,则前端无法通过任何方式获取到此 cookie 的内容。 - 当页面 url 发生了变化导致页面跳转时,未响应完毕的 ajax 请求将全部中断,状态变为
cancel
Infinity * 0
结果为NaN
- 可通过设置数组的
length
属性来截断数组,例如arr.length = 0
可以置空数组 parseInt()
、parseFloat()
和Number()
都会自动去除首尾空格,一些字符串自动转换为数字类型的操作也遵守这种规则Error
对象中只有message
属性是标准中要求必须要存在的,name
和stack
属性都是非标准的。- 时间戳代表从
1970年1月1日00:00:00 UTC
到指定日期的毫秒数,因此时间戳允许为负数,此时为1970年1月1日00:00:00 UTC
之前的时间。 - 利用事件循环特性,可以将操作转为异步操作,来实现延后操作执行顺序的效果。
- 如果一个节点没有父节点,设置
outerHTML
属性会报错 clientHeight
、clientTop
等client
系列属性只对块级元素有效,对于行内元素,其值为0
。- 当元素节点存在溢出,且溢出部分隐藏时,
scrollWidth
依旧返回元素总宽度,scrollHeight
同理。 - 使用 JS 修改 HTML 节点的
for
属性时,需要使用htmlFor
来代替,因为for
是保留字。 - 因为保留字原因,通过
el.style.xx
修改部分 css 属性时,需要加上字符串css
。比如float
需要改写为cssFloat
。 - 只有在当前节点的
dragover
事件中手动阻止默认行为,drop
事件才会被触发。这是因为浏览器默认不允许在元素上放置其他元素。 frames
是window
的别名- 对于顶层窗口,
window
与self
、top
、parent
是相等的
HTML 注释
由于历史上 JavaScript 可以兼容 HTML 代码的注释,所以 <!-- -->
也是合法注释
需要注意的是,-->
起始时,会注释掉当前行
1 | x = 1; <!-- x = 2; |
虽然编辑器中未将 x = 3
置灰,但其实它也被注释掉了,因此此处 x
的最终结果为 1
表达式还是代码块?
1 | { name: "Mari" } |
对于上面这种情况,存在两种含义:
- 如果这是一个代码块,那么这个代码块中的内容是一个名称为
name
的标签语句,但是这个标签语句没有任何效果 - 如果这是一个对象字面量,那么这个对象字面量中有一个属性
name
,其值为"Mari"
为了避免这种歧义,JavaScript 引擎的做法是,将以 {
开头的语句一律解析为代码块,而不是对象字面量。
如果确实想要将其解析为对象字面量,可以使用 ()
进行包裹,如:
1 | ( { name: "Mari" } ) |
arguments 是否可以修改?
arguments
对象是一个类数组对象,它的值可以通过索引来访问。
默认非严格模式下,修改 arguments
内元素的值会影响到函数的参数值。
1 | function test( a ) { |
但是在严格模式下,对 arguments
对象的修改并不会影响到函数的参数值。
1 | function test( a ) { |
可以得到结果,严格模式下,修改 arguments
内元素的值是有效的,但并不会影响到函数的参数值。
ES6 的模块自动启用了严格模式,因此在 NodeJS 中定义了
"type": "module"
时同样会默认启用严格模式,习惯使用 NodeJS 来测试代码时需要注意这一点。
eval()
eval()
函数可以将字符串当作 JavaScript 代码来执行,并返回执行后的返回值。通常用来直接执行得到的语句字符串。
1 | eval( "console.log( 1 )" ); // 输出 1 |
安全风险
eval()
的本质就是在当前作用域下插入执行一段 JavaScript 代码,其所执行的代码块具有与当前作用域相同的权限。
这就代表着这段代码块可以在当前作用域下定义变量,甚至是修改当前作用域下的变量值,这就存在很严重的安全问题。
1 | let a = 1; |
可以看到当前作用域下的变量 a
的值被修改了,同时后续代码中也接收到了 eval
中定义的变量 b
。
而在严格模式下,eval
函数会创建一个新的作用域,因此 eval
中定义的变量不会影响到当前作用域。
不过此时 eval
内部的代码块依然可以修改当前作用域下的变量值。
1 | ; |
除了严格模式以外,也可以通过使用 let
而非 var
定义变量,来避免 eval
对当前作用域的影响。
这是由于 let
定义的变量为块级作用域,在此处即表现为只能在 eval
内部访问,eval
执行完毕即销毁(产生闭包时除外)。
1 | eval( "let b = 2" ); |
别名
尽管很少遇到,但会存在将 eval
重新赋值给一个新的变量名的情况,这会导致 JavaScript 引擎在静态分析代码阶段无法判断执行的是否为 eval
函数。
1 | const e = eval; |
为了避免这种行为影响代码的优化,JavaScript 标准规定,在这种情况下 eval
内部的代码块的作用域统一为全局作用域。
1 | let a = 1; |
注:在 NodeJS 中,上面的代码会直接报错 “a is not defined”。这是因为在上面这种情况下,会尝试在全局对象
global
上寻找属性 a。
将最外层的let a = 1
修改为global.a = 1
即可正常得到输出结果 1。
switch 中的表达式
switch
和 case
语句部分都可以使用表达式
1 | const a = 10, b = "20"; |
null 与 undefined
语义上讲,null
代表空值,而 undefined
代表未定义。
而在实现上,他们有如下几种不同的行为:
- 对于有默认值的的函数参数,传递
undefined
时函数依旧会使用默认值,而传递null
则会覆盖参数默认值 - 使用
Number()
或+
转换为Number
类型时,undefined
被转为NaN
,null
则被转为0
typeof
检测未定义变量是可行的,返回undefined
,而null
从逻辑上来说表示一个空对象指针,因此使用 typeof 判断会得到object
undefined
的值是派生自 null
值的,因此有如下结果
1 | null == undefined // true |
建议如果想要声名一个计划将要保存值的对象,就将其初始化为
null
NaN
NaN
有如下个特点:
- 他还是一个
Number
类型 - 和任何数值计算都会得到
NaN
,包括Infinity
- 和自身比较永远为
false
- 转换为布尔值的时候为
false
- 任何值与
NaN
进行比较都会返回false
0 / 0 = NaN
Infinity
Infinity
有如下特点,其他表现行为和正常 Number 类型一致:
- 对普通数字,
Infinity +-*/
的时候统一为Infinity
。其中*
和/
遵循基本的运算正负号原则,Infinity
被当作除数时会得到一个+-0
(符号看被除数和除数的符号) - 和
0
相乘得到NaN
,和0
的其他运算遵循第一条 - 减去或除去自身时得到
NaN
- 与普通数字判断大小时,遵循字面意思的正负无穷大
Infinity === Infinity
结果为true
Infinity / Infinity = NaN
,Infinity - Infinity = NaN
- 除了
0
和NaN
以外,Number 类型 / 0
都得到对应符号的Infinity
数组的格式转换
数组转换为 String
类型时,表现形式与 join()
类似,结果为 ,
拼接的字符串。
1 | [].toString(); // "" |
数组转换为 Number
类型时,当数组内小于等于 1
个元素时,将此元素转为 Number
类型。反之得到 NaN
。
1 | Number( [] ); // 0 |
[]+{}与{}+[]
1 | [] + {}; // '[object Object]' |
第一种表达式为 x + y
表达式,会将 x
与 y
强制转换为 String
类型。空数组 []
被转换为 ''
,{}
被转换为 '[object Object]'
,结果为 [object Object]
在第二种表达式中,{}
被认作是空代码块,不执行,代码块不需要分号终结。因此该式为 +x
表达式,将 x
强转为Number
类型,[]
被转换为 0
,结果为 0
Number 的数值范围
当超出 Number 的数值范围时,会输出无法继续参与计算的 Infinity
(无穷)值,获取数值范围的方法如下:
1 | // 最小值 |
进制
计算时,所有进制表示的数值都会被转换为十进制数值。
八进制
以 0
开头,如 070
解析为 56
,当超出范围时前面的 0
失效,当作十进制解析,如 079
解析为 79
在严格模式下是禁止整数位首位为
0
的,此时会抛出一个错误:Legacy octal literals cannot be used in strict mode。
严格模式下应该使用0o
前缀来表示八进制数值,例如0o70
解析为56
十六进制
以0x
开头,如 0xA
解析为 10
浮点数
浮点数的最高精度是 17
位小数,它计算的精确度远不如整数,会有微小的误差,例如
1 | 0.1 + 0.2; // 0.30000000000000004 |
因此永远不要做如下判断:
1 | if ( a + b == 0.3 ) {} |
保存浮点数值需要的内存空间是整数值的两倍,因此 JavaScript 会尽可能的将浮点数转换为整数
默认情况,JavaScript 会将小数点后面带有连续 6
个及以上的零的浮点数转换为以 e 表示的科学计数法,如:
1 | 0.0000003 |
数值精度
本段落参考文章JavaScript 浮点数陷阱及解法
根据 IEEE 754 标准,即:1
个符号位、11
个指数位、52
个尾数位。
详细内容不再说明,由于其 52
个尾数位再加上省略的一位,可以得到 JS 最多能表示的精度: 2^53=9007199254740992
,使用科学计数法可以得到 9.007199254740992e+15
,它的长度为 16
位。
因此 JavaScript 选择在保存时,小数部分保留最多 16
位有效数字的精度。
而我们知道 0.1
的二进制是无限循环的,溢出 52
位后存在精度丢失,之所以依旧能够正常显示为 0.1
,就是因为这个自动保留 16
位精度导致的。
我们可以通过 toPrecision()
方法来验证。
1 | 0.1.toPrecision( 16 ); // "0.1000000000000000" |
需要注意的是,尽管小数是可以显示到 17
位有效数字的,但第 17
位将会遵循一定的情况存在较大的误差,该误差与整数超过 2^53
之后表现行为同理,例如:
1 | 0.30000000000000001 // 0.3 |
对 toFixed()
的影响
toFixed()
通常用来将数值四舍五入为指定小数位数的字符串,但贸然使用它会出现一些意外的结果。
1 | 1.005.toFixed( 2 ); // "1.00" |
由于数值精度与 16 位有效数字四舍五入的机制影响,1.005
并不是我们真实看到的 1.005
,我们可以借助 toPrecision()
来查看真实的数值。
1 | 1.005.toPrecision( 16 ); // "1.005" 在机制干扰下我们看到的正常结果 |
我们可以借助 Math.round()
通过转为整数并重新计算回小数的方式来解决
1 | Math.round( 1.005 * 100 ) / 100; // 1 |
然而我们并没有得到理想中的结果,这是因为 1.005 * 100
本身就得到了错误的结果 100.49999999999999
。这个问题我们会在下文进行解答。
使用 toPrecision()
来尝试解决
toPrecision()
方法用于将数值展示为指定精度的字符串。绝大多数情况下,toPrecision(16)
即为我们正常看到的结果。
1 | 1.0049999999999999.toPrecision( 10 ); // "1.005000000 |
当我们希望使用这个方法来解决精度问题时,可以封装为如下方法
1 | function strip( num, precision = 15 ) { |
至于为什么默认精度是 15
呢,因为这样可以解决绝大多数的 0001
和 0009
问题,更多的是一种经验之谈。如果需要,可以设置为更低的精度。
现在我们可以重新尝试解决上面的四舍五入问题,使用新封装的方法
1 | Math.round( strip( 1.005 * 100 ) ) / 100; // 1.01 |
比较运算符的作用方式
比较运算符并非是简单的将两侧值转换为数字后进行比较,而是根据待比较目标的类型来区分不同的情况考虑。
满足此部分内容特性的比较运算符为:
==
、!=
、>
、<
、>=
、<=
,并不包括严格相等运算符===
、!==
字符串的比较
当待比较两者均为字符串时,会从左到右依次比较字符的 Unicode
码值。若比较结果相同,则继续比较第二个,以此类推。
例如下面的实例,前三个字符码值相同,第四个字符中 i
的码值小于 r
的码值,得到比较结果。
1 | "i".codePointAt(0); // 105 |
数字字符串也不能绕过这个问题,因为比较的是字符的码值,而非数字的大小。
1 | "2" < "10" // false |
非字符串的比较
当两边的值中有一个不是字符串时,则又延伸出两种情况:
两者均为原始类型
此时会将两者转换为数字后进行比较。
1 | 10 > "4" // true |
两者中存在对象
此时会调用对象的 valueOf()
来对对象进行处理。若得到的还是对象,继续调用 toString()
方法。
其实这就是
Number()
在将对象转换为数值之前的处理方式
将待比较项中的对象处理为原始类型后,再次根据上述规则(是否都是字符串)进行比较。
1 | "2" < [10]; // false |
void 运算符
void
运算符可以对修饰的表达式进行求值,但是恒定返回 undefined
。利用这个特性,可以对一些操作进行省略。
1 | 1 + 1; |
逗号运算符
逗号 ,
其实也是运算符,其作用是对两个表达式进行求值,返回最后一个表达式的值。
1 | 1 + 1, 2 + 2; // 4 |
delete 操作符的返回值
delete
操作符用于删除对象的属性,其返回值为 true
或 false
,表示是否删除成功。
不过即使是对于不存在的属性,delete
也会返回 true
1 | const obj = { name: "Mari" }; |
仅有一种情况会使 delete
返回 false
,那就是尝试删除一个不可配置的属性
1 | const obj = Object.defineProperty( {}, "name", { |
需要注意的是,delete
操作符只能操作对象的自有属性。
因此在操作原型链上的属性时,生效方式与删除不存在的属性一致,即不会有任何效果,且返回 true
1 | const obj = { name: "Mari" }; |
余数运算符计算结果的符号问题
对于余数运算符 %
,其计算结果的符号与被除数的符号一致。
1 | -114 % 10; // -4 |
因此,为了得到正确的余数结果,建议使用绝对值函数进行处理。
1 | Math.abs( -114 % 10 ); // 4 |
变量提升
介于 JavaScript 引擎的工作方式为:先解析代码,再逐条执行。因此 var
声明变量的语句将会提升到当前作用域的头部,造成如下结果
1 | // 编写代码 |
结果为 undefined。
var 声明的变量为函数作用域,块作用域无法约束 var 声明变量的作用提升效果
抛出错误的建议操作
在使用 throw
与 reject()
抛出错误时,建议使用 Error
对象包裹。
1 | throw "出错了"; // 不建议 |
这样抛出的错误可以在 try catch
中通过 err.message
获取抛错内容。
1 | function foo() { |
try-catch 的 finally 代码块
try-catch
语句中的 finally
代码块会在 try
或 catch
代码块执行完毕后执行,哪怕是在 try
或 catch
代码块中使用了 return
语句。
下面的例子可以看到,try
代码块中的 return
语句正常返回了当前的结果。但是 finally
代码块中的语句依然正常执行了。
1 | let count = 0; |
finally
中的 return/throw
语句还会覆盖 try
或 catch
中的 return/throw
语句。
1 | function foo() { |
标签(label)
JavaScript 允许语句前面存在标签,类似于定位符,格式如下
1 | label: statement |
标签通常与 continue
和 break
配合使用,用于跳出指定循环。默认情况下,continue 与 break 只跳出当前循环
1 | foo: for (const numList of [[1, 2, 3], [4, 5, 6], [7, 8, 9]]) { |
ES5 中 Number 相关方法自动转换问题
在 ES5 中存在一些操作 Number
类型的方法,他们对符合既定要求的的参数会自动进行类型转换
parseInt
、parseFloat
第一个参数不是String
类型时,会自动调用toString()
方法将其转换为字符串isNaN
、isFinite
参数不是Number
类型时,会先尝试使用Number()
转换为数字再进行判断
这就会导致一些奇怪的现象:
1 | parseInt( 011, 8 ); // NaN,011 直接得到 9,然后被转换为字符串 "9","9" 不是 8 进制的合法数字,因此得到结果 NaN |
建议使用 ES6 中新增的 Number
上的同名静态方法来避免这种问题
1 | Number.isNaN( "mari" ); // false |
向 iframe 内部插入样式
iframe 不会加载外部 css,因此需要特殊操作来插入 css 样式表内容
1 | /* 添加嵌入组件的样式 */ |
需要注意的是,获取 iframe 内 window 对象的操作受到同源策略影响,非同源时受到浏览器安全限制,无法获取。
split() 与 正则表达式
split()
方法可以接受一个正则表达式作为参数,此时会将正则表达式匹配到的内容作为分隔符进行分割。
1 | "mari".split( /a/ ); // [ "m", "ri" ] |
需要注意的是,正则表达式中的捕获组会被保留在结果数组中。
1 | "mari".split( /(a)/ ); // [ "m", "a", "ri" ] |
JSON.stringify() 与 toJSON()
JSON.stringify()
方法在序列化对象时,若目标对象存在 toJSON()
方法,将会优先使用这个方法的返回值作为参数,忽略目标对象的其他属性。
反之若不存在 toJSON()
方法,则会正常对目标进行序列化。
1 | const obj = { |
这一特点的一个实践就是 Date
对象,因为 Date
对象内置 toJSON()
方法,因此序列化会得到这个方法的执行结果。
1 | const date = new Date(); |
对函数二次使用 bind
MDN 中的原话:
绑定函数可以通过调用 boundFn.bind(thisArg, /* more args */) 进一步进行绑定,从而创建另一个绑定函数 boundFn2。
新绑定的 thisArg 值会被忽略,因为 boundFn2 的目标函数是 boundFn,而 boundFn 已经有一个绑定的 this 值了。
通俗解释即:this
会永远指向第一次 bind
时所定义的目标。
1 | function foo() { |
上面两次函数执行均不会修改 obj2
,因为两个函数的 this
均指向 obj1
。
new 关键词的原理
new
关键词实际由如下几步完成
- 创建一个新对象,原型指向构造函数的
prototype
- 将构造函数的
this
指向新对象 - 执行构造函数内部代码
1 | function Author( name ) { |
instanceof 检查的范围
instanceof
操作符用于检查构造函数的 prototype
是否存在于目标对象的原型链上。
因此同一个实例对象可能会对多个构造函数都返回 true
。
1 | [] instanceof Array; // true |
instanceof
操作符只能检查对象是否是某个构造函数的实例,无法对基本数据类型进行检测。
1 | "mari" instanceof String; // false |
需要注意的是,尽管 null
是一个特殊的对象,但 instanceof
处理 null
时总会返回 false
。
1 | null instanceof Object; // false |
构造函数中的 return
如果构造函数中存在 return
语句,且返回值为对象类型,则会返回这个对象,而非构造函数中的 this
。
1 | function Author( name ) { |
如果返回值为非对象类型,则会无视并照旧返回构造函数中的 this
。
1 | function Author( name ) { |
ES6 种
Class
的构造函数与此同理
this 严格模式下的指向限制
通常情况下,函数内部的 this
可能会指向全局对象。严格模式下禁止这种情况的发生,此时 this
会指向 undefined
。
1 | // 正常模式 |
偷懒使用 moment.js
moment
是以当前对象进行 add()
、 date()
等运算的,所以计算多个值时,不要偷懒提取公共部分。例如:
1 | const date = moment(); |
这样会导致 start
比 end
早了一天即多减了一天。
跳转拦截与获取目标地址
1 | window.navigation.onnavigate = function (e) { |
定时器方法的其他参数
setTimeOut
与 setInterval
不仅仅只有两个参数,还可以跟随接受多个参数,这些参数会被传递给回调函数。
1 | setTimeout( ( ...args ) => { |
addEventListener 的第三个参数
该参数除了可以设置为 true
与 false
(指代捕获/冒泡阶段执行) 之外,还可以设置为一个对象,用于设置事件的一些属性。
对象拥有四个属性:
capture
:布尔值,是否在捕获阶段执行,默认false
once
:布尔值,是否只执行一次,默认false
passive
:布尔值,是否不允许阻止默认行为,默认false
signal
:值为一个AbortSignal
对象,当发出中断信号时,移除事件监听器
通过这种方式可以更加灵活的设置事件监听。
避免方法自行阻止默认行为
设置 passive
为 true
后,将会将此事件注册为被动事件。
若事件回调函数中调用了 preventDefault()
方法,当事件触发时,不会阻止默认行为,且会抛出报错(报错不会影响事件回调方法内部执行)。
1 | document.addEventListener( "click", e => e.preventDefault(), { passive: true } ); |
批量移除事件监听器
通过设置同一个 signal
对象,可以在一次中断信号中移除多个事件监听器。
1 | const controller = new AbortController(); |
事件的传播
事件的传播机制通常被错误的认为是单向传播,真实情况是一个“进去再出来”的流程。分为三个阶段:
- 捕获阶段:事件从根节点向目标节点传播
- 目标阶段:事件到达目标节点
- 冒泡阶段:事件从目标节点向根节点传播
对于目标节点,浏览器总是假定为触发位置的最深层嵌套节点。
例如,<div>
嵌套 <p>
嵌套 <span>
,点击 <span>
时,浏览器会认为 <span>
是目标节点。
基于这个理论基础,我们可以编写一个案例:
1 | // 为子元素绑定 |
点击子元素,可以得到输出结果: 父:捕获 子:捕获 子:冒泡 父:冒泡
若点击的位置子元素就是最深层嵌套节点,那么子元素的这两次触发均为目标阶段。
不能冒泡的事件
在目标阶段的事件监听器,即使是不能冒泡的事件,也依旧会执行绑定的冒泡阶段监听器。
这是因为不能冒泡的事件影响的是冒泡阶段,而不能影响目标阶段。
我们可以通过创建自定义事件来模拟这种情况。
1 | // 默认情况下,自定义事件是不允许冒泡的,即冒泡阶段不会执行 |
得到输出结果:父:捕获 子:捕获 子:冒泡
,捕获阶段的 父:捕获
和目标阶段的 子:捕获 子:冒泡
均被执行,而冒泡阶段的监听器 父:冒泡
没有被执行。
阻止事件传播
通过 stopPropagation()
方法可以阻止事件的传播,对此我们可以修改一下上方的案例。
1 | el.addEventListener( "click", e => console.log( "子:捕获" ), true ); |
我们在传播最开始的父元素捕获阶段就调用了 stopPropagation()
方法,此时整个事件传播会被立即终止,因此后续一系列传播均不会再触发。
此时仅输出 父:捕获
。
阻止元素上的其他事件监听器
若同一个元素上的绑定了多个事件触发器,则会按照绑定的顺序依次执行。
此时可以通过在回调函数中执行 stopImmediatePropagation()
来阻止自身之后的其他事件监听器的执行。
1 | el.addEventListener( "click", e => console.log( "回调1" ) ); |
点击后可以得到结果:回调1 回调2
,回调3 没有被执行。
stopImmediatePropagation()
同样也会阻止事件传播,也就是说,该方法比 stopPropagation()
更加彻底。
鼠标进入离开事件的区别
鼠标进入离开事件有两对,分别是 mouseenter
与 mouseleave
以及 mouseover
与 mouseout
,这两组有两个本质区别:
mouseover
与mouseout
会冒泡,而mouseenter
与mouseleave
不会mouseover
与mouseout
针对子元素覆盖父页面的可视区域问题,在进入子元素时同样会被判定为离开了父元素
根据这两点特性,我们针对一个案例来分别讨论捕获与冒泡时发生的现象。以 mouseenter
与 mouseover
作为案例讨论,mouseleave
与 mouseout
同理。
1 | <p id="target">监听元素<span>子元素</span></p> |
捕获阶段
在捕获阶段注册事件监听器
1 | target.addEventListener( "mouseover", e => console.log( "mouseover" ), true ); |
此时处于捕获阶段,第一条冒泡与否的区别不会对结果产生影响,仅第二条特性会影响结果:
- 进入
target
元素时,同时打印mouseenter
与mouseover
- 进入子元素时,以子元素为目标节点触发事件,事件传递会先通过
target
元素的两个捕获阶段事件再到达目标节点子元素,此时会同时打印mouseenter
与mouseover
- 离开子元素回到父元素时,由于第二条特性影响,仅打印一次
mouseover
- 离开父元素时,同时打印
mouseleave
与mouseout
冒泡阶段
在冒泡阶段注册事件监听器
1 | target.addEventListener( "mouseout", e => console.log( "mouseover" ) ); |
此时两条特性均会对结果造成影响:
- 进入
target
元素时,同时打印mouseenter
与mouseover
- 进入子元素时,由于
mouseover
可以冒泡,此时以子元素为目标节点触发事件,事件传递会冒泡传递到target
元素上绑定的mouseover
事件,此时仅会打印mouseover
- 离开子元素回到父元素时,由于第二条特性影响,仅打印一次
mouseover
- 离开父元素时,同时打印
mouseleave
与mouseout
鼠标事件的定位属性
鼠标事件存在有几个定位属性:clientX
、clientY
、pageX
、pageY
、screenX
、screenY
,这些属性的区别在于坐标的参照对象不同,参考下图。
浏览器重绘
浏览器重绘简单来说就是浏览器根据 DOM 树的变化,重新绘制页面的过程。不作太多解释,仅说明一下特殊特性。
重绘的频率
通常来讲,重绘频率以屏幕刷新率为准,以 60Hz
屏幕为例,两次重绘的间隔不会小于 1/60s
即 16.67ms
。
如果连接有多个副屏,以主屏幕刷新率为准
什么时候触发重绘
对于浏览器来说,它会累计所有的重绘操作,然后在下一次重绘时一次性执行。而不是每次修改均进行重绘。
也就是说,无论在例如 16.67ms
内进行了多少修改 DOM 的操作,浏览器都只会进行一次重绘。
例如下面的示例中虽然修改了两次 DOM,但只会进行一次重绘。
1 | console.time( "start" ); |
强制同步布局
尽管重绘的发生频率是有限制的,但浏览器重新计算样式是可以随时发生的。一些不当的代码操作会导致频繁多次的重新计算样式,会极大的对性能造成影响。
例如对于如下代码:
1 | div1.style.height = "200px"; |
可以看到,重绘前只发生了一次重新计算样式。这次重新计算样式是因为元素高度改变导致了布局变更,需要在重绘前重新计算一次样式。
而我们只增加一条打印:
1 | div1.style.height = "200px"; |
而这次在重绘前发生了两次重新计算样式。且第一次在摘要中明确表示了已强制重新计算。
之所以会发生这种原因,是因为当我们进行了对布局有影响的操作后,若此时执行 clientHeight
、getBroundingClientRect()
等获取元素尺寸的方法,
浏览器为了获取真实的数据结果,会强制进行一次同步布局,即重新计算样式。
重新计算样式的开销是较大的,因为这种机制存在,我们尤其需要避免下面这种写法:
1 | elList.forEach( el => { |
进行两次遍历,先批量获取高度,再批量写入是性能更优的方式
1 | const heightList = elList.map( el => el.clientHeight ); |
requestAnimationFrame
requestAnimationFrame
是一个专门用于执行动画的方法,该方法会将回调函数推迟到下一次重绘时执行,浏览器在每次准备重绘时,
会检查是否存在 requestAnimationFrame
的回调函数,若存在则会执行。
因此该方法的执行频率与浏览器的重绘频率一致,即与屏幕刷新率有关。
1 | requestAnimationFrame( () => { |
它的回调函数参数是一个异步执行的函数,其不属于微任务队列。既然是异步任务就要老实按照事件循环的规矩来,主线程执行完成之前没有他什么事。
因此下面的代码会造成死锁,在 while 执行完毕之前,rAF 的回调函数永远都不可能被执行。不可以想当然的认为下一次重绘也就是 16.67ms
(60HZ
为例) 后就会执行。
1 | let i = 0; |
同时执行多个 rAF
当我们同时执行多次 requestAnimationFrame
时,浏览器会将他们集体保留到下一次重绘前,然后全部执行,执行完再进行重绘,哪怕他们非常耗时。
1 | for ( let i = 0; i < 100; i++ ) { |
上面代码将会在下一次重绘时一次性全部输出。
嵌套调用 rAF
可以通过嵌套调用 requestAnimationFrame
来实现一个与屏幕刷新率相同的逐帧动画。
1 | function animate() { |
上面的代码并不会造成死锁。我们以 60Hz
屏幕为例,每一次执行方法内操作后都会等待下次重绘即 16.67ms
后再次执行,因此这是一种安全的自递归调用。
闪烁动画
你可能希望实现一个毫秒级的逐帧动画,但因为浏览器的重绘频率限制,并不能很好的通过 setTimeout
来实现。
1 | setTimeout( () => { |
实际运行结果是我们有时候会看到红闪黑,有时候直接就是黑色。这是因为我们并不能保证这两次的执行间隔会不会刚好在一个重绘周期内。
而且由于我们无法确定用户屏幕的刷新率,也就无法得知重绘周期的具体时间,因此这个方法并不是一个很好的选择。
而 requestAnimationFrame
则可以保证每次修改结果均会紧跟浏览器的重绘,可以实现一个稳定的闪烁动画
1 | function blink() { |
啊,这闪瞎眼的效果
批量修改元素高度优化
对于 浏览器重绘-强制同步布局 中的案例,尽管已经把读写拆离开来,但依旧不能保证后续在重绘前是否有其他的读操作。
此时依旧会产生无意义的重新计算样式。
可以使用 requestAnimationFrame
来优化这个问题,将所有的写操作集中到一次重绘前执行。
1 | elList.forEach( el => { |
后端所设置的 cookie
res.cookie()
所设置的 ck,虽然在浏览器 response header 中的 Set-Cookies
已经得到显示,但会被浏览器同源策略拦截,无法写入到浏览器 cookies 中。
此时需要前端调用接口时设置 { withCredentials: true }
(xhr) 或 { credentials: "include" }
(fetch)。
1 | fetch( targetUrl, { |
同时后端也需要设置 Access-Control-Allow-Origin 为前端的源地址,例如 http://localhost:8080
,不能是 *
,而且还要设置 res.header("Access-Control-Allow-Credentials", true)
。
1 | app.use( "*", (req, res, next) => { |
若无法设置该请求属性,则必须让前后端域名/ip保持一致,不允许 127.0.0.1
访问 localhost
或相反。
location 的相关方法
location.assign()
立即跳转到指定地址,与修改 location.href
相同。
1 | location.assign( "https://marrydream.top/" ); |
location.replace()
同样是立即跳转到指定地址,但与 assign()
不同的是,replace()
会在 history
中替换当前地址记录,无法再通过浏览器的后退按钮返回。
1 | location.replace( "https://marrydream.top/" ); |
location.reload()
重新加载当前页面,相当于按下刷新按钮,可接受一个布尔值参数,用于控制是否强制刷新。
1 | location.reload( true ); |
location.toString()
返回当前页面的完整 URL,相当于读取 location.href
。
1 | location.toString() === location.href; // true |
URL的编码与解码
网页 URL 只能包含合法字符,合法字符分为两类:
- 语义字符:
a-z
、A-Z
、0-9
、-
、_
、.
、~
、!
、*
、'
、(
、)
- 保留字符:
$
、&
、+
、,
、/
、:
、;
、=
、?
、@
、#
除了这些字符以外,其他字符都需要进行编码转义,编码后的字符以 %
开头,后面跟着两位十六进制数。
JavaScript 提供了四个方法用于 URL 的编码与解码。
encodeURI()
将语义字符与保留字符之外的字符进行编码,因此常用来编码整个 URL。
1 | encodeURI( `https://marrydream.top/web/Web前端` ); // https://marrydream.top/web/Web%E5%89%8D%E7%AB%AF |
encodeURIComponent()
仅不对语义字符进行编码,常用来编码 URL 的组成部分,例如查询参数与哈希值。
1 | encodeURIComponent( `https://marrydream.top/web/Web前端` ); // https%3A%2F%2Fmarrydream.top%2Fweb%2FWeb%E5%89%8D%E7%AB%AF |
decodeURI() 与 decodeURIComponent()
分别是 encodeURI()
与 encodeURIComponent()
的解码方法。
URLSearchParams 对象
其构造函数不仅可以传入一个对象,还可以是一个 URL 参数字符串,或是二维数组。
1 | // 参数字符串,有没有 ? 都可以 |
使用 toString()
可以输出不带 ?
的完整参数字符串,会自动对参数进行编码,不需要手动处理。
1 | new URLSearchParams( "?game=窝窝屎" ).toString(); // game=%E7%AA%9D%E7%AA%9D%E5%B1%8E |
参数被存入 URLSearchParams
对象时,会自动进行解码,不需要自己手动处理
1 | const params = new URLSearchParams( "?game=窝窝屎" ); |
get() 与 getAll()
URL 参数这种东西是有可能存在多个同名参数的,因此便有了这两个方法。
get()
方法会返回第一个匹配的参数值,而 getAll()
方法会返回所有匹配的参数值。
1 | const params = new URLSearchParams( "?age=17&age=18" ); |
与 fetch 的结合
fetch
可以直接使用 URLSearchParams
对象作为参数,会自动将参数拼接到 URL 中。
1 | fetch( "http://xxx.example.top/api", { |
也可以利用字符串拼接自动调用 toString()
方法的特性,来直接拼接 url 地址。
1 | fetch( "https://api-kozakura.marrydream.top/public/pic/random?" + new URLSearchParams( { format: "100y50" } ) ); |
blob 对象
blob
对象是一种特殊的二进制对象,通常用于存储二进制数据,例如图片、音频、视频等。
创建 blob 对象
Blob 的构造函数接受两个参数:
1 | new Blob(array [, options]) |
array
: 数组,内部的元素可以是字符串、二进制数据,表示新生成的Blob
实例对象的内容options
: 对象,目前只有一个属性type
, 用于设置blob
对象数据的 MIME 类型,默认是空字符串
1 | new Blob( [ "Hello, World!" ], { type: "text/plain" } ); |
这里分别创建一个 html 和 JSON 数据,结合 createObjectURL()
方法将 blob
对象转换为临时地址,方便查看结果。
1 | const htmlBlob = new Blob( [ "<h1>Hello, World!</h1>" ], { type: "text/html" } ); |
打开链接,分别可以看到一个设定好内容的 html
页面和一个 json
数据。
实例属性和方法
Blob
实例对象有两个属性:
size
: 表示blob
对象的大小,单位是字节type
: 表示blob
对象的 MIME 类型
1 | const blob = new Blob( [ JSON.stringify( { name: "mari", age: 17 } ) ], { type: "application/json" } ); |
存在一个方法 slice()
,用于截取 blob
对象的一部分,返回一个新的 blob
对象,可以起到拷贝的作用。
1 | blob.slice( [start [, end [, contentType]]] ) |
start
: 数值,表示截取的起始位置,默认为0
end
: 数值,表示截取的结束位置,默认为blob.size
contentType
: 字符串,表示新blob
实例的 MIME 类型,默认为空字符串
File
File
继承自 Blob
,是 Blob
的一种特殊形式,所有可以使用 Blob
的地方都可以使用 File
。
最常见的应用场景是在上传文件时,通过 input[type="file"]
获取到的文件对象就是 File
对象。
创建 File 对象
File 的构造函数接受三个参数:
1 | new File(array, name [, options]) |
array
: 与Blob
构造函数的array
参数相同name
: 字符串,表示文件名options
: 有两个属性type
与lastModified
,分别表示文件的 MIME 类型与最后修改时间
实例属性
相比 Blob
,File
多了两个属性:
name
: 文件名或文件路径lastModified
: 文件的最后修改时间
FileReader
FileReader
是一个用于读取文件的对象,可以读取 Blob
或 File
对象的内容。
1 | const reader = new FileReader(); |
实例属性
FileReader 有下面一系列实例属性:
readyState
: 表示读取状态,有三个值:0
(未读取)、1
(正在读取)、2
(读取完成)result
: 表示读取结果,结果类型取决于读取的方法error
: 表示读取错误,如果读取失败则会有一个错误对象
实例方法
实例有四个读取方法,读取方法均接受一个 blob
或 file
对象,在读取完成后会将相应类型的值存入 result
属性中:
readAsArrayBuffer()
: 得到一个ArrayBuffer
对象readAsBinaryString()
: 得到一个原始二进制字符串readAsDataURL()
: 得到一个base64
编码的字符串readAsText()
: 得到一个文本字符串
还有一个手动中断的方法:
abort()
: 中断读取操作,会将readyState
设置为2
,result
设置为null
。
监听事件
这些事件的监听均可以通过 on
属性前缀或 addEventListener
方法来实现:
loadstart
: 开始读取时触发loadend
: 读取结束时触发progress
: 读取过程中触发load
: 读取完成时触发error
: 读取失败时触发abort
: 用户手动中断时触发
案例
我们来实现一个拖拽后读取图片,并显示在页面上的案例。
1 | <div id="container"></div> |
1 | const container = document.getElementById( "container" ); |
form HTML 标签的默认提交行为
此时我们有如下 form 表单:
1 | <form action="https://api-kozakura.marrydream.top/public/pic/random" method="get"> |
在上面这个 HTML 结构中,点击 Get Image
按钮后,会触发 submit
事件,并进行浏览器所定义的默认提交行为。
若表单中的
<button>
元素没有设置type
属性,那么也会触发默认提交行为。
这个默认提交行为根据 method
值的不同分为两种情况。
get
此时将忽略掉 action
中 URL 的参数部分,将每一项表单的 name
作为键,value
作为值,以 name=value
的形式拼接到 action
的 URL 后面作为查询参数。
然后把当前页面的 URL 替换为拼接后的 URL,完成页面的跳转。
例如上面的案例,最终将会把当前页面替换为 https://api-kozakura.marrydream.top/public/pic/random?resize=100y50&format=png
页面。
post
此时会分为几步进行:
- 将请求头的
Content-Type
设置为application/x-www-form-urlencoded
- 依旧是将每一项表单的
name
作为键,value
作为值,以name=value
的形式拼接为键值对,使用&
连接所有键值对 - 作为请求体发送给
action
所指向的地址
例如上面的案例,最终将会向 https://api-kozakura.marrydream.top/public/pic/random
发送一个 POST
请求,请求体为 resize=100y50&format=png
。
自定义提交行为
可以通过在 submit
事件监听器中调用 preventDefault()
方法来阻止默认提交行为。
如果再需要使用默认的提交行为,手动调用 formEl.submit()
方法。
1 | formEl.addEventListener( "submit", e => { |
canvas 绘制跨域图片
canvas
绘制跨域图片时会出现画布污染,禁止对该画布使用 toDataURL
等方法,可通过给图片设置跨域属性解决
1 | images[src].setAttribute( "crossOrigin", "Anonymous" ); |
阻止浏览器自动填充
通常情况下,可设置 autocomplete="off"
来阻止浏览器自动填充,但该操作仅对普通输入框有效。
1 | <input type="text" autocomplete="off" /> |
对于密码框,则应该使用 autocomplete="new-password"
。
1 | <input type="password" autocomplete="new-password" /> |
vue-router
动态添加路由后页面空白
通常会在 beforeRouter
中添加动态路由,若停留在被添加的动态路由地址刷新页面的话,会出现白屏的现象。
这是 addRoute
还未设置完路由导致的,此时应当在添加动态路由后重新进入一次当前页面。
1 | router.beforeEach( ( to, from, next ) => { |
设置 replace: true
来避免生成路由历史记录。
vite
文件系统的静态资源引入路径
使用文件系统路径的别名时,需要始终使用绝对路径。此时 vite
不再对相对路径与别名进行动态转换。
此时由于网站 url 的原因,相对路径将会被解析为错误的路径。而别名则会原封不动的被引入,导致路径错误。
文件系统指代图片资源外的普通文件,如
.mp3
等。
解决方案
使用 import.meta.url
或 import from
静态资源引入来得到正确的绝对路径。
1 | new URL( "assets/test.mp3", import.meta.url ).href; |