Web前端开发(5) 事件、作用域、闭包
2023-08-09 14:53:19 # NJU # Web前端开发

7.1 JS 事件

1 JS 事件

1.1 事件

  • 事件和事件处理
    • 使 web 应用程序的响应性、动态性和交互性更强
    • 通过回调编程
  • JS 事件
    • 允许脚本影响用户与网页上元素的交互
    • 是否可以启动对页面的修改

1.2 事件驱动编程

  • 事件驱动编程是一种编程范式, 其中程序流由事件决定, 诸如用户操作(鼠标点击、按键)、传感器输出或来自其他程序/线程的消息
  • 事件驱动编程是图形用户界面和其它应用程序(如 JS web应用程序)中使用的主要范式, 这些应用程序以执行特定的操作来影响用户输入为中心

  • 在事件驱动的应用程序中, 通常有一个主循环监听事件, 然后在检测到其中一个事件时触发回调函数

  • 事件驱动程序可以用任何编程语言编写, 尽管使用提供高级抽象的语言(如闭包)更容易完成此任务

1.3 事件处理程序

  • 处理程序接收的输入的回调子例程(在 Java 和 JS 中被称为侦听器)
  • 当事件发生时调用的函数
  • 通常与 XHTML 元素相关联
  • 必须注册
    • 也就是说, 必须指定关联

1.4 JS 的用途

  • 事件处理程序可用于处理和验证用户输入、用户操作和浏览器操作

    • 每次加载页面时应该做的事情
    • 当页面关闭时应该做的事情
    • 当用户单击按钮时应该执行的操作
    • 当用户输入数据时应该验证的内容
  • 可以使用许多不同的方法让 JS 处理事件

    • HTML事件属性可以直接执行 JS 代码
    • HTML事件属性可以调用 JS 函数
    • 可以将自己的事件处理函数分配给 HTML 元素
    • 可以阻止事件的发送或处理

1.5 语法

  • element.addEventListener(event, function, useCapture);
    • 第一个参数是事件类型(如 “click” 或 “mousedown”)
    • 第⼆个参数是我们想在事件发生时调用的函数
    • 第三个参数是一个布尔值, 指定是使用事件冒泡还是使用事件捕获, 是可选参数
1
2
3
4
element.addEventListener("click", myFunction);
element.addEventListener("click", mySecondFunction);
document.getElementById("myDiv").addEventListener("click", myFunction, true);
element.removeEventListener("mousemove", myFunction);

1.6 观察者模式

  • 观察者模式是一种软件设计模式, 在这种模式中, 一个称为主题的对象维护一个名为观察者的依赖项列表, 通常通过调用它们的一个方法自动通知其任何状态变化

image-20221201225704728

  • 主要用于实现分布式事件处理系统
  • 观察者模式也是我们熟悉的 模型-视图-控制器(MVC) 体系结构模式中的关键部分。观察者模式在许多编程库和系统中实现, 包括几乎所有的GUI工具包
  • 发布者/订阅者模式的子集

image-20221201225734442

  • 事件使得一个主题可能有多个观察者队列

image-20221201225913162

1.6.1 特征

  • 好处
    • 主题与观察者之间的抽象耦合
    • 支持广播通信
  • 注意
    • 观察者模式会导致内存泄漏, 即所谓的失效侦听器问题

1.7 用 jQuery 方式附加事件处理程序

1
2
3
4
var hiddenBox = $( "#banner-message" );
$("#button-container button").on("click", function(event) {
hiddenBox.show();
});

2 事件类型

2.1 可能发生的不同事件

  • 在 Web 中, 事件在浏览器窗口中被触发并且通常被绑定到窗口内部的特定部分: 可能是一个元素、一系列元素、被加载到这个窗口的 HTML 代码或者是整个浏览器窗口。举几个可能发生的不同事件

    • 用户在某个元素上点击鼠标或悬停光标
    • 用户在键盘中按下某个按键
    • 用户调整浏览器的大小或者关闭浏览器窗口
    • 一个网页停止加载
    • 提交表单
    • 播放、暂停、关闭视频
    • 发生错误
  • Event reference

2.2 DOM 2 事件类型

  • 用户界面(UI)事件:
    • DOMFocusIn, DOMFocusOut, DOMActivate
  • 鼠标事件:
    • click, mousedown, mouseup, mouseover, mousemove, mouseout
  • 键盘事件: (not in DOM 2, but will in DOM 3)
  • 变动事件:
    • DOMSubtreeModified, DOMNodeInserted, …
  • HTML事件:
    • load, unload, abort, error, select, change, submit, reset, focus, blur, resize, scroll

2.3 HTML5 新事件

  • audio, video
    • canplay, playing, suspend, …
  • drag/drop
  • history
  • new form events
    • invalid
  • offline, online, …
  • message

2.4 触屏和移动设备事件

  • 功能强大的移动设备, 特别是带有触摸屏的移动设备被⼴泛采用, 要求创建新的事件类别
  • 在许多情况下, 触屏事件映射到传统事件类型, 如单击和滚动。但并⾮所有触屏UI的交互都模仿鼠标, 也不是所有的触摸都可以被视为鼠标事件
    • gesturestart, gestureend

2.5 事件类型的使用

  • 问题: 事件是棘⼿的, 有跨浏览器的不兼容性方面的原因:
    • 模糊W3C事件规范
    • IE不符合网络标准

2.6 事件对象

  • 有时候在事件处理函数内部, 您可能会看到一个固定指定名称的参数, 例如event, evt 或简单的 e。这被称为事件对象, 它被自动传递给事件处理函数, 以提供额外的功能和信息。事件对象具有以下属性/方法:

image-20221202141218046

1
2
3
function name(event) {
// an event handler function ...
}

2.7 鼠标事件

image-20221202141346694

3 事件处理模型

3.1 DOM 0

  • 此事件处理模型是由Netscape Navigator引入的, 到2005年仍然是跨浏览器最多的模型。有两种模型类型:
    • 内联模型: 事件处理程序作为元素的属性添加。
    • 传统模型: 可以通过脚本添加/删除事件处理程序。与内联模型一样, 每个事件只能注册一个事件处理程序。通过将处理程序名称分配给元素对象的事件属性来添加事件。要删除事件处理程序, 只需将属性设置为null。
1
2
3
4
5
6
7
8
9
<p>Hey <a href="http://www.example.com"
onclick="triggerAlert('Joe'); return false;">Joe
</a>!
</p>
<script>
function triggerAlert(name) {
window.alert("Hey " + name);
}
</script>
1
2
3
4
5
6
7
8
9
10
11
<script>
var triggerAlert = function () {
window.alert("Hey Joe");
};
// Assign an event handler
document.onclick = triggerAlert;
// Assign another event handler
window.onload = triggerAlert;
// Remove the event handler that was just assigned
window.onload = null;
</script>

3.2 DOM 事件流

image-20221202141751120

3.2.1 事件处理阶段

  • 在 DOM 兼容浏览器中, 事件流分为3个阶段:
    • 捕获阶段:事件从 Document 节点自上而下向目标节点传播的阶段
    • 目标阶段:真正的目标节点正在处理事件的阶段
    • 冒泡阶段:事件从目标节点自下而上向Document节点传播的阶段
  • 在现代浏览器中, 默认情况下, 所有事件处理程序都在冒泡阶段进行注册。

3.2.2 事件流

  • 每个事件都有一个目标节点, 可以通过事件访问该目标
1
2
3
4
5
6
7
element.onclick = handler(e);
function handler(e){
if(!e) var e = window.event;
// e refers to the event
// see detail of event
var original = e.eventTarget;
}
  • 每个事件都起源于浏览器, 并传递给DOM
  • 职责链模式

image-20221202142426300

3.2.3 DOM 2 中的方法

image-20221202142518683

3.2.4 浏览器兼容性

  • 表中的数字指定了完全支持这些方法的第一个浏览器版本

image-20221202142558788

3.3 阻止默认行为

  • 停止事件的传播
    • event.stopPropagation()
    • cancelBubble=true(IE某些版本)
  • 禁止默认行为
    • event.preventDefault()
1
2
3
4
video.onclick = function(e) {
e.stopPropagation();
video.play();
};

3.4 重写3.1中使用的示例

1
2
3
4
5
6
7
8
9
10
11
<script>
var heyJoe = function () {
window.alert("Hey Joe!");
}
// Add an event handler
document.addEventListener( "click", heyJoe, true ); // capture phase
// Add another event handler
window.addEventListener( "load", heyJoe, false ); // bubbling phase
// Remove the event handler just added
window.removeEventListener( "load", heyJoe, false );
</script>

3.5 Microsoft-specific 模型

  • 微软直到Internet Explorer 8 才遵循 W3C 模型, 因为它自⼰的模型是在 W3C 标准批准之前创建的。Internet Explorer 9 遵循 DOM3 事件, Internet Explorer 11 删除了对微软特定模型的支持

image-20221202143453557

3.6 IE 曾经的特定操作

  • 为了防止事件冒泡, 开发人员必须设置事件的cancelBubble属性
  • 为了防止事件的默认动作被调用, 开发人员必须设置事件的 returnValue 属性
  • 避免使用 !!

  • 跨浏览器的解决方式示例

1
2
3
4
5
6
7
8
var x = document.getElementById("myBtn");
if (x.addEventListener) {
// For all major browsers, except IE 8 and earlier
x.addEventListener("click", myFunction);
} else if (x.attachEvent) {
// For IE 8 and earlier versions
x.attachEvent("onclick", myFunction);
}

3.7 事件处理程序的绑定

  • 内联:
    • <a href="somewhere.html" onClick="myFunction()">
  • 传统:
    • element.onclick = myFunction;
  • DOM 2:
    • element.addEventListener("click", myFunction);
  • IE: (evil enough!)
    • element.attachEvent('onclick', myFunction);
  • JQuery, Prototype and so on:
    • jQuery.on()
    • Event.observe('target', 'click', myFunction);

7.2 JS 作用域与闭包

1 作用域

1.1 JS 作用域

  • 作用域是当前的执行上下文, 值(en-US)和表达式在其中”可见”或可被访问, 即作用域指的是有权访问的变量集合
    • 如果一个变量(en-US)或表达式不在当前的作用域中, 那么它是不可用的
    • 作用域也可以堆叠成层次结构, 子作用域可以访问父作用域, 反过来则不行
  • JavaScript 的作用域分以下三种:
    • 全局作用域:脚本模式运行所有代码的默认作用域
    • 模块作用域:模块模式中运行代码的作用域
    • 函数作用域:由函数创建的作用域
  • 此外, (ES6)用 let 或 const 声明的变量属于额外的作用域:
    • 块级作用域:用一对花括号(一个代码块)创建出来的作用域
    • 块级作用域只对 letconst 声明有效,对 var 声明无效。
1
2
3
4
5
6
7
8
9
{
var x = 1;
}
console.log(x); // 1

{
const x = 1;
}
console.log(x); // ReferenceError: x is not defined

1.2 JS 变量

  • 在 JavaScript 中, 对象和函数也是变量
  • 作用域决定了从代码不同部分对变量、对象和函数的可访问性

1.2.1 全局变量

  • 在函数之外声明的变量, 叫做全局变量, 因为它可被当前文档中的任何其他代码所访问
  • 全局变量的作用域是全局的:网页的所有脚本和函数都能够访问它
1
2
3
4
5
6
var carName = " Volvo";
// code here can use carName

function myFunction() {
// code here can use carName
}

1.2.2 自动全局

  • 如果为尚未声明的变量赋值, 此变量会自动成为全局变量
  • 这段代码将声明一个全局变量 carName, 即使在函数内进行了赋值。
1
2
3
4
5
6
// code here can use carName

function myFunction() {
carName = "Volvo";
// code here can use carName
}

1.3 函数作用域

  • 在函数内定义的变量不能在函数之外的任何地方访问, 因为变量仅仅在该函数的域的内部有定义
  • 相对应的, 一个函数可以访问定义在其范围内的任何变量和函数。
  • 换言之, 定义在全局域中的函数可以访问所有定义在全局域中的变量
  • 在另一个函数中定义的函数也可以访问在其父函数中定义的所有变量和父函数有权访问的任何其他变量
1
2
3
4
5
6
// code here can not use carName

function myFunction() {
var carName = "Volvo";
// code here can use carName
}

1.4 JS 变量的有效期

  • JavaScript 变量的有效期始于其被创建时
  • 局部变量会在函数完成时被删除
  • 全局变量会在关闭页面时被删除
  • 函数参数
    • 函数参数也是函数内的局部变量
  • HTML 中的全局变量
    • 通过JavaScript, 全局作用域形成了完整的 JavaScript 环境
    • 在 HTML 中, 全局作用域是 window。所有全局变量均属于 window 对象。

2 闭包

2.1 计数器困境

  • 假设想要使用一个变量来计算, 并且希望这个计数器对所有函数都可用
  • 可以使用一个全局变量和一个函数来增加计数器
1
2
3
4
5
6
7
var counter = 0;
function add() {
counter += 1;
}
add();
add();
add();
  • 但是如果在函数内部声明了计数器, 没有人可以在不调用 add() 的情况下更改它:
1
2
3
4
5
6
7
8
function add() {
var counter = 0;
counter += 1;
}
add();
add();
add();
// the counter should now be 3, but it does not work !

2.2 JS 嵌套函数

  • 所有函数都可以访问全局作用域
  • 事实上, 在JavaScript中, 所有函数都可以访问它们”上层”的作用域
  • JavaScript支持嵌套函数。嵌套函数可以访问它们”上层”的作用域
  • 在这个例子中, 内部函数plus()可以访问父函数中的counter变量
1
2
3
4
5
6
function add() {
var counter = 0;
function plus() {counter += 1;}
plus();
return counter;
}

2.3 JS 闭包

  • 给变量 add 分配一个自调用函数的返回值
  • 自调用函数只运行一次。它将计数器设置为零(0), 并返回一个函数表达式
  • 这样add就变成了一个函数。”奇妙的”部分是它可以访问父作用域中的计数器
  • 这称为JavaScript闭包。它使函数具有”私有”变量成为可能
  • 计数器受匿名函数作用域的保护, 只能使用add函数进行更改
  • 闭包是可以访问父作用域的函数, 即使父函数已经关闭。
1
2
3
4
5
6
7
8
9
var add = (function () {
var counter = 0;
return function () {
return counter += 1;
}
})();
add();
add();
add();

2.4 闭包的定义

  • 闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment, 词法环境)的引用的组合。换而言之, 闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中, 闭包会随着函数的创建而被同时创建
  • 闭包是函数和执行它的作用域组成的综合体 ——《JavaScript权威指南》
    • 所有的函数都是闭包
  • 函数可以访问它被创建时的上下文环境, 称为闭包 ——《JavaScript语言精粹》
    • 内部函数比它的外部函数具有更长的生命周期
  • 更简单的定义: 闭包是引用了自由变量的函数
    • 自由变量是作用域可以导出到外部作用域的变量
      • 函数内部变量和函数参数都可以是自由变量
      • 函数参数不包含 this 和 arguments

2.5 词法作用域

  • 词法(lexical)一词指的是, 词法作用域根据源代码中声明变量的位置来确定该变量在何处可用。嵌套函数可访问声明于它们外部作用域的变量。
1
2
3
4
5
6
7
8
function init() {
var name = "Mozilla"; // name 是一个被 init 创建的局部变量
function displayName() { // displayName() 是内部函数, 一个闭包
alert(name); // 使用了父函数中声明的变量
}
displayName();
}
init();

2.6 闭包应用场景

  • 闭包很有用, 因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。
    • 实现私有成员
    • 保护命名空间
    • 避免污染全局变量
    • 变量需要长期驻留在内存
1
2
3
4
5
6
7
8
9
function a() {
var i = 0;
function b() {
alert(++i);
}
return b;
}
var c = a();
c();
  • 用闭包模拟私有方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */

3 作用域链

3.1 作用域链

  • function 对象同其他对象一样, 拥有 可以编程访问的属性 和 一系列不能通过代码访问而仅供 js 引擎存取的内部属性, 其中一个是 [[scope]], 包含了一个函数被创建的作用域中对象的集合。称为作用域链(Scope chain)。决定了那些数据可以被函数访问。
1
2
3
4
function add(num1, num2) {
var sum = num1 + num2;
return sum;
}

image-20221202152529413

3.1.1 执行 add 函数

  • 执行环境和作用域链
  • 绿色的称为活动对象

image-20221202152636530

3.2 例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function initUI(){
var bd = document.body,
links = document.getElementsByTagName("a"),
i = 0,
len = links.length;

while(i < len){
update(links[i++]);
}
document.getElementById("go-btn").onclick = function(){
start();
};
bd.className = "active";
}
// 改良, 全局对象和局部对象
function initUI(){
var doc = document,
bd = doc.body,
links = doc.getElementsByTagName("a"),
i= 0,len = links.length;
...
}

3.3 改变作用域链-with

  • 不推荐使用with, 在 ECMAScript 5 严格模式中该标签已被禁止
  • 推荐的替代方案是声明一个临时变量来承载你所需要的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function initUI(){
with (document){ //avoid!
var bd = body,
links = getElementsByTagName("a"),
i= 0,
len = links.length;
while(i < len){
update(links[i++]);
}
getElementById("go-btn").onclick = function(){
start();
};
bd.className = "active";
}
}

3.3.1 改变作用域链-性能问题

image-20221202153709686

  • 顶层多了一个对象, 活动对象深了一层, 性能下降

3.4 try-catch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
methodThatMightCauseAnError(); }
catch (ex){
// 异常对象将添加到作用域链的首部
alert(ex.message); //scope chain is augmented here
}

// 建议使用
try {
methodThatMightCauseAnError(); }
catch (ex){
// 没有对局部变量的使用
handleError(ex); //delegate to handler method
}

3.5 闭包、作用域和内存

1
2
3
4
5
6
7
function assignEvents(){
var id = "xdi9592";
document.getElementById("save-btn")
.onclick = function(event){
saveDocument(id);
};
}

image-20221202154313441

3.5.1 闭包执行

image-20221202154522893

4 提升

4.1 提升(Hosting)

  • 引擎会在解释JavaScript代码之前首先进行编译,编译过程中的⼀部分⼯作就是找到所有的声明,并用合适的作用域将他们关联起来,这也正是词法作用域的核心内容。
  • JavaScript 变量的另⼀个不同寻常的地方是,你可以先使用变量稍后再声明变量而不会引发异常。这⼀概念称为变量提升;JavaScript 变量感觉上是被”提升”或移到了函数或语句的最前面。
  • 但是,提升后的变量将返回 undefined 值。因此在使用或引用某个变量之后进行声明和初始化操作,这个被提升的变量仍将返回 undefined 值。
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
/* 例子 1*/
console.log(x === undefined); // true
var x = 3;

/* 例子 2*/
// will return a value of undefined
var myvar = "my value";

(function() {
console.log(myvar); // undefined
var myvar = "local value";
})();


/* 例子 1*/
var x;
console.log(x === undefined); // true
x = 3;

/* 例子 2*/
var myvar = "my value";
(function() {
var myvar;
console.log(myvar); // undefined
myvar = "local value";
})();
  • 由于存在变量提升,⼀个函数中所有的var语句应尽可能地放在接近函数顶部的地方。这个习惯将大大提升代码的清晰度。

4.2 let const

  • ES6新增块级作用域。这个区块对这些变量从⼀开始就形成了封闭作用域,直到声明语句完成,这些变量才能被访问(获取或设置),否则会报错ReferenceError
  • 暂时性死区(temporal dead zone,简 TDZ),即代码块开始到变量声明语句完成之间的区域
  • 通过 let 声明的变量没有变量提升拥有暂时性死区,作用于块级作用域
    • 当进入变量的作用域(包围它的语法块),立即为它创建(绑定)存储空间,不会立即初始化,也不会被赋值
    • 访问(获取或设置)该变量会抛出异常 ReferenceError
    • 当执行到变量的声明语句时,如果变量定义了值则会被赋值,如果变量没有定义值,则被赋值为undefined
1
2
3
4
5
6
{ // TDZ starts at beginning of scope
console.log(bar); // undefined
console.log(foo); // ReferenceError
var bar = 1;
let foo = 2; // End of TDZ (for foo)
}

4.3 temporal

  • 使用术语”temporal”是因为区域取决于执行顺序(时间),而不是编写代码的顺序(位置)。例如,下面的代码会生效,是因为即使使用 let 变量的函数出现在变量声明之前,但函数的执行是在暂时性死区的外面
1
2
3
4
5
6
7
{
// TDZ starts at beginning of scope
const func = () => console.log(letVar); // OK
// Within the TDZ letVar access throws `ReferenceError`
let letVar = 3; // End of TDZ (for letVar)
func(); // Called outside TDZ!
}

4.4 函数提升

  • 对于函数来说,只有函数声明会被提升到顶部,而函数表达式不会被提升
1
2
3
4
5
6
7
8
9
10
11
/* 函数声明 */
foo(); // "bar"
function foo() {
console.log("bar");
}

/* 函数表达式 */
baz(); // 类型错误:baz 不是⼀个函数
var baz = function() {
console.log("bar2");
};

函数会提升 class关键字不会提升

4.5 举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); // The Window

var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()()); // My Object