caizhendi / blog

no something!
1 stars 0 forks source link

《javascript高级程序设计(第三版)》(2) #2

Open caizhendi opened 4 years ago

caizhendi commented 4 years ago

2020.04.01

阅读进度:第六章 6.2

1.理解对象

ECMA-262 把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。”这就相当于说对象是一组没有特定顺序的值。我们可以把 ECMAScript 的对象想象成散列表:无非就是一组名值对,其中值可以是数据或函数。

1.1属性类型

ECMAScript 中有两种属性:数据属性和访问器属性。

1.1.1数据属性

数据属性包含一个数据值的位置。在这个位置可以读取和写入值。
[[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true。
[[Enumerable]]:表示能否通过for-in循环返回属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true。
[[Writable]]:表示能否修改属性的值。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true。
[[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为undefined。对于像前面例子中那样直接在对象上定义的属性,它们[[Configurable]]、[[Enumerable]]和[[Writable]]特性都被设置为 true,而[[Value]]特性被设置为指定的值。
要修改属性默认的特性,必须使用 ECMAScript 5的Object.defineProperty()方法。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符(descriptor)对象的属性必须是:configurable、enumerable、writable和value。设置其中的一或多个值,可以修改对应的特性值。例子见P140
注:一旦把属性定义为不可配置的,就不能再把它变回可配置了。可以多次调用Object.defineProperty()方法修改同一个属性,但在把configurable特性设置为 false 之后就会有限制了

1.1.2访问器属性

访问器属性不包含数据值;它们包含一对儿 getter 和 setter 函数。
访问器属性有如下 4 个特性。

caizhendi commented 4 years ago

2020.04.02

阅读进度:第六章 6.2.3

1.创建对象

1.1工厂模式

工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程。见P144

function createPerson(name, age, job){
 var o = new Object();
 o.name = name;
 o.age = age;
 o.job = job;
 o.sayName = function(){
 alert(this.name);
 };
 return o;
}
var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor"); 

工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

1.2构造函数模式

可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。例子见P145

function Person(name, age, job){
 this.name = name;
 this.age = age;
 this.job = job;
 this.sayName = function(){
 alert(this.name);
 };
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。这个做法借鉴自其他 OO 语言,主要是为了区别于ECMAScript中的其他函数;因为构造函数本身也是函数,只不过可以用来创建对象而已。
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。

1.2.1将构造函数当作函数

构造函数与其他函数的唯一区别,就在于调用它们的方式不同。任何函数,只要通过new操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过 new 操作符来调用,那它跟普通函数也不会有什么两样。例子见P146

1.2.2构造函数的问题

构造函数模式虽然好用,但也并非没有缺点。使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。通过把函数定义转移到构造函数外部来解决这个问题。

function Person(name, age, job){
 this.name = name;
 this.age = age;
 this.job = job;
 this.sayName = sayName;
}
function sayName(){
 alert(this.name);
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor"); 

但又存在:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数。

caizhendi commented 4 years ago

2020.04.04

阅读进度:第六章 6.2.3.2 (原型与 in 操作符)P152

1.原型模式

我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。例子见P147
与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说, person1和person2 访问的都是同一组属性和同一个sayName()函数。要理解原型模式的工作原理,必须先理解ECMAScript中原型对象的性质。

1.1 理解原型对象

只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性包含一个指向 prototype属性所在函数的指针。就拿前面的例子来说, Person.prototype. constructor 指向 Person。而通过这个构造函数,我们还可继续为原型对象添加其他属性和方法。图见P148
要格外注意的是,虽然这两个实例都不包含属性和方法,但我们却可以调用person1.sayName()。这是通过查找对象属性的过程来实现的。
虽然在所有实现中都无法访问到[[Prototype]],但可以通过 isPrototypeOf()方法来确定对象之间是否存在这种关系。从本质上讲,如果[[Prototype]]指向调用 isPrototypeOf()方法的对象(Person.prototype),那么这个方法就返回 true,如下所示:

alert(Person.prototype.isPrototypeOf(person1)); //true 
alert(Person.prototype.isPrototypeOf(person2)); //true 
ECMAScript 5 增加了一个新方法,叫 Object.getPrototypeOf(),在所有支持的实现中,这个方法返回[[Prototype]]的值。
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Nicholas" 

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。也就是说,在我们调用person1.sayName()的时候,会先后执行两次搜索。首先,解析器会问:“实例 person1有sayName属性吗?”答:“没有。”然后,它继续搜索,再问:“person1 的原型有 sayName 属性吗?”答:“有。”于是,它就读取那个保存在原型对象中的函数。当我们调用person2.sayName()时,将会重现相同的搜索过程,得到相同的结果。而这正是多个对象实例共享原型所保存的属性和方法的基本原理。
虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。例子见P149底
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。不过,使用 delete操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性,见P150

delete person1.name; alert(person1.name); 

使用 hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(不要忘了它是从 Object继承来的)只在给定属性存在于对象实例中时,才会返回 true。
P151图展示了上面例子在不同情况下的实现与原型的关系(为了简单起见,图中省略了与Person构造函数的关系)。

hzjjg commented 4 years ago

牛逼

Get Outlook for Androidhttps://aka.ms/ghei36


From: caizhendi notifications@github.com Sent: Saturday, April 4, 2020 5:42:50 PM To: caizhendi/blog blog@noreply.github.com Cc: Subscribed subscribed@noreply.github.com Subject: Re: [caizhendi/blog] 《javascript高级程序设计(第三版)》(2) (#2)

2020.04.04 阅读进度:第六章 6.2.3.2 (原型与 in 操作符)P152 1.原型模式 我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。例子见P147 与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说, person1 和 person2 访问的都是同一组属性和同一个 sayName()函数。要理解原型模式的工作原理,必须先理解 ECMAScript 中原型对象的性质。 1.1 理解原型对象 只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor (构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。就拿前面的例子来说, Person.prototype. constructor 指向 Person。而通过这个构造函数,我们还可继续为原型对象添加其他属性和方法。图见P148 要格外注意的是,虽然这两个实例都不包含属性和方法,但我们却可以调用 person1.sayName()。这是通过查找对象属性的过程来实现的。 虽然在所有实现中都无法访问到[[Prototype]],但可以通过 isPrototypeOf()方法来确定对象之间是否存在这种关系。从本质上讲,如果[[Prototype]]指向调用 isPrototypeOf()方法的对象(Person.prototype),那么这个方法就返回 true,如下所示: alert(Person.prototype.isPrototypeOf(person1)); //true alert(Person.prototype.isPrototypeOf(person2)); //true ECMAScript 5 增加了一个新方法,叫 Object.getPrototypeOf(),在所有支持的实现中,这个方法返回[[Prototype]]的值。 alert(Object.getPrototypeOf(person1) == Person.prototype); //true alert(Object.getPrototypeOf(person1).name); //"Nicholas" 每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。也就是说,在我们调用 person1.sayName()的时候,会先后执行两次搜索。首先,解析器会问:“实例 person1 有 sayName 属性吗?”答:“没有。”然后,它继续搜索,再问:“person1 的原型有 sayName 属性吗?”答:“有。”于是,它就读取那个保存在原型对象中的函数。当我们调用 person2.sayName()时,将会重现相同的搜索过程,得到相同的结果。而这正是多个对象实例共享原型所保存的属性和方法的基本原理。 虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。例子见P149底 当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。即使将这个属性设置为 null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。不过,使用 delete 操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性,见P150 delete person1.name; alert(person1.name); 使用 hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(不要忘了它是从 Object 继承来的)只在给定属性存在于对象实例中时,才会返回 true。 P151图展示了上面例子在不同情况下的实现与原型的关系(为了简单起见,图中省略了与 Person 构造函数的关系)。

― You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHubhttps://nam11.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Fcaizhendi%2Fblog%2Fissues%2F2%23issuecomment-609003908&data=02%7C01%7C%7Cecd81a72744548ed71f108d7d87c8b19%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637215901724282214&sdata=dm30s%2BQJYxZvKdiq2rOQYPm6JZVtM%2F%2B3YZXPYdyYXG0%3D&reserved=0, or unsubscribehttps://nam11.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Fnotifications%2Funsubscribe-auth%2FADNAJT2CRH2SPWEEYBP7DA3RK36JVANCNFSM4LY6BL3Q&data=02%7C01%7C%7Cecd81a72744548ed71f108d7d87c8b19%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C637215901724282214&sdata=YKz7uRVH%2F2fQvAYPi0q7gnZbsNRCIu8v64%2BGXSYD7DY%3D&reserved=0.

caizhendi commented 4 years ago

2020.04.06

阅读进度:第六章 6.2.4 P159

1.原型与 in 操作符

有两种方式使用 in 操作符:单独使用和在 for-in 循环中使用。在单独使用时,in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。例子见P152
同时使用 hasOwnProperty()方法和in操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中,由于 in 操作符只要通过对象能够访问到属性就返回 true,hasOwnProperty()只在属性存在于实例中时才返回 true,因此只要 in 操作符返回 true 而 hasOwnProperty()返回false,就可以确定属性是原型中的属性。例子见P152底,P153顶
在使用 for-in循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性(即将[[Enumerable]]标记为 false 的属性)的实例属性也会在for-in循环中返回,因为根据规定,所有开发人员定义的属性都是可枚举的——只有在 IE8 及更早版本中例外。 IE 早期版本的实现中存在一个bug,即屏蔽不可枚举属性的实例属性不会出现在 for-in 循环中。例子见P153
兼容性:在 IE中,由于其实现认为原型的toString()方法被打上了值为false的[[Enumerable]]标记,因此应该跳过 该属性,结果我们就不会看到警告框。该 bug 会影响默认不可枚举的所有属性和方法,包括: hasOwnProperty()、propertyIsEnumerable()、toLocaleString()、toString()和 valueOf()。ECMAScript 5 也将 constructor和prototype属性的[[Enumerable]]特性设置为false,但并不是所有浏览器都照此实现。
要取得对象上所有可枚举的实例属性,可以使用 ECMAScript 5 的 Object.keys()方法。这个方法 接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。例子见P154上
如果是通过实例调用,则Object.keys()返回的数组只包含"name"和"age"这两个实例属性。 如果你想要得到所有实例属性,无论它是否可枚举,都可以使用 Object.getOwnPropertyNames()方法。例子见P154 注意结果中包含了不可枚举的 constructor 属性。Object.keys()和Object.getOwnPropertyNames()方法都可以用来替代for-in循环。支持这两个方法的浏览器有 IE9+、Firefox 4+、Safari 5+、Opera 12+和 Chrome。

2.更简单的原型语法

读者大概注意到了,前面例子中每添加一个属性和方法就要敲一遍Person.prototype。为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象,如下面的例子所示。

function Person(){
}
Person.prototype = {
 name : "Nicholas",
 age : 29,
 job: "Software Engineer",
 sayName : function () {
 alert(this.name);
 }
};

在上面的代码中,我们将 Person.prototype 设置为等于一个以对象字面量形式创建的新对象。 最终结果相同,但有一个例外:constructor属性不再指向 Person 了。前面曾经介绍过,每创建一 个函数,就会同时创建它的prototype对象,这个对象也会自动获得 constructor 属性。而我们在 这里使用的语法,本质上完全重写了默认的 prototype 对象,因此constructor属性也就变成了新 对象的 constructor 属性(指向 Object 构造函数),不再指向 Person 函数。此时,尽管 instanceof 操作符还能返回正确的结果,但通过constructor 已经无法确定对象的类型了。例子见P155
在此,用 instanceof 操作符测试 Object 和 Person 仍然返回 true,但 constructor 属性则 等于 Object 而不等于 Person 了。如果 constructor 的值真的很重要,可以像下面这样特意将它设 置回适当的值。

function Person(){
}
Person.prototype = {
 constructor : Person,
 name : "Nicholas",
 age : 29,
 job: "Software Engineer",
 sayName : function () {
 alert(this.name);
 }
};

注意,以这种方式重设constructor属性会导致它的[[Enumerable]]特性被设置为true。默认情况下,原生的constructor属性是不可枚举的,因此如果你使用兼容 ECMAScript 5 的 JavaScript 引 擎,可以试一试 Object.defineProperty()。

//重设构造函数,只适用于 ECMAScript 5 兼容的浏览器
Object.defineProperty(Person.prototype, "constructor", {
 enumerable: false,
 value: Person
});

3.原型的动态性

由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此。

var person = new Person();
Person.prototype.sayHi = function(){
 alert("hi");
};
person.sayHi(); //"hi"(没有问题!)

以上代码先创建了 Person的一个实例,并将其保存在 person 中。然后,下一条语句在 Person. prototype 中添加了一个方法sayHi()。即使person实例是在添加新方法之前创建的,但它仍然可以访问这个新方法。其原因可以归结为实例与原型之间的松散连接关系。当我们调用 person.sayHi()时,首先会在实例中搜索名为 sayHi 的属性,在没找到的情况下,会继续搜索原型。因为实例与原型之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到新的 sayHi 属性并返回保存在那里的函数。尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。 请记住:实例中的指针仅指向原型,而不指向构造函数。例子见P156 (有点不理解 需加强)

4.原生对象的原型

原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。
通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。可以像修改自定义对象的原型一样修改原生对象的原型,因此可以随时添加方法。
注意:尽管可以这样做,但我们不推荐在产品化的程序中修改原生对象的原型。如果因某个实现中缺少某个方法,就在原生对象的原型中添加这个方法,那么当在另一个支持该方法的实现中运行代码时,就可能会导致命名冲突。而且,这样做也可能会意外地重写原生方法。

5.原型对象的问题

原型模式缺点:它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。原型模式的最大问题是由其共享的本性所导致的。例子见P158

caizhendi commented 4 years ago

2020.04.07

阅读进度:第六章 6.3 P162

1.造函数模式和原型模式

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。例子见P159

function Person(name, age, job){
 this.name = name;
 this.age = age;
 this.job = job;
 this.friends = ["Shelby", "Court"];
}
Person.prototype = {
 constructor : Person,
 sayName : function(){
 alert(this.name);
 }
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true

这种构造函数与原型混成的模式,是目前在ECMAScript 中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。

2. 动态原型模式

有其他 OO 语言经验的开发人员在看到独立的构造函数和原型时,很可能会感到非常困惑。动态原型模式正是致力于解决这个问题的一个方案,它把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。例子见P159底

3. 寄生构造函数模式

这种模式 的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但 从表面上看,这个函数又很像是典型的构造函数。例子见P160 除了使用 new操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。 这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改Array构造函数,因此可以使用这个模式。例子见P161
关于寄生构造函数模式,有一点需要说明:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此, 不能依赖 instanceof 操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情况下,不要使用这种模式。

4. 稳妥构造函数模式

所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用this和new),或者在防止数据被其他应用程序(如Mashup程序)改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的 实例方法不引用 this;二是不使用new操作符调用构造函数。按照稳妥构造函数的要求,可以将前面的Person构造函数重写如下。例子见P161底

caizhendi commented 4 years ago

2020.04.08

阅读进度:第六章 6.3.3 P168

1.继承

继承是 OO 语言中的一个最为人津津乐道的概念。许多 OO 语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。ECMAScript只支持实现继承,而且其实现继承主要是依靠原型链 来实现的。

1.1原型链

ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。 实现原型链有一种基本模式,例子见P163顶 详解见163

1.1.1 别忘记默认的原型

所有函数的默认原型都是 Object的实例,因此默认原 型都会包含一个内部指针,指向 Object.prototype。

1.1.2 确定原型和实例的关系

可以通过两种方式来确定原型和实例之间的关系。第一种方式是使用instanceof操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回 true。例子见P165 object instanceof constructor
第二种方式是使用isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此isPrototypeOf()方法也会返回 true。例子见P165

1.1.3 谨慎地定义方法

给原型添加方法的代码一定要放在替换原型的语句之后。

function SuperType(){
 this.property = true;
}
SuperType.prototype.getSuperValue = function(){
 return this.property;
};
function SubType(){
 this.subproperty = false;
}
//继承了 SuperType
SubType.prototype = new SuperType();
//添加新方法
SubType.prototype.getSubValue = function (){
 return this.subproperty;
};
//重写超类型中的方法
SubType.prototype.getSuperValue = function (){
 return false;
};
var instance = new SubType();
alert(instance.getSuperValue()); //false

还有一点需要提醒读者,即在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这 样做就会重写原型链。例子见P166

function SuperType(){
 this.property = true;
}
SuperType.prototype.getSuperValue = function(){
 return this.property;
};
function SubType(){
 this.subproperty = false;
}
//继承了 SuperType
SubType.prototype = new SuperType();
//使用字面量添加新方法,会导致上一行代码无效
SubType.prototype = {
 getSubValue : function (){
 return this.subproperty;
 },
 someOtherMethod : function (){
 return false;
 }
};
var instance = new SubType();
alert(instance.getSuperValue()); //error!

以上代码展示了刚刚把SuperType的实例赋值给原型,紧接着又将原型替换成一个对象字面量而导致的问题。由于现在的原型包含的是一个 Object的实例,而非 SuperType 的实例,因此我们设想中的原型链已经被切断——SubType 和 SuperType 之间已经没有关系了。

1.1.4原型链的问题

最主要的问题来自包含引用类型值的原型。我们前面介绍过包含引用类型值的原型属性会被所有实例共享;而 这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。在通过原型来实现继承时,原 型实际上会变成另一个类型的实例。例子见P167
原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。

2.借用构造函数

在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫做借用构造函数(constructor stealing)的技术(有时候也叫做伪造对象或经典继承)。这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。。别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用 apply()和call()方法也可以在(将来)新创建的对象上执行构造函数。例子见P167底

function SuperType(){
 this.colors = ["red", "blue", "green"];
}
function SubType(){
 //继承了 SuperType
 SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green"

代码中加注释的那一行代码“借调”了超类型的构造函数。通过使用call()方法(或apply()方法也可以),我们实际上是在(未来将要)新创建的 SubType 实例的环境下调用了SuperType构造函数。这样一来,就会在新SubType对象上执行SuperType()函数中定义的所有对象初始化代码。结果,SubType的每个实例就都会具有自己的 colors属性的副本了。

2.1 传递参数

相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。

2.2 借用构造函数的问题

如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。考虑到这些问题,借用构造函数的技术也是很少单独使用的。

caizhendi commented 4 years ago

2020.04.09

阅读进度:第七章 175

1.组合继承

有时候也叫做伪经典继承,指的是将原型链和借用构造函数的 技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方 法的继承,而通过借用构造函数来实现对实例属性的继承。例子见P168底

function SuperType(name){
 this.name = name;
 this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
 alert(this.name);
};
function SubType(name, age){
 //继承属性
 SuperType.call(this, name);

 this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
 alert(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript中最常用的继承模式。而且,instanceof和isPrototypeOf()也能够用于识别基于组合继承创建的对象。

2.原型式继承

他的想法是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。为了达到这个目的,他给出了如下函数。

function object(o){
 function F(){}
 F.prototype = o;
 return new F();
}

在 object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。例子见P170
ECMAScript 5通过新增Object.create()方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()与 object()方法的行为相同。例子见P170
Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属 性。例子见P171

3.寄生式继承

寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。以下代码示范了寄生式继承模式。

function createAnother(original){
 var clone = object(original); //通过调用函数创建一个新对象
 clone.sayHi = function(){ //以某种方式来增强这个对象
 alert("hi");
 };
 return clone; //返回这个对象
}

在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。前面示范继承模式时使用的object()函数不是必需的;任何能够返回新对象的函数都适用于此模式。

4.寄生组合式继承

组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子 类型构造函数时重写这些属性。例子见P172
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式继承的基本模式如下所示。

function inheritPrototype(subType, superType){
 var prototype = object(superType.prototype); //创建对象
 prototype.constructor = subType; //增强对象
 subType.prototype = prototype; //指定对象
}

5.小结

在没有类的情况下,可以采用下列模式创建对象。

caizhendi commented 4 years ago

2020.04.17

阅读进度:第七章 7.3 P184

1、函数表达式

首先是 function关键字,然后是函数的名字,这就是指定函数名的方式。
关于函数声明,它的一个重要特征就是函数声明提升,意思是在执行代码之前会先读取函数声明。这就意味着可以把函数声明放在调用它的语句后面。
第二种创建函数的方式是使用函数表达式。

var functionName = function(arg0, arg1, arg2){
 //函数体
};

这种形式看起来好像是常规的变量赋值语句,即创建一个函数并将它赋值给变量functionName。这种情况下创建的函数叫做匿名函数因为function关键字后面没有标识符。匿名函数的 name 属性是空字符串。

1.1 递归

递归函数是在一个函数通过名字调用自身的情况下构成的。
例子见P177 arguments.callee是一个指向正在执行的函数的指针,因此可以用它来实现对函数 的递归调用。

function factorial(num){
 if (num <= 1){
 return 1;
 } else {
 return num * arguments.callee(num-1);
 }
}

因此,在编写递归函数时,使用 arguments.callee 总比使用函数名更保险。但在严格模式下,不能通过脚本访问 arguments.callee,访问这个属性会导致错误。不过,可以使用命名函数表达式来达成相同的结果。见P178

1.2闭包

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。例子见P178
。当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链。 然后,使用 arguments 和其他命名参数的值来初始化函数的活动对象(activation object)。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,……直至作为作用域链终点的全局执行环境。例子见P179
作用域链本质上是一个指向变量对象的指针列表,它只 引用但不实际包含变量对象。
P180 重点理解

//创建函数
var compareNames = createComparisonFunction("name");
//调用函数
var result = compareNames({ name: "Nicholas" }, { name: "Greg" });
//解除对匿名函数的引用(以便释放内存)
compareNames = null;

建议:由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,我们建议读者只在绝对必要时再考虑使用闭包。

1.2.1 闭包与变量

function createFunctions(){
 var result = new Array();
 for (var i=0; i < 10; i++){
 result[i] = function(){
 return i;
 };
 }
 return result;
}

因为每个函数的作用域链中都保存着createFunctions()函数的活动对象,所以它们引用的都是同一个变量 i 。 当 createFunctions()函数返回后,变量 i 的值是 10,此时每个函数都引用着保存变量 i 的同一个变量 对象,所以在每个函数内部 i 的值都是 10。但是,我们可以通过创建另一个匿名函数强制让闭包的行为 符合预期,如下所示。

function createFunctions(){
 var result = new Array();
 for (var i=0; i < 10; i++){
 result[i] = function(num){
 return function(){
 return num;
 };
 }(i);
 }
 return result;
}

1.2.2关于this对象

我们知道,this 对象是在运行时基于函数的执 行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性,因此其 this 对象通常指向window。当然,在通过call()或 apply()改变函数执行环境的情况下,this就会指向其他对象。
每个函数在被调用时都会自动取得两个特殊变量:this 和 arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。
注意:this 和arguments也存在同样的问题。如果想访问作用域中的arguments对象,必须将对该对象的引用保存到另一个闭包能够访问的变量中。
在几种特殊情况下,this 的值可能会意外地改变。

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

这里的 getName()方法只简单地返回 this.name 的值。以下是几种调用object.getName()的方式以及各自的结果。

object.getName(); //"My Object"
(object.getName)(); //"My Object"
(object.getName = object.getName)(); //"The Window",在非严格模式下

虽然加上括号之后,就好像只是在引用一个函数,但 this 的值得到了维持,因为 object.getName 和(object.getName)的定义是相同的。第三行代码先执行了一条赋值语句,然后再调用赋值后的结果。因为这个赋值表达式的值是 函数本身,所以 this 的值不能得到维持,结果就返回了"The Window"。

1.2.3内存泄露

具体来说,如果闭包的作用域链中保存着一个 HTML 元素,那么就意味着该元素将无法被销毁。来看下面的例子。

function assignHandler(){
 var element = document.getElementById("someElement");
 element.onclick = function(){
 alert(element.id);
 };
}

以上代码创建了一个作为element元素事件处理程序的闭包,而这个闭包则又创建了一个循环引 用(事件将在第13章讨论)。由于匿名函数保存了一个对 assignHandler()的活动对象的引用,因此 就会导致无法减少element的引用数。只要匿名函数存在,element 的引用数至少也是 1,因此它所 占用的内存就永远不会被回收。不过,这个问题可以通过稍微改写一下代码来解决,如下所示。

function assignHandler(){
 var element = document.getElementById("someElement");
 var id = element.id;

 element.onclick = function(){
 alert(id);
 };

 element = null;
}

通过把 element.id的一个副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用。但仅仅做到这一步,还是不能解决内存泄漏的问题。必须要记住:闭包会引用包含函数的整个活动对象,而其中包含着 element。即使闭包不直接引用element,包含函数的活动对象中也仍然会保存一个引用。因此,有必要把 element变量设置为null。这样就能够解除对 DOM 对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。

caizhendi commented 4 years ago

2020.04.19

阅读进度: 第八章 P193

1.模块模式

模块模式是为单例创建私有变量和特权方法。所谓单例指的就是只有一个实例的对象。按照惯例,JavaScript 是以对象字面量的方式来创建单例对象的。
模块模式通过为单例添加私有变量和特权方法能够使其得到增强,其语法形式如下:

var singleton = function(){
 //私有变量和私有函数
 var privateVariable = 10;
 function privateFunction(){
 return false;
 }
//特权/公有方法和属性
 return {
 publicProperty: true,
 publicMethod : function(){
 privateVariable++;
 return privateFunction();
 }
 };
}();

这个模块模式使用了一个返回对象的匿名函数。在这个匿名函数内部,首先定义了私有变量和函数。然后,将一个对象字面量作为函数的值返回。返回的对象字面量中只包含可以公开的属性和方法。由于这个对象是在匿名函数内部定义的,因此它的公有方法有权访问私有变量和函数。从本质上来讲,这个对象字面量定义的是单例的公共接口。这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的。例子见P190

2.增强的模块模式

进一步改进了模块模式,即在返回对象之前加入对其增强的代码。这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况。例子见P191

3.小结

函数表达式不同于函数声明。函数声明要求有名字,但函数表达式不需要。没有名字的函数表达式也叫做匿名函数。 递归函数应该始终使用arguments.callee来递归地调用自身,不要使用函数名——函数名可 能会发生变化。
当在函数内部定义了其他函数时,就创建了闭包。闭包有权访问包含函数内部的所有变量,原理如下:
在后台执行环境中,闭包的作用域链包含着它自己的作用域、包含函数的作用域和全局作用域。通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。 但是,当函数返回了一个闭包时,这个函数的作用域将会一直在内存中保存到闭包不存在为止。使用闭包可以在 JavaScript 中模仿块级作用域(JavaScript 本身没有块级作用域的概念)。要点如下:
创建并立即调用一个函数,这样既可以执行其中的代码,又不会在内存中留下对该函数的引用。结果就是函数内部的所有变量都会被立即销毁——除非将某些变量赋值给了包含作用域(即外 部作用域)中的变量。 闭包还可以用于在对象中创建私有变量,可以使用闭包来实现公有方法,而通过公有方法可以访问在包含作用域中定义的变量。有权访问私有变量的公有方法叫做特权方法。

caizhendi commented 4 years ago

2020.04.18

阅读进度:第八章 P192

1.模仿块级作用域

JavaScript 没有块级作用域的概念。这意味着在块语句中定义的变量,实际上是在包含函数中而非语句中创建的。

function outputNumbers(count){
 for (var i=0; i < count; i++){
 alert(i);
 }
 alert(i); //计数
}

这个函数中定义了一个for循环,而变量i的初始值被设置为 0。在 Java、C++等语言中,变量 i 只会在 for 循环的语句块中有定义,循环一旦结束,变量 i 就会被销毁。可是在 JavaScrip 中,变量 i是定义在 ouputNumbers()的活动对象中的,因此从它有定义开始,就可以在函数内部随处访问它。即使像下面这样错误地重新声明同一个变量,也不会改变它的值。

function outputNumbers(count){
 for (var i=0; i < count; i++){
 alert(i);
 }

 var i; //重新声明变量
 alert(i); //计数
}

JavaScript 从来不会告诉你是否多次声明了同一个变量;遇到这种情况,它只会对后续的声明视而不见。匿名函数可以用来模仿块级作用域并避免这个问题。 用作块级作用域(通常称为私有作用域)的匿名函数的语法如下所示。

(function(){
 //这里是块级作用域
})();

将函数声明包含在一对圆括号中,表示它实际上是一个 函数表达式。而紧随其后的另一对圆括号会立即调用这个函数。

function(){
 //这里是块级作用域
}(); //出错!

JavaScript 将function关键字当作一个函数声明的开始,而函 数声明后面不能跟圆括号。然而,函数表达式的后面可以跟圆括号。要将函数声明转换成函数表达式, 只要像下面这样给它加上一对圆括号即可。

(function(){
 //这里是块级作用域
})();

无论在什么地方,只要临时需要一些变量,就可以使用私有作用域,例如:

function outputNumbers(count){
 (function () {
 for (var i=0; i < count; i++){
 alert(i);
 }
 })();
 alert(i); //导致一个错误!
}

在匿名函数中定义的任何变量,都会在执行结束时被销毁。
这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。一般来说,我们都应该尽量少向全局作用域中添加变量和函数。这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。

2.私有变量

JavaScript 中没有私有成员的概念;所有对象属性都是公有的。有一个私有变量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。

function add(num1, num2){
 var sum = num1 + num2;
 return sum;
}

在这个函数内部,有 3 个私有变量:num1、num2 和 sum。在函数内部可以访问这几个变量,但在函数外部则不能访问它们。如果在这个函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量。而利用这一点,就可以创建用于访问私有变量的公有方法。
我们把有权访问私有变量和私有函数的公有方法称为特权方法(privileged method)。有两种在对象 上创建特权方法的方式。第一种是在构造函数中定义特权方法,基本模式如下。

function MyObject(){
 //私有变量和私有函数
 var privateVariable = 10;
 function privateFunction(){
 return false;
 }
 //特权方法
 this.publicMethod = function (){
 privateVariable++;
 return privateFunction();
 };
}

这个模式在构造函数内部定义了所有私有变量和函数。然后,又继续创建了能够访问这些私有成员的特权方法。能够在构造函数中定义特权方法,是因为特权方法作为闭包有权访问在构造函数中定义的所有变量和函数。
利用私有和特权成员,可以隐藏那些不应该被直接修改的数据,例子见P187
不过,在构造函数中定义特权方法也有一个缺点,那就是你必须使用构造函数模式来达到这个目的。

2.1静态私有变量

通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法,其基本模式如下所示。例子见p188

(function(){

 //私有变量和私有函数
 var privateVariable = 10;
 function privateFunction(){
 return false;
 }
 //构造函数
 MyObject = function(){
 };
 //公有/特权方法
 MyObject.prototype.publicMethod = function(){
 privateVariable++;
 return privateFunction();
 };
})();

这个模式创建了一个私有作用域,并在其中封装了一个构造函数及相应的方法。 需要注意的是,这个模式在定义构造函数时并没有使用函数声明,而是使用了函数表达式。函数声明只能创建局部函数,但那并不是我们想要的。出于同样的原因,我们也没 有在声明 MyObject 时使用 var 关键字。记住:初始化未经声明的变量,总是会创建一个全局变量。因此,MyObject就成了一个全局变量,能够在私有作用域之外被访问到。但也要知道,在严格模式下 给未经声明的变量赋值会导致错误。
这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用。

caizhendi commented 4 years ago

2020.04.20

阅读进度: 8.1.5 P199

1.window 对象

BOM 的核心对象是window,它表示浏览器的一个实例。它既是通过JavaScript访问浏览器窗口的一个接口,又是 ECMAScript 规定的 Global 对象。

1.1.1全局作用域

所有在全局作用域中声明 的变量、函数都会变成 window 对象的属性和方法。例子见p193
定义全局变量与在 window 对象上直接定义属性还 是有一点差别:全局变量不能通过delete操作符删除,而直接在 window对象上的定义的属性可以。兼容:IE8及更早版本在遇到使用delete删除window属性的语句时,不管该属性最初是如何创建的,都会抛出错误,以示警告。IE9及更高版本不会抛出错误。
尝试访问未声明的变量会抛出错误,但是通过查询 window 对象,可以知道某个可能未声明的变量是否存在。

1.1.2窗口关系及框架

如果页面中包含框架,则每个框架都拥有自己的window 对象,并且保存在frames集合中。在frames集合中,可以通过数值索引(从0开始,从左至右,从上到下)或者框架名称来访问相应的 window 对 象。每个 window 对象都有一个name属性,其中包含框架的名称。例子见P195
最好使用 top而非window来引用这些框架(例如,通过 top.frames[0])。top对象始终指向最高(最外)层的框架,也就是浏览器窗口。
与 top 相对的另一个window对象是parent。parent(父)对象始终指向当前框架的直接上层框架。在某些情况下,parent有可能等于top;但在没有框架的情况下,parent 一定等于top(此时它们都等于window)。例子见p196
注意,除非最高层窗口是通过window.open()打开的,否则其 window对象的name属性不会包含任何值。与框架有关的最后一个对象是self,它始终指向window;实际上,self 和 window 对象可以互 换使用。引入 self 对象的目的只是为了与top和parent对象对应起来,因此它不格外包含其他值。
所有这些对象都是 window 对象的属性,可以通过 window.parent、window.top等形式来访问。同时,这也意味着可以将不同层次的window对象连缀起来,例如window.parent.parent.frames[0]。

1.1.3窗口位置

用来确定和修改window对象位置的属性和方法有很多。
IE、Safari、Opera 和 Chrome 都提供了 screenLeft 和 screenTop属性,分别用于表示窗口相对于屏幕左边和上边的位置。Firefox 则在 screenX 和 screenY 属性中提供相同的窗口位置信息,Safari 和 Chrome 也同时支持这两个属性。
使用下列代码可以跨浏览器取得窗口左边和上边的位置。

var leftPos = (typeof window.screenLeft == "number") ?
 window.screenLeft : window.screenX;
var topPos = (typeof window.screenTop == "number") ?
 window.screenTop : window.screenY;

在 IE、Opera 中,screenLeft 和 screenTop 中保存 的是从屏幕左边和上边到由window对象表示的页面可见区域的距离。换句话说,如果 window 对象是 最外层对象,而且浏览器窗口紧贴屏幕最上端——即 y 轴坐标为 0,那么 screenTop 的值就是位于页面 可见区域上方的浏览器工具栏的像素高度。但是,在 Chrome、Firefox和 Safari中,screenY 或screenTop 中保存的是整个浏览器窗口相对于屏幕的坐标值,即在窗口的 y 轴坐标为 0 时返回 0。
最终结果,就是无法在跨浏览器的条件下取得窗口左边和上边的精确坐标值。然而,使用 moveTo() 和 moveBy()方法倒是有可能将窗口精确地移动到一个新位置。这两个方法都接收两个参数,其中moveTo()接收的是新位置的x和y坐标值,而moveBy()接收的是在水平和垂直方向上移动的像素数。例子见p198
注意:这两个方法都不适用于框架,只能对最外层的 window 对象使用。

1.1.4窗口大小

跨浏览器确定一个窗口的大小不是一件简单的事。IE9+、Firefox、Safari、Opera 和 Chrome 均为此提 供了 4 个属性:innerWidth、innerHeight、outerWidth 和 outerHeight。
跨浏览器确定一个窗口的大小不是一件简单的事。IE9+、Firefox、Safari、Opera 和 Chrome 均为此提 供了 4 个属性:innerWidth、innerHeight、outerWidth 和 outerHeight。在 IE9+、Safari 和 Firefox 中,outerWidth和outerHeight返回浏览器窗口本身的尺寸(无论是从最外层的 window 对象还是从 某个框架访问)。在Opera中,这两个属性的值表示页面视图容器①的大小。而 innerWidth 和 innerHeight 则表示该容器中页面视图区的大小(减去边框宽度)。在 Chrome 中,outerWidth、outerHeight 与 innerWidth、innerHeight返回相同的值,即视口(viewport)大小而非浏览器窗口大小。
在 IE、Firefox、Safari、Opera 和 Chrome 中,document.documentElement.clientWidth 和 document.documentElement.clientHeight中保存了页面视口的信息。IE6中,这些属性必须在标准模式下才有效;如果是混杂模式,就必须通过 document.body.clientWidth 和 document.body. clientHeight 取得相同信息。而对于混杂模式下的 Chrome,则无论通过 document.documentElement 还是 document.body 中的 clientWidth 和 clientHeight 属性,都可以取得视口的大小。 虽然最终无法确定浏览器窗口本身的大小,但却可以取得页面视口的大小,如下所示。

var pageWidth = window.innerWidth,
 pageHeight = window.innerHeight;

if (typeof pageWidth != "number"){
 if (document.compatMode == "CSS1Compat"){
 pageWidth = document.documentElement.clientWidth;
 pageHeight = document.documentElement.clientHeight;
 } else {
 pageWidth = document.body.clientWidth;
 pageHeight = document.body.clientHeight;
 }
}

对于移动设备,window.innerWidth和window.innerHeight 保存着可见视口,也就是屏幕上可见页面区域的大小。移动 IE 浏览器不支持这些属性,但通过 document.documentElement.clientWidth 和 document.documentElement.clientHeihgt 提供了相同的信息。随着页面的缩放,这些值 也会相应变化。 在其他移动浏览器中,document.documentElement 度量的是布局视口,即渲染后页面的实际大小(与可见视口不同,可见视口只是整个页面中的一小部分)。移动 IE 浏览器把布局视口的信息保存在document.body.clientWidth和document.body.clientHeight中。这些值不会随着页面缩放变化。
使用 resizeTo()和resizeBy()方法可以调整浏览器窗口的大小。这两个方法都接收两个参数,其中resizeTo()接收浏览器窗口的新宽度和新高度,而resizeBy()接收新窗口与原窗口的宽 度和高度之差。例子见p199 需要注意的是,这两个方法与移动窗口位置的方法类似,也有可能被浏览器禁用;而且,在Opera和IE7(及更高版本)中默认就是禁用的。另外,这两个方法同样不适用于框架,而只能对最外层的window对象使用。

caizhendi commented 4 years ago

2020.04.21

阅读进度: 8.2 P207

1. 导航和打开窗口

使用 window.open()方法既可以导航到一个特定的 URL,也可以打开一个新的浏览器窗口。这个方法可以接收 4 个参数:要加载的URL、窗口目标、一个特性字符串以及一个表示新页面是否取代浏览器历史记录中当前加载页面的布尔值。通常只须传递第一个参数,最后一个参数只在不打开新窗口的情况下使用。)传递了第二个参数,而且该参数是已有窗口或框架的名称,那么就会在具有该名称的窗口或框架中加载第一个参数指定的 URL。例子见200
第二个参数也可以是下列任何一个特殊的窗口名 称:_self、_parent、_top 或_blank。

1.1弹出窗口

如果给 window.open()传递的第二个参数并不是一个已经存在的窗口或框架,那么该方法就会根据在第三个参数位置上传入的字符串创建一个新窗口或新标签页。如果没有传入第三个参数,那么就会打开一个带有全部默认设置(工具栏、地址栏和状态栏等)的新浏览器窗口(或者打开一个新标签页——根据浏览器设置)。第三个参数是一个逗号分隔的设置字符串,表示在新窗口中都显示哪些特性。设置选项见p200
可以通过逗号分隔的名值对列表来指定。其中,名值对以等号 表示(注意,整个特性字符串中不允许出现空格)。

window.open("http://www.wrox.com/","wroxWindow", "height=400,width=400,top=10,left=10,resizable=yes");

window.open()方法会返回一个指向新窗口的引用。引用的对象与其他window对象大致相似,但我们可以对其进行更多控制。例如,有些浏览器在默认情况下可能不允许我们针对主浏览器窗口调整大小或移动位置,但却允许我们针对通过window.open()创建的窗口调整大小或移动位置。通过这个返回的对象,可以像操作其他窗口一样操作新打开的窗口。

var wroxWin = window.open("http://www.wrox.com/","wroxWindow",
 "height=400,width=400,top=10,left=10,resizable=yes");
//调整大小
wroxWin.resizeTo(500,500);
//移动位置
wroxWin.moveTo(100,100);
调用 close()方法还可以关闭新打开的窗口。
wroxWin.close();

这个方法仅适用于通过window.open()打开的弹出窗口。对于浏览器的主窗口,如果没有得到用户的允许是不能关闭它的。不过,弹出窗口倒是可以调用top.close()在不经用户允许的情况 下关闭自己。
新创建的 window对象有一个opener属性,其中保存着打开它的原始窗口对象。这个属性只在弹出窗口中的最外层 window 对象(top)中有定义,而且指向调用 window.open()的窗口或框架。

var wroxWin = window.open("http://www.wrox.com/","wroxWindow",
 "height=400,width=400,top=10,left=10,resizable=yes");
alert(wroxWin.opener == window); //true

在 Chrome 中,将新创建的标签页的opener属性设置为 null,即表示在单独的进程中运行新标签页,将 opener 属性设置为null就是告诉浏览器新创建的标签页不需要与打开它的标签页通信,因此可以在独立的进程中运行。标签页之间的联系一旦切断,将没有办法恢复。

1.2安全限制

对于那些不是用户有意打开的弹出窗口,Chrome采取了不同的处理方式。它不会像其他浏览器那样简单地屏蔽这些弹出窗口,而是只显示它们的标题栏,并把它们放在浏览器窗口的右下角。更多浏览器的限制见p202

1.3弹出窗口屏蔽程序

大多数浏览器都内置有弹出窗口屏蔽程序,于是,在弹出窗口被屏蔽时,就应该考虑两种可能性。如果是浏览器内置的屏蔽程序阻止的弹出窗口,那么window.open()很可能会返回null。此时,只要检测这个返回的值就可以确定弹出窗口是否被屏蔽了。

var wroxWin = window.open("http://www.wrox.com", "_blank");
if (wroxWin == null){
 alert("The popup was blocked!");
}

如果是浏览器扩展或其他程序阻止的弹出窗口,那么 window.open()通常会抛出一个错误。因此,要想准确地检测出弹出窗口是否被屏蔽,必须在检测返回值的同时,将对 window.open()的调用封装在一个try-catch 块中,如下所示。

var blocked = false;
try {
 var wroxWin = window.open("http://www.wrox.com", "_blank");
 if (wroxWin == null){
 blocked = true;
 }
} catch (ex){
 blocked = true;
}
if (blocked){
 alert("The popup was blocked!");
}

在任何情况下,以上代码都可以检测出调用window.open()打开的弹出窗口是不是被屏蔽了。但要注意的是,检测弹出窗口是否被屏蔽只是一方面,它并不会阻止浏览器显示与被屏蔽的弹出窗口有关的消息。

1.4间歇调用和超时调用

JavaScript 是单线程语言,但它允许通过设置超时值和间歇时间值来调度代码在特定的时刻执行。前者是在指定的时间过后执行代码,而后者则是每隔指定的时间就执行一次代码。
超时调用需要使用window对象的setTimeout()方法,它接受两个参数:要执行的代码和以毫秒表示的时间(即在执行代码前需要等待多少毫秒)。其中,第一个参数可以是一个包含JavaScript代码的字符串(就和在 eval()函数中使用的字符串一样),也可以是一个函数。例如,下面对setTimeout()的两次调用都会在一秒钟后显示一个警告框。

//不建议传递字符串!
setTimeout("alert('Hello world!') ", 1000);
//推荐的调用方式
setTimeout(function() {
 alert("Hello world!");
}, 1000);

虽然这两种调用方式都没有问题,但由于传递字符串可能导致性能损失,因此不建议以字符串作为第一个参数。第二个参数是一个表示等待多长时间的毫秒数,但经过该时间后指定的代码不一定会执行。setTimeout()的第二个参数告诉JavaScript再过多长时间把当前任务添加到队列中。如果队列是空的,那么添加的代码会立即 执行;如果队列不是空的,那么它就要等前面的代码执行完了以后再执行。
调用 setTimeout()之后,该方法会返回一个数值ID,表示超时调用。这个超时调用ID是计划执行代码的唯一标识符,可以通过它来取消超时调用。要取消尚未执行的超时调用计划,可以调用clearTimeout()方法并将相应的超时调用ID作为参数传递给它,如下所示。

//设置超时调用
var timeoutId = setTimeout(function() {
 alert("Hello world!");
}, 1000);
//注意:把它取消
clearTimeout(timeoutId);

只要是在指定的时间尚未过去之前调用clearTimeout(),就可以完全取消超时调用。前面的代码在设置超时调用之后马上又调用了clearTimeout(),结果就跟什么也没有发生一样。
注意:超时调用的代码都是在全局作用域中执行的,因此函数中 this 的值在非严格模 式下指向 window 对象,在严格模式下是 undefined。
间歇调用与超时调用类似,只不过它会按照指定的时间间隔重复执行代码,直至间歇调用被取消或者页面被卸载。设置间歇调用的方法是setInterval(),它接受的参数与 setTimeout()相同:要执行的代码(字符串或函数)和每次执行之前需要等待的毫秒数。下面来看一个例子。

//不建议传递字符串!
setInterval ("alert('Hello world!') ", 10000);
//推荐的调用方式
setInterval (function() {
 alert("Hello world!");
}, 10000);

调用 setInterval()方法同样也会返回一个间歇调用 ID,该 ID 可用于在将来某个时刻取消间歇调用。要取消尚未执行的间歇调用,可以使用clearInterval()方法并传入相应的间歇调用ID。取消间歇调用的重要性要远远高于取消超时调用,因为在不加干涉的情况下,间歇调用将会一直执行到页面卸载。
在使用超时调用时,没有必要跟踪超时调用ID,因为每次执行代码之后,如果不再设置另一次超时调用,调用就会自行停止。一般认为,使用超时调用来模拟间歇调用的是一种最佳模式。在开发环境下,很少使用真正的间歇调用,原因是后一个间歇调用可能会在前一个间歇调用结束之前启动。而像前面示例中那样使用超时调用,则完全可以避免这一点。所以,最好不要使用间歇调用。

1.5系统对话框

浏览器通过 alert()、confirm()和prompt()方法可以调用系统对话框向用户显示消息。它们的外观由操作系统及(或)浏览器设置决定,而不是由CSS决定。此外,通过这几个方法打开的对话框都是同步和模态的。也就是说,显示这些对话框的时候代码会停止执行,而关掉这些对话框后代码又会恢复执行。
为了确定用户是单击了 OK 还是 Cancel,可以检查 confirm()方法返回的布尔值:true表示单击了OK,false 表示单击了 Cancel 或单击了右上角的 X 按钮。
如果用户单击了OK按钮,则prompt()返回文本输入域的值;如果用户单击了Cancel或没有单击OK而是通过其他方式关闭了对话框,则该方法返回 null。
除了上述三种对话框之外,GoogleChrome浏览器还引入了一种新特性。如果当前脚本在执行过程中会打开两个或多个对话框,那么从第二个对话框开始,每个对话框中都会显示一个复选框,以便用户阻止后续的对话框显示,除非用户刷新页面。如果用户勾选了其中的复选框,并且关闭了对话框,那么除非用户刷新页面,所有后续的系统对话框(包括警告框、确认框和提示框)都会被屏蔽。还有两个可以通过JavaScript 打开的对话框,即“查找”和“打印”。这两个对话框都是异步显示的,能够将控制权立即交还给脚本。window.print();显示“打印”对话框,window.find();显示“查找”对话框。

caizhendi commented 4 years ago

2020.04.22

阅读进度: 8.4 P214

1. location对象

提供了与当前窗口中加载的文档有关的信息,还提供了一 些导航功能。location对象是很特别的一个对象,因为它既是 window 对象的属性,也是 document 对象的属性;换句话说,window.location和document.location 引用的是同一个对象。
下表列出了 location 对象的所有属性,见p207

1.1查询字符串参数

创建一个函数,用以解析查询字符串,然后返回包含所有参数的一个对象:

function getQueryStringArgs(){
 //取得查询字符串并去掉开头的问号
 var qs = (location.search.length > 0 ? location.search.substring(1) : ""),

 //保存数据的对象
 args = {},

 //取得每一项
 items = qs.length ? qs.split("&") : [],
 item = null,
 name = null,
value = null,
 //在 for 循环中使用
 i = 0,
 len = items.length;
 //逐个将每一项添加到 args 对象中
 for (i=0; i < len; i++){
 item = items[i].split("=");
 name = decodeURIComponent(item[0]);
 value = decodeURIComponent(item[1]);
 if (name.length) {
 args[name] = value;
 }
 }

 return args;
}

1.2位置操作

使用 location对象可以通过很多方式来改变浏览器的位置。首先,也是最常用的方式,就是使用assign()方法并为其传递一个URL,如下所示。location.assign("http://www.wrox.com");
这样,就可以立即打开新URL并在浏览器的历史记录中生成一条记录。如果是将location.href或window.location 设置为一个URL值,也会以该值调用assign()方法。
修改location对象的其他属性也可以改变当前加载的页面。下面的例子展示了通过将hash、search、hostname、pathname和port属性设置为新值来改变URL。例子见p209
每次修改 location的属性(hash除外),页面都会以新 URL 重新加载。
当通过上述任何一种方式修改URL之后,浏览器的历史记录中就会生成一条新记录,因此用户通过单击“后退”按钮都会导航到前一个页面。要禁用这种行为,可以使用 replace()方法。这个方法只接受一个参数,即要导航到的 URL;结果虽然会导致浏览器位置改变,但不会在历史记录中生成新记录。在调用replace()方法之后,用户不能回到前一个页面。
与位置有关的最后一个方法是reload(),作用是重新加载当前显示的页面。如果调用reload()时不传递任何参数,页面就会以最有效的方式重新加载。也就是说,如果页面自上次请求以来并没有改变过,页面就会从浏览器缓存中重新加载。如果要强制从服务器重新加载,则需要像下面这样为该方法 传递参数true。

location.reload(); //重新加载(有可能从缓存中加载)
location.reload(true); //重新加载(从服务器重新加载)

2. navigator 对象

下表列 出了存在于所有浏览器中的属性和方法,以及支持它们的浏览器版本。见p210 常用userAgent用来检测显示网页的浏览器类型。

2.1检测插件

检测浏览器中是否安装了特定的插件是一种最常见的检测例程。对于非 IE 浏览器,可以使用 plugins 数组来达到这个目的。该数组中的每一项都包含下列属性。
name:插件的名字。
description:插件的描述。
filename:插件的文件名。
length:插件所处理的 MIME 类型数量。
在检测插件时,需要像下面这样循环迭代每个插件并将插件的 name 与给定的名字进行比较。

//检测插件(在 IE 中无效)
function hasPlugin(name){
 name = name.toLowerCase();
 for (var i=0; i < navigator.plugins.length; i++){
 if (navigator. plugins [i].name.toLowerCase().indexOf(name) > -1){
 return true;
 }
 }
 return false;
}

检测 IE 中的插件比较麻烦,因为 IE不支持Netscape 式的插件。在IE中检测插件的唯一方式就是使用专有的 ActiveXObject类型,并尝试创建一个特定插件的实例。。IE 是以 COM 对象的方式实现插 件的,而 COM 对象使用唯一标识符来标识。

//检测 IE 中的插件
function hasIEPlugin(name){
 try {
 new ActiveXObject(name);
 return true;
 } catch (ex){
 return false;
 }
}

2.2注册处理程序

Firefox 2为 navigator对象新增了registerContentHandler()和 registerProtocolHandler()方法。这两个方法可以让一个站点指明它可以处理特定类型的信息。
registerContentHandler()方法接收三个参数:要处理的 MIME 类型、可以处理该 MIME 类型的页面的 URL 以及应用程序的名称。例如:

navigator.registerContentHandler("application/rss+xml",
 "http://www.somereader.com?feed=%s", "Some Reader");

第一个参数是RSS源的MIME类型。第二个参数是应该接收 RSS 源 URL的URL,其中的%s表示RSS源URL,由浏览器自动插入。当下一次请求RSS源时,浏览器就会打开指定的 URL,而相应的Web应用程序将以适当方式来处理该请求。
类似的调用方式也适用于registerProtocolHandler()方法,它也接收三个参数:要处理的协议(例如,mailto 或 ftp)、处理该协议的页面的URL和应用程序的名称。例如,要想将一个应用程序注册为默认的邮件客户端,可以使用如下代码。

navigator.registerProtocolHandler("mailto",
 "http://www.somemailclient.com?cmd=%s", "Some Mail Client");

这个例子注册了一个mailto协议的处理程序,该程序指向一个基于Web的电子邮件客户端。同样,第二个参数仍然是处理相应请求的URL,而%s则表示原始的请求。

caizhendi commented 4 years ago

2020.04.23

阅读进度: 第九章 P217

1. screen对象

screen 对象基本上只用来表明客户端的能力,其中包括浏览器窗口外部的显示器的信息,如像素宽度和高度等。每个浏览器中的screen对象都包含着各不相同的属性,下表列出了所有属性及支持相应属性的浏览器。见p214
时候也可能会用到其中的信息来调整浏览器窗口大小,使其占据屏幕的可用空间,例如:window.resizeTo(screen.availWidth, screen.availHeight);
涉及移动设备的屏幕大小时,情况有点不一样。运行 iOS 的设备始终会像是把设备竖着拿在手里一样,因此返回的值是768×1024。而Android设备则会相应调用 screen.width 和 screen.height 的值。

2.history对象

history 对象保存着用户上网的历史记录,从窗口被打开的那一刻算起。因为history是window对象的属性,因此每个浏览器窗口、每个标签页乃至每个框架,都有自己的 history对象与特定的window对象关联。出于安全方面的考虑,开发人员无法得知用户浏览过的 URL。不过,借由用户访问过的页面列表,同样可以在不知道实际 URL 的情况下实现后退和前进。
使用 go()方法可以在用户的历史记录中任意跳转,可以向后也可以向前。这个方法接受一个参数,表示向后或向前跳转的页面数的一个整数值。负数表示向后跳转(类似于单击浏览器的“后退”按钮),正数表示向前跳转(类似于单击浏览器的“前进”按钮)。例子:

//后退一页
history.go(-1);
//前进一页
history.go(1);
//前进两页
history.go(2);

也可以给 go()方法传递一个字符串参数,此时浏览器会跳转到历史记录中包含该字符串的第一个位置——可能后退,也可能前进,具体要看哪个位置最近。如果历史记录中不包含该字符串,那么这个方法什么也不做,例如:

//跳转到最近的 wrox.com 页面
history.go("wrox.com");
//跳转到最近的 nczonline.net 页面
history.go("nczonline.net");
另外,还可以使用两个简写方法 back()和 forward()来代替 go()。顾名思义,这两个方法可以 模仿浏览器的“后退”和“前进”按钮。
//后退一页
history.back();
//前进一页
history.forward();

history 对象还有一个length属性,保存着历史记录的数量。这个数量包括所有历史记录,即所有向后和向前的记录。对于加载到窗口、标签页或框架中的第一个页面而言, history.length 等于 0。

3.小结

浏览器对象模型(BOM)以window对象为依托,表示浏览器窗口以及页面可见区域。同时,window 对象还是 ECMAScript 中的Global对象,因而所有全局变量和函数都是它的属性,且所有原生的构造函数及其他函数也都存在于它的命名空间下。

hzjjg commented 4 years ago

caizhendi commented 4 years ago

2020.04.25

阅读进度: 9.3.1 P222
检测 Web 客户端的手段很多,而且各有利弊。但最重要的还是要知道,不到万不得已,就不要使用客户端检测。只要能找到更通用的方法,就应该优先采用更通用的方法。一言以蔽之,先设计最通用的方案,然后再使用特定于浏览器的技术增强该方案。

1. 能力检测

最常用也最为人们广泛接受的客户端检测形式是能力检测(又称特性检测)。能力检测的目标不是识别特定的浏览器,而是识别浏览器的能力。采用这种方式不必顾及特定的浏览器如何如何,只要确定浏览器支持特定的能力,就可以给出解决方案。能力检测的基本模式如下:

if (object.propertyInQuestion){
 //使用 object.propertyInQuestion
}

要理解能力检测,首先必须理解两个重要的概念。如前所述,第一个概念就是先检测达成目的的最常用的特性。第二个重要的概念就是必须测试实际要用到的特性。一个特性存在,不一定意味着另一个特性也存在。例子:

function getWindowWidth(){
 if (document.all){ //假设是 IE
 return document.documentElement.clientWidth; //错误的用法!!!
 } else {
 return window.innerWidth;
 }
}

这是一个错误使用能力检测的例子。getWindowWidth()函数首先检查document.all是否存在,如果是则返回 document.documentElement.clientWidth。

1.1更可靠的能力检测

能力检测对于想知道某个特性是否会按照适当方式行事(而不仅仅是某个特性存在)非常有用。上一节中的例子利用类型转换来确定某个对象成员是否存在,但这样你还是不知道该成员是不是你想要的。来看下面的函数,它用来确定一个对象是否支持排序。

//不要这样做!这不是能力检测——只检测了是否存在相应的方法
function isSortable(object){
 return !!object.sort;
}

这个函数通过检测对象是否存在sort()方法,来确定对象是否支持排序。问题是,任何包含sort属性的对象也会返回 true。 检测某个属性是否存在并不能确定对象是否支持排序。更好的方式是检测 sort 是不是一个函数。这里的 typeof 操作符用于确定sort的确是一个函数,因此可以调用它对数据进行排序。
在可能的情况下,要尽量使用typeof进行能力检测。特别是,宿主对象没有义务让typeof返回合理的值。最令人发指的事儿就发生在IE中。大多数浏览器在检测到 document.createElement() 存在时,都会返回 true。

//在 IE8 及之前版本中不行
function hasCreateElement(){
 return typeof document.createElement == "function";
}

这就意味着,在浏览器环境下测试任何对象的某个特性是否 存在,要使用下面这个函数。

function isHostMethod(object, property) {
 var t = typeof object[property];
 return t=='function' ||
 (!!(t=='object' && object[property])) ||
 t=='unknown';
}

目前使用 isHostMethod()方法还是比较可靠的,因为它考虑到了浏览器的怪异行为。不过也要注意,宿主对象没有义务保持目前的实现方式不变,也不一定会模仿已有宿主对象的行为。所以,这个函数——以及其他类似函数,都不能百分之百地保证永远可靠。作为开发人员,必须对自己要使用某个功能的风险作出理性的估计。

1.1.2能力检测,不是浏览器检测

实际上,根据浏览器不同将能力组合起来是更可取的方式。如果你知道自己的应用程序需要使用某些特定的浏览器特性,那么最好是一次性检测所有相关特性,而不要分别检测。

//确定浏览器是否支持 Netscape 风格的插件
var hasNSPlugins = !!(navigator.plugins && navigator.plugins.length);
//确定浏览器是否具有 DOM1 级规定的能力
var hasDOM1 = !!(document.getElementById && document.createElement &&
 document.getElementsByTagName);

2.怪癖检测

与能力检测类似,怪癖检测的目标是识别浏览器的特殊行为。但与能力检测确认浏览器支持什么能力不同,怪癖检测是想要知道浏览器存在什么缺陷。 例如,IE8 及更早版本中存在一个bug,即如果某个实例属性与[[Enumerable]]标记为false的某个原型属性同名,那么该实例属性将不会出现在fon-in循环当中。可以使用如下代码来检测这种“怪癖”。

var hasDontEnumQuirk = function(){
 var o = { toString : function(){} };
 for (var prop in o){
if (prop == "toString"){
 return false;
 }
 }
 return true;
}();

另一个经常需要检测的“怪癖”是 Safari 3以前版本会枚举被隐藏的属性。见p221

hzjjg commented 4 years ago

caizhendi commented 4 years ago

2020.04.26

阅读进度: 9.3.2 P228

1. 用户代理字符串的历史

HTTP 规范(包括1.0和1.1版)明确规定,浏览器应该发送简短的用户代理字符串,指明浏览器的名称和版本号。RFC 2616(即HTTP1.1协议规范)是这样描述用户代理字符串的:
“产品标识符常用于通信应用程序标识自身,由软件名和版本组成。使用产品标识符的大多数领域也允许列出作为应用程序主要部分的子产品,由空格分隔。按照惯例,产品要按照相应的重要程度依次列出,以便标识应用程序。”用户代理字符串应该以一组产品的形式给出,字符串格式为:标识符/产品 版本号。

1.1早期的浏览器

美国 NCSA发布了世界上第一款Web浏览器Mosaic。其用户代理字符串非常简单:Mosaic/0.9。 Netscape Communications公司介入浏览器开发领域后,遂将自己产品的代号定名为Mozilla,其用户代理字符串 具有如下格式:Mozilla/版本号 [语言] (平台; 加密类型)。 注: 加密类型,即安全加密的类型。可能的值有 U(128 位加密)、I(40位加密)和N(未加密)。1.2Netscape Navigator 3 和 Internet Explorer 3
1996 年,NetscapeNavigator3发布,随即超越Mosaic 成为当时最流行的Web浏览器。而用户代理字符串只作了一些小的改变,删除了语言标记,同时允许添加操作系统或系统使用的CPU等可选信息。于是,格式变成如下所示。
Mozilla/版本号 (平台; 加密类型 [; 操作系统或 CPU 说明])
微软也发布了其第一款赢得用户广泛认可的Web浏览器,即 Internet Explorer 3。由于Netscape浏览器在当时占绝对市场份额,许多服务器在提供网页之前都要专门检测该浏览器。如果用户通过 IE 打不开相关网页,那么这个新生的浏览器很可能就会夭折。于是,微软决定将IE的用户代理字符串修改成兼容 Netscape 的形式,结果如下:
Mozilla/2.0 (compatible; MSIE 版本号; 操作系统) IE 成功地将 自己标识为Mozilla,从而伪装成Netscape Navigator。微软的这一做法招致了很多批评,因为它违反了 浏览器标识的惯例。更不规范的是,IE 将真正的浏览器版本号插入到了字符串的中间。

1.3 Netscape Communicator 4 和 IE4~IE8

见p223

1.4 Gecko

Gecko 是 Firefox的呈现引擎。当初的Gecko是作为通用 Mozilla 浏览器的一部分开发的,而第一个 采用 Gecko 引擎的浏览器是 Netscape 6。
新格式:Mozilla/Mozilla 版本号 (平台; 加密类型; 操作系统或 CPU; 语言; 预先发行版本) Gecko/Gecko 版本号 应用程序或产品/应用程序或产品版本号
具体见p224底

1.5 WebKit

Apple 公司宣布要发布自己的 Web 浏览器,名字定为 Safari。Safari 的呈现引擎叫 WebKit。 这款新浏览器和呈现引擎的开发人员也遇到了与Internet Explorer3.0类似的问题:如何确保这款浏览器不被流行的站点拒之门外?答案就是向用户代理字符串中放入足够多的信息,以便站点能够信任它与其他流行的浏览器是兼容的。于是,WebKit的用户代理字符串就具备了如下格式:
Mozilla/5.0 (平台; 加密类型; 操作系统或 CPU; 语言) AppleWebKit/AppleWebKit 版本号 (KHTML, like Gecko) Safari/Safari 版本号

1.6 Konqueror

KDE Linux 集成的 Konqueror,是一款基于 KHTML 开源呈现引擎的浏览器。尽管 Konqueror 只 能在 Linux 中使用,但它也有数量可观的用户。为确保最大限度的兼容性,Konqueror效仿IE选择了如下用户代理字符串格式:
Mozilla/5.0 (compatible; Konqueror/ 版本号; 操作系统或 CPU )

1.7 Chrome

谷歌公司的 Chrome浏览器以WebKit作为呈现引擎,但使用了不同的 JavaScript 引擎。在 Chrome 0.2 这个最初的 beta版中,用户代理字符串完全取自WebKit,只添加了一段表示 Chrome 版本号的信息,格 式如下:
Mozilla/5.0 ( 平台; 加密类型; 操作系统或 CPU; 语言) AppleWebKit/AppleWebKit 版本号 (KHTML, like Gecko) Chrome/ Chrome 版本号 Safari/ Safari 版本

1.8 Opera

Opera 应该是最有争议的一款浏览器了。Opera 默认的用户代理字符串是所有现代浏览器中最合理的——正确地标识了自身及其版本号。在 Opera 8.0 之前,其用户代理字符 串采用如下格式:
Opera/ 版本号 (操作系统或 CPU; 加密类型) [语言]

1.9 iOS 和 Android

移动操作系统 iOS 和 Android 默认的浏览器都基于 WebKit,而且都像它们的桌面版一样,共享相同的基本用户代理字符串格式。iOS 设备的基本格式如下:
Mozilla/5.0 (平台; 加密类型; 操作系统或 CPU like Mac OS X; 语言) AppleWebKit/AppleWebKit 版本号 (KHTML, like Gecko) Version/浏览器版本号 Mobile/移动版本号 Safari/Safari 版本号
Android 浏览器中的默认格式与iOS的格式相似,没有移动版本号(但有 Mobile 记号)。例如:
Mozilla/5.0 (Linux; U; Android 2.2; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1

caizhendi commented 4 years ago

2020.04.27

阅读进度: 第十章

1. 用户代理字符串检测技术

考虑到历史原因以及现代浏览器中用户代理字符串的使用方式,通过用户代理字符串来检测特定的浏览器并不是一件轻松的事。因此,首先要确定的往往是你需要多么具体的浏览器信息。不推荐使用以下代码:

if (isIE6 || isIE7) { //不推荐!!!
 //代码
}

这个例子是想要在浏览器为IE6或IE7时执行相应代码。这种代码其实是很脆弱的,因为它要依据特定的版本来决定做什么。如果是IE8怎么办呢?只要IE有新版本出来,就必须更新这些代码。不过,像下面这样使用相对版本号则可以避免此问题:

if (ieVer >=6){
 //代码
}

1.1识别呈现引擎

如前所述,确切知道浏览器的名字和版本号不如确切知道它使用的是什么呈现引擎。如果Firefox、Camino 和 Netscape都使用相同版本的Gecko,那它们一定支持相同的特性。我们要编写的脚本将主要检测五大呈现引擎:IE、Gecko、WebKit、KHTML 和 Opera。
为了不在全局作用域中添加多余的变量,我们将使用模块增强模式来封装检测脚本。检测脚本的基本代码结构如下所示:例子见p229
这里声明了一个名为client的全局变量,用于保存相关信息。匿名函数内部定义了一个局部变量engine,它是一个包含默认设置的对象字面量。在这个对象字面量中,每个呈现引擎都对应着一个属性,属性的值默认为 0。如果检测到了哪个呈现引擎,那么就以浮点数值形式将该引擎的版本号写入相应的属性。而呈现引擎的完整版本(是一个字符串),则被写入 ver 属性。作这样的区分可以支持像下面这样编写代码:

if (client.engine.ie) { //如果是 IE,client.ie 的值应该大于 0
 //针对 IE 的代码
} else if (client.engine.gecko > 1.5){
 if (client.engine.ver == "1.8.1"){
 //针对这个版本执行某些操作
 }
}

要正确地识别呈现引擎,关键是检测顺序要正确。由于用户代理字符串存在诸多不一致的地方,如果检测顺序不对,很可能会导致检测结果不正确。为此,第一步就是识别Opera,因为它的用户代理字符串有可能完全模仿其他浏览器。我们不相信Opera,是因为(任何情况下)其用户代理字符串(都)不会将自己标识为 Opera。
应该放在第二位检测的呈现引擎是WebKit。因为WebKit 的用户代理字符串中包含"Gecko"和"KHTML"这两个子字符串,所以如果首先检测它们,很可能会得出错误的结论。
WebKit 的用户代理字符串中的"AppleWebKit"是独一无二的,因此检测这个字符串最合适。下面就是检测该字符串的示例代码:

var ua = navigator.userAgent;
if (window.opera){
 engine.ver = window.opera.version();
 engine.opera = parseFloat(engine.ver);
} else if (/AppleWebKit\/(\S+)/.test(ua)){
 engine.ver = RegExp["$1"];
 engine.webkit = parseFloat(engine.ver);
}

WebKit 版本与Safari版本的详细对应情况如下表所示。见p230底
接下来要测试的呈现引擎是 KHTML。同样,KHTML 的用户代理字符串中也包含"Gecko",因此 在排除 KHTML 之前,我们无法准确检测基于Gecko的浏览器。相应检测代码见p231
在排除了 WebKit 和 KHTML之后,就可以准确地检测 Gecko 了。但是,在用户代理字符串中,Gecko的版本号不会出现在字符串"Gecko"的后面,而是会出现在字符串"rv:"的后面。这样,我们就必须使用一个比前面复杂一些的正则表达式,如下所示。例子见p231底
最后一个要检测的呈现引擎就是IE了。IE的版本号位于字符串"MSIE"的后面、一个分号的前面,因此相应的正则表达式非常简单,如下所示:p232

1.2识别浏览器

对 Opera 和 IE 而言,browser 对象中的值等于 engine 对象中的值。对 Konqueror 而言,browser. konq 和 browser.ver 属性分别等于 engine.khtml 和 engine.ver属性。为了检测Chrome和Safari,我们在检测引擎的代码中添加了 if 语句。提取Chrome 的版本号时,需要查找字符串"Chrome/"并取得该字符串后面的数值。而提取Safari的版本号时,则需要查找字符串 "Version/"并取得其后的数值。由于这种方式仅适用于Safari3及更高版本,因此需要一些备用的代 码,将WebKit的版本号近似地映射为Safari 的版本号。
在检测 Firefox的版本时,首先要找到字符串"Firefox/",然后提取出该字符串后面的数值(即版本号)。当然,只有呈现引擎被判别为Gecko时才会这样做。

1.3识别平台

那些具有各种平台版本的浏览器(如Safari、Firefox 和 Opera)在不同的平台下可能会有不同的问题。目前的三大主流平台是Windows、Mac和Unix(包括各种 Linux)。为了检测这些平台,还需要像下面这样再添加一个新对象。

var client = function(){
 var engine = {
 //呈现引擎
 ie: 0,
 gecko: 0,
 webkit: 0,
 khtml: 0,
 opera: 0,
 //具体的版本号
 ver: null
 };
 var browser = {
 //浏览器
 ie: 0,
 firefox: 0,
 safari: 0,
 konq: 0,
 opera: 0,
 chrome: 0,
 //具体的版本号
 ver: null
 };
 var system = {
 win: false,
 mac: false,
 x11: false
 };
 //在此检测呈现引擎、平台和设备
 return {
 engine: engine,
 browser: browser,
 system: system
 };
}();

在确定平台时,检测navigator.platform要比检测用户代理字符串更简单,后者在不同浏览器中会给出不同的平台信息。而navigator.platform属性可能的值包括"Win32"、"Win64"、"MacPPC"、"MacIntel"、"X11"和"Linux i686",这些值在不同的浏览器中都是一致的。

1.4识别Windows 操作系统

在 Windows 平台下,还可以从用户代理字符串中进一步取得具体的操作系统信息。在WindowsXP之前,Windows 有两种版本,分别针对家庭用户和商业用户。针对家庭用户的版本分别是 Windows 95、 98 和 Windows ME。而针对商业用户的版本则一直叫做Window NT,最后由于市场原因改名为Windows2000。这两个产品线后来又合并成一个由WindowsNT发展而来的公共的代码基,代表产品就是WindowsXP。随后,微软在 WindowsXP基础上又构建了WindowsVista。具体见p236

1.5识别移动设备

添加见p239
然后,通常简单地检测字符串"iPhone"、"iPod"和"iPad",就可以分别设置相应属性的值了。

system.iphone = ua.indexOf("iPhone") > -1;
system.ipod = ua.indexOf("iPod") > -1;
system.ipod = ua.indexOf("iPad") > -1;

检查系统是不是MacOS、字符串中是否存在"Mobile",可以保证无论是什么版本,system.ios中都不会是 0。然后,再使用正则表达式确定是否存在iOS的版本号。如果有,将system.ios设置为表示版本号的浮点值;否则,将版本设置为 2。
检测 Android操作系统也很简单,也就是搜索字符串"Android"并取得紧随其后的版本号。
最后一种主要的移动设备平台是WindowsMobile(也称为 Windows CE),用于 Pocket PC 和Smartphone 中。由于从技术上说这些平台都属于Windows平台,因此 Windows 平台和操作系统都会返 回正确的值。

1.6识别游戏系统

检测前述游戏系统的代码如下:

system.wii = ua.indexOf("Wii") > -1;
system.ps = /playstation/i.test(ua);

对于 Wii,只要检测字符串"Wii"就够了,而其他代码将发现这是一个Opera浏览器,并将正确的版本号保存在 client.browser.opera中。对于Playstation,我们则使用正则表达式来以不区分大小写的方式测试用户代理字符串。

  1. 完整的代码
    以下是完整的用户代理字符串检测脚本,包括检测呈现引擎、平台、Windows操作系统、移动设备和游戏系统。见p242下
    3.使用方法 我们在前面已经强调过了,用户代理检测是客户端检测的最后一个选择。只要可能,都应该优先采用能力检测和怪癖检测。用户代理检测一般适用于下列情形。
    • (1) 不能直接准确地使用能力检测或怪癖检测。例如,某些浏览器实现了为将来功能预留的存根 (stub)函数。在这种情况下,仅测试相应的函数是否存在还得不到足够的信息。
    • (2) 同一款浏览器在不同平台下具备不同的能力。这时候,可能就有必要确定浏览器位于哪个平 台下。
    • (3) 为了跟踪分析等目的需要知道确切的浏览器。

      4.小结

      客户端检测方法有3种:

  2. 能力检测:在编写代码之前先检测特定浏览器的能力。例如,脚本在调用某个函数之前,可能 要先检测该函数是否存在。这种检测方法将开发人员从考虑具体的浏览器类型和版本中解放出 来,让他们把注意力集中到相应的能力是否存在上。能力检测无法精确地检测特定的浏览器和 版本。
  3. 怪癖检测:怪癖实际上是浏览器实现中存在的 bug,例如早期的 WebKit 中就存在一个怪癖,即 它会在 for-in循环中返回被隐藏的属性。怪癖检测通常涉及到运行一小段代码,然后确定浏览器是否存在某个怪癖。由于怪癖检测与能力检测相比效率更低,因此应该只在某个怪癖会干扰脚本运行的情况下使用。怪癖检测无法精确地检测特定的浏览器和版本。
  4. 用户代理检测:通过检测用户代理字符串来识别浏览器。用户代理字符串中包含大量与浏览器 有关的信息,包括浏览器、平台、操作系统及浏览器版本。用户代理字符串有过一段相当长的发展历史,在此期间,浏览器提供商试图通过在用户代理字符串中添加一些欺骗性信息,欺骗网站相信自己的浏览器是另外一种浏览器。用户代理检测需要特殊的技巧,特别是要注意Opera会隐瞒其用户代理字符串的情况。即便如此,通过用户代理字符串仍然能够检测出浏览器所用 的呈现引擎以及所在的平台,包括移动设备和游戏系统。 在决定使用哪种客户端检测方法时,一般应优先考虑使用能力检测。怪癖检测是确定应该如何处理 代码的第二选择。而用户代理检测则是客户端检测的最后一种方案,因为这种方法对用户代理字符串具 有很强的依赖性。
caizhendi commented 4 years ago

2020.04.28

阅读进度: 10.1.2 P253

DOM(文档对象模型)是针对 HTML 和 XML 文档的一个 API(应用程序编程接口)。1998 年 10 月 DOM1级规范成为W3C的推荐标准,为基本的文档结构及查询提供了接口。

1.节点层次

节点分为几种不同的类 型,每种类型分别表示文档中不同的信息及(或)标记。每个节点都拥有各自的特点、数据和方法,另 外也与其他节点存在某种关系。节点之间的关系构成了层次,而所有页面标记则表现为一个以特定节点 为根节点的树形结构。以下面的 HTML 为例:

<html>
 <head>
 <title>Sample Page</title>
 </head>
 <body>
 <p>Hello World!</p>
 </body>
</html>

文档节点是每个文档的根节点。在这个例子中,文档节点只有一个子节点,即元素,我们称之为文档元素。文档元素是文档的最外层元素,文档中的其他所有元素都包含在文档元素中。每个文档只能有一个文档元素。在 HTML 页面中,文档元素始终都是元素。在 XML 中,没有预定义的元素,因此任何元素都可能成为文档元素。HTML元素通过元素节点表示,特性(attribute) 通过特性节点表示,文档类型通过文档类型节点表示,而注释则通过注释节点表示。

1.1Node类型

DOM1 级定义了一个 Node 接口,该接口将由 DOM 中的所有节点类型实现。这个 Node接口在JavaScript 中是作为 Node类型实现的;除了IE之外,在其他所有浏览器中都可以访问到这个类型。JavaScript中的所有节点类型都继承自Node类型,因此所有节点类型都共享着相同的基本属性和方法。每个节点都有一个nodeType属性,用于表明节点的类型。节点类型由在 Node 类型中定义的下列12个数值常量来表示,任何节点类型必居其一:

Node.ELEMENT_NODE(1);
Node.ATTRIBUTE_NODE(2);
Node.TEXT_NODE(3);
Node.CDATA_SECTION_NODE(4);
Node.ENTITY_REFERENCE_NODE(5);
Node.ENTITY_NODE(6);
Node.PROCESSING_INSTRUCTION_NODE(7);
Node.COMMENT_NODE(8);
Node.DOCUMENT_NODE(9);
Node.DOCUMENT_TYPE_NODE(10);
Node.DOCUMENT_FRAGMENT_NODE(11);
Node.NOTATION_NODE(12)

为了确保跨浏览器兼容,最好还是将nodeType属性与数字值进行比较,如下所示:

if (someNode.nodeType == 1){ //适用于所有浏览器
 alert("Node is an element.");
}

1.1.1 nodeName 和 nodeValue 属性

在使用这两个值以前,最好是像下面这样先检测一下节点的类型。

if (someNode.nodeType == 1){
 value = someNode.nodeName; //nodeName 的值是元素的标签名
}

在这个例子中,首先检查节点类型,看它是不是一个元素。如果是,则取得并保存nodeName的值。对于元素节点,nodeName中保存的始终都是元素的标签名,而 nodeValue 的值则始终为 null。

1.1.2 节点关系

节点间的各种关系可以用传统的家族关系来描 述,相当于把文档树比喻成家谱。每个节点都有一个 childNodes 属性,其中保存着一个 NodeList 对象。NodeList是一种类数组对象,用于保存一组有序的节点,可以通过位置来访问这些节点。请注意,虽然可以通过方括号语法来访问NodeList的值,而且这个对象也有 length 属性,但它并不是 Array 的实例。NodeList对象的独特之处在于,它实际上是基于 DOM 结构动态执行查询的结果,因此DOM结构的变化能够自动反映 在 NodeList 对象中。

下面的例子展示了如何访问保存在 NodeList 中的节点——可以通过方括号,也可以使用 item() 方法。

var firstChild = someNode.childNodes[0];
var secondChild = someNode.childNodes.item(1);
var count = someNode.childNodes.length;

对 arguments对象使用Array.prototype.slice()方法可以 将其转换为数组。而采用同样的方法,也可以将 NodeList 对象转换为数组。来看下面的例子:

//在 IE8 及之前版本中无效
var arrayOfNodes = Array.prototype.slice.call(someNode.childNodes,0);

要想在 IE 中将NodeList转换为数组,必须手动枚举所有成员。下列代码在所有浏览器中都可以运行,例子见p250 每个节点都有一个parentNode属性,该属性指向文档树中的父节点。包含在childNodes列表中的所有节点都具有相同的父节点,因此它们的parentNode属性都指向同一个节点。此外,包含在childNodes列表中的每个节点相互之间都是同胞节点。通过使用列表中每个节点的 previousSibling和nextSibling属性,可以访问同一列表中的其他节点。列表中第一个节点的 previousSibling属性值为null,而列表中最后一个节点的 nextSibling属性的值同样也为null。当然,如果列表中只有一个节点,那么该节点的 nextSibling 和 previousSibling 都为 null。

另外,hasChildNodes()也是一个非常有用的方法,这个方法在节点包含一或多个子节点的情况下返回true;应该说,这是比查询childNodes列表的length属性更简单的方法。

所有节点都有的最后一个属性是ownerDocument,该属性指向表示整个文档的文档节点。这种关系表示的是任何节点都属于它所在的文档,任何节点都不能同时存在于两个或更多个文档中。通过这个属性,我们可以不必在节点层次中通过层层回溯到达顶端,而是可以直接访问文档节点。

1.1.3 操作节点

DOM 提供了一些操作节点的方法。其中,最常用的方法是 appendChild(),用于向childNodes列表的末尾添加一个节点。添加节点后,childNodes的新增节点、父节点及以前的最后一个子节点的关系指针都会相应地得到更新。更新完成后,appendChild()返回新增的节点。 如果需要把节点放在childNodes列表中某个特定的位置上,而不是放在末尾,那么可以使用insertBefore()方法。这个方法接受两个参数:要插入的节点和作为参照的节点。插入节点后,被插入的节点会变成参照节点的前一个同胞节点(previousSibling),同时被方法返回。如果参照节点是null,则insertBefore()与 appendChild()执行相同的操作,如下面的例子所示。

//插入后成为最后一个子节点
returnedNode = someNode.insertBefore(newNode, null);
alert(newNode == someNode.lastChild); //true
//插入后成为第一个子节点
var returnedNode = someNode.insertBefore(newNode, someNode.firstChild);
alert(returnedNode == newNode); //true
alert(newNode == someNode.firstChild); //true
//插入到最后一个子节点前面
returnedNode = someNode.insertBefore(newNode, someNode.lastChild);
alert(newNode == someNode.childNodes[someNode.childNodes.length-2]); //true

前面介绍的 appendChild()和insertBefore()方法都只插入节点,不会移除节点。而下面要介绍的replaceChild()方法接受的两个参数是:要插入的节点和要替换的节点。要替换的节点将由这个方法返回并从文档树中被移除,同时由要插入的节点占据其位置。例子:

//替换第一个子节点
var returnedNode = someNode.replaceChild(newNode, someNode.firstChild);
//替换最后一个子节点
returnedNode = someNode.replaceChild(newNode, someNode.lastChild);

如果只想移除而非替换节点,可以使用removeChild()方法。这个方法接受一个参数,即要移除的节点。被移除的节点将成为方法的返回值。

1.1.4 其他方法

第一个就是 cloneNode(),用于创建调用这个方法的节点 的一个完全相同的副本。cloneNode()方法接受一个布尔值参数,表示是否执行深复制。在参数为 true 的情况下,执行深复制,也就是复制节点及其整个子节点树;在参数为false的情况下,执行浅复制, 即只复制节点本身。复制后返回的节点副本属于文档所有,但并没有为它指定父节点。因此,这个节点 副本就成为了一个“孤儿”,除非通过appendChild()、insertBefore()或replaceChild()将它添加到文档中。

注意:cloneNode()方法不会复制添加到DOM节点中的 JavaScript属性,例如事件处理程序等。这个方法只复制特性、(在明确指定的情况下也复制)子节点,其他一切 都不会复制。IE在此存在一个bug,即它会复制事件处理程序,所以我们建议在复制之前最好先移除事件处理程序。

最后一个方法是normalize(),这个方法唯一的作用就是处理文档树中的文本节点。由于解析器的实现或 DOM 操作等原因,可能会出现文本节点不包含文本,或者接连出现两个文本节点的情况。当在某个节点上调用这个方法时,就会在该节点的后代节点中查找上述两种情况。如果找到了空文本节点,则删除它;如果找到相邻的文本节点,则将它们合并为一个文本节点。

hzjjg commented 4 years ago

caizhendi commented 4 years ago

2020.04.29

阅读进度: 10.1.3 P261

1.Document类型

JavaScript 通过Document类型表示文档。在浏览器中,document 对象是 HTMLDocument(继承 自Document 类型)的一个实例,表示整个HTML页面。而且,document 对象是 window对象的一个属性,因此可以将其作为全局对象来访问。Document 节点具有下列特征:

Document 类型可以表示 HTML 页面或者其他基于 XML 的文档。不过,最常见的应用还是作为 HTMLDocument 实例的 document对象。通过这个文档对象,不仅可以取得与页面有关的信息,而且还能操作页面的外观及其底层结构。

1.1文档的子节点

虽然 DOM 标准规定 Document 节点的子节点可以是 DocumentType、Element、ProcessingInstruction 或 Comment,但还有两个内置的访问其子节点的快捷方式。第一个就是documentElement属性,该属性始终指向 HTML 页面中的元素。另一个就是通过childNodes 列表访问文档元素, 但通过 documentElement 属性则能更快捷、更直接地访问该元素。以下面这个简单的页面为例。

<html>
 <body>

 </body>
</html>

这个页面在经过浏览器解析后,其文档中只包含一个子节点,即元素。可以通过 documentElement 或 childNodes 列表来访问这个元素,如下所示。

var html = document.documentElement; //取得对<html>的引用
alert(html === document.childNodes[0]); //true
alert(html === document.firstChild); //true

作为 HTMLDocument 的实例,document对象还有一个 body 属性,直接指向元素。因为开发人员经常要使用这个元素,所以document.body在JavaScript代码中出现的频率非常高,其用法如下。

var body = document.body; //取得对<body>的引用

所有浏览器都支持 document.documentElement 和 document.body属性。Document另一个可能的子节点是 DocumentType。通常将标签看成一个与文档其他部分不同的实体,可以通过doctype属性(在浏览器中是 document.doctype)来访问它的信息。 浏览器对 document.doctype的支持差别很大,可以给出如下总结:

1.2文档信息

作为 HTMLDocument的一个实例,document对象还有一些标准的 Document对象所没有的属性。这些属性提供了 document对象所表现的网页的一些信息。其中第一个属性就是title,包含着元素中的文本——显示在浏览器窗口的标题栏或标签页上。通过这个属性可以取得当前页面的标题,也可以修改当前页面的标题并反映在浏览器的标题栏中。修改 title 属性的值不会改变<title>元素。</p> <p>接下来要介绍的3个属性都与对网页的请求有关,它们是 URL、domain和referrer。URL属性中包含页面完整的 URL(即地址栏中显示的URL),domain属性中只包含页面的域名,而referrer属性中则保存着链接到当前页面的那个页面的URL。在没有来源页面的情况下,referrer属性中可能会包含空字符串。所有这些信息都存在于请求的HTTP头部,只不过是通过这些属性让我们能够在JavaScrip中访问它们而已,如下面的例子所示。</p> <pre><code class="language-js">//取得完整的 URL var url = document.URL; //取得域名 var domain = document.domain; //取得来源页面的 URL var referrer = document.referrer;</code></pre> <p>URL 与 domain 属性是相互关联的。例如,如果 document.URL等于http://www.wrox.com/WileyCDA/, 那么 document.domain 就等于 www.wrox.com。</p> <p>在这 3 个属性中,只有domain是可以设置的。但由于安全方面的限制,也并非可以给 domain 设 置任何值。如果URL中包含一个子域名,例如p2p.wrox.com,那么就只能将 domain 设置为"wrox.com" (URL 中包含"www",如www.wrox.com时,也是如此)。不能将这个属性设置为URL中不包含的域,如下面的例子所示。见p256</p> <p>当页面中包含来自其他子域的框架或内嵌框架时,能够设置 document.domain就非常方便了。由于跨域安全限制,来自不同子域的页面无法通过 JavaScript 通信。而通过将每个页面的document.domain设置为相同的值,这些页面就可以互相访问对方包含的JavaScript 对象了。</p> <p>浏览器对 domain属性还有一个限制,即如果域名一开始是“松散的”(loose),那么不能将它再设 置为“紧绷的”(tight)。换句话说,在将document.domain 设置为"wrox.com"之后,就不能再将其设置回"p2p.wrox.com",否则将会导致错误,如下面的例子所示。</p> <pre><code class="language-js">//假设页面来自于 p2p.wrox.com 域 document.domain = "wrox.com"; //松散的(成功) document.domain = "p2p.wrox.com"; //紧绷的(出错!)</code></pre> <h2>1.3查找元素</h2> <p>取得元素的操作可以使用document对象的几个方法来完成。其中,Document 类型为此提供了两个方 法:getElementById()和getElementsByTagName()。</p> <p>第一个方法,getElementById(),接收一个参数:要取得的元素的ID。如果找到相应的元素则返回该元素,如果不存在带有相应ID的元素,则返回null。注意,这里的 ID 必须与页面中元素的id特性(attribute)严格匹配,包括大小写。以下面的元素为例。IE8 及较低版本不区分 ID 的大小写。 如果页面中多个元素的ID值相同,getElementById()只返回文档中第一次现的元素。IE7及较低版本还为此方法添加了一个有意思的“怪癖”:name特性与给定 ID 匹配的表单元素(<code><input>、<textarea>、<button>及<select></code>)也会被该方法返回。如果有哪个表单元素的 name 特性等于指定的ID,而且该元素在文档中位于带有给定ID的元素前面,那么IE就会返回那个表单元素。例子见p257</p> <p>另一个常用于取得元素引用的方法是getElementsByTagName()。这个方法接受一个参数,即要取得元素的标签名,而返回的是包含零或多个元素的NodeList。在 HTML 文档中,这个方法会返回一个HTMLCollection 对象,作为一个“动态”集合,该对象与 NodeList 非常类似。</p> <p>这行代码会将一个 HTMLCollection 对象保存在 images 变量中。与NodeList对象类似,可以使用方括号语法或 item()方法来访问 HTMLCollection 对象中的项。而这个对象中元素的数量则可以通过其 length 属性取得,如下面的例子所示。</p> <pre><code class="language-js">alert(images.length); //输出图像的数量 alert(images[0].src); //输出第一个图像元素的 src 特性 alert(images.item(0).src); //输出第一个图像元素的 src 特性</code></pre> <p>HTMLCollection对象还有一个方法,叫做namedItem(),使用这个方法可以通过元素的name特性取得集合中的项。那么就可以通过如下方式从 images变量中取得这个<img>元素:</p> <pre><code class="language-html"><img src="myimage.gif" name="myImage"></code></pre> <pre><code class="language-js">var myImage = images.namedItem("myImage");</code></pre> <p>对命名的项也可以使用方括号语法来访问,如下所示:</p> <p>var myImage =images["myImage"];对HTMLCollection而言,我们以向方括号中传入数值或字符串形式的索引值。在后,对数值索引就会调用item(),而对字符串索引就会调用 namedItem()。</p> <p><strong>注意</strong>:虽然标准规定标签名需要区分大小写,但为了最大限度地与既有HTML页面兼容,传给getElementsByTagName()的标签名是不需要区分大写的。</p> <p>第三个方法,也是只有HTMLDocument类型才有的方法,是 getElementsByName()。顾名思义,这个方法会返回带有给定name特性的所有元素。最常使用getElementsByName()方法的情况是取得单选按钮;为了确保发送给浏览器的值正确无误,所有单选按钮必须具有相同的 name 特性,例子见p258</p> <p>与 getElementsByTagName()类似,getElementsByName()方法也会返回一个HTMLCollectioin。但是,对于这里的单选按钮来说,namedItem()方法则只会取得第一项(因为每一项的 name 特性都相同)。</p> <h2>1.4特殊集合</h2> <p>除了属性和方法,document对象还有一些特殊的集合。这些集合都是HTMLCollection对象,为访问文档常用的部分提供了快捷方式,包括:</p> <ol> <li>document.anchors,包含文档中所有带 name 特性的<code><a></code>元素;</li> <li>document.applets,包含文档中所有的<code><applet></code>元素,因为不再推荐使用<code><applet></code>元素,所以这个集合已经不建议使用了;</li> <li>document.forms,包含文档中所有的<code><form></code>元素,与 document.getElementsByTagName("form")得到的结果相同;</li> <li>document.images,包含文档中所有的<code><img></code>元素,与 document.getElementsByTagName("img")得到的结果相同;</li> <li>document.links,包含文档中所有带 href 特性的<code><a></code>元素。</li> </ol> <h2>1.5 DOM一致性检测</h2> <p>检测浏览器实现了DOM的哪些部分就十分必要了。document.implementation属性就是为此提供相应信息和功能的对象,与浏览器对 DOM的实现直接对应。DOM1 级只为 document.implementation规定了一个方法,即 hasFeature()。这个方法接受两个参数:要检测的 DOM 功能的名称及版本号。如果浏览器支持给定名称和版本的功能,则该方法返回true,如下面的例子所示:</p> <pre><code class="language-js">var hasXmlDom = document.implementation.hasFeature("XML", "1.0");</code></pre> <p>具体列表见p259</p> <p>我们建议多数情况下,在使用DOM的某些特殊的功能之前,最好除了检测hasFeature()之外,还同时使用能力检测。</p> <h2>1.6文档写入</h2> <p>有一个 document对象的功能已经存在很多年了,那就是将输出流写入到网页中的能力。这个能力体现在下列 4 个方法中:write()、writeln()、open()和 close()。其中,write()和writeln()方法都接受一个字符串参数,即要写入到输出流中的文本。write()会原样写入,而writeln()则会在字符串的末尾添加一个换行符(\n)。在页面被加载的过程中,可以使用这两个方法向页面中动态地加入内容,例子见p260</p> <p>此外,还可以使用write()和writeln()方法动态地包含外部资源,例如 JavaScript 文件等。在包 含 JavaScript 文件时,必须注意不能像下面的例子样直接包含字符串"",因为这会导致该字符串被解释为脚本块的结束,它后面的代码将无法执行。为避免这个问题,只需加入转义字符\即可。 方法 open()和close()分别用于打开和关闭网页的输出流。如果是在页面加载期间使用write()或writeln()方法,则不需要用到这两个方法。</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/hzjjg"><img src="https://avatars.githubusercontent.com/u/14288079?v=4" />hzjjg</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <p>阅</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.06</h1> <p>阅读进度: 10.1.3.4 P266</p> <h2>1. Element类型</h2> <p>Element 类型用 于表现 XML 或 HTML元素,提供了对元素标签名、子节点及特性的访问。Element 节点具有以下特征: </p> <ul> <li>nodeType 的值为 1;</li> <li>nodeName 的值为元素的标签名;</li> <li>nodeValue 的值为 null;</li> <li>parentNode 可能是 Document 或 Element;</li> <li>其子节点可能是Element、Text、Comment、ProcessingInstruction、CDATASection或EntityReference。 要访问元素的标签名,可以使用nodeName属性,也可以使用 tagName属性;这两个属性会返回相同的值(使用后者主要是为了清晰起见)。以下面的元素为例: <pre><code class="language-html"><div id="myDiv"></div></code></pre> <p>可以像下面这样取得这个元素及其标签名:</p> <pre><code class="language-js">var div = document.getElementById("myDiv"); alert(div.tagName); //"DIV" alert(div.tagName == div.nodeName); //true</code></pre> <p>在 HTML 中,标签名始终都以全部大写表示;而在 XML(有时候也包括XHTML)中,标签名则始终会与源代码中的保持一致。假如你不确定自己的脚本将会在 HTML 还是 XML文档中执行,最好是在比较之前将标签名转换为相同的大小写形式,如下面的例子所示: </p> <pre><code class="language-js">if (element.tagName.toLowerCase() == "div"){ //这样最好(适用于任何文档) //在此执行某些操作 }</code></pre> <h3> 1.1HTML元素</h3> <p>所有 HTML 元素都由HTMLElement类型表示,不是直接通过这个类型,也是通过它的子类型来表示。HTMLElement 类型直接继承自Element并添加了一些属性。添加的这些属性分别对应于每个HTML元素中都存在的下列标准特性。</p></li> <li>id,元素在文档中的唯一标识符。</li> <li>title,有关元素的附加说明信息,一般通过工具提示条显示出来。</li> <li>lang,元素内容的语言代码,很少使用。</li> <li>dir,语言的方向,值为"ltr"(left-to-right,从左至右)或"rtl"(right-to-left,从右至左),也很少使用。</li> <li>className,与元素的class特性对应,即为元素指定的CSS类。没有将这个属性命名为class,是因为 class 是 ECMAScript的保留字(有关保留字的信息,请参见第 1 章)。<br /> 以下面的 HTML 元素为例: <pre><code class="language-html"><div id="myDiv" class="bd" title="Body text" lang="en" dir="ltr"><div></code></pre> <p>元素中指定的所有信息,都可以通过下列 JavaScript 代码取得: </p> <pre><code class="language-js">var div = document.getElementById("myDiv"); alert(div.id); //"myDiv"" alert(div.className); //"bd" alert(div.title); //"Body text" alert(div.lang); //"en" alert(div.dir); //"ltr"</code></pre> <p>当然,像下面这样通过为每个属性赋予新的值,也可以修改对应的每个特性:</p> <pre><code class="language-js">div.id = "someOtherId"; div.className = "ft"; div.title = "Some other text"; div.lang = "fr"; div.dir ="rtl";</code></pre> <p>并不是对所有属性的修改都会在页面中直观地表现出来。对 id 或lang的修改对用户而言是透明不可见的(假设没有基于它们的值设置的 CSS 样式),而对 title 的修改则只会在鼠标移动到这个元素之上时才会显示出来。对dir的修改会在属性被重写的那一刻,立即影响页面中文本的左、右对齐方式。修改className 时,如果新类关联了与此前不同的CSS样式,那么就会立即应用新的样式。<br /> 所有 HTML 元素都是由HTMLElement或者其更具体的子类型来表示的。下表列出了所有HTML元素以及与之关联的类型(以斜体印刷的元素表示已经不推荐使用了)。注意,表中的这些类型在Opera、Safari、Chrome 和 Firefox 中都可以通过 JavaScript 访问,但在 IE8 之前的版本中不能通 过 JavaScript 访问。<br /> 表见p263、p264</p> <h3> 1.2 取得特性</h3> <p>每个元素都有一或多个特性,这些特性的用途是给出相应元素或其内容的附加信息。操作特性的 DOM 方法主要有三个,分别是getAttribute()、setAttribute()和 removeAttribute()。这三个方法可以针对任何特性使用,包括那些以 HTMLElement 类型属性的形式定义的特性。来看下面的例子:  </p> <pre><code class="language-js">var div = document.getElementById("myDiv"); alert(div.getAttribute("id")); //"myDiv" alert(div.getAttribute("class")); //"bd" alert(div.getAttribute("title")); //"Body text" alert(div.getAttribute("lang")); //"en" alert(div.getAttribute("dir")); //"ltr"</code></pre> <p>注意,传递给getAttribute()的特性名与实际的特性名相同。因此要想得到class特性值,应该传入"class"而不是"className",后者只有在通过对象属性访问特性时才用。如果给定名称的特性不存在,getAttribute()返回 null。<br /> 通过 getAttribute()方法也可以取得自定义特性(即标准 HTML 语言中没有的特性)的值,例子见p264<br /> 不过,特性的名称是不区分大小写的,即"ID"和"id"代表的都是同一个特性。另外也要注意,根 据 HTML5 规范,自定义特性应该加上 data-前缀以便验证。<br /> 任何元素的所有特性,也都可以通过DOM元素本身的属性来访问。当然,HTMLElement也会有5个属性与相应的特性一一对应。不过,只有公认的(非自定义的)特性才会以属性的形式添加到DOM对象中。以下面的元素为例: </p> <pre><code class="language-html"><div id="myDiv" align="left" my_special_attribute="hello!"></div></code></pre> <p>因为 id 和 align在HTML中是<div>的公认特性,因此该元素的 DOM对象中也将存在对应的属性。不过,自定义特性 my_special_attribute在Safari、Opera、Chrome 及 Firefox中是不存在的;但IE却会为自定义特性也创建属性,如下面的例子所示: </p> <pre><code class="language-js">alert(div.id); //"myDiv" alert(div.my_special_attribute); //undefined(IE 除外) alert(div.align); //"left"</code></pre> <p>有两类特殊的特性,它们虽然有对应的属性名,但属性的值与通过 getAttribute()返回的值并不 相同。</p></li> <li>第一类特性就是style,用于通过CSS为元素指定样式。在通过 getAttribute()访问时,返 回的 style 特性值中包含的是CSS文本,而通过属性来访问它则会返回一个对象。</li> <li>第二类与众不同的特性是 onclick 这样的事件处理程序。当在元素上使用时,onclick 特性中包 含的是 JavaScript 代码,如果通过 getAttribute()访问,则会返回相应代码的字符串。而在访问 onclick 属性时,则会返回一个 JavaScript 函数(如果未在元素中指定相应特性,则返回 null)。这是 因为 onclick 及其他事件处理程序属性本身就应该被赋予函数值。 <h3>1.3设置特性</h3> <p>与 getAttribute()对应的方法是setAttribute(),这个方法接受两个参数:要设置的特性名和值。如果特性已经存在,setAttribute()会以指定的值替换现有的值;如果特性不存在,setAttribute()则创建该属性并设置相应的值。来看下面的例子: </p> <pre><code class="language-js">div.setAttribute("id", "someOtherId"); div.setAttribute("class", "ft"); div.setAttribute("title", "Some other text"); div.setAttribute("lang","fr"); div.setAttribute("dir", "rtl");</code></pre> <p>因为所有特性都是属性,所以直接给属性赋值可以设置特性的值,如下所示。</p> <pre><code class="language-js">div.id = "someOtherId"; div.align = "left";</code></pre> <p>不过,像下面这样为DOM元素添加一个自定义的属性,该属性不会自动成为元素的特性。<br /> 要介绍的最后一个方法是removeAttribute(),这个方法用于彻底删除元素的特性。调用这个方法不仅会清除特性的值,而且也会从元素中完全删除特性,如下所示: </p> <pre><code class="language-js">div.removeAttribute("class");</code></pre> <p>这个方法并不常用,但在序列化DOM元素时,可以通过它来确切地指定要包含哪些特性。<br /> <strong>注意:</strong> IE6 及以前版本不支持 removeAttribute()。</p></li> </ul> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.07</h1> <p>阅读进度: 10.1.4 P270</p> <h3>1.1.4 attributes 属性</h3> <p>Element 类型是使用attributes属性的唯一一个DOM节点类型。attributes属性中包含一个NamedNodeMap,与 NodeList类似,也是一个“动态”的集合。元素的每一个特性都由一个Attr 节点表示,每个节点都保存在 NamedNodeMap 对象中。NamedNodeMap 对象拥有下列方法。 </p> <ol> <li>getNamedItem(name):返回 nodeName 属性等于 name 的节点;</li> <li>removeNamedItem(name):从列表中移除 nodeName 属性等于 name 的节点;</li> <li>setNamedItem(node):向列表中添加节点,以节点的 nodeName 属性为索引;</li> <li>item(pos):返回位于数字 pos 位置处的节点。 attributes 属性中包含一系列节点,每个节点的nodeName就是特性的名称,而节点的nodeValue就是特性的值。要取得元素的 id 特性,可以使用以下代码。 <pre><code class="language-js">var id = element.attributes.getNamedItem("id").nodeValue; </code></pre> <p>以下是使用方括号语法通过特性名称访问节点的简写方式。</p> <pre><code class="language-js">var id = element.attributes["id"].nodeValue; </code></pre> <p>也可以使用这种语法来设置特性的值,即先取得特性节点,然后再将其 nodeValue 设置为新值,如下所示。 </p> <pre><code class="language-js">element.attributes["id"].nodeValue = "someOtherId"; </code></pre> <p>调用 removeNamedItem()方法与在元素上调用 removeAttribute()方法的效果相同——直接删除具有给定名称的特性。下面的例子展示了两个方法间唯一的区别,即 removeNamedItem()返回表示被删除特性的 Attr 节点。 </p> <pre><code class="language-js">var oldAttr = element.attributes.removeNamedItem("id"); </code></pre> <p>最后,setNamedItem()是一个很不常用的方法,通过这个方法可以为元素添加一个新特性,为此需要为它传入一个特性节点,如下所示。</p> <pre><code class="language-js">element.attributes.setNamedItem(newAttr); </code></pre> <p>一般来说,由于前面介绍的 attributes 的方法不够方便,因此开发人员更多的会使用getAttribute()、removeAttribute()和 setAttribute()方法。<br /> 不过,如果想要遍历元素的特性,attributes 属性倒是可以派上用场。代码见p267 关于以上代码的运行结果,以下是两点必要的说明。</p> <ul> <li>针对 attributes 对象中的特性,不同浏览器返回的顺序不同。这些特性在 XML 或 HTML 代码中出现的先后顺序,不一定与它们出现在 attributes 对象中的顺序一致。</li> <li>IE7 及更早的版本会返回 HTML 元素中所有可能的特性,包括没有指定的特性。换句话说,返回 100 多个特性的情况会很常见。 针对 IE7 及更早版本中存在的问题,可以对上面的函数加以改进,让它只返回指定的特性。每个特 specified的属性,这个属性的值如果为true,则意味着要么是在HTML中指定了相应特性,要么是通过setAttribute()方法设置了该特性。 <h3>1.4.5 创建元素</h3> <p>使用 document.createElement()方法可以创建新元素。这个方法只接受一个参数,即要创建元素的标签名。这个标签名在 HTML 文档中不区分大小写,而在XML(包括XHTML)文档中,则是区分大小写的。例如,使用下面的代码可以创建一个<div>元素。</p> <pre><code class="language-js">var div = document.createElement("div"); </code></pre> <p>在使用 createElement()方法创建新元素的同时,也为新元素设置了 ownerDocuemnt 属性。此时,还可以操作元素的特性,为它添加更多子节点,以及执行其他操作。来看下面的例子。</p> <pre><code class="language-js">div.id = "myNewDiv"; div.className = "box"; </code></pre> <p>在新元素上设置这些特性只是给它们赋予了相应的信息。由于新元素尚未被添加到文档树中,因此设置这些特性不会影响浏览器的显示。要把新元素添加到文档树,可以使用 appendChild()、insertBefore()或replaceChild()方法。 在 IE 中可以以另一种方式使用 createElement(),即为这个方法传入完整的元素标签,也可以包含属性,如下面的例子所示。</p> <pre><code class="language-js">var div = document.createElement("<div id=\"myNewDiv\" class=\"box\"></div >"); </code></pre> <p>这种方式有助于避开在 IE7 及更早版本中动态创建元素的某些问题。下面是已知的一些这类问题。</p></li> </ul></li> <li>不能设置动态创建的<iframe>元素的 name 特性。</li> <li>不能通过表单的 reset()方法重设动态创建的<input>元素。</li> <li>动态创建的 type 特性值为"reset"的<buttou>元素重设不了表单。</li> <li>动态创建的一批 name 相同的单选按钮彼此毫无关系。name 值相同的一组单选按钮本来应该用于表示同一选项的不同值,但动态创建的一批这种单选按钮之间却没有这种关系。 上述所有问题都可以通过在createElement()中指定完整的HTML标签来解决。代码见p269 但是,由于这样的用法要求使用浏览器检测,因此我们建议只在需要避开 IE 及更早版本中上述某个问题的情况下使用。其他浏览器都不支持这种用法。 <h3>1.1.6 元素的子节点</h3> <p>元素可以有任意数目的子节点和后代节点,因为元素可以是其他元素的子节点。元素的childNodes属性中包含了它的所有子节点,这些子节点有可能是元素、文本节点、注释或处理指令。不同浏览器在看待这些节点方面存在显著的不同,以下面的代码为例。</p> <pre><code class="language-html"><ul id="myList"> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul> </code></pre> <p>如果是 IE 来解析这些代码,那么<code><ul></code>元素会有3个子节点,分别是3个<code><li></code>元素。但如果是在其他浏览器中,<code><ul></code>元素都会有7个元素,包括3个<code><li></code>元素和4个文本节点(表示<code><li></code>元素之间的空白符)。如果像下面这样将元素间的空白符删除,那么所有浏览器都会返回相同数目的子节点。<br /> 一定不要忘记浏览器间的这一差别。这意味着在执行某项操作以前,通常都要先检查一下 nodeTpye 属性,如下面的例子所示。</p> <pre><code class="language-js">for (var i=0, len=element.childNodes.length; i < len; i++){ if (element.childNodes[i].nodeType == 1){ //执行某些操作 } } </code></pre> <p>这个例子会循环遍历特定元素的每一个子节点,然后只在子节点的 nodeType 等于 1(表示是元素节点)的情况下,才会执行某些操作。<br /> 元素也支持getElementsByTagName()方法。在通过元素调用这个方法时,除了搜索起点是当前元素之外,其他方面都跟通过document调用这个方法相同,因此结果只会返回当前元素的后代。例如,要想取得前面<code><ul></code>元素中包含的所有<code><li></code>元素,可以使用下列代码。</p> <pre><code class="language-js">var ul = document.getElementById("myList"); var items = ul.getElementsByTagName("li"); </code></pre> <p>要注意的是,这里<code><ul></code>的后代中只包含直接子元素。不过,如果它包含更多层次的后代元素,那么各个层次中包含的<code><li></code>元素也都会返回。</p></li> </ol> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.08</h1> <p>阅读进度: 10.1.5 P273</p> <h3>1.4 Text类型</h3> <p>文本节点由 Text 类型表示,包含的是可以照字面解释的纯文本内容。纯文本中可以包含转义后的HTML 字符,但不能包含 HTML 代码。Text 节点具有以下特征:</p> <ol> <li>nodeType 的值为 3;</li> <li>nodeName 的值为"#text";</li> <li>nodeValue 的值为节点所包含的文本;</li> <li>parentNode 是一个 Element;</li> <li>不支持(没有)子节点。 可以通过 nodeValue 属性或 data 属性访问 Text 节点中包含的文本,这两个属性中包含的值相同。对 nodeValue 的修改也会通过 data 反映出来,反之亦然。使用下列方法可以操作节点中的文本。 <ul> <li>appendData(text):将 text 添加到节点的末尾。</li> <li>deleteData(offset, count):从 offset 指定的位置开始删除 count 个字符。</li> <li>insertData(offset, text):在 offset 指定的位置插入 text。</li> <li>replaceData(offset, count, text):用 text 替换从 offset 指定的位置开始到 offset+count 为止处的文本。</li> <li>splitText(offset):从 offset 指定的位置将当前文本节点分成两个文本节点。</li> <li>substringData(offset, count):提取从 offset 指定的位置开始到 offset+count 为止 处的字符串。 除了这些方法之外,文本节点还有一个 length 属性,保存着节点中字符的数目。而且,nodeValue.length 和 data.length 中也保存着同样的值。 在默认情况下,每个可以包含内容的元素最多只能有一个文本节点,而且必须确实有内容存在。 <pre><code class="language-html"><!-- 没有内容,也就没有文本节点 --> <div></div> <!-- 有空格,因而有一个文本节点 --> <div> </div> <!-- 有内容,因而有一个文本节点 --> <div>Hello World!</div> </code></pre> <p>上面代码给出的第一个<code><div></code>元素没有内容,因此也就不存在文本节点。开始与结束标签之间只要存在内容,就会创建一个文本节点。因此,第二个<code><div></code>元素中虽然只包含一个空格,但仍然有一个文本子节点;文本节点的 nodeValue 值是一个空格。第三个<code><div></code>也有一个文本节点,其 nodeValue 的值为"Hello World!"。可以使用以下代码来访问这些文本子节点。</p> <pre><code class="language-js">var textNode = div.firstChild; //或者 div.childNodes[0] div.firstChild.nodeValue = "Some other message"; </code></pre> <p>如果这个文本节点当前存在于文档树中,那么修改文本节点的结果就会立即得到反映。另外,在修改文本节点时还要注意,此时的字符串会经过 HTML(或 XML,取决于文档类型)编码。换句话说,小于号、大于号或引号都会像下面的例子一样被转义。</p> <pre><code class="language-js">//输出结果是"Some &lt;strong&gt;other&lt;/strong&gt; message" div.firstChild.nodeValue = "Some <strong>other</strong> message"; </code></pre> <h3>1.4.1 创建文本节点</h3> <p>可以使用 document.createTextNode()创建新文本节点,这个方法接受一个参数——要插入节点中的文本。与设置已有文本节点的值一样,作为参数的文本也将按照 HTML 或 XML 的格式进行编码。 </p> <pre><code class="language-js">var textNode = document.createTextNode("<strong>Hello</strong> world!"); </code></pre> <p>在创建新文本节点的同时,也会为其设置 ownerDocument 属性。不过,除非把新节点添加到文档树中已经存在的节点中,否则我们不会在浏览器窗口中看到新节点。下面的代码会创建一个<code><div></code>元素并向其中添加一条消息。</p> <pre><code class="language-js">var element = document.createElement("div"); element.className = "message"; var textNode = document.createTextNode("Hello world!"); element.appendChild(textNode); document.body.appendChild(element); </code></pre> <h3>1.4.2 规范化文本节点</h3> <p>DOM 文档中存在相邻的同胞文本节点很容易导致混乱,因为分不清哪个文本节点表示哪个字符串。另外,DOM 文档中出现相邻文本节点的情况也不在少数,于是就催生了一个能够将相邻文本节点合并的方法。这个方法是由 Node 类型定义的(因而在所有节点类型中都存在),名叫 normalize()。如果在一个包含两个或多个文本节点的父元素上调用 normalize()方法,则会将所有文本节点合并成一个节点,结果节点的 nodeValue 等于将合并前每个文本节点的 nodeValue 值拼接起来的值。来看一个例子。 </p> <pre><code class="language-js">var element = document.createElement("div"); element.className = "message"; var textNode = document.createTextNode("Hello world!"); element.appendChild(textNode); var anotherTextNode = document.createTextNode("Yippee!"); element.appendChild(anotherTextNode); document.body.appendChild(element); alert(element.childNodes.length); //2 element.normalize(); alert(element.childNodes.length); //1 alert(element.firstChild.nodeValue); // "Hello world!Yippee!" </code></pre> <p>浏览器在解析文档时永远不会创建相邻的文本节点。这种情况只会作为执行 DOM 操作的结果出现。</p> <h3>1.4.3 分割文本节点</h3> <p>Text 类型提供了一个作用与 normalize()相反的方法:splitText()。这个方法会将一个文本节点分成两个文本节点,即按照指定的位置分割 nodeValue 值。原来的文本节点将包含从开始到指定位置之前的内容,新文本节点将包含剩下的文本。这个方法会返回一个新文本节点,该节点与原节点的parentNode 相同。来看下面的例子。</p> <pre><code class="language-js">var element = document.createElement("div"); element.className = "message"; var textNode = document.createTextNode("Hello world!"); element.appendChild(textNode); document.body.appendChild(element); var newNode = element.firstChild.splitText(5); alert(element.firstChild.nodeValue); //"Hello" alert(newNode.nodeValue); //" world!" alert(element.childNodes.length); //2 </code></pre> <p>在这个例子中,包含"Hello world!"的文本节点被分割为两个文本节点,从位置 5 开始。位置 5是"Hello"和"world!"之间的空格,因此原来的文本节点将包含字符串"Hello",而新文本节点将包含文本"world!"(包含空格)。分割文本节点是从文本节点中提取数据的一种常用 DOM 解析技术。</p></li> </ul></li> </ol> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.09</h1> <p>阅读进度: 10.2 P277</p> <h3>1.5 Comment 类型</h3> <p>注释在 DOM 中是通过 Comment 类型来表示的。Comment 节点具有下列特征:</p> <ol> <li>nodeType 的值为 8;</li> <li>nodeName 的值为"#comment";</li> <li>nodeValue 的值是注释的内容;</li> <li>parentNode 可能是 Document 或 Element;</li> <li>不支持(没有)子节点。 Comment 类型与 Text 类型继承自相同的基类,因此它拥有除 splitText()之外的所有字符串操作方法。与 Text 类型相似,也可以通过 nodeValue 或 data 属性来取得注释的内容。<br /> 注释节点可以通过其父节点来访问,以下面的代码为例。 <pre><code class="language-html"><div id="myDiv"><!--A comment --></div> </code></pre> <p>在此,注释节点是<code><div></code>元素的一个子节点,因此可以通过下面的代码来访问它。 </p> <pre><code class="language-js">var div = document.getElementById("myDiv"); var comment = div.firstChild; alert(comment.data); //"A comment" </code></pre> <p>另外,使用 document.createComment()并为其传递注释文本也可以创建注释节点,如下面的例子所示。</p> <pre><code class="language-js">var comment = document.createComment("A comment "); </code></pre> <p>浏览器不会识别位于<code></html></code>标签后面的注释。如果要访问注释节点,一定要保证它们是<code><html></code>元素的后代(即位于<code><html></code>和<code></html></code>之间)。 <strong>注意:</strong> 在 IE8 中,注释节点被视作标签名为"!"的元素。也就是说,使用getElementsByTagName()可以取得注释节点。尽管 IE9 没有把注释当成元素,但它仍然通过一个名为 HTMLCommentElement 的构造函数来表示注释。</p> <h3>1.6 CDATASection类型</h3> <p>CDATASection 类型只针对基于 XML 的文档,表示的是 CDATA 区域。与 Comment 类似,CDATASection 类型继承自 Text 类型,因此拥有除 splitText()之外的所有字符串操作方法。CDATASection 节点具有下列特征:</p></li> <li>nodeType 的值为 4;</li> <li>nodeName 的值为"#cdata-section";</li> <li>nodeValue 的值是 CDATA 区域中的内容;</li> <li>parentNode 可能是 Document 或 Element;</li> <li>不支持(没有)子节点。 CDATA 区域只会出现在 XML 文档中,因此多数浏览器都会把 CDATA 区域错误地解析为 Comment或 Element。以下面的代码为例: <pre><code class="language-html"><div id="myDiv"><![CDATA[This is some content.]]></div> </code></pre> <p>这个例子中的<code><div></code>元素应该包含一个 CDATASection 节点。可是,四大主流浏览器无一能够这样解析它。即使对于有效的 XHTML 页面,浏览器也没有正确地支持嵌入的 CDATA 区域。<br /> 在真正的 XML 文档中,可以使用 document.createCDataSection()来创建 CDATA 区域,只需为其传入节点的内容即可。 </p> <h3>1.7 DocumentType类型</h3> <p>DocumentType 类型在 Web 浏览器中并不常用,仅有 Firefox、Safari 和 Opera 支持它。DocumentType 包含着与文档的 doctype 有关的所有信息,它具有下列特征:</p></li> <li>nodeType 的值为 10;</li> <li>nodeName 的值为 doctype 的名称;</li> <li>nodeValue 的值为 null;</li> <li>parentNode 是 Document;</li> <li>不支持(没有)子节点。 在 DOM1 级中,DocumentType 对象不能动态创建,而只能通过解析文档代码的方式来创建。支持它的浏览器会把 DocumentType 对象保存在 document.doctype 中 。 DOM1 级描述了DocumentType 对象的 3 个属性:name、entities 和 notations。其中,name 表示文档类型的名称;entities 是由文档类型描述的实体的 NamedNodeMap 对象;notations 是由文档类型描述的符号的NamedNodeMap 对象。通常,浏览器中的文档使用的都是 HTML 或 XHTML 文档类型,因而 entities和 notations 都是空列表(列表中的项来自行内文档类型声明)。但不管怎样,只有 name 属性是有用的。这个属性中保存的是文档类型的名称,也就是出现在<!DOCTYPE 之后的文本。以下面严格型 HTML4.01 的文档类型声明为例: <pre><code class="language-html"><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> </code></pre> <p>DocumentType 的 name 属性中保存的就是"HTML":</p> <pre><code class="language-js">alert(document.doctype.name); //"HTML" </code></pre> <h3>1.8 DocumentFragment类型</h3> <p>在所有节点类型中,只有 DocumentFragment 在文档中没有对应的标记。DOM 规定文档片段(document fragment)是一种“轻量级”的文档,可以包含和控制节点,但不会像完整的文档那样占用额外的资源。DocumentFragment 节点具有下列特征:</p></li> <li>nodeType 的值为 11;</li> <li>nodeName 的值为"#document-fragment";</li> <li>nodeValue 的值为 null;</li> <li>parentNode 的值为 null;</li> <li>子节点可以是 Element、ProcessingInstruction、Comment、Text、CDATASection 或EntityReference。 虽然不能把文档片段直接添加到文档中,但可以将它作为一个“仓库”来使用,即可以在里面保存将来可能会添加到文档中的节点。要创建文档片段,可以使用 document.createDocumentFragment()方法,如下所示: <pre><code class="language-js">var fragment = document.createDocumentFragment(); </code></pre> <p>文档片段继承了 Node 的所有方法,通常用于执行那些针对文档的 DOM 操作。如果将文档中的节点添加到文档片段中,就会从文档树中移除该节点,也不会从浏览器中再看到该节点。添加到文档片段中的新节点同样也不属于文档树。可以通过 appendChild()或 insertBefore()将文档片段中内容添加到文档中。在将文档片段作为参数传递给这两个方法时,实际上只会将文档片段的所有子节点添加到相应位置上;文档片段本身永远不会成为文档树的一部分。来看下面的 HTML 示例代码:</p> <pre><code class="language-html"><ul id="myList"></ul> </code></pre> <p>假设我们想为这个<code><ul></code>元素添加 3 个列表项。如果逐个地添加列表项,将会导致浏览器反复渲染(呈现)新信息。为避免这个问题,可以像下面这样使用一个文档片段来保存创建的列表项,然后再一次性将它们添加到文档中。</p> <pre><code class="language-js">var fragment = document.createDocumentFragment(); var ul = document.getElementById("myList"); var li = null; for (var i=0; i < 3; i++){ li = document.createElement("li"); li.appendChild(document.createTextNode("Item " + (i+1))); fragment.appendChild(li); } ul.appendChild(fragment); </code></pre> <h3>1.9 Attr类型</h3> <p>元素的特性在 DOM 中以 Attr 类型来表示。在所有浏览器中(包括 IE8),都可以访问 Attr 类型的构造函数和原型。从技术角度讲,特性就是存在于元素的 attributes 属性中的节点。特性节点具有下列特征:</p></li> <li>nodeType 的值为 2;</li> <li>nodeName 的值是特性的名称;</li> <li>nodeValue 的值是特性的值;</li> <li>parentNode 的值为 null;</li> <li>在 HTML 中不支持(没有)子节点;</li> <li>在 XML 中子节点可以是 Text 或 EntityReference。 尽管它们也是节点,但特性却不被认为是 DOM 文档树的一部分。开发人员最常使用的是 getAttribute()、setAttribute()和remveAttribute()方法,很少直接引用特性节点。 Attr 对象有 3 个属性:name、value 和 specified。其中,name 是特性名称(与 nodeName 的值相同),value 是特性的值(与 nodeValue 的值相同),而 specified 是一个布尔值,用以区别特性是在代码中指定的,还是默认的。 使用 document.createAttribute()并传入特性的名称可以创建新的特性节点。例如,要为元素添加 align 特性,可以使用下列代码: <pre><code class="language-js">var attr = document.createAttribute("align"); attr.value = "left"; element.setAttributeNode(attr); alert(element.attributes["align"].value); //"left" alert(element.getAttributeNode("align").value); //"left" alert(element.getAttribute("align")); //"left" </code></pre> <p>为了将新创建的特性添加到元素中,必须使用元素的 setAttributeNode()方法。添加特性之后,可以通过下列任何方式访问该特性:attributes 属性、getAttributeNode()方法以及 getAttribute()方法。其中,attributes和 getAttributeNode()都会返回对应特性的 Attr 节点,而 getAttribute()则只返回特性的值。</p></li> </ol> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.10</h1> <p>阅读进度: 10.2.3 P281</p> <h2>2.DOM 操作技术</h2> <h3>2.1 动态脚本</h3> <p>使用<code><script></code>元素可以向页面中插入 JavaScript 代码,一种方式是通过其 src 特性包含外部文件,另一种方式就是用这个元素本身来包含代码。而这一节要讨论的动态脚本,指的是在页面加载时不存在,但将来的某一时刻通过修改 DOM 动态添加的脚本。跟操作 HTML 元素一样,创建动态脚本也有两种方式:插入外部文件和直接插入 JavaScript 代码。 动态加载的外部 JavaScript 文件能够立即运行,比如下面的<code><script></code>元素:</p> <pre><code class="language-html"><script type="text/javascript" src="client.js"></script> </code></pre> <p>创建这个节点的 DOM 代码如下所示:</p> <pre><code class="language-js">var script = document.createElement("script"); script.type = "text/javascript"; script.src = "client.js"; document.body.appendChild(script); </code></pre> <p>显然,这里的 DOM 代码如实反映了相应的 HTML 代码。不过,在执行最后一行代码把<code><script></code>元素添加到页面中之前,是不会下载外部文件的。也可以把这个元素添加到<code><head></code>元素中,效果相同。整个过程可以使用下面的函数来封装:</p> <pre><code class="language-js">function loadScript(url){ var script = document.createElement("script"); script.type = "text/javascript"; script.src = url; document.body.appendChild(script); } </code></pre> <p>加载完成后,就可以在页面中的其他地方使用这个脚本了。问题只有一个:怎么知道脚本加载完成呢?遗憾的是,并没有什么标准方式来探知这一点。不过,与此相关的一些事件倒是可以派上用场,但要取决于所用的浏览器。 另一种指定 JavaScript 代码的方式是行内方式,如下面的例子所示:</p> <pre><code class="language-js"><script type="text/javascript"> function sayHi(){ alert("hi"); } </script> </code></pre> <p>从逻辑上讲,下面的 DOM 代码是有效的:</p> <pre><code class="language-js">var script = document.createElement("script"); script.type = "text/javascript"; script.appendChild(document.createTextNode("function sayHi(){alert('hi');}")); document.body.appendChild(script); </code></pre> <p>在 Firefox、Safari、Chrome 和 Opera 中,这些 DOM 代码可以正常运行。但在 IE 中,则会导致错误。IE 将<code><script></code>视为一个特殊的元素,不允许 DOM 访问其子节点。不过,可以使用<code><script></code>元素的text 属性来指定 JavaScript 代码,像下面的例子这样:</p> <pre><code class="language-js">var script = document.createElement("script"); script.type = "text/javascript"; script.text = "function sayHi(){alert('hi');}"; document.body.appendChild(script); </code></pre> <p>这里,首先尝试标准的 DOM 文本节点方法,因为除了 IE(在 IE 中会导致抛出错误),所有浏览器都支持这种方式。如果这行代码抛出了错误,那么说明是 IE,于是就必须使用 text 属性了。整个过程可以用以下函数来表示:</p> <pre><code class="language-js">function loadScriptString(code){ var script = document.createElement("script"); script.type = "text/javascript"; try { script.appendChild(document.createTextNode(code)); } catch (ex){ script.text = code; } document.body.appendChild(script); } </code></pre> <p>以这种方式加载的代码会在全局作用域中执行,而且当脚本执行后将立即可用。实际上,这样执行代码与在全局作用域中把相同的字符串传递给 eval()是一样的。</p> <h3>2.2 动态样式</h3> <p>能够把 CSS 样式包含到 HTML 页面中的元素有两个。其中,<code><link></code>元素用于包含来自外部的文件,而<code><style></code>元素用于指定嵌入的样式。与动态脚本类似,所谓动态样式是指在页面刚加载时不存在的样式;动态样式是在页面加载完成后动态添加到页面中的。</p> <pre><code class="language-html"><link rel="stylesheet" type="text/css" href="styles.css"> </code></pre> <p>使用 DOM 代码可以很容易地动态创建出这个元素:</p> <pre><code class="language-js">var link = document.createElement("link"); link.rel = "stylesheet"; link.type = "text/css"; link.href = "style.css"; var head = document.getElementsByTagName("head")[0]; head.appendChild(link); </code></pre> <p>需要注意的是,必须将<code><link></code>元素添加到<code><head></code>而不是<code><body></code>元素,才能保证在所有浏览器中的行为一致。 加载外部样式文件的过程是异步的,也就是加载样式与执行 JavaScript 代码的过程没有固定的次序。一般来说,知不知道样式已经加载完成并不重要;不过,也存在几种利用事件来检测这个过程是否完成的技术。 按照相同的逻辑,下列 DOM 代码应该是有效的:</p> <pre><code class="language-js">var style = document.createElement("style"); style.type = "text/css"; style.appendChild(document.createTextNode("body{background-color:red}")); var head = document.getElementsByTagName("head")[0]; head.appendChild(style); </code></pre> <p>以上代码可以在 Firefox、Safari、Chrome 和 Opera 中运行,在 IE 中则会报错。IE 将<code><style></code>视为 一个特殊的、与<code><script></code>类似的节点,不允许访问其子节点。事实上,IE 此时抛出的错误与向<code><script></code>元素添加子节点时抛出的错误相同。解决 IE 中这个问题的办法,就是访问元素的 styleSheet 属性,该属性又有一个 cssText 属性,可以接受 CSS 代码。如下:</p> <pre><code class="language-js">var style = document.createElement("style"); style.type = "text/css"; try{ style.appendChild(document.createTextNode("body{background-color:red}")); } catch (ex){ style.styleSheet.cssText = "body{background-color:red}"; } var head = document.getElementsByTagName("head")[0]; head.appendChild(style); </code></pre> <p><strong>注意:</strong> 如果专门针对 IE 编写代码,务必小心使用 styleSheet.cssText 属性。在重用同一个<code><style></code>元素并再次设置这个属性时,有可能会导致浏览器崩溃。同样,将cssText 属性设置为空字符串也可能导致浏览器崩溃。</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.11</h1> <p>阅读进度: 11章 P286</p> <h3>2.3 操作表格</h3> <p><code><table></code>元素是 HTML 中最复杂的结构之一。要想创建表格,一般都必须涉及表示表格行、单元格、表头等方面的标签。假设我们要使用 DOM 来创建下面的 HTML 表格。</p> <pre><code class="language-html"><table border="1" width="100%"> <tbody> <tr> <td>Cell 1,1</td> <td>Cell 2,1</td> </tr> <tr> <td>Cell 1,2</td> <td>Cell 2,2</td> </tr> </tbody> </table> </code></pre> <p>要使用核心 DOM 方法创建这些元素,得需要像下面这么多的代码:</p> <pre><code class="language-js">//创建 table var table = document.createElement("table"); table.border = 1; table.width = "100%"; //创建 tbody var tbody = document.createElement("tbody"); table.appendChild(tbody); //创建第一行 var row1 = document.createElement("tr"); tbody.appendChild(row1); var cell1_1 = document.createElement("td"); cell1_1.appendChild(document.createTextNode("Cell 1,1")); row1.appendChild(cell1_1); var cell2_1 = document.createElement("td"); cell2_1.appendChild(document.createTextNode("Cell 2,1")); row1.appendChild(cell2_1); //创建第二行 var row2 = document.createElement("tr"); tbody.appendChild(row2); var cell1_2 = document.createElement("td"); cell1_2.appendChild(document.createTextNode("Cell 1,2")); row2.appendChild(cell1_2); var cell2_2= document.createElement("td"); cell2_2.appendChild(document.createTextNode("Cell 2,2")); row2.appendChild(cell2_2); //将表格添加到文档主体中 document.body.appendChild(table); </code></pre> <p>显然,DOM 代码很长,还有点不太好懂。为了方便构建表格,HTML DOM 还为<code><table></code>、<code><tbody></code> 和<code><tr></code>元素添加了一些属性和方法。 为<code><table></code>元素添加的属性和方法如下。见p282 使用这些属性和方法,可以极大地减少创建表格所需的代码数量。例如,使用这些属性和方法可以将前面的代码重写如下(加阴影的部分是重写后的代码)。</p> <pre><code class="language-js">//创建 table var table = document.createElement("table"); table.border = 1; table.width = "100%"; //创建 tbody var tbody = document.createElement("tbody"); table.appendChild(tbody); //创建第一行 tbody.insertRow(0); tbody.rows[0].insertCell(0); tbody.rows[0].cells[0].appendChild(document.createTextNode("Cell 1,1")); tbody.rows[0].insertCell(1); tbody.rows[0].cells[1].appendChild(document.createTextNode("Cell 2,1")); //创建第二行 tbody.insertRow(1); tbody.rows[1].insertCell(0); tbody.rows[1].cells[0].appendChild(document.createTextNode("Cell 1,2")); tbody.rows[1].insertCell(1); tbody.rows[1].cells[1].appendChild(document.createTextNode("Cell 2,2")); //将表格添加到文档主体中 document.body.appendChild(table); </code></pre> <p>创建单元格的方式也十分相似,即通过<code><tr></code>元素调用 insertCell()方法并传入放置单元格的位置。然后,就可以通过 tbody.rows[0].cells[0]来引用新插入的单元格,因为新创建的单元格被插入到了这一行的位置 0 上。</p> <h3>2.4 使用NodeList</h3> <p>理解 NodeList 及其“近亲”NamedNodeMap 和 HTMLCollection,是从整体上透彻理解 DOM 的关键所在。这三个集合都是“动态的”;换句话说,每当文档结构发生变化时,它们都会得到更新。因此,它们始终都会保存着最新、最准确的信息。从本质上说,所有 NodeList 对象都是在访问 DOM 文档时实时运行的查询。<br /> 下列代码会导致无限循环:</p> <pre><code class="language-js">var divs = document.getElementsByTagName("div"), i, div; for (i=0; i < divs.length; i++){ div = document.createElement("div"); document.body.appendChild(div); } </code></pre> <p>第一行代码会取得文档中所有<code><div></code>元素的 HTMLCollection。由于这个集合是“动态的”,因此只要有新<code><div></code>元素被添加到页面中,这个元素也会被添加到该集合中。浏览器不会将创建的所有集合都保存在一个列表中,而是在下一次访问集合时再更新集合。 在遇到上例中所示的循环代码时,就会导致一个有趣的问题。每次循环都要对条件 i < divs.length 求值,意味着会运行取得所有<code><div></code>元素的查询。考虑到循环体每次都会创建一个新<code><div></code>元素并将其添加到文档中,因此divs.length 的值在每次循环后都会递增。既然 i 和 divs.length 每次都会同时递增,结果它们的值永远也不会相等。 如果想要迭代一个 NodeList,最好是使用 length 属性初始化第二个变量,然后将迭代器与该变量进行比较,如下面的例子所示:</p> <pre><code class="language-js">var divs = document.getElementsByTagName("div"), i, len, div; for (i=0, len=divs.length; i < len; i++){ div = document.createElement("div"); document.body.appendChild(div); } </code></pre> <p><strong>注意:</strong> 应该尽量减少访问 NodeList 的次数。因为每次访问 NodeList,都会运行一次基于文档的查询。所以,可以考虑将从 NodeList 中取得的值缓存起来。</p> <h2>3 小结</h2> <p>DOM 是语言中立的 API,用于访问和操作 HTML 和 XML 文档。DOM1 级将 HTML 和 XML 文档形象地看作一个层次化的节点树,可以使用 JavaScript 来操作这个节点树,进而改变底层文档的外观和结构。 DOM 由各种节点构成,简要总结如下。 </p> <ul> <li>最基本的节点类型是 Node,用于抽象地表示文档中一个独立的部分;所有其他类型都继承自Node。</li> <li>Document 类型表示整个文档,是一组分层节点的根节点。在 JavaScript 中,document 对象是Document 的一个实例。使用 document 对象,有很多种方式可以查询和取得节点。</li> <li>Element 节点表示文档中的所有 HTML 或 XML 元素,可以用来操作这些元素的内容和特性。</li> <li>另外还有一些节点类型,分别表示文本内容、注释、文档类型、CDATA 区域和文档片段。<br /> DOM 操作往往是 JavaScript 程序中开销最大的部分,而因访问 NodeList 导致的问题为最多。NodeList 对象都是“动态的”,这就意味着每次访问NodeList 对象,都会运行一次查询。有鉴于此,最好的办法就是尽量减少 DOM 操作。</li> </ul> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.13</h1> <p>阅读进度: 11.3 P289</p> <h2>1. 选择符API</h2> <p>Selectors API(www.w3.org/TR/selectors-api/)是由 W3C 发起制定的一个标准,致力于让浏览器原生支持 CSS 查询。<br /> Selectors API Level 1 的核心是两个方法:querySelector()和 querySelectorAll()。在兼容的浏览器中,可以通过 Document 及 Element 类型的实例调用它们。目前已完全支持 Selectors API Level 1的浏览器有 IE 8+、Firefox 3.5+、Safari 3.1+、Chrome 和 Opera 10+。</p> <h3>1.1 querySelector()方法</h3> <p>querySelector()方法接收一个 CSS 选择符,返回与该模式匹配的第一个元素,如果没有找到匹配的元素,返回 null。请看下面的例子。 </p> <pre><code class="language-js">//取得 body 元素 var body = document.querySelector("body"); //取得 ID 为"myDiv"的元素 var myDiv = document.querySelector("#myDiv"); //取得类为"selected"的第一个元素 var selected = document.querySelector(".selected"); //取得类为"button"的第一个图像元素 var img = document.body.querySelector("img.button"); </code></pre> <p>通过 Document 类型调用 querySelector()方法时,会在文档元素的范围内查找匹配的元素。而通过 Element 类型调用 querySelector()方法时,只会在该元素后代元素的范围内查找匹配的元素。</p> <h3>1.2 querySelectorAll()方法</h3> <p>querySelectorAll()方法接收的参数与 querySelector()方法一样,都是一个 CSS 选择符,但返回的是所有匹配的元素而不仅仅是一个元素。这个方法返回的是一个 NodeList 的实例。<br /> 具体来说,返回的值实际上是带有所有属性和方法的 NodeList,而其底层实现则类似于一组元素的快照,而非不断对文档进行搜索的动态查询。这样实现可以避免使用 NodeList 对象通常会引起的大多数性能问题。 与querySelector()类似,能够调用 querySelectorAll()方法的类型包括 Document、DocumentFragment 和 Element。下面是几个例子。 </p> <pre><code class="language-js">//取得某<div>中的所有<em>元素(类似于 getElementsByTagName("em")) var ems = document.getElementById("myDiv").querySelectorAll("em"); //取得类为"selected"的所有元素 var selecteds = document.querySelectorAll(".selected"); //取得所有<p>元素中的所有<strong>元素 var strongs = document.querySelectorAll("p strong"); </code></pre> <p>要取得返回的 NodeList 中的每一个元素,可以使用 item()方法,也可以使用方括号语法,比如: </p> <pre><code class="language-js">var i, len, strong; for (i=0, len=strongs.length; i < len; i++){ strong = strongs[i]; //或者 strongs.item(i) strong.className = "important"; } </code></pre> <h3>1.3 matchesSelector()方法</h3> <p>Selectors API Level 2 规范为 Element 类型新增了一个方法 matchesSelector()。这个方法接收一个参数,即 CSS 选择符,如果调用元素与该选择符匹配,返回 true;否则,返回 false。看例子。 </p> <pre><code class="language-js">if (document.body.matchesSelector("body.page1")){ //true } </code></pre> <p>截至 2011 年年中,还没有浏览器支持 matchesSelector()方法;不过,也有一些实验性的实现。IE 9+通过 msMatchesSelector()支持该方法,Firefox 3.6+通过 mozMatchesSelector()支持该方法,Safari 5+和 Chrome 通过 webkitMatchesSelector()支持该方法。因此,如果你想使用这个方法,最好是编写一个包装函数。 </p> <pre><code class="language-js">function matchesSelector(element, selector){ if (element.matchesSelector){ return element.matchesSelector(selector); } else if (element.msMatchesSelector){ return element.msMatchesSelector(selector); } else if (element.mozMatchesSelector){ return element.mozMatchesSelector(selector); } else if (element.webkitMatchesSelector){ return element.webkitMatchesSelector(selector); } else { throw new Error("Not supported."); } } if (matchesSelector(document.body, "body.page1")){ //执行操作 } </code></pre> <h2>2. 元素遍历</h2> <p>对于元素间的空格,IE9 及之前版本不会返回文本节点,而其他所有浏览器都会返回文本节点。这样,就导致了在使用 childNodes 和 firstChild 等属性时的行为不一致。为了弥补这一差异,而同时又保持 DOM 规范不变,Element Traversal 规范(www.w3.org/TR/ElementTraversal/)新定义了一组属性。<br /> Element Traversal API 为 DOM 元素添加了以下 5 个属性。 </p> <ol> <li>childElementCount:返回子元素(不包括文本节点和注释)的个数。</li> <li>firstElementChild:指向第一个子元素;firstChild 的元素版。</li> <li>lastElementChild:指向最后一个子元素;lastChild 的元素版。</li> <li>previousElementSibling:指向前一个同辈元素;previousSibling 的元素版。 5 .nextElementSibling:指向后一个同辈元素;nextSibling 的元素版。 过去,要跨浏览器遍历某元素的所有子元素,需要像下面这样写代码。 <pre><code class="language-js">var i, len, child = element.firstChild; while(child != element.lastChild){ if (child.nodeType == 1){ //检查是不是元素 processChild(child); } child = child.nextSibling; } </code></pre> <p>使用 Element Traversal 新增的元素,代码会更简洁。</p> <pre><code class="language-js">var i, len, child = element.firstElementChild; while(child != element.lastElementChild){ processChild(child); //已知其是元素 child = child.nextElementSibling; } </code></pre> <p><strong>注意:</strong> 支持 Element Traversal 规范的浏览器有 IE 9+、Firefox 3.5+、Safari 4+、Chrome 和 Opera 10+。</p></li> </ol> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.14</h1> <p>阅读进度: 11.3.6 P294</p> <h2>3. HTML5</h2> <p>HTML5 规范则围绕如何使用新增标记定义了大量 JavaScript API。其中一些 API 与 DOM 重叠,定义了浏览器应该支持的 DOM 扩展。 </p> <h3>3.1 与类相关的扩充</h3> <p>让开发人员适应并增加对 class 属性的新认识,HTML5 新增了很多 API,致力于简化 CSS 类的用法。 </p> <ol> <li>getElementsByClassName()方法 HTML5 添加的 getElementsByClassName()方法是最受人欢迎的一个方法,可以通过 document对象及所有 HTML 元素调用该方法。 getElementsByClassName()方法接收一个参数,即一个包含一或多个类名的字符串,返回带有指定类的所有元素的 NodeList。传入多个类名时,类名的先后顺序不重要。来看下面的例子。 <pre><code class="language-js">//取得所有类中包含"username"和"current"的元素,类名的先后顺序无所谓 var allCurrentUsernames = document.getElementsByClassName("username current"); //取得 ID 为"myDiv"的元素中带有类名"selected"的所有元素 var selected = document.getElementById("myDiv").getElementsByClassName("selected");</code></pre> <p>调用这个方法时,只有位于调用元素子树中的元素才会返回。在 document 对象上调用getElementsByClassName()始终会返回与类名匹配的所有元素,在元素上调用该方法就只会返回后代元素中匹配的元素。 使用这个方法可以更方便地为带有某些类的元素添加事件处理程序,从而不必再局限于使用 ID 或标签名。不过别忘了,因为返回的对象是 NodeList,所以使用这个方法与使用 getElementsByTagName()以及其他返回 NodeList 的 DOM 方法都具有同样的性能问题。 <strong>兼容:</strong> 支持 getElementsByClassName()方法的浏览器有 IE 9+、Firefox 3+、Safari 3.1+、Chrome 和Opera 9.5+。</p></li> <li>classList 属性 在操作类名时,需要通过 className 属性添加、删除和替换类名。因为 className 中是一个字符串,所以即使只修改字符串一部分,也必须每次都设置整个字符串的值。比如,以下面的 HTML 代码为例。 <pre><code class="language-html"><div class="bd user disabled">...</div> </code></pre> <p>这个<code><div></code>元素一共有三个类名。要从中删除一个类名,需要把这三个类名拆开,删除不想要的那个,然后再把其他类名拼成一个新字符串。请看下面的例子。</p> <pre><code class="language-js">//删除"user"类 //首先,取得类名字符串并拆分成数组 var classNames = div.className.split(/\s+/); //找到要删的类名 var pos = -1, i, len; for (i=0, len=classNames.length; i < len; i++){ if (classNames[i] == "user"){ pos = i; break; } } //删除类名 classNames.splice(i,1); //把剩下的类名拼成字符串并重新设置 div.className = classNames.join(" "); </code></pre> <p>HTML5 新增了一种操作类名的方式,可以让操作更简单也更安全,那就是为所有元素添加classList 属性。这个 classList 属性是新集合类型 DOMTokenList 的实例。与其他 DOM 集合类似,DOMTokenList 有一个表示自己包含多少元素的 length 属性,而要取得每个元素可以使用 item()方法,也可以使用方括号语法。此外,这个新类型还定义如下方法。</p> <ul> <li>add(value):将给定的字符串值添加到列表中。如果值已经存在,就不添加了。</li> <li>contains(value):表示列表中是否存在给定的值,如果存在则返回 true,否则返回 false。</li> <li>remove(value):从列表中删除给定的字符串。</li> <li>toggle(value):如果列表中已经存在给定的值,删除它;如果列表中没有给定的值,添加它。 这样,前面那么多行代码用下面这一行代码就可以代替了: <pre><code class="language-js">div.classList.remove("user");</code></pre> <p>以上代码能够确保其他类名不受此次修改的影响。其他方法也能极大地减少类似基本操作的复杂性,如下面的例子所示。</p> <pre><code class="language-js">//删除"disabled"类 div.classList.remove("disabled"); //添加"current"类 div.classList.add("current"); //切换"user"类 div.classList.toggle("user"); //确定元素中是否包含既定的类名 if (div.classList.contains("bd") && !div.classList.contains("disabled")){ //执行操作 ) //迭代类名 for (var i=0, len=div.classList.length; i < len; i++){ doSomething(div.classList[i]); }</code></pre> <h3>3.2 焦点管理</h3> <p>HTML5 也添加了辅助管理 DOM 焦点的功能。首先就是 document.activeElement 属性,这个属性始终会引用 DOM 中当前获得了焦点的元素。元素获得焦点的方式有页面加载、用户输入(通常是通过按 Tab 键)和在代码中调用 focus()方法。来看几个例子。</p> <pre><code class="language-js">var button = document.getElementById("myButton"); button.focus(); alert(document.activeElement === button); //true </code></pre> <p>默认情况下,文档刚刚加载完成时,document.activeElement 中保存的是 document.body 元素的引用。文档加载期间,document.activeElement 的值为 null。 另外就是新增了 document.hasFocus()方法,这个方法用于确定文档是否获得了焦点。</p> <pre><code class="language-js">var button = document.getElementById("myButton"); button.focus(); alert(document.hasFocus()); //true </code></pre> <p>查询文档获知哪个元素获得了焦点,以及确定文档是否获得了焦点,这两个功能最重要的用途是提高 Web 应用的无障碍性。无障碍 Web 应用的一个主要标志就是恰当的焦点管理,而确切地知道哪个元素获得了焦点是一个极大的进步,至少我们不用再像过去那样靠猜测了。 <strong>兼容:</strong> 实现了这两个属性的浏览器的包括 IE 4+、Firefox 3+、Safari 4+、Chrome 和 Opera 8+。</p> <h3>3.3 HTMLDocument的变化</h3></li> </ul></li> <li>readyState 属性 IE4 最早为 document 对象引入了 readyState 属性。然后,其他浏览器也都陆续添加这个属性,最终 HTML5 把这个属性纳入了标准当中。Document 的 readyState 属性有两个可能的值: <ul> <li>loading,正在加载文档;</li> <li>complete,已经加载完文档。 使用 document.readyState 的最恰当方式,就是通过它来实现一个指示文档已经加载完成的指示器。在这个属性得到广泛支持之前,要实现这样一个指示器,必须借助 onload 事件处理程序设置一个标签,表明文档已经加载完毕。document.readyState 属性的基本用法如下。 <pre><code class="language-js">if (document.readyState == "complete"){ //执行操作 }</code></pre> <p><strong>兼容:</strong> 支持 readyState 属性的浏览器有 IE4+、Firefox 3.6+、Safari、Chrome 和 Opera 9+。</p></li> </ul></li> <li>兼容模式 自从 IE6 开始区分渲染页面的模式是标准的还是混杂的,检测页面的兼容模式就成为浏览器的必要功能。IE 为此给 document 添加了一个名为 compatMode 的属性,这个属性就是为了告诉开发人员浏览器采用了哪种渲染模式。就像下面例子中所展示的那样,在标准模式下,document.compatMode 的值等于"CSS1Compat",而在混杂模式下,document.compatMode 的值等于"BackCompat"。 <pre><code class="language-js">if (document.compatMode == "CSS1Compat"){ alert("Standards mode"); } else { alert("Quirks mode"); } </code></pre></li> <li>head 属性 HTML5 新增了 document.head 属性,引用文档的<head>元素。要引用文档的<head>元素,可以结合使用这个属性和另一种后备方法。 <pre><code class="language-js">var head = document.head || document.getElementsByTagName("head")[0]; ### 3.4 字符集属性 HTML5 新增了几个与文档字符集有关的属性。其中,charset 属性表示文档中实际使用的字符集,也可以用来指定新字符集。默认情况下,这个属性的值为"UTF-16",但可以通过<meta>元素、响应头部或直接设置 charset 属性修改这个值。来看一个例子。 ```js alert(document.charset); //"UTF-16" document.charset = "UTF-8"; </code></pre> <p>另一个属性是 defaultCharset,表示根据默认浏览器及操作系统的设置,当前文档默认的字符集应该是什么。如果文档没有使用默认的字符集,那 charset 和 defaultCharset 属性的值可能会不一样。</p> <h3>3.5 自定义数据属性</h3> <p>HTML5 规定可以为元素添加非标准的属性,但要添加前缀 data-,目的是为元素提供与渲染无关的信息,或者提供语义信息。这些属性可以任意添加、随便命名,只要以 data-开头即可。来看一个例子。</p> <pre><code class="language-html"><div id="myDiv" data-appId="12345" data-myname="Nicholas"></div> </code></pre> <p>添加了自定义属性之后,可以通过元素的 dataset 属性来访问自定义属性的值。dataset 属性的值是 DOMStringMap 的一个实例,也就是一个名值对儿的映射。在这个映射中,每个 data-name 形式的属性都会有一个对应的属性,只不过属性名没有 data-前缀。见例子:</p> <pre><code class="language-js">//本例中使用的方法仅用于演示 var div = document.getElementById("myDiv"); //取得自定义属性的值 var appId = div.dataset.appId; var myName = div.dataset.myname; //设置值 div.dataset.appId = 23456; div.dataset.myname = "Michael"; //有没有"myname"值呢? if (div.dataset.myname){ alert("Hello, " + div.dataset.myname); }</code></pre></li> </ol> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.15</h1> <p>阅读进度: 11.4 P298</p> <h3>3.6 插入标记</h3> <p>,使用插入标记的技术,直接插入 HTML 字符串不仅更简单,速度也更快。以下与插入标记相关的 DOM 扩展已经纳入了 HTML5 规范。 </p> <ol> <li>innerHTML 属性 在读模式下,innerHTML 属性返回与调用元素的所有子节点(包括元素、注释和文本节点)对应的 HTML 标记。在写模式下,innerHTML 会根据指定的值创建新的 DOM 树,然后用这个 DOM 树完全替换调用元素原先的所有子节点。下面是一个例子。 <pre><code class="language-html"><div id="content"> <p>This is a <strong>paragraph</strong> with a list following it.</p> <ul> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul> </div></code></pre> <p>对于上面的<code><div></code>元素来说,它的 innerHTML 属性会返回如下字符串。</p> <pre><code class="language-html"><p>This is a <strong>paragraph</strong> with a list following it.</p> <ul> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul> </code></pre> <p>但是,不同浏览器返回的文本格式会有所不同。IE 和 Opera 会将所有标签转换为大写形式,而 Safari、Chrome 和 Firefox 则会原原本本地按照原先文档中(或指定这些标签时)的格式返回 HTML,包括空格和缩进。<br /> <strong>注意:</strong> 不要指望所有浏览器返回的 innerHTML 值完全相同。 在写模式下,innerHTML 的值会被解析为 DOM 子树,替换调用元素原来的所有子节点。因为它的值被认为是 HTML,所以其中的所有标签都会按照浏览器处理 HTML 的标准方式转换为元素(同样,这里的转换结果也因浏览器而异)。如果设置的值仅是文本而没有 HTML 标签,那么结果就是设置纯文本,如下所示。 </p> <pre><code class="language-js">div.innerHTML = "Hello world!"; </code></pre> <p>为 innerHTML 设置的包含 HTML 的字符串值与解析后 innerHTML 的值大不相同。来看下面的</p> <pre><code class="language-js">div.innerHTML = "Hello & welcome, <b>\"reader\"!</b>"; </code></pre> <p>以上操作得到的结果如下:</p> <pre><code class="language-html"><div id="content">Hello &amp; welcome, <b>&quot;reader&quot;!</b></div> </code></pre> <p>使用 innerHTML 属性也有一些限制。比如,在大多数浏览器中,通过 innerHTML 插入<code><script></code>元素并不会执行其中的脚本。IE8 及更早版本是唯一能在这种情况下执行脚本的浏览器,但必须满足一些条件。 </p> <ul> <li>必须为<code><script></code>元素指定 defer 属性</li> <li>是<code><script></code>元素必须位于(微软所谓的)“有作用域的元素”(scoped element)之后<br /> <code><script></code>元素被认为是“无作用域的元素”(NoScope element),也就是在页面中看不到的元素,与<code><style></code>元素或注释类似。如果通过 innerHTML 插入的字符串开头就是一个“无作用域的元素”,那么 IE 会在解析这个字符串前先删除该元素。换句话说,以下代码达不到目的: <pre><code class="language-js">div.innerHTML = "<script defer>alert('hi');<\/script>"; //无效</code></pre> <p>此时,innerHTML 字符串一开始(而且整个)就是一个“无作用域的元素”,所以这个字符串会变成空字符串。如果想插入这段脚本,必须在前面添加一个“有作用域的元素”,可以是一个文本节点,也可以是一个没有结束标签的元素如<code><input></code>。例如,下面这几行代码都可以正常执行: </p> <pre><code class="language-js">div.innerHTML = "_<script defer>alert('hi');<\/script>"; div.innerHTML = "<div>&nbsp;</div><script defer>alert('hi');<\/script>"; div.innerHTML = "<input type=\"hidden\"><script defer>alert('hi');<\/script>"; </code></pre></li> <li>第一行代码会在<code><script></code>元素前插入一个文本节点。事后,为了不影响页面显示,你可能需要移除这个文本节点。</li> <li>第二行代码采用的方法类似,只不过使用的是一个包含非换行空格的<code><div></code>元素。如果仅仅插入一个空的<code><div></code>元素,还是不行;必须要包含一点儿内容,浏览器才会创建文本节点。同样,为了不影响页面布局,恐怕还得移除这个节点。</li> <li>第三行代码使用的是一个隐藏的<code><input></code>域,也能达到相同的效果。不过,由于隐藏的<code><input></code>域不影响页面布局,因此这种方式在大多数情况下都是首选。 大多数浏览器都支持以直观的方式通过 innerHTML 插入<code><style></code>元素,例如: <pre><code class="language-js">div.innerHTML = "<style type=\"text/css\">body {background-color: red; }</style>"; </code></pre> <p><strong>但在 IE8 及更早版本中,</strong> <code><style></code>也是一个“没有作用域的元素”,因此必须像下面这样给它前置一个“有作用域的元素”:</p> <pre><code class="language-js">div.innerHTML = "_<style type=\"text/css\">body {background-color: red; }</style>"; div.removeChild(div.firstChild);</code></pre> <p>并不是所有元素都支持 innerHTML 属性。不支持 innerHTML 的元素有:<code><col>、<colgroup>、<frameset>、<head>、<html>、<style>、<table>、<tbody>、<thead>、<tfoot>和<tr></code>。此外,在 IE8 及更早版本中,<code><title></code>元素也没有 innerHTML 属性。 </p></li> </ul></li> </ol> <p>无论什么时候,只要使用 innerHTML 从外部插入 HTML,都应该首先以可靠的方式处理 HTML。<br /> IE8 为此提供了 window.toStaticHTML()方法,这个方法接收一个参数,即一个 HTML 字符串;返回一个经过无害处理后的版本——从源 HTML 中删除所有脚本节点和事件处理程序属性。下面就是一个例子:</p> <pre><code class="language-js">var text = "<a href=\"#\" onclick=\"alert('hi')\">Click Me</a>"; var sanitized = window.toStaticHTML(text); //Internet Explorer 8 only alert(sanitized); //"<a href=\"#\">Click Me</a>" </code></pre> <ol start="2"> <li>outerHTML 属性 在读模式下,outerHTML 返回调用它的元素及所有子节点的 HTML 标签。在写模式下,outerHTML会根据指定的 HTML 字符串创建新的 DOM 子树,然后用这个 DOM 子树完全替换调用元素。下面是一个例子。 <pre><code class="language-js"><div id="content"> <p>This is a <strong>paragraph</strong> with a list following it.</p> <ul> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul> </div> </code></pre> <p>如果在<code><div></code>元素上调用 outerHTML,会返回与上面相同的代码,包括<code><div></code>本身。不过,由于浏览器解析和解释 HTML 标记的不同,结果也可能会有所不同。(<strong>这里的不同与使用 innerHTML 属性时存在的差异性质是一样的。</strong>)<br /> 使用 outerHTML 属性以下面这种方式设置值: </p> <pre><code class="language-js">div.outerHTML = "<p>This is a paragraph.</p>"; </code></pre> <p>这行代码完成的操作与下面这些 DOM 脚本代码一样: </p> <pre><code class="language-js">var p = document.createElement("p"); p.appendChild(document.createTextNode("This is a paragraph.")); div.parentNode.replaceChild(p, div);</code></pre> <p>结果,就是新创建的<code><p></code>元素会取代 DOM 树中的<code><div></code>元素。<br /> <strong>兼容性:</strong> 支持 outerHTML 属性的浏览器有 IE4+、Safari 4+、Chrome 和 Opera 8+。Firefox 7 及之前版本都不支持 outerHTML 属性。 </p></li> <li>insertAdjacentHTML()方法 插入标记的最后一个新增方式是insertAdjacentHTML()方法。这个方法最早也是在IE中出现的,它接收两个参数:插入位置和要插入的 HTML 文本。第一个参数必须是下列值之一: <ul> <li>"beforebegin",在当前元素之前插入一个紧邻的同辈元素;</li> <li>"afterbegin",在当前元素之下插入一个新的子元素或在第一个子元素之前再插入新的子元素;</li> <li>"beforeend",在当前元素之下插入一个新的子元素或在最后一个子元素之后再插入新的子元素;</li> <li>"afterend",在当前元素之后插入一个紧邻的同辈元素。 注意,这些值都必须是小写形式。第二个参数是一个 HTML 字符串(与 innerHTML 和 outerHTML的值相同),如果浏览器无法解析该字符串,就会抛出错误。以下是这个方法的基本用法示例。 <pre><code class="language-js">//作为前一个同辈元素插入 element.insertAdjacentHTML("beforebegin", "<p>Hello world!</p>"); //作为第一个子元素插入 element.insertAdjacentHTML("afterbegin", "<p>Hello world!</p>"); //作为最后一个子元素插入 element.insertAdjacentHTML("beforeend", "<p>Hello world!</p>"); //作为后一个同辈元素插入 element.insertAdjacentHTML("afterend", "<p>Hello world!</p>"); </code></pre> <p><strong>支持</strong> insertAdjacentHTML()方法的浏览器有 IE、Firefox 8+、Safari、Opera 和 Chrome。 </p></li> </ul></li> <li>内存与性能问题 使用本节介绍的方法替换子节点可能会导致浏览器的内存占用问题,尤其是在 IE 中,问题更加明显。在删除带有事件处理程序或引用了其他 JavaScript 对象子树时,就有可能导致内存占用问题。假设某个元素有一个事件处理程序(或者引用了一个 JavaScript 对象作为属性),在使用前述某个属性将该元素从文档树中删除后,元素与事件处理程序(或 JavaScript 对象)之间的绑定关系在内存中并没有一并删除。如果这种情况频繁出现,页面占用的内存数量就会明显增加。因此,在使用 innerHTML、outerHTML 属性和 insertAdjacentHTML()方法时,最好先手工删除要被替换的元素的所有事件处理程序和 JavaScript 对象属性<strong>(第 13 章将进一步讨论事件处理程序)</strong>。<br /> 使用 innerHTML,仍然还是可以为我们提供很多便利的。一般来说,在插入大量新 HTML 标记时,使用 innerHTML 属性与通过多次 DOM 操作先创建节点再指定它们之间的关系相比,效率要高得多。这是因为在设置 innerHTML 或 outerHTML 时,就会创建一个 HTML解析器。这个解析器是在浏览器级别的代码(通常是 C++编写的)基础上运行的,因此比执行 JavaScript快得多。不可避免地,创建和销毁 HTML 解析器也会带来性能损失,所以最好能够将设置 innerHTML或 outerHTML 的次数控制在合理的范围内。例如,下列代码使用 innerHTML 创建了很多列表项: <pre><code class="language-js">for (var i=0, len=values.length; i < len; i++){ ul.innerHTML += "<li>" + values[i] + "</li>"; //要避免这种频繁操作!! } </code></pre> <p>这种每次循环都设置一次 innerHTML 的做法效率很低。而且,每次循环还要从 innerHTML 中读取一次信息,就意味着每次循环要访问两次 innerHTML。最好的做法是单独构建字符串,然后再一次性地将结果字符串赋值给 innerHTML,像下面这样: </p> <pre><code class="language-js">var itemsHtml = ""; for (var i=0, len=values.length; i < len; i++){ itemsHtml += "<li>" + values[i] + "</li>"; } ul.innerHTML = itemsHtml; </code></pre> <h3>3.7 scrollIntoView()方法</h3> <p>scrollIntoView()可以在所有 HTML 元素上调用,通过滚动浏览器窗口或某个容器元素,调用元素就可以出现在视口中。如果给这个方法传入 true 作为参数,或者不传入任何参数,那么窗口滚动之后会让调用元素的顶部与视口顶部尽可能平齐。如果传入 false 作为参数,调用元素会尽可能全部出现在视口中,(可能的话,调用元素的底部会与视口顶部平齐。)不过顶部不一定平齐,例如:</p> <pre><code class="language-js">//让元素可见 document.forms[0].scrollIntoView(); </code></pre> <p>当页面发生变化时,一般会用这个方法来吸引用户的注意力。实际上,为某个元素设置焦点也会导致浏览器滚动并显示出获得焦点的元素。 支持 scrollIntoView()方法的浏览器有 IE、Firefox、Safari 和 Opera。</p></li> </ol> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.16</h1> <p>阅读进度: 第12章 P305</p> <h2>4.专有扩展</h2> <h3>4.1 文档模式</h3> <p>IE8 引入了一个新的概念叫“文档模式”(document mode)。页面的文档模式决定了可以使用什么功能。文档模式决定了你可以使用哪个级别的 CSS,可以在 JavaScript 中使用哪些 API,以及如何对待文档类型(doctype)。到了 IE9,总共有以下 4 种文档模式:</p> <ul> <li>IE5:以混杂模式渲染页面(IE5 的默认模式就是混杂模式)。IE8 及更高版本中的新功能都无法使用。</li> <li>IE7:以 IE7 标准模式渲染页面。IE8 及更高版本中的新功能都无法使用。</li> <li>IE8:以 IE8 标准模式渲染页面。IE8 中的新功能都可以使用,因此可以使用 Selectors API、更多CSS2 级选择符和某些 CSS3 功能,还有一些 HTML5 的功能。不过 IE9 中的新功能无法使用。</li> <li>IE9:以 IE9 标准模式渲染页面。IE9 中的新功能都可以使用,比如 ECMAScript 5、完整的 CSS3以及更多 HTML5 功能。这个文档模式是最高级的模式。 要强制浏览器以某种模式渲染页面,可以使用 HTTP 头部信息 X-UA-Compatible,或通过等价的<code><meta></code>标签来设置: <pre><code class="language-html"><meta http-equiv="X-UA-Compatible" content="IE=IEVersion"> </code></pre> <p><strong>注意,</strong>这里 IE 的版本(IEVersion)有以下一些不同的值,而且这些值并不一定与上述 4 种文档模式对应。</p></li> <li>Edge:始终以最新的文档模式来渲染页面。忽略文档类型声明。对于 IE8,始终保持以 IE8 标准模式渲染页面。对于 IE9,则以 IE9 标准模式渲染页面。</li> <li>EmulateIE9:如果有文档类型声明,则以 IE9 标准模式渲染页面,否则将文档模式设置为 IE5。</li> <li>EmulateIE8:如果有文档类型声明,则以 IE8 标准模式渲染页面,否则将文档模式设置为 IE5。</li> <li>EmulateIE7:如果有文档类型声明,则以 IE7 标准模式渲染页面,否则将文档模式设置为 IE5。</li> <li>9:强制以 IE9 标准模式渲染页面,忽略文档类型声明。</li> <li>8:强制以 IE8 标准模式渲染页面,忽略文档类型声明。</li> <li>7:强制以 IE7 标准模式渲染页面,忽略文档类型声明。</li> <li>5:强制将文档模式设置为 IE5,忽略文档类型声明。 比如,要想让文档模式像在 IE7 中一样,可以使用下面这行代码: <pre><code class="language-html"><meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7"> </code></pre> <p>如果不打算考虑文档类型声明,而直接使用 IE7 标准模式,那么可以使用下面这行代码:</p> <pre><code class="language-html"><meta http-equiv="X-UA-Compatible" content="IE=7"> </code></pre> <p>没有规定说必须在页面中设置 X-UA-Compatible。默认情况下,浏览器会通过文档类型声明来确定是使用最佳的可用文档模式,还是使用混杂模式。<br /> 通过 document.documentMode 属性可以知道给定页面使用的是什么文档模式。这个属性是 IE8中新增的,它会返回使用的文档模式的版本号(在 IE9 中,可能返回的版本号为 5、7、8、9):</p> <pre><code class="language-js">var mode = document.documentMode; </code></pre> <h3>4.2 children属性</h3> <p>由于 IE9 之前的版本与其他浏览器在处理文本节点中的空白符时有差异,因此就出现了 children属性。这个属性是 HTMLCollection 的实例,只包含元素中同样还是元素的子节点。除此之外,children 属性与 childNodes 没有什么区别,即在元素只包含元素子节点时,这两个属性的值相同。下面是访问 children 属性的示例代码: </p> <pre><code class="language-js">var childCount = element.children.length; var firstChild = element.children[0]; </code></pre> <p><strong>支持 children 属性的浏览器有 IE5、Firefox 3.5、Safari 2(但有 bug)、Safari 3(完全支持)、Opera8和 Chrome(所有版本)。IE8 及更早版本的 children 属性中也会包含注释节点,但 IE9 之后的版本则只返回元素节点。</strong></p> <h3>4.3 contains()方法</h3> <p>在实际开发中,经常需要知道某个节点是不是另一个节点的后代。IE 为此率先引入了 contains()方法,以便不通过在 DOM 文档树中查找即可获得这个信息。调用 contains()方法的应该是祖先节点,也就是搜索开始的节点,这个方法接收一个参数,即要检测的后代节点。如果被检测的节点是后代节点,该方法返回 true;否则,返回 false。以下是一个例子:</p> <pre><code class="language-js">alert(document.documentElement.contains(document.body)); //true </code></pre> <p>这个例子测试了<code><body></code>元素是不是<code><html></code>元素的后代,在格式正确的 HTML 页面中,以上代码返回 true。<strong>支持 contains()方法的浏览器有 IE、Firefox 9+、Safari、Opera 和 Chrome。</strong><br /> 使用 DOM Level 3 compareDocumentPosition()也能够确定节点间的关系。<strong>支持这个方法的浏览器有 IE9+、Firefox、Safari、Opera 9.5+和 Chrome。</strong>如前所述,这个方法用于确定两个节点间的关系,返回一个表示该关系的位掩码( bitmask)。下表列出了这个位掩码的值。见P300<br /> 为模仿 contains()方法,应该关注的是掩码 16。可以对 compareDocumentPosition()的结果执行按位与,以确定参考节点(调用 compareDocumentPosition()方法的当前节点)是否包含给定的节点(传入的节点)。来看下面的例子: </p> <pre><code class="language-js">var result = document.documentElement.compareDocumentPosition(document.body); alert(!!(result & 16)); </code></pre> <p>执行上面的代码后,结果会变成 20(表示“居后”的 4 加上表示“被包含”的 16)。对掩码 16 执行按位操作会返回一个非零数值,而两个逻辑非操作符会将该数值转换成布尔值。<br /> 通用的 contains 函数见p300</p> <h3>4.4 插入文本</h3> <p>IE 原来专有的插入标记的属性 innerHTML 和 outerHTML 已经被 HTML5 纳入规范。但另外两个插入文本的专有属性则没有这么好的运气。这两个没有被 HTML5 看中的属性是 innerText和 outerText。</p> <ol> <li>innerText 属性 通过 innertText 属性可以操作元素中包含的所有文本内容,包括子文档树中的文本。在通过innerText 读取值时,它会按照由浅入深的顺序,将子文档树中的所有文本拼接起来。在通过innerText 写入值时,结果会删除元素的所有子节点,插入包含相应文本值的文本节点。来看下面这个 HTML 代码示例。 <pre><code class="language-html"><div id="content"> <p>This is a <strong>paragraph</strong> with a list following it.</p> <ul> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul> </div> </code></pre> <p>对于这个例子中的<code><div></code>元素而言,其 innerText 属性会返回下列字符串:</p> <pre><code class="language-js">This is a paragraph with a list following it. Item 1 Item 2 Item 3 </code></pre> <p>使用 innerText 属性设置这个<code><div></code>元素的内容,则只需一行代码:</p> <pre><code class="language-js">div.innerText = "Hello world!"; </code></pre> <p>执行这行代码后,页面的 HTML 代码就会变成如下所示。</p> <pre><code class="language-html"><div id="content">Hello world!</div></code></pre> <p>设置innerText属性移除了先前存在的所有子节点,完全改变了DOM子树。此外,设置innerText属性的同时,也对文本中存在的 HTML 语法字符(小于号、大于号、引号及和号)进行了编码。 设置 innerText 永远只会生成当前节点的一个子文本节点,而为了确保只生成一个子文本节点,就必须要对文本进行 HTML 编码。利用这一点,可以通过 innerText 属性过滤掉 HTML 标签。方法是将 innerText 设置为等于 innerText,这样就可以去掉所有 HTML 标签,比如:</p> <pre><code class="language-js">div.innerText = div.innerText; </code></pre> <p>执行这行代码后,就用原来的文本内容替换了容器元素中的所有内容(包括子节点,因而也就去掉了 HTML 标签)。<br /> 支持 innerText 属性的浏览器包括 IE4+、Safari 3+、Opera 8+和 Chrome。Firefox 虽然不支持innerText,但支持作用类似的 textContent 属性。textContent 是 DOM Level 3 规定的一个属性,其他支持 textContent 属性的浏览器<strong>还有 IE9+、Safari 3+、Opera 10+和 Chrome。</strong><br /> 函数检测是否用哪个属性: 见p032</p></li> <li>outerText 属性<br /> 除了作用范围扩大到了包含调用它的节点之外,outerText 与 innerText 基本上没有多大区别。在读取文本值时,outerText 与 innerText 的结果完全一样。但在写模式下,outerText 就完全不同了:outerText 不只是替换调用它的元素的子节点,而是会替换整个元素(包括子节点)。<br /> <strong>支持 outerText 属性的浏览器有 IE4+、Safari 3+、Opera 8+和 Chrome。</strong>由于这个属性会导致调用它的元素不存在,因此并不常用。<strong>不建议使用</strong> <h3>4.5 滚动</h3> <p>HTML5 在将scrollIntoView()纳入规范之后,仍然还有其他几个专有方法可以在不同的浏览器中使用。下面列出的几个方法都是对 HTMLElement 类型的扩展,因此在所有元素中都可以调用。 </p></li> </ol></li> <li>scrollIntoViewIfNeeded(alignCenter):只在当前元素在视口中不可见的情况下,才滚动浏览器窗口或容器元素,最终让它可见。如果当前元素在视口中可见,这个方法什么也不做。如果将可选的 alignCenter 参数设置为 true,则表示尽量将元素显示在视口中部(垂直方向)。Safari 和 Chrome 实现了这个方法。</li> <li>scrollByLines(lineCount):将元素的内容滚动指定的行高,lineCount 值可以是正值, 也可以是负值。Safari 和 Chrome 实现了这个方法。</li> <li>scrollByPages(pageCount):将元素的内容滚动指定的页面高度,具体高度由元素的高度决 定。Safari 和 Chrome 实现了这个方法。 希望大家要注意的是,scrollIntoView()和 scrollIntoViewIfNeeded()的作用对象是元素的容器,而 scrollByLines()和 scrollByPages()影响的则是元素自身。例子见304 <h2>5 小结</h2></li> <li>Selectors API,定义了两个方法,让开发人员能够基于 CSS 选择符从 DOM 中取得元素,这两个方法是 querySelector()和 querySelectorAll()。</li> <li>Element Traversal,为 DOM 元素定义了额外的属性,让开发人员能够更方便地从一个元素跳到另一个元素。之所以会出现这个扩展,是因为浏览器处理 DOM 元素间空白符的方式不一样。</li> <li>HTML5,为标准的 DOM 定义了很多扩展功能。其中包括在 innerHTML 属性这样的事实标准基础上提供的标准定义,以及为管理焦点、设置字符集、滚动页面而规定的扩展 API。</li> </ul> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.17</h1> <p>阅读进度: 12.2 P309</p> <h2>1. DOM变化</h2> <p>DOM1 级主要定义的是 HTML 和 XML 文档的底层结构。DOM2 和 DOM3 级则在这个结构的基础上引入了更多的交互能力,也支持了更高级的 XML 特性。 DOM2 和 DOM3级分为许多模块(模块之间具有某种关联),分别描述了 DOM 的某个非常具体的子集:</p> <ul> <li>DOM2 级核心(DOM Level 2 Core):在 1 级核心基础上构建,为节点添加了更多方法和属性。</li> <li>DOM2 级视图(DOM Level 2 Views):为文档定义了基于样式信息的不同视图。</li> <li>DOM2 级事件(DOM Level 2 Events):说明了如何使用事件与 DOM 文档交互。</li> <li>DOM2 级样式(DOM Level 2 Style):定义了如何以编程方式来访问和改变 CSS 样式信息。</li> <li>DOM2 级遍历和范围(DOM Level 2 Traversal and Range):引入了遍历 DOM 文档和选择其特定部分的新接口。</li> <li>DOM2 级 HTML(DOM Level 2 HTML):在 1 级 HTML 基础上构建,添加了更多属性、方法和新接口。 <strong>DOM3 级又增加了“XPath”模块和“加载与保存”(Load and Save)模块。</strong> <h3>1.1DOM 变化</h3> <p>DOM2 级和 3 级的目的在于扩展 DOM API,以满足操作 XML 的所有需求,同时提供更好的错误处理及特性检测能力。“DOM2 级核心”没有引入新类型,它只是在 DOM1 级的基础上通过增加新方法和新属性来增强了既有类型。“DOM3级核心”同样增强了既有类型,但也引入了一些新类型。 类似地,“DOM2 级视图”和“DOM2 级 HTML”模块也增强了 DOM 接口,提供了新的属性和方法。 可以通过下列代码来确定浏览器是否支持这些 DOM 模块:</p> <pre><code class="language-js">var supportsDOM2Core = document.implementation.hasFeature("Core", "2.0"); var supportsDOM3Core = document.implementation.hasFeature("Core", "3.0"); var supportsDOM2HTML = document.implementation.hasFeature("HTML", "2.0"); var supportsDOM2Views = document.implementation.hasFeature("Views", "2.0"); var supportsDOM2XML = document.implementation.hasFeature("XML", "2.0"); </code></pre> <h4>1.1.1 针对XML命名空间的变化</h4> <p>有了 XML 命名空间,不同 XML 文档的元素就可以混合在一起,共同构成格式良好的文档,而不必担心发生命名冲突。从技术上说,HTML 不支持 XML 命名空间,但 XHTML 支持 XML 命名空间。因此,本节给出的都是 XHTML 的示例。 命名空间要使用 xmlns 特性来指定。XHTML 的命名空间是<a href="http://www.w3.org/1999/xhtml">http://www.w3.org/1999/xhtml</a>, 在任何格式良好 XHTML 页面中,都应该将其包含在<code><html></code>元素中,如下面的例子所示。</p> <pre><code class="language-html"><html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Example XHTML page</title> </head> <body> Hello world! </body> </html> </code></pre> <p>对这个例子而言,其中的所有元素默认都被视为 XHTML 命名空间中的元素。要想明确地为 XML命名空间创建前缀,可以使用 xmlns 后跟冒号,再后跟前缀,如下所示:</p> <pre><code class="language-html"><xhtml:html xmlns:xhtml="http://www.w3.org/1999/xhtml"> <xhtml:head> <xhtml:title>Example XHTML page</xhtml:title> </xhtml:head> <xhtml:body> Hello world! </xhtml:body> </xhtml:html> </code></pre> <p>这里为 XHTML 的命名空间定义了一个名为 xhtml 的前缀,并要求所有 XHTML 元素都以该前缀开头。<br /> 有时候为了避免不同语言间的冲突,也需要使用命名空间来限定特性,如下面的例子所示。</p> <pre><code class="language-html"><xhtml:html xmlns:xhtml="http://www.w3.org/1999/xhtml"> <xhtml:head> <xhtml:title>Example XHTML page</xhtml:title> </xhtml:head> <xhtml:body xhtml:class="home"> Hello world! </xhtml:body> </xhtml:html> </code></pre> <p>这个例子中的特性 class 带有一个 xhtml 前缀。在只基于一种语言编写 XML 文档的情况下,命名空间实际上也没有什么用。不过,在混合使用两种语言的情况下,命名空间的用处就非常大了。来看一看下面这个混合了 XHTML 和 SVG 语言的文档:</p> <pre><code class="language-html"><html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Example XHTML page</title> </head> <body> <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 100 100" style="width:100%; height:100%"> <rect x="0" y="0" width="100" height="100" style="fill:red"/> </svg> </body> </html> </code></pre> <p>在这个例子中,通过设置命名空间,将<code><svg></code>标识为了与包含文档无关的元素。此时,<code><svg></code>元素的所有子元素,以及这些元素的所有特性,都被认为属于<a href="http://www.w3.org/2000/svg">http://www.w3.org/2000/svg</a> 命名空间。即使这个文档从技术上说是一个 XHTML文档,但因为有了命名空间,其中的 SVG代码也仍然是有效的。 在查询一个特殊标签名时,应该将结果包含在哪个命名空间中呢?“DOM2 级核心”通过为大多数 DOM1 级方法提供特定于命名空间的版本解决了这个问题。</p> <ol> <li>Node 类型的变化 在 DOM2 级中,Node 类型包含下列特定于命名空间的属性。 </li> </ol></li> <li>localName:不带命名空间前缀的节点名称。</li> <li>namespaceURI:命名空间 URI 或者(在未指定的情况下是)null。</li> <li>prefix:命名空间前缀或者(在未指定的情况下是)null。 当节点使用了命名空间前缀时,其 nodeName 等于 prefix+":"+ localName。以下面的文档为例: <pre><code class="language-html"><html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Example XHTML page</title> </head> <body> <s:svg xmlns:s="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 100 100" style="width:100%; height:100%"> <s:rect x="0" y="0" width="100" height="100" style="fill:red"/> </s:svg> </body> </html> </code></pre> <p>对于<code><html></code>元素来说,它的 localName 和 tagName 是"html",namespaceURI 是"<a href="http://www.w3.org/1999/xhtml">http://www.w3.org/1999/xhtml</a>" ,而 prefix 是 null。对于<code><s:svg></code>元素而言,它的 localName 是"svg",tagName 是"s:svg",namespaceURI 是"<a href="http://www.w3.org/2000/svg">http://www.w3.org/2000/svg</a>" ,而 prefix 是"s"。 DOM3 级在此基础上更进一步,又引入了下列与命名空间有关的方法。</p></li> <li>isDefaultNamespace(namespaceURI):在指定的 namespaceURI 是当前节点的默认命名空间的情况下返回 true。</li> <li>lookupNamespaceURI(prefix):返回给定 prefix 的命名空间。</li> <li>lookupPrefix(namespaceURI):返回给定 namespaceURI 的前缀。 针对前面的例子,可以执行下列代码: <pre><code class="language-js">alert(document.body.isDefaultNamespace("http://www.w3.org/1999/xhtml"); //true //假设 svg 中包含着对<s:svg>的引用 alert(svg.lookupPrefix("http://www.w3.org/2000/svg")); //"s" alert(svg.lookupNamespaceURI("s")); //"http://www.w3.org/2000/svg" </code></pre> <p>在取得了一个节点,但不知道该节点与文档其他元素之间关系的情况下,这些方法是很有用的。</p> <ol start="2"> <li>Document 类型的变化 DOM2 级中的 Document 类型也发生了变化,包含了下列与命名空间有关的方法。 </li> </ol></li> <li>createElementNS(namespaceURI, tagName):使用给定的 tagName 创建一个属于命名空间 namespaceURI 的新元素。</li> <li>createAttributeNS(namespaceURI, attributeName):使用给定的 attributeName 创建一个属于命名空间 namespaceURI 的新特性。</li> <li>getElementsByTagNameNS(namespaceURI, tagName):返回属于命名空间 namespaceURI的 tagName 元素的 NodeList。 使用这些方法时需要传入表示命名空间的 URI(而不是命名空间前缀),如下面的例子所示。 <pre><code class="language-js">//创建一个新的 SVG 元素 var svg = document.createElementNS("http://www.w3.org/2000/svg","svg"); //创建一个属于某个命名空间的新特性 var att = document.createAttributeNS("http://www.somewhere.com", "random"); //取得所有 XHTML 元素 var elems = document.getElementsByTagNameNS("http://www.w3.org/1999/xhtml", "*"); </code></pre> <p><strong>只有在文档中存在两个或多个命名空间时,这些与命名空间有关的方法才是必需的。</strong></p> <ol start="3"> <li>Element 类型的变化 “DOM2 级核心”中有关 Element 的变化,主要涉及操作特性。新增的方法如下。</li> </ol></li> <li>getAttributeNS(namespaceURI,localName):取得属于命名空间 namespaceURI 且名为localName 的特性。</li> <li>getAttributeNodeNS(namespaceURI,localName):取得属于命名空间 namespaceURI 且名为 localName 的特性节点。</li> <li>getElementsByTagNameNS(namespaceURI, tagName):返回属于命名空间 namespaceURI的 tagName 元素的 NodeList。</li> <li>hasAttributeNS(namespaceURI,localName):确定当前元素是否有一个名为 localName的特性,而且该特性的命名空间是 namespaceURI。注意,“DOM2 级核心”也增加了一个hasAttribute()方法,用于不考虑命名空间的情况。</li> <li>removeAttriubteNS(namespaceURI,localName):删除属于命名空间 namespaceURI 且名为 localName 的特性。</li> <li>setAttributeNS(namespaceURI,qualifiedName,value):设置属于命名空间 namespaceURI 且名为 qualifiedName 的特性的值为 value。</li> <li>setAttributeNodeNS(attNode):设置属于命名空间 namespaceURI 的特性节点。 <ol start="4"> <li>NamedNodeMap 类型的变化 NamedNodeMap 类型也新增了下列与命名空间有关的方法。由于特性是通过 NamedNodeMap 表示的,因此这些方法多数情况下只针对特性使用。</li> </ol></li> <li>getNamedItemNS(namespaceURI,localName):取得属于命名空间 namespaceURI 且名为localName 的项。</li> <li>removeNamedItemNS(namespaceURI,localName):移除属于命名空间 namespaceURI 且名为 localName 的项。</li> <li>setNamedItemNS(node):添加 node,这个节点已经事先指定了命名空间信息。</li> </ul> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.18</h1> <p>阅读进度: 12.2 P312</p> <h3>1.2 其他方面的变化</h3> <p>DOM 的其他部分在“DOM2 级核心”中也发生了一些变化。这些变化与 XML 命名空间无关,而是更倾向于确保 API 的可靠性及完整性。</p> <ol> <li>DocumentType 类型的变化 DocumentType 类型新增了 3 个属性:publicId、systemId 和 internalSubset。其中,前两个属性表示的是文档类型声明中的两个信息段,这两个信息段在 DOM1 级中是没有办法访问到的。以下面的 HTML 文档类型声明为例。 <pre><code class="language-html"><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"></code></pre> <p>对这个文档类型声明而言,publicId是"-//W3C//DTD HTML 4.01//EN",而systemId是"<a href="http://www.w3.org/TR/html4/strict.dtd">http://www.w3.org/TR/html4/strict.dtd</a>"。<br /> 在支持 DOM2 级的浏览器中,应该可以运行下列代码。</p> <pre><code class="language-js">alert(document.doctype.publicId); alert(document.doctype.systemId); </code></pre> <p>最后一个属性 internalSubset,用于访问包含在文档类型声明中的额外定义,以下面的代码为例。</p> <pre><code class="language-html"><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd" [<!ELEMENT name (#PCDATA)>] ></code></pre> <p>访问 document.doctype.internalSubset 将得到"<!ELEMENT name (#PCDATA)>"。这种内部子集(internal subset)在 HTML 中极少用到,在 XML 中可能会更常见一些。</p></li> <li>Document 类型的变化 Document 类型的变化中唯一与命名空间无关的方法是 importNode()。这个方法的用途是从一个文档中取得一个节点,然后将其导入到另一个文档,使其成为这个文档结构的一部分。需要注意的是,每个节点都有一个 ownerDocument 属性,表示所属的文档。如果调用 appendChild()时传入的节点属于不同的文档(ownerDocument 属性的值不一样),则会导致错误。但在调用 importNode()时传入不同文档的节点则会返回一个新节点,这个新节点的所有权归当前文档所有。<br /> importNode()方法与 Element 的 cloneNode()方法非常相似,它接受两个参数:要复制的节点和一个表示否复制子节点的布尔值。返回的结果是原来节点的副本,但能够在当前文档中使用。来看下面的例子: <pre><code class="language-js">var newNode = document.importNode(oldNode, true); //导入节点及其所有子节点 document.body.appendChild(newNode); </code></pre> <p><strong>这个方法在 HTML 文档中并不常用,在 XML 文档中用得比较多</strong> “DOM2 级视图”模块添加了一个名为 defaultView 的属性,其中保存着一个指针,指向拥有给定文档的窗口(或框架)。除 IE 之外的所有浏览器都支持 defaultView 属性。在 IE 中有一个等价的属性名叫 parentWindow(Opera 也支持这个属性)。因此,要确定文档的归属窗口,可以使用以下代码。</p> <pre><code class="language-js">var parentWindow = document.defaultView || document.parentWindow; </code></pre> <p>除了上述一个方法和一个属性之外,“DOM2级核心”还为 document.implementation 对象规定了两个新方法:createDocumentType()和 createDocument()。前者用于创建一个新的 DocumentType节点,接受 3 个参数:文档类型名称、publicId、systemId。例如,下列代码会创建一个新的 HTML4.01 Strict 文档类型。</p> <pre><code class="language-js">var doctype = document.implementation.createDocumentType("html", "-//W3C//DTD HTML 4.01//EN", "http://www.w3.org/TR/html4/strict.dtd"); </code></pre> <p>由于既有文档的文档类型不能改变,因此 createDocumentType()只在创建新文档时有用;创建新文档时需要用到 createDocument()方法。这个方法接受 3 个参数:针对文档中元素的 namespaceURI、文档元素的标签名、新文档的文档类型。下面这行代码将会创建一个空的新XML 文档。 </p> <pre><code class="language-js">var doc = document.implementation.createDocument("", "root", null); </code></pre> <p>这行代码会创建一个没有命名空间的新文档,文档元素为<code><root></code>,而且没有指定文档类型。要想创建一个 XHTML 文档,可以使用以下代码。</p> <pre><code class="language-js">var doctype = document.implementation.createDocumentType("html", " -//W3C//DTD XHTML 1.0 Strict//EN", "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"); var doc = document.implementation.createDocument("http://www.w3.org/1999/xhtml", "html", doctype); </code></pre> <p>“DOM2 级 HTML”模块也为 document.implementation 新增了一个方法,名叫 createHTMLDocument()。这个方法的用途是创建一个完整的HTML 文档,包括<code><html>、<head>、<title></code>和<code><body></code>元素。这个方法只接受一个参数,即新创建文档的标题(放在<code><title></code>元素中的字符串),返回新的 HTML 文档,如下所示:</p> <pre><code class="language-js">var htmldoc = document.implementation.createHTMLDocument("New Doc"); alert(htmldoc.title); //"New Doc" alert(typeof htmldoc.body); //"object" </code></pre> <p>通过调用 createHTMLDocument()创建的这个文档,是 HTMLDocument 类型的实例,因而具有该类型的所有属性和方法,包括 title 和 body 属性。<strong>只有 Opera 和 Safari 支持这个方法。</strong></p></li> <li>Node 类型的变化 Node 类型中唯一与命名空间无关的变化,就是添加了 isSupported()方法。与 DOM1 级为 document.implementation引入的 hasFeature()方法类似,isSupported()方法用于确定当前节点具有什么能力。<br /> 这个方法也接受相同的两个参数:特性名和特性版本号。如果浏览器实现了相应特性,而且能够基于给定节点执行该特性,isSupported()就返回 true。来看一个例子: <pre><code class="language-js">if (document.body.isSupported("HTML", "2.0")){ //执行只有"DOM2 级 HTML"才支持的操作 } </code></pre> <p>DOM3 级引入了两个辅助比较节点的方法:isSameNode()和 isEqualNode()。这两个方法都接受一个节点参数,并在传入节点与引用的节点相同或相等时返回 true。所谓相同,指的是两个节点引用的是同一个对象。所谓相等,指的是两个节点是相同的类型,具有相等的属性(nodeName、nodeValue,等等),而且它们的 attributes 和 childNodes 属性也相等(相同位置包含相同的值)。来看一个例子。</p> <pre><code class="language-js">var div1 = document.createElement("div"); div1.setAttribute("class", "box"); var div2 = document.createElement("div"); div2.setAttribute("class", "box"); alert(div1.isSameNode(div1)); //true alert(div1.isEqualNode(div2)); //true alert(div1.isSameNode(div2)); //false </code></pre> <p><strong>这里创建了两个具有相同特性的<code><div></code>元素。这两个元素相等,但不相同。</strong> DOM3 级还针对为 DOM 节点添加额外数据引入了新方法。其中,setUserData()方法会将数据指定给节点,它接受 3 个参数:要设置的键、实际的数据(可以是任何数据类型)和处理函数。以下代码可以将数据指定给一个节点。</p> <pre><code class="language-js">document.body.setUserData("name", "Nicholas", function(){}); </code></pre> <p>然后,使用 getUserData()并传入相同的键,就可以取得该数据,如下所示:</p> <pre><code class="language-js">var value = document.body.getUserData("name"); </code></pre> <p>传入 setUserData()中的处理函数会在带有数据的节点被复制、删除、重命名或引入一个文档时调用,因而你可以事先决定在上述操作发生时如何处理用户数据。<br /> 处理函数接受 5 个参数:表示操作类型的数值(1 表示复制,2 表示导入,3 表示删除,4 表示重命名)、数据键、数据值、源节点和目标节点。在删除节点时,源节点是 null;除在复制节点时,目标节点均为 null。在函数内部,你可以决定如何存储数据。来看下面的例子。 </p> <pre><code class="language-js">var div = document.createElement("div"); div.setUserData("name", "Nicholas", function(operation, key, value, src, dest){ if (operation == 1){ dest.setUserData(key, value, function(){}); } }); var newDiv = div.cloneNode(true); alert(newDiv.getUserData("name")); //"Nicholas" </code></pre> <p>这里,先创建了一个<code><div></code>元素,然后又为它添加了一些数据(用户数据)。在使用 cloneNode()复制这个元素时,就会调用处理函数,从而将数据自动复制到了副本节点。结果在通过副本节点调用getUserData()时,就会返回与原始节点中包含的相同的值。 </p></li> <li>框架的变化 框架和内嵌框架分别用 HTMLFrameElement 和 HTMLIFrameElement 表示,它们在 DOM2级中都有了一个新属性,名叫 contentDocument。这个属性包含一个指针,指向表示框架内容的文档对象。在此之前,无法直接通过元素取得这个文档对象(只能使用 frames 集合)。可以像下面这样使用这个属性。 <pre><code class="language-js">var iframe = document.getElementById("myIframe"); var iframeDoc = iframe.contentDocument; //在 IE8 以前的版本中无效</code></pre> <p>由于 contentDocument 属性是 Document 类型的实例,因此可以像使用其他 HTML 文档一样使用它,包括所有属性和方法。<br /> <strong>Opera、Firefox、Safari 和 Chrome 支持这个属性。</strong><br /> IE8 之前不支持框架中的 contentDocument 属性,但支持一个名叫 contentWindow 的属性,该属性返回框架的 window 对象,而这个 window 对象又有一个 document 属性。<br /> 想在上述所有浏览器中访问内嵌框架的文档对象,可以使用下列代码。 </p> <pre><code class="language-js">var iframe = document.getElementById("myIframe"); var iframeDoc = iframe.contentDocument || iframe.contentWindow.document; </code></pre> <p><strong>所有浏览器都支持 contentWindow 属性。</strong></p></li> </ol> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.19</h1> <p>阅读进度: 12.2.2 P317</p> <h2>2. 样式</h2> <p>在 HTML 中定义样式的方式有 3 种:通过<code><link/></code>元素包含外部样式表文件、使用<code><style/></code>元素定义嵌入式样式,以及使用 style 特性定义针对特定元素的样式。“DOM2 级样式”模块围绕这 3 种应用样式的机制提供了一套 API。要确定浏览器是否支持 DOM2 级定义的 CSS 能力,可以使用下列代码。</p> <pre><code class="language-js">var supportsDOM2CSS = document.implementation.hasFeature("CSS", "2.0"); var supportsDOM2CSS2 = document.implementation.hasFeature("CSS2", "2.0"); </code></pre> <h3>2.1 访问元素的样式</h3> <p>任何支持 style 特性的 HTML 元素在 JavaScript 中都有一个对应的 style 属性。<br /> 在 style 特性中指定的任何 CSS 属性都将表现为这个style 对象的相应属性。对于使用短划线(分隔不同的词汇,例如 background-image)的 CSS 属性名,必须将其转换成驼峰大小写形式,才能通过 JavaScript 来访问。下表列出了几个常见的 CSS 属性及其在 style 对象中对应的属性名。见p313<br /> <strong>一个不能直接转换的 CSS 属性就是 float。</strong> 由于 float 是 JavaScript 中的保留字,因此不能用作属性名。<br /> “DOM2 级样式”规范规定样式对象上相应的属性名应该是 cssFloat;Firefox、Safari、Opera 和 Chrome 都支持这个属性,而 IE支持的则是 styleFloat。<br /> 只要取得一个有效的 DOM 元素的引用,就可以随时使用 JavaScript 为其设置样式。以下是几个例子。</p> <pre><code class="language-js">var myDiv = document.getElementById("myDiv"); //设置背景颜色 myDiv.style.backgroundColor = "red"; //改变大小 myDiv.style.width = "100px"; myDiv.style.height = "200px"; //指定边框 myDiv.style.border = "1px solid black";</code></pre> <p>在以这种方式改变样式时,元素的外观会自动被更新。 <strong>在标准模式下,所有度量值都必须指定一个度量单位。</strong> 在混杂模式下,可以将style.width 设置为"20",浏览器会假设它是"20px";但在标准模式下,将style.width 设置为"20"会导致被忽略——因为没有度量单位。在实践中,最好始终都指定度量单位。<br /> 通过 style 对象同样可以取得在 style 特性中指定的样式。以下面的 HTML 代码为例。</p> <pre><code class="language-html"><div id="myDiv" style="background-color:blue; width:10px; height:25px"></div> </code></pre> <p>在 style 特性中指定的样式信息可以通过下列代码取得。</p> <pre><code class="language-js">alert(myDiv.style.backgroundColor); //"blue" alert(myDiv.style.width); //"10px" alert(myDiv.style.height); //"25px" </code></pre> <p>如果没有为元素设置 style 特性,那么 style 对象中可能会包含一些默认的值,但这些值并不能准确地反映该元素的样式信息。 </p> <ol> <li>DOM 样式属性和方法 “DOM2级样式”规范还为 style 对象定义了一些属性和方法。这些属性和方法在提供元素的 style特性值的同时,也可以修改样式。下面是属性和方法: <ul> <li>cssText:如前所述,通过它能够访问到 style 特性中的 CSS 代码。</li> <li>length:应用给元素的 CSS 属性的数量。</li> <li>parentRule:表示 CSS 信息的 CSSRule 对象。本节后面将讨论 CSSRule 类型。</li> <li>getPropertyCSSValue(propertyName):返回包含给定属性值的 CSSValue 对象。</li> <li>getPropertyPriority(propertyName):如果给定的属性使用了!important 设置,则返回"important";否则,返回空字符串。</li> <li>getPropertyValue(propertyName):返回给定属性的字符串值。</li> <li>item(index):返回给定位置的 CSS 属性的名称。</li> <li>removeProperty(propertyName):从样式中删除给定属性。</li> <li>setProperty(propertyName,value,priority):将给定属性设置为相应的值,并加上优先权标志("important"或者一个空字符串)。<br /> 通过cssText 属性可以访问 style特性中的 CSS代码。在读取模式下,cssText 返回浏览器对style特性中 CSS 代码的内部表示。在写入模式下,赋给 cssText 的值会重写整个 style 特性的值;也就是说,以前通过 style 特性指定的样式信息都将丢失。例如,如果通过 style 特性为元素设置了边框,然后再以不包含边框的规则重写 cssText,那么就会抹去元素上的边框。下面是使用 cssText 属性的一个例子。 <pre><code class="language-js">myDiv.style.cssText = "width: 25px; height: 100px; background-color: green"; alert(myDiv.style.cssText); </code></pre> <p>设置 cssText 是为元素应用多项变化最快捷的方式,因为可以一次性地应用所有变化。<br /> 设计 length 属性的目的,就是将其与 item()方法配套使用,以便迭代在元素中定义的 CSS 属性。在使用 length 和 item()时,style 对象实际上就相当于一个集合,都可以使用方括号语法来代替item()来取得给定位置的 CSS 属性,如下面的例子所示。 </p> <pre><code class="language-js">for (var i=0, len=myDiv.style.length; i < len; i++){ alert(myDiv.style[i]); //或者 myDiv.style.item(i) } </code></pre> <p>无论是使用方括号语法还是使用 item()方法,都可以取得 CSS 属性名("background-color",不是"backgroundColor")。然后,就可以在 getPropertyValue()中使用取得的属性名进一步取得属性的值,如下所示。 </p> <pre><code class="language-js">var prop, value, i, len; for (i=0, len=myDiv.style.length; i < len; i++){ prop = myDiv.style[i]; //或者 myDiv.style.item(i) value = myDiv.style.getPropertyValue(prop); alert(prop + " : " + value); } </code></pre> <p>getPropertyValue()方法取得的始终都是 CSS 属性值的字符串表示。如果你需要更多信息,可以使用 getPropertyCSSValue()方法,它返回一个包含两个属性的 CSSValue 对象,这两个属性分别是:cssText 和 cssValueType。其中,cssText 属性的值与 getPropertyValue()返回的值相同,而 cssValueType 属性则是一个数值常量,表示值的类型:0 表示继承的值,1 表示基本的值,2 表示值列表,3 表示自定义的值。以下代码既输出 CSS 属性值,也输出值的类型。 </p> <pre><code class="language-js">var prop, value, i, len; for (i=0, len=myDiv.style.length; i < len; i++){ prop = myDiv.style[i]; //或者 myDiv.style.item(i) value = myDiv.style.getPropertyCSSValue(prop); alert(prop + " : " + value.cssText + " (" + value.cssValueType + ")"); } </code></pre> <p>在实际开发中,getPropertyCSSValue()使用得比 getPropertyValue()少得多。 <strong>IE9+、Safarie3+以及 Chrome 支持这个方法。Firefox 7 及之前版本也提供这个访问,但调用总返回 null。</strong> 要从元素的样式中移除某个 CSS 属性,需要使用 removeProperty()方法。使用这个方法移除一个属性,意味着将会为该属性应用默认的样式(从其他样式表经层叠而来)。例如,要移除通过 style特性设置的 border 属性,可以使用下面的代码。 </p> <pre><code class="language-js">myDiv.style.removeProperty("border"); </code></pre> <p>在不确定某个给定的 CSS 属性拥有什么默认值的情况下,就可以使用这个方法。只要移除相应的属性,就可以为元素应用默认值。</p></li> </ul></li> <li>计算的样式 虽然 style 对象能够提供支持 style 特性的任何元素的样式信息,但它不包含那些从其他样式表层叠而来并影响到当前元素的样式信息。“DOM2 级样式”增强了 document.defaultView,提供了getComputedStyle()方法。这个方法接受两个参数:要取得计算样式的元素和一个伪元素字符串(例如":after")。如果不需要伪元素信息,第二个参数可以是 null。getComputedStyle()方法返回一个 CSSStyleDeclaration 对象(与 style 属性的类型相同),其中包含当前元素的所有计算的样式。 <pre><code class="language-html"><!DOCTYPE html> <html> <head> <title>Computed Styles Example</title> <style type="text/css"> #myDiv { background-color: blue; width: 100px; height: 200px; } </style> </head> <body> <div id="myDiv" style="background-color: red; border: 1px solid black"></div> </body> </html> </code></pre> <p>应用给这个例子中<code><div></code>元素的样式一方面来自嵌入式样式表(<code><style></code>元素中的样式),另一方面来自其 style 特性。但是,style 特性中设置了 backgroundColor 和 border,没有设置 width和 height,后者是通过样式表规则应用的。以下代码可以取得这个元素计算后的样式。 </p> <pre><code class="language-js">var myDiv = document.getElementById("myDiv"); var computedStyle = document.defaultView.getComputedStyle(myDiv, null); alert(computedStyle.backgroundColor); // "red" alert(computedStyle.width); // "100px" alert(computedStyle.height); // "200px" alert(computedStyle.border); // 在某些浏览器中是"1px solid black" </code></pre> <p>在这个元素计算后的样式中,背景颜色的值是"red",宽度值是"100px",高度值是"200px"。我们注意到,背景颜色不是"blue",因为这个样式在自身的 style 特性中已经被覆盖了。边框属性可能会也可能不会返回样式表中实际的 border 规则(Opera 会返回,但其他浏览器不会)。<br /> 存在这个差别的 原因是不同浏览器解释综合(rollup)属性(如 border)的方式不同,因为设置这种属性实际上会涉及很多其他属性。在设置 border 时,实际上是设置了四个边的边框宽度、颜色、样式属性( border-left-width 、 border-top-color 、 border-bottom-style , 等 等 )。 因 此 , 即 使computedStyle.border 不会在所有浏览器中都返回值,但 computedStyle.borderLeftWidth 会返回值。<br /> <strong>需要注意的是,即使有些浏览器支持这种功能,但表示值的方式可能会有所区别。例如,Firefox 和 Safari 会将所有颜色转换成 RGB 格式(例如红色是 rgb(255,0,0))。</strong><br /> IE 不支持 getComputedStyle()方法,但它有一种类似的概念。在 IE 中,每个具有 style 属性的元素还有一个 currentStyle 属性。这个属性是 CSSStyleDeclaration 的实例,包含当前元素全部计算后的样式。取得这些样式的方式也差不多,如下面的例子所示。</p> <pre><code class="language-js">var myDiv = document.getElementById("myDiv"); var computedStyle = myDiv.currentStyle; alert(computedStyle.backgroundColor); //"red" alert(computedStyle.width); //"100px" alert(computedStyle.height); //"200px" alert(computedStyle.border); //undefined </code></pre> <p><strong>与 DOM 版本的方式一样,IE 也没有返回 border 样式,因为这是一个综合属性。</strong><br /> 无论在哪个浏览器中,最重要的一条是要记住所有计算的样式都是只读的;不能修改计算后样式对象中的 CSS 属性。此外,计算后的样式也包含属于浏览器内部样式表的样式信息,因此任何具有默认值的 CSS 属性都会表现在计算后的样式中。例如,所有浏览器中的 visibility 属性都有一个默认值,但这个值会因实现而异。在默认情况下,有的浏览器将 visibility 属性设置为"visible",而有的浏览器则将其设置为"inherit"。换句话说,不能指望某个 CSS 属性的默认值在不同浏览器中是相同的。<strong>如果你需要元素具有某个特定的默认值,应该手工在样式表中指定该值。</strong></p></li> </ol> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.20</h1> <p>阅读进度: 12.2.3 P320</p> <h3>2.3 操作样式表</h3> <p>CSSStyleSheet 类型表示的是样式表,包括通过<code><link></code>元素包含的样式表和在<code><style></code>元素中定义的样式表。有读者可能记得,这两个元素本身分别是由 HTMLLinkElement 和 HTMLStyleElement 类型表示的。但是,CSSStyleSheet 类型相对更加通用一些,它只表示样式表,而不管这些样式表在 HTML中是如何定义的。此外,上述两个针对元素的类型允许修改 HTML 特性,但 CSSStyleSheet 对象则是一套只读的接口(有一个属性例外)。<br /> 使用下面的代码可以确定浏览器是否支持 DOM2 级样式表。 </p> <pre><code class="language-js">var supportsDOM2StyleSheets = document.implementation.hasFeature("StyleSheets", "2.0"); </code></pre> <p>CSSStyleSheet 继承自 StyleSheet,后者可以作为一个基础接口来定义非 CSS 样式表。从StyleSheet 接口继承而来的属性如下:</p> <ul> <li>disabled:表示样式表是否被禁用的布尔值。这个属性是可读/写的,将这个值设置为 true 可以禁用样式表。</li> <li>href:如果样式表是通过<code><link></code>包含的,则是样式表的 URL;否则,是 null。</li> <li>media:当前样式表支持的所有媒体类型的集合。与所有 DOM 集合一样,这个集合也有一个length 属性和一个 item()方法。也可以使用方括号语法取得集合中特定的项。如果集合是空列表,表示样式表适用于所有媒体。在 IE 中,media 是一个反映<code><link></code>和<code><style></code>元素 media特性值的字符串。</li> <li>ownerNode:指向拥有当前样式表的节点的指针,样式表可能是在 HTML 中通过<code><link></code>或<code><style/></code>引入的(在 XML 中可能是通过处理指令引入的)。如果当前样式表是其他样式表通过@import 导入的,则这个属性值为 null。IE 不支持这个属性。</li> <li>parentStyleSheet:在当前样式表是通过@import 导入的情况下,这个属性是一个指向导入它的样式表的指针。</li> <li>title:ownerNode 中 title 属性的值。</li> <li>type:表示样式表类型的字符串。对 CSS 样式表而言,这个字符串是"type/css"。<br /> 除 了disabled 属性之外,其他属性都是只读的。在支持以上所有这些属性的基础上,CSSStyleSheet 类型还支持下列属性和方法:</li> <li>cssRules:样式表中包含的样式规则的集合。IE 不支持这个属性,但有一个类似的 rules 属性。</li> <li>ownerRule:如果样式表是通过@import 导入的,这个属性就是一个指针,指向表示导入的规则;否则,值为 null。IE 不支持这个属性。</li> <li>deleteRule(index):删除 cssRules 集合中指定位置的规则。IE 不支持这个方法,但支持一个类似的 removeRule()方法。</li> <li>insertRule(rule,index):向 cssRules 集合中指定的位置插入 rule 字符串。IE 不支持这个方法,但支持一个类似的 addRule()方法。<br /> 应用于文档的所有样式表是通过 document.styleSheets 集合来表示的。通过这个集合的 length属性可以获知文档中样式表的数量,而通过方括号语法或 item()方法可以访问每一个样式表。来看一个例子。 <pre><code class="language-js">var sheet = null; for (var i=0, len=document.styleSheets.length; i < len; i++){ sheet = document.styleSheets[i]; alert(sheet.href); } </code></pre> <p>以上代码可以输出文档中使用的每一个样式表的 href 属性(<strong><code><style></code>元素包含的样式表没有href 属性</strong>)。<br /> 不同浏览器的 document.styleSheets 返回的样式表也不同。所有浏览器都会包含<code><style></code>元素和 rel 特性被设置为"stylesheet"的<code><link></code>元素引入的样式表。IE 和 Opera 也包含 rel 特性被设置为"alternate stylesheet"的<code><link></code>元素引入的样式表。<br /> 也可以直接通过<code><link></code>或<code><style></code>元素取得 CSSStyleSheet 对象。DOM 规定了一个包含CSSStyleSheet 对象的属性,名叫 sheet;除了 IE,其他浏览器都支持这个属性。IE 支持的是styleSheet 属性。要想在不同浏览器中都能取得样式表对象,可以使用下列代码。 </p> <pre><code class="language-js">function getStyleSheet(element){ return element.sheet || element.styleSheet; } //取得第一个<link/>元素引入的样式表 var link = document.getElementsByTagName("link")[0]; var sheet = getStylesheet(link); </code></pre> <p>这里的 getStyleSheet()返回的样式表对象与 document.styleSheets 集合中的样式表对象相同。 </p> <h3>2.3.1 CSS 规则</h3> <p>CSSRule 对象表示样式表中的每一条规则。实际上,CSSRule 是一个供其他多种类型继承的基类型,其中最常见的就是 CSSStyleRule 类型,表示样式信息(其他规则还有@import、@font-face、@page 和@charset,但这些规则很少有必要通过脚本来访问)。CSSStyleRule 对象包含下列属性。 </p></li> <li>cssText:返回整条规则对应的文本。由于浏览器对样式表的内部处理方式不同,返回的文本可能会与样式表中实际的文本不一样;Safari 始终都会将文本转换成全部小写。IE 不支持这个属性。</li> <li>parentRule:如果当前规则是导入的规则,这个属性引用的就是导入规则;否则,这个值为null。IE 不支持这个属性。</li> <li>parentStyleSheet:当前规则所属的样式表。IE 不支持这个属性。</li> <li>selectorText:返回当前规则的选择符文本。由于浏览器对样式表的内部处理方式不同,返回的文本可能会与样式表中实际的文本不一样(例如,Safari 3 之前的版本始终会将文本转换成全部小写)。在 Firefox、Safari、Chrome 和 IE 中这个属性是只读的。Opera允许修改 selectorText。</li> <li>style:一个 CSSStyleDeclaration 对象,可以通过它设置和取得规则中特定的样式值。</li> <li>type:表示规则类型的常量值。对于样式规则,这个值是 1。IE 不支持这个属性。<br /> 其中三个最常用的属性是 cssText、selectorText 和 style。cssText 属性与 style.cssText属性类似,但并不相同。前者包含选择符文本和围绕样式信息的花括号,后者只包含样式信息(类似于元素的 style.cssText)。此外,cssText 是只读的,而 style.cssText 也可以被重写。<br /> 大多数情况下,仅使用 style 属性就可以满足所有操作样式规则的需求了。这个对象就像每个元素上的 style 属性一样,可以通过它读取和修改规则中的样式信息。以下面的 CSS 规则为例。 <pre><code class="language-css">div.box { background-color: blue; width: 100px; height: 200px; } </code></pre> <p>假设这条规则位于页面中的第一个样式表中,而且这个样式表中只有这一条样式规则,那么通过下列代码可以取得这条规则的各种信息。</p> <pre><code class="language-js">var sheet = document.styleSheets[0]; var rules = sheet.cssRules || sheet.rules; //取得规则列表 var rule = rules[0]; //取得第一条规则 alert(rule.selectorText); //"div.box" alert(rule.style.cssText); //完整的 CSS 代码 alert(rule.style.backgroundColor); //"blue" alert(rule.style.width); //"100px" alert(rule.style.height); //"200px" </code></pre> <p>使用这种方式,可以像确定元素的行内样式信息一样,确定与规则相关的样式信息。与使用元素的方式一样,在这种方式下也可以修改样式信息,如下面的例子所示。</p> <pre><code class="language-js">var sheet = document.styleSheets[0]; var rules = sheet.cssRules || sheet.rules; //取得规则列表 var rule = rules[0]; //取得第一条规则 rule.style.backgroundColor = "red" </code></pre> <p>必须要注意的是,以这种方式修改规则会影响页面中适用于该规则的所有元素。换句话说,如果有两个带有 box 类的<code><div></code>元素,那么这两个元素都会应用修改后的样式。 </p> <h3>2.3.2 创建规则</h3> <p>DOM 规定,要向现有样式表中添加新规则,需要使用 insertRule()方法。这个方法接受两个参数:规则文本和表示在哪里插入规则的索引。下面是一个例子。 </p> <pre><code class="language-js">sheet.insertRule("body { background-color: silver }", 0); //DOM 方法</code></pre> <p>这个例子插入的规则会改变元素的背景颜色。插入的规则将成为样式表中的第一条规则(插入到了位置 0)——规则的次序在确定层叠之后应用到文档的规则时至关重要。Firefox、Safari、Opera 和 Chrome都支持 insertRule()方法。<br /> IE8 及更早版本支持一个类似的方法,名叫 addRule(),也接收两必选参数:选择符文本和 CSS样式信息;一个可选参数:插入规则的位置。在 IE 中插入与前面例子相同的规则,可使用如下代码。 </p> <pre><code class="language-js">sheet.addRule("body", "background-color: silver", 0); //仅对 IE 有效</code></pre> <p><strong>有关这个方法的规定中说,最多可以使用 addRule()添加 4 095 条样式规则。超出这个上限的调用将会导致错误。</strong> 要以跨浏览器的方式向样式表中插入规则,可以使用下面的函数。这个函数接受 4 个参数:要向其中添加规则的样式表以及与 addRule()相同的 3 个参数,如下所示。</p> <pre><code class="language-js">function insertRule(sheet, selectorText, cssText, position){ if (sheet.insertRule){ sheet.insertRule(selectorText + "{" + cssText + "}", position); } else if (sheet.addRule){ sheet.addRule(selectorText, cssText, position); } } </code></pre> <p>下面是调用这个函数的示例代码。</p> <pre><code class="language-js">insertRule(document.styleSheets[0], "body", "background-color: silver", 0); </code></pre> <h3>2.3.3 删除规则</h3> <p>从样式表中删除规则的方法是 deleteRule(),这个方法接受一个参数:要删除的规则的位置。例如,要删除样式表中的第一条规则,可以使用以下代码。</p> <pre><code class="language-js">sheet.deleteRule(0); //DOM 方法</code></pre> <p>IE 支持的类似方法叫 removeRule(),使用方法相同,如下所示:</p> <pre><code class="language-js">sheet.removeRule(0); //仅对 IE 有效</code></pre></li> </ul> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.21</h1> <p>阅读进度: 12.3 P326</p> <h3>2.3 元素大小</h3> <p>本节介绍的属性和方法并不属于“DOM2 级样式”规范,但却与 HTML 元素的样式息息相关。DOM中没有规定如何确定页面中元素的大小。IE 为此率先引入了一些属性,以便开发人员使用。目前,所有主要的浏览器都已经支持这些属性。 </p> <ol> <li>偏移量 首先要介绍的属性涉及偏移量(offset dimension),包括元素在屏幕上占用的所有可见的空间。元素的可见大小由其高度、宽度决定,包括所有内边距、滚动条和边框大小(注意,不包括外边距)。通过下列 4 个属性可以取得元素的偏移量。 <ul> <li>offsetHeight:元素在垂直方向上占用的空间大小,以像素计。包括元素的高度、(可见的)水平滚动条的高度、上边框高度和下边框高度。</li> <li>offsetWidth:元素在水平方向上占用的空间大小,以像素计。包括元素的宽度、(可见的)垂直滚动条的宽度、左边框宽度和右边框宽度。</li> <li>offsetLeft:元素的左外边框至包含元素的左内边框之间的像素距离。</li> <li>offsetTop:元素的上外边框至包含元素的上内边框之间的像素距离。<br /> 其中,offsetLeft 和 offsetTop 属性与包含元素有关,包含元素的引用保存在 offsetParent属性中。offsetParent 属性不一定与 parentNode 的值相等。例如,<code><td></code>元素的 offsetParent 是作为其祖先元素的<code><table></code>元素,因为<code><table></code>是在 DOM 层次中距<code><td></code>最近的一个具有大小的元素。<br /> 12-1 形象地展示了上面几个属性表示的不同大小。见p321<br /> 要想知道某个元素在页面上的偏移量,将这个元素的 offsetLeft 和 offsetTop 与其 offsetParent的相同属性相加,如此循环直至根元素,就可以得到一个基本准确的值。以下两个函数就可以用于分别取得元素的左和上偏移量。 <pre><code class="language-js">function getElementLeft(element){ var actualLeft = element.offsetLeft; var current = element.offsetParent; while (current !== null){ actualLeft += current.offsetLeft; current = current.offsetParent; } return actualLeft; } function getElementTop(element){ var actualTop = element.offsetTop; var current = element.offsetParent; while (current !== null){ actualTop += current. offsetTop; current = current.offsetParent; } return actualTop; } </code></pre> <p>这两个函数利用 offsetParent 属性在 DOM 层次中逐级向上回溯,将每个层次中的偏移量属性合计到一块。对于简单的 CSS 布局的页面,这两函数可以得到非常精确的结果。对于使用表格和内嵌框架布局的页面,由于不同浏览器实现这些元素的方式不同,因此得到的值就不太精确了。<br /> <strong>所有这些偏移量属性都是只读的,而且每次访问它们都需要重新计算。因此,应该尽量避免重复访问这些属性;如果需要重复使用其中某些属性的值,可以将它们保存在局部变量中,以提高性能。</strong> </p></li> </ul></li> <li>客户区大小 元素的客户区大小(client dimension),指的是元素内容及其内边距所占据的空间大小。有关客户区大小的属性有两个:clientWidth 和 clientHeight。其中,clientWidth 属性是元素内容区宽度加上左右内边距宽度;clientHeight 属性是元素内容区高度加上上下内边距高度。图 12-2 形象地说明了这些属性表示的大小。见p322 从字面上看,客户区大小就是元素内部的空间大小,因此滚动条占用的空间不计算在内。<br /> 如下面的例子所示,要确定浏览器视口大小,可以使用 document.documentElement 或 document.body(在 IE7 之前的版本中)的clientWidth 和 clientHeight。 <pre><code class="language-js">function getViewport(){ if (document.compatMode == "BackCompat"){ return { width: document.body.clientWidth, height: document.body.clientHeight }; } else { return { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight }; } } </code></pre> <p>这个函数首先检查 document.compatMode 属性,以确定浏览器是否运行在混杂模式。<br /> Safari 3.1之前的版本不支持这个属性,因此就会自动执行 else 语句。Chrome、Opera 和 Firefox 大多数情况下都运行在标准模式下,因此它们也会前进到 else 语句。这个函数会返回一个对象,包含两个属性:width和 height;表示浏览器视口(<code><html></code>或<code><body></code>元素)的大小。<br /> <strong>与偏移量相似,客户区大小也是只读的,也是每次访问都要重新计算的。</strong></p></li> <li>滚动大小 最后要介绍的是滚动大小(scroll dimension),指的是包含滚动内容的元素的大小。以下是 4 个与滚动大小相关的属性。 <ul> <li>scrollHeight:在没有滚动条的情况下,元素内容的总高度。</li> <li>scrollWidth:在没有滚动条的情况下,元素内容的总宽度。</li> <li>scrollLeft:被隐藏在内容区域左侧的像素数。通过设置这个属性可以改变元素的滚动位置。</li> <li>scrollTop:被隐藏在内容区域上方的像素数。通过设置这个属性可以改变元素的滚动位置。 scrollWidth 和 scrollHeight 主要用于确定元素内容的实际大小。例如,通常认为<code><html></code>元素是在 Web 浏览器的视口中滚动的元素(<strong>IE6 之前版本运行在混杂模式下时是<code><body></code>元素</strong>)。因此,带有垂直滚动条的页面总高度就是 document.documentElement.scrollHeight。<br /> 直滚动条的页面总高度就是 document.documentElement.scrollHeight。 对于不包含滚动条的页面而言, scrollWidth 和 scrollHeight 与 clientWidth 和clientHeight 之间的关系并不十分清晰。在这种情况下,基于 document.documentElement 查看这些属性会在不同浏览器间发现一些不一致性问题,如下所述。 </li> <li>Firefox 中这两组属性始终都是相等的,但大小代表的是文档内容区域的实际尺寸,而非视口的尺寸。</li> <li>Opera、Safari 3.1 及更高版本、Chrome 中的这两组属性是有差别的,其中 scrollWidth 和scrollHeight 等于视口大小,而 clientWidth 和 clientHeight 等于文档内容区域的大小。 </li> <li>IE(在标准模式)中的这两组属性不相等,其中 scrollWidth 和 scrollHeight 等于文档内容区域的大小,而 clientWidth 和 clientHeight 等于视口大小。<br /> 在确定文档的总高度时(包括基于视口的最小高度时),必须取得 scrollWidth/clientWidth 和scrollHeight/clientHeight 中的最大值,才能保证在跨浏览器的环境下得到精确的结果。下面就是这样一个例子。 <pre><code class="language-js">var docHeight = Math.max(document.documentElement.scrollHeight, document.documentElement.clientHeight); var docWidth = Math.max(document.documentElement.scrollWidth, document.documentElement.clientWidth); </code></pre> <p>注意,对于运行在混杂模式下的 IE,则需要用 document.body 代替 document.documentElement。 通过 scrollLeft 和 scrollTop 属性既可以确定元素当前滚动的状态,也可以设置元素的滚动位置。在元素尚未被滚动时,这两个属性的值都等于 0。如果元素被垂直滚动了,那么 scrollTop 的值会大于 0,且表示元素上方不可见内容的像素高度。如果元素被水平滚动了,那么 scrollLeft 的值会大于 0,且表示元素左侧不可见内容的像素宽度。<br /> 下面这个函数会检测元素是否位于顶部,如果不是就将其回滚到顶部。 </p> <pre><code class="language-js">function scrollToTop(element){ if (element.scrollTop != 0){ element.scrollTop = 0; } } </code></pre></li> </ul></li> <li> <p>确定元素大小 IE、Firefox 3+、Safari 4+、Opera 9.5及 Chrome为每个元素都提供了一个 getBoundingClientRect()方法。这个方法返回会一个矩形对象,包含 4 个属性:left、top、right 和 bottom。这些属性给出了元素在页面中相对于视口的位置。<br /> 浏览器的实现稍有不同。IE8 及更早版本认为文档的左上角坐标是(2, 2),而其他浏览器包括 IE9 则将传统的(0,0)作为起点坐标。因此,就需要在一开始检查一下位于(0,0)处的元素的位置,在 IE8 及更早版本中,会返回(2,2),而在其他浏览器中会返回(0,0)。来看下面的函数: </p> <pre><code class="language-js">function getBoundingClientRect(element){ if (typeof arguments.callee.offset != "number"){ var scrollTop = document.documentElement.scrollTop; var temp = document.createElement("div"); temp.style.cssText = "position:absolute;left:0;top:0;"; document.body.appendChild(temp); arguments.callee.offset = -temp.getBoundingClientRect().top - scrollTop; document.body.removeChild(temp); temp = null; } var rect = element.getBoundingClientRect(); var offset = arguments.callee.offset; return { left: rect.left + offset, right: rect.right + offset, top: rect.top + offset, bottom: rect.bottom + offset }; } </code></pre> <p>解释:<br /> 这个函数使用了它自身的属性来确定是否要对坐标进行调整。第一步是检测属性是否有定义,如果没有就定义一个。最终的 offset 会被设置为新元素上坐标的负值,实际上就是在 IE 中设置为2,在Firefox 和 Opera 中设置为0。为此,需要创建一个临时的元素,将其位置设置在(0,0),然后再调用其getBoundingClientRect()。而之所以要减去视口的 scrollTop,是为了防止调用这个函数时窗口被滚动了。这样编写代码,就无需每次调用这个函数都执行两次 getBoundingClientRect()了。接下来,再在传入的元素上调用这个方法并基于新的计算公式创建一个对象。<br /> 对于不支持 getBoundingClientRect()的浏览器,可以通过其他手段取得相同的信息。一般来说,right 和 left 的差值与 offsetWidth 的值相等,而 bottom 和 top 的差值与 offsetHeight相等。而且,left 和 top 属性大致等于使用本章前面定义的 getElementLeft()和 getElementTop()函数取得的值。综合上述,就可以创建出下面这个跨浏览器的函数:</p> <pre><code class="language-js">function getBoundingClientRect(element){ var scrollTop = document.documentElement.scrollTop; var scrollLeft = document.documentElement.scrollLeft; if (element.getBoundingClientRect){ if (typeof arguments.callee.offset != "number"){ var temp = document.createElement("div"); temp.style.cssText = "position:absolute;left:0;top:0;"; document.body.appendChild(temp); arguments.callee.offset = -temp.getBoundingClientRect().top - scrollTop; document.body.removeChild(temp); temp = null; } var rect = element.getBoundingClientRect(); var offset = arguments.callee.offset; return { left: rect.left + offset, right: rect.right + offset, top: rect.top + offset, bottom: rect.bottom + offset }; } else { var actualLeft = getElementLeft(element); var actualTop = getElementTop(element); return { left: actualLeft - scrollLeft, right: actualLeft + element.offsetWidth - scrollLeft, top: actualTop - scrollTop, bottom: actualTop + element.offsetHeight - scrollTop } } } </code></pre> <p>这个函数在 getBoundingClientRect()有效时,就使用这个原生方法,而在这个方法无效时则使用默认的计算公式。在某些情况下,这个函数返回的值可能会有所不同,例如使用表格布局或使用滚动元素的情况下。 <strong>由于这里使用了 arguments.callee,所以这个方法不能在严格模式下使用。</strong></p> </li> </ol> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.22</h1> <p>阅读进度: 12.3.2 P330</p> <h2>3 遍历</h2> <p>“DOM2 级遍历和范围”模块定义了两个用于辅助完成顺序遍历 DOM 结构的类型:NodeIterator和 TreeWalker。这两个类型能够基于给定的起点对 DOM 结构执行深度优先(depth-first)的遍历操作。<br /> 在与 DOM 兼容的浏览器中(<strong>Firefox 1 及更高版本、Safari 1.3 及更高版本、Opera 7.6 及更高版本、Chrome0.2 及更高版本</strong>),都可以访问到这些类型的对象。IE 不支持 DOM 遍历。使用下列代码可以检测浏览器对 DOM2 级遍历能力的支持情况。 </p> <pre><code class="language-js">var supportsTraversals = document.implementation.hasFeature("Traversal", "2.0"); var supportsNodeIterator = (typeof document.createNodeIterator == "function"); var supportsTreeWalker = (typeof document.createTreeWalker == "function"); </code></pre> <p>如前所述,DOM 遍历是深度优先的 DOM 结构遍历,也就是说,移动的方向至少有两个(取决于使用的遍历类型)。遍历以给定节点为根,不可能向上超出 DOM 树的根节点。以下面的 HTML 页面为例。 </p> <pre><code class="language-js"><!DOCTYPE html> <html> <head> <title>Example</title> </head> <body> <p><b>Hello</b> world!</p> </body> </html></code></pre> <p>页面的 DOM 树见p327 图12-4 任何节点都可以作为遍历的根节点。如果假设<code><body></code>元素为根节点,那么遍历的第一步就是访问<code><p></code>元素,然后再访问同为<code><body></code>元素后代的两个文本节点。不过,这次遍历永远不会到达<code><html></code>、<code><head></code>元素,也不会到达不属于<code><body></code>元素子树的任何节点。而以 document 为根节点的遍历则可以访问到文档中的全部节点。图 12-5 展示了对以 document 为根节点的 DOM 树进行深度优先遍历的先后顺序。<br /> 从 document 开始依序向前,访问的第一个节点是 document,访问的最后一个节点是包含"world!"的文本节点。从文档最后的文本节点开始,遍历可以反向移动到 DOM 树的顶端。此时,访问的第一个节点是包含"Hello"的文本节点,访问的最后一个节点是 document 节点。NodeIterator和 TreeWalker 都以这种方式执行遍历。 </p> <h3>3.1 NodeIterator</h3> <p>NodeIterator 类型是两者中比较简单的一个,可以使用 document.createNodeIterator()方法创建它的新实例。这个方法接受下列 4 个参数。 </p> <ul> <li>root:想要作为搜索起点的树中的节点。</li> <li>whatToShow:表示要访问哪些节点的数字代码。</li> <li>filter:是一个 NodeFilter 对象,或者一个表示应该接受还是拒绝某种特定节点的函数。</li> <li>entityReferenceExpansion:布尔值,表示是否要扩展实体引用。这个参数在 HTML 页面中没有用,因为其中的实体引用不能扩展。<br /> whatToShow 参数是一个位掩码,通过应用一或多个过滤器(filter)来确定要访问哪些节点。这个参数的值以常量形式在 NodeFilter 类型中定义,如下所示。见p328 除了 NodeFilter.SHOW_ALL 之外,可以使用按位或操作符来组合多个选项,如下面的例子所示: <pre><code class="language-js">var whatToShow = NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT; </code></pre> <p>可以通过 createNodeIterator()方法的 filter 参数来指定自定义的 NodeFilter 对象,或者指定一个功能类似节点过滤器(node filter)的函数。每个 NodeFilter 对象只有一个方法,即acceptNode();如果应该访问给定的节点,该方法返回 NodeFilter.FILTER_ACCEPT,如果不应该访问给定的节点,该方法返回 NodeFilter.FILTER_SKIP。<br /> 由于 NodeFilter 是一个抽象的类型,因此不能直接创建它的实例。在必要时,只要创建一个包含 acceptNode()方法的对象,然后将这个对象传入createNodeIterator()中即可。<br /> 例如,下列代码展示了如何创建一个只显示<code><p></code>元素的节点迭代器。 </p> <pre><code class="language-js">var filter = { acceptNode: function(node){ return node.tagName.toLowerCase() == "p" ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; } }; var iterator = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT, filter, false); </code></pre> <p>第三个参数也可以是一个与 acceptNode()方法类似的函数,如下所示。</p> <pre><code class="language-js">var filter = function(node){ return node.tagName.toLowerCase() == "p" ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; }; var iterator = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT, filter, false); </code></pre> <p>一般来说,这就是在 JavaScript 中使用这个方法的形式,这种形式比较简单,而且也跟其他的JavaScript 代码很相似。如果不指定过滤器,那么应该在第三个参数的位置上传入 null。<br /> 下面的代码创建了一个能够访问所有类型节点的简单的 NodeIterator。 </p> <pre><code class="language-js">var iterator = document.createNodeIterator(document, NodeFilter.SHOW_ALL, null, false); </code></pre> <p><strong>NodeIterator 类型的两个主要方法是 nextNode()和 previousNode()。</strong> 在深度优先的 DOM 子树遍历中,nextNode()方法用于向前前进一步,而 previousNode()用于向后后退一步。<br /> 在刚刚创建的 NodeIterator 对象中,有一个内部指针指向根节点,因此第一次调用 nextNode()会返回根节点。当遍历到 DOM 子树的最后一个节点时,nextNode()返回 null。previousNode()方法的工作机制类似。当遍历到 DOM 子树的最后一个节点,且 previousNode()返回根节点之后,再次调用它就会返回 null。<br /> 以下面的 HTML 片段为例。 </p> <pre><code class="language-html"><div id="div1"> <p><b>Hello</b> world!</p> <ul> <li>List item 1</li> <li>List item 2</li> <li>List item 3</li> </ul> </div> </code></pre> <p>假设我们想要遍历<code><div></code>元素中的所有元素,那么可以使用下列代码。 </p> <pre><code class="language-js">var div = document.getElementById("div1"); var iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, null, false); var node = iterator.nextNode(); while (node !== null) { alert(node.tagName); //输出标签名 node = iterator.nextNode(); } </code></pre> <p>在这个例子中,第一次调用 nextNode()返回<code><p></code>元素。因为在到达 DOM 子树末端时 nextNode()返回 null,所以这里使用了 while 语句在每次循环时检查对 nextNode()的调用是否返回了 null。执行上面的代码会显示如下标签名: </p></li> <li>DIV</li> <li>P</li> <li>B</li> <li>UL</li> <li>LI</li> <li>LI</li> <li>LI<br /> 也许用不着显示那么多信息,你只想返回遍历中遇到的<code><li></code>元素。很简单,只要使用一个过滤器即可,如下面的例子所示。 <pre><code class="language-js">var div = document.getElementById("div1"); var filter = function(node){ return node.tagName.toLowerCase() == "li" ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; }; var iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, filter, false); var node = iterator.nextNode(); while (node !== null) { alert(node.tagName); //输出标签名 node = iterator.nextNode(); } </code></pre> <p>由于 nextNode()和 previousNode()方法都基于 NodeIterator 在 DOM 结构中的内部指针工作,所以 DOM 结构的变化会反映在遍历的结果中。<br /> <strong>Firefox 3.5 之前的版本没有实现 createNodeIterator()方法,但却支持下一节要讨论的 createTreeWalker()方法。</strong></p></li> </ul> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.23</h1> <p>阅读进度: 12.4.1.1 P333</p> <h3>3.2 TreeWalker</h3> <p>TreeWalker 是 NodeIterator 的一个更高级的版本。除了包括 nextNode()和 previousNode()在内的相同的功能之外,这个类型还提供了下列用于在不同方向上遍历 DOM 结构的方法。 </p> <ul> <li>parentNode():遍历到当前节点的父节点;</li> <li>firstChild():遍历到当前节点的第一个子节点;</li> <li>lastChild():遍历到当前节点的最后一个子节点;</li> <li>nextSibling():遍历到当前节点的下一个同辈节点;</li> <li>previousSibling():遍历到当前节点的上一个同辈节点。<br /> 创建 TreeWalker 对象要使用 document.createTreeWalker()方法,这个方法接受的 4 个参数与 document.createNodeIterator()方法相同:<br /> 作为遍历起点的根节点、要显示的节点类型、过滤器和一个表示是否扩展实体引用的布尔值。由于这两个创建方法很相似,所以很容易用 TreeWalker来代替 NodeIterator,如下面的例子所示。 <pre><code class="language-html"><div id="div1"> <p><b>Hello</b> world!</p> <ul> <li>List item 1</li> <li>List item 2</li> <li>List item 3</li> </ul> </div> </code></pre> <pre><code class="language-js">var div = document.getElementById("div1"); var filter = function(node){ return node.tagName.toLowerCase() == "li"? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; }; var walker= document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT, filter, false); var node = iterator.nextNode(); while (node !== null) { alert(node.tagName); //输出标签名 node = iterator.nextNode(); } </code></pre> <p>在使用 NodeIterator 对象时,NodeFilter.FILTER_SKIP 与 NodeFilter.FILTER_REJECT 的作用相同:跳过指定的节点。但在使用 TreeWalker 对象时,NodeFilter.FILTER_SKIP 会跳过相应节点继续前进到子树中的下一个节点,而 NodeFilter.FILTER_REJECT 则会跳过相应节点及该节点的整个子树。<br /> 例如,将前面例子中的NodeFilter.FILTER_SKIP 修改成 NodeFilter.FILTER_REJECT,结果就是不会访问任何节点。这是因为第一个返回的节点是<code><div></code>,它的标签名不是"li",于是就会返回 NodeFilter.FILTER_REJECT,这意味着遍历会跳过整个子树。在这个例子中,<code><div></code>元素是遍历的根节点,于是结果就会停止遍历。 当然,TreeWalker 真正强大的地方在于能够在 DOM 结构中沿任何方向移动。使用 TreeWalker遍历 DOM 树,即使不定义过滤器,也可以取得所有<code><li></code>元素,如下面的代码所示。 </p> <pre><code class="language-js">var div = document.getElementById("div1"); var walker = document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT, null, false); walker.firstChild(); //转到<p> walker.nextSibling(); //转到<ul> var node = walker.firstChild(); //转到第一个<li> while (node !== null) { alert(node.tagName); node = walker.nextSibling(); } </code></pre> <p>因为我们知道<code><li></code>元素在文档结构中的位置,所以可以直接定位到那里,即使用 firstChild()转到<code><p></code>元素,使用 nextSibling()转到<code><ul></code>元素,然后再使用 firstChild()转到第一个<code><li></code>元素。注意,此处 TreeWalker 只返回元素(由传入到 createTreeWalker()的第二个参数决定)。因此,可以放心地使用 nextSibling()访问每一个<code><li></code>元素,直至这个方法最后返回 null。<br /> TreeWalker 类型还有一个属性,名叫 currentNode,表示任何遍历方法在上一次遍历中返回的节点。通过设置这个属性也可以修改遍历继续进行的起点,如下面的例子所示。 </p> <pre><code class="language-js">var node = walker.nextNode(); alert(node === walker.currentNode); //true walker.currentNode = document.body; //修改起点</code></pre> <p>与 NodeIterator 相比,TreeWalker 类型在遍历 DOM 时拥有更大的灵活性。<strong>由于 IE 中没有对应的类型和方法,所以使用遍历的跨浏览器解决方案非常少见。</strong> </p> <h2>4.范围</h2> <p>为了让开发人员更方便地控制页面,“DOM2 级遍历和范围”模块定义了“范围”(range)接口。通过范围可以选择文档中的一个区域,而不必考虑节点的界限(选择在后台完成,对用户是不可见的)。<br /> <strong>Firefox、Opera、Safari 和Chrome 都支持 DOM 范围。IE 以专有方式实现了自己的范围特性。</strong> </p> <h3>4.1 DOM中的范围</h3> <p>DOM2 级在 Document 类型中定义了 createRange()方法。在兼容 DOM 的浏览器中,这个方法属于 document 对象。使用 hasFeature()或者直接检测该方法,都可以确定浏览器是否支持范围。 </p> <pre><code class="language-js">var supportsRange = document.implementation.hasFeature("Range", "2.0"); var alsoSupportsRange = (typeof document.createRange == "function"); </code></pre> <p>如果浏览器支持范围,那么就可以使用 createRange()来创建 DOM 范围,如下所示:</p> <pre><code class="language-js">var range = document.createRange(); </code></pre> <p>与节点类似,新创建的范围也直接与创建它的文档关联在一起,不能用于其他文档。创建了范围之后,接下来就可以使用它在后台选择文档中的特定部分。而创建范围并设置了其位置之后,还可以针对范围的内容执行很多种操作,从而实现对底层 DOM 树的更精细的控制。<br /> 每个范围由一个 Range 类型的实例表示,这个实例拥有很多属性和方法。下列属性提供了当前范围在文档中的位置信息。 </p></li> <li>startContainer:包含范围起点的节点(即选区中第一个节点的父节点)。</li> <li>startOffset:范围在 startContainer 中起点的偏移量。如果 startContainer 是文本节点、注释节点或 CDATA 节点,那么 startOffset 就是范围起点之前跳过的字符数量。否则,startOffset 就是范围中第一个子节点的索引。</li> <li>endContainer:包含范围终点的节点(即选区中最后一个节点的父节点)。</li> <li>endOffset:范围在 endContainer 中终点的偏移量(与 startOffset 遵循相同的取值规则)。</li> <li>commonAncestorContainer:startContainer 和 endContainer 共同的祖先节点在文档树中位置最深的那个。</li> </ul> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.24</h1> <p>阅读进度: 12.4.1.4 P337</p> <h3>4.1</h3> <ol> <li>用 DOM 范围实现简单选择<br /> 要使用范围来选择文档中的一部分,最简的方式就是使用 selectNode()或 selectNodeContents()。<br /> 这两个方法都接受一个参数,即一个 DOM 节点,然后使用该节点中的信息来填充范围。其中,selectNode()方法选择整个节点,包括其子节点;而 selectNodeContents()方法则只选择节点的子节点。以下面的 HTML 代码为例。 <pre><code class="language-html"><!DOCTYPE html> <html> <body> <p id="p1"><b>Hello</b> world!</p> </body> </html> </code></pre> <p>我们可以使用下列代码来创建范围:</p> <pre><code class="language-js">var range1 = document.createRange(); range2 = document.createRange(); p1 = document.getElementById("p1"); range1.selectNode(p1); range2.selectNodeContents(p1); </code></pre> <p>这里创建的两个范围包含文档中不同的部分:rang1 包含<code><p/></code>元素及其所有子元素,而 rang2 包含<code><b/></code>元素、文本节点"Hello"和文本节点"world!"(如图 12-6 所示)。见p333<br /> 在调用 selectNode()时,startContainer、endContainer 和 commonAncestorContainer都等于传入节点的父节点,也就是这个例子中的 document.body。而 startOffset 属性等于给定节点在其父节点的 childNodes 集合中的索引(在这个例子中是 1——因为兼容 DOM 的浏览器将空格算作一个文本节点),endOffset 等于 startOffset 加 1(因为只选择了一个节点)。<br /> 在调用 selectNodeContents()时,startContainer、endContainer 和 commonAncestorContainer 等于传入的节点,即这个例子中的<code><p></code>元素。而 startOffset 属性始终等于 0,因为范围从给定节点的第一个子节点开始。最后,endOffset 等于子节点的数量(node.childNodes.length),在这个例子中是 2。<br /> 此外,为了更精细地控制将哪些节点包含在范围中,还可以使用下列方法。 </p> <ul> <li>setStartBefore(refNode):将范围的起点设置在 refNode 之前,因此 refNode 也就是范围选区中的第一个子节点。同时会将 startContainer 属性设置为 refNode.parentNode,将startOffset 属性设置为 refNode 在其父节点的 childNodes 集合中的索引。</li> <li>setStartAfter(refNode):将范围的起点设置在 refNode 之后,因此 refNode 也就不在范围之内了,其下一个同辈节点才是范围选区中的第一个子节点。同时会将 startContainer 属性设置为 refNode.parentNode,将 startOffset 属性设置为 refNode 在其父节点的childNodes 集合中的索引加 1。</li> <li>setEndBefore(refNode):将范围的终点设置在 refNode 之前,因此 refNode 也就不在范围之内了,其上一个同辈节点才是范围选区中的最后一个子节点。同时会将 endContainer 属性设置为refNode.parentNode,将 endOffset 属性设置为 refNode 在其父节点的 childNodes集合中的索引。</li> <li>setEndAfter(refNode):将范围的终点设置在 refNode 之后,因此 refNode 也就是范围选区中的最后一个子节点。同时会将 endContainer 属性设置为 refNode.parentNode,将endOffset 属性设置为 refNode 在其父节点的 childNodes 集合中的索引加 1。<br /> 在调用这些方法时,所有属性都会自动为你设置好。不过,要想创建复杂的范围选区,也可以直接指定这些属性的值。 </li> </ul></li> <li>用 DOM 范围实现复杂选择<br /> 要创建复杂的范围就得使用 setStart()和 setEnd()方法。这两个方法都接受两个参数:一个参照节点和一个偏移量值。对 setStart()来说,参照节点会变成 startContainer,而偏移量值会变成startOffset。对于 setEnd()来说,参照节点会变成 endContainer,而偏移量值会变成 endOffset。<br /> 可以使用这两个方法来模仿 selectNode()和 selectNodeContents()。来看下面的例子: <pre><code class="language-js"> var range1 = document.createRange(); range2 = document.createRange(); p1 = document.getElementById("p1"); p1Index = -1; i, len; for (i=0, len=p1.parentNode.childNodes.length; i < len; i++) { if (p1.parentNode.childNodes[i] == p1) { p1Index = i; break; } }</code></pre></li> </ol> <p>range1.setStart(p1.parentNode, p1Index); range1.setEnd(p1.parentNode, p1Index + 1); range2.setStart(p1, 0); range2.setEnd(p1, p1.childNodes.length); </p> <pre><code>显然,要选择这个节点(使用 range1),就必须确定当前节点(p1)在其父节点的 childNodes集合中的索引。而要选择这个节点的内容(使用 range2),也不必计算什么;只要通过 setStart()和 setEnd()设置默认值即可。模仿 selectNode()和 selectNodeContents()并不是 setStart()和 setEnd()的主要用途,它们更胜一筹的地方在于能够选择节点的一部分。 假设你只想选择前面 HTML 示例代码中从"Hello"的"llo"到"world!"的"o"——很容易做到。第一步是取得所有节点的引用,如下面的例子所示: ```js var p1 = document.getElementById("p1"); helloNode = p1.firstChild.firstChild; worldNode = p1.lastChild; </code></pre> <p>实际上,"Hello"文本节点是<code><p></code>元素的孙子节点,因为它本身是<code><b></code>元素的一个子节点。因此,p1.firstChild取得的是<code><b></code>,而p1.firstChild.firstChild取得的才是这个文本节点。"world!"文本节点是<code><p></code>元素的第二个子节点(也是最后一个子节点),因此可以使用 p1.lastChild 取得该节点。然后,必须在创建范围时指定相应的起点和终点,如下面的例子所示。 </p> <pre><code class="language-js">var range = document.createRange(); range.setStart(helloNode, 2); range.setEnd(worldNode, 3); </code></pre> <p>因为这个范围的选区应该从"Hello"中"e"的后面开始,所以在 setStart()中传入 helloNode的同时,传入了偏移量 2(即"e"的下一个位置;"H"的位置是 0)。设置选区的终点时,在 setEnd()中传入 worldNode 的同时传入了偏移量 3,表示选区之外的第一个字符的位置,这个字符是"r",它的位置是 3(位置 0 上还有一个空格)。如图 12-7 所示。见p335<br /> 由于 helloNode 和 worldNode 都是文本节点,因此它们分别变成了新建范围的 startContainer和 endContainer。此时 startOffset 和 endOffset 分别用以确定两个节点所包含的文本中的位置,而不是用以确定子节点的位置(就像传入的参数为元素节点时那样)。此时的 commonAncestorContainer 是<code><p></code>元素,也就是同时包含这两个节点的第一个祖先元素。 </p> <ol start="3"> <li>操作 DOM 范围中的内容<br /> 在创建范围时 ,内部会为这个范围创建一个文档片段,范围所属的全部节点都被添加到了这个文档片段中。<br /> 为了创建这个文档片段,范围内容的格式必须正确有效。在前面的例子中,我们创建的选区分别开始和结束于两个文本节点的内部,因此不能算是格式良好的 DOM 结构,也就无法通过 DOM 来表示。但是,范围知道自身缺少哪些开标签和闭标签,它能够重新构建有效的 DOM 结构以便我们对其进行操作。<br /> 另外,文本节点"world!"也被拆分为两个文本节点,一个包含"wo",另一个包含"rld!"。最终的DOM 树如图 12-8 所示,右侧是表示范围的文档片段的内容。见p336<br /> 像这样创建了范围之后,就可以使用各种方法对范围的内容进行操作了(<strong>注意,表示范围的内部文档片段中的所有节点,都只是指向文档中相应节点的指针</strong>)。<br /> 第一个方法,也是最容易理解的方法,就是 deleteContents()。这个方法能够从文档中删除范围所包含的内容。例如: <pre><code class="language-js">var p1 = document.getElementById("p1"); helloNode = p1.firstChild.firstChild; worldNode = p1.lastChild; range = document.createRange(); range.setStart(helloNode, 2); range.setEnd(worldNode, 3); range.deleteContents(); </code></pre> <p>执行以上代码后,页面中会显示如下 HTML 代码:</p> <pre><code class="language-html"><p><b>He</b>rld!</p> </code></pre> <p>与 deleteContents()方法相似,extractContents()也会从文档中移除范围选区。但这两个方法的区别在于,extractContents()会返回范围的文档片段。利用这个返回的值,可以将范围的内容插入到文档中的其他地方。如下面的例子所示: </p> <pre><code class="language-js">var p1 = document.getElementById("p1"); helloNode = p1.firstChild.firstChild; worldNode = p1.lastChild; range = document.createRange(); range.setStart(helloNode, 2); range.setEnd(worldNode, 3); var fragment = range.extractContents(); p1.parentNode.appendChild(fragment); </code></pre> <p>在这个例子中,我们将提取出来的文档片段添加到了文档<code><body></code>元素的末尾。(记住,在将文档片段传入 appendChild()方法中时,添加到文档中的只是片段的子节点,而非片段本身。)结果得到如下HTML 代码:</p> <pre><code class="language-html"><p><b>He</b>rld!</p> <b>llo</b> wo </code></pre> <p>还一种做法,即使用 cloneContents()创建范围对象的一个副本,然后在文档的其他地方插入该副本。如下面的例子所示:</p> <pre><code class="language-js">helloNode = p1.firstChild.firstChild, worldNode = p1.lastChild, range = document.createRange(); range.setStart(helloNode, 2); range.setEnd(worldNode, 3); var fragment = range.cloneContents(); p1.parentNode.appendChild(fragment); </code></pre> <p>这个方法与 extractContents()非常类似,因为它们都返回文档片段。它们的主要区别在于,cloneContents()返回的文档片段包含的是范围中节点的副本,而不是实际的节点。执行上面的操作后,页面中的 HTML 代码应该如下所示: </p> <pre><code class="language-html"><p><b>Hello</b> world!</p> <b>llo</b> wo </code></pre> <p><strong>注意,那就是在调用上面介绍的方法之前,拆分的节点并不会产生格式良好的文档片段。换句话说,原始的 HTML 在 DOM 被修改之前会始终保持不变。</strong></p></li> </ol> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.25</h1> <p>阅读进度: 12.4.2 P340</p> <ol start="4"> <li>插入 DOM 范围中的内容<br /> 利用范围,可以删除或复制内容,还可以像前面介绍的那样操作范围中的内容。使用 insertNode()方法可以向范围选区的开始处插入一个节点。假设我们想在前面例子中的 HTML 前面插入以下 HTML代码: <pre><code class="language-html"><span style="color: red">Inserted text</span> </code></pre> <p>可以使用下列代码:</p> <pre><code class="language-js">var p1 = document.getElementById("p1"); helloNode = p1.firstChild.firstChild; worldNode = p1.lastChild; range = document.createRange(); range.setStart(helloNode, 2); range.setEnd(worldNode, 3); var span = document.createElement("span"); span.style.color = "red"; span.appendChild(document.createTextNode("Inserted text")); range.insertNode(span); </code></pre> <p>运行以上 JavaScript 代码,就会得到如下 HTML 代码:</p> <pre><code class="language-html"><p id="p1"><b>He<span style="color: red">Inserted text</span>llo</b> world</p> </code></pre> <p>注意,<code><span></code>正好被插入到了"Hello"中的"llo"前面,而该位置就是范围选区的开始位置。还要注意的是,由于这里没有使用上一节介绍的方法,结果原始的 HTML 并没有添加或删除<code><b></code>元素。使用这种技术可以插入一些帮助提示信息,例如在打开新窗口的链接旁边插入一幅图像。<br /> 除了向范围内部插入内容之外,还可以环绕范围插入内容,此时就要使用 surroundContents()方法。这个方法接受一个参数,即环绕范围内容的节点。在环绕范围插入内容时,后台会执行下列步骤。<br /> (1) 提取出范围中的内容(类似执行 extractContent());<br /> (2) 将给定节点插入到文档中原来范围所在的位置上;<br /> (3) 将文档片段的内容添加到给定节点中。<br /> 可以使用这种技术来突出显示网页中的某些词句,例如下列代码:</p> <pre><code class="language-js">var p1 = document.getElementById("p1"); helloNode = p1.firstChild.firstChild; worldNode = p1.lastChild; range = document.createRange(); range.selectNode(helloNode); var span = document.createElement("span"); span.style.backgroundColor = "yellow"; range.surroundContents(span);</code></pre> <p>会给范围选区加上一个黄色的背景。得到的 HTML 代码如下所示:</p> <pre><code class="language-html"><p><b><span style="background-color:yellow">Hello</span></b> world!</p> </code></pre></li> <li>折叠 DOM 范围<br /> 所谓折叠范围,就是指范围中未选择文档的任何部分。可以用文本框来描述折叠范围的过程。<br /> 假设文本框中有一行文本,你用鼠标选择了其中一个完整的单词。然后,你单击鼠标左键,选区消失,而光标则落在了其中两个字母之间。<br /> 同样,在折叠范围时,其位置会落在文档中的两个部分之间,可能是范围选区的开始位置,也可能是结束位置。图 12-9 展示了折叠范围时发生的情形。见p339<br /> 使用 collapse()方法来折叠范围,这个方法接受一个参数,一个布尔值,表示要折叠到范围的哪一端。参数 true 表示折叠到范围的起点,参数 false 表示折叠到范围的终点。要确定范围已经折叠完毕,可以检查 collapsed 属性,如下所示: <pre><code class="language-js">range.collapse(true); //折叠到起点 alert(range.collapsed); //输出 true</code></pre> <p>检测某个范围是否处于折叠状态,可以帮我们确定范围中的两个节点是否紧密相邻。例如,对于下面的 HTML 代码: </p> <pre><code class="language-html"><p id="p1">Paragraph 1</p><p id="p2">Paragraph 2</p> </code></pre> <p>如果我们不知道其实际构成(比如说,这行代码是动态生成的),那么可以像下面这样创建一个范围。 </p> <pre><code class="language-js">var p1 = document.getElementById("p1"), p2 = document.getElementById("p2"), range = document.createRange(); range.setStartAfter(p1); range.setStartBefore(p2); alert(range.collapsed); //输出 true </code></pre> <p>在这个例子中,新创建的范围是折叠的,因为 p1 的后面和 p2 的前面什么也没有。</p></li> <li>比较 DOM 范围<br /> 在有多个范围的情况下,可以使用 compareBoundaryPoints()方法来确定这些范围是否有公共的边界(起点或终点)。这个方法接受两个参数:表示比较方式的常量值和要比较的范围。表示比较方式的常量值如下所示。 <ul> <li>Range.START_TO_START(0):比较第一个范围和第二个范围的起点;</li> <li>Range.START_TO_END(1):比较第一个范围的起点和第二个范围的终点;</li> <li>Range.END_TO_END(2):比较第一个范围和第二个范围的终点;</li> <li>Range.END_TO_START(3):比较第一个范围的终点和第一个范围的起点。<br /> compareBoundaryPoints()方法可能的返回值如下:如果第一个范围中的点位于第二个范围中的点之前,返回-1;如果两个点相等,返回 0;如果第一个范围中的点位于第二个范围中的点之后,返回1。来看下面的例子。 <pre><code class="language-js">var range1 = document.createRange(); var range2 = document.createRange(); var p1 = document.getElementById("p1"); range1.selectNodeContents(p1); range2.selectNodeContents(p1); range2.setEndBefore(p1.lastChild); alert(range1.compareBoundaryPoints(Range.START_TO_START, range2)); //0 alert(range1.compareBoundaryPoints(Range.END_TO_END, range2)); //1 </code></pre> <p>在这个例子中,两个范围的起点实际上是相同的,因为它们的起点都是由 selectNodeContents()方法设置的默认值来指定的。因此,第一次比较返回 0。但是,range2 的终点由于调用 setEndBefore()已经改变了,结果是 range1 的终点位于 range2 的终点后面(见图 12-10 p340),因此第二次比较返回 1。 </p></li> </ul></li> <li>复制 DOM 范围<br /> 可以使用 cloneRange()方法复制范围。这个方法会创建调用它的范围的一个副本。 <pre><code class="language-js">var newRange = range.cloneRange(); </code></pre> <p>新创建的范围与原来的范围包含相同的属性,而修改它的端点不会影响原来的范围。</p></li> <li>清理 DOM 范围<br /> 在使用完范围之后,最好是调用 detach()方法,以便从创建范围的文档中分离出该范围。调用detach()之后,就可以放心地解除对范围的引用,从而让垃圾回收机制回收其内存了。来看下面的例子。 <pre><code class="language-js">range.detach(); //从文档中分离 range = null; //解除引用</code></pre> <p><strong>在使用范围的最后再执行这两个步骤是我们推荐的方式。一旦分离范围,就不能再恢复使用了。</strong></p></li> </ol> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.26</h1> <p>阅读进度: 第13章 P345</p> <h4>4.2 IE8 及更早版本中的范围</h4> <p>虽然 IE9 支持 DOM 范围,但 IE8 及之前版本不支持 DOM 范围。不过,IE8 及早期版本支持一种类似的概念,即文本范围(text range)。文本范围是 IE 专有的特性,其他浏览器都不支持。顾名思义,文本范围处理的主要是文本(不一定是 DOM 节点)。<br /> 通过<code><body>、<button>、<input></code>和<code><textarea></code>等这几个元素,可以调用 createTextRange()方法来创建文本范围。以下是一个例子: </p> <pre><code class="language-js">var range = document.body.createTextRange(); </code></pre> <p>像这样通过 document 创建的范围可以在页面中的任何地方使用(通过其他元素创建的范围则只能在相应的元素中使用)。与 DOM 范围类似,使用 IE 文本范围的方式也有很多种。 </p> <ol> <li>用 IE 范围实现简单的选择<br /> 选择页面中某一区域的最简单方式,就是使用范围的 findText()方法。<br /> 这个方法会找到第一次出现的给定文本,并将范围移过来以环绕该文本。如果没有找到文本,这个方法返回 false;否则返回true。同样,仍然以下面的 HTML 代码为例。 <pre><code class="language-html"><p id="p1"><b>Hello</b> world!</p> </code></pre> <p>要选择"Hello",可以使用下列代码。</p> <pre><code class="language-js">var range = document.body.createTextRange(); var found = range.findText("Hello"); </code></pre> <p>在执行完第二行代码之后,文本"Hello"就被包围在范围之内了。为此,可以检查范围的 text 属性来确认(这个属性返回范围中包含的文本),或者也可以检查 findText()的返回值——在找到了文本的情况下返回值为 true。例如: </p> <pre><code class="language-js">alert(found); //true alert(range.text); //"Hello" </code></pre> <p>findText()传入另一个参数,即一个表示向哪个方向继续搜索的数值。负值表示应该从当前位置向后搜索,而正值表示应该从当前位置向前搜索。<br /> 因此,要查找文档中前两个"Hello"的实例,应该使用下列代码。 </p> <pre><code class="language-js">var found = range.findText("Hello"); var foundAgain = range.findText("Hello", 1); </code></pre> <p>IE 中与 DOM 中的 selectNode()方法最接近的方法是 moveToElementText(),这个方法接受一个 DOM 元素,并选择该元素的所有文本,包括 HTML 标签。下面是一个例子。 </p> <pre><code class="language-js">var range = document.body.createTextRange(); var p1 = document.getElementById("p1"); range.moveToElementText(p1); </code></pre> <p>在文本范围中包含 HTML 的情况下,可以使用 htmlText 属性取得范围的全部内容,包括 HTML和文本,如下面的例子所示。 </p> <pre><code class="language-js">alert(range.htmlText); </code></pre> <p>parentElement()方法与 DOM 的 commonAncestorContainer 属性类似。 </p> <pre><code class="language-js">var ancestor = range.parentElement(); </code></pre> <p>这样得到的父元素始终都可以反映文本选区的父节点。 </p></li> <li>使用 IE 范围实现复杂的选择<br /> 在 IE 中创建复杂范围的方法,就是以特定的增量向四周移动范围。为此,IE 提供了 4 个方法:move()、moveStart()、moveEnd()和 expand()。这些方法都接受两个参数:移动单位和移动单位的数量。其中,移动单位是下列一种字符串值。 <ul> <li>"character":逐个字符地移动。</li> <li>"word":逐个单词(一系列非空格字符)地移动。</li> <li>"sentence":逐个句子(一系列以句号、问号或叹号结尾的字符)地移动。</li> <li>"textedit":移动到当前范围选区的开始或结束位置。<br /> 通过 moveStart()方法可以移动范围的起点,通过 moveEnd()方法可以移动范围的终点,移动的幅度由单位数量指定,如下面的例子所示。 <pre><code class="language-js">range.moveStart("word", 2); //起点移动 2 个单词 range.moveEnd("character", 1); //终点移动 1 个字符</code></pre> <p>使用 expand()方法可以将范围规范化。换句话说,expand()方法的作用是将任何部分选择的文本全部选中。例如,当前选择的是一个单词中间的两个字符,调用 expand("word")可以将整个单词都包含在范围之内。<br /> 而 move()方法则首先会折叠当前范围(让起点和终点相等),然后再将范围移动指定的单位数量,如下面的例子所示。 </p> <pre><code class="language-js">range.move("character", 5); //移动 5 个字符</code></pre> <p>调用 move()之后,<strong>范围的起点和终点相同</strong>,因此必须再使用 moveStart()或 moveEnd()创建新的选区。 </p></li> </ul></li> <li>操作 IE 范围中的内容<br /> 在 IE 中操作范围中的内容可以使用 text 属性或 pasteHTML()方法。如前所述,通过 text 属性可以取得范围中的内容文本;但是,也可以通过这个属性设置范围中的内容文本。 <pre><code class="language-js">var range = document.body.createTextRange(); range.findText("Hello"); range.text = "Howdy"; </code></pre> <p>如果仍以前面的 Hello World 代码为例,执行以上代码后的 HTML 代码如下。</p> <pre><code class="language-html"><p id="p1"><b>Howdy</b> world!</p> </code></pre> <p><strong>注意,在设置 text 属性的情况下,HTML 标签保持不变。</strong><br /> 要向范围中插入 HTML 代码,就得使用 pasteHTML()方法,如下面的例子所示。</p> <pre><code class="language-js">var range = document.body.createTextRange(); range.findText("Hello"); range.pasteHTML("<em>Howdy</em>"); </code></pre> <p>执行这些代码后,会得到如下 HTML。 </p> <pre><code class="language-html"><p id="p1"><b><em>Howdy</em></b> world!</p> </code></pre></li> <li>折叠 IE 范围<br /> IE 为范围提供的 collapse()方法与相应的 DOM 方法用法一样:传入 true 把范围折叠到起点,传入 false 把范围折叠到终点。例如: <pre><code class="language-js">range.collapse(true); //折叠到起点</code></pre> <p>没有对应的 collapsed 属性让我们知道范围是否已经折叠完毕。为此,必须使用 boundingWidth 属性,该属性返回范围的宽度(以像素为单位)。如果 boundingWidth 属性等于 0, 就说明范围已经折叠了: </p> <pre><code class="language-js">var isCollapsed = (range.boundingWidth == 0); </code></pre> <p>还有 boundingHeight、boundingLeft 和 boundingTop 等属性,虽然它们都不像boundingWidth 那么有用,但也可以提供一些有关范围位置的信息。 </p></li> <li>比较 IE 范围<br /> IE 中的 compareEndPoints()方法与 DOM 范围的 compareBoundaryPoints()方法类似。<br /> 这个方法接受两个参数:比较的类型和要比较的范围。比较类型的取值范围是下列几个字符串值:"StartToStart"、"StartToEnd"、"EndToEnd"和"EndToStart"。这几种比较类型与比较 DOM 范围时使用的几个值是相同的。<br /> 同样与 DOM 类似的是,compareEndPoints()方法也会按照相同的规则返回值,即如果第一个范围的边界位于第二个范围的边界前面,返回-1;如果二者边界相同,返回 0;如果第一个范围的边界位于第二个范围的边界后面,返回 1。仍以前面的 Hello World 代码为例,下列代码将创建两个范围,一个选择"Hello world!"(包括<code><b></code>标签),另一个选择"Hello"。 <pre><code class="language-js">var range1 = document.body.createTextRange(); var range2 = document.body.createTextRange(); range1.findText("Hello world!"); range2.findText("Hello"); alert(range1.compareEndPoints("StartToStart", range2)); //0 alert(range1.compareEndPoints("EndToEnd", range2)); //1</code></pre> <p>由于这两个范围共享同一个起点,所以使用 compareEndPoints()比较起点返回 0。而 range1的终点在 range2 的终点后面,所以 compareEndPoints()返回 1。<br /> IE 中还有两个方法,也是用于比较范围的:isEqual()用于确定两个范围是否相等,inRange()用于确定一个范围是否包含另一个范围。下面是相应的示例。 </p> <pre><code class="language-js">var range1 = document.body.createTextRange(); var range2 = document.body.createTextRange(); range1.findText("Hello World"); range2.findText("Hello"); alert("range1.isEqual(range2): " + range1.isEqual(range2)); //false alert("range1.inRange(range2):" + range1.inRange(range2)); //true </code></pre> <p>由于这两个范围的终点不同,所以它们不相等,调用 isEqual()返回 false。由于 range2 实际位于 range1 内部,它的终点位于后者的终点之前、起点之后,所以 range2 被包含在 range1 内部,调用 inRange()返回 true。</p></li> <li>复制 IE 范围 在 IE 中使用 duplicate()方法可以复制文本范围,结果会创建原范围的一个副本,如下面的例子所示。 <pre><code class="language-js">var newRange = range.duplicate(); </code></pre> <p><strong>新创建的范围会带有与原范围完全相同的属性。</strong></p> <h2>5. 小结</h2> <p>“DOM2 级核心”为不同的 DOM 类型引入了一些与 XML 命名空间有关的方法。<strong>这些变化只在使用 XML 或 XHTML 文档时才有用</strong>;对于 HTML 文档没有实际意义。除了与 XML 命名空间有关的方法外,“DOM2 级核心”还定义了以编程方式创建Document 实例的方法,也支持了创建 DocumentType 对象。<br /> “DOM2 级样式”模块主要针对操作元素的样式信息而开发,其特性简要总结如下。 </p> <ul> <li>每个元素都有一个关联的 style 对象,可以用来确定和修改行内的样式。</li> <li>要确定某个元素的计算样式(包括应用给它的所有 CSS 规则),可以使用 getComputedStyle()方法。</li> <li>IE不支持 getComputedStyle()方法,但为所有元素都提供了能够返回相同信息 currentStyle属性。</li> <li>可以通过 document.styleSheets 集合访问样式表。</li> <li>除 IE 之外的所有浏览器都支持针对样式表的这个接口,IE 也为几乎所有相应的 DOM 功能提供了自己的一套属性和方法。 “DOM2 级遍历和范围”模块提供了与 DOM 结构交互的不同方式,简要总结如下。 </li> <li>遍历即使用 NodeIterator 或 TreeWalker 对 DOM 执行深度优先的遍历。</li> <li>NodeIterator 是一个简单的接口,只允许以一个节点的步幅前后移动。而 TreeWalker 在提供相同功能的同时,还支持在 DOM 结构的各个方向上移动,包括父节点、同辈节点和子节点等方向。</li> <li>范围是选择 DOM 结构中特定部分,然后再执行相应操作的一种手段。</li> <li>使用范围选区可以在删除文档中某些部分的同时,保持文档结构的格式良好,或者复制文档中的相应部分。</li> <li>IE8 及更早版本不支持“DOM2 级遍历和范围”模块,但它提供了一个专有的文本范围对象,可以用来完成简单的基于文本的范围操作。IE9 完全支持 DOM 遍历。</li> </ul></li> </ol> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.27</h1> <p>阅读进度: 13.2 P348</p> <h1>13 事件</h1> <p>事件,就是文档或浏览器窗口中发生的一些特定的交互瞬间。事件最早是在 IE3 和 Netscape Navigator 2 中出现的,当时是作为分担服务器运算负载的一种手段。在 IE4 和 Navigator 4 发布时,这两种浏览器都提供了相似但不相同的 API,这些 API 并存经过了好几个主要版本。DOM2 级规范开始尝试以一种符合逻辑的方式来标准化 DOM 事件。IE9、Firefox、Opera、Safari 和 Chrome 全都已经实现了“DOM2 级事件”模块的核心部分。IE8 是最后一个仍然使用其专有事件系统的主要浏览器。 </p> <h2>1.事件流</h2> <p>事件流描述的是从页面中接收事件的顺序。但有意思的是,IE 和 Netscape 开发团队居然提出了差不多是完全相反的事件流的概念。IE 的事件流是事件冒泡流,而 Netscape Communicator 的事件流是事件捕获流。 </p> <h3>1.1 事件冒泡</h3> <p>IE 的事件流叫做事件冒泡(event bubbling),即事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。以下面的 HTML 页面为例: </p> <pre><code class="language-html"><!DOCTYPE html> <html> <head> <title>Event Bubbling Example</title> </head> <body> <div id="myDiv">Click Me</div> </body> </html> </code></pre> <p>如果你单击了页面中的<code><div></code>元素,那么这个 click 事件会按照如下顺序传播: (1) <code><div></code><br /> (2) <code><body></code><br /> (3) <code><html></code><br /> (4) document<br /> 也就是说,click 事件首先在<code><div></code>元素上发生,而这个元素就是我们单击的元素。然后,click事件沿 DOM 树向上传播,在每一级节点上都会发生,直至传播到 document 对象。<br /> 图 13-1 展示了事件冒泡的过程。见p346<br /> 所有现代浏览器都支持事件冒泡,但在具体实现上还是有一些差别。<strong>IE5.5 及更早版本中的事件冒泡会跳过<code><html></code>元素(从<code><body></code>直接跳到 document)。IE9、Firefox、Chrome 和 Safari 则将事件一直冒泡到 window 对象。</strong> </p> <h3>1.2 事件捕获</h3> <p>Netscape Communicator 团队提出的另一种事件流叫做事件捕获(event capturing)。事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。事件捕获的用意在于在事件到达预定目标之前捕获它。如果仍以前面的 HTML 页面作为演示事件捕获的例子,那么单击<code><div></code>元素就会以下列顺序触发 click 事件。<br /> (1) document<br /> (2) <code><html></code> (3) <code><body></code> (4) <code><div></code> 在事件捕获过程中,document 对象首先接收到 click 事件,然后事件沿 DOM 树依次向下,一直传播到事件的实际目标,即<code><div></code>元素。图 13-2 展示了事件捕获的过程。见p347<br /> 虽然事件捕获是 Netscape Communicator 唯一支持的事件流模型,但 IE9、Safari、Chrome、Opera和 Firefox 目前也都支持这种事件流模型。 </p> <h3>1.3 DOM事件流</h3> <p>“DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。<br /> 首先发生的是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。<br /> 以前面简单的 HTML 页面为例,单击<code><div></code>元素会按照图13-3所示顺序触发事件。<br /> 在 DOM 事件流中,实际的目标(<code><div></code>元素)在捕获阶段不会接收到事件。这意味着在捕获阶段,事件从 document 到<code><html></code>再到<code><body></code>后就停止了。<strong>下一个阶段是“处于目标”阶段,于是事件在<code><div></code>上发生,并在事件处理(后面将会讨论这个概念)中被看成冒泡阶段的一部分。</strong>然后,冒泡阶段发生,事件又传播回文档。<br /> 多数支持 DOM 事件流的浏览器都实现了一种特定的行为;即使“DOM2 级事件”规范明确要求捕获阶段不会涉及事件目标,但 IE9、Safari、Chrome、Firefox 和 Opera 9.5 及更高版本都会在捕获阶段触发事件对象上的事件。<br /> <strong>IE9、Opera、Firefox、Chrome 和 Safari 都支持 DOM 事件流;IE8 及更早版本不支持 DOM 事件流。</strong></p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.28</h1> <p>阅读进度: 13.3 P355</p> <h2>2.事件处理程序</h2> <p>事件就是用户或浏览器自身执行的某种动作。诸如 click、load 和 mouseover,都是事件的名字。而响应某个事件的函数就叫做事件处理程序(或事件侦听器)。事件处理程序的名字以"on"开头,因此click 事件的事件处理程序就是 onclick,load 事件的事件处理程序就是 onload。 </p> <h3>2.1 HTML事件处理程序</h3> <p>例如,要在按钮被单击时执行一些 JavaScript,可以像下面这样编写代码: </p> <pre><code class="language-js"><input type="button" value="Click Me" onclick="alert('Clicked')" /> </code></pre> <p>这个操作是通过指定 onclick 特性并将一些 JavaScript代码作为它的值来定义的。<br /> 由于这个值是 JavaScript,因此不能在其中使用未经转义的 HTML 语法字符,例如和号(&)、双引号("")、小于号(<)或大于号(>)。为了避免使用 HTML 实体,这里使用了单引号。如果想要使用双引号,那么就要将代码改写成如下所示: </p> <pre><code class="language-js"><input type="button" value="Click Me" onclick="alert(&quot;Clicked&quot;)" /> </code></pre> <p>在 HTML 中定义的事件处理程序可以包含要执行的具体动作,也可以调用在页面其他地方定义的脚本,如下面的例子所示: </p> <pre><code class="language-js"><script type="text/javascript"> function showMessage(){ alert("Hello world!"); } </script></code></pre> <pre><code class="language-html"><input type="button" value="Click Me" onclick="showMessage()" /> </code></pre> <p>在这个例子中,单击按钮就会调用 showMessage()函数。这个函数是在一个独立的<code><script></code>元素中定义的,当然也可以被包含在一个外部文件中。<strong>事件处理程序中的代码在执行时,有权访问全局作用域中的任何代码。</strong><br /> 这样会创建一个封装着元素属性值的函数。这个函数中有一个局部变量 event,也就是事件对象。 </p> <pre><code class="language-html"><!-- 输出 "click" --> <input type="button" value="Click Me" onclick="alert(event.type)"> </code></pre> <p>通过 event 变量,可以直接访问事件对象,你不用自己定义它,也不用从函数的参数列表中读取。在这个函数内部,this 值等于事件的目标元素,例如: </p> <pre><code class="language-html"><!-- 输出 "Click Me" --> <input type="button" value="Click Me" onclick="alert(this.value)"> </code></pre> <p>关于这个动态创建的函数,另一个有意思的地方是它扩展作用域的方式。在这个函数内部,可以像访问局部变量一样访问 document 及该元素本身的成员。这个函数使用 with 像下面这样扩展作用域: </p> <pre><code class="language-js">function(){ with(document){ with(this){ //元素属性值 } } } </code></pre> <p>如此一来,事件处理程序要访问自己的属性就简单多了。下面这行代码与前面的例子效果相同: </p> <pre><code class="language-html"><!-- 输出 "Click Me" --> <input type="button" value="Click Me" onclick="alert(value)"> </code></pre> <p>如果当前元素是一个表单输入元素,则作用域中还会包含访问表单元素(父元素)的入口,这个函数就变成了如下所示: </p> <pre><code class="language-js">function(){ with(document){ with(this.form){ with(this){ //元素属性值 } } } } </code></pre> <p>实际上,这样扩展作用域的方式,无非就是想让事件处理程序无需引用表单元素就能访问其他表单字段。例如: </p> <pre><code class="language-html"><form method="post"> <input type="text" name="username" value=""> <input type="button" value="Echo Username" onclick="alert(username.value)"> </form> </code></pre> <p>单击按钮会显示文本框中的文本。<strong>注意的是,这里直接引用了 username 元素。</strong> 不过,在 HTML 中指定事件处理程序有两个缺点。</p> <ol> <li>存在一个时差问题。因为用户可能会在HTML 元素一出现在页面上就触发相应的事件,但当时的事件处理程序有可能尚不具备执行条件。 很多 HTML 事件处理程序都会被封装在一个 try-catch 块中,以便错误不会浮出水面,如下面的例子所示: <pre><code class="language-html"><input type="button" value="Click Me" onclick="try{showMessage();}catch(ex){}"> </code></pre></li> <li>扩展事件处理程序的作用域链在不同浏览器中会导致不同结果。不同 JavaScript引擎遵循的标识符解析规则略有差异,很可能会在访问非限定对象成员时出错。<br /> 通过 HTML 指定事件处理程序的最后一个缺点是<strong>HTML 与 JavaScript 代码紧密耦合。</strong>如果要更换事件处理程序,就要改动两个地方:HTML 代码和 JavaScript 代码。 </li> </ol> <h3>2.2 DOM0 级事件处理程序</h3> <p>通过 JavaScript 指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。<br /> 原因一是简单,二是具有跨浏览器的优势。要使用 JavaScript 指定事件处理程序,首先必须取得一个要操作的对象的引用。<br /> 每个元素(包括 window 和 document)都有自己的事件处理程序属性,这些属性通常全部小写,例如 onclick。将这种属性的值设置为一个函数,就可以指定事件处理程序,如下所示: </p> <pre><code class="language-js">var btn = document.getElementById("myBtn"); btn.onclick = function(){ alert("Clicked"); }; </code></pre> <p>使用 DOM0 级方法指定的事件处理程序被认为是元素的方法。因此,这时候的事件处理程序是在元素的作用域中运行;换句话说,程序中的 this 引用当前元素。来看一个例子。 </p> <pre><code class="language-js">var btn = document.getElementById("myBtn"); btn.onclick = function(){ alert(this.id); //"myBtn" }; </code></pre> <p>以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理。<br /> 也可以删除通过 DOM0 级方法指定的事件处理程序,只要像下面这样将事件处理程序属性的值设置为 null 即可: </p> <pre><code class="language-js">btn.onclick = null; //删除事件处理程序</code></pre> <h3>2.3 DOM2 级事件处理程序</h3> <p>“DOM2级事件”定义了两个方法,用于处理指定和删除事件处理程序的操作:addEventListener()和 removeEventListener()。<br /> 所有 DOM 节点中都包含这两个方法,并且它们都接受 3 个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是 true,表示在捕获阶段调用事件处理程序;如果是 false,表示在冒泡阶段调用事件处理程序。<br /> 要在按钮上为 click 事件添加事件处理程序,可以使用下列代码:</p> <pre><code class="language-js">var btn = document.getElementById("myBtn"); btn.addEventListener("click", function(){ alert(this.id); }, false); </code></pre> <p>与 DOM0 级方法一样,这里添加的事件处理程序也是在其依附的元素的作用域中运行。<br /> 使用 DOM2 级方法添加事件处理程序的主要好处是可以添加多个事件处理程序。来看下面的例子。 </p> <pre><code class="language-js">var btn = document.getElementById("myBtn"); btn.addEventListener("click", function(){ alert(this.id); }, false); btn.addEventListener("click", function(){ alert("Hello world!"); }, false); </code></pre> <p>这里为按钮添加了两个事件处理程序。这两个事件处理程序会按照添加它们的顺序触发,因此首先会显示元素的 ID,其次会显示"Hello world!"消息。<br /> 通过 addEventListener()添加的事件处理程序只能使用 removeEventListener()来移除;移除时传入的参数与添加处理程序时使用的参数相同。这也意味着通过 addEventListener()添加的匿名函数将无法移除,如下面的例子所示。 </p> <pre><code class="language-js">var btn = document.getElementById("myBtn"); btn.addEventListener("click", function(){ alert(this.id); }, false); //这里省略了其他代码 btn.removeEventListener("click", function(){ //没有用! alert(this.id); }, false);</code></pre> <p>传入 removeEventListener() 中的事件处理程序函数必须与传入 addEventListener()中的相同,如下面的例子所示。 </p> <pre><code class="language-js">var btn = document.getElementById("myBtn"); var handler = function(){ alert(this.id); }; btn.addEventListener("click", handler, false); //这里省略了其他代码 btn.removeEventListener("click", handler, false); //有效!</code></pre> <p>大多数情况下,都是将事件处理程序添加到事件流的冒泡阶段,这样可以最大限度地兼容各种浏览器。最好只在需要在事件到达目标之前截获它的时候将事件处理程序添加到捕获阶段。<strong>如果不是特别需要,我们不建议在事件捕获阶段注册事件处理程序。</strong><br /> <strong>IE9、Firefox、Safari、Chrome 和 Opera 支持 DOM2 级事件处理程序。</strong> </p> <h3>2.4 IE事件处理程序</h3> <p>IE 实现了与 DOM 中类似的两个方法:attachEvent()和 detachEvent()。这两个方法接受相同的两个参数:事件处理程序名称与事件处理程序函数。由于 IE8 及更早版本只支持事件冒泡,所以通过attachEvent()添加的事件处理程序都会被添加到冒泡阶段。<br /> 要使用 attachEvent()为按钮添加一个事件处理程序: </p> <pre><code class="language-js">var btn = document.getElementById("myBtn"); btn.attachEvent("onclick", function(){ alert("Clicked"); }); </code></pre> <p><strong>注意,attachEvent()的第一个参数是"onclick",而非 DOM 的 addEventListener()方法中的"click"。</strong><br /> 在IE 中使用 attachEvent()与使用 DOM0 级方法的主要区别在于事件处理程序的作用域。<br /> 使用 DOM0 级方法的情况下,事件处理程序会在其所属元素的作用域内运行;在使用 attachEvent()方法的情况下,事件处理程序会在全局作用域中运行,因此 this 等于 window。 </p> <pre><code class="language-js">var btn = document.getElementById("myBtn"); btn.attachEvent("onclick", function(){ alert(this === window); //true }); </code></pre> <p>与 addEventListener()类似,attachEvent()方法也可以用来为一个元素添加多个事件处理程序。 </p> <pre><code class="language-js">var btn = document.getElementById("myBtn"); btn.attachEvent("onclick", function(){ alert("Clicked"); }); btn.attachEvent("onclick", function(){ alert("Hello world!"); }); </code></pre> <p>与 DOM方法不同的是,<strong>这些事件处理程序不是以添加它们的顺序执行,而是以相反的顺序被触发。</strong>单击这个例子中的按钮,首先看到的是"Hello world!",然后才是"Clicked"。<br /> 使用 attachEvent()添加的事件可以通过 detachEvent()来移除,条件是必须提供相同的参数。与 DOM 方法一样,这也意味着添加的匿名函数将不能被移除。不过,只要能够将对相同函数的引用传给 detachEvent(),就可以移除相应的事件处理程序。 </p> <pre><code class="language-js">var btn = document.getElementById("myBtn"); var handler = function(){ alert("Clicked"); }; btn.attachEvent("onclick", handler); //这里省略了其他代码 btn.detachEvent("onclick", handler); </code></pre> <p><strong>支持 IE 事件处理程序的浏览器有 IE 和 Opera。</strong></p> <h3>2.5 跨浏览器的事件处理程序</h3> <p>第一个要创建的方法是 addHandler(),它的职责是视情况分别使用 DOM0 级方法、DOM2 级方法或 IE 方法来添加事件。这个方法属于一个名叫 EventUtil 的对象,本书将使用这个对象来处理浏览器间的差异。addHandler()方法接受 3 个参数:要操作的元素、事件名称和事件处理程序函数。<br /> 与 addHandler()对应的方法是 removeHandler(),它也接受相同的参数。这个方法的职责是移除之前添加的事件处理程序——无论该事件处理程序是采取什么方式添加到元素中的,如果其他方法无效,默认采用 DOM0 级方法。 </p> <pre><code class="language-js">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; } } }; </code></pre> <p>这两个方法首先都会检测传入的元素中是否存在 DOM2 级方法。如果存在 DOM2 级方法,则使用该方法:传入事件类型、事件处理程序函数和第三个参数 false(表示冒泡阶段)。如果存在的是 IE 的方法,则采取第二种方案。这两个方法首先都会检测传入的元素中是否存在 DOM2 级方法。如果存在 DOM2 级方法,则使用该方法:传入事件类型、事件处理程序函数和第三个参数 false(表示冒泡阶段)。如果存在的是 IE 的方法,则采取第二种方案。<br /> 可以像下面这样使用 EventUtil 对象:</p> <pre><code class="language-js">var btn = document.getElementById("myBtn"); var handler = function(){ alert("Clicked"); }; EventUtil.addHandler(btn, "click", handler); //这里省略了其他代码 EventUtil.removeHandler(btn, "click", handler); </code></pre> <p>addHandler()和 removeHandler()没有考虑到所有的浏览器问题,<strong>例如在 IE 中的作用域问题。</strong>不过,使用它们添加和移除事件处理程序还是足够了。此外还要注意,<strong>DOM0 级对每个事件只支持一个事件处理程序。</strong></p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.30</h1> <p>阅读进度: 13.3.3 P360</p> <h2>3 事件对象</h2> <p>在触发 DOM 上的某个事件时,会产生一个事件对象 event,这个对象中包含着所有与事件有关的信息。包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。<br /> 例如,鼠标操作导致的事件对象中,会包含鼠标位置的信息,而键盘操作导致的事件对象中,会包含与按下的键有关的信息。所有浏览器都支持 event 对象,但支持方式不同。 </p> <h3>3.1 DOM中的事件对象</h3> <p>兼容 DOM 的浏览器会将一个 event 对象传入到事件处理程序中。无论指定事件处理程序时使用什么方法(DOM0 级或 DOM2 级),都会传入 event 对象。 </p> <pre><code class="language-js">var btn = document.getElementById("myBtn"); btn.onclick = function(event){ alert(event.type); //"click" }; btn.addEventListener("click", function(event){ alert(event.type); //"click" }, false); </code></pre> <p>这个例子中的两个事件处理程序都会弹出一个警告框,显示由 event.type 属性表示的事件类型。<br /> 这个属性始终都会包含被触发的事件类型,例如"click"(与传入 addEventListener()和removeEventListener()中的事件类型一致)。<br /> 在通过 HTML 特性指定事件处理程序时,变量 event 中保存着 event 对象。请看下面的例子。 </p> <pre><code class="language-html"><input type="button" value="Click Me" onclick="alert(event.type)"/> </code></pre> <p>以这种方式提供 event 对象,可以让 HTML 特性事件处理程序与 JavaScript 函数执行相同的操作。<br /> event 对象包含与创建它的特定事件有关的属性和方法。触发的事件类型不一样,可用的属性和方法也不一样。具体见p355底<br /> 在事件处理程序内部,对象 this 始终等于 currentTarget 的值,而 target 则只包含事件的实际目标。如果直接将事件处理程序指定给了目标元素,则 this、currentTarget 和 target 包含相同的值。 </p> <pre><code class="language-js">var btn = document.getElementById("myBtn"); btn.onclick = function(event){ alert(event.currentTarget === this); //true alert(event.target === this); //true }; </code></pre> <p>这个例子检测了 currentTarget 和 target 与 this 的值。由于 click 事件的目标是按钮,因此这三个值是相等的。<strong>如果事件处理程序存在于按钮的父节点中(例如 document.body),那么这些值是不相同的。</strong> </p> <pre><code class="language-js">document.body.onclick = function(event){ alert(event.currentTarget === document.body); //true alert(this === document.body); //true alert(event.target === document.getElementById("myBtn")); //true }; </code></pre> <p>当单击这个例子中的按钮时,this 和 currentTarget 都等于 document.body,因为事件处理程序是注册到这个元素上的。然而,target 元素却等于按钮元素,因为它是 click 事件真正的目标。由于按钮上并没有注册事件处理程序,结果 click 事件就冒泡到了 document.body,在那里事件才得到了处理。<br /> 在需要通过一个函数处理多个事件时,可以使用 type 属性。例如: </p> <pre><code class="language-js">var btn = document.getElementById("myBtn"); var handler = function(event){ switch(event.type){ case "click": alert("Clicked"); break; case "mouseover": event.target.style.backgroundColor = "red"; break; case "mouseout": event.target.style.backgroundColor = ""; break; } }; btn.onclick = handler; btn.onmouseover = handler; btn.onmouseout = handler; </code></pre> <p>这个例子定义了一个名为 handler 的函数,用于处理 3 种事件:click、mouseover 和 mouseout。 这里通过检测 event.type属性,让函数能够确定发生了什么事件,并执行相应的操作。<br /> 要阻止特定事件的默认行为,可以使用 preventDefault()方法。例如,链接的默认行为就是在被单击时会导航到其 href 特性指定的 URL。如果你想阻止链接导航这一默认行为,那么通过链接的onclick 事件处理程序可以取消它,如下面的例子所示。 </p> <pre><code class="language-js">var link = document.getElementById("myLink"); link.onclick = function(event){ event.preventDefault(); }; </code></pre> <p><strong>只有 cancelable 属性设置为 true 的事件,才可以使用 preventDefault()来取消其默认行为。</strong><br /> 另外,stopPropagation()方法用于立即停止事件在 DOM 层次中的传播,即取消进一步的事件捕获或冒泡。例如,直接添加到一个按钮的事件处理程序可以调用 stopPropagation(),从而避免触发注册在 document.body 上面的事件处理程序,如下面的例子所示。 </p> <pre><code class="language-js">var btn = document.getElementById("myBtn"); btn.onclick = function(event){ alert("Clicked"); event.stopPropagation(); }; document.body.onclick = function(event){ alert("Body clicked"); }; </code></pre> <p>事件对象的 eventPhase 属性,可以用来确定事件当前正位于事件流的哪个阶段。如果是在捕获阶段调用的事件处理程序,那么 eventPhase 等于 1;如果事件处理程序处于目标对象上,则 eventPhase 等于 2;如果是在冒泡阶段调用的事件处理程序,eventPhase等于 3。<strong>注意的是,尽管“处于目标”发生在冒泡阶段,但 eventPhase 仍然一直等于 2。</strong> </p> <pre><code class="language-js">var btn = document.getElementById("myBtn"); btn.onclick = function(event){ alert(event.eventPhase); //2 }; document.body.addEventListener("click", function(event){ alert(event.eventPhase); //1 }, true); document.body.onclick = function(event){ alert(event.eventPhase); //3 }; </code></pre> <p><strong>只有在事件处理程序执行期间,event 对象才会存在;一旦事件处理程序执行完成,event 对象就会被销毁。</strong> </p> <h3>3.2 IE中的事件对象</h3> <p>与访问 DOM 中的 event 对象不同,要访问 IE 中的 event 对象有几种不同的方式,取决于指定事件处理程序的方法。在使用 DOM0 级方法添加事件处理程序时,event 对象作为 window 对象的一个属性存在。 </p> <pre><code class="language-js">var btn = document.getElementById("myBtn"); btn.onclick = function(){ var event = window.event; alert(event.type); //"click" }; </code></pre> <p>在此,我们通过 window.event 取得了 event 对象,并检测了被触发事件的类型(IE 中的 type属性与 DOM 中的 type 属性是相同的)。<br /> 如果事件处理程序是使用 attachEvent()添加的,那么就会有一个 event 对象作为参数被传入事件处理程序函数中。</p> <pre><code class="language-js">var btn = document.getElementById("myBtn"); btn.attachEvent("onclick", function(event){ alert(event.type); //"click" }); </code></pre> <p>在像这样使用 attachEvent()的情况下,也可以通过 window 对象来访问 event 对象,就像使用DOM0 级方法时一样。不过为方便起见,同一个对象也会作为参数传递。<br /> 如果是通过HTML特性指定的事件处理程序,那么还可以通过一个名叫event的变量来访问event对象(与 DOM 中的事件模型相同)。 </p> <pre><code class="language-html"><input type="button" value="Click Me" onclick="alert(event.type)"> </code></pre> <p>IE 的 event 对象同样也包含与创建它的事件相关的属性和方法。其中很多属性和方法都有对应的或者相关的 DOM 属性和方法。与 DOM 的 event 对象一样,这些属性和方法也会因为事件类型的不同而不同,但所有事件对象都会包含下表所列的属性和方法。见p359<br /> 因为事件处理程序的作用域是根据指定它的方式来确定的,所以不能认为 this 会始终等于事件目标。故而,最好还是使用 event.srcElement 比较保险。 </p> <pre><code class="language-js">var btn = document.getElementById("myBtn"); btn.onclick = function(){ alert(window.event.srcElement === this); //true }; btn.attachEvent("onclick", function(event){ alert(event.srcElement === this); //false }); </code></pre> <p>在第一个事件处理程序中(使用 DOM0 级方法指定的),srcElement 属性等于 this,但在第二个事件处理程序中,这两者的值不相同。<br /> 如前所述,returnValue 属性相当于 DOM 中的 preventDefault()方法,它们的作用都是取消给定事件的默认行为。只要将 returnValue 设置为 false,就可以阻止默认行为。 </p> <pre><code class="language-js">var link = document.getElementById("myLink"); link.onclick = function(){ window.event.returnValue = false; };</code></pre> <p>这个例子在onclick事件处理程序中使用returnValue达到了阻止链接默认行为的目的。<strong>与DOM不同的是,在此没有办法确定事件是否能被取消。</strong><br /> 相应地,cancelBubble 属性与 DOM 中的 stopPropagation()方法作用相同,都是用来停止事件冒泡的。由于 IE 不支持事件捕获,因而只能取消事件冒泡;但 stopPropagatioin()可以同时取消事件捕获和冒泡。 </p> <pre><code class="language-js">var btn = document.getElementById("myBtn"); btn.onclick = function(){ alert("Clicked"); window.event.cancelBubble = true; }; document.body.onclick = function(){ alert("Body clicked"); };</code></pre> <p>通过在 onclick 事件处理程序中将 cancelBubble 设置为 true,就可阻止事件通过冒泡而触发document.body 中注册的事件处理程序。 </p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/hzjjg"><img src="https://avatars.githubusercontent.com/u/14288079?v=4" />hzjjg</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <p>阅</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.05.31</h1> <p>阅读进度: 13.4.2 P367</p> <h3>3.3 跨浏览器的事件对象</h3> <p>IE 中 event 对象的全部信息和方法 DOM 对象中都有,只不过实现方式不一样。不过,这种对应关系让实现两种事件模型之间的映射非常容易。可以对前面介绍的 EventUtil 对象加以增强,添加如下方法以求同存异。 </p> <pre><code class="language-js">var EventUtil = { addHandler: function(element, type, handler){ //省略的代码 }, getEvent: function(event){ return event ? event : window.event; }, getTarget: function(event){ return event.target || event.srcElement; }, preventDefault: function(event){ if (event.preventDefault){ event.preventDefault(); } else { event.returnValue = false; } }, removeHandler: function(element, type, handler){ //省略的代码 }, stopPropagation: function(event){ if (event.stopPropagation){ event.stopPropagation(); } else { event.cancelBubble = true; } } }; </code></pre> <p>我们为 EventUtil 添加了 4 个新方法。第一个是 getEvent(),它返回对 event对象的引用。在使用这个方法时,必须假设有一个事件对象传入到事件处理程序中,而且要把该变量传给这个方法。</p> <pre><code class="language-js">btn.onclick = function(event){ event = EventUtil.getEvent(event); }; </code></pre> <p><strong>将这一行代码添加到事件处理程序的开头,就可以确保随时都能使用 event 对象,而不必担心用户使用的是什么浏览器。</strong><br /> 第二个方法是 getTarget(),它返回事件的目标。在这个方法内部,会检测 event 对象的 target属性,如果存在则返回该属性的值;否则,返回 srcElement 属性的值。 </p> <pre><code class="language-js">btn.onclick = function(event){ event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); }; </code></pre> <p>第三个方法是 preventDefault(),用于取消事件的默认行为。在传入 event 对象后,这个方法会检查是否存在 preventDefault()方法,如果存在则调用该方法。如果 preventDefault()方法不存在,则将 returnValue 设置为 false。 </p> <pre><code class="language-js">var link = document.getElementById("myLink"); link.onclick = function(event){ event = EventUtil.getEvent(event); EventUtil.preventDefault(event); }; </code></pre> <p><strong>以上代码可以确保在所有浏览器中单击该链接都不会打开另一个页面。</strong><br /> 第四个方法是 stopPropagation(),其实现方式类似。首先尝试使用 DOM 方法阻止事件流,否则就使用 cancelBubble 属性。 </p> <pre><code class="language-js">var btn = document.getElementById("myBtn"); btn.onclick = function(event){ alert("Clicked"); event = EventUtil.getEvent(event); EventUtil.stopPropagation(event); }; document.body.onclick = function(event){ alert("Body clicked"); }; </code></pre> <p><strong>由于 IE 不支持事件捕获,因此这个方法在跨浏览器的情况下,也只能用来阻止事件冒泡。</strong> </p> <h2>4. 事件类型</h2> <p>Web 浏览器中可能发生的事件有很多类型。“DOM3级事件”规定了以下几类事件。 </p> <ul> <li>UI(User Interface,用户界面)事件,当用户与页面上的元素交互时触发;</li> <li>焦点事件,当元素获得或失去焦点时触发;</li> <li>鼠标事件,当用户通过鼠标在页面上执行操作时触发;</li> <li>滚轮事件,当使用鼠标滚轮(或类似设备)时触发;</li> <li>文本事件,当在文档中输入文本时触发;</li> <li>键盘事件,当用户通过键盘在页面上执行操作时触发;</li> <li>合成事件,当为 IME(Input Method Editor,输入法编辑器)输入字符时触发;</li> <li>变动(mutation)事件,当底层 DOM 结构发生变化时触发。</li> <li>变动名称事件,当元素或属性名变动时触发。此类事件已经被废弃,没有任何浏览器实现它们。<br /> <strong>HTML5 也定义了一组事件,而有些浏览器还会在 DOM 和 BOM 中实现其他专有事件。</strong><br /> DOM3 级事件模块在 DOM2 级事件模块基础上重新定义了这些事件,也添加了一些新事件。包括IE9 在内的所有主流浏览器都支持 DOM2 级事件。IE9 也支持 DOM3 级事件。 </li> </ul> <h3>4.1 UI事件</h3> <p>UI 事件指的是那些不一定与用户操作有关的事件。这些事件在 DOM 规范出现之前,都是以这种或那种形式存在的,而在 DOM 规范中保留是为了向后兼容。现有的 UI 事件如下。 </p> <ul> <li>DOMActivate:表示元素已经被用户操作(通过鼠标或键盘)激活。<strong>这个事件在 DOM3 级事件中被废弃,但 Firefox 2+和 Chrome 支持它。考虑到不同浏览器实现的差异,不建议使用这个事件。</strong></li> <li>load:当页面完全加载后在 window 上面触发,当所有框架都加载完毕时在框架集上面触发,当图像加载完毕时在<code><img></code>元素上面触发,或者当嵌入的内容加载完毕时在<code><object></code>元素上面触发。</li> <li>unload:当页面完全卸载后在 window 上面触发,当所有框架都卸载后在框架集上面触发,或者当嵌入的内容卸载完毕后在<code><object></code>元素上面触发。</li> <li>abort:在用户停止下载过程时,如果嵌入的内容没有加载完,则在<code><object></code>元素上面触发。</li> <li>error:当发生 JavaScript 错误时在 window 上面触发,当无法加载图像时在<code><img></code>元素上面触发,当无法加载嵌入内容时在<code><object></code>元素上面触发,或者当有一或多个框架无法加载时在框架集上面触发。</li> <li>select:当用户选择文本框(<code><input></code>或<code><texterea></code>)中的一或多个字符时触发。第 14 章将继续讨论这个事件。</li> <li>resize:当窗口或框架的大小变化时在 window 或框架上面触发。</li> <li>scroll:当用户滚动带滚动条的元素中的内容时,在该元素上面触发。<code><body></code>元素中包含所加载页面的滚动条。<br /> 多数这些事件都与 window 对象或表单控件相关。<br /> 除了 DOMActivate 之外,其他事件在 DOM2 级事件中都归为 HTML 事件(DOMActivate 在 DOM2级中仍然属于 UI 事件)。要确定浏览器是否支持 DOM2 级事件规定的 HTML 事件。 <pre><code class="language-js">var isSupported = document.implementation.hasFeature("HTMLEvents", "2.0"); </code></pre> <p><strong>注意,只有根据“DOM2 级事件”实现这些事件的浏览器才会返回 true。而以非标准方式支持这些事件的浏览器则会返回 false。</strong><br /> 要确定浏览器是否支持“DOM3 级事件”定义的事件。 </p> <pre><code class="language-js">var isSupported = document.implementation.hasFeature("UIEvent", "3.0"); </code></pre> <ol> <li>load 事件<br /> 当页面完全加载后(包括所有图像、JavaScript 文件、CSS 文件等外部资源),就会触发 window 上面的 load 事件。有两种定义 onload 事件处理程序的方式。 第一种方式是: <pre><code class="language-js">EventUtil.addHandler(window, "load", function(event){ alert("Loaded!"); }); </code></pre> <p>这是通过 JavaScript 来指定事件处理程序的方式,使用了本章前面定义的跨浏览器的 EventUtil对象。<br /> 第二种指定 onload 事件处理程序的方式是为<code><body></code>元素添加一个 onload 特性 </p> <pre><code class="language-html"><!DOCTYPE html> <html> <head> <title>Load Event Example</title> </head> <body onload="alert('Loaded!')"> </body> </html> </code></pre> <p>一般来说,在 window 上面发生的任何事件都可以在<code><body></code>元素中通过相应的特性来指定,因为在 HTML 中无法访问 window 元素。实际上,这只是为了保证向后兼容的一种权宜之计,但所有浏览器都能很好地支持这种方式。<br /> <strong>根据“DOM2 级事件”规范,应该在 document 而非 window 上面触发 load 事件。但是,所有浏览器都在 window 上面实现了该事件,以确保向后兼容。</strong><br /> 图像上面也可以触发 load 事件,无论是在 DOM 中的图像元素还是 HTML 中的图像元素。因此,可以在 HTML 中为任何图像指定 onload 事件处理程序: </p> <pre><code class="language-html"><img src="smile.gif" onload="alert('Image loaded.')"> </code></pre> <p>同样的功能也可以使用 JavaScript 来实现:</p> <pre><code class="language-js">var image = document.getElementById("myImage"); EventUtil.addHandler(image, "load", function(event){ event = EventUtil.getEvent(event); alert(EventUtil.getTarget(event).src); }); </code></pre> <p>在创建新的<code><img></code>元素时,可以为其指定一个事件处理程序,以便图像加载完毕后给出提示。此时,<strong>最重要的是要在指定 src 属性之前先指定事件</strong></p> <pre><code class="language-js">EventUtil.addHandler(window, "load", function(){ var image = document.createElement("img"); EventUtil.addHandler(image, "load", function(event){ event = EventUtil.getEvent(event); alert(EventUtil.getTarget(event).src); }); document.body.appendChild(image); image.src = "smile.gif"; }); </code></pre> <p><strong>这里有一点需要格外注意:新图像元素不一定要从添加到文档后才开始下载,只要设置了 src 属性就会开始下载。</strong><br /> 同样的功能也可以通过使用 DOM0 级的 Image 对象实现。在 DOM 出现之前,开发人员经常使用Image 对象在客户端预先加载图像。可以像使用<code><img></code>元素一样使用 Image 对象,只不过无法将其添加到 DOM 树中。 </p> <pre><code class="language-js">EventUtil.addHandler(window, "load", function(){ var image = new Image(); EventUtil.addHandler(image, "load", function(event){ alert("Image loaded!"); }); image.src = "smile.gif"; }); </code></pre> <p><strong>有的浏览器将 Image 对象实现为<code><img></code>元素,但并非所有浏览器都如此,所以最好将它们区别对待。</strong><br /> 还有一些元素也以非标准的方式支持 load 事件。在 IE9+、Firefox、Opera、Chrome 和 Safari 3+及更高版本中,<code><script></code>元素也会触发 load 事件,以便开发人员确定动态加载的 JavaScript 文件是否加载完毕。<br /> 与图像不同,只有在设置了<code><script></code>元素的 src 属性并将该元素添加到文档后,才会开始下载 JavaScript 文件。换句话说,对于<code><script></code>元素而言,指定 src 属性和指定事件处理程序的先后顺序就不重要了。 </p> <pre><code class="language-js"> EventUtil.addHandler(window, "load", function(){ var script = document.createElement("script"); EventUtil.addHandler(script, "load", function(event){ alert("Loaded"); }); script.src = "example.js"; document.body.appendChild(script);</code></pre></li> </ol></li> </ul> <p>}); </p> <pre><code>这个例子使用了跨浏览器的 EventUtil 对象为新创建的```<script>```元素指定了onload 事件处理程序。此时,大多数浏览器中 event 对象的 target 属性引用的都是```<script>```节点,而在 Firefox 3 之前的版本中,引用的则是 document。IE8 及更早版本不支持```<script>```元素上的 load 事件。 IE 和 Opera 还支持```<link>```元素上的 load 事件,以便开发人员确定样式表是否加载完毕。 ```js EventUtil.addHandler(window, "load", function(){ var link = document.createElement("link"); link.type = "text/css"; link.rel= "stylesheet"; EventUtil.addHandler(link, "load", function(event){ alert("css loaded"); }); link.href = "example.css"; document.getElementsByTagName("head")[0].appendChild(link); }); </code></pre> <p>与<code><script></code>节点类似,在未指定 href 属性并将<code><link></code>元素添加到文档之前也不会开始下载样式表。 </p> <ol start="2"> <li>unload 事件<br /> 与 load 事件对应的是 unload 事件,这个事件在文档被完全卸载后触发。只要用户从一个页面切换到另一个页面,就会发生 unload 事件。<br /> <strong>而利用这个事件最多的情况是清除引用,以避免内存泄漏。</strong><br /> 有两种指定 onunload 事件处理程序的方式。第一种方式是使用 JavaScript <pre><code class="language-js">EventUtil.addHandler(window, "unload", function(event){ alert("Unloaded"); });</code></pre> <p>指定事件处理程序的第二种方式,也是为<code><body></code>元素添加一个特性(与 load 事件相似) </p> <pre><code class="language-html"><!DOCTYPE html> <html> <head> <title>Unload Event Example</title> </head> <body onunload="alert('Unloaded!')"> </body> </html> </code></pre> <p><strong>要小心编写 onunload 事件处理程序中的代码。既然 unload 事件是在一切都被卸载之后才触发,那么在页面加载后存在的那些对象,此时就不一定存在了。此时,操作 DOM 节点或者元素的样式就会导致错误。</strong><br /> <strong>根据“DOM2 级事件”,应该在<code><body></code>元素而非 window 对象上面触发 unload事件。不过,所有浏览器都在 window 上实现了 unload 事件,以确保向后兼容。</strong> </p></li> <li>resize 事件<br /> 当浏览器窗口被调整到一个新的高度或宽度时,就会触发 resize 事件。这个事件在 window(窗口)上面触发,因此可以通过 JavaScript 或者<code><body></code>元素中的 onresize 特性来指定事件处理程序。<br /> 我们还是推荐使用如下所示的 JavaScript 方式: <pre><code class="language-js">EventUtil.addHandler(window, "resize", function(event){ alert("Resized"); });</code></pre> <p>关于何时会触发 resize 事件,不同浏览器有不同的机制。<br /> <strong>IE、Safari、Chrome 和 Opera 会在浏览器窗口变化了 1 像素时就触发 resize 事件,然后随着变化不断重复触发。</strong><br /> <strong>Firefox 则只会在用户停止调整窗口大小时才会触发 resize 事件。</strong><br /> <strong>浏览器窗口最小化或最大化时也会触发 resize 事件。</strong></p></li> <li>scroll 事件 scroll 事件是在 window 对象上发生的,但它实际表示的则是页面中相应元素的变化。在混杂模式下,可以通过<code><body></code>元素的 scrollLeft 和 scrollTop 来监控到这一变化;<br /> 而在标准模式下,除 Safari 之外的所有浏览器都会通过<code><html></code>元素来反映这一变化(Safari 仍然基于<code><body></code>跟踪滚动位置) <pre><code class="language-js">EventUtil.addHandler(window, "scroll", function(event){ if (document.compatMode == "CSS1Compat"){ alert(document.documentElement.scrollTop); } else { alert(document.body.scrollTop); } }); </code></pre> <p>与 resize 事件类似,scroll 事件也会在文档被滚动期间重复被触发,所以有必要尽量保持事件处理程序的代码简单。</p></li> </ol> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.06.01</h1> <p>阅读进度: 13.4.3.1 p370</p> <h3>4.2 焦点事件</h3> <p>焦点事件会在页面元素获得或失去焦点时触发。利用这些事件并与 document.hasFocus()方法及document.activeElement 属性配合,可以知晓用户在页面上的行踪。有以下 6 个焦点事件。</p> <ul> <li>blur:在元素失去焦点时触发。这个事件不会冒泡;所有浏览器都支持它。</li> <li>DOMFocusIn:在元素获得焦点时触发。这个事件与 HTML 事件 focus 等价,但它冒泡。只有Opera 支持这个事件。<strong>DOM3 级事件废弃了 DOMFocusIn,选择了 focusin。</strong></li> <li>DOMFocusOut:在元素失去焦点时触发。这个事件是 HTML 事件 blur 的通用版本。只有 Opera支持这个事件。<strong>DOM3 级事件废弃了 DOMFocusOut,选择了 focusout。</strong></li> <li>focus:在元素获得焦点时触发。这个事件不会冒泡;所有浏览器都支持它。</li> <li>focusin:在元素获得焦点时触发。这个事件与 HTML 事件 focus 等价,但它冒泡。支持这个事件的浏览器有 IE5.5+、Safari 5.1+、Opera 11.5+和 Chrome。</li> <li>focusout:在元素失去焦点时触发。这个事件是 HTML 事件 blur 的通用版本。支持这个事件的浏览器有 IE5.5+、Safari 5.1+、Opera 11.5+和 Chrome。 </li> </ul> <p>这一类事件中最主要的两个是 focus 和 blur,它们都是 JavaScript 早期就得到所有浏览器支持的事件。<strong>这些事件的最大问题是它们不冒泡。</strong><br /> 当焦点从页面中的一个元素移动到另一个元素,会依次触发下列事件:<br /> (1) focusout 在失去焦点的元素上触发;<br /> (2) focusin 在获得焦点的元素上触发;<br /> (3) blur 在失去焦点的元素上触发;<br /> (4) DOMFocusOut 在失去焦点的元素上触发;<br /> (5) focus 在获得焦点的元素上触发;<br /> (6) DOMFocusIn 在获得焦点的元素上触发。<br /> 其中,blur、DOMFocusOut 和 focusout 的事件目标是失去焦点的元素;而 focus、DOMFocusIn和 focusin 的事件目标是获得焦点的元素。<br /> 要确定浏览器是否支持这些事件,可以使用如下代码:</p> <pre><code class="language-js">var isSupported = document.implementation.hasFeature("FocusEvent", "3.0"); </code></pre> <p><strong>即使 focus 和 blur 不冒泡,也可以在捕获阶段侦听到它们。</strong> </p> <h3>4.3 鼠标与滚轮事件</h3> <p>鼠标事件是 Web 开发中最常用的一类事件,毕竟鼠标还是最主要的定位设备。DOM3 级事件中定义了 9 个鼠标事件,简介如下。 </p> <ul> <li>click:在用户单击主鼠标按钮(一般是左边的按钮)或者按下回车键时触发。这一点对确保易访问性很重要,意味着 onclick 事件处理程序既可以通过键盘也可以通过鼠标执行。</li> <li>dblclick:在用户双击主鼠标按钮(一般是左边的按钮)时触发。从技术上说,这个事件并不是 DOM2 级事件规范中规定的,但鉴于它得到了广泛支持,所以 DOM3 级事件将其纳入了标准。</li> <li>mousedown:在用户按下了任意鼠标按钮时触发。不能通过键盘触发这个事件。</li> <li>mouseenter:在鼠标光标从元素外部首次移动到元素范围之内时触发。这个事件不冒泡,而且在光标移动到后代元素上不会触发。DOM2 级事件并没有定义这个事件,但 DOM3 级事件将它纳入了规范。IE、Firefox 9+和 Opera 支持这个事件。</li> <li>mouseleave:在位于元素上方的鼠标光标移动到元素范围之外时触发。这个事件不冒泡,而且在光标移动到后代元素上不会触发。DOM2 级事件并没有定义这个事件,但 DOM3 级事件将它纳入了规范。IE、Firefox 9+和 Opera 支持这个事件。</li> <li>mousemove:当鼠标指针在元素内部移动时重复地触发。不能通过键盘触发这个事件。</li> <li>mouseout:在鼠标指针位于一个元素上方,然后用户将其移入另一个元素时触发。又移入的另一个元素可能位于前一个元素的外部,也可能是这个元素的子元素。不能通过键盘触发这个事件。</li> <li>mouseover:在鼠标指针位于一个元素外部,然后用户将其首次移入另一个元素边界之内时触发。不能通过键盘触发这个事件。</li> <li>mouseup:在用户释放鼠标按钮时触发。不能通过键盘触发这个事件。 </li> </ul> <p>页面上的所有元素都支持鼠标事件。<strong>除了 mouseenter 和 mouseleave,所有鼠标事件都会冒泡</strong>,也可以被取消,而取消鼠标事件将会影响浏览器的默认行为。取消鼠标事件的默认行为还会影响其他事件。<br /> 只有在同一个元素上相继触发 mousedown 和 mouseup 事件,才会触发 click 事件;如果mousedown 或 mouseup 中的一个被取消,就不会触发 click 事件。<br /> 只有触发两次 click 事件,才会触发一次 dblclick 事件。如果有代码阻止了连续两次触发 click 事件(可能是直接取消 click事件,也可能通过取消 mousedown 或 mouseup 间接实现),那么就不会触发 dblclick 事件了。<br /> 这 4个事件触发的顺序始终如下:<br /> (1) mousedown<br /> (2) mouseup<br /> (3) click<br /> (4) mousedown<br /> (5) mouseup<br /> (6) click<br /> (7) dblclick<br /> IE8 及之前版本中的实现有一个小 bug,因此在双击事件中,会跳过第二个 mousedown 和 click事件,其顺序如下:<br /> (1) mousedown<br /> (2) mouseup<br /> (3) click<br /> (4) mouseup<br /> (5) dblclick<br /> <strong>IE9 修复了这个 bug</strong><br /> 使用以下代码可以检测浏览器是否支持以上 DOM2 级事件 </p> <pre><code class="language-js">var isSupported = document.implementation.hasFeature("MouseEvents", "2.0"); </code></pre> <p>要检测浏览器是否支持上面的所有事件,可以使用以下代码:</p> <pre><code class="language-js">var isSupported = document.implementation.hasFeature("MouseEvent", "3.0") </code></pre> <p><strong>注意,DOM3 级事件的 feature 名是"MouseEvent",而非"MouseEvents"。</strong> </p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/caizhendi"><img src="https://avatars.githubusercontent.com/u/15686186?v=4" />caizhendi</a> commented <strong> 4 years ago</strong> </div> <div class="markdown-body"> <h1>2020.06.02</h1> <p>阅读进度: 13.4.3.6 P374</p> <ol> <li>客户区坐标位置 鼠标事件都是在浏览器视口中的特定位置上发生的。这个位置信息保存在事件对象的 clientX 和clientY 属性中。所有浏览器都支持这两个属性,它们的值表示事件发生时鼠标指针在视口中的水平和垂直坐标。<br /> 可以使用类似下列代码取得鼠标事件的客户端坐标信息: <pre><code class="language-js">var div = document.getElementById("myDiv"); EventUtil.addHandler(div, "click", function(event){ event = EventUtil.getEvent(event); alert("Client coordinates: " + event.clientX + "," + event.clientY); }); </code></pre></li> <li>页面坐标位置<br /> 通过客户区坐标能够知道鼠标是在视口中什么位置发生的,而页面坐标通过事件对象的 pageX 和pageY 属性,能告诉你事件是在页面中的什么位置发生的。这两个属性表示鼠标光标在页面中的位置,因此坐标是从页面本身而非视口的左边和顶边计算的。<br /> 以下代码可以取得鼠标事件在页面中的坐标: <pre><code class="language-js">var div = document.getElementById("myDiv"); EventUtil.addHandler(div, "click", function(event){ event = EventUtil.getEvent(event); alert("Page coordinates: " + event.pageX + "," + event.pageY); }); </code></pre> <p><strong>在页面没有滚动的情况下,pageX 和 pageY 的值与 clientX 和 clientY 的值相等。</strong><br /> IE8 及更早版本不支持事件对象上的页面坐标,不过使用客户区坐标和滚动信息可以计算出来。这时候需要用到 document.body(混杂模式)或 document.documentElement(标准模式)中的scrollLeft 和 scrollTop 属性。计算过程如下所示: </p> <pre><code class="language-js">var div = document.getElementById("myDiv"); EventUtil.addHandler(div, "click", function(event){ event = EventUtil.getEvent(event); var pageX = event.pageX, pageY = event.pageY; if (pageX === undefined){ pageX = event.clientX + (document.body.scrollLeft || document.documentElement.scrollLeft); } if (pageY === undefined){ pageY = event.clientY + (document.body.scrollTop || document.documentElement.scrollTop); } alert("Page coordinates: " + pageX + "," + pageY); }); </code></pre></li> <li>屏幕坐标位置<br /> 鼠标事件发生时,不仅会有相对于浏览器窗口的位置,还有一个相对于整个电脑屏幕的位置。而通过 screenX 和 screenY 属性就可以确定鼠标事件发生时鼠标指针相对于整个屏幕的坐标信息。<br /> 可以使用类似下面的代码取得鼠标事件的屏幕坐标: <pre><code class="language-js">var div = document.getElementById("myDiv"); EventUtil.addHandler(div, "click", function(event){ event = EventUtil.getEvent(event); alert("Screen coordinates: " + event.screenX + "," + event.screenY); }); </code></pre></li> <li>修改键<br /> 虽然鼠标事件主要是使用鼠标来触发的,但在按下鼠标时键盘上的某些键的状态也可以影响到所要采取的操作。这些修改键就是 Shift、Ctrl、Alt 和 Meta(在 Windows 键盘中是 Windows 键,在苹果机中是 Cmd 键),它们经常被用来修改鼠标事件的行为。<br /> DOM 为此规定了 4 个属性,表示这些修改键的状态:shiftKey、ctrlKey、altKey 和 metaKey。<strong>这些属性中包含的都是布尔值,如果相应的键被按下了,则值为 true,否则值为 false。</strong>当某个鼠标事件发生时,通过检测这几个属性就可以确定用户是否同时按下了其中的键。 <pre><code class="language-js">var div = document.getElementById("myDiv"); EventUtil.addHandler(div, "click", function(event){ event = EventUtil.getEvent(event); var keys = new Array(); if (event.shiftKey){ keys.push("shift"); } if (event.ctrlKey){ keys.push("ctrl"); } if (event.altKey){ keys.push("alt"); } if (event.metaKey){ keys.push("meta"); } alert("Keys: " + keys.join(",")); }); </code></pre> <p><strong>IE9、Firefox、Safari、Chrome 和 Opera 都支持这 4 个键。IE8 及之前版本不支持metaKey 属性。</strong> </p></li> <li> <p>相关元素<br /> 在发生 mouseover 和 mouserout 事件时,还会涉及更多的元素。这两个事件都会涉及把鼠标指针从一个元素的边界之内移动到另一个元素的边界之内。对 mouseover 事件而言,事件的主目标是获得光标的元素,而相关元素就是那个失去光标的元素。类似地,对 mouseout 事件而言,事件的主目标是失去光标的元素,而相关元素则是获得光标的元素。 </p> <pre><code class="language-html"><!DOCTYPE html> <html> <head> <title>Related Elements Example</title> </head> <body> <div id="myDiv" style="background-color:red;height:100px;width:100px;"></div> </body> </html> </code></pre> <p>如果鼠标指针一开始位于这个<code><div></code>元素上,然后移出 了这个元素,那么就会在<code><div></code>元素上触发 mouseout 事件,相关元素就是<code><body></code>元素。与此同时,<code><body></code>元素上面会触发 mouseover 事件,而相关元素变成了<code><div></code>。<br /> DOM 通过 event 对象的 relatedTarget 属性提供了相关元素的信息。这个属性只对于 mouseover和mouseout事件才包含值;对于其他事件,这个属性的值是null。<br /> <strong>IE8及之前版本不支持relatedTarget属性</strong>,但提供了保存着同样信息的不同属性。在 mouseover 事件触发时,IE 的 fromElement 属性中保存了相关元素;在 mouseout 事件触发时,IE 的 toElement 属性中保存着相关元素。(IE9 支持所有这些属性。)可以把下面这个跨浏览器取得相关元素的方法添加到 EventUtil 对象中。 </p> <pre><code class="language-js">var EventUtil = { //省略了其他代码 getRelatedTarget: function(event){ if (event.relatedTarget){ return event.relatedTarget; } else if (event.toElement){ return event.toElement; } else if (event.fromElement){ return event.fromElement; } else { return null; } }, //省略了其他代码 }; </code></pre> <p>与以前添加的跨浏览器方法一样,这个方法也使用了特性检测来确定返回哪个值。</p> <pre><code class="language-js">var div = document.getElementById("myDiv"); EventUtil.addHandler(div, "mouseout", function(event){ event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); var relatedTarget = EventUtil.getRelatedTarget(event); alert("Moused out of " + target.tagName + " to " + relatedTarget.tagName); }); </code></pre> </li> </ol> </div> </div> <div class="page-bar-simple"> <a href="/caizhendi/blog/2?page=2" class="next">Next</a> </div> <div class="footer"> <ul class="body"> <li>© <script> document.write(new Date().getFullYear()) </script> Githubissues.</li> <li>Githubissues is a development platform for aggregating issues.</li> </ul> </div> <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script> <script src="/githubissues/assets/js.js"></script> <script src="/githubissues/assets/markdown.js"></script> <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.4.0/build/highlight.min.js"></script> <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.4.0/build/languages/go.min.js"></script> <script> hljs.highlightAll(); </script> </body> </html>