JavaScript高级程序设计

1、对未初始化的对象执行 typeof 操作符会返回 “undefined”,对未声明的对象执行 typeof 操作符同样也会返回 “undefined”

var message

typeof message  // "undefined"
typeof a  // "undefined"

这个结果有逻辑上的合理性。因为虽然这两种变量从技术角度来看有本质区别,但实际上无论对哪种变量也不可能执行真正的操作。

2、Null 类型是第二个只有一个值得数据类型,这个特殊的值是 null。从逻辑角度来看,null 值表示一个空指针对象,而这也正是使用 typeof 操作符检测 null 值会返回“object” 的原因

var car = null
typeof car // "object"

undefined 是派生自 null 值的,

undefined ==  null  // true

3、浮点数

0.1 + 0.2    // 0.30000000000000004

关于浮点数值计算会产生舍入误差的问题,有一点需要明确:这是基于使用 IEEE754 数值浮点计算的通病,ECMAScript并非独此一家,其他使用相同数值格式的语言也存在这个问题。

4、字符串的特点

  ECMAScript中的字符串是不可变的,也就是说,字符串一旦创建,它们的值就不能被改变。要改变某个变量保存的字符串,首先要销毁原来的字符串,然后用另一个包含新值的字符串填充该变量

5、Object类型

var o = new Object()

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

  • constructor:保存着用于创建当前对象的函数。对于前面的例子而言,构造函数(constructor)就是Object()
  • hasOwnProperty(propertyName):用于检查给定的属性在当前对象实例中(而不是在实例的原型中)是否存在。
  • isPrototypeOf(object):用于检查传入的对象是否是传入对象的原型
  • propertyIsEnumerable(propertyName):用于检查给定的属性是否能够使用 for-in 语句来枚举。
  • toLocaleString():返回对象的字符串表示,该字符串与执行环境的地区对应
  • toString():返回对象的字符串表示
  • valueOf():返回对象的字符串、数值或布尔值表示。通常与 toString() 方法的返回值相同

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

6、操作符

在对非数值应用一元加操作符时,对象会先调用它们的 valueOf() 和(或)toString() 方法,在转换得到的值

var s1 = "01"; 
var s2 = "1.1"; 
var s3 = "z"; 
var b = false; 
var f = 1.1; 
var o = {     
  valueOf: function() {         
    return -1;     
  } 
}; 
 
s1 = +s1;     // 值变成数值 1 
s2 = +s2;     // 值变成数值 1.1 
s3 = +s3;     // 值变成 NaN 
b = +b;       // 值变成数值 0 
f = +f;       // 值未变,仍然是 1.1 
o = +o;       // 值变成数值-1 

7、函数传递参数

function setName(obj){
  obj.name = "Dylan"
}
var person = new Object()
setName(person)
console.log(person.name); // Dylan

  以上代码中创建了一个对象,并将其保存在了变量 person 中。然后,这个变量被传递到 setName() 函数中之后就被复制给了 obj 。在这个函数内部,obj 和 person引用的是同一个对象。换句话说,即使这个变量是按值传递的,obj 也会按引用来访问同一个对象。于是,当在函数内部为 obj 添加 name 属性后,函数外部的 person也将有所反映;因为person指向的对象在堆内存中只有一个,而且是全局对象。有很多开发人员错误地任务:在局部作用域中修改的对象会在全局作用域中反映出来,就说明参数是按引用传递的。为了证明对象是按值传递的,我们再看一看下面这个经过修改的例子:

function setName(obj){
  obj.name = "Dylan"
  obj = new Object()
  obj.name = "Jerry"
}
var person = new Object()
setName(person)
console.log(person.name); // Dylan

个人见解:函数的参数 obj 是按值传递的,它是person的一份复制。但如果obj是一个对象的话,这里实际上是person值的一个复制(引用类型的对象,值为一个指针,指向堆栈中的内容)

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

8、执行环境及作用域

  执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。

  全局执行环境是最外围的一个执行环境。根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样。在 Web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出---例如关闭网页或浏览器---时才会被销毁)。

  每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。ECMAScript程序中的执行流整数由这个方便的机制控制着。

  当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行环境所在环境的变量对象。如果这个环境是函数,则将其活动对象作为变量对象。活动对象在最开始时只包含一个变量,即 arguments 对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。

  标识符解析是沿着作用域链一级一级的搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直到找到标识符为止(如果找不到标识符,通常会导致错误发生)

9、垃圾收集机制

  函数中局部变量的正常生命周期:局部变量只在函数执行的过程中存在。而在这个过程中,会为局部变量在栈(或堆)内存上分配相应的空间,以便存储它们的值。然后在函数中使用这些变量,直至函数执行结束。此时,局部变量就没有存在的必要了,因此可以释放它们的内存以便将来使用。垃圾回收器必须跟踪哪个变量有用哪个变量没用,对于不再有用的变量打上标记,以备将来收回其占用的内存。

  JavaScript中最常用的垃圾收集方式是标记清除,当变量进入环境时(例如,在函数中声明一个变量)。就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存。因为只要执行流进入相应的环境,就可能会用到它们。当变量离开环境时,则将其标记为“离开环境”。

  可以使用任何方式来标记变量。比如,可以翻转某个特殊的位来记录一个变量何时进入环境,或者使用一个“进入环境的”变量列表及一个“离开环境的”变量列表来跟踪哪个变量发生了变化。说到底,如何标记变量其实不重要,关键在于采取什么策略。

  垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。

10、变量、作用域、内存,小结

  JavaScript变量可以用来保存两种类型的值:基本类型值和引用类型值。基本类型值源于以下5种基本数据类型:Undefined、Null、Boolean、Number 和 String。基本类型值和引用类型值有以下特点:

  • 基本类型值在内存中占据固定大小的空间,因此被保存在栈内存中
  • 从一个变量向另一个变量复制基本类型的值,会创建这个值的一个副本
  • 引用类型的值是对象,保存在堆内存中
  • 包含引用类型值的变量实际上包含的并不是对象本身,而是一个指向该对象的指针
  • 从一个变量向另一个变量复制引用类型的值,复制的实际是指针,因此两个变量最终都指向同一个对象
  • 确定一个值是哪种基本类型可以使用 typeof 操作符,而确定一个值是哪种引用类型可以使用 instanceof 操作符

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

  • 执行环境有全局执行环境(也称全局环境)和函数执行环境之分
  • 每次进入一个新执行环境,都会创建一个用于搜索变量和函数的作用域链; 
  • 函数的局部环境不仅有权访问函数作用域中的变量,而且有权访问其包含(父)环境,乃至全 局环境;
  • 全局环境只能访问在全局环境中定义的变量和函数,而不能直接访问局部环境中的任何数据; 
  • 变量的执行环境有助于确定应该何时释放内存。 

  JavaScript 是一门具有自动垃圾收集机制的编程语言,开发人员不必关心内存分配和回收问题。可 以对 JavaScript的垃圾收集例程作如下总结:

  • 离开作用域的值将被自动标记为可以回收,因此将在垃圾收集期间被删除。 
  • “标记清除”是目前主流的垃圾收集算法,这种算法的思想是给当前不使用的值加上标记,然 后再回收其内存。 
  • 另一种垃圾收集算法是“引用计数”,这种算法的思想是跟踪记录所有值被引用的次数。JavaScript 引擎目前都不再使用这种算法;但在 IE中访问非原生 JavaScript对象(如 DOM元素)时,这种 算法仍然可能会导致问题。 
  • 当代码中存在循环引用现象时,“引用计数”算法就会导致问题。 
  •  解除变量的引用不仅有助于消除循环引用现象,而且对垃圾收集也有好处。为了确保有效地回 收内存,应该及时解除不再使用的全局对象、全局对象属性以及循环引用变量的引用。

11、数组迭代方法

  • every():对数组中的每一项运行给定函数,如果该函数对每一项都返回 true,则返回 true。
  • some():对数组中的每一项运行给定函数,如果该函数对任意一项返回 true,则返回 true。 
  • filter():对数组中的每一项运行给定函数,返回该函数会返回 true 的项组成的数组。 
  • forEach():对数组中的每一项运行给定函数。这个方法没有返回值。 
  • map():对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组。 

12、归并方法

  ECMAScript 5 还新增了两个归并数组的方法:reduce()和 reduceRight()。这两个方法都会迭 代数组的所有项,然后构建一个终返回的值。其中,reduce()方法从数组的第一项开始,逐个遍历 到后。而 reduceRight()则从数组的后一项开始,向前遍历到第一项。 

  这两个方法都接收两个参数:一个在每一项上调用的函数和(可选的)作为归并基础的初始值。传给 reduce()和 reduceRight()的函数接收 4 个参数:前一个值、当前值、项的索引和数组对象。这个函数返回的任何值都会作为第一个参数自动传给下一项。第一次迭代发生在数组的第二项上,因此第 一个参数是数组的第一项,第二个参数就是数组的第二项。 

使用reduce求和

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

第一次执行回调函数,prev 是 1,cur 是 2。第二次,prev 是 3(1加 2的结果),cur 是 3(数组 的第三项)。这个过程会持续到把数组中的每一项都访问一遍,后返回结果。 

reduceRight()的作用类似,只不过方向相反而已。

13、函数的内部属性

  在函数内部,有两个特殊的对象:arguments 和 this。其中,arguments 是一个类数组对象,包含着传入函数中的所有参数。。虽然 arguments 的主要用途是保存函数参数, 但这个对象还有一个名叫 callee 的属性,该属性是一个指针,指向拥有这个 arguments 对象的函数。 请看下面这个非常经典的阶乘函数。 

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

  定义阶乘函数一般都要用到递归算法;如上面的代码所示,在函数有名字,而且名字以后也不会变 的情况下,这样定义没有问题。但问题是这个函数的执行与函数名 factorial 紧紧耦合在了一起。为 了消除这种紧密耦合的现象,可以像下面这样使用 arguments.callee。 

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

14、基本包装类型

  为了便于操作基本类型值,ECMAScript 还提供了 3 个特殊的引用类型:Boolean、Number 和 String。这些类型与本章介绍的其他引用类型相似,但同时也具有与各自的基本类型相应的特殊行为。 实际上,每当读取一个基本类型值的时候,后台就会创建一个对应的基本包装类型的对象,从而让我们 能够调用一些方法来操作这些数据。

var s1 = "some text"; 
var s2 = s1.substring(2); 

  这个例子中的变量 s1 包含一个字符串,字符串当然是基本类型值。而下一行调用了 s1 的 substring()方法,并将返回的结果保存在了 s2 中。我们知道,基本类型值不是对象,因而从逻辑上 讲它们不应该有方法(尽管如我们所愿,它们确实有方法)。其实,为了让我们实现这种直观的操作, 后台已经自动完成了一系列的处理。当第二行代码访问 s1 时,访问过程处于一种读取模式,也就是要 从内存中读取这个字符串的值。而在读取模式中访问字符串时,后台都会自动完成下列处理。 

  • (1) 创建 String 类型的一个实例;
  • (2) 在实例上调用指定的方法; 
  • (3) 销毁这个实例。

 可以将以上三个步骤想象成是执行了下列 ECMAScript代码。 

var s1 = new String("some text"); 
var s2 = s1.substring(2); 
s1 = null; 

  

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

var s1 = "some text"; 
s1.color = "red"; 
alert(s1.color);  //undefined 

  在此,第二行代码试图为字符串 s1 添加一个 color 属性。但是,当第三行代码再次访问 s1 时, 其 color 属性不见了。问题的原因就是第二行创建的 String 对象在执行第三行代码时已经被销毁了。 第三行代码又创建自己的 String 对象,而该对象没有 color 属性。 

15、构造函数模式

特点:

  • 没有显示地创建对象
  • 直接将属性和方法赋给了this对象
  • 没有return语句

 使用new操作符,会经历以下几个步骤

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

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

function Person(){ } 
 
Person.prototype.name = "Nicholas"; 
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer"; 
Person.prototype.sayName = function(){     alert(this.name);  }; 
 
var keys = Object.keys(Person.prototype); 
alert(keys);       //"name,age,job,sayName" 
 
var p1 = new Person(); 
p1.name = "Rob"; 
p1.age = 31; 
var p1keys = Object.keys(p1); 
alert(p1keys);    //"name,age" 

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

var keys = Object.getOwnPropertyNames(Person.prototype); 
alert(keys);    //"constructor,name,age,job,sayName" 

注意结果中包含了不可枚举的 constructor 属性

Object.keys()和 Object.getOwnProperty- Names()方法都可以用来替代 for-in 循环

 16、组合使用构造函数模式与原型模式

  构造函数模式用于定义实 例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本, 但同时又共享着对方法的引用,大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参 数;可谓是集两种模式之长。

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

17、寄生构造函数模式

  这种模式 的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但 从表面上看,这个函数又很像是典型的构造函数。

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; 
} 
 
var friend = new Person("Nicholas", 29, "Software Engineer"); 
friend.sayName();  //"Nicholas" 

  

  除了使用 new 操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实 是一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个 return 语句,可以重写调用构造函数时返回的值。 

 18、原型链继承

function SuperType(){
  this.property = true
}
SuperType.prototype.getSuperValue = function(){
  return this.property
}

function SubType(){
  this.subProperty = false
}
// 继承了SuperType
SubType.prototype = new SuperType()

SubType.prototype.getSubValue = function(){
  return this.subProperty
}
var instance = new SubType()
console.log(instance.getSuperValue()); // true

  注意 instance.constructor 现在指向的 是 SuperType,这是因为原来 SubType.prototype 中的 constructor 被重写了的缘故。

  SubType继承了SuperType,而SuperType继承了Object。当调用instance.toString() 时,实际上调用的是保存在 Object.prototype 中的那个方法。  

  实际上,不是 SubType 的原型的 constructor 属性被重写了,而是 SubType 的原型指向了另一个对象—— SuperType 的原型,而这个原型对象的 constructor 属性指向的是 SuperType。 

问题:引用类型值的原型属性会被所有实例共享;

19、借用构造函数继承

  基本思想:在子类型构造函数的内部调用超类型构造函数。

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

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

20、组合继承

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

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

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript中常用的继 承模式。

21、原型式继承

借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型

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

  ECMAScript 5通过新增 Object.create()方法规范化了原型式继承。这个方法接收两个参数:一 个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下, Object.create()与 object()方法的行为相同。

注意,Object.create() 是一个浅复制

var person = {
  name: "Dylan",
  friends: ["1","2","3"]
}

var person1 = Object.create(person)
person1.name = "Dylan1"
person1.friends.push("4")
console.log(person.name) // Dylan
console.log(person.friends) // [ '1', '2', '3', '4' ]

var person2 = Object.create(person)
person2.name = "Dylan2"
person2.friends.push("5")
console.log(person.name) // Dylan
console.log(person.friends) // [ '1', '2', '3', '4', '5' ]
console.log(person1.friends)  // [ '1', '2', '3', '4', '5' ]

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

var person = {
     name: "Nicholas",
     friends: ["Shelby", "Court", "Van"] 
};

var anotherPerson = Object.create(person, {
     name: {
         value: "Greg"
     } 
});     

alert(anotherPerson.name); //"Greg" 

22、寄生式继承

  思路:创建一个仅用于封装继承过程的函数,该 函数在内部以某种方式来增强对象,后再像真地是它做了所有工作一样返回对象。

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

  在这个例子中,createAnother()函数接收了一个参数,也就是将要作为新对象基础的对象。然 后,把这个对象(original)传递给 object()函数,将返回的结果赋值给 clone。再为 clone 对象 添加一个新方法 sayHi(),后返回 clone 对象。可以像下面这样来使用 createAnother()函数:

var person = {
     name: "Nicholas",
     friends: ["Shelby", "Court", "Van"] 
}; 
 
var anotherPerson = createAnother(person); 
anotherPerson.sayHi(); //"hi" 

  缺点:使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一 点与构造函数模式类似。 

23、寄生组合式继承

  前面说过,组合继承是 JavaScript 常用的继承模式;不过,它也有自己的不足。组合继承大的 问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是 在子类型构造函数内部。没错,子类型终会包含超类型对象的全部实例属性,但我们不得不在调用子 类型构造函数时重写这些属性。再来看一看下面组合继承的例子。 

function SuperType(name){
     this.name = name;
     this.colors = ["red", "blue", "green"]; 
} 
 
SuperType.prototype.sayName = function(){
     alert(this.name); 
}; 
 
function SubType(name, age){
       SuperType.call(this, name);         //第二次调用 SuperType()
          this.age = age; 
} 
 
SubType.prototype = new SuperType();    //第一次调用 SuperType() 
SubType.prototype.constructor = SubType; 
SubType.prototype.sayAge = function(){
     alert(this.age); 
};

  在第一次调用 SuperType 构造函数时, SubType.prototype 会得到两个属性:name 和 colors;它们都是 SuperType 的实例属性,只不过 现在位于 SubType 的原型中。当调用 SubType 构造函数时,又会调用一次 SuperType 构造函数,这 一次又在新对象上创建了实例属性 name 和 colors。于是,这两个属性就屏蔽了原型中的两个同名属 性。

  有两组 name 和 colors 属性:一组在实例上,一组在 SubType 原型中。这就是调 用两次 SuperType 构造函数的结果。好在我们已经找到了解决这个问题方法——寄生组合式继承。 

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

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

父亲的原型(继承到方法)  +  儿子的构造函数(得到属性)

  这个示例中的 inheritPrototype()函数实现了寄生组合式继承的简单形式。这个函数接收两 个参数:子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本。第二 步是为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性。 后一步,将新创建的对象(即副本)赋值给子类型的原型。这样,我们就可以用调用 inherit- Prototype()函数的语句,去替换前面例子中为子类型原型赋值的语句了

function SuperType(name){
     this.name = name;
     this.colors = ["red", "blue", "green"]; 
} 
SuperType.prototype.sayName = function(){
     alert(this.name); 
}; 
function SubType(name, age){
       SuperType.call(this, name);
       this.age = age; 
} 
inheritPrototype(SubType, SuperType); SubType.prototype.sayAge
= function(){ alert(this.age); };

  这个例子的高效率体现在它只调用了一次 SuperType 构造函数,并且因此避免了在 SubType. prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf()。开发人员普遍认为寄生组合式继承是引用类型理想的继承范式。

总结:

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

  • 工厂模式,使用简单的函数创建对象,为对象添加属性和方法,然后返回对象。这个模式后来 被构造函数模式所取代。 
  • 构造函数模式,可以创建自定义引用类型,可以像创建内置对象实例一样使用 new 操作符。不 过,构造函数模式也有缺点,即它的每个成员都无法得到复用,包括函数。由于函数可以不局 限于任何对象(即与对象具有松散耦合的特点),因此没有理由不在多个对象间共享函数。 
  • 原型模式,使用构造函数的 prototype 属性来指定那些应该共享的属性和方法。组合使用构造 函数模式和原型模式时,使用构造函数定义实例属性,而使用原型定义共享的属性和方法。 

   

  JavaScript 主要通过原型链实现继承。原型链的问题是对象实例共享所有继承的属性和方法,因此不适宜单独使用。解决这个问题的技术是借 用构造函数,即在子类型构造函数的内部调用超类型构造函数。这样就可以做到每个实例都具有自己的 属性,同时还能保证只使用构造函数模式来定义类型。使用多的继承模式是组合继承,这种模式使用原型链继承共享的属性和方法,而通过借用构造函数继承实例属性。 

  • 原型式继承,可以在不必预先定义构造函数的情况下实现继承,其本质是执行对给定对象的浅 复制。而复制得到的副本还可以得到进一步改造。 
  • 寄生式继承,与原型式继承非常相似,也是基于某个对象或某些信息创建一个对象,然后增强 对象,后返回对象。为了解决组合继承模式由于多次调用超类型构造函数而导致的低效率问 题,可以将这个模式与组合继承一起使用。 
  • 寄生组合式继承,集寄生式继承和组合继承的优点与一身,是实现基于类型继承的有效方式。

24、函数表达式---递归

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

var anotherFactorial = factorial; 
factorial = null; 
console.log(anotherFactorial(4)); // 报错

  以上代码先把 factorial()函数保存在变量 anotherFactorial 中,然后将 factorial 变量设 置为 null,结果指向原始函数的引用只剩下一个。但在接下来调用 anotherFactorial()时,由于必 须执行 factorial(),而 factorial 已经不再是函数,所以就会导致错误。在这种情况下,使用 argu- ments.callee 可以解决这个问题。 

  我们知道,arguments.callee 是一个指向正在执行的函数的指针,因此可以用它来实现对函数 的递归调用。

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

var anotherFactorial = factorial; 
factorial = null; 
console.log(anotherFactorial(4)); // 24

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

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

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

25、闭包

  在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量

function compare(value1, value2){
     if (value1 < value2){
         return -1;
     } else if (value1 > value2){
         return 1;
     } else {
         return 0;
     } 
} 
 
var result = compare(5, 10);

  以上代码先定义了 compare()函数,然后又在全局作用域中调用了它。当调用 compare()时,会 创建一个包含 arguments、value1 和 value2 的活动对象。全局执行环境的变量对象(包含 result 和 compare)在 compare()执行环境的作用域链中则处于第二位。

  后台的每个执行环境都有一个表示变量的对象——变量对象。全局环境的变量对象始终存在,而像 compare()函数这样的局部环境的变量对象,则只在函数执行的过程中存在。在创建 compare()函数 时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中。 当调用 compare()函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对 象构建起执行环境的作用域链。此后,又有一个活动对象(在此作为变量对象使用)被创建并被推入执 行环境作用域链的前端。对于这个例子中 compare()函数的执行环境而言,其作用域链中包含两个变 量对象:本地活动对象和全局变量对象。显然,作用域链本质上是一个指向变量对象的指针列表,它只 引用但不实际包含变量对象。

  无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲, 当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。 但是,闭包的情况又有所不同。

  在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。

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

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

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

这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。 一般来说,我们都应该尽量少向全局作用域中添加变量和函数。

(function(){
     var now = new Date();
     if (now.getMonth() == 0 && now.getDate() == 1){
         alert("Happy new year!");
     } 
})(); 

  把上面这段代码放在全局作用域中,可以用来确定哪一天是 1月 1日;如果到了这一天,就会向用 户显示一条祝贺新年的消息。其中的变量 now 现在是匿名函数中的局部变量,而我们不必在全局作用域 中创建它。

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

26、私有变量

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

  我们把有权访问私有变量和私有函数的公有方法称为特权方法(privileged method)。

  有两种在对象 上创建特权方法的方式。

  第一种是在构造函数中定义特权方法

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

在创建 MyObject 的实例后,除了使用 publicMethod()这一个途 径外,没有任何办法可以直接访问 privateVariable 和 privateFunction()。 

缺点:必须使用构造函数来达到这个目的

27、静态私有变量

(function(){
    var name = "";
    Person = function(value){
         name = value;
    };
    Person.prototype.getName = function(){
         return name;
    };
    Person.prototype.setName = function (value){ 
         name = value;
     };
})(); 
 
var person1 = new Person("Nicholas"); 
alert(person1.getName());  //"Nicholas" 
person1.setName("Greg"); 
alert(person1.getName());  //"Greg" 
 
var person2 = new Person("Michael"); 
alert(person1.getName()); //"Michael" 
alert(person2.getName()); //"Michael" 

  这个例子中的Person构造函数与getName()和setName()方法一样,都有权访问私有变量name。 在这种模式下,变量 name 就变成了一个静态的、由所有实例共享的属性。也就是说,在一个实例上调 用 setName()会影响所有实例。而调用 setName()或新建一个 Person 实例都会赋予 name 属性一个 新值。结果就是所有实例都会返回相同的值。

  以这种方式创建静态私有变量会因为使用原型而增进代码复用,但每个实例都没有自己的私有变 量。到底是使用实例变量,还是静态私有变量,终还是要视你的具体需求而定。 

28、BOM

//这里会抛出错误,因为 oldValue 未定义 
var newValue = oldValue; 
 
//这里不会抛出错误,因为这是一次属性查询 
//newValue 的值是 undefined 
var newValue = window.oldValue;

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

  新图像元素不一定要从添加到文档后才开始 下载,只要设置了 src 属性就会开始下载。 

  与图像不同,只有在设置了<script>元素的 src 属性并将该元素添加到文档后,才会开始下 载 JavaScript 文件。换句话说,对于<script>元素而言,指定 src 属性和指定事件处理程序的先后顺 序就不重要了。

  与<script>节点类似,在未指定 href 属性并将<link>元素添加到文档之前也不会开始下载样式表。 

29、DOMContentLoaded 事件 

  window 的 load 事件会在页面中的一切都加载完毕时触发,但这个过程可能会因为要 加载的外部资源过多而颇费周折。而 DOMContentLoaded 事件则在形成完整的 DOM树之后就会触发, 不理会图像、JavaScript 文件、CSS 文件或其他资源是否已经下载完毕。与 load 事件不同, DOMContentLoaded 支持在页面下载的早期添加事件处理程序,这也就意味着用户能够尽早地与页面 进行交互。

30、触摸与手势事件 

  •  touchstart:当手指触摸屏幕时触发;即使已经有一个手指放在了屏幕上也会触发。 
  •  touchmove:当手指在屏幕上滑动时连续地触发。在这个事件发生期间,调用preventDefault() 可以阻止滚动。 
  • touchend:当手指从屏幕上移开时触发。 
  • touchcancel:当系统停止跟踪触摸时触发。

   上面这几个事件都会冒泡,也都可以取消。虽然这些触摸事件没有在 DOM规范中定义,但它们却 是以兼容 DOM的方式实现的。因此,每个触摸事件的 event 对象都提供了在鼠标事件中常见的属性: bubbles、cancelable、view、clientX、clientY、screenX、screenY、detail、altKey、shiftKey、 ctrlKey 和 metaKey。

   除了常见的 DOM属性外,触摸事件还包含下列三个用于跟踪触摸的属性。 

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

在触摸屏幕上的元素 时,这些事件(包括鼠标事件)发生的顺序如下:

(1)touchstart 

(2)mouseover 

(3)mousemove(一次)  

(4)mousedown 

(5)mouseup 

(6)click 

(7)touchend 

31、内存和性能

事件委托

  对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事 件处理程序,就可以管理某一类型的所有事件。例如,click 事件会一直冒泡到 document 层次。也就 是说,我们可以为整个页面指定一个 onclick 事件处理程序,而不必给每个可单击的元素分别添加事 件处理程序。

<ul id="myLinks">
     <li id="goSomewhere">Go somewhere</li>
     <li id="doSomething">Do something</li>
     <li id="sayHi">Say hi</li> 
</ul>

使用事件委托

var list = document.getElementById("myLinks"); 
 
document.addEventListener("click", function(event){
    var target = EventUtil.getTarget(event); 

    switch(target.id){
        case "doSomething":
             document.title = "I changed the document's title";
             break; 
        case "goSomewhere":
             location.href = "http://www.wrox.com";
             break; 
        case "sayHi":
             alert("hi");
             break;
     } 
})

  如果可行的话,也可以考虑为 document 对象添加一个事件处理程序,用以处理页面上发生的某种 特定类型的事件。这样做与采取传统的做法相比具有如下优点。

  • document 对象很快就可以访问,而且可以在页面生命周期的任何时点上为它添加事件处理程序 (无需等待 DOMContentLoaded 或 load 事件)。换句话说,只要可单击的元素呈现在页面上, 就可以立即具备适当的功能。 
  • 在页面中设置事件处理程序所需的时间更少。只添加一个事件处理程序所需的 DOM引用更少, 所花的时间也更少。
  • 整个页面占用的内存空间更少,能够提升整体性能。  

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

  • 有必要限制一个页面中事件处理程序的数量,数量太多会导致占用大量内存,而且也会让用户 感觉页面反应不够灵敏。 
  • 建立在事件冒泡机制之上的事件委托技术,可以有效地减少事件处理程序的数量。 
  •  建议在浏览器卸载页面之前移除页面中的所有事件处理程序。 

 32、富文本框编辑

 使用contenteditable属性 

  这个属性是由IE最早实现的。可以把 contenteditable 属性应用给页面中的任何元素,然后用户立即就可以编辑该元素。 这种方法之所以受到欢迎,是因为它不需要 iframe、空白页和 JavaScript,只要为元素设置 contenteditable 属性即可。 

<div class="editable" id="richedit" contenteditable></div> 

  这样,元素中包含的任何文本内容就都可以编辑了,就好像这个元素变成了<textarea>元素一样。 通过在这个元素上设置 contenteditable 属性,也能打开或关闭编辑模式。 

var div = document.getElementById("richedit"); 
div.contentEditable = "true"; 

  contenteditable 属性有三个可能的值:"true"表示打开、"false"表示关闭,"inherit"表示 从父元素那里继承(因为可以在 contenteditable 元素中创建或删除元素)。

操作富文本

  与富文本编辑器交互的主要方式,就是使用 document.execCommand()。这个方法可以对文档执 行预定义的命令,而且可以应用大多数格式。可以为 document.execCommand()方法传递 3个参数: 要执行的命令名称、表示浏览器是否应该为当前命令提供用户界面的一个布尔值和执行命令必须的一个 值(如果不需要值,则传递 null)。为了确保跨浏览器的兼容性,第二个参数应该始终设置为 false, 因为 Firefox会在该参数为 true 时抛出错误。 

1

原文地址:https://www.cnblogs.com/haishen/p/12069613.html