1. 如何理解"CommonJS 模块是运行时加载,ES6 模块是编译时输出接口"
这句话主要描述了 CommonJS 模块和 ES6 模块在加载和输出接口方面的两个不同特点。
运行时加载(CommonJS 模块)
CommonJS 模块是在代码运行时动态加载并执行。当需要使用到某个模块时,它会先读取该模块的文件内容,然后通过 require 函数将其解析为一个对象。这个过程是在运行时进行的,这也就意味着每次引入模块时,你都会得到一个新的对象实例。
// a.js
module.exports = {
message: "Hello, CommonJS!"
};
// b.js
const a = require("./a");
console.log(a.message); // 输出 "Hello, CommonJS!"
编译时输出接口(ES6 模块)
ES6 模块与 CommonJS 的区别在于,前者采用的是静态导出和导入的方式。这意味着,在代码编译阶段就能确定模块之间的依赖关系,而不必等到运行时。因此,ES6 模块能够实现更好的性能优化和代码分析。
// a.mjs
export const message = "Hello, ES6 Module!";
// b.mjs
import { message } from "./a.mjs";
console.log(message); // 输出 "Hello, ES6 Module!"
总结:CommonJS 模块是运行时加载,每次引入模块都会得到一个新的对象实例。而 ES6 模块是在编译阶段就确定了接口输出,有利于性能优化和代码分析。
2. 点击穿透
点击穿透(Click-through)是一个用户界面(UI)上的问题,指的是当用户点击屏幕时,事件可能会被传递到预期之外的元素,导致不正确或者意料之外的行为。在 Web 开发中,这种现象通常与 CSS 层叠、事件冒泡和捕获有关。
以下是解决点击穿透问题的一些建议:
- 阻止事件冒泡:使用
event.stopPropagation()阻止事件继续传递到父元素。这可以确保事件仅作用于触发它的元素。
element.addEventListener('click', (event) => {
event.stopPropagation();
// 你的事件处理逻辑
});
- 阻止默认行为:使用
event.preventDefault()来阻止浏览器对事件的默认响应。例如,此方法可防止链接跳转或表单提交。
element.addEventListener('click', (event) => {
event.preventDefault();
// 你的事件处理逻辑
});
- 使用 pointer-events:在 CSS 中,您可以使用
pointer-events属性来定义特定元素是否接收鼠标事件。要禁止元素接收鼠标事件,可以将其设置为none。
.element {
pointer-events: none;
}
请注意,pointer-events 不支持所有浏览器版本,请确保检查兼容性。
- 使用透明遮罩层:在需要阻止点击事件的区域上方放置一个透明遮罩层。此方法可确保用户点击遮罩时不会触发底下元素的事件。
<div class="overlay"></div>
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.0001); /* 使用极低透明度使遮罩仍具有背景颜色,以便接收点击事件 */
z-index: 9999; /* 保持遮罩位于其他元素之上 */
}
总之,要解决点击穿透问题,可以尝试这些方法,如阻止事件冒泡、阻止默认行为、使用 pointer-events 属性或添加透明遮罩层。查找最合适的方法取决于应用场景和需求。
3.如何解决跨域问题
跨域是一个安全策略,被浏览器用来限制一个域(domain)的文档或脚本如何与另一个源的资源进行交互。当你试图从一个域访问另一个域的资源时,就可能会遇到跨域问题。幸运的是,有几种方法可以解决跨域问题:
-
CORS(跨源资源共享):这是最常用的解决跨域问题的方法。在服务器端,你可以设置HTTP头
Access-Control-Allow-Origin来指定哪些域可以访问你的资源。例如,你可以设置Access-Control-Allow-Origin: *来允许所有域访问你的资源,或者设置Access-Control-Allow-Origin: https://example.com来只允许example.com访问你的资源。 -
JSONP(JSON with Padding):这是一种早期的解决跨域问题的方法,它利用了
<script>标签没有同源策略的限制。服务器返回一段可执行的JavaScript代码,这段代码以函数调用的形式包含了你需要的JSON数据。然而,JSONP有安全风险,并且只支持GET请求,所以现在很少使用。 -
代理服务器:如果你不能修改服务器来启用CORS,你可以设置一个代理服务器来转发你的请求。代理服务器在同源下接收你的请求,然后将请求转发给目标服务器,并将服务器的响应返回给你。
-
WebSockets:WebSockets不受同源策略的限制,所以可以用来进行跨域通信。然而,WebSockets主要用于实时、双向的通信,可能不适合所有的应用场景。
5.CORS(跨域资源共享)
跨源资源共享(CORS, Cross-Origin Resource Sharing)是一种机制,它使用额外的 HTTP 头来告诉浏览器,让运行在一个 origin(源)上的 Web 应用被准许访问来自不同源服务器上的指定的资源。当一个 web 应用进行 CORS 跨源请求时,浏览器会向服务器发送一个预检请求,通过预检请求询问服务器是否允许该跨源请求。
CORS机制作用在 Web 服务器上,可以允许服务器指定哪些站点可以访问服务器的资源。CORS 通过设置一些特殊的 HTTP 头信息来实现,最重要的是 Access-Control-Allow-Origin。这个头的值可以设为允许访问的源站点的地址或者 "*" 以允许所有的站点进行访问。
比如服务器返回的HTTP头信息中包含以下字段:
Access-Control-Allow-Origin: https://example.com
这意味着只有 example.com 这个源(origin)上的网页能够访问这个服务器的资源。
或者服务器返回的HTTP头信息中包含:
Access-Control-Allow-Origin: *
这意味着任何源(origin)上的网页都能访问这个服务器的资源。
除了 Access-Control-Allow-Origin,CORS还定义了一些其他的 HTTP 头,比如 Access-Control-Allow-Methods(用于指定允许的 HTTP 方法),Access-Control-Allow-Headers(用于指定允许的 HTTP 头),Access-Control-Allow-Credentials(用于指定是否允许发送 Cookie)等。
需要注意的是,CORS 并不会阻止请求被发送,而是在请求返回后由浏览器决定是否将响应结果传递给前端JavaScript。因此,即使请求被服务器接收并成功处理,如果响应的 CORS 头不正确,浏览器依然会报告一个错误,并不会让前端JavaScript接触到响应内容。
6. 为什么form表单提交没有跨域问题,但ajax提交有跨域问题
因为原页面用 form 提交到另一个域名之后,原页面的脚本无法获取新页面中的内容。
7. 服务器代理转发时,该如何处理 cookie?
当使用服务器代理转发请求时,处理cookie的问题就变得比较复杂。由于浏览器的同源策略,cookie默认只会发送给创建它的域,这意味着代理服务器需要进行一些特殊的处理才能正确地转发cookie。
以下是几种可能的处理方法:
-
设置代理服务器的域和目标服务器的域一致:这样,浏览器就会将cookie发送给代理服务器,代理服务器可以在转发请求时将cookie附加到请求头中。但是,这种方法需要对DNS和服务器的配置进行一些复杂的调整。
-
使用特殊的HTTP头来转发cookie:当浏览器向代理服务器发送请求时,你可以使用JavaScript读取cookie,并将它设置为一个特殊的HTTP头(例如
X-Forwarded-Cookie)。然后,代理服务器可以读取这个头,并将它作为cookie附加到转发的请求中。这种方法需要修改前端的JavaScript代码和代理服务器的代码,但不需要对DNS或服务器进行特殊配置。
无论使用哪种方法,你都需要确保你的代理服务器使用HTTPS来保护数据的安全,因为cookie可能包含敏感信息。此外,你也需要确保你的代理服务器只接受来自可信任源的请求,以防止被用作反向代理攻击的工具。
另外,你也需要考虑到第三方cookie的问题。由于浏览器的隐私设置,用户可能会阻止第三方cookie的发送。如果你的应用依赖第三方cookie,你可能需要向用户解释为什么你的应用需要这些cookie,或者寻找不使用cookie的替代方法。
所以浏览器认为这是安全的。
而 AJAX 是可以读取响应内容的,因此浏览器不能允许你这样做。
如果你细心的话你会发现,其实请求已经发送出去了,你只是拿不到响应而已。
所以浏览器这个策略的本质是,一个域名的 JS ,在未经允许的情况下,不得读取另一个域名的内容。但浏览器并不阻止你向另一个域名发送请求。
8. ES6的类
ECMAScript 6 (ES6),也被称为 ECMAScript 2015,引入了一种新的语法来创建对象和处理继承,这就是类(class)。JavaScript的类实际上是基于原型的继承的语法糖,它并没有引入一种与基于原型的继承完全不同的面向对象继承模型。
在ES6中,可以使用class关键字来定义一个类。下面是一个简单的例子:
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
getArea() {
return this.height * this.width;
}
}
const rectangle = new Rectangle(5, 8);
console.log(rectangle.getArea()); // 输出: 40
在上面的例子中,Rectangle是一个类,它有一个构造函数(constructor)和一个方法(getArea)。当你用new关键字创建一个新的Rectangle实例时,构造函数会被自动调用。this关键字在类中用来引用实例自身。
ES6类还支持继承,可以使用extends关键字来实现。例如:
class Square extends Rectangle {
constructor(sideLength) {
super(sideLength, sideLength);
}
}
const square = new Square(5);
console.log(square.getArea()); // 输出: 25
在这个例子中,Square类继承自Rectangle类。Square的构造函数调用了super函数,super函数会调用父类的构造函数,用来设置父类的属性。
这就是ES6类的基本用法。使用类可以让JavaScript的面向对象编程更加清晰和简单。
9. documen.write 和 innerHTML 的区别?
document.write() 和 .innerHTML 都是浏览器中用来操作 DOM(Document Object Model,文档对象模型)的方法,但它们的使用场景和行为有所不同。
-
document.write()
document.write()是一个较为老旧的 JavaScript 方法,主要在文档加载过程中直接写入 HTML 输出流中。如果在文档加载完成后(例如在window.onload事件处理函数中)调用document.write(),那么整个文档内容会被清空,然后被document.write()的参数替代。示例:
document.write("<p>Hello World!</p>");
以上代码会在文档中添加一个新的段落。如果这段代码在文档加载完成后运行,那么它将清空整个文档,并只显示 "Hello World!" 这一段落。
2. .innerHTML
`.innerHTML` 是一个 DOM 元素的属性,可以用来获取或设置该元素的 HTML 内容。与 `document.write()` 不同,`.innerHTML` 不会影响到其他的元素,只影响到设置了 `.innerHTML` 的元素。
示例:
document.getElementById("myDiv").innerHTML = "<p>Hello World!</p>";
以上代码会找到 ID 为 "myDiv" 的元素,然后将其内容设置为 "Hello World!" 这一段落。这并不会影响到其他的元素。
比较
总的来说,document.write() 是一个较为老旧的方法,现代的 Web 开发中很少使用。因为如果在文档加载完成后使用,会清空整个文档,这可能会导致一些预期之外的效果。另一方面,.innerHTML 则更加灵活,可以用来改变某个元素的内容,而不会影响到其他元素。但是,它也有一些缺点,比如它无法添加 <script> 元素,并且可能存在 XSS(跨站脚本攻击)的安全风险。如果你需要在运行时动态地添加或修改 HTML,你可能会更喜欢使用其他的方法,比如 createElement、appendChild 等 DOM API,或者使用一些库(如 jQuery、React 等)提供的方法。
使用 .innerHTML 来添加 <script> 元素确实可以将 <script> 标签添加到 HTML 结构中,但新添加的脚本并不会执行。这是因为 .innerHTML 会解析和添加 HTML 内容,但不会执行由此方法添加的 <script> 标签内的 JavaScript。
然而,这并不意味着使用 .innerHTML 就能完全避免 XSS 攻击。例如,攻击者可能会尝试插入像 <img src='x' onerror='恶意代码'> 这样的代码。在这种情况下,当图片加载失败时,onerror 事件会被触发,而 onerror 属性内的恶意代码会被执行。
因此,当你处理用户输入或者其他不可信的数据并使用 .innerHTML 插入到文档中时,你应该始终对这些数据进行适当的清理或者转义,以防止可能的 XSS 攻击。
10. 什么是 Virtual DOM?为什么 Virtual DOM 比原生 DOM 快?
Virtual DOM(虚拟DOM)是一种编程概念,在这个概念中,一个 "虚拟" 的节点树用于模拟真实的浏览器DOM的变更。这个模型提供了一种在Javascript中更方便高效地更新DOM的方法。
虚拟DOM的工作原理如下:
-
当应用的状态改变时,全新的虚拟DOM树会被创建(这个过程非常快,因为这发生在内存中,而不是在实际的DOM上)。
-
然后,新的虚拟DOM树与旧的树进行比较,这个过程称为 "diffing"。框架会找出两棵树中的不同,也就是实际DOM需要做出什么改变来反映新的应用状态。
-
最后,这些改变会在实际的DOM上进行,这个过程称为 "reconciliation"。这个更新过程是高效的,因为只有实际需要改变的部分才会被更改,而不需要重建整个DOM树。
然后,你可能会问,为什么虚拟DOM会比直接操作原生DOM快呢?
原因在于直接操作DOM的代价非常高昂。创建节点、删除节点、修改节点等操作都很复杂,并且如果频繁操作DOM,可能会导致页面重绘和重排,这会使得用户体验变差。然而,使用虚拟DOM技术,只有当必要的时候,才会把变更应用到真实DOM上,而且是以最小的必要改动,这样可以大大提高性能。
要注意的是,虚拟DOM并不总是比原生DOM快。在大部分情况下,由于避免了不必要的DOM操作,虚拟DOM会更快,但在一些特殊的场景中,虚拟DOM可能会比原生DOM慢,比如当你需要频繁地进行大量的DOM更新时。然而,对于大多数的web应用来说,虚拟DOM提供的性能优势和易用性使得它成为了一种很好的解决方案。
11. 比较两个 DOM 树的差异的时间复杂度
O(n^3)
在理论上,比较两个树形结构的差异(也称为树形差异或树形对比)确实是一个非常昂贵的操作,时间复杂度可以达到 O(n^3),其中 n 是树中节点的数量。这是因为在最坏的情况下,你可能需要比较树中的每一个节点与其他所有节点。
总结一下,对于旧树上的点A来说,它要和新树上的所有点比较,复杂度为O(n),然后如果点A在新树上没找到的话,点A会被删掉,然后遍历新树上的所有点找到一个去填空,复杂度增加为了O(n^2),这样的操作会在旧树上的每个点进行,最终复杂度为O(n^3)。
然而,这只是理论上的最坏情况。在实际应用中,如 React 和其他使用虚拟 DOM 的框架,都使用了优化算法,使得在大多数情况下,比较两个虚拟 DOM 树的复杂度接近于线性。
然而,对于虚拟DOM,框架如React使用了一些策略来优化这个过程,使得时间复杂度在大多数情况下接近于 O(n)。
-
同层比较:React只会将同一层级的元素进行比较,这大大减少了需要比较的节点数。这意味着它不会尝试匹配不同层级的元素。
-
类型比较:如果两个元素的类型不同(例如一个是div,一个是p),React会直接替换这个元素及其所有子元素,而不尝试进一步比较它们的差异。
-
Key属性:通过为列表中的每个子元素赋予一个稳定的key,React可以快速匹配和确定哪些子元素被添加,删除,或移动。
通过这些策略,React 可以在实践中高效地比较虚拟DOM树的差异,即使在理论上这是一个复杂的问题。
11. 什么是函数式编程
函数式编程(Functional Programming,简称 FP)是一种编程范式,它将计算过程视为一系列的函数调用。函数式编程强调的是"表达什么"而不是"怎么做",即注重描述问题而非过程。它有以下的一些特点和原则:
-
纯函数:函数式编程强调使用纯函数,也就是说,对于相同的输入,纯函数总是会返回相同的输出,并且没有副作用。这使得纯函数非常容易进行测试和理解。
-
不可变性:在函数式编程中,数据一旦被创建,就不能改变。如果你需要修改某些数据,你应该创建一个新的副本。这消除了许多错误和复杂性,因为你不需要担心数据被其他部分的代码改变。
-
函数是一等公民:在函数式编程中,函数被视为"一等公民",这意味着你可以将函数作为参数传递给其他函数,或者作为其他函数的返回值。
-
声明式编程:函数式编程是声明式的,而不是命令式的。这意味着你的代码更关注你想要的结果是什么,而不是如何达到这个结果。
-
递归:由于函数式编程避免使用状态和可变数据,所以循环构造通常会被递归替代。
JavaScript并不是一种纯粹的函数式编程语言,但是它支持函数式编程风格,并提供了一些功能来支持这种风格,例如高阶函数、闭包等。其他的一些纯函数式编程语言包括Haskell和Erlang。
12. Object.defineProperty()
Object.defineProperty() 是一个 JavaScript 中的方法,用于在一个对象上定义新的属性或修改现有属性,并返回这个对象。
在你的示例中,Object.defineProperty() 被用于向 pojo 对象添加一个名为 "sex" 的属性,其值为 "male",并且这个属性被设置为不可枚举(enumerable: false)。
"不可枚举" 意味着这个属性不会出现在对象的枚举属性中。换句话说,当你遍历一个对象的属性时,这个 "sex" 属性不会被包含在内。这可以通过 for...in 循环或 Object.keys() 方法来检验。
那么,为什么要设置一个属性为 "不可枚举" 呢?主要有以下原因:
-
隐藏一些内部状态:有时,你可能想要在一个对象上设置一些内部状态,这些状态不应该被外部代码所看到。将这些属性设置为不可枚举,可以避免这些属性被无意中遍历到。
-
防止属性被修改:将属性设置为不可枚举,可以在一定程度上防止这个属性被修改。虽然 JavaScript 并没有提供真正的私有属性,但是通过设置不可枚举属性,可以使得这个属性更不容易被误操作。
-
模仿内建对象的行为:很多内建的 JavaScript 对象(如 Array、Date 等)有一些内部的、不可枚举的属性。如果你正在创建一个类似的自定义对象,你可能也想让一些属性不可枚举,以使得你的对象在行为上更接近内建对象。
注意,尽管不可枚举的属性在遍历时不会被包含,但是它们仍然可以被直接访问和修改。例如,如果你有一个对象 pojo,你仍然可以通过 pojo.sex 来访问和修改 "sex" 属性,即使它是不可枚举的。
13. JSON.stringify的局限性
两个定义——序列化安全、序列化不安全。
- 序列化安全——当某个类型变量/常量经过序列化-反序列化过程后仍能保持原类型和值,则该类型/常量是序列化安全的。
- 序列化不安全——当某个类型变量/常量经过序列化-反序列化过程后无法保持原类型和值,则该类型/常量是序列化不安全的。
JSON.stringify方法的详细规则如下:
- 对于JavaScript中的六种原始类型,JSON语法支持数字、字符串、布尔值、null四种,不支持undefined,Symbol;
- NaN、Infinity和-Infinity序列化的结果是null;
- JSON语法不支持函数;
- 对于Set,Map,RegExp、Error对象,JSON语法无法保证序列化安全;
- 日期对象序列化的结果是ISO格式的字符串(调用了Date对象的toJSON方法),但JSON.parse()依然保留它们字符串形态,并不会将其还原为日期对象;
- JSON.stringify()只能序列化对象的可枚举的自有属性;
14. 虚拟DOM是如何提高应用程序性能的?
提高性能的本质都是为了:减少操作真实 DOM 以减少性能消耗, 方法有两类:
- 通过将 DOM 操作转换为对 JavaScript 对象的操作
- 因为真实 DOM 的操作会引起页面的重绘和重排,这是非常消耗性能的。而虚拟 DOM 操作只需要更新 JavaScript 对象,不会引起页面的重绘和重排。
- 通过 diff 算法等优化策略
- 在更新虚拟 DOM 树时,会通过 diff 算法比较新旧虚拟DOM树的差异,只更新需要更新的部分,从而减少对真实DOM的操作次数。
- 虚拟 DOM 还使用批处理技术,将多个DOM操作合并为单个操作,从而进一步提高性能。
15. 输入URL到页面呈现
- 浏览器通过DNS服务器得到域名的IP地址,向这个IP地址请求得到HTML文本
- 浏览器渲染进程解析HTML文本,构建DOM树
- 解析HTML的同时,如果遇到内联样式或者样式文件,则下载并构建样式规则,如果遇到JavaScript脚本,则会下载执行脚本
- DOM树和CSSOM构建完成之后,渲染进程将两者合并成渲染树(render tree)
- 渲染进程开始对渲染树进行布局,生成布局树(layout tree)
- 渲染树对布局树进行绘制,生成绘制记录
16. 为什么js脚本要放在html最后最好
将 JavaScript 脚本放在 HTML 文档的最后(通常是 </body> 标签之前)是一种常见的最佳实践。这主要有以下几个原因:
-
阻塞行为:当浏览器在解析 HTML 文档并遇到
<script>标签时,它会停止 HTML 解析,然后开始下载脚本文件(如果src属性存在),之后解析和执行脚本,最后再恢复解析 HTML。如果将大型脚本放在<head>中或在 HTML 文档的上部,那么用户将会看到一个空白页面,直到脚本完成加载和执行,这可能会影响用户体验。 -
DOM 依赖:许多 JavaScript 代码会操作 DOM 元素,比如添加事件监听器、改变元素样式等。如果脚本在目标元素之前执行,那么目标元素可能还未被浏览器解析和创建,这会导致错误或不期望的行为。将脚本放在
</body>标签之前,可以保证当脚本执行时,所有的 DOM 元素都已被完全加载。 -
顺序执行:JavaScript 脚本默认是顺序执行的。如果你有多个脚本并且它们之间有依赖关系,那么你需要确保它们按正确的顺序执行。
需要注意的是,虽然将脚本放在 HTML 文档底部是一种常见的最佳实践,但在有些情况下,其他方法可能更好。例如,你可以使用 async 或 defer 属性来改变脚本的加载和执行行为。async 属性可以让脚本异步加载,而 defer 属性可以让脚本在文档解析完成后执行。这些方法都可以帮助提高页面加载速度,但是需要注意处理好脚本间的依赖关系。
17. 标记清除和引用计数
标记清除
JavaScript 采用垃圾回收机制来自动回收不再需要的对象所占用的内存。一种常用的垃圾回收算法是 "标记-清除"(Mark-and-Sweep)。
这个算法分为两个阶段:标记(Mark)阶段和清除(Sweep)阶段。
-
标记阶段:这个阶段的任务是遍历所有的对象,查找出所有活动对象(也就是还在被使用的对象)。这通常是通过从一组根(root)对象开始,递归地访问所有从根对象可达的对象。在 JavaScript 中,根对象通常是全局对象以及当前执行上下文中的对象。这些可访问的对象被标记为活动的,其余的对象则被认为是非活动的,也就是垃圾。
-
清除阶段:在所有的对象都被标记后,清除阶段就会开始。在这个阶段,垃圾回收器会释放所有非活动对象所占用的内存。这通常是通过简单地将这些对象所占用的内存标记为可用,以便于后续的内存分配。
需要注意的是,虽然标记-清除算法相对简单,但是它也有一些潜在的问题。例如,它可能会导致内存碎片化,因为被释放的对象可能会分散在内存中的各个地方。此外,标记-清除算法需要停止所有的 JavaScript 执行,直到整个过程完成,这可能会导致性能问题。为了解决这些问题,许多现代的 JavaScript 引擎采用了更复杂的垃圾回收算法,比如分代收集(Generational Collection)和增量收集(Incremental Collection)等。
标记整理
标记阶段与标记清除法没什么区别,只是标记结束后,标记整理法会将存活的对象向内存的一边移动,最后清理掉边界内存。
引用计数
引用计数的含义是跟踪记录每个值被引用的次数。当一个变量A被赋值时,这个值的引用次数就是1,当变量A重新赋值后,则之前那个值的引用次数就减1。当引用次数变成0时,则说明没有办法再访问这个值了,所以就可以清除这个值占用的内存了。
18. CommonJs和Es Module的区别
CommonJs
- CommonJs可以动态加载语句,代码发生在运行时
- CommonJs混合导出,还是一种语法,只不过不用声明前面对象而已,当我导出引用对象时之前的导出就被覆盖了
- CommonJs导出值是拷贝,可以修改导出的值,这在代码出错时,不好排查引起变量污染
Es Module
- Es Module是静态的,不可以动态加载语句,只能声明在该文件的最顶部,代码发生在编译时
- Es Module混合导出,单个导出,默认导出,完全互不影响
- Es Module导出是引用值之前都存在映射关系,并且值都是可读的,不能修改
19. HTTP 缓存策略:强缓存和协商缓存
20. 深浅拷贝: 原理与实现
于Object.assign()而言,如果对象的属性值为简单类型(string,number),通过Object.assign({},srcobj);得到的新对象为深拷贝;如果属性值为对象或其他引用类型,那对于这个对象而言其实是浅拷贝的,这是Object.assign()特别需要注意的地方。
21. JS继承
JS继承 JS继承面试的时候怎么说?答应我,不要再死记硬背了好吗?
寄生式组合继承
-
利用原型链继承,实现实例对父类原型(
Animal.prototype)方法与属性的继承; -
借用构造函数继承,实现实例对父类构造函数(
function Animal() {})里方法与属性的继承 -
使用寄生式继承解决了组合继承带来的缺陷。
22. Web Worker——JS 的多线程
-
使用 worker 构造函数创建 worker 线程,需要传递一个同源脚本文件 URL;
-
使用
postMessage、onmessage方法事件进行消息传递; -
通过
onerror事件处理异常,使用close方法关闭线程; -
worker 线程任务可以是 JS 脚本文件,也可以是内联脚本;
-
在项目中使用 worker,webpack 4 需要安装
worker-loader,webpack 5 和 vite 原生支持。
一些限制:
- 同源限制:创建 worker 线程的时候需要分配一个 JS 文件,该文件必须是同源的,且不能是本地文件;
- 环境隔离:worker 线程所在的上下文环境与主线程不一样,无法读取网页的 DOM 对象,全局对象不再是 window,可以通过 this 或 self 访问。
- 通信受限:主线程和 worker 线程不能直接通信,通过
postMessage方法进行消息传递。
23. DOM事件机制(事件代理,事件捕获,事件冒泡)
DOM 0级
事件处理的步骤:先找到DOM节点,然后把处理函数赋值给该节点对象的事件属性。
DOM0级事件处理程序的缺点在于一个处理程序「事件」无法同时绑定多个处理函数,比如我还想在按钮点击事件上加上另外一个函数。
var btn = document.getElementById('btn');
btn.onclick = function() {
alert('Hello World');
}
btn.onclick = function() {
alert('没想到吧,我执行了,哈哈哈');
}
DOM 2级
DOM2级事件在DOM0级事件的基础上弥补了一个处理程序无法同时绑定多个处理函数的缺点,允许给一个处理程序添加多个处理函数。也就是说,使用DOM2事件可以随意添加多个处理函数,移除DOM2事件要用removeEventListener。代码如下:
<button type="button" id="btn">点我试试</button>
<script>
var btn = document.getElementById('btn');
function fn() {
alert('Hello World');
}
btn.addEventListener('click', fn, false);
// 解绑事件,代码如下
// btn.removeEventListener('click', fn, false);
</script>
事件代理
由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)。
那么利用事件冒泡或捕获的机制,我们可以对事件绑定做一些优化。 在JS中,如果我们注册的事件越来越多,页面的性能就越来越差,因为:
- 函数是对象,会占用内存,内存中的对象越多,浏览器性能越差
- 注册的事件一般都会指定DOM元素,事件越多,导致DOM元素访问次数越多,会延迟页面交互就绪时间。
- 删除子元素的时候不用考虑删除绑定事件
优点:
- 减少内存消耗,提高性能
- 动态绑定事件
e.currentTarget和e.target
e.target指向触发事件监听的对象「事件的真正发出者」。e.currentTarget指向添加监听事件的对象「监听事件者」。