JavaScript高级程序设计-笔记

第3章 基本概念

3.1 语法

3.2 关键字和保留字

3.3 变量

3.4 数据类型

5种简单数据类型:
Undefined、Null、Boolean、Number、String

1种复杂数据类型:Object(本质上是由一组无序的名值对组成的)

3.4.1 typeof 操作符

对一个值使用typeof操作符可能会返回下列某个字符:

  • “undefined”:如果这个值未定义
  • “boolean”:如果这个值是布尔值
  • “number”:如果这个值是数值
  • “string”:如果这个值是字符串
  • “object”:如果这个值是对象或者null
  • “function”:如果这个值是函数

调用typeof null会返回"object",因为特殊值 null 被认为是一个空的对象引用

3.4.2 Undefined 类型

Undefined类型只有一个值,即特殊的 undefined,在使用 var 声明变量但未对其加以初始化时,这个变量的值就是 undefined,即

var msg;
cosnole.log(msg == undefined);  // true

另外,对未初始化的变量执行 typeof 和对未声明的变量执行 typeof 都会返回 undefined,如:

var msg;
// var age;

console.log(typeof msg);  // "undefined"
console.log(typeof age);  // "undefined"

3.4.3 Null 类型

Null 类型是第二个只有一个值的数据类型,这个值是 null,null 表示一个空对象指针,所以使用 typeof 返回的是“object”

实际上,undefined 是派生自 null 的,所有它们的相等性测试要返回true:

console.log(null == undefined);  // true

只要在意保存对象的变量还没有真正的保存对象,就应该明确地让该变量保存 null 值

3.4.4 Boolean 类型

虽然 Boolean 类型的字面值只有两个 true 和 false,但是ECMAScript所有类型的值都有与这两个Boolean值等价的值:

数据类型 转换为true的值 转换为true的值
Boolean true false
String 任何的非空字符串 “”(空字符串)
Number 任何非零数字值(包括无穷大) 0和NaN
Object 任何对象 null
Undefined n/a(不适用) undefined

以上规则在控制流语句(例如 if )中非常重要。

3.4.5 Number 类型

最基本的数字字面量格式是十进制整数

var num = 55;

整数还可以使用八进制和十六进制。

1. 浮点数值

浮点数值,即数值中必须包括一个小数点,而且小数点后必须至少有一个数字。

由于保存浮点数值需要的内存空间是整数的两倍,所以有时会将浮点数值转换为整数值:

var floatNum1 = 1.;  // 解析为1
var floatNum2 = 10.0;  // 解析为10

对于极大或极小的值可以使用 e 表示法(科学计数法)

var bigFloatNum = 3.125e7;  // 等于31250000
var smallFloatNum = 3e-17;  // 等于0.00000000000000003

默认情况下,ECMAScript 会将小数点后带有6个零以上的浮点数值转换为 e 表示法,例如:
0.0000003会被转换为 3e-7

浮点整数的最高精度为17位小数,但是进行算术计算的时候精准度不如整数,例如:0.1 加 0.2 结果不是 0.3,而是 0.30000000000000004,这样的误差会导致无法测定特定的浮点数值。

2. 数值范围

ECMAScript中
最小的数值为:Number.MIN_VALUE,在大多数浏览器中该数值为5e-324;
最大的数值为:Number.MIN_VALUE ,在大多数浏览器中该数值为1.7976931348623157e+308

如果某次计算的结果值超出了数值范围,就会转换成特殊的 Infinity 值( -Infinity 负无穷或 Infinity 正无穷)

可以使用isFinite()函数判断是否为无穷数。

3. NaN

NaN(Not a Number)是一个特殊的数值,该数值表示一个本来要返回数值的操作没返回数值的情况(这样就不会抛出错误了)

NaN有两个特点:

  • 任何涉及NaN的操作结果都是Nan
  • NaN与任何值都不相等,即使是和自己

由于以上特点,我们使用 isNaN() 函数来判断一个数是不是数值:

console.log(isNaN(NaN));    // true
console.log(isNaN(10));     // false(10是一个数值)
console.log(isNaN('10'));   // false(可以转换为数值10)
console.log(isNaN('blue'));  // true(不能转换为数值)
console.log(isNaN(true));   // false(不能转换为数值)

isNaN()同样适用于对象,基于对象调用时,首先会调用对象的valueOf()方法,然后再确定返回的值可否转换为数值。

4. 数值转换

有三个函数可以把非数值转换为数值:

  • Number()
  • parseInt()
  • parseFloat()

Number()可以用于任何数据类型,parseInt 和 parseFloat专门用于把字符串转换成数值

Number() 函数在转换时规则比较复杂且不够合理,所以更常用的是 parseInt() 函数

parseInt() 函数在转换字符串时有几个规则

  • 它会忽略字符串前面的空格,直至找到第一个非空字符串。
  • 如果第一个字符不是数字字符或负数,parseInt() 就会返回 NaN。
  • 如果第一个字符是数字字符,parseInt 会继续解析第二个字符,知道解析完所有的字符或遇到一个非数字字符。

可以在转换时,指定第二个参数即转换使用的基数(即多少进制),来消除 parseInt() 在进制方面的困惑。

var num1 = parseInt("10", 2);   // 2(二进制)
var num2 = parseInt("10", 8);   // 8(八进制)
var num3 = parseInt("10", 10);  // 10(十进制)
var num4 = parseInt("10", 16);  // 16(十六进制)

parseFloat也会从第一个字符开始解析每个字符,也是一直解析到字符末尾,或者解析到遇见一个无效的浮点数字字符为止。也就是说第一个小数点是有效的,第二个就是无效的,后面的字符会被忽略。

parseFloat还有一个特点是会忽略前导的零。它只能解析十进制数值。

3.4.6 String 类型

双引号和单引号都可以,但是双引号开头的字符必须以双引号结尾,单引号开头的字符必须以单引号结尾。

1. 字符字面量

  • \n:换行
  • \t:换行
  • \b:换行
  • \r:换行
  • \f:换行
  • \:换行

字符字面量占一个字符

2. 字符串的特点

字符串不可修改,也就是说字符串一旦创建,它的值就不能改变,如果改变,则需要销毁原来的字符串

3. 转换为字符串

有两个方法:toString() 和 String()

toString() 也可以传递一个参数来制定基数(用多少进制)来返回字符串

String() 方法有几个规则:

  • 如果该值有 toString() 方法,则返回调用该方法的值
  • null,返回'null'
  • undefined,返回'undefined'

3.4.7 Object 类型

详见 第5章第6章

3.5 操作符

操作符有:算术操作符(加号和减号)、位操作符、关系操作符和相等操作符

3.5.1 一元操作符

1. 递增和递减操作符

2. 一元加减操作符

一元加号操作符(+)放在数值前面,不会产生任何影响
在对非数值应用一元加号操作符时,该操作符会像 Number() 转型函数一样对数值进行转换。

一元减号操作符(-)放在数值前面,表示该数值的负数
当应用非数值时,也是先像 Number() 一样进行转换为负数。

3.5.2 位操作符

查阅原书

3.5.3 布尔操作符

1. 逻辑非(!)

2. 逻辑与(&&)

3. 逻辑或(||)

3.5.4 乘性操作符

1. 乘法

2. 除法

3. 求模

3.5.5 加性操作符

1. 加法(+)

加法操作符有以下规则:

  • 如果两个操作数都是数值,执行常规的加法计算
  • 如果两个操作数都是字符串,则将两个操作数拼接起来
  • 如果只有一个操作数是字符串,则将另一个操作数转换为字符串,然后再将两个字符串拼接起来
  • 如果有一个操作数是对象、数值或布尔值,则调用它们的 toString() 方法取得相应的字符串值,然后再应用前3个规则。对于 null 和 undefined ,分别获得字符串'null'和'undefined'

2. 减法

减法操作符有以下规则:

  • 如果两个操作数都是数值,执行常规的减法计算
  • 如果只有一个操作数是字符串、布尔值、null 或 undefined,则先使用 Number() 将其转换为数值,然后再将进行减法规则进行计算
  • 如果有一个操作数是对象,则调用对象的 valueOf() 方法取得相应的数值。如果没有 valueOf 方法,则调用 toString() 方法,然后再将其转换为数值进行计算

3.5.6 关系操作符

小于(<)、大于(>)、小于等于(<=)和大于等于(>=)在操作非数值时,也要进行数据转换和一些奇怪的操作,以下是规则:

  • 如果两个操作数都是数值,则执行数值比较
  • 如果两个操作数都是字符串,则比较两个字符串对应的字符编码值
  • 如果一个操作数是数值,则将另一个操作数转换为一个数值,然后执行数值比较
  • 如果一个操作数是对象,则调用 valueOf() ,再使用前面的规则进行比较。没有 valueOf 就调用 toString 执行比较。
  • 如果一个操作数是布尔值,先转换为数值,然后进行比较

与 NaN 进行比较时,永远返回 false。

3.5.7 相等操作符

1. 相等和不相等(先转换再比较)

在转换数据类型时,遵循下列规则:

  • 如果有一个操作数是布尔值,则在比较之前先将其转换为数值
  • 如果一个操作数是字符串,另一个操作数是数值,则比较之前先将字符串转换为数值
  • 如果有一个操作数是对象,另一个操作数不是,则调用对象的 valueOf() 方法,用得到的基本类型值按前面的规则进行比较

这两个操作符比较时遵循以下规则:

  • null 和 undefined 是相等的
  • 比较相等性之前,不能将 null 和 undefined 转换成其他任何值
  • 如果有一个操作数为 NaN,则相等操作符返回 false, 不相等操作符返回 true
  • 如果两个操作数都是对象,比较它俩是不是同一个对象,如果都指向一个对象则返回 true

2. 全等和不全等(全等和不全等)

除了在比较之前不转换操作数之外, 全等和不全等与相等和不相等没有什么区别

3.5.8 条件操作符

条件操作符的语法形式:

variable = boolean_expression ? true_value : false_value;

3.5.9 赋值操作符

= 和 +=,以及其他

3.5.10 逗号操作符

一条语句中执行多个操作,例如:

var num1, num2, num3;

用于赋值时,逗号操作符总会返回表达式中的最后一项:

var num = (1, 4, 5, 0); // num的值为0

3.6 语句

3.6.1 if语句

3.6.2 do-while语句

3.6.3 while语句

3.6.4 for语句

3.6.5 for-in语句

for-in语句是迭代语句,用来枚举对象的属性。

for(var property in window) statement

3.6.6 label语句

label: statement

3.6.7 break和continue语句

3.6.8 with语句

将代码的作用域设置到一个特定的对象上

with (expression) statement

开发大型应用程序的时候,不建议使用with语法

3.6.9 switch语句

switch (expression){
  case value: statement
    break;
  case value: statement
    break;
  case value: statement
    break;
  default: statement
}

javascript中,switch中可以使用任何数据类型,每个case值也不一定是常量,也可以是变量,甚至是表达式。

3.7 函数

3.7.1 理解函数

3.7.2 没有重载

重名函数,后一个函数会覆盖前一个函数。

第4章 变量、作用域和内存问题

4.1 基本类型和引用类型的值

4.1.1 动态的属性

4.1.2 复制变量值

4.1.3 传递参数

4.1.4 检测类型

在检测基本数据类型时使用 typeof 操作符:

  • Undefined:返回"undefined"
  • Null:返回"object"
  • Boolean:返回"boolean"
  • Number:返回"number"
  • String:返回"string"
  • Object:返回"object"
  • Function:返回"function"

检测引用类型的时候,使用 instanceOf 操作符:

result = variable instanceOf constructor

如果变量是给定引用类型(根据它的原型链来识别)的实例,那么就会返回true。

4.2 执行环境及作用域

第5章 引用类型

5.1 Object类型

5.2 Array类型

Array 类型的 length 属性不是只读的,可以通过设置这个属性从数组的末尾移除项或者添加新项目(新项目都为 undefined )

5.2.1 检测数组

使用 Array.isArray(value) 来检测,兼容性:数组检测

5.2.2 转换方法

toString 或 join(separator) 返回以传入字符为分隔符的字符串。

5.2.3 栈方法

栈是一种后进先出的数据结构,栈中项的插入或移出都是发生站栈的顶部。

  1. push():在数组末尾添加任意个项,并返回新数组的长度
  2. pop():移除数组的最后一项,并返回移除的值

5.2.4 队列方法

队列是一种先进先出的数据结构,队列在列表的末端添加项,从列表前端移除项

  1. shift():移除数组的第一项,并返回移除的值
  2. unshift():在数组前端添加任意个项,并返回新数组的长度

5.2.5 重排序方法

  1. reverse():反转调用方法该数组项的顺序,也返回排序后的数组
  2. sort():按升序排列数组项,一般接收一个比较函数,比较函数接收两个参数:
  • 如果第一个参数需要在第二个参数之前,则返回负数
  • 如果相等则返回0
  • 如果想要第一个参数在第二个参数之后,则返回正数

如下:

function compare(value1, value2) {
  if (value1 < value2) {
    return -1
  } else if (value1 > value2) {
    return 1;
  } else {
    return 0;
  }
}
var arr = [3, 1, 5, 2, 4];
arr.sort(compare);

5.2.6 操作方法

  1. concat(data):先创建当前数组的一个副本,并将接收到的参数添加到这个副本的末尾,并返回新构建的数组
  2. slice(start, end):接收两个参数,即返回项起始和结束位置
  3. splice(index, howmany, item1, …, itemN):较为强大的方法,第一个参数为操作的起始位置,第二个参数是操作几个数据,之后的参数为新添加的数据,具体有以下三种用法
  • 删除:splice(0, 2),删除前两项
  • 插入:splice(1, 0, ‘red’, ‘blue’),在位置1处插入red和blue两个字符串
  • 替换:splice(1, 1, ‘black’),替换数组的第二个数据为字符串black
    splice始终会返回一个数组,该数组包含了从原始数组中删除的项,如果没有删除项则返回空数组

5.2.7 位置方法

indexOf()lastIndexOf(),分别为从开头向后找,和从末尾朝前开始找,对比是用全等。返回数据的位置。

5.2.8 迭代方法

共有5个迭代方法,接收两个参数,一个是每一项上运行的函数(函数接收三个参数,数组项的值,该项的索引和数组对象本身),和(可选)该函数的作用域对象—影响this的值。以下方法对数组的每一项运行传入的函数,返回规则如下:

  • forEach():对数组每一项运行函数,无返回值。
  • every():如果该函数对每一项都返回 true,则返回 true;
  • some():如果该函数对任意一项返回 true,则返回 true;
  • filter():返回该函数会返回 true 的项组成的数组;
  • map():返回每次函数调用的结果组成的数组;

5.2.9 缩小方法

reduce() 和 reduceRight(),这两个方法都会迭代数组的每一项,然后构建一个最终返回的值。reduce() 从第一项开始,reduceRight 是从最后一项开始。
都接收两个参数,每一项上调用的函数和(可选)作为缩小基础的初始值。
该函数接收4个参数,前一个值,当前值,项的索引,数组对象。这个函数返回的任意值都会成第一个参数传给下一项。第一次迭代发生在数组的第二项。

var array = [1, 2, 3, 4, 5];
var sum = array.reduce(function (prev, cur, index, array) {
  return prev + cur;
});
console.log(sum);   // 15

上述函数,第一次调用的时候,prev为1,cur为2.第二次调用的时候,prev为3(1+2的结果),cur为3(数组的第三项).

5.3 Date类型

// TODO

5.4 RegExp类型

var expression  = / pattern / flags ;

flags表示正则表达式带有的标志,用以标明正则表达式的行为:

  • g:表示全局(globe)模式,表示该正则将应用到所有字符串,而不会在遇到第一个匹配项时停止;
  • i:表示不区分大小写(case-insensitive)模式,确定匹配项是忽略字符串的大小写;
  • m:表示多行(multiline)模式,即到达一行字符串末尾后,还会继续向下一行中寻找是否存在匹配项。

5.4.1 RegExp实例属性(不常用)

5.4.2 RegExp实例方法

  • exec():专门为捕获组设置的方法,
    在全局模式下,每次调用 exec 方法会在字符串中查找新的匹配项
    在普通模式下,多次调用 exec 方法始终返回第一个匹配项的信息。
  • text():接收一个字符串参数,判断该字符串是否匹配: pattern.test(string);

5.4.3 RegExp构造函数属性(查阅原书)

5.4.4 模式的局限性(查阅原书)

5.5 Function类型

  • 函数声明:function foo(arguments){};
  • 函数表达式:var foo = function(arguments){};
  • Function构造函数:var foo = new Function(argument1, argument2, ... , ‘return ....;’);()不推荐。

5.5.1 没有重载

在声明第二个函数名后,第二个函数,实际将引用传给了第二个函数体,第一个函数体在内存中,无法被引用。

5.5.2 函数声明与函数表达式

函数声明提升:使用函数声明的方式构建的函数,会在代码开始解析时,将该函数声明提升到代码顶部

foo();

function foo() {
  return 'function is running.';
}

以上代码可以正常运行,而下面的代码却不可以正常运行:

foo();

var foo = function () {
  return 'function is running.';
}

上面代码调用foo方法时,foo没有保存对函数的引用所以会报错,因为没有函数声明提升

5.5.3 作为值的函数

JS中函数名本身就是变量,所以函数可以直接当值来使用。也就是说,可以将一个函数xx像参数一样传给另外一个函数,也可以将函数作为另一个函数d额结果返回。

1. 当做参数传递:

function callSomeFunction(someFunction, someArgument) {
  return someFunction(someArgument);
}

function add10(num) {
  return num + 10;
}

var result = callSomeFunction(add10, 5);  // 15

2. 当做结果返回:

例如我们要对一个对象数组排序,并指明按照其中一个属性来排序,则需要一个新的排序函数:

var array = [{
  name: 'aaa',
  age: 50
}, {
  name: 'bbb',
  age: 23
}, {
  name: 'ccc',
  age: 78
}];

function createCompareFunction(propertyName) {
  return function (object1, object2) {
    var value1 = object1[propertyName];
    var value2 = object2[propertyName];

    if (value1 < value2) {
      return -1
    } else if (value1 > value2) {
      return 1;
    } else {
      return 0;
    }
  }
}

array.sort(createCompareFunction('name'));
console.log(array[0].name); // aaa

array.sort(createCompareFunction('age'));
console.log(array[0].name); // bbb

第一次是按姓名排序,所以aaa为第一项,第二次是按年龄排序,所以bbb为第一项。

5.5.4 函数内部属性(重点)

1. arguments对象的callee属性

该属性是一个指针,指向这个拥有arguments对象的函数。如下一道经典的阶乘函数:

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

PS:如果使用 return numfactorial(num - 1),则会有太大的耦合,不合理*

2. this对象

this引用的是函数据以执行的环境对象。

3. caller

该属性中保存着调用当前函数的函数引用

function outer() {
  inner();
}

function inner() {
  console.log(inner.caller);
}

outer();

上述代码执行后会打印出outer()函数的源码。因为outer调用了inner,所以caller就指向了outer()。

5.5.5 函数的属性和方法

  • length:函数希望接收的命名参数的个数;
  • prototype:第六章详述(重点)

以下两个方法的用途都是在特定作用域中调用函数实际上等于设置函数体内this的值:

  • apply():接收两个参数,一个是运行函数的作用域,另外一个是参数数组(可以是arguments对象或Array实例)。
    fun.apply(this, arguments);
    // 或者
    fun.apply(this, [arg1, arg2]);
    
  • call():第一个参数和apply()相同,不同的是传递给函数的参数必须逐个列举出来。
    fun.call(this, arg1, arg2, ..., argN);
    

这两个函数真正的强大之处是可以扩充函数赖以运行的作用域。这种方式最大的好处是,对象不需要与函数有任何耦合关系,例如:

window.name = 'window';

var o = {
  name: 'object'
};

function getName() {
  alert(this.name);
}

getName();  // 将当前环境变量对象(即全局对象)传入,显示'window'
getName.call(this); // 将当前环境变量对象(即全局对象)传入,显示'window'
getName.call(window); // 将window对象传入,显示'window'
getName.call(o);  // 将o对象传入,显示'object'

由上述代码可发现,使用call或apply方法可以使对象与函数不需要有任何的耦合关系。

ECMAScript 5 还定义了bind() 方法,该方法会创建一个函数的实例,其this值会被绑定到传给bind() 函数的对象上:

window.name = 'window';

var o = {
  name: 'object'
};

function getName() {
  console.log(this.name);
}

// 使用bind方法
var objectGetName = getName.bind(o);
objectGetName();  // 'object'

5.6 基本包装类

为了方便操作基本类型值,ECMAScript还提供了三个特殊的包装类:Boolean、Number和String。

如下这段程序:

var s1 = 'some text';
var s2 = s1.subString(2);

s1是基本数据类型,本来不应该有方法,但是实际上,后台已经自动完成了一系列的处理,第二行访问s1的时候,访问程序处于一种读取模式,就是从内存中读取这个字符串的值,而在读取模式中访问字符串的时候,后台都会自动完成下列处理。

  • 创建String类型的一个实例
  • 在实例上调用这个方法
  • 销毁这个实例
    上述三个步骤可以用以下代码描述:
var s1 = new String('some text');
var s2 = s1.subString(2);
s1 = null;

引用类型和基本包装类的主要区别为对象的生存期:

1. 使用new操作创建的引用类型实例,在执行流离开之前,一直存在于内存中:

var s1 = new String('some text');   // 使用new操作符创建
s1.name = 'new property';
console.log(s1.name); // 'new property'

2. 而自动创建的基本包装类型的对象,则只存在代码执行的瞬间,然后被立即销毁,这意味着我们不能给基本包装类添加属性和方法

var s1 = 'some text';   // 自动创建的基本包装类

s1.name = 'new property';
console.log(s1.name); // undefined

上述结果是undefined的原因是第二行创建的String对象在执行第三行代码的时候已经被销毁,第三行又新创建了自己的String对象,而该对象是没有color属性的。

可以通过显式调用Boolean、Number和String来创建基本包装类对象,但不推荐,因为会分不清是在处理基本类型还是引用类型的值。

  • 对基本包装类的实例调用typeof会返回“object”。

  • Object构造函数会像工厂方法一样,根据传入值的类型,返回相应基本包装类的实例。

    var obj = new Object(‘some string’);
    console.log(obj instanceof String); // true
    
  • 使用new调用基本包装类型的构造函数,与直接调用同名的转型函数是不一样的:

    var value = '25';
    console.log(typeof Number(value));  // “number”
    
    var obj = new Number(value);
    console.log(typeof obj);    // “object”
    

5.6.1 Boolean类型

是与布尔值对应的引用类型。创建对象的语法:

var booleanObj = new Boolean( true/false );

由于布尔表达式的对象会永远转换为true,所以无论是new Boolean(true)还是new Boolean(false)在布尔表达式中都为true,所以尽可能不要使用Boolean包装类。

5.6.2 Number类型

是与数字值相对应的引用类型,创建语法如下:

var numberObj = new Number( 10 );   // 传入对应的数值。

除了继承的方法之外,Number类型还提供了格式化为字符串的方法:

  • toFixed():会按照指定的小数位数返回数值的字符串表示。
    var num = 10;
    console.log(num.toFixed(2));    // "10.00"
    
    可以自动舍入。
  • toExponential():该方法返回以指数表示法表示的数值的字符串形式。
  • toPrecision():如果想得到某个数值最合适的格式会用到的方法。该方法可能会返回固定大小格式,传入表示所有数字的位数:
    var num = 99;
    console.log( num.toPrecision(1) );  // 1e+2
    console.log( num.toPrecision(2) );  // 99
    console.log( num.toPrecision(3) );  // 99.0
    

仍然不建议显式实例化。

5.6.3 String类型

String类型是字符串的包装类,创建方法:

var s = new String(“some string”);

每个实例都有一个length属性。即使字符串内包含双字节字符,每个字符也算一个字符(例如汉字)。

1. 字符方法

用于访问字符串中特定字符的方法,charAt( )和charCodeAt( )。两个方法都接收一个参数,即基于0的字符位置。

  • charAt():方法返回给定位置的字符
  • charCodeAt():返回的是字符编码。
    ECMAScript5定义了在支持的浏览器中访问字符的方法,即类数组方法:
var s = ‘hello world’;
console.log( s[1] );    // ‘e’

2. 字符串操作方法

  • concat():用于将多个字符串拼接起来。
    还提供了三个创建新字符串的方法,都返回被操作字符串的子字符串,都接收两个参数,一个是开始位置,第二个参数表示子字符串在哪个位置结束:
  • slice():第二个参数指的是最后一个字符串的位置;
  • substr():第二个字符串指定的是返回字符的个数;
  • substring():第二个参数指的是最后一个字符串的位置;

参数为负数的情况下,假设字符串长度为10:

  • slice():将传入的负数与字符串的长度相加;
    例:slice( -3 ) 相当于slice( 10 + (-3) ) 即slice( 7 );

  • substr():将第一个负数参数加上字符串长度,第二个负数参数转换为0;
    例:substr( -3, -6 ) 相当于substr( 7, 0 );

  • substring():会将所有的负数参数转换为0;
    例:substring( -3 ) 相当于substring( 0 );

3. 字符串位置方法

有两个查询子字符串的方法:indexOf()和lastIndexOf()。

都接收两个参数,第一个参数为需要查找的子字符串,第二个参数(可选)指定从哪个位置开始查找。

4. trim()方法

该方法会创建一个字符串副本,然后删除前置和后缀的空格,原字符串的空格依然存在。

5. 字符串大小写转换方法

toUpperCase()和toLowerCase();

该方法会创建一个字符串副本,然后进行大小写转换,原字符串不变。

6. 字符串的模式匹配方法

  • match():在字符串上调用该方法,只接收一个参数,要么是正则表达式,要么就是RegExp对象。返回匹配结果的数组。该数组的内容依赖于 regexp 是否具有全局标志 g。 如果没找到匹配结果返回 null。例如:
    var str="The rain in SPAIN stays mainly in the plain"; 
    var n=str.match(/ain/g);
    console.log(n); // ain,ain,ain
    
  • search():参数同match,返回第一个匹配项的索引,如果没有则返回-1。
  • replace():该方法接收两个参数,第一个参数可以是字符串或正则表达式,第二个参数可以是一个字符串或一个函数。
    • 如果第一个参数为字符串,则只会替换第一个子字符串;
    • 如果想匹配所有的子字符串,则第一个参数需要传入正则表达式,且指定全局(g)标志。
    • 第二个参数为字符串或者函数(查阅原书)。
  • spilt():基于指定分隔符将字符串分割为若干个子字符串。第一个参数可以使字符串或者是正则表达式,第二个参数可选,用于指定返回数组的大小。

7. localeCompare()方法

查看原书

8. fromCharCode()方法

String构造函数的静态方法,接收一个或多个字符编码,将他们转换为一个字符串。例:

console.log(String.fromCharCode(104, 101, 108, 108, 111));// hello

9. HTML方法(不常用)

5.7 单体内置对象

5.7.1 Global对象

1. URI编码方法

encodeURI()和encodeURIComponent()方法可以对URI进行编码。它们用特殊的utf-8编码替换掉所有无效的字符。

var uri = 'http://www.rickcole.com/illegal value.html#start';

// http://www.rickcole.com/illegal%20value.html#start
console.log(encodeURI(uri));

// http%3A%2F%2Fwww.rickcole.com%2Fillegal%20value.html%23start
console.log(encodeURIComponent(uri));
  • encodeURI():不会对本身属于URI的特殊字符进行编码,例如冒号、正斜杠、问好和井字号
  • encodeURIComponent():会对它发现的任何非标准字符进行编码。
    对应的解码方法是decodeURI()和decodeURIComponent()。

2. eval()方法

eval()方法接收需要执行的ECMAScript代码字符串。被执行的代码具有与该执行环境相同的作用域链。

在eval()中创建的任何变量或函数都不会被提升,因为在解析代码的时候,他们被包含在一个字符串里,所以只有在eval()执行的时候创建

3. Global对象的属性

undefined、Nan、Infinity等都是Global对象的属性,更多查询原文。

4. window对象

看第八章

5.7.2 Math对象

1. Math对象的属性(查阅原书)

2. min()和 max()方法

Math.min(1, 4, 8, 3, 9);
// 或
var values = [1, 4, 8, 3, 9];
Math.max.apply(Math, values);

3. 舍入方法

  • Math.ceil():执行向上舍入,即将数值向上舍入为最接近的整数;
  • Math.floor():执行向下舍入,即将数值向下舍入为最接近的整数;
  • Math.round():执行四舍五入。

例子,源数据为 1.1、1.5、1.9

  • ceil():2、2、2
  • floor():1、1、1
  • round():1、2、2

random()方法

Math.random返回0到1之间的一个随机数,但不包括0和1。
取随机数的模板:

Math.floor( Math.random() * 取值范围 + 起始值 );

例如,想要获得1到10的随机整数,则可以这么写:

Math.floor( Math.random() * 10 + 1 );

4. 其他方法

查询原书。

第6章 面向对象的程序设计

对象定义:“无序属性的集合,其属性可以包含基本值,对象或者函数。”

每个对象都是基于一个引用类型创建的,这个引用类型可以是原生类型,也可以是开发人员定义的类型。

6.1 理解对象

首选对象字面量来创建新的对象

6.1.1 属性类型

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

1. 数据属性

数据属性包含一个数据值的位置,在这个位置可以读取和写入值。数据属性有4个描述其行为的特性。

  • [[Configurable]]:表示能否通过delete删除属性从而重新定义属性。能否修改属性的特性,或者能否把属性修改为访问器属性。
    通过对象字面量创建的对象,该特性初始值为true

  • [[Enumerable]]:能否通过for-in循环返回属性,
    通过对象字面量创建的对象,该特性初始值为true

  • [[Writable]]:能否修改属性的值。
    通过对象字面量创建的对象,该特性初始值为true

  • [[Value]]:包含这个属性的数据值。
    这个特性的初始值为undefined

要修改属性默认的特性,必须使用Object.defineProperty()方法,这个方法接收三个参数:属性所在对象、属性的名字和一个描述符对象(描述符必须为:configurable、enumerable、writable和value)。设置一个或多个值,可以修改对应的特性值。 例如:

var person = {};
Object.defineProperty(person, 'name', {
  writable: false,
  value: 'Rick'
});

console.log(person.name); // Rick
person.name = 'Cole';
console.log(person.name); // 仍然是Rick,因为name属性的writable特性设置的为false

类似的规则也适用于不可配置属性。

PS:一旦把属性定义为不可配置的,就不能把它变回可配置了。

2. 访问器属性

访问器属性不包含数据值,它们包含一对儿gettersetter函数(并不是必要的),在读取访问器属性的时候会调用getter函数,在写入访问器属性的时候,会调用setter函数。访问器属性有如下4个特性:

  • [[Configurable]]:表示能否通过delete删除属性从而重新定义属性。能否修改属性的特性,或者能否把属性修改为数据属性。
    通过对象字面量创建的对象,该特性初始值为true

  • [[Enumerable]]:能否通过for-in循环返回属性,
    通过对象字面量创建的对象,该特性初始值为true

  • [[Get]]:在读取属性时调用的函数,默认值为undefined。
    这个特性的初始值为undefined

  • [[Set]]:在写入属性时调用的函数,默认值为undefined。
    这个特性的初始值为undefined

访问器属性不能直接定义,必须使用Object.defineProperty()来定义。例子:

var book = {
  _year: 2018,  // 带下划线的属性表示只能通过对象方法访问的属性
  edition: 1
};

Object.defineProperty(book, 'year', {
  get: function () {
    return this._year;
  },
  set: function (newValue) {
    if (newValue > 2018) {
      this._year = newValue;
      this.edition += newValue - 2018;
    }
  }
});

book.year = 2019;
console.log(book.edition);  // 为 2

上述代码创建了一个对象,定义了两个默认属性,其中有下划线的_year表示的是只能通过对象方法访问的属性。

上述代码,如果改变的year属性的值,会导致_year也变为2019,而edition变为了2,这是使用访问器属性的常见方法,即设置一个属性的值会导致其他属性值发生变化。

不一定同时指定gettersetter,没有指定setter意味着属性是不能写的,没有指定getter意味着属性是不能读的。

6.1.2 定义多个属性

使用Object.defineProperties()来一次定义多个属性。该方法接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加的或修改的属性一一对应。例:

 var book = {};

Object.defineProperties(book, {
  // 带下划线的属性表示只能通过对象方法访问的属性
  _year: {
    value: 2018
  },
  edition: {
    value: 1
  },
  year: {
    get: function () {
      return this._year;
    },
    set: function (newValue) {
      if (newValue > 2018) {
        this._year = newValue;
        this.edition += newValue - 2018;
      }
    }
  }
});

book.year = 2019;
console.log(book.edition);  // 为 2

上述代码的作用同6.1.1中的例子,只是是在同一时间创建的。

6.1.3 读取属性的特性

可以通过Object.getOwnPropertyDescriptor()取得给定属性的描述符。接收两个参数:属性所在的对象、要读取其描述符的属性名称。

返回值是一个对象,

  • 如果是访问器属性,则这个对象的属性有configurable、enumerable、get和set
  • 如果是数据属性,则该对象的属性有configurable、enumerable、writable和value

6.2 创建对象

使用Object构造函数和对象字面量都可以创建对象,但有个缺点是使用同一个接口创建很多对象会产生大量的重复代码。所以开始寻求工厂类。

6.2.1 工厂模式

用函数来封装以特定接口创建对象的细节:

function createPerson(name, age) {
  var o = new Object();
  o.name = name;
  o.age = age;
  o.sayName = function () {
    alert(this.name);
  };
  return o;
}

思考:工厂模式解决了创建对个相似对象时代码重用的问题,但是没有解决对象识别的问题(即怎么知道一个对象的类型)。

6.2.2 构造函数模式
自定义构造函数:

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayName = function () {
    alert(this.name);
  };
}

var person = new Person('rick', 22);
console.log(person.constructor == Person);
// 构造函数属性等于Person

// 更多的是使用instanceof操作符来判断
console.log(person instanceof Object);  // true
console.log(person instanceof Person);  // true

以上述方法调用构造函数会经历以下4个步骤:

  1. 创建一个新对象;
  2. 将构造函数的作用域赋给新对象(因此this就指向了新对象);
  3. 执行后构造函数中的代码(为这个新对象添加属性);
  4. 返回新对象

1. 将构造函数当做函数

var person = new Person('rick', 23);
person.sayName(); // rick

Person('Morty', 14);  // 添加到window全局变量对象上
window.sayName(); // Morty

// 在另一个对象的作用域中调用
var o = {};
Person.call(o, 'Jerry', 35);
o.sayName();  // Jerry

2. 构造函数的问题

构造函数的每一个实例包含一个不同的Function实例(sayName()),但是创建两个完成相同任务的 sayName 方法的确没有必要,因此可以通过把函数定义转移到构造函数外来解决这个问题:

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayName = sayName;
}

function sayName() {
  alert(this.name);
}

思考:这样的确解决了两个函数做一件事的问题,但是又出现了新问题:

  • 在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实;
  • 如果对象需要定义多个方法,则需要定义多个全局函数,那这个自定义的引用类型就没有丝毫封装性可言

以上问题可以用原型模式来解决

6.2.3 原型模式

我们创建的每一个函数都有一个 prototype 属性,prototype 就是通过调用构造函数而创建的那个对象的实例的原型对象。

使用原型对象的好处是可以让所有对象实例共享他所包含的属性和方法。不必在构造函数中定义对象实例的信息,而是可以将这些信息添加到原型对象中。

function Person() {
}

Person.prototype.name = 'Rick';
Person.prototype.age = 23;
Person.prototype.sayName = function () {
  alert(this.name);
}

var p1 = new Person();
var p2 = new Person();
console.log(p1.sayName == p2.sayName);  // true

如上所示,p1和p2访问的是同一组属性和方法。

1. 理解原型对象

任何时候,只要创建了一个新函数,就会为该函数创建一个prototype属性,这个属性指向该函数的原型对象。

默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。

前面例子来看,Person.prototype.constructor指向Person,通过这个构造函数,我们还可以继续向原型对象添加其他属性和方法。

重要:当调用构造函数创建一个新实例后,该实例内部将包含一个指针(内部属性),指向构造函数的原型对象。ES5中将这个指针称为[[Prototype]],在脚本中不能访问[[Prototype]],但在Firefox、Safari和Chrome中每个对象都支持一个属性_proto_;其他实现里这个属性对脚本不可见。

需要注意的一点,该连接存在于实例和构造方法的原型对象之间,而不是实例和构造方法之间。

以前面的Person构造函数和Person.prototype创建实例的代码为例,下图展示了各个对象之间的关系:

理解原型对象

上图解析:

  • Person:为构造函数,创建该函数后,就会为该函数创建一个 prototype 属性,这个属性指向该函数的原型对象 Person prototype;
  • Person prototype:会自动获得一个 constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针,通过构造函数Person,还可以继续给Person prototype 添加属性和方法;
  • person1person2:为Person实例,它们内部都包含一个指针[[Prototype]],指向构造函数的原型对象(即Person prototype,而不是Person)

虽然无法直接访问[[Prototype]],但可以通过isPrototypeOf()方法来确认对象之间是否存在这种关系。如上例子:

Person.prototype.isPrototypeOf(p1);     // true
Person.prototype.isPrototypeOf(p2);     // true

取得一个实例的原型对象:ES5新增了一个新的方法,Object.getPrototypeOf(),接收一个参数,即想要取得其原型对象的实例,在所有支持的实现中,该方法返回[[Prototype]]的值。例如:

Object.getPrototypeOf(p1) == Person.prototype;  // true
Object.getPrototypeOf(p1).name;  // Rick

总结:任何实例的内部属性prototype指向的是该创建该实例的构造函数的原型对象。所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。

每当代码读取某个对象的某个属性时,都会按照以下的顺序进行搜索,如果找到该属性则返回并停止搜索,如果没找到便继续搜索:

  1. 对象实例本身;
  2. 指针指向的原型对象;

为对象实例添加属性时,会屏蔽原型对象中保存的同名属性,但不会修改,即使设置为null也只会在实例中设置该属性,但使用delete操作符可以完全删除实例属性,便可以访问原型中的属性。

检测一个属性是存在于实例中或存在于原型对象中,使用方法hasOwnProperty()来检测:

var p1 = new Person();
var p2 = new Person();

p1.name = 'own name'; // 设置实例name属性
p1.hasOwnProperty('name');  // true

// 没有设置实例name属性
p2.hasOwnProperty('name');  // false

方法总结

  • isPrototypeOf():判断某个函数的原型对象是否是一个实例对象的原型对象;
  • getPrototypeOf():取得某个实例的原型对象;
  • hasOwnProperty():检测一个属性时存在实例中,还是存在原型对象中。

2. 原型与 in 操作符

in操作符有两种使用方法:一种是在 for-in 代码中使用;一种是单独使用。

单独使用:

如果通过对象可以访问某个属性时返回 true,无论该属性是存在于实例中还是存在于原型对象中。例:

console.log(‘name’ in p1);  // true,p1中可以访问到name属性
console.log(‘name’ in p2);  // true,同上。

所以可以组合使用inhasOwnProperty()来确定某个属性是存在于实例中,还是存在于原型中:

function hasPrototypeProperty(object, name) {
  return !object.hasOwnProperty(name) && (name in object);
}
使用for-in循环时:

返回的是所有能够通过对象访问、可枚举(enumerable)的属性。

要想取得对象上所有可枚举的实例属性,可以使用ES5中Object.keys()方法,该方法接收一个对象,并返回所有可枚举属性的字符串数组。

function Person() {
}

Person.prototype.name = 'Rick';
Person.prototype.age = '23';
Person.prototype.job = 'it';
Person.prototype.sayName = function () {
  alert(this.name);
};

var keys = Object.keys(Person.prototype);
console.log(keys);  // name, age, job, sayName

var p = new Person();
p.name = 'Morty';
p.age = 'age';

var pKeys = Object.keys(p1);
console.log(pKeys); // name, age

如果是函数原型对象直接调用,则返回name、age和job属性字符串,如果是Person实例调用,则只返回name和age属性字符串。

如果你想得到所有实例属性,无论它是否可以枚举,都可以使用Object.getOwnPropertyNames()方法:

var keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys);  // 'constructor,name,age,job,sayName'

结果中包含了不可枚举的constructor属性。Object.keys()Object.getOwnPropertyNames()方法都可以用来替代for-in循环。

3. 更简单的原型语法

使用对象字面量直接重写整个原型对象:

function Person() {}

Person.prototype = {
  name: 'Rick',
  age: 23,
  job: 'it'
};

上述代码我们将函数的原型对象设置成了以对象字面量形式创建的新对象,但是有一个特殊的地方,constructor不再指向Person

*:这样的操作其实是重写了函数默认的prototype对象,所以constructor属性就变成了新对象的constructor属性-->即指向Object构造函数,不再指向Person。尽管使用instanceOf可以得到正确的结果,但是通过constructor已经无法找到正确的函数。

Person.prototype = {
  constructor: Person,
  name: 'Rick',
  age: 23,
  job: 'it'
}

可以如上特意设置为Person,但是这样设置的constructor属性的[[Enumerable]]特性值被设置为true,但默认情况下constructor是不可枚举的,所以可以在支持ES5的浏览器中使用如下函数进行设置:

Object.defineProperty(Person.prototype, 'constructor', {
  enumerable: false,
  value: Person
});

4. 原型的动态性

由于在对象中查找值的过程是一次搜索,所以在原型对象上所做的任何修改操作都可以动态的在实例中反映出来,即使先创建实例再修改原型对象也是。例:

var p = new Person();
Person.prototype.sayHi = function () {
  alert('hi');
}
p.sayHi();    //  正常运行

但是在重写原型对象后,情况就有所不同:

var friend = new Person();
Person.prototype = {
  constructor: Person,
  name: 'Rick',
  age: 23,
  job: 'IT',
  sayName: function () {
    alert(this.name);
  }
}
friend.sayName();   //  error

重写对象后,Person函数与最初的原型对象便切断了原来的联系,并指向了新的原型对象,而sayName方法是在新的原型对象中定义的,而p实例中的原型指针指向的是最初的Person原型对象-是没有sayName属性的。如图:

重写原型后

5. 原生对象的原型对象

所有的原生对象都在其构造函数的原型上定义了方法。通过原生对象的原型,不仅可以取得所有默认方法的引用,也可以随时添加新的方法(不推荐)。

String.prototype.newFunction = function(){}

6. 原型对象的问题

所有实例在默认的情况下会取得相同的属性值。而实例经常是需要有属于自己的全部属性的,所以很少单独使用原型模式。

6.2.4 组合使用构造函数和原型模式(常用模式

创建自定义类型最常见的方式是组合使用构造函数和原型模式:构造函数定义实例属性,而原型模式用于定义方法和共享的属性。

// 构造函数定义实例属性
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
}

// 原型模式构造函数
Person.prototype = {
  constructor: Person,
  sayName: function () {
    alert(this.name);
  }
};

6.2.5 动态原型模式

构造函数和原型独立存在可能会感到困惑,所以可以使用动态原型模式来解决这个问题,它把所有的信息封装在了构造函数里,而在构造函数中,只有在必要的情况下才初始化原型。例:

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;

  if (typeof this.sayName != 'function') {
    Person.prototype.sayName = function () {
      alert(this.name);
    }
  } 
}

上述代码,只有在sayName()方法不存在的情况下才会将他添加到原型中。而且添加sayName()方法的代码只有初次调用构造方法的时候才会被调用,之后便不需要做任何修改。

6.2.6 寄生构造函数模式

上述模式都不适用的人情况下可以使用寄生构造函数模式。

function Person(name, age, job) {
  var o = new Object();
  
  o.name = name;
  o.age = age;
  o.job = job;

  o.sayName = function () {
    alert(this.name);
  };

  return o;
}

该模式除了使用 new 操作符和将包装函数称为构造函数以外,其实和工厂模式是一模一样的。

该模式可以在特殊情况下使用,例如想创建一个具有额外方法的数组,但不能直接修改Array的构造函数,可以使用这个模式:

function specialArray() {

  // 创建数组
  var array = new Array();

  // 添加值
  array.push.apply(array, arguments);

  // 添加特殊方法
  array.toPipedString = function () {
    return this.join('|');
  }

  // 返回数组
  return array;
}

6.2.7 稳妥构造函数模式

稳妥对象(durable objects):指的是没有公共属性,而且其方法不引用this的对象。该模式适用于在安全执行环境下:

function Person(name, age, job) {
  var o = new Object();
  // 定义私有变量
  o.name = name;
  o.age = age;
  o.job = job;

  o.sayName = function () {
    alert(name);  // 不使用this来访问name
  };
  return o;
}

上述代码中,除了调用sayName属性,没有其他方法访问到其数据成员。

6.3 继承

ECMScript 只支持实现继承,而且其实现继承主要是依靠原型链。

6.3.1 原型链

原型链的基本思想是:利用原型让一个引用类型继承另一个引用类型。

回顾构造函数、原型和实例的关系:

  • 每个构造函数都有一个原型对象,而创建一个构造函数,都会为该函数创建一个指向其原型对象的prototype属性;

  • 每个原型对象都会获得一个constructor(构造函数)属性,指向其对应的构造函数;

  • 调用构造函数创建了一个实例后,该实例内部将包含一个指针[[prototype]],指向其构造函数的原型对象。

假如,让旧类型的原型对象等于另一个新类型的实例,此时,该原型对象将包含一个指向新原型的指针,而这个新类型内也包含着一个指向新构造函数的指针,如果这个新原型又是另一个类型的实例,则上述关系依然成立,如此层层递进就构成了实例与原型的链条,这就是原型链。例如下:

function Dad() {
  this.property = 'dad';  // 实例属性
}

// 原型方法
Dad.prototype.getDadValue = function () {
  return this.property;
}

function Son() {
  this.sonProperty = 'son';
}

// 继承了Dad
Son.prototype = new Dad();
Son.prototype.getDadValue = function () {
  return this.property;
}

var instance = new Son();
instance.getDadValue(); // ‘dad’
继承

上图可以看出,重写了 Son 的原型对象 prototype,使其等于 Dad 的实例,因此 Son 的原型对象中就有了指向 Dad 原型对象的指针,因此也就继承了 Dad
中的属性和方法。

getDadValue 方法在 Dad 的原型中,因为它是一个原型方法;

而 property 属性在 Son 原型中是因为 property 是一个实例属性,而 Son 的原型通过继承成为了 Dad 的实例。
因此,当以读取模式搜索一个实例属性的时候,首先在实例中搜索,然后再到原型中搜索,如果有继承的情况,则继续沿着原型链继续向上搜索。搜索过程总是要到原型链末端才会停止。

1. 别忘了默认的原型(Object.prototype)

所有引用类型都继承 Object,而这个继承也是通过原型链实现的。也就是说,所有函数的默认原型都是 Object 的实例,因此,每个默认原型都会包含一个指针指向 Object.prototype,所以所有自定义类型都会继承 toString(),valueOf()
等方法的原因。

2. 确定原型和实例的关系

有两种方法确定原型和实例之间的关系:

  • instanceOf:只要实例与原型链中出现的构造函数匹配,就返回 true,如上例可知,instance instanceOf Objectinstance instanceOf Dadinstance instanceOf Son都会返回 true,因为 instance 是这三个构造函数任意一个的实例。

  • isPrototypeOf():Object.prototype.isPrototypeOf(instance);

3. 谨慎地定义方法

无论是重写父类的某个方法,还是添加父类中没有的方法,代码都要放在替换原型语句的后面。

注意:使用原型链实现继承时,不能用对象字面量的方法创建原型方法,这样做会替换掉原原型对象,破坏原型链。

4. 原型链的问题

  1. 包含引用类型值的原型属性会被所有实例共享,所以要在构造函数中定义属性而不是在原型对象中;但是,通过实现原型链继承,子类原型会指向父类实例,而父类构造方法内定义的属性,会作为实例属性,顺理成章的成为了子类实例的原型属性。例如:
function SuperType() {
  this.letters = ['a', 'b', 'c'];
}

function SubType() {
}

SubType.prototype = new SuperType();

var ins1 = new SubType();
ins1.letters.push('d');
console.log(ins1.letters); // "a", "b", "c", "d"

var ins2 = new SubType();
console.log(ins2.letters); // 同样是"a", "b", "c", "d",因为SubType的每个实例会共享letters属性
  1. 创建子类的实例时,无法给父类的构造函数传递参数,或者说没有办法在不影响所有对象实例的情况下,给父类构造函数传递参数。

由于以上问题,很少会单独使用原型链。

6.3.2 借用构造函数

借用构造函数(constructor stealing),或者是伪造对象或经典继承,该技术的思想很简单:在子类型构造函数内调用父类的构造函数。例:

function SuperType() {
  this.colors = ['red', 'blue'];
}

function SubType() {
  // 继承了SuperType
  SuperType.call(this);
}

var ins1 = new SubType();
ins1.colors.push('yellow');
console.log(ins1.colors); // “red, blue, yellow”

var ins2 = new SubType();
console.log(ins2.colors); // “red, blue”

上述代码中,通过使用 call() 和 apply() 方法,实际上是在(未来将要)新建
SubType 实例的时候调用了 SuperType 构造函数,这样就会在新的 SubType
对象上执行 SuperType() 函数中定义对象的所有方法,因此每个 SubType 实例也就拥有了自己的 letters 副本。

  • 传递参数
    借用构造函数可以在子类的构造函数中,向父类传递参数。
function SuperType(name) {
  this.name = name;
}

function SubType() {
  // 继承了SuperType,同时还传递了参数
  SuperType.call(this, 'Rick');

  // 实例属性
  this.age = 23;
}

var instance = new SubType();
console.log(instance.name);
console.log(instance.age);
  • 借用构造函数的问题
    该方法仍然存在函数复用的问题,而且父类原型对象中的方法对于子类实例也是不可见的,所以借用构造函数方法也是很少使用的。

6.3.3 组合继承(常用模式)

组合继承(combination inheritance),也叫伪经典继承,思想为:使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样既实现了函数的复用,还保证每个函数都有自己的属性。

function Super(name) {
  this.name = name;
  this.letters = ['a', 'b'];
}

Super.prototype.sayName = function () {
  alert(this.name);
}

function Sub(name, age) {
  // 继承属性
  Super.call(this, name);
  this.age = age;
}

// 继承方法
Sub.prototype = new Super();

Sub.prototype.sayAge = function () {
  alert(this.age);
}

var instance1 = new Sub('Rick', 23);
instance1.letters.push('c');
console.log(instance1.letters); // "a", "b", "c"
instance1.sayName();  // "Rick"
instance1.sayAge(); // 23

var instance2 = new Sub('Morty', 12);
console.log(instance2.letters); // "a", "b"
instance2.sayName();  // "Morty"
instance2.sayAge(); // 12

这样就可以让不同的Sub实例拥有各自的实例属性和相同的方法了。

6.3.4 原型式继承

原型式继承的想法是:借助原型,可以基于已有的对象,然后建立新的对象,同时还不用因此创建新的自定义类型,例子如下:

function createObject(o) {
  function F() {
  }

  F.prototype = o;
  return new F();
}

在createObject内部,先创建了一个临时性构造函数,然后将传入的已有对象作为该构造函数的原型对象,最后返回了这个临时类型的一个新实例。本质上该函数对传入的对象进行了一次浅复制。

ES5新增Object.create()方法,规范化了原型化继承,该方法传入两个参数,一个是作为新对象原型的对象和(可选)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()和上述方法没差别。例:

var person = {
  name: 'Rick',
  friends: ['Lebron', 'Kobe', 'Wade']
};

var anotherPerson = Object.create(person);
anotherPerson.name = 'Morty';
anotherPerson.friends.push('Jessica');

当传入第二个参数的时候,与Object.defineProperties()方法的第二个参数格式相同,每个属性都是通过自己的描述符定义的,以这种方式指定的任何属性都会覆盖原型对象上的同名属性。例:

// person部分代码如上
var anotherPerson = Object.create(person, {
  name: {
    value: 'Morty'
  }
});

如果没有必要兴师动众的创建构造函数,而只想让一个对象与另一个对象保持类似的话,就可以使用原型式继承。

不过原型式继承同样有共享带有引用对象值的问题。

6.3.5 寄生式继承

寄生式(parasitic)继承的思路与寄生式构造函数和工厂模式类似:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式增强该对象,最后再返回该对象。例:

function createAnother(original) {
  var clone = Object.create(original);
  clone.sayHi = function () {
    alert('hi');
  }
  return clone;
}

但是寄生式继承因为不能复用代码而效率低下,这点与构造函数类似。

6.3.6 寄生组合式继承(最理想模式)

上述说到,组合继承是JS最常用的继承模式,但也有不足,那就是无论在什么情况下爱,都会调用两次父类构造函数:一次是在创建子类原型时,一次是在子类构造函数内。这样就会有两组属性:一组在子类的原型对象中,一组在实例中,实例中的属性会覆盖掉子类原型中的同名属性,导致效率低下。

寄生组合继承:通过借用构造函数来继承属性,通过原型链的混合形式来继承方法。

其思路为:不必为了指定子类型的原型而调用父类的构造函数,所需要的只是父类的一个副本。本质上是使用寄生式继承来继承超类的原型,然后将结果指定给子类的原型。该模式代码如下:

function inheritPrototype(subType, superType) {
  // 创建对象
  var prototype = Object.create(superType.prototype);

  // 增强对象
  prototype.constructor = subType;
  
  // 指定对象
  subType.prototype = prototype;
}

上述代码中,接收两个参数,第一个参数是子类型的构造函数,第二个参数是父类的构造函数。函数内部代码解读如下:

  1. 创建父类的原型的一个副本;
  2. 为创建的这个副本添加 constructor 属性,弥补因重写原型而丢失的默认
    constructor 属性;
  3. 将创建的新对象(副本)赋值给子类型的原型。

这样,就可以使用 inheritPrototype 方法来替换前例子中为子类型原型赋值的语句了。

function Super(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}

Super.prototype.sayName = function () {
  alert(this.name);
};

function Sub(name, age) {
  // 继承属性
  Super.call(this, name);
  this.age = age;
}

inheritPrototype(Sub, Super); // 调用寄生组合继承方法

Sub.prototype.sayAge = function () {
  alert(this.age);
};
寄生组合继承方法

上例的高效体现于之调用了一次父类构造函数,因此避免了在子类构造函数原型上创建重复、不必要的属性。同时还能保持原型链的完整。

该模式的继承是引用类型最理想的继承模式。

第7章 函数表达式

函数有两种定义方式,一种是函数声明,一种是函数表达式。

函数声明:最重要的一个特性是函数声明提升。而函数表达式没有该特性。

7.1 递归

递归是指在一个函数内通过名字调用自身的情况,如下这个阶乘案例:

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

如上的代码可以实现阶乘的需求,但是存在函数名过于耦合的问题,所以可以使用如下代码:

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

可以通过arguments.callee来代替函数名,但是在严格模式下不可以访问arguments.callee,所以在严格模式下使用命名函数表达式来达成同样的目的:

var factorial = (function f(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * f(num - 1);
  }
});

上述在严格模式和非严格模式下都可以运行。

7.2 闭包

闭包是指有权访问另一个函数作用域中变量的函数。创建闭包最常用的方式是在一个函数内部创建另外一个函数。

function createComparison(propertyName) {
  return function (obj1, obj2) {
    var value1 = obj1[propertyName];
    var value2 = obj2[propertyName];

    if (value1 < value2) {
      return -1;
    } else if (value1 > value2) {
      return 1;
    } else {
      return 0;
    }
  }
}

加粗的代码是一个内部函数(一个匿名函数)的代码,该代码访问了外部函数中的变量propertyName,之所以能访问这个变量是因为内部函数的作用域链中包含外部函数的作用域。

理解作用域链:

每当一个函数被调用的时候,会创建一个执行环境(execution context)及相应的作用域链,并把作用域链赋给一个特殊的内部属性(即[[Scope]])。然后使用this、arguments 和其他命名参数的值来初始化函数的活动对象(activation object)。在作用域链中,外部函数的活动对象永远是第二位,外部函数的外部函数活动对象是第三位。

function compare(value1, value2) {
  if (value1 < value2) {
    return -1;
  } else if (value1 > value2) {
    return 1;
  } else {
    return 0;
  }
}

var result = compare(5, 10);

上例中,当第一次调用 compare 函数的时候,会创建一个活动对象(包含this、arguments、value1、value2)处于作用域链的第一位,全局环境的变量对象(包含this、result、compare)在 compare 执行环境的作用域链中处于第二位。如图下:


compare函数

后台的每个执行函数都有一个表示变量的对象-变量对象。全局环境的变量对象一直存在,而局部函数的变量对象只有执行的时候才会存在。

闭包的情况:

一般情况下,当函数执行完毕后,局部活动对象就会销毁,内存中仅保留全局作用域,但是闭包情况有所不同。

在另一个函数中定义的函数会将包含函数(外部函数)的活动对象添加到它的作用域中,下图展示了下列代码执行时,包含函数与内部匿名函数的作用域链的情况。

// 将被返回的匿名函数的引用赋给了变量compare;
var compare = createComparison('name');
var result = compare({name: 'Rick'}, {name: 'Morty'});
包含函数与内部匿名函数的作用域链的情况

在匿名函数在 createComparison() 中被返回时,它的作用域链被初始化为包含 createComparison() 函数的活动对象和全局变量对象,这样匿名函数就可以访问createComparison 函数中的所有变量。更重要的是,createComparison 函数执行完后,即返回匿名函数后,其执行环境的作用域链会被销毁,但它的活动对象仍然保留在内存中,直到匿名函数被销毁后,createComparison 函数的活动对象才会被销毁。

注意:由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存,过度使用闭包会使内存占用过多,所以应该谨慎使用闭包。

附录:闭包的实例

function foo(x) {
  var tmp = 3;
  function bar(y) {
    alert(x + y + (++tmp));
  }
  bar(10);
}
foo(2);

上述代码无论执行多少次结果都为16,因为在function里嵌套function,内部function可以访问外部function里的参数和变量。但这并不是闭包,再看如下代码:

function foo(x) {
  var tmp = 3;
  return function (y) {
    alert(x + y + (++tmp));
  }
}

var bar = foo(2); // bar 现在是一个闭包
bar(10);

上述代码执行也会返回16,虽然虽然 bar 不直接处于 foo
的内部作用域,但 bar 还是能访问 x 和 tmp。
但是,由于 tmp 仍存在于 bar 闭包的内部,所以它还是会自加1,而且你每次调用 bar 时它都会自加1。

7.2.1 闭包和变量

由于ES5中没有块级作用域,所以闭包只能取得包含函数中任何变量的最后一个值。闭包保存的是整个变量对象而不是某个值,如下:

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

上述函数返回的函数数组中的每个函数都返回10,因为每个函数的作用域链中都保存着 createFunctions 函数的活动对象,所以它们引用的是同一个变量 i,当createFunction 返回后,i 的值是10,每个函数都引用着保存i = 10的同一个活动对象。要想返回不同的函数,则需要如下操作:

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

通过另一个匿名函数,制造另一个闭包,就可以让每个函数返回对应的索引值。

7.2.2 this对象

this对象是在运行时基于函数执行环境而绑定的:

全局环境中,this等于window,而当函数被作为某个对象的方法调用的时候,this就是这个对象。

匿名函数的执行环境具有全局性,因此this对象通常指向window。如:

var name = 'The Window';
var object = {
  name: 'The Object',
  getName: function () {
    return function () {
      return this.name;
    }
  }
}
console.log(object.getName());  //  The Window

上述代码中 object.getName() 等同于:

var closureFunction = object.getName();
closureFunction();

由上代码可看出,在 object.getName() 相当于在全局环境下调用闭包,所以闭包内的 this 对象自然是 window 对象。而想要取得 object 对象内的 name 属性,则需要如下操作:

var name = 'The Window';
var object = {
  name: 'The Object',
  getName: function () {
    var that = this;
    return function () {
      return that.name;
    }
  }
}
console.log(object.getName());  //  The Object

在定义匿名函数的时候,将 this 值赋给了一个叫 that 的变量,而在定义了闭包之后,闭包也可以访问该变量,即使函数返回闭包之后,that 也仍然引用着 object,所以调用 getName() 返回了 ’The Object’ ;

7.2.3 内存泄漏

如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素无法被销毁。具体案例查看原书。

7.3 模仿块级作用域

JavaScript没有块级作用域的概念,所以用作块级作用域(私有作用域)的匿名函数的写法是:

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

函数声明function(){}后面是不可以加括号的,而函数表达式是可以加括号的。要想将函数声明转换为函数表达式,需要加一对括号:

(function(){});

在私有作用域内定义的所有变量都会在执行结束后被销毁。

7.4 私有变量

在函数内定义的变量都是私有变量,私有变量包括:

  • 函数的参数
  • 局部变量
  • 在函数内定义的其他函数。

通过闭包可以创建访问私有变量的公有方法,能访问私有方法和私有变量的公有方法称为特权方法

可以在构造函数中定义特权方法,但是必须使用构造函数模式来达到该目的,缺点就是每个对象都会创建一组相同的方法。使用静态私有变量可以避免这个问题。

(function () {
  // 私有变量和私有函数
  var privateVariable = 10;

  function privateFunction() {
    return false;
  }

  // 构造函数
  MyObject = function () {
  };

  MyObject.prototype.publicFunction = function () {
    privateVariable++;
    return privateFunction();
  }
})();

该模式创建了私有作用域,使用函数表达式定义了全局构造函数,并使用原型定义特权方法。

该模式的特点是实例共享私有变量和函数。

(function () {
  var name = "";

  Person = function (value) {
    name = value;
  };

  Person.prototype.getName = function () {
    return name;
  }
})();

也可以如上定义私有方法,根据需求。

注:多查找一层作用域链,就会影响查找速度,这是使用闭包和私有变量的不足之处。

7.4.2 模块模式

模块模式(module pattern)是指,为单例创建私有变量和特权方法,js一般是使用对象字面量创建单例。

var singleton = {
  name: value,
  method: function () {
    // Method Code
  }
}

模块模式通过为单例添加私有变量和特权方法可以使其得到增强。

var singleton = function () {
  // 私有变量和私有函数
  var privateVariable = 10;

  function privateFunction() {
    return false;
  }

  // 特权/公有方法和属性
  return {
    publicVariable: true,
    publicFunction: function () {
      privateVariable++;
      return privateFunction();
    }
  }
}

上述代码本质上是该对象字面量定义的是单例的公共接口。这种模式在需要对单例进行某种初始化,同时又需要维护其私有变量的时候很有用。

var application = function () {

  // 私有变量和函数
  var components = new Array();

  // 初始化
  components.push(new BaseComponent())

  // 公共
  return {
    getComponentCount: function () {
      return components.length;
    },

    registerComponent: function (component) {
      if (typeof component == 'object') {
        components.push(component);
      }
    }
  }
}();

在web应用程序中,经常需要使用一个单例来管理应用程序级的信息。
上述例子创建了一个简单的管理组件的application对象,两个特权方法前者是返回已注册的组件数量,后者是用于注册新组件。

7.4.3 增强的模块模式

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

var application = function () {

  // 私有变量和函数
  var components = new Array();

  // 初始化
  components.push(new BaseComponent());

  // 创建application的一个局部副本
  var app = new BaseComponent();

  // 公共
  app.getComponentCount = function () {
    return components.length;
  }

  app.registerComponent = function (component) {
    if (typeof component == 'object') {
      components.push(component);
    }
  }

  // 返回这个副本
  return app;
}();

上述代码实际上是 application 对象的局部变量版,然后又为 app 添加了特权方法,最后返回 app,仍然是将它赋值给全局变量 application。

第8章 BOM

BOM(Browser Object Module),浏览器对象模型。

8.1 window对象

window有双重角色,它是JavaScript访问浏览器的一个接口,也是ECMAScript定义的Global对象。

8.1.1 全局作用域

在全局声明的变量、函数都是window的属性和方法。

8.1.2 窗口关系及框架(iframe)

每个 iframe 内都拥有自己的 window 对象,保存在frames 集合中,可以通过索引访问:

window.frames[index];

不过最好使用 top.frames[0] 来获取顶部 iframe;

8.1.3 窗口位置

8.1.4 窗口大小

8.1.5 导航和打开窗口

8.1.6 间歇调用和超时调用

setTimeout()该方法会返回一个超时调用ID,可以通过它来取消超时调用:

var timeoutId = setTimeout(function () {
}, 1000);
clearTimeout(timeoutId);

第二个参数为表示等待多长时间的毫秒数,但经过该时间后指定的代码不一定执行。

JavaScript是一个单线程序的解释器,因此一定时间内只能执行一段代码。为了控制要执行的代码,就有一个JavaScript任务队列。这些任务会按照将它们添加到队列的顺序执行。

setTimeout() 的第二个参数告诉 JavaScript 再过多长时间将该任务添加到队列中,如果队列是空的,那么添加的代码会立即执行,如果队列不是空的,那么就要等前面的代码执行完了再执行。

以下是一个常见的间歇调用的实例:

var num = 0;
var max = 20;

function incrementNumber() {
  num++;

  // 如果执行次数未达到 max 设定的值,则设置另一个超时调用
  if (num < max) {
    setTimeout(incrementNumber, 500);
  } else {
    console.log('Done!');
  }
}

setTimeout(incrementNumber, 500);

一般来说,使用超时调用模拟间歇调用时最佳模式,因为后一个间歇调用可能会在前一个间歇调用结束之前启动,所以最好不要使用间歇调用。

8.1.7 系统对话框

alert、confirm。
prompt() 用于提示用户输入一些文本,接收两个参数,一个是提示文本,第二个是默认值。

var result = prompt('who is this?', '');
if (result) {
  console.log('this is ' + result);
}

确认:result 返回文本框内的值;
取消:result 返回 null。

8.2 location对象

window.location 和 document.location 引用的是同一个对象。下面是 location 的所有属性:


location对象的属性

8.2.1 查询字符串参数

getUrlParamByKey(k) {
  var params = {}; // 参数对象
  var search = location.search;
  if (!search) {
    return '';
  }
  var s = search.substring(1).split('&');
  s.forEach(function(item) {
    var key = item.substring(0, item.indexOf('='));
    var param = item.substring(item.indexOf('=') + 1);
    params[key] = param;
  });
  return params[k] || '';
}

8.2.2 位置操作

改变位置最常用的是

location.href = url;

而如果不想生成一条历史数据,则使用

location.replace(url);

重新加载当前页面使用 reload() ,如果传入 true 为参数,则会从服务器重新加载页面信息,如果不传参数则有可能在缓存中取数据。

8.3 navigator对象

识别客户端浏览器的事实标准。(属性和方法查阅api);

8.3.1 检测插件

非ie浏览器可以使用 plugins 数组来查看是否安装了特定的插件,该数组的每一项都包含以下属性:

  • name:插件的名字;
  • description:插件的描述;
  • filename:插件的文件名;
  • length:插件所处理的MIME类型的数量。

在非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;
}

// 检测Flash
console.log(hasPlugin('Flash'));

// 检测QuickTime
console.log(hasPlugin('QuickTime'));

在ie浏览器中检测插件:

function hasIEPlugin(name) {
  try {
    new ActiveXObject(name);
    return true;
  } catch (e) {
    return false;
  }
}

// 检测Flash
console.log(hasPlugin('ShockwaveFlash.ShockwaveFlash'));

// 检测QuickTime
console.log(hasPlugin('QuickTime.QuickTime'));

8.3.2 注册处理程序

可以让一个站点指明它可以处理特定类型的信息。

8.4 screen对象(用处不大)

8.5 history对象

go()方法可以在用户的历史记录中任意跳转。传入参数不同有不同的作用:

  • 如果为数字,则正数为向前跳转,负数为向后跳转;
  • 如果为字符串,会跳转至包含该字符串的最近一个历史记录。

还有两个 history.back() 和 history.forward() 来代替
go()。

第9章 客户端检测

不到万不得已,不要使用客户端检测,先设计最通用的方案,然后再针对特定浏览器技术增强该方案。

第10章 DOM

10.1节点层次

10.1.1 Node类型

DOM1 级定义的 Node 接口,由 DOM 中的所有节点所实现。JavaScript 中的所有节点类型都继承自此 Node 类型,所有节点共享 Node 类型的基本属性和方法。

1. nodeName 和 nodeValue

对于元素节点,nodeName始终是元素的标签名,而nodeValued的值则始终为null;

2. 节点关系

  • childNodes:保存着一个NodeList对象;
  • NodeList:类数组对象,保存一组有序的节点;
  • previousSiblingnextSibling:前一个或后一个同胞节点;

3. 操作节点

  • appendChild():用于向 childNodes 列表末尾添加一个字节点,如果新 node 在文档中,则从原来的位置移到新的位置,DOM 树可以看作是一系列指针连接起来的。
    该方法返回新增的节点,例如:
newNode == someNode.appendChild(newNode);
  • insertBefore(newNode, nodeReference):可以把新节点放在某个位置上,第一个参数为要插入的节点,第二个参数为作为参照的节点,插入后新节点会成为参照节点的 previousSibling。
    该方法返回被插入的节点,例:
newNode == someNode.insertBefore(newNode, referNode);
  • replaceChild(newNode, replacedNode):可以替换字节点list中的node,第一个参数为新插入的节点,第二个参数为要替换掉的节点。移除后的节点仍为文档所有,但是没有了位置。
    该方法返回新节点。
  • removeChild(dumpedNode):参数为需要移除的节点,移除后的节点仍为文档所有,但是没有了位置。
    该方法返回移除的节点。

4. 其他方法

  • cloneNode(deepClone):可以创建一个调用该方法节点完全相同的一个副本节点,参数为是否为深复制,如果为 true,则复制其本身和所有字节点;如果为
    false,则只复制其本身。
    该方法返回复制后的节点。
  • normalize():处理文档树中的文本节点。

10.1.2 Document类型

最常见的是作为HTMLDocument实例的document对象。
document的属性:

  • nodeType:9;
  • nodeName:“#document”;
  • nodeValue:null;
  • parentNode:null;
  • ownerDocument:null;

1. 文档的子节点

  • document.documentElement:取得对html的引用。
  • document.body:取得对body的引用。
  • document.doctype:取得对<!DOCTYPE>的引用,浏览器对该节点的支持不一致,所以用处不大。

2. 文档信息

document对象还有一些标准Document对象没有的属性。

  • document.title:html中title元素的文本,可以通过document.title = ‘somestring’来设置标题。
  • document.URL:页面的完整URL。不可设置。
  • document.domain:页面的域名。可设置,但是域名必须是URL中包含的子域名。
    当页面中包含来自其他子域的框架或者内嵌框架时,可以将两个页面的domain设为相同的值就可以通过JavaScript互相通信了。
    PS:如果将域名设置为“loose”后,便不能设置能“tight”,例如:domain设置为demo.com后,就不可以设置回www.demo.com
  • document.referrer:链接到当前页面的前一页面的URL。不可设置。

3. 查找元素

  • document.getElementById:根据元素ID查找匹配的元素,返回符合匹配的第一个元素的引用。
  • document.getElementsByTagName:根据元素标签名查找元素,返回符合匹配的零个或若干个元素的NodeList。可以通过 NodeList[index] 或者NodeList[name] (元素 name 特性的值)进行查找。
  • document.getElementsByTagName:返回带有给定
    name 的所有元素。

4. 特殊集合

下面的集合都为HTMLCollection对象。

  • document.anchors:返回所有带name属性的a元素。
  • document.applets:返回所有applets元素(不推荐使用)
  • document.forms:返回所有的form元素,等同于document.getElementsByTagName(‘form’);
  • document.images:返回素有img元素。
  • document.links:返回所有带href元素的a元素。

5. DOM一致性检测:查阅原书。

6. 文档写入

  • document.write() 和 document.writeln():后方法有换行的功能。
  • document.open() 和 document.close():打开和关闭网页输出流。

10.1.3 Element类型

Element类型用于表现XML和HTML元素,提供了元素的标签名,子节点和特性的访问。Element节点有以下特征:

  • nodeType:1;
  • nodeName:元素的标签名(或者使用tagName访问标签名);
  • nodeValue:null;
  • parentNode:Document或Element;

1. HTML元素(HTMLElement类型)

HTMLElement直接继承Element,并添加了下列属性(以下元素都可取得或者修改相应的特性值):

  • Id:元素在文档的唯一标识符;
  • title:有关元素的附加说明;
  • lang:元素内容的语言代码,很少用到;
  • dir:语言的方向,“ltr”(left-to-right),“rtl”(right-to-left);
  • className:元素的class特性对应;

2. 取得特性 getAttribute(attr)

根据特性名取得特性值。

ps:取得class特性的值时传入“class”而不是“className”,如特性名不存在则返回 null。

3. 设置特性 setAttribute(attr, value)

第一个是需要设置的特性,第二个为值,也可以通过直接给特性属性赋值来设置

div.id = newId。

removeAttribute(attr)可以删除特定属性。

4. attributes属性

attributes 属性包含一个 NamedNodeMap,不常用。

5. 创建元素 document.createElement()

该方法只接受一个参数,即要创建的元素标签名(HTML中不分大小,而XML和XHTML中区分大小)并返回该DOM元素的引用

创建后可以为新元素添加特性或子节点,还需要通过appendChild()、insertBefore()、replaceChild()这几种方法添加到 document 中才会起作用。

6. 元素的子节点childNodes

由于不同浏览器对子节点的定义不同,所以在通过
childNodes 遍历节点时先判断 nodeType == 1,如果是元素节点则遍历;

也可以使用getElementsByTagName(),返回当前元素后代中符合匹配的元素集合。

10.1.4 Text类型

文本节点由Text类型表示,包含可以照字面解释的纯文本内容。可以包含转义后的HTML字符,但是不能包含HTML代码。Text节点有以下特征:

  • nodeType:3;
  • nodeName:“#text”;
  • nodeValue:表示节点所包含的文本(也可以使用data取得同样的值);
  • parentNode:为一个Element;
  • 没有子节点;

可以使用以下操作来操作节点中的文本:

  • appendData(text):将text添加到节点末尾;
  • deleteData(offset, count):从offset指定位置删除count个字符;
  • insertData(offset, text):从offset位置插入text;
  • replaceData(offset, count, text):用text替换从offset起到offset+count为止的字符;
  • splitText(offset):从offset位置将文本节点拆分成两个文本节点;
  • substringData(offset, count):提取从offset起始到offset+count为止的字符串;

可以通过nodeValue来设置文本节点的值。

1. 创建文本节点:

可以使用 document.createTextNode(text) 创建新文本节点,传入要创建节点的文本值。

一般来说每个元素只能有一个文本子节点,不过在某些情况下也可能包含多个文本子节点,例如:

var element = document.createElement('div');
var textNode = document.createTextNode('hello world');
element.appendChild(textNode);
var anotherTextNode = document.createTextNode(' oh yeah~');
element.appendChild(anotherTextNode);

2. 规范化文本节点

如上所示,文档中出现相邻的同胞文本节点会比较混乱,于是就有了将相邻节点合并为一个节点的方法:normalize(),可以在包含多个文本子节点的父元素上调用此方法,例如(示例接上):

var element = document.createElement('div');
var textNode = document.createTextNode('hello world');
element.appendChild(textNode);
var anotherTextNode = document.createTextNode(' oh yeah~');
element.appendChild(anotherTextNode);

element.normalize();
console.log(element.childNodes.length);         // 1
console.log(element.firstChild.nodeValue)       // hello world oh yeah~

3. 分割文本节点

splitText(offset) 可以将文本节点分成两个文本子节点。

10.1.5 Comment类型

注释在DOM中是通过Comment类型来表示的,Comment类型有以下特征:

  • nodeType:8;
  • nodeName:“#comment”;
  • nodeValue:注释的内容;
  • parentNode可能是Element或Document;
  • 没有子节点;
    Comment与Text类型继承自相同的基类,所以它拥有除splitText之外所有的字符串操作方法。

10.1.6 CDATASection类型

CDATASection类型只针对XML文档,表示CDATA区域,与Comment类型,也拥有除splitText()以外所有的字符串操作方法,有以下特征:

  • nodeType:4;
  • nodeName:“#cdata-section”;
  • nodeValue:为cdata区域中的内容;
  • parentNode:Element或Document;
  • 没有子节点;

10.1.7 DocumentType类型(不常用)

10.1.8 DocumentFragment类型(不常用)

10.1.9 Attr类型

元素的特性在DOM中用Attr类型来表示。特性就是存在于元素attributes属性中的节点,特性节点具有以下特征:

  • nodeType:11
  • nodeName:特性的名称;
  • nodeValue:特性的值;
  • parentNode:null;
  • 在HTML中没有子节点,在XML中子节点可以使Text或EntityReference

很少使用该节点。

10.2 DOM操作技术

10.2.1 动态脚本

一种方法是:

var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'demo.js';
document.body.appendChild(script);

第二种方法是:

var script = document.createElement('script');
script.type = 'text/javascript';
var code = 'function(){alert('demo.js')}';
script.appendChild(code);
document.body.appendChild(script);

10.2.2 动态样式(查阅原文)

10.2.3 操作表格(查阅原文)

10.2.4 使用NodeList

NodeList 和“近亲” NamedNodeMap 和
HTMLCollection,都是“动态的”,每当文档内容发生变化,它们都会进行更新

第11章 DOM扩展

对DOM的两个主要扩展为Selectors API(选择符API)和HTML5.

11.1 选择符API

Selectors API Level 1的核心方法是:querySelector() 和
querySelectorAll()。

可以通过Document及Element类型的实例来调用他们。

11.1.1 querySelector()方法

querySelector() 方法接收一个CSS选择符,返回与该模式匹配的第一个元素,如果没有后的话就返回null。

11.1.2 querySelectorAll()方法

同样接收一个 CSS 选择符,返回所有与该模式匹配的元素,返回一个 NodeList 实例。

其底层相当于一组元素的快照。返回的NodeList可以通过 item(index) 或 [] 访问。可以调用该方法的有Document、DocumentFragment 和 Element。

11.1.3 matchesSelector()方法(支持性很差)

Selectors API Level 2为Element类型新增了一个方法,这个方法接收一个参数,即CSS选择符,如果调用元素与该选择符匹配则返回true,否则返回false。

11.2 元素遍历

对于元素之间的空格,IE9及之前的版本不会返回文本节点,而其他浏览器都会返回文本节点,这样导致了
childNodes 和 firstChild等属性时行为不一致,所以为了保持DOM规范,Element Traversal 规范新定义了一组属性。

Element Traversal API为DOM元素添加了以下5个属性:

  • childElementCount:返回子元素(不包括文本节点和注释)的个数。
  • firstElementChild:指向第一个子元素;firstChild的元素版。
  • lastElementChild:指向最后一个子元素;lastChild的元素版。
  • previousElementSibling:指向前一个同辈元素;previousSibling的元素版。
  • nextElementSibling:指向后一个同辈元素;nextSibling的元素版。

11.3 HTML5

本节只谈论HTML5与DOM相关的内容。

11.3.1 与类相关的扩充

1. getElementsByClassName()

可以通过 document 对象对所有HTML元素调用该方法。
该方法接收一个参数,即一个包含一个或多个类名的字符串,返回带有指定类的所有元素的 NodeList 。传入类名顺序不重要。

// 取得所有类中包含“username”和“current”的元素,类名的先后顺序无关
var allCurrentUsername = document.getElementsByClassName('username current');

2. classList属性

该属性是新集合类型DOMTokenList的实例。与其它集合相似,有一个length属性,可以使用item()或[]访问每个元素,还定义了如下方法:

  • add(value):将给定字符串添加到列表中,如果值已存在,则不添加
  • remove(value):从列表中删除给定的字符串
  • contains(value):表示列表是否存在给定的值,存在返回true,否则返回false
  • toggle(value):如果列表中已存在给定的值,则删除,否则便添加

支持classList的浏览器有FireFox 3.6+ 和Chrome。附录:如何在ie中添加classList属性:

if (!("classList" in document.documentElement)) {
  Object.defineProperty(HTMLElement.prototype, 'classList', {
    get: function() {
      var self = this;

      function update(fn) {
        return function(value) {
          var classes = self.className.split(/\s+/g),
            index = classes.indexOf(value);

          fn(classes, index, value);
          self.className = classes.join(" ");
        }
      }

      return {
        add: update(function(classes, index, value) {
          if (!~index) classes.push(value);
        }),

        remove: update(function(classes, index) {
          if (~index) classes.splice(index, 1);
        }),

        toggle: update(function(classes, index, value) {
          if (~index)
            classes.splice(index, 1);
          else
            classes.push(value);
        }),

        contains: function(value) {
          return !!~self.className.split(/\s+/g).indexOf(value);
        },

        item: function(i) {
          return self.className.split(/\s+/g)[i] || null;
        }
      };
    }
  });
}

11.3.2 焦点管理

1. document.activeElement

该属性始终会引用DOM中当前获得焦点的元素,获得焦点的方法有,页面加载、用户输入(通常是Tab键)、和在代码中调用focus();

2. document.hasFocus()

可以检测文档是否获得了焦点

11.3.3 HTMLDocument的变化

HTML5扩展了HTMLDocument,增加了新功能。

1. document.readyState

该属性有两个值,一个是loading:正在加载文档;complete:已经加载文档。该属性经常用来实现一个文档已经加载完成的指示器。

If(document.readyState == ‘complete’){
    // do something
}

2. 兼容模式(document.compatMode)

页面的模式区分标准的或混杂的,检测页面兼容模式就成了必要功能。
标准模式下:document.compatMode的值为“CSS1Compat”;
混杂模式下:document.compatMode的值为“BackCompat”;

3. document.head属性

该属性引用head元素。

11.3.4 字符集属性

charset属性表示文档中实际使用的字符集,可以直接指定新字符集,也可以通过<meta>元素。

defaultCharset属性,表示浏览器默认的字符集。

11.3.5 自定义数据属性

HTML5规定可以为元素添加非标准属性,但要添加前缀
data- 。

可以使用dataset属性来访问自定义属性的值。

PS:某些版本的ie浏览器中不支持dataset属性,使用getAttribute('data-xxx') 和 setAttribute('data-xxx') 方法操作自定义数据属性。

11.3.6 插入标记

1. innerHTML

在读模式下,innerHTML属性返回调用元素的所有子节点(包括元素,注释和文本节点)对应的HTML标签。

在写模式下,innerHTML会根据指定的值建立新的DOM树,并完全替换掉调用元素的所有子节点。

2. outerHTML

在读模式下,outerHTML属性返回调用它的元素以及所有子节点的HTML标签。

在写模式下,innerHTML会根据指定的值建立新的DOM树,并完全替换掉调用元素。

3. insertAdjacentHTML

它接收两个参数:插入位置和要插入的文本,第一个参数是下列值之一:

  • beforebegin:当前元素前插入一个紧邻同辈元素;
  • afterbegin:在第一个子元素之前再插入新的子元素;
  • beforeend:在最后一个子元素之后再插入新的子元素;
  • afterend:在当前元素之后插入一个紧邻的同辈元素。
    第二个参数为HTML字符串(与HTML用法相同);

4. 内存与性能问题

在删除带有事件处理对象的子树时,有可能导致内存问题。(查阅原文)

11.3.7 scrollIntoView

该方法可以在任意HTML元素上调用,通过滚动浏览器窗口或者容器元素,调用元素就会出现在视口中。

如果传入true,或者不传参数,那么窗口滚动后会让调用元素顶部与视口顶部尽可能齐平。

如果传入false,调用元素会尽可能的全部出现在视口内,有可能调用元素底部与浏览器底部齐平,但是顶部不一定齐平。

11.4 专有扩展

11.4.1 文档模式

文档模式决定了你可以使用哪个级别的CSS,可以使用JavaScript中哪些API,一起如何对待文档类型。

可以通过 document.documentMode 查看给定页面是什么文档模式,他会返回文档模式的版本号。(剩余内容查阅原文)

11.4.2 children属性

该属性是HTMLCollection的实例,只包含元素中同样还是元素的子节点。

11.4.3 contains()方法

该方法接收一个参数,即要检测的后代节点。(剩余内容查阅原文)

11.4.4 插入文本

innerText 和 outerText(不常用,查阅原文)。

11.4.5 滚动

  • scrollIntoViewIfNeeded(alignCenter):只在当前元素不可见的情况下则滚动浏览器或视窗,最终让他可见,如果当前元素已可见,则不做任何操作。传入参数如果为true则尽量将元素显示在(垂直方向)中部。
  • scrollByLines(linecount):将元素滚动指定行高,可以是正值可以是负值。
  • scrollByPages(pageCount):将元素滚动指定页面高度,具体高度由元素高度决定。

scrollIntoView() 是唯一一个浏览器都支持的方法,所以使用该方法较为稳妥。

第12章 DOM2和DOM3

DOM1级主要定义了HTML和XML文档的底层结构。DOM2和DOM3则在此结构的基础上引入了更多的交互能力,也支持了更高级的XML特性。为此DOM2和DOM3分为许多模块,分别描述了DOM的某个非常具体的子集,模块如下:

  • DOM2级核心(DOM Level 2 Core):在1级核心基础上构建,为节点添加了更多方法和属性;
  • DOM2级视图(DOM Level 2 Views):为文档定义了基于样式信息的不同视图。
  • DOM2级事件(DOM Level 2 Events):说明了如何使用事件与DOM文档交互。
  • DOM2级样式(DOM Level 2 Style):定义了如何以编程的形式来访问和改变CSS样式信息。
  • DOM2级遍历和范围(DOM Level 2 Traversal and Range):引入了遍历DOM文档和选择其特定部分的新接口。
  • DOM2级HTML(DOM Level 2 HTML):在1级HTML基础上构建,添加了更多属性、方法和属性;

本章介绍除“DOM 2级事件以外的所有内容”。

12.1 DOM变化

12.1.1 针对XML命名空间的变化

第13章 事件

13.1 事件流

13.1.1 事件冒泡

ie的事件流为冒泡,即事件开始是由最具体的元素(嵌套层次最深的那个节点),然后再逐级上升到较为不具体的节点。


事件冒泡

所有现代浏览器都支持冒泡。

13.1.2 事件捕获

事件捕获的思想是不太具体的节点先接收到事件,然后捕获到具体的节点。


事件捕获

老版本浏览器不支持事件捕获,所以很少使用事件捕获。

13.1.3 DOM事件流

“DOM2级事件流”规定了事件流包括三个阶段,事件捕获阶段、处于目标阶段和事件冒泡阶段。

事件流

123为事件捕获阶段,4为处于目标阶段,567为事件冒泡阶段。案例如下:

// 捕获事件
html.addEventListener('click', function () {
  console.log('事件捕获 html');
}, true);

body.addEventListener('click', function () {
  console.log('事件捕获 body');
}, true);

div.addEventListener('click', function () {
  console.log('事件捕获 div');
}, true);

// 冒泡事件
html.addEventListener('click', function () {
  console.log('事件冒泡 html');
}, false);

body.addEventListener('click', function () {
  console.log('事件冒泡 body');
}, false);

div.addEventListener('click', function () {
  console.log('事件冒泡 div');
}, false);

点击div时,结果如下:

事件流测试

可以看出事件流的过程为捕获-->冒泡。

13.2 事件处理程序

事件是用户或浏览器执行的某项动作,而相应某个事件的函数就叫做事件处理程序。

13.2.1 HTML事件处理程序

即在HTML中指定事件处理程序,即:

<input type=”button” value=”click me” onclick=”showInfo()”/>

但是这样的做法有几个缺点:

  1. 用户有可能在html元素一出现页面上就触发相应的事件,而该事件处理函数尚未解析,那么就会引发错误。
  2. 这样扩展事件处理程序的作用域链在不同浏览器中会导致不同结果。不同JavaScript引擎遵守的标识符解析规则略有差异,很有可能在访问非限定对象成员出错。
  3. 就是HTML和JavaScript代码紧密耦合。

13.2.2 DOM0级事件处理程序

传统方式就是将一个函数赋值给一个事件处理程序的属性。

btn.onclick = function () {
  console.log('click');
}

13.2.3 DOM2级事件处理程序

DOM2级事件定义了一个方法,addEventListener() 和
removeEventListener(),该函数接收三个参数:

  • 要处理的事件名,
  • 事件处理程序
  • 一个boolean值(true表示在捕获阶段调用事件处理程序,false表示在冒泡阶段调用事件处理程序)

该方式可以添加多个事件处理程序,并按照代码顺序触发。

可以通过 removeEventListener() 移除事件处理程序,但是必须传入完全相同的参数,即不能使用匿名函数。

最好是将事件处理函数添加到冒泡阶段,可以兼容所有浏览器。

13.2.4 ie事件处理程序

IE中实现了两个方法:attachEvent() 和 detachEvent()。

这两个函数接收两个相同的参数:

  • 事件处理程序名称
  • 事件处理函数。

ie8及更早的浏览器只支持冒泡。

div.atttachEvent(‘onclick’, function(){});

注意是onclick而不是click。

添加多个事件处理时,事件会根据代码顺序反向执行。

13.2.5 跨浏览器的事件处理程序

第一个方法是addHandler,根据情况分别使用DOM0,DOM2或IE事件处理程序,接收三个参数:

  • 要操作的元素
  • 事件名称
  • 事件处理函数。
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;
    }
  }
};

使用案例:

var btn = document.getElementById('btn');
var handler = function () {
  console.log('clicked');
};

EventUtil.addHandler(btn, 'click', handler);
// 省略其他代码
EventUtil.removeHandler(btn, 'click', handler);

13.3 事件对象

触发DOM事件时,会产生一个事件对象event,该对象包含与该事件有关的信息。

13.3.1 DOM中的事件对象

事件对象完整的属性方法查阅原书。

事件处理内部,this 的值永远等于 currentTarget 的值,而 target 则只包含事件的实际目标。

如果将事件处理程序指定给子元素,则this,currentTarget,target包含相同的值。

重点:当把事件程序注册到父元素的时候,this 和
currentTarget 的值都是父元素,而 target 的值是事件真正的目标。

13.3.2 ie中的事件对象(查阅原书)

13.3.3 跨浏览器的事件对象

var EventUtil = {
  addHandler: function (element, type, handler) {
    // 省略,见上文
  },
  removeHandler: 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;
    }
  },
  stopPropagation: function (event) {
    if (event.stopPropagation) {
      event.stopPropagation();
    } else {
      event.cancelBubble = true;
    }
  }
};

使用方法如下:

btn.onclick = function (event) {
  event = EventUtil.getEvent(event);
};

13.4 事件类型

13.4.1 UI事件

UI事件是指那些不一定与用户操作有关的事件。

1. load事件

当页面完全加载后(包括所有图像,JavaScript文件、CSS文件等外部资源),就会触发window的load事件。

2. unload事件

这个事件在文档被完全卸载后触发。

3. resize事件

当一个浏览器的窗口调整到一个新的高度或宽度,则触发resize事件。

4. scroll事件

虽然该事件是在window对象上发生的,但是表示的是页面中相应元素的变化。在文档滚动的时候触发。

13.4.2 焦点事件

focus 和 blur;

13.4.3 鼠标与滚轮事件

DOM3中定义了9个鼠标事件。

  • click:
  • dbclick:
  • mousedown:
  • mouseenter:
  • mouseleave:
  • mousemove:
  • mouseout:
  • mouseover:
  • mouseup:

只有在同一个元素上相继触发mousedown和mouseup,才会触发click事件。

1. 客户区坐标位置

位置信息保存在事件对象的 clientX 和 clientY 属性中。
ps:这些值不包括页面滚动的距离,所以这个位置并不表示鼠标在页面上的位置。

2. 页面坐标位置

事件对象的pageX和pageY属性,表示的鼠标光标在页面中的位置。

3. 屏幕坐标位置

事件对象的 screenX 和 screenY 属性,表示鼠标在屏幕中的位置。

4. 修改键

在按下鼠标时键盘上的某些键的状态也可以影响到索要采取的操作。这些修改键就是Shfit、Ctrl、Alt和Meta(Windows中是Windows键,苹果机中是Cmd键)。DOM规定了4个属性,表示修改键的状态:shiftKey、ctrlKey、altKey和metaKey。

ele.addEventListener('click', function (e) {
  var keys = [];
  if (e.shiftKey) {
    keys.push('shift');
  }
...
  alert('keys:' + keys.join(','));
})

5. 相关元素

发生 mouseover 和 mouseout 事件时,会涉及相关元素,例如 mouseover,事件的主元素是获得鼠标的元素,而相关元素就是失去鼠标的元素,vice versa。

6. 鼠标按钮

在 mousedown 和 mouseup 事件来说,其 event 存在一个 button 属性,表示按下或释放的按钮。

7. 更多事件信息

8. 鼠标滚轮事件

当用户通过鼠标滚轮进行页面交互的时候,就会触发
mousewheel 事件。
其事件对象中有 wheelDelta 属性,向前滚动,wheelDelta 是120的倍数,向后滚动的时候是-120的倍数。

9. 触摸设备

  • 不支持dbclick事件;
  • 单击元素会触发mousemove事件,如果导致内容变化,则不再有其他事件发生;如果没有变化,则依次发生mousedown、mouseup和click事件。

10. 无障碍性问题

13.4.4 键盘与文本事件

有三个键盘事件:

  • keydown:按下任意键时触发,如果按住不放会重复触发该事件。
  • keypress:按下字符键时触发,如果按住不放会重复触发该事件。按下Esc也会触发该事件。
  • keyup:当用户释放键盘上的键时触发。

所有元素都支持该事件,但是 input 文本框最常使用。

1. 编码

event 对象中的 keyCode 属性。有键盘键码和数字字母键码。具体查阅原书。

2. 字符编码

按下能够插入或删除字符的键都会触发 keypress 事件。
event 对象有一个 charCode 属性,该属性只有在发生keypress 事件时才有值,该值是按下的键所代表的ASCII编码。

3. DOM3变化

不再包含 charCode 属性,包含两个新属性:key 和
char。
key 是取代 keyCode,它是一个和字符串,按下非字符键时,是相应键的名字(“shift”);按下字符键时时相应的文本字符。
char 在按下字符键时和 key 一样,如果是非字符键则为null。

因为存在跨浏览器问题,不推荐使用key和char

4. textInput 事件

当用户在可编辑区域内输入字符时,就会触发该事件

该事件和 keypress 事件的不同之处:

  • 任何可以获取焦点的都可以触发 keypress 事件,而只有可编辑区域才可以触发textInput事件
  • textInput 只有用户按下能够输入实际字符的键时才会触发,而 keypress 事件则在按下那些能够影响文本显示的键时也会触发。

5. 设备中的键盘事件

13.4.5 复合事件

13.4.6 变动事件

1. 删除节点

使用removeChild()或replaceChild()从DOM中删除节点时,首先会触发DOMNodeRemoved事件:

  • 该事件的event.target是被删除的节点;
  • 而event.relatedNode包含着目标节点父节点的引用。

如果被移除的节点包含子节点,那么在所有的子节点以及这个被移除的节点上会相继触发DOMNodeRemovedFromDocument事件。该事件不会冒泡:

  • 该事件的event.target是相应的子节点或被移除的节点。

紧随其后的是DOMSubtreeModified事件:

  • 该事件的event.target是被移除节点的父节点。

2. 插入节点

使用appendChild()、replaceChild()或insertBefore()向DOM中插入节点的时候,首先会触发DOMNodeInserted事件(该事件冒泡):

  • 该事件的event.target是被插入的节点;
  • event.relatedNode属性包含对父节点的引用。

紧接着会在新插入的节点上触发DOMNodeInsertedIntoDocument事件,该事件不冒泡,必须在插入节点之前为它添加这个事件处理程序:

  • 该事件的event.target是被插入的节点。

最后一个触发的事件是DOMSubtreeModified,触发与新插入节点的父节点。

13.4.7 HTML5事件

本节讨论浏览器完善支持的事件。

1. contextmenu事件

上下文菜单,在pc中是鼠标右键,调出上下文菜单项。
该事件是冒泡的,所以可以在document指定一个事件处理程序,来处理页面上所有的此类事件,例如自定义上下文菜单项:

var menu = document.getElementById('MyMenu');
menu.style.left = event.clienX + 'px';
menu.style.top = event.clienY + 'px';
menu.style.visibility = 'visible';

2. beforeunload事件

这个事件在页面卸载之前触发,可以通过它来取消卸载并继续使用原页面。但是不能彻底取消该事件,因为那就相当于用户无法离开当前页面了。如下:
window.addEventListener('beforeunload', function (e) {
e.returnValue = '确定离开当前页面吗?'
});

3. DOMContentLoaded事件

该事件在形成完整的DOM树之后就会触发,不理会图像、js文件、css文件或其他资源是否已经加载完毕。
对于不支持的浏览器,建议设置一个时间为0毫秒的超时调用。

4. readystatechange事件

该事件提供与文档或元素的加载状态有关的信息。支持该事件的每个对象都有一个readyState属性,可能包含下列5个值中的一个:

  • uninitialized(未初始化):对象存在但未初始化;
  • loading(正在加载):对象正在加载数据;
  • loaded(加载完毕):对象加载数据完成;
  • interactive(交互):可操作对象了,但还没有完全加载;
  • complete(完成):对象已经加载完成。

该事件与load事件一起使用的时候,无法预测两个事件触发的先后顺序,而且更复杂的是交互阶段有可能晚于完成阶段,所以,有必要同时检测交互阶段和完成阶段:

EventUtil.addHandler(document, 'readystatechange', function (event) {
  if (document.readyState == 'interactive' || document.readyState == 'complete') {
    EventUtil.removeHandler(document, 'readystatechange', arguments.callee);
    console.log('Content loaded');
  }
});

5. pageshow和pagehide事件

FireFox和Opera有个特性叫“往返缓存”(back-forward cache, 或者bfcache),可以让用户使用后退或前进键加快页面跳转速度。
第一个事件是pageshow:

  • 在重新加载页面的时候,这个事件在load事件触发后触发;
  • 而对于bfcache中的页面,将会在页面状态完全恢复的时候触发。
    虽然事件的目标是document,但是需要将事件处理程序绑定到window对象上。
(function () {
  var showCount = 0;

  EventUtil.addHandler(window, 'load', function () {
    console.log('load fired');
  });

  EventUtil.addHandler(window, 'pageshow', function () {
    showCount++;
    console.log('show has been fired' + showCount + 'times.');
  });
})();

上述例子中,当后退回该页面的时候,计数会增加。

除了常用的属性外,pageshow事件的event对象还包含一个叫persisted布尔值属性。如果页面保存在bfcache中,则这个属性的值为true;

6. hashchange事件

H5新增了hashchange事件,以便在url的参数列表(url中#后面的所有字符串)发生变化的时候通知开发人员。

必须要把hashchange事件处理程序添加给window对象。然后url参数只要变化就会被调用。此时的event对象应该额外包含两个属性:oldUrl和newUrl。

13.4.8 设备事件

1. orientationchange事件

safari浏览器中添加了该事件。移动safari的window.orientation属性中可能包含3个值:0表示肖像模式,90表示向左旋转的横向模式,-90表示向右旋转的横向模式。180是指的头朝下,但是该模式尚未支持。

只要用户改变了设备的查看模式,就会触发orientationchange事件。

2. MozOrientation事件

检测设备方向的事件。只有带加速针的设备才支持该事件。

3. deviceorientation事件

该事件的意图是告诉开发人员设备在空间中朝向哪儿。而不是如何移动。

4. devicemotion事件

该事件告诉开发人员设备什么时候移动,而不是设备方向如何改变。

13.4.9 触摸与手势事件

1. 触摸事件

触摸事件会在用户手指放在屏幕上、在屏幕上滑动时或从屏幕上移开时触发。有以下几种触摸事件:

  • touchstart:手指触碰屏幕时触发;即时已经有一个手指放在屏幕上也会触发
  • touchmove:当手指在屏幕上滑动的时候触发。在该事件发生期间,调用preventDefault() 方法可以组织滚动
  • touchend:当手指在屏幕上移开时触发
  • touchcancel:当系统停止跟踪触摸时触发。

这几个事件都会冒泡,也都可以取消。除了常见的 DOM 属性以外,还添加了三个用于追踪触摸的属性:

  • touches:表示当前跟踪的触摸操作的 Touch 对象的数组
  • targetTouches:特定于事件目标的 Touch 对象的数组
  • changeTouches:表示自上次触摸以来发生了什么改变的 Touch 对象的数组

每个Touch对象包含下列属性:

  • clientX:触摸目标在视口中的x坐标
  • clientY:触摸目标在视口中的y坐标
  • identitifier:标识触摸的唯一ID
  • pageX:触摸目标在页面中的x坐标
  • pageY:触摸目标在页面中的y坐标
  • screenX:触摸目标在屏幕中的x坐标
  • screenY:触摸目标在屏幕中的y坐标
  • target:触摸的 DOM 节点目标

在touchend事件发生的时候,touched集合中就没有任何Touch对象了,因为不存在活动着的触摸操作,因此此时就应该转而使用changetouches。
这些事件在所有元素上都会触发,因此可以分别操作页面的不同部分。在触摸屏幕上时,这些事件(包括鼠标)的发生顺序是:

  1. touchstart
  2. mouseover
  3. mousemove
  4. mousedown
  5. mouseup
  6. click
  7. touchend

2. 手势事件

Safari中引入了一组手势事件。当两个手指触摸屏幕的时候就会产生手势,手势通常会改变显示项的大小或者旋转。有三个手势事件:

  • gesturestart:当一个手指已经按在屏幕上而另一个手指又触摸屏幕时触发
  • gesturechange:当触摸屏幕的任何一个手指的位置发生变化时触发
  • gestureend:当任何一个手指从屏幕上移开时触发

每个手势事件对象event都包含着标准的鼠标事件属性。此外还包含两个额外的属性:rotation,表示手指变化引起的旋转角度,负值表示逆时针旋转,正值表示顺时针;scale,表示手指尖距离的变化,从1开始,随着距离增加而增大。

13.5 内存和性能

13.4.1 事件委托

利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的事件。而不用给每个元素绑定事件

给下列元素的每个 li 绑定事件:

<ul id="myLinks">
  <li id="link1">link1</li>
  <li id="link2">link2</li>
  <li id="sayHi">sayHi</li>
</ul>

可以给最高层次的元素添加一个事件处理程序:

var list = document.getElementById('myLinks');
EventUtil.addHandler(list, 'click', function () {
  // 通过取到的id进行分别处理
});

如果可行的话在document对象上添加事件处理程序,用以处理页面上发生的某种特定类型的事件:

  • document 对象很快就可以访问,而且可以在页面任何阶段进行添加事件处理程序(不用等DOMContentLoaded 或 load 事件)。
  • 在页面中设置事件处理程序所需的时间更少,只添加一个事件处理程序所需的 DOM 引用更少,所花的时间也更少
  • 整个页面占用的内存空间更少,能够提升整体性能。

最适合委托事件的是:click、mousedown、mouseup、keydown、keyup和keypress。

13.5.2 移除事件处理程序

内存中留有过时不用的“空事件处理程序”,也是造成Web应用程序内存与性能问题的主要原因。

  1. 当页面移除带有事件处理程序的元素时。如果你知道某个元素即将被移除,那么最好手动移除事件处理程序。
  2. 卸载页面的时候,如果页面卸载之前没有清理干净事件处理程序,那它们就会永远停留在内存中了。一般的做法,通过onunload移除所有的事件处理程序。

13.6 模拟事件(查阅原书)

第14章 表单脚本

// 省略

第22章 高级技巧

22.1 高级函数

22.1.1 安全的类型检测

任何值上面调用 toString 方法,都会返回一个[object NativeConstructorName]格式的字符串。每个类在内部都有一个属性[[Class]]属性,这个属性就指定了上述字符串中的构造函数名,例:

Object.prototype.toString.call(value);  //[object Array]

由于原生数组的构造函数与全局作用域无关,因此可以使用 toString 保证返回值的统一。利用这一点可以创建检测数组、方法以及正则表达式的方法:

/**
 * 检测一个值是不是数组
 * @param value 传入的值
 * @returns {boolean}
 */
function isArray(value) {
  return Object.prototype.toString.call(value) == '[object Array]';
}

/**
 * 检测一个值是不是函数
 * @param value 传入的值
 * @returns {boolean}
 */
function isFunction(value) {
  return Object.prototype.toString.call(value) == '[object Function]';
}

/**
 * 检测一个值是不是正则表达式
 * @param value 传入的值
 * @returns {boolean}
 */
function isRegExp(value) {
  return Object.prototype.toString.call(value) == '[object RegExp]';
}

22.1.2 作用域安全的构造函数

当构造函数模式中的构造函数:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

在上述构造函数没使用 new 关键字调用时,this 会映射到全局变量 window 上,导致错误对象属性的增加。

解决该问题的办法是创建一个作用域安全的构造函数

作用域安全的构造函数在任何更改前,首先确定 this 对象是否是正确的类型如果不是,则创建新的实例并返回,避免了在全局变量上意外设置属性。

function Person(name, age) {
  if (this instanceof Person) {
    this.name = name;
    this.age = age;
  } else {
    return new Person(name, age);
  }
}

var person1 = Person('rick', 23);
console.log(window.name); // ""
console.log(person1.name);  //"rick"

var person2 = new Person('morty', 13);
console.log(person2.name);  //"morty"

上述模式锁定了执行构造函数的环境,如果使用借用构造函数模式实现继承而且不使用原型链,继承将会被破坏。代码如下:

// 父类
function Super(value) {
  if (this instanceof Super) {
    this.value = value;
  } else {
    return new Super(value);
  }
}

// 子类继承父类
function Sub() {
  Super.call(this, 'super');    // 借用构造函数的继承
}

var sub = new Sub();
console.log(sub.value);     //undefined

在子类构造方法中借用Super构造函数实现继承,但是由于Super是作用域安全的构造函数,在检测到调用Super构造函数的对象this的类型是Sub时,变回新建一个Super,并返回。因此Sub类型的this对象属性没有得到增长,同时也没用到Super的返回值,所以value属性的值为undefined。

而借用构造函数结合原型链或者寄生组合,便可以解决该方法。例子:

//...Super和Sub的构造函数如上
Sub.prototype = new Super();

var sub = new Sub();
console.log(sub.value);     // super

多个程序员在同一个页面上写javascript,作用域安全就很重要了,对全局对象的更改会导致一些难以追踪的问题。

22.1.3 惰性载入函数

因为浏览器之间有的差异,所以会包含大量的if代码,引导到正确的分支代码中:

function addEvent(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;
  }
}  

但只要第一次判断完支持性后,它就会一直支持了,这种测试也就没有存在的必要了,如果if语句不必每次都执行,那么代码可以更快的运行。

解决方案是惰性载入函数。惰性载入函数指的是函数执行的分支仅会发生一次,有两种实现惰性载入的方式:
第一种是在函数被调用的时候再处理函数。

function addEvent(type, element, handler) {
  if (element.addEventListener) {
    addEvent = function (type, element, handler) {
      element.addEventListener(type, handler, false);
    }
  }
  else if (element.attachEvent) {
    addEvent = function (type, element, handler) {
      element.attachEvent('on' + type, handler);
    }
  }
  else {
    addEvent = function (type, element, handler) {
      element['on' + type] = handler;
    }
  }
  return addEvent(type, element, handler);
}  

第一次调用函数的时候,if的每个分支都会给函数进行赋值,覆盖了原函数,下一次执行的方法的时候,直接执行被分配的函数,而不需要再一次执行if语句。

第二种方法是声明函数的时候就指定适当的函数,这样第一次调用就不会损失性能了,但是首次加载的时候会损失一点性能。

var addEvent = (function () {  
    if (document.addEventListener) {  
        return function (type, element, fun) {  
            element.addEventListener(type, fun, false);  
        }  
    }  
    else if (document.attachEvent) {  
        return function (type, element, fun) {  
            element.attachEvent('on' + type, fun);  
        }  
    }  
    else {  
        return function (type, element, fun) {  
            element['on' + type] = fun;  
        }  
    }  
})();  

22.1.4 函数绑定

绑定函数是一个可以将函数绑定到指定环境的函数,一般如下:

function bind(fn, context) {
  return function () {
    return fn.apply(context, arguments);
  }
}

ES5定义了原生的 bind() 方法。

22.1.5 函数柯里化

函数柯里化用于创建已经设置好了一个或多个参数的函数。当函数被调用的时候,返回的函数还需要设置一些传入的参数。一个简单的例子如下:

柯里化一般由以下步骤完成:调用另外一个函数并且为他传入要柯里化的函数和必要参数,下面是通用方法:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容