Yao Lirong's Blog

JavaScript Manual

2022/06/11

I hate web programming, but looks like I really have to learn it. Notes from Liao Xuefeng’s JS course

Introduction

Variables and Objects

  • Declare a variable with var: var x = 3
  • JavaScript has both null and undefined, but most of the time we just use null
  • Print out value in browser: console.log()
  • Ternary if-else Operator: condition ? then : else

Strict Mode

JavaScript在设计之初,为了方便初学者学习,并不强制要求用var申明变量。这个设计错误带来了严重的后果:如果一个变量没有通过var申明就被使用,那么该变量就自动被申明为全局变量:

1
i = 10; // i现在是全局变量

在同一个页面的不同的JavaScript文件中,如果都不用var申明,恰好都使用了变量i,将造成变量i互相影响,产生难以调试的错误结果。

与之相对,使用var申明的变量是局部变量。

在strict模式下运行的JavaScript代码,强制通过var申明变量,未使用var申明变量就使用的,将导致运行错误。启用strict模式的方法是在JavaScript代码的第一行写上:

1
'use strict';

Strings

Strings are immutable in JS.

1
2
3
4
5
6
7
8
9
var s = 'Hello, world!';
s.length; // 13

s[6]; // ' '
s[13]; // undefined 超出范围的索引不会报错,但一律返回undefined

// 可以用 + 链接多个字符串,但也有一种更简洁的方法可以在字符串中嵌套其他变量
var name = '小明';
var message = "你好, ${name}";

Array

1
2
var arr = [1, 2, 3.14, 'Hello', null, true];
arr.length; // 6

You can assign values to array, but 如果通过索引赋值时,索引超过了范围,同样会引起Array大小的变化:

1
2
3
var arr = [1, 2, 3];
arr[1] = 99; // arr == [1, 99, 3]
arr[5] = 'x'; // arr == [1, 2, 3, undefined, undefined, 'x']
  • arr.push(e) adds variable e to the end of the array, arr.pop() delete an element from the end of the array
  • arr.unshift(e) adds variable e to the beginning of the array, arr.shift() delete an element from the beginning of the array

Object

JavaScript Object is a key-value map that can be accessed both in traditional object way and the python dict way.

1
2
3
4
5
var xiaohong = {
name: '小红',
birth: 1990,
tags: ['js', 'web', 'mobile'],
'middle-school': 'No.1 Middle School'};

xiaohong 的属性名 middle-school 不是一个有效的变量,就需要用''括起来。访问这个属性也无法使用.操作符,必须用 ['xxx'] 来访问。其他规则的变量虽然也可以用 xiaohong['name'] 来访问 xiaohongname 属性,不过 xiaohong.name 的写法更简洁。

1
2
3
xiaohong['middle-school']; // 'No.1 Middle School'
xiaohong['name']; // '小红'
xiaohong.name; // '小红'

给不存在的属性赋值以声明新的属性,也可以使用 delete 删除一个既有的属性

1
2
3
4
xiaoming.age = 18; // 新增一个age属性
xiaoming.age; // 18
delete xiaoming.age; // 删除age属性
xiaoming.age; // undefined

Use in to detect whether an object has a certain property. 不过要小心,如果in判断一个属性存在,这个属性不一定是xiaoming的,它可能是xiaoming继承得到的。比如 toString 定义在 object 对象中,而所有对象都继承 object,所以 xiaoming 也拥有 toString 属性。要判断一个属性是否是xiaoming自身拥有的,而不是继承得到的,可以用hasOwnProperty()方法:

1
2
3
4
'name' in xiaoming; // true 
'toString' in xiaoming; // true
xiaoming.hasOwnProperty('name'); // true
xiaoming.hasOwnProperty('toString'); // false

Loop

  • for loop: for (i=0; i<arr.length; i++) {...}

  • for of loop: for (var key of xiaoming) {...}. This is a better version of for in loop JS has. To filter out those inherited properties, use

    1
    2
    3
    4
    for (var key of xiaoming) {
    if (xiaoming.hasOwnProperty(key)) {
    console.log(key); // 'name', 'age', 'city'
    } }
  • while loop: while (n > 0) { ... }

  • do while loop: do { ... } while()

Function

1
2
3
4
5
6
7
var abs = function (x) {
return x;
};

function abs(x) {
return x;
}

由于JavaScript允许传入任意个参数而不影响调用,因此传入的参数比定义的参数多也没有问题,虽然函数内部并不需要这些参数:

1
2
abs(10, 'blablabla'); // 返回10
abs(-9, 'haha', 'hehe', null); // 返回9

传入的参数比定义的少也没有问题:此时abs(x)函数的参数x将收到undefined,计算结果为NaN

1
abs(); // 返回NaN

JSON

JSON 是 JavaScript Object Notation 的缩写,实际上是 JavaScript 的一个子集。所以我们可以非常方便地将 Java Object 转换成 JSON。

1
var s = JSON.stringify(xiaoming, null, " ");

要输出得好看一些,可以加上参数,按缩进输出:

1
JSON.stringify(xiaoming, null, '  ');

第二个参数用于控制如何筛选对象的键值,如果我们只想输出指定的属性,可以传入Array

1
JSON.stringify(xiaoming, ['name', 'birth'], '  ');

结果:

1
2
{ "name": "小明",
"birth": 1990}

还可以传入一个函数,这样对象的每个键值对都会被函数先处理:

1
2
3
4
5
6
7
8
function convert(key, value) {
if (typeof value === 'string') {
return value.toUpperCase();
}
return value;
}

JSON.stringify(xiaoming, convert, ' ');

上面的代码把所有属性值都变成大写:

1
2
3
4
5
{
"name": "小明",
"birth": 1990,
'middle-school': 'No.1 MIDDLE SCHOOL'
}

如果我们还想要精确控制如何序列化小明,可以给xiaoming定义一个toJSON()的方法,直接返回JSON应该序列化的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var xiaoming = {
name: '小明',
age: 14,
birth: 1990,
middle-school: 'No.1 MIDDLE SCHOOL'
toJSON: function () {
return { // 只输出name和age,并且改变了key:
'Name': this.name,
'Age': this.age
};
}
};

JSON.stringify(xiaoming); // '{"Name":"小明","Age":14}'

拿到一个JSON格式的字符串,我们直接用JSON.parse()把它变成一个JavaScript对象:

1
2
3
4
JSON.parse('[1,2,3,true]'); // [1, 2, 3, true]
JSON.parse('{"name":"小明","age":14}'); // Object {name: '小明', age: 14}
JSON.parse('true'); // true
JSON.parse('123.45'); // 123.45

jQuery

$ is an alias to jQuery. Therefore, if you encounter error Uncaught TypeError: $ is not a function, you can just replace the $ with jQuery instead (e.g. jQuery('#abc'))

Intro to Selector

jQuery Object

一个 jQuery 命令返回的对象是 jQuery 对象。它类似数组,每个元素都是一个引用了DOM节点的对象。以下面的查找为例,

  • 如果idabc<div>存在,返回的jQuery对象如下:

    1
    [<div id="abc">...</div>]
  • 如果idabc<div>不存在,返回的jQuery对象如下:

    1
    []

总之jQuery的选择器不会返回undefined或者null,这样的好处是你不必在下一行判断if (div === undefined)

jQuery对象和DOM对象之间可以互相转化:

1
2
3
var div = $('#abc'); // jQuery对象
var divDom = div.get(0); // 假设存在div,获取第1个DOM元素
var another = $(divDom); // 重新把DOM包装为jQuery对象

通常情况下你不需要获取DOM对象,直接使用jQuery对象更加方便。如果你拿到了一个DOM对象,那可以简单地调用$(aDomObject)把它变成jQuery对象,这样就可以方便地使用jQuery的API了。

按ID查找

在ID前加上 # 以进行ID查找

1
2
// 查找<div id="abc">:
var div = $('#abc');

按tag查找

直接写上tag名称进行tag查找:

1
2
var ps = $('p'); // 返回所有<p>节点
ps.length; // 数一数页面有多少个<p>节点

按class查找

在class名称前加一个. 进行按class查找

1
2
3
4
var a = $('.red'); // 所有节点包含`class="red"`都将返回
// 例如:
// <div class="red">...</div>
// <p class="green red">...</p>

通常很多节点有多个class,我们可以查找同时包含redgreen的节点:

1
2
3
4
var a = $('.red.green'); // 注意没有空格!
// 符合条件的节点:
// <div class="red green">...</div>
// <div class="blue green red">...</div>

按属性查找

一个DOM节点除了idclass外还可以有很多属性,很多时候按属性查找会非常方便,比如在一个表单中按属性来查找:

1
2
3
var email = $('[name=email]'); // 找出<??? name="email">
var passwordInput = $('[type=password]'); // 找出<??? type="password">
var a = $('[items="A B"]'); // 找出<??? items="A B">

当属性的值包含空格等特殊字符时,需要用双引号括起来。

按属性查找还可以使用前缀查找或者后缀查找:

1
2
3
4
var icons = $('[name^=icon]'); // 找出所有name属性值以icon开头的DOM
// 例如: name="icon-1", name="icon-2"
var names = $('[name$=with]'); // 找出所有name属性值以with结尾的DOM
// 例如: name="startswith", name="endswith"

这个方法尤其适合通过class属性查找,且不受class包含多个名称的影响:

1
2
var icons = $('[class^="icon-"]'); // 找出所有class包含至少一个以`icon-`开头的DOM
// 例如: class="icon-clock", class="abc icon-home"

AND查找

直接写多个条件,条件间不加空格来执行AND查找。

如果我们查找$('[name=email]'),很可能把表单外的<div name="email">也找出来,但我们只希望查找<input>,就可以这么写:

1
var emailInput = $('input[name=email]'); // 不会找出<div name="email">

前文中的同时查找多个class也是一个例子。同样的,根据tag和class来组合查找也很常见:

1
var tr = $('tr.red'); // 找出<tr class="red ...">...</tr>

OR查找

把多个选择器用,组合起来,查找所有符合任一选择器条件的 DOM 节点。

1
2
$('p,div'); // 把<p>和<div>都选出来
$('p.red,p.green'); // 把<p class="red">和<p class="green">都选出来

要注意的是,选出来的元素是按照它们在HTML中出现的顺序排列的,而且不会有重复元素。例如,<p class="red green">不会被上面的$('p.red,p.green')选择两次。

Descendent Selector

以如下结构为例

1
2
3
4
5
6
7
8
<!-- HTML结构 -->
<div class="testing">
<ul class="lang">
<li class="lang-javascript">JavaScript</li>
<li class="lang-python">Python</li>
<li class="lang-lua">Lua</li>
</ul>
</div>

层级选择器(Descendant Selector)

如果两个DOM元素具有层级关系,就可以用$('ancestor descendant')来选择,层级之间用空格隔开。

要选出上例中的 JavaScript,可以用层级选择器:

1
2
$('ul.lang li.lang-javascript'); // [<li class="lang-javascript">JavaScript</li>]
$('div.testing li.lang-javascript'); // [<li class="lang-javascript">JavaScript</li>]

因为<div><ul>都是<li>的祖先节点,所以上面两种方式都可以选出相应的<li>节点。

要选择所有的<li>节点,用:

1
$('ul.lang li');

这种层级选择器相比单个的选择器好处在于,它缩小了选择范围,因为首先要定位父节点,才能选择相应的子节点,这样避免了页面其他不相关的元素。例如:

1
$('form[name=upload] input');

就把选择范围限定在name属性为upload的表单里。如果页面有很多表单,其他表单的<input>不会被选择。

多层嵌套也是允许的:

1
$('form.test p input'); // 在form表单选择被<p>包含的<input>

子选择器(Child Selector)

Child Selector $('parent>child') 与 Descendant Selector 几乎完全一样,但是限定了层级关系必须是父子关系,就是<child>节点必须是<parent>节点的直属子节点。还是以上面的例子:

1
2
$('ul.lang>li.lang-javascript'); // 可以选出[<li class="lang-javascript">JavaScript</li>]
$('div.testing>li.lang-javascript'); // [], 无法选出,因为<div>和<li>不构成父子关系

过滤器(Filter)

过滤器一般不单独使用,它通常附加在选择器上,帮助我们更精确地定位元素。观察过滤器的效果:

1
2
3
4
5
6
7
$('ul.lang li'); // 选出JavaScript、Python和Lua 3个节点

$('ul.lang li:first-child'); // 仅选出JavaScript
$('ul.lang li:last-child'); // 仅选出Lua
$('ul.lang li:nth-child(2)'); // 选出第N个元素,N从1开始
$('ul.lang li:nth-child(even)'); // 选出序号为偶数的元素
$('ul.lang li:nth-child(odd)'); // 选出序号为奇数的元素

查找和过滤

本节使用如下例子

1
2
3
4
5
6
7
8
<!-- HTML结构 -->
<ul class="lang">
<li class="js dy">JavaScript</li>
<li class="dy">Python</li>
<li id="swift">Swift</li>
<li class="dy">Scheme</li>
<li name="haskell">Haskell</li>
</ul>

查找

find()查找:

1
2
3
var ul = $('ul.lang'); // 获得<ul>
var dy = ul.find('#swift'); // 获得 Lua
var equiv = $('ul.lang>#swift'); // 与前两行等效

节点中移动

此小节介绍的所有函数都是当无参数调用时,返回目标节点。传入参数时,参数是过滤条件,当符合条件时返回目标节点,不符合时返回空 jQuery 对象

如果要从当前节点开始向上查找,使用parent()方法:

1
2
3
var swf = $('#swift'); // 获得Swift
var parent = swf.parent(); // 获得Swift的上层节点<ul>
var a = swf.parent('.red'); // 获得Swift的上层节点<ul>,同时传入过滤条件。如果ul不符合条件,返回空jQuery对象

对于位于同一层级的节点,可以通过next()prev()方法,例如当我们已经拿到Swift节点后:

1
2
3
4
5
6
7
var swift = $('#swift');

swift.next(); // Scheme
swift.next('[name=haskell]'); // 空的jQuery对象,因为Swift的下一个元素Scheme不符合条件[name=haskell]

swift.prev(); // Python
swift.prev('.dy'); // Python,因为Python同时符合过滤器条件.dy

函数式编程

filter()方法可以过滤掉不符合选择器条件的节点:

1
2
var langs = $('ul.lang li'); // 拿到JavaScript, Python, Swift, Scheme和Haskell
var a = langs.filter('.dy'); // 拿到JavaScript, Python, Scheme

或者传入一个函数,要特别注意函数内部的this被绑定为DOM对象,不是jQuery对象:

1
2
3
4
var langs = $('ul.lang li'); // 拿到JavaScript, Python, Swift, Scheme和Haskell
langs.filter(function () {
return this.innerHTML.indexOf('S') === 0; // 返回S开头的节点
}); // 拿到Swift, Scheme

map()方法把一个jQuery对象包含的若干DOM节点转化为其他对象:

1
2
3
4
var langs = $('ul.lang li'); // 拿到JavaScript, Python, Swift, Scheme和Haskell
var arr = langs.map(function () {
return this.innerHTML;
}).get(); // 用get()拿到包含string的Array:['JavaScript', 'Python', 'Swift', 'Scheme', 'Haskell']

Modifying DOM Contents

HTML

本小节使用以下例子:

1
2
3
4
5
<!-- HTML结构 -->
<ul id="test-ul">
<li class="js">JavaScript</li>
<li name="book">Java &amp; JavaScript</li>
</ul>

使用 text()html() 方法分别获取节点的文本和原始HTML文本。无参数调用是获取文本/HTML,传入参数就变成设置文本/HTML。

1
2
3
4
5
6
$('#test-ul li[name=book]').text(); // 'Java & JavaScript'
$('#test-ul li[name=book]').html(); // 'Java &amp; JavaScript'

var j1 = $('#test-ul li.js');
j1.html('<span style="color: red">JavaScript</span>'); // 第一行被设置成红色的 JavaScript
$('#test-ul li[name=book]').text('书'); // 第二行被设置成 "书"

一个jQuery对象可以包含0个或任意个DOM对象,它的方法实际上会作用在对应的每个DOM节点上。如果作用在一个空的 jQuery 节点上,也不会报错

1
2
$('#test-ul li').text('JS'); // 两个节点都变成了JS
$('#not-exist').text('Hello'); // 不会报错,没有节点被设置成 'Hello'

CSS

和 HTML 类似,对 jQuery 对象下的 css('name') 读取 CSS 属性, css('name', 'value') 方法设置 CSS 属性

1
2
3
4
5
var div = $('#test-div');
div.css('color'); // '#000033', 获取CSS属性
div.css('color', '#336699'); // 设置CSS属性
div.css('color', ''); // 清除CSS属性
div.css('background-color', '#ffd351').css('color', 'red'); // 连续设置两个 CSS 属性

Modifying DOM Structure

对于如下HTML片段,

1
2
3
4
5
6
7
8
<div id="test-div">
<ul>
<li><span>JavaScript</span></li>
<li><span>Python</span></li>
<li><span>Swift</span></li>
</ul>
</div>

添加 DOM

首先要拿到<ul>节点:

1
var ul = $('#test-div>ul');

然后,调用append()传入HTML片段。append()把DOM添加到最后,prepend()则把DOM添加到最前。

1
ul.append('<li><span>Haskell</span></li>');

除了接受字符串,append()还可以传入原始的DOM对象或jQuery对象

1
2
3
4
5
6
7
8
// 创建DOM对象:
var ps = document.createElement('li');
ps.innerHTML = '<span>Pascal</span>';
// 添加DOM对象:
ul.append(ps);

// 添加jQuery对象:
ul.append($('#scheme'));

同级节点可以用after()或者before()方法。即如果要把新节点插入到指定位置,例如,JavaScript和Python之间,那么,可以先定位到JavaScript,然后用after()方法:

1
2
var js = $('#test-div>ul>li:first-child');
js.after('<li><span>Lua</span></li>');

删除 DOM

要删除DOM节点,拿到jQuery对象后直接调用remove()方法就可以了。如果jQuery对象包含若干DOM节点,实际上可以一次删除多个DOM节点:

1
2
var li = $('#test-div>ul>li');
li.remove(); // 所有<li>全被删除

Event

假设要在用户点击了超链接时弹出提示框,我们用jQuery这样绑定一个click事件:

1
2
3
4
5
6
7
8
9
10
11
/* HTML:
*
* <a id="test-link" href="#0">点我试试</a>
*
*/

// 获取超链接的jQuery对象:
var a = $('#test-link');
a.on('click', function () {
alert('Hello!');
});

on方法用来绑定一个事件,我们需要传入事件名称和对应的处理函数。

另一种更简化的写法是直接调用click()方法:两者完全等价。我们通常用这种写法。

1
2
3
a.click(function () {
alert('Hello!');
});

事件类型

鼠标事件
  • click: 鼠标单击时触发;
  • dblclick:鼠标双击时触发;
  • mouseenter:鼠标进入时触发;
  • mouseleave:鼠标移出时触发;
  • mousemove:鼠标在DOM内部移动时触发;
  • hover:鼠标进入和退出时触发两个函数,相当于mouseenter加上mouseleave。
键盘事件

键盘事件仅作用在当前焦点的DOM上,通常是<input><textarea>

  • keydown:键盘按下时触发;
  • keyup:键盘松开时触发;
  • keypress:按一次键后触发。
其他事件
  • focus:当DOM获得焦点时触发;
  • blur:当DOM失去焦点时触发;
  • change:当<input><select><textarea>的内容改变时触发;
  • submit:当<form>提交时触发;
  • ready:当页面被载入并且DOM树完成初始化后触发。

ready

其中,ready仅作用于document对象。由于ready事件在DOM完成初始化后触发,且只触发一次,所以非常适合用来写其他的初始化代码。假设我们想给一个<form>表单绑定submit事件,下面的代码没有预期的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html>
<head>
<script>
// 代码有误:
$('#testForm').on('submit', function () {
alert('submit!');
});
</script>
</head>
<body>
<form id="testForm">
...
</form>
</body>

因为JavaScript在此执行的时候,<form>尚未载入浏览器,所以$('#testForm)返回[],并没有绑定事件到任何DOM上。所以我们自己的初始化代码必须放到document对象的ready事件中,保证DOM已完成初始化。这样写就没有问题了。因为相关代码会在DOM树初始化后再执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<head>
<script>
$(document).on('ready', function () {
$('#testForm).on('submit', function () {
alert('submit!');
});
});
</script>
</head>
<body>
<form id="testForm">
...
</form>
</body>

由于ready事件使用非常普遍,所以可以这样简化:

1
2
3
4
5
6
$(document).ready(function () {
// on('submit', function)也可以简化:
$('#testForm).submit(function () {
alert('submit!');
});
});

甚至还可以再简化为:

1
2
3
$(function () {
// init...
});

上面的这种写法最为常见。如果你遇到$(function () {...})的形式,牢记这是document对象的ready事件处理函数。

完全可以反复绑定事件处理函数,它们会依次执行:

1
2
3
4
5
6
7
8
9
$(function () {
console.log('init A...');
});
$(function () {
console.log('init B...');
});
$(function () {
console.log('init C...');
});

事件参数

有些事件,如mousemovekeypress,我们需要获取鼠标位置和按键的值,否则监听这些事件就没什么意义了。所有事件都会传入Event对象作为参数,可以从Event对象上获取到更多的信息:

1
2
3
4
5
$(function () {
$('#testMouseMoveDiv').mousemove(function (e) {
$('#testMouseMoveSpan').text('pageX = ' + e.pageX + ', pageY = ' + e.pageY);
});
});

取消绑定

一个已被绑定的事件可以解除绑定,通过off('click', function)实现:

1
2
3
4
5
6
7
8
9
10
function hello() {
alert('hello!');
}

a.click(hello); // 绑定事件

// 10秒钟后解除绑定:
setTimeout(function () {
a.off('click', hello);
}, 10000);

需要特别注意的是,下面这种写法是无效的:

1
2
3
4
5
6
7
8
9
// 绑定事件:
a.click(function () {
alert('hello!');
});

// 解除绑定:
a.off('click', function () {
alert('hello!');
});

这是因为两个匿名函数虽然长得一模一样,但是它们是两个不同的函数对象,off('click', function () {...})无法移除已绑定的第一个匿名函数。

为了实现移除效果,可以使用off('click')一次性移除已绑定的click事件的所有处理函数。

同理,无参数调用off()一次性移除已绑定的所有类型的事件处理函数。

事件触发条件

一个需要注意的问题是,事件的触发总是由用户操作引发的。例如,我们监控文本框的内容改动:

1
2
3
4
var input = $('#test-input');
input.change(function () {
console.log('changed...');
});

当用户在文本框中输入时,就会触发change事件。但是,如果用JavaScript代码去改动文本框的值,将不会触发change事件:

1
2
var input = $('#test-input');
input.val('change it!'); // 无法触发change事件

有些时候,我们希望用代码触发change事件,可以直接调用无参数的change()方法来触发该事件:

1
2
3
var input = $('#test-input');
input.val('change it!');
input.change(); // 触发change事件

input.change()相当于input.trigger('change'),它是trigger()方法的简写。

为什么我们希望手动触发一个事件呢?如果不这么做,很多时候,我们就得写两份一模一样的代码。

CATALOG
  1. 1. Introduction
    1. 1.1. Variables and Objects
    2. 1.2. Strict Mode
    3. 1.3. Strings
    4. 1.4. Array
    5. 1.5. Object
    6. 1.6. Loop
  2. 2. Function
  3. 3. JSON
  4. 4. jQuery
    1. 4.1. Intro to Selector
      1. 4.1.1. jQuery Object
      2. 4.1.2. 按ID查找
      3. 4.1.3. 按tag查找
      4. 4.1.4. 按class查找
      5. 4.1.5. 按属性查找
      6. 4.1.6. AND查找
      7. 4.1.7. OR查找
    2. 4.2. Descendent Selector
      1. 4.2.1. 层级选择器(Descendant Selector)
      2. 4.2.2. 子选择器(Child Selector)
      3. 4.2.3. 过滤器(Filter)
    3. 4.3. 查找和过滤
      1. 4.3.1. 查找
      2. 4.3.2. 节点中移动
      3. 4.3.3. 函数式编程
    4. 4.4. Modifying DOM Contents
      1. 4.4.1. HTML
      2. 4.4.2. CSS
    5. 4.5. Modifying DOM Structure
      1. 4.5.1. 添加 DOM
      2. 4.5.2. 删除 DOM
    6. 4.6. Event
      1. 4.6.1. 事件类型
        1. 4.6.1.1. 鼠标事件
        2. 4.6.1.2. 键盘事件
        3. 4.6.1.3. 其他事件
      2. 4.6.2. ready
      3. 4.6.3. 事件参数
      4. 4.6.4. 取消绑定
      5. 4.6.5. 事件触发条件