前端知识中有很多相近的概念或 API,相信不少人在开发中有注意到这些相近的概念或 API,但是有时不会深入去了解异同,只要某个 API 能满足开发需求即可。
本文将介绍一些相近的概念和 API,让你能更清晰地了解它们的异同,在使用时更游刃有余。
1. cookie vs localStorage vs sessionStorage
前端开发中,这三个本地存储方案可以说是很常见的,用一张图说明下它们的区别:
图片来源:local-storage-vs-session-storage-vs-cookies[1]
图中从存储大小、是否自动过期、服务端是否可以获取、是否支持 HTTP 请求传输和数据持久性方面进行对比。除了图中几个部分,在作用域方面,cookie 由域名和路径决定,localStorage 和 sessionStorge 都是遵守同源策略。
最后再提几个关于在使用 sessionStorage 的时偶尔会陌生的知识点:
-
sessionStorage 数据在各个直接打开的浏览器页签中是不会同步的,这意味着你打开了两个同域名的网站,在其中一个设置了 sessionStorage 数据,另一个页面是不会同步这个数据的(而 localStorage 会),也就是说 sessionStorage 除了关闭浏览器时不会保留数据,各个页签数据的同步也和 localStorage 不一样。 -
如果你在当前页设置了一些 sessionStorage 数据,然后通过 window.open
或<a>
标签打开,新页签会同步一份当前页副本,随后两个页签的 sessionStorage 又会是独立的,不过要注意打开新页签的rel
属性(用于指定当前文档与被链接文档的关系)要设置为opener
。
2. querySelectorAll vs getElementsByTagName
querySelectorAll
可以根据传入的 CSS 选择器查找 HTML 元素,使用上比 getElementsByTagName
更灵活。
它们之间的不同点在于:querySelectorAll
返回的是一个静态的 NodeList,而 getElementsByTagName
返回的是动态的。
来看下面这个示例:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
接下来使用两个方法获取 li
元素类数组,然后再动态插入一个 li
,最后查看两个类数组的长度。
const listItems = document.querySelectorAll('li');
const listItems2 = document.getElementsByTagName('li');
console.log(listItems.length, listItems2.length); // 3,3
const list = document.querySelector('ul');
const li = document.createElement('li');
li.innerHTML = '4';
list.appendChild(li);
console.log(listItems.length, listItems2.length); // 3, 4
可以看到 querySelectorAll
方法获取的类数组长度在动态添加 li
后还是 3,而 getElementsByTagName
的为 4。
常用的获取元素方法中getElementsByClassName
方法、element.childNodes
和 element.children
返回的也是动态 NodeList。
3. children vs childNodes
children
和 childNodes
都可以用来获取元素的子节点,不同的是 children
只会获取 HTML 元素节点,而 childNodes
会获取到非 HTML 元素节点,包括文本、注释节点等。
<ul>
<!-- 这里有有些内容 -->
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
const parent = document.querySelector('ul');
// 输出 HTMLCollection(3) [li, li, li]
console.log(parent.children)
// 输出 NodeList(10) [text, comment, text, text, li, text, li, text, li, text]
console.log(parernt.childNodes)
4. microtasks vs macrotasks
宏任务和微任务概念也经常在前端中出现,与之相关的就是事件循环机制。事件循环机制是必须掌握的,宏任务和微任务也可以了解下,实际开发中碰到相关问题能反应过来是宏任务和微任务的不同即可。
宏任务包括:
-
setTimeout
andsetInterval
的回调 -
DOM 操作 -
I/O 操作 (Node 中读写文件) -
requestAnimationFrame
微任务包括:
-
Promises
的 resolve 和 reject -
MutationObserver
回调 -
Node 中的 process.nextTick
事件循环机制如下图:
宏任务微任务执行顺序如下图:
最后配合一个例子看下效果:
console.log('Script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
new Promise((resolve) => {
console.log('Promise')
}).then(function () {
console.log('Promise then')
})
console.log('Script end')
// 输出顺序为: Script start、Promise、Script end、Promise then、setTimeout
一个更清晰的图(源[2]):
5. setTimeout(0) vs requestAnimationFrame
setTimeout(0)
和 requestAnimationFrame
都能把代码延迟到下一个动画帧运行,它们的不同在于:
-
setTimeout(0)
将代码推到事件循环的任务队列中,如果任务队列中有大量任务,setTimeout(0)
就不会立即执行。 -
requestAnimationFrame
会在下一次渲染前执行,而不是在事件循环中执行,它能自动与显示器刷新率同步。不过,它只有在浏览器准备好渲染新帧时才会执行,如果标签页处于非激活状态,它就不会运行。
处理动画时,requestAnimationFrame
更合适, 如果你要延迟执行代码的话,可以直接使用 setTimeout(0)
。
补充一个小点:setTimeout
的语法是 setTimeout(functionRef, delay, param1, param2, /* … ,*/ paramN)
,除了回调函数和延迟时间,后续参数都会作为回调函数的参数。
// 1 秒后输出 delay 1s
setTimeout(console.log, 1000, 'delay 1s')
6. naturalWidth vs width
naturalWidth
是元素的自然宽度,它永远不会改变。例如,一张 100px 宽的图片的 naturalWidth
始终是 100px,即使通过 CSS 或 JavaScript 调整图片大小后也不变。
而 width
是可以改变的,可以通过 CSS 或 JavaScript 设置。
7. stopImmediatePropagation vs stopPropagation
stopImmediatePropagation()
方法与 stopPropagation()
方法一样,可阻止事件冒泡。但是,stopImmediatePropagation()
方法会阻止元素同一事件的其他监听器。
button.addEventListener('click', function () {
console.log('foo')
})
button.addEventListener('click', function (e) {
console.log('bar')
e.stopImmediatePropagation()
})
button.addEventListener('click', function () {
console.log('baz')
})
上面代码中按钮点击后只会输出 foo
and bar
,baz
的事件监听函数不会触发。
8. HTML 字符实体 vs Unicode 字符
HTML 实体是特殊字符序列,用来表示可能被误认为是 HTML 代码的字符,如小于号 (<
) 或双引号 (&
)。
下面是一些常见的 HTML 实体:
-
<
代表小于号<
-
>
代表大于号>
-
&
代表于符号&
-
"
代表双引号"
-
'
或'
代表单引号'
-
代表空格
HTML 字符实体相比 Unicode 字符会更好记些,同时浏览器对 HTML 字符实体支持更好。
Unicode 是表示字符或符号的特定代码,它们用于显示标准字符集中可能没有的字符,如非拉丁字母或特殊符号。
一些 Unicode 字符示例:
-
u00A9
表示版权符号 (©) -
u2192
表示右箭头 (→) -
u2615
代表咖啡杯 (☕) -
u1F60E
代表戴着墨镜的笑脸 (😎) -
u2764
表示一颗红心 (❤)
Unicode 可以表示任何语言的任何字符或符号,不过旧版本浏览器的支持性可能没那么好。
9. script async vs script defer
当浏览器碰到 script
标签时,会执行以下步骤:
-
暂停文档解析 -
创建一个新请求来下载脚本 -
下载完成后执行脚本 -
继续解析文档
script
标签会阻塞整个文档的解析,为了提供更好的体验,HTML5 为 script
标签提供了两个属性,它们是 async
和 defer
。
<script src="/path/to/script.js" async></script>
<script src="/path/to/script.js" defer></script>
这两个属性让浏览器知道,该脚本与文档解析可以同时进行。
async
和 defer
的效果如上图。
async
会在下载完成后立即执行(下载不阻塞 HTML 解析,执行会),所以多个 script
标签都使用 async
属性的话,是不能保证多个 script
的执行顺序,而使用 defer
的话,下载完后会等待 HTML 解析完成再执行,可以保证多个 script
的执行顺序。
所以 async
一般在独立的脚本上使用,如埋点脚本。
还有一点,动态加载的脚本 async
默认为 true,如果你不需要,可以设置为 false
:
const script = document.createElement('script');
script.src = '/path/to/script.js';
script.async = false;
document.head.appendChild(script);
10. __proto__ vs prototype
__proto__
和 prototype
的区别很简单:
-
__proto__
是对象实例的属性 -
prototype
是构造函数的属性
当你使用 __proto__
时,你是正在查找对象原型链上的属性和方法,而 prototype
对象定义了所有实例都将拥有的共享属性和方法。
如上图,Letter
函数的 prototype
属性和其三个实例的 __proto__
属性都是指向 Letter
的原型链对象 Letter.prototype
。
11.Dependencies vs devDependencies vs peerDependencies
dependencies
代表依赖项是项目中的一部分,最终会被一起打包到生产代码中,当你执行 npm install
时,你依赖的那个包的依赖也会自动安装,比如你项目使用到了 antd
, npm install
时 antd
的依赖项 classnames
也会被安装,这就是你有时候没安装一些库,但是也可以使用的原因。
"dependencies": {
"lodash": "^4.17.21"
}
devDenpendencies
代表依赖项是仅在开发过程中才需要的,代码的最终生产版本并不需要这些依赖项。
"devDependencies": {
"jest": "^29.6.4"
}
peerDependencies
代表使用这个库时需要的依赖项,和 dependencies
不同的是,它不会在 npm install
时被安装,需要你显式的在自己项目下安装。各个包管理器的各个版本对 peerDependencies
的处理可能都不同,有兴趣的可以继续深入了解。
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
},
12. isNaN vs Number.isNaN
isNaN
是一个全局函数,用于判断参数是否为 NaN
,不过,在判断参数是否为 NaN
之前,它会尝试先将参数转换为数字。
isNaN('hello'); // true
isNaN(undefined); // true
isNaN({}); // true
isNaN([]); // false +[] === 0
isNaN(42); // false
在 ES6 中引入了 Number.isNaN
函数,与 isNaN
不同的是,在判断前 Number.isNaN
不会转换参数。
Number.isNaN('hello'); // false
Number.isNaN(undefined); // false
Number.isNaN({}); // false
Number.isNaN([]); // false
Number.isNaN(42); // false
Number.isNaN(NaN); // true
一般来说,使用 Number.isNaN
比 isNaN
更准确。
13. 默认参数 vs 或操作符
JavaScript 提供了两种为函数参数设置默认值的方法:使用默认参数或 OR (||
) 操作符,两者在最终效果上会有一些不同。
先来看默认参数:
const sayHello = (name = 'World') => {
console.log(`Hello, ${name}!`);
};
sayHello(); // `Hello, World!`
sayHello(undefined); // `Hello, World!`
sayHello(null); // `Hello, null!`
sayHello(''); // `Hello, !`
sayHello("Phuoc Nguyen"); // `Hello, Phuoc Nguyen!`
可以看到默认参数只有为 undefined
的时候,默认参数才会生效。不传和传 undefined
效果一致。
再来看或操作符:
const sayHello2 = (name) => {
const withDefaultName = name || 'World';
console.log(`Hello, ${withDefaultName}!`);
};
sayHello2(); // `Hello, World!`
sayHello2(undefined); // `Hello, World!`
sayHello2(null); // `Hello, World!`
sayHello2(''); // `Hello, World!`
sayHello2("Phuoc Nguyen"); // `Hello, Phuoc Nguyen!`
可以看到参数只要是 falsy
值(undefined
、null
、NaN
、0
、""
和 false
),都会使用代码中默认参数,这个就是和 ES6 默认参数不同的地方。
14. null vs undefined
null
和 undefined
的不同点如下:
-
undefined
表示变量已经被声明,但未被赋值;null
用来表示变量没有值。
let foo;
console.log(foo); // undefined
let foo = null;
console.log(foo); // null
-
undefined
和null
代表的类型不同。
console.log(typeof undefined); // 'undefined'
console.log(typeof null); // 'object'
除了以上两点不同之外,还有两点值得关注的:
-
undefined
和null
进行比较的结果。
null == undefined; // true
null === undefined; // false
-
JSON.stringify
会忽略undefined
, 但是会保留null
。
JSON.stringify({
name: 'John',
address: null,
age: undefined,
});
// {"name":"John","address":null}
小结
前端中还有很多相近的概念和 API,在业务开发时可能没时间去了解,但是有空的时候还是可以花点时间去掌握其中的异同,扎实自己的前端基础。
原创文章,作者:guozi,如若转载,请注明出处:https://www.sudun.com/ask/79569.html