《JavaScript高级程序设计》读书笔记

Javascript由以下三部分组成:

  1. 核心(ECMAScript)

  2. 文档对象模型(DOM)

  3. 浏览器对象模型(BOM)

ECMAScript组成部分:
语法、类型、语句、关键字、保留子、操作符、对象。

按照惯例,外部 JavaScript 文件带有.js 扩展名。但这个扩展名不是必需的,因为 浏览器不会检查包含 JavaScript 的文件的扩展名。这样一来,使用 JSP、PHP 或其他 服务器端语言动态生成 JavaScript 代码也就成为了可能。但是,服务器通常还是需要 看扩展名决定为响应应用哪种 MIME 类型。如果不使用.js 扩展名,请确保服务器能 返回正确的 MIME 类型。 

无论如何包含代码,只要不存在 defer 和 async 属性,浏览器都会按照<script>元素在页面中出现的先后顺序对它们依次进行解析。

ECMAScript 5 引入了严格模式(strict mode)的概念:"use strict”;严格模式下,JavaScript 的执行结果会有很大不同。

控制语句中使用代码块({...})——即使代码块中只有一条语句。

ECMAScript 中有 5 种简单数据类型(也称为基本数据类型):
Undefined、Null、Boolean、Number 和 String。
还有 1 种复杂数据类型——Object,
Object 本质上是由一组无序的名值对组成的。

字面值 undefined 的主要目的是用于比较。

对于尚未声明过的变量,只能执行一项操作,即使用 typeof 操作符检测其数据类型。

即便未初始化的变量会自动被赋予 undefined 值,但显式地初始化变量依然是明智的选择。

从逻辑角度来看,null 值表示一个空对象指针,而这也正是使用 typeof 操作符检测 null 值时会返回"object”的原因。

如果定义的变量准备在将来用于保存对象,那么最好将该变量初始化为 null 而不是其他值。这样 一来,只要直接检查 null 值就可以知道相应的变量是否已经保存了一个对象的引用。

实际上,undefined 值是派生自 null 值的:
alert(null == undefined);    //true
alert(null === undefined);    //false
相等操作符(==)出于比较的目的会转换其操作数

只要意在保存对象的变量还没有真正保存对象,就应该明确地让该变量保存 null 值。这样做不仅可以 体现 null 作为空对象指针的惯例,而且也有助于进一步区分 null 和 undefined。

八进制字面量在严格模式下是无效的,会导致支持的 JavaScript 引擎抛出错误。

在默认情况下,ECMASctipt 会将那些小数点后面带有 6 个零以上的浮点数值转换为以 e 表示法 表示的数值(例如,0.0000003 会被转换成 3e7)。

浮点数值的最高精度是 17 位小数,但在进行算术计算时其精确度远远不如整数。例如,0.1 加 0.2 的结果不是 0.3,而是 0.30000000000000004。因此,永远不要测试某个特定的浮点数值
关于浮点数值计算会产生舍入误差的问题,有一点需要明确:这是使用基于 IEEE754 数值的浮点计算的通病,ECMAScript 并非独此一家;其他使用相同数值格 式的语言也存在这个问题。

使用 isFinite()函数判断一个数值是不是有穷的(是不是位于最小[-Infinity]和最大[Infinity]的数值之间)。

访问 Number.NEGATIVE_INFINITYNumber.POSITIVE_INFINITY 也可以 得到负和正 Infinity 的值。
可以想见,这两个属性中分别保存着-Infinity 和 Infinity。

NaN 本身有两个非同寻常的特点。
首先,
任何涉及 NaN 的操作(例如 NaN/10)都会返回 NaN,这 个特点在多步计算中有可能导致问题。
其次,NaN 与任何值都不相等,包括 NaN 本身。例如,下面的代 码会返回 false:
alert(NaN == NaN); //false
针对 NaN 的这两个特点,ECMAScript 定义了 isNaN()函数。

只有 0 除以 0 才会返回 NaN,正数除以 0 返回 Infinity,负数除以 0 返回-Infinity。

尽管有点儿不可思议,但 isNaN()确实也适用于对象。在基于对象调用 isNaN() 函数时,会首先调用对象的 valueOf()方法,然后确定该方法返回的值是否可以转 换为数值。如果不能,则基于这个返回值再调用 toString()方法,再测试返回值。 而这个过程也是 ECMAScript 中内置函数和操作符的一般执行流程。

有 3 个函数可以把非数值转换为数值:Number()、parseInt()和 parseFloat()。
一元加操作符的操作与 Number()函数相同。

由于 parseFloat()只解析十进制值,因此它没有用第二个参数指定基 数的用法。最后还要注意一点:如果字符串包含的是一个可解析为整数的数(没有小数点,或者小数点后 都是零),parseFloat()会返回整数。

var lang = "Java”;
lang = lang + "Script";

变量 lang 开始时包含字符串"Java"。而第二行代码把 lang 的值重新定义为"Java" 与"Script"的组合,即"JavaScript"。实现这个操作的过程如下:首先创建一个能容纳 10 个字符的 新字符串,然后在这个字符串中填充"Java"和"Script",最后一步是销毁原来的字符串"Java"和字符串"Script",因为这两个字符串已经没用了。这个过程是在后台发生的,而这也是在某些旧版本的浏览器(例如版本低于 1.0 的 Firefox、IE6 等)中拼接字符串时速度很慢的原因所在。但这些浏览器后 来的版本已经解决了这个低效率问题。

要把一个值转换为一个字符串有两种方式。第一种是使用几乎每个值都有的 toString()方法。数值、布尔值、对象和字符串值(没错,每个字符串也都有一个 toString()方法,该方法返回字符串的一个副本)都有 toString()方法。
null 和 undefined 值没有这个方法,因为它们没有对应的包装器类,从而没有属性和方法。

使用转型函数 String(),这个 函数能够将任何类型的值转换为字符串。String()函数遵循下列转换规则:

  1. 如果值有 toString()方法,则调用该方法(没有参数)并返回相应的结果;

  2. 如果值是 null,则返回"null”;

  3. 如果值是 undefined,则返回"undefined"。

ECMAScript 中的对象其实就是一组数据和功能的集合
对象可以通过执行 new 操作符后跟要创建 的对象类型的名称来创建。

在 ECMAScript 中,如果不给构造函数传递参数,则可 以省略后面的那一对圆括号。

var o = new Object; // 有效,但不推荐省略圆括号

Object 的每个实例都具有下列属性和方法:

  1. constructor:保存着用于创建当前对象的函数。

  2. hasOwnProperty(propertyName):用于检查给定的属性在当前对象实例中(而不是在实例的原型中)是否存在。其中,作为参数的属性名(propertyName)必须以字符串形式指定。

  3. isPrototypeOf(object):用于检查传入的对象是否是传入对象的原型。

  4.  propertyIsEnumerable(propertyName):用于检查给定的属性是否能够使用 for-in 语句。

  5.  toLocaleString():返回对象的字符串表示,该字符串与执行环境的地区对应。

  6.  toString():返回对象的字符串表示。

  7. valueOf():返回对象的字符串、数值或布尔值表示。通常与 toString()方法的返回值相同。

由于在 ECMAScript 中 Object 是所有对象的基础,因此所有对象都具有这些基本的属性和方法。

在对非数值应用一元加操作符时,该操作符会像 Number()转型函数一样对这个值执行转换。
一元减操作符遵循与一元加操作符相同的规则,最后再将得到的数值转换为负数。

ECMAScript 中的所有数 值都以 IEEE-754 64 位格式存储,但位操作符并不直接操作 64 位的值。而是先将 64 位的值转换成 32 位 的整数,然后执行操作,最后再将结果转换回 64 位。

计算一个数值的二进制补码,需要经过下列 3 个步骤:

  1. 求这个数值绝对值的二进制码(例如,要求-18 的二进制补码,先求 18 的二进制码);

  2. 求二进制反码,即将 0 替换为 1,将 1 替换为 0;

  3. 得到的二进制反码加 1。

如果对非数值应用位操作符,会先使用 Number()函数将该值转换为一个数值(自动完成),然后 再应用位操作。得到的结果将是一个数值。

按位非操作的本质:操作数的负值减 1。
例如:~25 == -25-1。
由于按位非是在数值表示的最底层执行操作,因此速度更快。

注意,左移不会影响操作数的符号位。

有符号的右移操作符由两个大于号(>>)表示,这个操作符会将数值向右移动,但保留符号位(即正负号标记)。
无符号右移操作符由 3 个大于号(>>>)表示,这个操作符会将数值的所有 32 位都向右移动。对正 数来说,无符号右移的结果与有符号右移相同。但是对负数来说,情况就不一样了。首先,无符号右移是以 0 来填充空位,而不是像有符号右移那 样以符号位的值来填充空位。

逻辑非操作符也可以用于将一个值转换为与其对应的布尔值。
使用
两个逻辑非操作符,实际 上就会模拟 Boolean()转型函数的行为
其中,第一个逻辑非操作会基于无论什么操作数返回一个布尔值,而第二个逻辑非操作则对该布尔值求反,于是就得到了这个值真正对应的布尔值。
alert(!!"blue”);  //true
alert(!!NaN); //false

不能在逻辑与操作中使用未定义的值。

利用逻辑或的短路特性来避免为变量赋 null 或 undefined 值。
例如:var myObject = preferredObject || backupObject;

两组操作符:相等和不相等——先转换再比较,全等和不全等——仅比较而不转换

由于相等和不相等操作符存在类型转换问题,而为了保持代码中数据类型的完整性,我们推荐使用全等和不全等操作符。

for-in 语句是一种精准的迭代语句,可以用来枚举对象的属性(非原生属性)。

ECMAScript 对象的属性没有顺序。因此,通过 for-in 循环输出的属性名的顺序是不可预测的。 具体来讲,所有属性都会被返回一次,但返回的先后次序可能会因浏览器而异。在使用 for-in 循环之前,先检测确认该对象的值不是 null 或 undefined。

 

with(location){

    var qs = search.substring(1);

    var hostName = hostname;

    var url = href;

}在 with 语句的代码块 内部,每个变量首先被认为是一个局部变量,而如果在局部环境中找不到该变量的定义,就会查询 location 对象中是否有同名的属性。如果发现了同名属性,则以 location 对象属性的值作为变量的值。由于大量使用 with 语句会导致性能下降,同时也会给调试代码造成困难,因此在开发大型应用程序时,不建议使用 with 语句。

switch 语句中使用任何数据类型(在很多其他语言中只能使用数值),无论是字符串,还是对象都没有 问题。其次,每个 case 的值不一定是常量,可以是变量,甚至是表达式

switch 语句在比较值时使用的是全等操作符,因此不会发生类型转换(例如,字符串"10"不等于数值 10)。

arguments 的值永远与对应命名参数的值保持同步。

function doAdd(num1, num2) {

    arguments[1] = 10;

    alert(arguments[0] + num2);

}
每次执行这个 doAdd()函数都会重写第二个参数,将第二个参数的值修改为 10。因为 arguments 对象中的值会自动反映到对应的命名参数,所以修改 arguments[1],也就修改了 num2,结果它们的 值都会变成 10。不过,这并不是说读取这两个值会访问相同的内存空间;它们的内存空间是独立的,但它们的值会同步。另外还要记住,如果只传入了一个参数,那么为 arguments[1]设置的值不会反应到命名参数中。这是因为 arguments 对象的长度是由传入的参数个数决定的,不是由定义函数时的命名 参数的个数决定的。

严格模式对如何使用 arguments 对象做出了一些限制。首先,像前面例子中那样的赋值会变得无效。也就是说,即使把 arguments[1]设置为 10,num2 的值仍然还是 undefined。其次,重写 arguments 的值会导致语法错误(代码将不会执行)。

ECMAScript 变量可能包含两种不同数据类型的值: 基本类型值引用类型值
基本类型值指的是 简单的数据段,而引用类型值指那些可能由多个值构成的对象。

如果从一个变量向另一个变量复制基本类型的值,会在变量对象上创建一个新值,然后把该值复制 到为新变量分配的位置上。
当从一个变量向另一个变量复制引用类型的值时,同样也会将存储在变量对象中的值复制一份放到 为新变量分配的空间中。不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一 个对象。复制操作结束后,两个变量实际上将引用同一个对象。因此,改变其中一个变量,就会影响另 一个变量。

ECMAScript 中所有函数的参数都是按值传递。也就是说,把函数外部的值复制给函数内部的参 数,就和把值从一个变量复制到另一个变量一样。基本类型值的传递同基本类型变量的复制一样,而 引用类型值的传递,则如同引用类型变量的复制一样。有不少开发人员在这一点上可能会感到困惑,因 为访问变量有按值和按引用两种方式,而参数只能按值传递。在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量(即命名参数,或者用 ECMAScript 的概念来说,就是 arguments 对象中的一个元素)。在向参数传递引用类型的值时,会把 这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部。

function setName(obj) { 
    obj.name = "Nicholas"; 
    obj = new Object(); 
    obj.name = "Greg";

}

var person = new Object();

setName(person);

alert(person.name);    //"Nicholas"

如果 person 是按引用传递的,那么 person 就会自动被修改为指向其 name 属性值 为"Greg"的新对象。但是,当接下来再访问 person.name 时,显示的值仍然是"Nicholas"。这说明 即使在函数内部修改了参数的值,但原始的引用仍然保持未变。实际上,当在函数内部重写 obj 时,这 个变量引用的就是一个局部对象了。而这个局部对象会在函数执行完毕后立即被销毁。

 

可以把 ECMAScript 函数的参数想象成局部变量

 

确定一个值是哪种基本类型可以使用 typeof 操作符,而确定一个值是哪种引用类型可以使用 instanceof 操作符。

 

JavaScript 没有块级作用域

 

垃圾收集机制的原理其实很简单:找出那些不再继续使用的变 量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间), 周期性地执行这一操作。

两个策略:

  1. JavaScript 中最常用的垃圾收集方式是标记清除(mark-and-sweep)。主流浏览器都是标记清除式的 垃圾收集策略(或类似的策略),只不过垃圾收集的时间间隔互有不同。

  2. 另一种不太常见的垃圾收集策略叫做引用计数(reference counting)。例如Objective-C。

将变量设置为 null 意味着切断变量与它此前引用的值之间的连接。

分配给 Web 浏览器的可用内存数量通常要比分配给桌面应用程序的少。这样做的目的主要是出于安全方面的考虑, 目的是防止运行 JavaScript 的网页耗尽全部系统内存而导致系统崩溃。内存限制问题不仅会影响给变量 分配内存,同时还会影响调用栈以及在一个线程中能够同时执行的语句数量。

一旦数据不再有用,最好通过将其值设置为 null 来释放其引用——这个做法叫做解除引用(dereferencing)。不过,解除一个值的引用并不意味着自动回收该值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。

 所有变量(包括基本类型和引用类型)都存在于一个执行环境(也称为作用域)当中,这个执行环境决定了变量的生命周期,以及哪一部分代码可以访问其中的变量。以下是关于执行环境的几 点总结:

  1. 执行环境有全局执行环境(也称为全局环境)和函数执行环境之分;

  2. 每次进入一个新执行环境,都会创建一个用于搜索变量和函数的作用域链;

  3. 函数的局部环境不仅有权访问函数作用域中的变量,而且有权访问其包含(父)环境,乃至全局环境;

  4. 全局环境只能访问在全局环境中定义的变量和函数,而不能直接访问局部环境中的任何数据;

  5. 变量的执行环境有助于确定应该何时释放内存。

 

对象字面量也是向函数传递大量可选参数的首选方式。

 

JavaScript访问对象属性:

  1. 点表示法;

  2. 方括号法。主要优点是可以通过变量来访问属性,

    例如:var propertyName = "name";alert(person[propertyName]); //"Nicholas"

    如果属性名中包含会导致语法错误的字符,或者属性名使用的是关键字或保留字,也可以使用方括 号表示法。通常,除非必须使用变量来访问属性,否则我们建议使用点表示法。

 

在使用 Array 构造函数时也可以省略 new 操作符。

var colors = Array(3); // 创建一个包含 3 项的数组

与对象一样,在使用数组字面量表示法时,也不会调用 Array 构造函数。

 

数组的 length 属性很有特点——它不是只读的。
因此,通过设置这个属性,可以从数组的末尾移除项或向数组中添加新项。请看下面的例子:

var colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组

colors.length = 2;

alert(colors[2]); //undefined

如果将其 length 属性设置为大于数组 项数的值,则新增的每一项都会取得 undefined 值,如下所示:

colors.length = 4;

alert(colors[3]); //undefined

利用 length 属性也可以方便地在数组末尾添加新项,如下所示:

var colors = ["red", "blue", "green"];// 创建一个包含 3 个字符串的数组

colors[colors.length= "black”;  //(在位置3)添加一种颜色

colors[colors.length] = "brown”; //(在位置4)再添加一种颜色

 

sort()方法会调用每个数组项的 toString()转型方法,然后比较得到的字符串,以 确定如何排序。即使数组中的每一项都是数值,sort()方法比较的也是字符串。

var values = [0, 1, 5, 10, 15];

values.sort();

alert(values);     //0,1,10,15,5

 

正则表达式中的元字符包括: 11 ( [ { \ ^ $ | ) ? * + .]}

这些元字符在正则表达式中都有一或多种特殊用途,因此如果想要匹配字符串中包含的这些字符,

就必须对它们进行转义

 

由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。

 

最后一种定义函数的方式是使用 Function 构造函数。Function 构造函数可以接收任意数量的参数, 但最后一个参数始终都被看成是函数体,而前面的参数则枚举出了新函数的参数。

var sum = new Function("num1", "num2", "return num1 + num2"); // 不推荐

函数名想象为指针,也有助于理解为什么 ECMAScript 中没有函数重载的概念。

两个同名函数,而结果则是后面的函数覆盖了前面的函数。

 

解析器在向执行环境中加载数据时,对函数声明函数表达式并非一视同仁。
解析器会率
先读取函数声明,并使其在执行任何代码之前可用(可以访问);
至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。

在函数内部,有两个特殊的对象:arguments 和 this

虽然 arguments 的主要用途是保存函数参数, 但这个对象还有一个名叫 callee 的属性,该属性是一个指针,指向拥有这个 arguments 对象的函数。

function factorial(num){

    if (num <=1) {

        return 1;

    } else {

        return num * arguments.callee(num-1)

} }

 

this 引用的是函数据以执行的环境对象——或者也可以说是 this 值(当在网页的全局作用域中调用函数时, this 对象引用的就是 window)。

 

一定要牢记,函数的名字仅仅是一个包含指针的变量而已。因此,即使是在不同的环境中执行,全局的 sayColor()函数与 o.sayColor()指向的仍然是同一 个函数。

 

ECMAScript 5 也规范化了另一个函数对象的属性:caller。这个属性中保存着调用当前函数的函数的引用, 如果是在全局作用域中调用当前函数,它的值为 null

每个函数都包含两个属性:length 和 prototype
其中,length 属性表示函数希望接收的命名参数的个数, 

每个函数都包含两个非继承而来的方法:apply()和 call()

这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内 this 对象的值。

首先,apply()方法接收两个参数:
一个是在其中运行函数的作用域,另一个是参数数组。
其中,第二个参数可以是 Array 的实例,也可以是 arguments 对象。

 

call()方法与 apply()方法的作用相同,它们的区别仅在于接收参数的方式不同。对于 call() 方法而言,第一个参数是 this 值没有变化,变化的是其余参数都直接传递给函数。换句话说,在使用 call()方法时,传递给函数的参数必须逐个列举出来。

 

传递参数并非 apply()和 call()真正的用武之地;

它们真正强大的地方是能够扩充函数赖以运行的作用域

 

使用 call()(或 apply())来扩充作用域的最大好处,就是对象不需要与方法有任何耦合关系

每个函数继承的 toLocaleString()和 toString()方法始终都返回函数的代码。返回代码的格式则因浏览器而异。

 

每当读取一个基本类型值的时候,后台就会创建一个对应的基本包装类型的对象,从而让我们能够调用一些方法来操作这些数据。

引用类型与基本包装类型的主要区别就是对象的生存期。使用 new 操作符创建的引用类型的实例, 在执行流离开当前作用域之前都一直保存在内存中。而自动创建的基本包装类型的对象,则只存在于一 行代码的执行瞬间,然后立即被销毁。这意味着我们不能在运行时为基本类型值添加属性和方法。来看 下面的例子:

    var s1 = "some text";

    s1.color = "red";

    alert(s1.color);   //undefined

 

使用 new 调用基本包装类型的构造函数,与直接调用同名的转型函数是不一样的。

var value = "25";

var number = Number(value); //转型函数 

alert(typeof number); //“number"

 

var obj = new Number(value); //构造函数

alert(typeof obj); //"object"

 

基本类型与引用类型的布尔值还有两个区别。首先,typeof 操作符对基本类型返回"boolean", 而对引用类型返回"object"。其次,由于 Boolean 对象是 Boolean 类型的实例,所以使用 instanceof 操作符测试 Boolean 对象会返回 true,而测试基本类型的布尔值则返回 false。

 

ECMAScript 提供了三个基于子字符串创建新字符串的方法:slice()、substr()和 substring()。

 

encodeURI()主要用于整个 URI,而 encodeURIComponent()主要用于对 URI 中的某一段。

它们的主要区别在于,encodeURI()不会对本身属于 URI 的特殊字符进行编码,例如冒号、正斜杠、 问号和井字号;而 encodeURIComponent()则会对它发现的任何非标准字符进行编码。

一般来说,我们使用 encodeURIComponent()方法的时候要比使用 encodeURI()更多,因为在实践中更常见的是对查询字符串参数而不是对基础 URI 进行编码。

与 encodeURI()和 encodeURIComponent()方法对应的两个方法分别是 decodeURI()和 decodeURIComponent()。其中,decodeURI()只能对使用 encodeURI()替换的字符进行解码。

decodeURIComponent()能够解码使用 encodeURIComponent()编码的所有字符,即它可以解码任何特殊字符的编码。

 

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

 

能够解释代码字符串的能力非常强大,但也非常危险。因此在使用 eval()时必 须极为谨慎,特别是在用它执行用户输入数据的情况下。否则,可能会有恶意用户输 入威胁你的站点或应用程序安全的代码(即所谓的代码注入)。

 

舍入为整数的几个方法:Math.ceil()、Math.floor()和 Math.round()。 这三个方法分别遵循下列舍入规则:

  1. Math.ceil()执行向上舍入,即它总是将数值向上舍入为最接近的整数;

  2. Math.floor()执行向下舍入,即它总是将数值向下舍入为最接近的整数;

  3. Math.round()执行标准舍入,即它总是将数值四舍五入为最接近的整数。

 利用 Math.random() 从某个整数范围内随机选择一个值。
值 = Math.floor(Math.random() * 可能值的总数 + 第一个可能的值)
举例来说,如果你想选择一个 1 到 10 之间的数值,可以像下面这样编写代码:
var num = Math.floor(Math.random() * 10 + 1);
或 num = Math.random() * 10 + 1|0;

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

我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象, 而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

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

 无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor (构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。

当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部 属性),指向构造函数的原型对象。ECMA-262 第 5 版中管这个指针叫[[Prototype]]。虽然在脚本中 没有标准的方式访问[[Prototype]],但 Firefox、Safari 和 Chrome 在每个对象上都支持一个属性 __proto__;而在其他实现中,这个属性对脚本则是完全不可见的。不过,要明确的真正重要的一点就 是,这个连接存在于实例构造函数的原型对象之间,而不是存在于实例与构造函数之间。

使用 delete 操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性。

使用 hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。

有两种方式使用 in 操作符单独使用和在 for-in 循环中使用。
在单独使用时,in 操作符会在通过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中。

要取得对象上所有可枚举的实例属性,可以使用 ECMAScript 5 的 Object.keys()方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。

当使用字面量来写原型对象时constructor 属性默认指向Object,所以要重置。

调用构造函数时会为实例添加一个指向最初原型的 [[Prototype]]指针,
把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。
请记住:实例中的指针仅指向原型,而不指向构造函数。

不推荐在产品化的程序中修改原生对象的原型。如果因 某个实现中缺少某个方法,就在原生对象的原型中添加这个方法,那么当在另一个支 持该方法的实现中运行代码时,就可能会导致命名冲突。而且,这样做也可能会意外 地重写原生方法。

构造函数、原型和实例的关系:
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

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

在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做就会重写原型链。

组合继承:

指的是将原型链和借用构造函数的 技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方 法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数 复用,又能够保证每个实例都有它自己的属性。

 

寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背 后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型 原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型 的原型。

寄生组合式继承的基本模式如下所示。

function inheritPrototype(subType, superType){

    var prototype = object(superType.prototype);//创建对象

    prototype.constructor = subType;//增强对象

    subType.prototype = prototype;//指定对象

}

YUI 的 YAHOO.lang.extend()方法采用了寄生组合继承,从而让这种模式首次 出现在了一个应用非常广泛的 JavaScript 库中。

 

关于函数声明,它的一个重要特征就是函数声明提升(function declaration hoisting),意思是在执行代码之前会先读取函数声明。这就意味着可以把函数声明放在调用它的语句后面。

 

在编写递归函数时,使用 arguments.callee 总比使用函数名更保险。

但在严格模式下,不能通过脚本访问 arguments.callee,访问这个属性会导致错误。不过,可 以使用命名函数表达式来达成相同的结果。例如:

var factorial = (function f(num){

        if (num <= 1){

            return 1;

        } else {

            return num * f(num-1);

} });

以上代码创建了一个名为 f()的命名函数表达式,然后将它赋值给变量 factorial。即便把函数 赋值给了另一个变量,函数的名字 f 仍然有效,所以递归调用照样能正确完成。这种方式在严格模式和 非严格模式下都行得通。

当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链。 然后,使用 arguments 和其他命名参数的值来初始化函数的活动对象(activation object)。但在作用域 链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,......直至作为作用域链终点的全局执行环境。

 

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。
过度使用闭包可能会导致内存占用过多,我们建议读者只在绝对必要时再考虑使用闭 包。
虽然像 V8 等优化后的 JavaScript 引擎会尝试回收被闭包占用的内存,但请大家 还是要慎重使用闭包。

 

外部函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当外部函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,外部函数的活动对象才会被销毁。

 

闭包只能取得包含函数中任何变量的最 后一个值。别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量。我们可以通过创建另一个匿名函数强制让闭包的行为符合预期:

function createFunctions(){

    var result = new Array();

    for (var i=0; i < 10; i++){

        result[i] = function(num){

            return function(){

              return num;

   }; 

        }(i);

    }

    return result;

}

 

每个函数在被调用时都会自动取得两个特殊变量:this 和 arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。不过,把外部作用域中的 this 对象保存在一个闭包能够访问 到的变量里,就可以让闭包访问该对象了。

 

用作块级作用域(通常称为私有作用域)的匿名函数的语法如下所示。

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

})();

函数表达式的后面可以跟圆括号。

这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。

 

任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。 私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。

 

在 JavaScript 编程中,函数表达式是一种非常有用的技术。使用函数表达式可以无须对函数命名, 从而实现动态编程。匿名函数,也称为拉姆达函数,是一种使用 JavaScript 函数的强大方式。

 

在后台执行环境中,闭包的作用域链包含着它自己的作用域包含函数的作用域全局作用域

当函数返回了一个闭包时,这个函数的作用域将会一直在内存中保存到闭包不存在为止。

 

创建并立即调用一个函数,这样既可以执行其中的代码,又不会在内存中留下对该函数的引用。

 

先设计最通用的方案,然后再使用特定于浏览器的技术增强该方案。

 

一般应优先考虑使用能力检测怪癖检测是确定应该如何处理 代码的第二选择。而用户代理检测则是客户端检测的最后一种方案,因为这种方法对用户代理字符串具 有很强的依赖性。

 

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

function convertToArray(nodes){

    var array = null;

try {

        array = Array.prototype.slice.call(nodes, 0); //针对非 IE 浏览器
 } catch (ex) {

            array = new Array();

            for (var i=0, len=nodes.length; i < len; i++){

                array.push(nodes[i]);

            }

}

        return array;

}

 

Document 类型为此提供了两个方 法:getElementById()和 getElementsByTagName()。

第三个方法,也是只有 HTMLDocument 类型才有的方法,是 getElementsByName()。

 

document 对象还有一些特殊的集合。这些集合都是 HTMLCollection 对象, 为访问文档常用的部分提供了快捷方式,包括:

  1. document.anchors,包含文档中所有带 name 特性的<a>元素;

  2. document.forms,包含文档中所有的<form>元素,与 document.getElementsByTagName("form")得到的结果相同;

  3. document.images,包含文档中所有的<img>元素,与document.getElementsByTagName("img")得到的结果相同;

  4. document.links,包含文档中所有带 href 特性的<a>元素。

 

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);

使用了 try-catch 语句来捕获 IE 抛出的错误,然后再 使用针对 IE 的特殊方式来设置样式。

 

理解 DOM 的关键,就是理解 DOM 对性能的影响。DOM 操作往往是 JavaScript 程序中开销最大的 部分,而因访问 NodeList 导致的问题为最多。NodeList 对象都是“动态的”,这就意味着每次访问 NodeList 对象,都会运行一次查询。有鉴于此,最好的办法就是尽量减少 DOM 操作。

 

Selectors API Level 1 的核心是两个方法:querySelector()和 querySelectorAll()。在兼容的浏 览器中,可以通过 Document 及 Element 类型的实例调用它们。

 

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

配的元素,返回 null。

通过 Document 类型调用 querySelector()方法时,会在文档元素的范围内查找匹配的元素。而 通过 Element 类型调用 querySelector()方法时,只会在该元素后代元素的范围内查找匹配的元素。

 

querySelectorAll()方法接收的参数与 querySelector()方法一样,都是一个 CSS 选择符,但 返回的是所有匹配的元素而不仅仅是一个元素。这个方法返回的是一个 NodeList 的实例。

具体来说,返回的值实际上是带有所有属性和方法的 NodeList,而其底层实现则类似于一组元素 的快照,而非不断对文档进行搜索的动态查询。这样实现可以避免使用 NodeList 对象通常会引起的大多数性能问题。

 

要强制浏览器以某种模式渲染页面,可以使用 HTTP 头部信息 X-UA-Compatible,或通过等价的 <meta>标签来设置:

<meta http-equiv="X-UA-Compatible" content="IE=IEVersion”>

Edge:始终以最新的文档模式来渲染页面。忽略文档类型声明。对于 IE8,始终保持以 IE8 标 准模式渲染页面。对于 IE9,则以 IE9 标准模式渲染页面。

div.innerText = "Hello & welcome, <b>\"reader\"!</b>";

运行以上代码之后,会得到如下所示的结果。

<div id="content">Hello &amp; welcome, &lt;b&gt;&quot;reader&quot;!&lt;/b&gt;</div>

将 innerText 设置为等于 innerText,这样就可以去掉所有 HTML 标签,比如:

div.innerText = div.innerText;

执行这行代码后,就用原来的文本内容替换了容器元素中的所有内容(包括子节点,因而也就去掉 了 HTML 标签)。

 

function getInnerText(element){

    return (typeof element.textContent == "string") ?

        element.textContent : element.innerText;

}

function setInnerText(element, text){

    if (typeof element.textContent == "string"){

        element.textContent = text;

    } else {

        element.innerText = text;

    }

}

 

无论在哪个浏览器中,最重要的一条是要记住所有计算的样式都是只读的;不能修改计算后样式对 象中的 CSS 属性。此外,计算后的样式也包含属于浏览器内部样式表的样式信息,因此任何具有默认值 的 CSS 属性都会表现在计算后的样式中。

 

所有这些偏移量属性都是只读的,而且每次访问它们都需要重新计算。因此,应 该尽量避免重复访问这些属性;如果需要重复使用其中某些属性的值,可以将它们保 存在局部变量中,以提高性能。

 

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

 

“DOM2 级事件”定义了两个方法,用于处理指定和删除事件处理程序的操作:

addEventListener() 和 removeEventListener()。

所有 DOM 节点中都包含这两个方法,并且它们都接受 3 个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是 true,表示在捕获 阶段调用事件处理程序;如果是 false,表示在冒泡阶段调用事件处理程序。

其中第二个参数还可以是对象: element.addEventListener(event, object [,capture] );
事件会自动在传入对象中寻找handleEvent方法,也就是 object.handleEvent。
这样,在 element 触发event事件后,调用的是handleEvent 方法,
注意这里面的 this 是指向对象本身
而普通的函数,this传入函数里面的this 是指向事件的。


IE 实现了与 DOM 中类似的两个方法:

attachEvent()和 detachEvent()。

这两个方法接受相同 的两个参数:事件处理程序名称与事件处理程序函数。由于 IE8 及更早版本只支持事件冒泡,所以通过 attachEvent()添加的事件处理程序都会被添加到冒泡阶段。

 

在 IE 中使用 attachEvent()与使用 DOM0 级方法的主要区别在于事件处理程序的作用域。在使 用 DOM0 级方法的情况下,事件处理程序会在其所属元素的作用域内运行;在使用 attachEvent()方 法的情况下,事件处理程序会在全局作用域中运行,因此 this 等于 window。

 

对“事件处理程序过多”问题的解决方案就是事件委托
事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。

另外,在不需要的时候移除事件处理程序,也是解决这个问题的一种方案。

 

最适合采用事件委托技术的事件包括 click、mousedown、mouseup、keydown、keyup 和 keypress。 虽然 mouseover 和 mouseout 事件也冒泡,但要适当处理它们并不容易,而且经常需要计算元素的位置。

 

在使用事件时,需要考虑如下一些内存与性能方面的问题

  1. 有必要限制一个页面中事件处理程序的数量,数量太多会导致占用大量内存,而且也会让用户感觉页面反应不够灵敏。

  2. 建立在事件冒泡机制之上的事件委托技术,可以有效地减少事件处理程序的数量。

  3. 建议在浏览器卸载页面之前移除页面中的所有事件处理程序

 

解决重复提交表单的办法有两个:

在第一次提交表单后就禁用提交按钮,

或者利用 onsubmit 事件处理程序取消后续的 表单提交操作。

 

跨文档消息传送(cross-document messaging),有时候简称为 XDM,指的是在来自不同域的页面间传递消息

 

使用 try-catch 最适合处理那些我们无法控制的错误。

JSONP 是 JSON with padding(填充式 JSON 或参数式 JSON)的简写,是应用 JSON 的一种新方法, 在后来的 Web 服务中非常流行。JSONP 看起来与 JSON 差不多,只不过是被包含在函数调用中的 JSON, 就像下面这样。

callback({ "name": "Nicholas" });

JSONP 由两部分组成:回调函数和数据。回调函数是当响应到来时应该在页面中调用的函数。回调 函数的名字一般是在请求中指定的。而数据就是传入回调函数中的 JSON 数据。

 

惰性载入表示函数执行的分支仅会发生一次。有两种实现惰性载入的方式,

第一种就是在函数被调用时再处理函数。

第二种实现惰性载入的方式是在声明函数时就指定适当的函数。

 

与函数绑定紧密相关的主题是函数柯里化(function currying),它用于创建已经设置好了一个或多 个参数的函数。函数柯里化的基本方法和函数绑定是一样的:使用一个闭包返回一个函数。两者的区别 在于,当函数被调用时,返回的函数还需要设置一些传入的参数。

 

JavaScript 中的柯里化函数和绑定函数提供了强大的动态函数创建功能。

 

数组分块(array chunking)的技术,小块小块地处理数组,通 常每次一小块。基本的思路是为要处理的项目创建一个队列,然后使用定时器取出下一个要处理的项目

进行处理,接着再设置另一个定时器。

setTimeout(function(){

//取出下一个条目并处理

var item = array.shift(); process(item);

//若还有条目,再设置另一个定时器 
if(array.length > 0){

        setTimeout(arguments.callee, 100);

    }

}, 100);

 

一旦某个函数需要花 50ms 以上的时间完成,那么最好看看能否将任务分割为一系列可以使用定时器的小任务

 

函数节流背后的基本思想是指,某些代码不可以在没有间断的情况连续重复执行。第一次调用函数, 创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清除前一次的定时器 并设置另一个。如果前一个定时器已经执行过了,这个操作就没有任何意义。然而,如果前一个定时器 尚未执行,其实就是将其替换为一个新的定时器。目的是只有在执行函数的请求停止了一段时间之后才 执行。

节流在 resize 事件中是最常用的。

 

function throttle(method, context) {

clearTimeout(method.tId);

method.tId= setTimeout(function(){

        method.call(context);

    }, 100);

}

 

只要应用的某个部分过分依赖于另一部分,代码就是耦合过紧,难于维护。
典型的问题如:对象直接引用另一个对象,并且当修改其中一个的同时需要修改另外一个。
紧密耦合的软件难于维护并且需要经常重写

  1. 解耦 HTML/JavaScript

  2. 解耦 CSS/JavaScript

  3. 解耦应用逻辑/事件处理程序

 

以下是要牢记的应用和业务逻辑之间松散耦合的几条原则:

  1. 勿将 event 对象传给其他方法;只传来自 event 对象中所需的数据;

  2. 任何可以在应用层面的动作都应该可以在不执行任何事件处理程序的情况下进行;

  3. 任何事件处理程序都应该处理事件,然后将处理转交给应用逻辑。

 

虽然命名空间会需要多写一些代码,但是对于可维护的目的而言是值得的。

 

显示在用户界面上的字符串应该以允许 进行语言国际化的方式抽取出来。URL 也应被抽取出来,因为它们有随着应用成长而改变的倾向。基本 上,有着可能由于这样那样原因会变化的这些数据,那么都会需要找到函数并在其中修改代码 。而每次 修改应用逻辑的代码,都可能会引入错误。可以通过将数据抽取出来变成单独定义的常量的方式,应用逻辑与数据修改隔离开来

 

关键在于将数据和使用它的逻辑进行分离。要注意的值的类型如下所示。

  1. 重复值——任何在多处用到的值都应抽取为一个常量。这就限制了当一个值变了而另一个没变 的时候会造成的错误。这也包含了 CSS 类名。

  2. 用户界面字符串 —— 任何用于显示给用户的字符串,都应被抽取出来以方便国际化。

  3. URLs —— 在 Web 应用中,资源位置很容易变更,所以推荐用一个公共地方存放所有的 URL。

  4. 任意可能会更改的值 —— 每当你在用到字面量值的时候,你都要问一下自己这个值在未来是不是会变化。如果答案是“是”,那么这个值就应该被提取出来作为一个常量。

 

性能:

注意作用域

  1. 避免全局查找:使用全局变量和函数肯定要比局部的开销更大,因为要涉及作用域链上的查找。将在一个函数中会用到多次的全局对象存储为局部变量总是没错的。

  2. 避免 with 语句:会增加其中执行的代码的作用域链的长度。

选择正确方法

  1. 避免不必要的属性查找:一旦多次用到对象属性,应该将其存储在局部变量中。第一次访问该值会是 O(n),然而后续的访问 都会是 O(1),就会节省很多。

  2. 优化循环:减值迭代、简化终止条件、简化循环体、使用后测试循环。

  3. 展开循环:Duff 装置技术(针对大数据集)

  4. 避免双重解释

  5. 性能的其他注意事项:原生方法较快、Switch 语句较快、位运算符较快

最小化语句数

  1. 多个变量声明

  2. 插入迭代值

  3.  使用数组和对象字面量

优化DOM交互

  1. 最小化现场更新

  2. 使用 innerHTML

  3. 使用事件代理

  4. 注意 HTMLCollection:记住,任何时候要访问 HTMLCollection,不管它是一个属性还是一个方法,都是在文档上进 行一个查询,这个查询开销很昂贵。

 

JSLint 可以查找 JavaScript 代码中的语法错误以及常见的编码错误。

posted @ 2015-01-01 23:17  阿郎博客  阅读(10255)  评论(5编辑  收藏  举报