探新Web前端开发(五)

addEventListener()和removeEventListener()

例子:
var btn = document.querySelector('button');
function bgChange() {
var rndCol = 'rgb(' + random(255) + ',' + random(255) + ',' + random(255) + ')';
document.body.style.backgroundColor = rndCol;
}
btn.addEventListener('click', bgChange);

又可以写成匿名函数的形式:
btn.addEventListener('click', function() {
var rndCol = 'rgb(' + random(255) + ',' + random(255) + ',' + random(255) + ')';
document.body.style.backgroundColor = rndCol;
});

这种机制比前面讨论的旧机制有一些优点。首先,有一个对应的函数removeEventListener(),它删除以前添加的监听器。例如,这将删除本节中第一个代码块中设置的侦听器:
btn.removeEventListener('click', bgChange);
这在简单个的、小型的项目中可能不是很有用,但是在大型的、复杂的项目中就非常有用了,可以非常高效地清除不用的事件处理器,另外在其他的一些场景中也非常有效——比如你需要在不同环境下运行不同的事件处理器,你只需要恰当地删除或者添加事件处理器即可。

你也可以给同一个监听器注册多个处理器,下面这种方式不能实现这一点:
imyElement.onclick = functionA;
myElement.onclick = functionB;

第二行会覆盖第一行,但是下面这种方式就会正常工作了:
myElement.addEventListener('click', functionA);
myElement.addEventListener('click', functionB);

当元素被点击时两个函数都会工作

事件对象

有时候在事件处理函数内部,您可能会看到一个固定指定名称的参数,例如event,evt或简单的e。 这被称为事件对象,它被自动传递给事件处理函数,以提供额外的功能和信息
事件对象 e 的target属性始终是事件刚刚发生的元素的引用。
例如:
var divs = document.querySelectorAll('div');
for (var i = 0; i < divs.length; i++) {
divs[i].onclick = function(e) {
e.target.style.backgroundColor = bgChange();
}
}

防止默认行为

有时你会遇到一写情况,您希望事件不执行它的默认行为。 最常见的例子是Web表单,例如自定义注册表单。 当你填写详细信息并按提交按钮时,自然行为是将数据提交到服务器上的指定页面进行处理,并将浏览器重定向到某种“成功消息”页面(或 相同的页面,如果另一个没有指定。)

当用户没有正确提交数据时,麻烦就来了 - 作为开发人员,你希望停止提交信息给服务器,并给他们一个错误提示,告诉他们什么做错了,以及需要做些什么来修正错误。 一些浏览器支持自动的表单数据验证功能,但由于许多浏览器不支持,因此建议不要依赖这些功能,并实现自己的验证检查。 我们来看一个简单的例子。
首先,一个简单的HTML表单,要求输入你的姓名:

<form>
  <div>
    <label for="fname">First name: </label>
    <input id="fname" type="text">
  </div>
  <div>
    <label for="lname">Last name: </label>
    <input id="lname" type="text">
  </div>
  <div>
     <input id="submit" type="submit">
  </div>
</form>
<p></p>

这里我们用一个onsubmit事件处理程序(在提交的时候,在一个表单上发起submit事件)来实现一个非常简单的检查,用于测试文本字段是否为空。 如果是,我们在事件对象上调用preventDefault()函数,这样就停止了表单提交,然后在我们表单下面的段落中显示一条错误消息,告诉用户什么是错误的:
var form = document.querySelector('form');
var fname = document.getElementById('fname');
var lname = document.getElementById('lname');
var para = document.querySelector('p');
form.onsubmit = function(e) {
if (fname.value === '' || lname.value === '') {
e.preventDefault();
para.textContent = 'You need to fill in both names!';
}
}

事件冒泡和捕获

事件冒泡和捕获是描述在同一个元素上同时激活两个相同事件类型的不同的事件处理程序时会发生什么的两种机制.
这是一个非常简单的示例,显示和隐藏<div>其中的<video>元素:

<button>Display video</button>

<div class="hidden">
  <video>
    <source src="rabbit320.mp4" type="video/mp4">
    <source src="rabbit320.webm" type="video/webm">
    <p>Your browser doesn't support HTML5 video. Here is a <a href="rabbit320.mp4">link to the video</a> instead.</p>
  </video>
</div>

<button>点击时,通过将类属性<div>从… 更改hidden到showing(示例的CSS包含这两个类,分别将屏幕和屏幕分别放置在屏幕上):显示视频:
btn.onclick = function() {
videoBox.setAttribute(‘class’, ‘showing’);
}
然后我们再添加一些onclick事件处理程序 - 第一个到<div>第二个事件处理程序<video>。这个想法是当<div>视频外部的区域被点击时,应该再次隐藏该框; 当视频本身被点击时,视频应该开始播放。
videoBox.onclick = function() {
videoBox.setAttribute(‘class’, ‘hidden’);
};

video.onclick = function() {
video.play();
};
但是有一个问题 - 当你点击video开始播放的视频时,它会在同一时间导致<div>也被隐藏。 这是因为video在<div>之内 - video是<div>的一个子元素 - 所以点击video实际上是同时也运行<div>上的事件处理程序。

冒泡和捕捉解释

当一个事件触发了一个有父元素的元素(例如我们的<video>时),现代浏览器运行两个不同的阶段 - 捕获阶段和冒泡阶段。 在捕获阶段:

  • 浏览器检查元素的最外层祖先(<html>)是否在捕获阶段中注册了一个onclick事件处理程序,如果是,则运行它。
  • 然后,它移动到<html>中的下一个元素,并执行相同的操作,然后是下一个元素,依此类推,直到到达实际点击的元素。

在冒泡阶段,恰恰相反:

  • 浏览器检查实际点击的元素是否在冒泡阶段中注册了一个onclick事件处理程序,如果是,则运行它
  • 然后它移动到下一个直接的祖先元素,并做同样的事情,然后是下一个,等等,直到它到达<html>元素。

在现代浏览器中,默认情况下,所有事件处理程序都在冒泡阶段进行注册。因此在这:

  • 它找到video.onclick…处理程序并运行它,因此视频首先开始播放。
  • 然后它找到videoBox.onclick…处理程序并运行它,因此视频也被隐藏。

用stopPropagation()修复问题

这是令人讨厌的行为,但有一种方法来解决它!标准事件对象具有可用的名为 stopPropagation()的函数, 当在事件对象上调用该函数时,它只会让当前事件处理程序运行,但事件不会在bubble链上进一步扩大,因此不会再有任何以外的事件处理程序运行。
video.onclick = function(e) {
e.stopPropagation();
video.play();
};

事件委托

冒泡也允许我们利用事件委托 - 这个概念依赖于这样一个事实:如果你想要一些代码运行,当你点击任何一个大量的子元素,你可以设置事件监听器在他们的父母,并有事件监听器气泡对每个孩子的影响,而不是必须单独设置每个孩子的事件监听器。

JavaScript 对象基础

一个对象是一个包含相关资料和功能的集体(通常由一些变量和函数组成,我们称之为对象里面的属性和方法),让我们通过一个例子来了解它们
var person = {
name : [‘Bob’, ‘Smith’],
age : 32,
gender : ‘male’,
interests : [‘music’, ‘skiing’],
bio : function() {
alert(this.name[0] + ’ ’ + this.name[1] + ’ is ’ + this.age + ’ years old. He likes ’ + this.interests[0] + ’ and ’ + this.interests[1] + ‘.’);
},
greeting: function() {
alert(‘Hi! I\’m ’ + this.name[0] + ‘.’);
}
};
保存刷新后, 尝试在你的input标签里输入下面的内容:
person.name[0]
person.age
person.interests[1]
person.bio()
person.greeting()
对象成员的值可以是任意的,在我们的person对象里有字符串(string),数字(number),两个数组(array),两个函数(function)。前4个成员是资料项目,被称为对象的属性(property),后两个成员是函数,允许对象对资料做一些操作,被称为对象的方法(method)

构建函数和对象实例

让我们看看如何通过一个普通的函数定义一个”人“:
function Person(name) {
this.name = name;
this.greeting = function() {
alert(‘Hi! I\’m ’ + this.name + ‘.’);
};
}
那如何调用构建函数创建新的实例呢?

  1. 将下面的代码加在你之前的代码下面:
    var person1 = new Person(‘Bob’);
    var person2 = new Person(‘Sarah’);
  2. 保存并刷新浏览器,在console 里输入如下代码:
    person1.name
    person1.greeting()
    person2.name
    person2.greeting()

你现在看到页面上有两个对象,每一个保存在不同的命名空间里,当你访问它们的属性和方法时,你需要使用 person1或者 person2 来调用它们。

创建我们最终的构造函数

移除掉你之前写的所有代码, 用如下构造函数替代 —— 实现原理上,这与我们之前的例子并无二致, 只是变得稍稍复杂了些:
function Person(first, last, age, gender, interests) {
this.name = {
first,
last
};
this.age = age;
this.gender = gender;
this.interests = interests;
this.bio = function() {
alert(this.name.first + ’ ’ + this.name.last + ’ is ’ + this.age + ’ years old. He likes ’ + this.interests[0] + ’ and ’ + this.interests[1] + ‘.’);
};
this.greeting = function() {
alert(‘Hi! I\’m ’ + this.name.first + ‘.’);
};
};
接下来加上这样一行代码, 用来创建它的一个对象实例:
var person1 = new Person(‘Bob’, ‘Smith’, 32, ‘male’, [‘music’, ‘skiing’]);

对象原型

JavaScript 常被描述为一种基于原型的语言——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 ,它解释了为何一个对象会拥有定义在其他对象中的属性和方法。

准确地说,这些属性和方法定义在 Object 的构造器函数之上,而非对象实例本身。

在传统的 OOP 中,首先定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。在 JavaScript 中并不如此复制——而是在对象实例和它的构造器之间建立一个连接(作为原型链中的一节),以后通过上溯原型链,在构造器中找到这些属性和方法。

理解原型对象

让我们回到 Person() 构造器的例子。
在 JavaScript 控制台输入 “person1.”,你会看到,浏览器将根据这个对象的可用的成员名称进行自动补全

在这个列表中,你可以看到定义在 person1 的原型对象、即 Person() 构造器中的成员—— name、age、gender、interests、bio、greeting。同时也有一些其他成员—— watch、valueOf 等等——这些成员定义在 Person() 构造器的原型对象、即 Object 之上。下图展示了原型链的运作机制。

那么,调用 person1 的“实际定义在 Object 上”的方法时,会发生什么?比如:
person1.valueOf()
在这个例子中发生了如下过程:

  • 浏览器首先检查,person1 对象是否具有可用的 valueOf() 方法。
  • 如果没有,则浏览器检查 person1 对象的原型对象(即 Person)是否具有可用的 valueof() 方法。
  • 如果也没有,则浏览器检查 Person() 构造器的原型对象(即 Object)是否具有可用的 valueOf() 方法。Object 具有这个方法,于是该方法被调用

注意:必须重申,原型链中的方法和属性没有被复制到其他对象——它们被访问需要通过前面所说的“原型链”的方式

prototype 属性:继承成员被定义的地方

那么,那些继承的属性和方法在哪儿定义呢?如果你查看 Object 参考页,会发现左侧列出许多属性和方法——大大超过我们在 person1 对象中看到的继承成员的数量。某些属性或方法被继承了,而另一些没有——为什么呢?

原因在于,继承的属性和方法是定义在 prototype 属性之上的,那些以 Object.prototype. 开头的属性,而非仅仅以 Object. 开头的属性。prototype 属性的值是一个对象,我们希望被原型链下游的对象继承的属性和方法,都被储存在其中。

create()

用 Object.create() 方法创建新的对象实例。
例如,在上个例子的 JavaScript 控制台中输入:
var person2 = Object.create(person1);
create() 实际做的是从指定原型对象创建一个新的对象。这里以 person1 为原型对象创建了 person2 对象。在控制台输入:
person2.__proto__
结果返回 person1 对象。

constructor 属性

每个对象实例都具有 constructor 属性,它指向创建该实例的构造器函数。

  1. 例如,在控制台中尝试下面的指令:
    person1.constructor
    person2.constructor

    都将返回 Person() 构造器,因为该构造器包含这些实例的原始定义。
    一个小技巧是,你可以在 constructor 属性的末尾添加一对圆括号(括号中包含所需的参数),从而用这个构造器创建另一个对象实例。毕竟构造器是一个函数,故可以通过圆括号调用;只需在前面添加 new 关键字,便能将此函数作为构造器使用。
  2. 在控制台中输入:
    var person3 = new person1.constructor('Karen', 'Stephenson', 26, 'female', ['playing drums', 'mountain climbing']);
  3. 现在尝试访问新建对象的属性,例如:
    person3.name.first
    person3.age
    person3.bio()

此外,constructor 属性还有其他用途。比如,想要获得某个对象实例的构造器的名字,可以这么用:
person1.constructor.name

修改原型

从我们从下面这个例子来看一下如何修改构造器的 prototype 属性。
这段代码将为构造器的 prototype 属性添加一个新的方法:
Person.prototype.farewell = function() {
alert(this.name.first + ' has left the building. Bye for now!');
}

在浏览器中加载页面,然后在控制台输入:
person1.farewell();

你会看到一条警告信息,其中还显示了构造器中定义的人名;这很有用。但更关键的是,整条继承链动态地更新了,任何由此构造器创建的对象实例都自动获得了这个方法。

再想一想这个过程。我们的代码中定义了构造器,然后用这个构造器创建了一个对象实例,此后向构造器的 prototype 添加了一个新的方法:
function Person(first, last, age, gender, interests) {
// 属性与方法定义
};
var person1 = new Person('Tammi', 'Smith', 32, 'neutral', ['music', 'skiing', 'kickboxing']);
Person.prototype.farewell = function() {
alert(this.name.first + ' has left the building. Bye for now!');
}

但是 farewell() 方法仍然可用于 person1 对象实例——旧有对象实例的可用功能被自动更新了。这证明了先前描述的原型链模型。这种继承模型下,上游对象的方法不会复制到下游的对象实例中;下游对象本身虽然没有定义这些方法,但浏览器会通过上溯原型链、从上游对象中找到它们。这种继承模型提供了一个强大而可扩展的功能系统。

事实上,一种极其常见的对象定义模式是,在构造器(函数体)中定义属性、在 prototype 属性上定义方法。如此,构造器只包含属性定义,而方法则分装在不同的代码块,代码更具可读性。例如:
// 构造器及其属性定义
function Test(a,b,c,d) {
// 属性定义
};
// 定义第一个方法
Test.prototype.x = function () { ... }
// 定义第二个方法
Test.prototype.y = function () { ... }
// 等等……

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值