JavaScript进阶知识要点
JS事件
绑定事件与解除事件
绑定事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const element = document.getElementById("myElement");
element.addEventListener("click", function (event) { console.log('元素被点击了'); });
element.addEventListener("keydown", function (event) { console.log(`按下了键:${event.key}`); });
|
解除事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const element = document.getElementById('yourElementId');
function clickHandlerFunction() { }
function keydownHandlerFunction() { }
element.removeEventListener("click", clickHandlerFunction);
element.removeEventListener("keydown", keydownHandlerFunction);
|
事件冒泡和事件捕获的区别,如何阻止?
事件冒泡(Bubbling):
事件从触发事件的目标元素开始,逐级向上冒泡到 DOM 树的根节点。
首先执行目标元素上的事件处理程序,然后是父元素,再是更高层次的祖先元素。
事件冒泡是默认的事件传播方式。
事件捕获(Capturing):
事件从 DOM 树的根节点开始,逐级向下捕获到触发事件的目标元素
首先执行根节点上的事件处理程序,然后是子元素,再是更低层次的子孙元素。
事件捕获通常需要显式启用,通过 addEventListener的第三个参数设置为 true 来启用事件捕获。
**应用:**addEventListener第三个参数:true 为捕获,false 为冒泡,默认false。event.stopPropagation()阻止冒泡
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
| <!DOCTYPE html> <html>
<head> <style> .parent { padding: 20px; background-color: lightgray; }
.child { padding: 20px; background-color: lightblue; } </style> </head>
<body> <div class="parent"> <div class="child"> <button id="btn">点击我</button> </div> </div> <script> const parent = document.querySelector('.parent'); const child = document.querySelector('.child'); const btn = document.getElementById('btn');
parent.addEventListener('click', function (event) { console.log('父级元素点击事件'); }, true);
child.addEventListener('click', function (event) { console.log('子级元素点击事件'); });
btn.addEventListener('click', function (event) { console.log('按钮点击事件'); event.stopPropagation(); }, false); </script> </body>
</html>
|
JS 事件循环机制-宏任务微任务是如何工作的?
1.同步任务直接执行
2.遇到微任务放到微任务队列(Promise.then/process.nextTick 等等)
3.遇到宏任务放到宏任务队列(setTimeout/setInterval 等等)
4.执行完所有同步任务
5.执行微任务队列中的任务
6.执行宏任务队列中的任务
问打印顺序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| console.log(1); Promise.resolve().then(() => { console.log(2); setTimeout(() => { console.log(3); }, 0); setTimeout(() => { console.log(4); new Promise((resolve) => { console.log(5); resolve(); }).then(() => { console.log(6); }); }, 0); console.log(7); });
|
分析过程
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
| console.log(1);
const microTaskQueue = [];
const macroTaskQueue = [];
Promise.resolve().then(() => { console.log(2); setTimeout(() => { console.log(3); }, 0); setTimeout(() => { console.log(4); new Promise((resolve) => { console.log(5); resolve(); }).then(() => { console.log(6); }); }, 0); console.log(7); }).then(() => { microTaskQueue.push(() => console.log('额外微任务')); });
function processMicroTasks() { while (microTaskQueue.length > 0) { const task = microTaskQueue.shift(); task(); } }
function eventLoop() { processMicroTasks(); while (macroTaskQueue.length > 0) { const macroTask = macroTaskQueue.shift(); macroTask(); processMicroTasks(); } }
function simulateSetTimeout(callback, delay) { setTimeout(() => { macroTaskQueue.push(callback); eventLoop(); }, delay); }
simulateSetTimeout(() => { console.log(3); }, 0); simulateSetTimeout(() => { console.log(4); new Promise((resolve) => { console.log(5); resolve(); }).then(() => { console.log(6); microTaskQueue.push(() => console.log('Promise 产生的微任务')); processMicroTasks(); }); }, 0);
processMicroTasks();
|
事件循环-以下代码输出结果
1 2 3 4 5 6 7 8 9 10 11 12
| setTimeout(() => { console.log('timeout'); }, 0);
function test() { console.log('test'); return Promise.resolve().then(() => { test(); }); }
test();
|
考察重点:事件循环中,宏任务与微任务的执行优先级。
答案:持续输出 test 且 不会输出 timeout(重点)
解释:微任务执行优先级高于宏任务,pormise.then callback 会挂载到微任务队列,而 setTimeout callback 会挂载到宏任务队列,每次在执行微任务队列任务时,又重新执行 test(),test运行时会往微任务队列中添加一个微任务,如此循环,所以宏任务队列始终没机会,所以不会输出timeout。
事件循环进阶(1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| Promise.resolve().then(() => { console.log(0); return Promise.resolve(4); }).then((res) => { console.log(res); });
Promise.resolve().then(() => { console.log(1); }).then(() => { console.log(2); }).then(() => { console.log(3); }).then(() => { console.log(5); });
|
0,1,2,3,4,5
事件循环进阶(2)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const first = () => ( new Promise((resolve, reject) => { console.log(3); let p = new Promise((resolve, reject) => { console.log(7); setTimeout(() => { console.log(5); resolve(6); }, 0); resolve(1); }); resolve(2); p.then(arg => { console.log(arg); }); }) );
first().then((arg) => { console.log(arg); console.log(4); });
|
3,7,4,1,2,5,Promise{1}
事件循环进阶(3)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| let a; let b = new Promise((resolve) => { console.log(1); setTimeout(() => { resolve(); }, 1000); }).then(() => { console.log(2); });
a = new Promise(async (resolve) => { console.log(a); await b; console.log(a); console.log(3); await a; resolve(true); console.log(4); });
console.log(5);
|
a(undefined),5,”等待一秒”,2,Promise{},3
结束:因为 a.then 需要被 resolve 才能被执行,而 resolve 又在 a.then 内,因此 a.then 无法执行。
事件循环进阶(4)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const promiseA = Promise.resolve('1'); promiseA.then((res) => { console.log('a:', res); }).then((res) => { console.log('a:', res); });
const promiseB = Promise.resolve('2'); promiseB.then((res) => { console.log('b:', res); }); promiseB.then((res) => { console.log('b:', res); });
|
a:1’’b:2’’b:2’’a: undefined
闭包
闭包及其作用
**定义:**闭包是指引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。
**作用:**闭包可以保留其被定义时的作用域,这意味着闭包内部可以访问外部函数的局部变量,即使外部函数已经执行完毕。这种特性使得闭包可以在后续调用中使用这些变量。
**注意:**闭包会使得函数内部的变量在函数执行后仍然存在于内存中,直到没有任何引用指向闭包。如果不注意管理闭包,可能会导致内存泄漏问题。
1 2 3 4 5 6 7
| for (var i = 0; i < 10; ++i) { (function (index) { setTimeout(function () { console.log(index); }, 1000); })(i); }
|
1 2 3 4 5 6 7 8 9 10 11 12
| const accumulation = function (initValue) { let result = initValue; return function (value) { result += value; return result; }; };
const add = accumulation(5); console.log(add(3)); console.log(add(2));
|
Promise
Promise 是 JavaScript 中处理异步操作的一种模式和对象,它提供了一种更优雅的方式来处理异步代码,尤其是处理回调地狱(callback hell)问题
Promise有三种状态:
Pending(进行中):Promise的初始状态,表示异步操作尚未完成,也不失败。
Fulfilled(已完成)表示异步操作成功完成,其结果值可用
Rejected(已失败):表示异步操作失败,包含失败的原因。
1 2 3
| const myPromise = new Promise((resolve, reject) => { });
|
模拟实现
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
| function MyPromise(executor) { this.state = 'pending'; this._value = undefined; this.callbacks = [];
const resolve = (value) => { if (this.state === 'pending') { this.state = 'fulfilled'; this._value = value; this.callbacks.forEach(callback => { callback.onFulfilled(value); }); } };
const reject = (reason) => { if (this.state === 'pending') { this.state = 'rejected'; this._value = reason; this.callbacks.forEach(callback => { callback.onRejected(reason); }); } };
try { executor(resolve, reject); } catch (error) { reject(error); } }
MyPromise.prototype.then = function (onFulfilled, onRejected) { if (this.state === 'fulfilled') { onFulfilled(this._value); } else if (this.state === 'rejected') { onRejected(this._value); } else if (this.state === 'pending') { this.callbacks.push({ onFulfilled, onRejected }); } };
const promise = new MyPromise((resolve, reject) => { setTimeout(() => { resolve("成功!"); }, 1000); });
promise.then( (result) => { console.log("成功: " + result); }, (error) => { console.log("失败: " + error); } );
|
Promise all/allSettle/any/race 的使用场景
Promise.all
1 2 3 4 5 6 7 8 9
| Promise.all([ Promise.resolve('p1'), Promise.resolve('p2'), Promise.resolve('p3') ]).then(results => { console.log('success', results); }).catch(error => { console.error('error', error); });
|
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
| function requestApi1() { return new Promise((resolve) => { resolve('data from api-1'); }); }
function requestApi2() { return new Promise((resolve) => { resolve('data from api-2'); }); }
function requestApi3() { return new Promise((resolve) => { resolve('data from api-3'); }); }
function render(panel, data) { console.log(`Rendering ${panel} with ${data}`); }
Promise.all([ requestApi1(), requestApi2(), requestApi3() ]).then(results => { render('pannelA', results[0]); render('pannelB', results[1]); render('pannelC', results[2]); }).catch(error => { console.error('error', error); });
|
Promise.allSettled
1 2 3 4 5 6 7 8 9
| Promise.allSettled([ Promise.resolve('p1'), Promise.reject(new Error('p2 failed')), Promise.resolve('p3') ]).then(results => { console.log('success:', results); }).catch(error => { console.log('fail:', error); });
|
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
| function uploadLogFragment(fragment) { return new Promise((resolve, reject) => { const isSuccess = Math.random() > 0.2; setTimeout(() => { if (isSuccess) { resolve(`上传 ${fragment} 成功`); } else { reject(new Error(`上传 ${fragment} 失败`)); } }, 500); }); }
Promise.allSettled([ uploadLogFragment('日志片段 - 1'), uploadLogFragment('日志片段 - 2'), uploadLogFragment('日志片段 - 3') ]).then(results => { console.log('所有上传操作已完成:', results); const successResults = results.filter(result => result.status === 'fulfilled').map(result => result.value); const failResults = results.filter(result => result.status === 'rejected').map(result => result.reason); console.log('成功结果:', successResults); console.log('失败结果:', failResults); });
|
Promise.any
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| Promise.any([ new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('p1 failed')); }, 100); }), new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('p2 failed')); }, 200); }), new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('p3 failed')); }, 300); }) ]).then((result) => { console.log('success', result); }).catch((error) => { console.log('error', error); });
|
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
| function requestCDN(cdnUrl) { return new Promise((resolve, reject) => { setTimeout(() => { const isSuccess = Math.random() > 0.3; if (isSuccess) { resolve(`Successfully connected to ${cdnUrl}`); } else { reject(new Error(`Failed to connect to ${cdnUrl}`)); } }, Math.random() * 1000); }); }
function grabTicket(ticketUrl) { return new Promise((resolve, reject) => { setTimeout(() => { const isSuccess = Math.random() > 0.4; if (isSuccess) { resolve(`Successfully grabbed a ticket from ${ticketUrl}`); } else { reject(new Error(`Failed to grab a ticket from ${ticketUrl}`)); } }, Math.random() * 1000); }); }
Promise.any([ requestCDN('CDN地址 - 1'), requestCDN('CDN地址 - 2'), requestCDN('CDN地址 - 3') ]).then((res) => { console.log('找到有效 CDN:', res); }).catch((error) => { console.log('所有 CDN 均失败:', error); });
Promise.any([ grabTicket('抢票地址 - 1'), grabTicket('抢票地址 - 2'), grabTicket('抢票地址 - 3') ]).then((res) => { console.log('抢到票:', res); }).catch((error) => { console.log('所有抢票请求均失败:', error); });
|
Promise.race
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| Promise.race([ new Promise((resolve, reject) => { setTimeout(() => { reject('p1'); }, 100); }), new Promise((resolve, reject) => { setTimeout(() => { reject('p2'); }, 200); }), new Promise((resolve, reject) => { setTimeout(() => { reject('p3'); }, 300); }) ]).then((res) => { console.log('success ', res); }).catch((error) => { console.log('error', error); });
|
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
| async function selfFetch(api, { timeout }) { return Promise.race([ new Promise((resolve) => { setTimeout(() => { resolve('fetch success'); }, 500); }), new Promise((resolve, reject) => { setTimeout(() => { reject('request timeout'); }, timeout); }) ]); }
selfFetch('/api', { timeout: 300 }) .then((result) => { console.log('success', result); }) .catch((error) => { console.error('fail', error); });
|
script 标签 async 和 defer 的区别
默认情况(无 async 和 defer 属性):如果<1script>标签既没有 async 属性,也没有 defer 属性浏览器会按照标签在 HTML 中的顺序,阻塞页面渲染,下载后并同步加载脚本,脚本会阻塞页面的加载和清染。
async属件:如果<1script>标签带有 async属性,脚本将异步下载并执行,不会阻塞页面的加载和染。脚本将在下载完成后立即执行,而不管其在文档中的位置。
1
| <script src="example.js" async></script>
|
defer 属性: 如果<script〉标签带有 defer 属性,脚本也会异步下载,但不会立即执行。它将在文档解析完成(DOMContentLoaded 事件之前)时按照它们在文档中的顺序执行。
1
| <script src="example.js" async></script>
|
总结:如果没有指定async或 defer 属性,脚本默认是同步的,会阻塞页面加载。如果使用 async属性,脚本会异步加载和执行。如果使用 defer 属性,脚本也会异步加载,但在文档解析完成后按顺序执行。根据页面性能和脚本执行时机的需求,您可以选择适当的属性。
地狱函数
如何解决异步回调地狱
**定义:**异步回调地狱是指在嵌套的回调函数中处理多个异步操作,导致代码变得混乱和难以维护的情况。
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
| const fs = require('fs');
fs.readFile('file1.txt', function (err, data1) { if (err) { console.error(err); return; } fs.readFile('file2.txt', function (err, data2) { if (err) { console.error(err); return; } const mergedData = data1 + data2; fs.writeFile('merged.txt', mergedData, function (err) { if (err) { console.error(err); return; } fs.readFile('merged.txt', function (err, mergedContent) { if (err) { console.error(err); return; } console.log('Sending data to server:', mergedContent.toString()); }); }); }); });
|
解决方案:
使用 Promise 对象: Promises 出现主要为解决异步回调地狱,是一种处理异步操作的方式,它允许你链式调用.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
| function asyncFunction() { return new Promise((resolve, reject) => { setTimeout(() => { const success = true; if (success) { resolve('Async function result'); } else { reject(new Error('Async function failed')); } }, 1000); }); }
function anotherAsyncFunction(result) { return new Promise((resolve, reject) => { setTimeout(() => { const finalResult = result + ' - Another async function processed'; resolve(finalResult); }, 1000); }); }
asyncFunction() .then(result => { return anotherAsyncFunction(result); }) .then(finalResult => { console.log('Final result:', finalResult); }) .catch(error => { console.error('Error:', error.message); });
|
使用 async/await: async/await 是 ES6 的异步处理方式,它允许你使用类似同步代码的方式来处理异步操作。这使得代码更具可读性。
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
| function asyncFunction1() { return new Promise((resolve, reject) => { setTimeout(() => { const success = true; if (success) { resolve('Result from asyncFunction1'); } else { reject(new Error('asyncFunction1 failed')); } }, 1000); }); }
function asyncFunction2(result) { return new Promise((resolve, reject) => { setTimeout(() => { const finalResult = result +'and processed by asyncFunction2'; const innerSuccess = true; if (innerSuccess) { resolve(finalResult); } else { reject(new Error('asyncFunction2 failed')); } }, 1000); }); }
async function fetchData() { try { const result1 = await asyncFunction1(); const result2 = await asyncFunction2(result1); console.log('Final result:', result2); } catch (error) { console.error('Error:', error.message); } }
fetchData();
|
Generators 和 yield: 使用生成器函数和 yield 关键字来编写可暂停和可恢复的异步代码,以更容易处理复杂的异步流程。
使用库和工具: 使用异步控制库(如Asyncjs)或工具(如RxJS)处理异步操作,提高代码的可读性和维护楚
模块化和拆分代码: 将异步操作拆分为小的、可重用的函数或模块,在主代码中调用,减少嵌套的回调函数。
元素拖动
元素拖动实现方案
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
| <!DOCTYPE html> <html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>元素拖动示例</title> <style type="text/css"> body { display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
.draggable { width: 100px; height: 100px; background-color: #3498db; color: #fff; text-align: center; line-height: 100px; cursor: grab; user-select: none; position: absolute; } </style> </head>
<body> <div class="draggable" id="draggableElement">拖动我</div> <script> const draggableElement = document.getElementById("draggableElement"); let offsetX, offsetY; let isDragging = false;
draggableElement.addEventListener("mousedown", (event) => { isDragging = true; offsetX = event.clientX - draggableElement.getBoundingClientRect().left; offsetY = event.clientY - draggableElement.getBoundingClientRect().top; draggableElement.style.cursor = "grabbing"; });
document.addEventListener("mousemove", (event) => { if (isDragging) { const newX = event.clientX - offsetX; const newY = event.clientY - offsetY; draggableElement.style.left = newX + "px"; draggableElement.style.top = newY + "px"; } });
document.addEventListener("mouseup", () => { isDragging = false; draggableElement.style.cursor = "grab"; }); </script> </body>
</html>
|
ES6 的继承和 ES5 的继承的区别
ES6 类继承:
1.Class 和 extends 关键字:ES6引入了class 和 extends 关键字,使得创建类和继承更加直观和易于理1解。类提供了一种更面向对象的编程方式。
2.构造函数: ES6 类继承通过构造函数constructor 定义类的初始化逻辑,并通过 super 调用父类的构造2函数。这使得继承更加符合直觉。
3.方法定义: 类中的方法不再需要使用原型链,而是可以直接定义在类内部。这让方法的定义更集中和易读。
4.super 关键字: super 关键字用于在子类中调用父类的方法,包括构造函数和普通方法。4.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class Animal { constructor(name) { this.name = name; } speak() { console.log(this.name + ' makes a sound'); } }
class Dog extends Animal { constructor(name, breed) { super(name); this.breed = breed; } speak() { console.log(this.name + ' barks'); } }
const myDog = new Dog("Buddy", "Golden Retriever"); myDog.speak();
|
ES5 原型继承:
原型链继承:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| function Animal(name) { this.name = name; }
Animal.prototype.speak = function() { console.log(this.name + ' makes a sound'); };
function Dog(name, breed) { Animal.call(this, name); this.breed = breed; }
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
var myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak();
|
缺点:
属性共享: 子类共享了父类原型上的属性,一旦父类有引用类型,其中一个实例修改了这个引用类型的属性值,会会影响所有其他实例。
不能传递参数:无法向父类构造函数传参,因为父类构造函数已经被调用。
寄生组合继承
结合了构造函数继承和原型继承,通过在子类构造函数内部调用父类构造函数来继承属性,然后通过0bject.create()方法来继承父类原型上的方法。克服构造函数继承和原型继承各自的缺点。
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
| function Animal(name) { this.name = name; }
Animal.prototype.speak = function () { console.log(this.name + ' makes a sound'); };
function Dog(name, breed) { Animal.call(this, name); this.breed = breed; }
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function () { console.log(this.name + ' barks'); };
var myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak();
|
首先使用构造函数继承来继承属性,然后使用 0bject.create(Animal.prototype)继承了父类的原型。这种方式避免了原型链中属性共享的问题,并允许更灵活地定义子类的构造函数和方法。