JavaScript进阶知识要点

JS事件

绑定事件与解除事件

绑定事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 通过 ID 获取元素
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'); // 这里要根据实际情况替换ID

// 定义点击事件处理函数(与绑定事件时使用的函数需为同一引用)
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 并将其 then 回调加入微任务队列
Promise.resolve().then(() => {
console.log(2);
// 将 setTimeout 回调加入宏任务队列
setTimeout(() => {
console.log(3);
}, 0);
// 将另一个 setTimeout 回调加入宏任务队列
setTimeout(() => {
console.log(4);
// 创建新的 Promise,将其 then 回调加入微任务队列
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();
}
}

// 模拟 setTimeout 将回调加入宏任务队列
function simulateSetTimeout(callback, delay) {
setTimeout(() => {
macroTaskQueue.push(callback);
eventLoop();
}, delay);
}

// 手动将之前的 setTimeout 回调加入宏任务队列
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);

// 先执行同步代码中的 Promise 微任务
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)); // 输出 8
console.log(add(2)); // 输出 10

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
// 自定义 MyPromise 类
function MyPromise(executor) {
// 初始化 Promise 的状态和结果
this.state = 'pending';
this._value = undefined;
// 回调函数数组,用于存储成功和失败回调
this.callbacks = [];

// 定义 resolve 函数,用于将 Promise 状态从 pending 变为 fulfilled
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this._value = value;
this.callbacks.forEach(callback => {
callback.onFulfilled(value);
});
}
};

// 定义 reject 函数,用于将 Promise 状态从 pending 变为 rejected
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this._value = reason;
this.callbacks.forEach(callback => {
callback.onRejected(reason);
});
}
};

// 执行 executor 函数,传入 resolve 和 reject 作为参数
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}

// 为 MyPromise 原型添加 then 方法
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
// 模拟请求 CDN 的函数
function requestCDN(cdnUrl) {
return new Promise((resolve, reject) => {
// 模拟请求耗时,随机 0 - 1 秒
setTimeout(() => {
// 模拟 70% 的成功概率
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) => {
// 模拟抢票耗时,随机 0 - 1 秒
setTimeout(() => {
// 模拟 60% 的成功概率
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);
});
}

// 场景 1: 寻找有效 CDN
Promise.any([
requestCDN('CDN地址 - 1'),
requestCDN('CDN地址 - 2'),
requestCDN('CDN地址 - 3')
]).then((res) => {
console.log('找到有效 CDN:', res);
// 应用 CDN 的后续操作可以在这里添加
}).catch((error) => {
console.log('所有 CDN 均失败:', error);
});

// 场景 2: 抢票
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
// 定义 selfFetch 函数,用于实现请求超时控制
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 函数发起请求
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');

// 读取文件"file1.txt”
fs.readFile('file1.txt', function (err, data1) {
if (err) {
console.error(err);
return;
}
// 读取文件"file2.txt"
fs.readFile('file2.txt', function (err, data2) {
if (err) {
console.error(err);
return;
}
const mergedData = data1 + data2;
// 将这两个文件的内容合并到新文件“merged.txt”中。
fs.writeFile('merged.txt', mergedData, function (err) {
if (err) {
console.error(err);
return;
}
// 读取“merged.txt”文件并将内容发送到服务器。
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/await 处理异步操作的函数
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 函数
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(); // 输出“Buddy barks”

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
// 定义 Animal 构造函数
function Animal(name) {
this.name = name;
}
// 为 Animal 的原型添加 speak 方法
Animal.prototype.speak = function() {
console.log(this.name + ' makes a sound');
};

// 定义 Dog 构造函数
function Dog(name, breed) {
// 调用 Animal 构造函数初始化 name 属性
Animal.call(this, name);
this.breed = breed;
}
// 设置 Dog 的原型继承自 Animal 的原型
Dog.prototype = Object.create(Animal.prototype);
// 修正 Dog 原型的构造函数指向
Dog.prototype.constructor = Dog;

// 创建 Dog 实例
var myDog = new Dog('Buddy', 'Golden Retriever');
// 调用 speak 方法
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
// 定义 Animal 构造函数
function Animal(name) {
this.name = name;
}
// 为 Animal 原型添加 speak 方法
Animal.prototype.speak = function () {
console.log(this.name + ' makes a sound');
};

// 定义 Dog 构造函数
function Dog(name, breed) {
// 使用构造函数继承,继承属性
Animal.call(this, name);
this.breed = breed;
}
// 使用 Object.create 继承原型
Dog.prototype = Object.create(Animal.prototype);
// 修复 constructor 引用
Dog.prototype.constructor = Dog;
// 重写 speak 方法
Dog.prototype.speak = function () {
console.log(this.name + ' barks');
};

// 创建 Dog 实例
var myDog = new Dog('Buddy', 'Golden Retriever');
// 调用 speak 方法
myDog.speak(); // 输出“Buddy barks”

首先使用构造函数继承来继承属性,然后使用 0bject.create(Animal.prototype)继承了父类的原型。这种方式避免了原型链中属性共享的问题,并允许更灵活地定义子类的构造函数和方法。