liuqinh2s' blog

Do something cool!

箭头函数表达式的语法比函数表达式更简洁,并且没有自己的 thisargumentssupernew.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数

引入箭头函数有两个方面的作用:

  1. 更简短的函数
  2. 不绑定 this
阅读全文 »

参考

微任务、宏任务与 Event-Loop

宏任务和微任务到底是什么?

JavaScript 中的 Event Loop(事件循环)机制

什么是 Event-Loop

JS 是单线程执行的,所以要执行多任务的话依赖于异步和事件循环(Event Loop)。JS 是单线程的,指的是 JS 引擎线程。在浏览器环境中,有 JS 引擎线程和渲染线程,且这两个线程互斥。node 环境中,只有 JS 引擎线程。

JS 引擎常驻内存,等待宿主(浏览器或 node)分配宏任务,反复等待-执行即为事件循环。

Event Loop 中,每次循环称为 tick,每次 tick 的任务如下:

  1. 读宏任务队列,执行其同步代码直至结束,这个过程中可能会有宏任务和微任务入队
  2. 读微任务,并执行,直至微任务队列为空
  3. 如果宿主为浏览器,可能会渲染页面

ES6 规范中,microtask 称为 jobs,macrotask 称为 task。宏任务是由宿主发起的,而微任务由 JavaScript 自身发起。

在 ES3 以及以前的版本中,JavaScript 引擎自身没有发起异步请求的能力,也就没有微任务的存在。在 ES5 之后,JavaScript 引入了 Promise,这样,不需要浏览器,JavaScript 引擎自身也能发起异步任务了。

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
// async function async1() {
// console.log('async1 start'); // 2
// await async2();
// console.log('async1 end'); // 7
// }

// async function async2() {
// console.log('async2 start'); // 3
// return new Promise((resolve, reject) => {
// console.log('async2 promise'); // 4
// resolve();
// })
// }

function sync1(){
return new Promise((resolve, reject)=>{
console.log('async1 start'); // 2
return new Promise((resolve, reject) => {
console.log('async2 start'); // 3
return new Promise((resolve, reject) => {
console.log('async3 start'); // 4
resolve();
}).then(()=>{
console.log('async3 end'); // 7
resolve();
})
}).then(() => {
console.log('async2 end'); // 9
resolve();
})
}).then(()=>{
console.log('async1 end'); // 10
})
}

console.log('script start'); // 1

setTimeout(function() {
console.log('setTimeout'); // 11
}, 0);

sync1();

new Promise(function(resolve) {
console.log('promise1 start'); // 5
resolve();
}).then(function() {
console.log('promise1 end'); // 8
})

console.log('script end'); // 6

// script start
// async1 start
// async2 start
// async3 start
// promise1 start
// script end
// async3 end
// promise1 end
// async2 end
// async1 end
// setTimeout

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
new Promise(resolve => {
setTimeout(()=>{
console.log(1);
new Promise(resolve => {
resolve();
})
.then(() => {console.log(2);})
})
resolve();
})
.then(() => {
console.log('then1')
let a = new Promise(resolve => {
resolve();
})
.then(() => {console.log(3);})
Promise.resolve().then(() => {console.log(4);})
.then(() => {console.log(5);})
.then(() => {console.log(6);})
console.log(7);
})
.then(() => {
console.log('then2')
new Promise((resolve) => {
resolve()
})
.then(() => {
console.log(8);
new Promise((resolve) => {
resolve()
})
.then(() => {console.log(9)})
})
.then(() => {
console.log(10);
})
})
.then(() => {
console.log('then3')
console.log(11);
})
.then(() => {
console.log('then4')
console.log(12);
})
// then1
// 7
// 3
// 4
// then2
// 5
// 8
// then3
// 11
// 6
// 9
// 10
// then4
// 12
// 1
// 2
  1. setTimeout回调函数是宏任务,Promise.then的回调函数是微任务
  2. new Promise的 executor 函数是同步执行的
  3. then需等new Promise的 executor 函数执行完,或上一个 then 执行完,才入队

面试中经常被问到跨域问题,可以说这是前端网络安全必问的一道题。我感觉之前每次都答得不是很好,要搞清楚几个关键的点:

  1. 跨域问题是浏览器才有的问题,如果其他程序请求后端,是不存在跨域问题的,比如爬虫,webpack server,后端程序
  2. 前端跨域问题的关键在于后端是否允许跨域,如果后端允许,则也是不存在跨域问题的,正是利用了这一点,XSS 攻击才能成功。
  3. 跨域请求是能发送成功的,只是请求结果读取不到而已。正是利用了这一点,CSRF 攻击才能成功。
阅读全文 »

事件

事件编程是一种观察者模式,可以很好的解耦页面的表现和页面的行为。

事件流

事件冒泡 - IE 浏览器:从子节点到祖先节点
事件捕获 - Netscape 浏览器:从祖先节点到子节点
DOM 事件流 - DOM2 Event 规范 3 个阶段:事件捕获 -> 到达目标 -> 事件冒泡

事件冒泡:
事件冒泡

事件捕获:
事件捕获

DOM 事件流:
DOM 事件流

事件处理程序

HTML 事件处理程序

1
<input type="button" value="Click Me" onclick="console.log('Clicked')"/>

也可以调用函数:

1
2
3
4
5
6
<script>
function showMessage() {
console.log("Hello world!");
}
</script>
<input type="button" value="Click Me" onclick="showMessage()"/>

用这种方式 HTML 和 JavaScript 强耦合,所以一般不用。

DOM0 事件处理程序

1
2
3
4
const btn = document.getElementById("myBtn");
btn.onclick = function(){
console.log('Clicked')
}

注销:

1
btn.onclick = null;

这种方式添加的事件处理程序是注册在事件流冒泡阶段

DOM2 事件处理程序

1
2
addEventListener();
removeEventListener();

这两个方法暴露在所有 DOM 节点上,它们接受 3 个参数:

  1. 事件名
  2. 事件处理函数
  3. 布尔值,true 表示在捕获阶段调用事件处理函数,false(默认值)表示在冒泡阶段调用事件处理函数。(面试被问到过)

相比于 DOM0 事件处理程序,这种方式可以添加多个事件处理程序:

1
2
3
4
5
6
7
let btn = document.getElementById("myBtn");
btn.addEventListener("click", () => {
console.log(this.id);
}, false);
btn.addEventListener("click", () => {
console.log("Hello world!");
}, false);

执行的顺序是按绑定的顺序来的。

常见问题

  1. 解绑必须针对同一个函数,不能对匿名函数解绑
  2. 解绑第三个参数的 capture 必须相同,否则也会解绑失败
1
2
3
element.addEventListener("mousedown", handleMouseDown, true);
element.removeEventListener("mousedown", handleMouseDown, false); // 失败
element.removeEventListener("mousedown", handleMouseDown, true); // 成功

第一个调用失败是因为 useCapture 没有匹配。第二个调用成功,是因为 useCapture 匹配相同。

IE 事件处理程序

IE 实现了与 DOM 类似的方法,即 attachEvent()和 detachEvent()。这两个方法接收两个同样 的参数:事件处理程序的名字和事件处理函数。因为 IE8 及更早版本只支持事件冒泡,所以使用 attachEvent()添加的事件处理程序会添加到冒泡阶段。

1
2
3
4
5
6
7
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function() {
console.log("Clicked");
});
btn.attachEvent("onclick", function() {
console.log("Hello world!");
});

执行顺序和绑定顺序相反

跨浏览器事件处理程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var EventUtil = {
addHandler: function (element, type, handler) {
if (element.addEventListener) {
element.addEventListener(type, handler, false);
} else if (element.attachEvent) {
element.attachEvent("on" + type, handler);
} else {
element["on" + type] = handler;
}
},
removeHandler: function (element, type, handler) {
if (element.removeEventListener) {
element.removeEventListener(type, handler, false);
} else if (element.detachEvent) {
element.detachEvent("on" + type, handler);
} else {
element["on" + type] = null;
}
},
};

就是兼容这三种事件绑定方式而已。

事件对象

DOM 事件对象

1
2
3
4
5
6
7
let btn = document.getElementById("myBtn");
btn.onclick = function(event) {
console.log(event.type); // "click"
};
btn.addEventListener("click", (event) => {
console.log(event.type); // "click"
}, false);

不管以 DOM0 还是 DOM2 的方式,绑定事件处理程序,都会传入 event,事件对象。

this, currentTarget, target 的区别

this 是调用函数的对象,currentTarget 是事件函数注册的目标,而 target 是事件的实际目标。如果事件处理程序直接添加在了意图的目标,则 this、currentTarget 和 target 的值是一样的。下面的例子展示了这两个属性都等于 this 的情形:

1
2
3
4
5
let btn = document.getElementById("myBtn");
btn.onclick = function(event) {
console.log(event.currentTarget === this); // true
console.log(event.target === this); // true
};

上面的代码检测了 currentTarget 和 target 的值是否等于 this。因为 click 事件的目标是按钮,所以这 3 个值是相等的。如果这个事件处理程序是添加到按钮的父节点(如 document.body)上,那么它们的值就不一样了。比如下面的例子在 document.body 上添加了单击处理程序:

1
2
3
4
5
document.body.onclick = function(event) {
console.log(event.currentTarget === document.body); // true
console.log(this === document.body); // true
console.log(event.target === document.getElementById("myBtn")); // true
};

这种情况下点击按钮,this 和 currentTarget 都等于 document.body,这是因为它是注册事件处理程序的元素。而 target 属性等于按钮本身,这是因为那才是 click 事件真正的目标。由于按钮本身并没有注册事件处理程序,因此 click 事件冒泡到 document.body,从而触发了在它上面注册的处理程序。

preventDefault 和 stopPropagation

preventDefault()方法用于阻止特定事件的默认动作。比如,链接的默认行为就是在被单击时导航到 href 属性指定的 URL。如果想阻止这个导航行为,可以在 onclick 事件处理程序中取消,如下面的例子所示:

1
2
3
4
let link = document.getElementById("myLink");
link.onclick = function(event) {
event.preventDefault();
};

stopPropagation()方法用于立即阻止事件流在 DOM 结构中传播,取消后续的事件捕获或冒泡。例如,直接添加到按钮的事件处理程序中调用 stopPropagation(),可以阻止 document.body 上注册的事件处理程序执行。比如:

1
2
3
4
5
6
7
8
let btn = document.getElementById("myBtn");
btn.onclick = function(event) {
console.log("Clicked");
event.stopPropagation();
};
document.body.onclick = function(event) {
console.log("Body clicked");
};

eventPhase

eventPhase 的取值及含义

常量 阶段 描述
​​0​ ​ Event.NONE 无阶段 事件未处于任何处理阶段(如事件未传播或已结束)
​​1​​ Event.CAPTURING_PHASE 捕获阶段 事件从根节点(如 document)向下传播至目标元素
​​2​​ Event.AT_TARGET 目标阶段 事件已到达目标元素(event.target)并正在处理
​​3​​ Event.BUBBLING_PHASE 冒泡阶段 事件从目标元素向上冒泡至根节点
1
2
3
4
5
6
7
8
9
10
let btn = document.getElementById("myBtn");
btn.onclick = function(event) {
console.log(event.eventPhase); // 2 事件到达
};
document.body.addEventListener("click", (event) => {
console.log(event.eventPhase); // 1 事件捕获
}, true);
document.body.onclick = (event) => {
console.log(event.eventPhase); // 3 事件冒泡
};

IE 事件对象

1
2
3
4
5
var btn = document.getElementById("myBtn");
btn.onclick = function() {
let event = window.event;
console.log(event.type); // "click"
};
1
2
3
4
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(event) {
console.log(event.type); // "click"
});

由于事件处理程序的作用域取决于指定它的方式,因此 this 值并不总是等于事件目标。为此,更
好的方式是使用事件对象的 srcElement 属性代替 this。下面的例子表明,不同事件对象上的
srcElement 属性中保存的都是事件目标:

1
2
3
4
5
6
7
var btn = document.getElementById("myBtn");
btn.onclick = function() {
console.log(window.event.srcElement === this); // true
};
btn.attachEvent("onclick", function(event) {
console.log(event.srcElement === this); // false
});

事件委托

事件委托(Event Delegation)是 JavaScript 中一种利用事件冒泡机制的编程技术,通过将事件监听器绑定在父元素而非子元素上,统一处理子元素的事件。其核心原理是事件从触发元素(子元素)逐层向上(父元素)冒泡,父元素通过event.target识别实际触发实际的子元素,并执行相应逻辑。

优势:

  1. 性能优化
    • 减少事件监听器数量:避免为大量子元素单独绑定事件,降低内存占用
  2. 动态元素支持
    • 自动处理新增元素:动态生成的子元素无需重新绑定事件,父元素监听器可以监听到
  3. 代码简洁性和可维护性:
    • 逻辑集中化:事件处理统一在父元素中管理,减少重复代码

适用场景:

  1. 动态内容
    • 如通过 AJAX 请求加载的列表,实时添加的表单项等
  2. 大量相似元素
    • 例如千级列表项,按钮组,避免逐个绑定事件

局限性:

  1. 不支持不冒泡的事件:focus、blur、load 等事件无冒泡机制,需单独绑定

核心 API:URLSearchParams

现代浏览器原生支持的查询参数处理标准接口

基础使用示例:

1
2
3
4
5
6
// 从当前URL获取参数
const params = new URLSearchParams(window.location.search);
// 获取单个参数值
const name = params.get('name'); // 返回字符串或null
// 获取重复参数的所有值(如 ?color=red&color=blue)
const colors = params.getAll('color'); // 返回数组:["red", "blue"]

动态修改参数:

1
2
3
params.set('page', '2');    // 更新参数,如果原先无此参数,则会新加,如果原来有多个值将删除其他所有值
params.append('filter', 'new'); // 添加新值(不覆盖旧值)
params.delete('old'); // 删除参数

参数遍历

1
2
3
for(const [key, value] of params){  // 或params.entries()
console.log(key, value);
}

结合 URL API 实现完整解析

1
2
3
const url = new URL('https://example.com/path?name=John#section');
console.log(url.pathname); // 输出:/path
console.log(url.searchParams.get('name')); // 输出:John

注意事项

URLSearchParams 会使用+替代空格:

1
2
3
4
const query = new URLSearchParams({
lang: 'zh aa',
sort: 'desc'
}).toString(); // 输出:lang=zh+aa&sort=desc
0%