javascript--6面向对象程序设计

这一章应该是Javascript中最抽象的一章,其中原型、原型链、构造函数等多次出现,几乎贯穿了整个章节。而对于创建对象和继承,也都是基于原型和构造函数而来的。因此这一部分的内容需要细细琢磨。尤其是对于原型、原型链,应该多画图,加深理解。
1、面向对象的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。ECMAScript中没有类的概念,因此它的对象业余基于类的语言中的对象有所不同
2、创建对象:
(1)、最简单的方式
创建一个Object实例,然后为它添加属性和方法:
var person = new Object();

person.name = "Nico";
person.age = 29;
person.job = "software engineer";

person.sayName = function(){
   console.log( this.name );
}

person.sayName();
这种模式有一个缺点,使用同一个接口创建很多对象时,会产生大量的重复代码。
(2)、工厂模式
         这种模式抽象了穿件具体对象的过程:
function createPerson( name, age, job ){
var o = new Object();
o.name = name;
o.job  = job;
o.age  = age;

o.sayName = function(){
console.log(this.name);
};
return o;
}

var p1 = createPerson("Nico", 29,"soft eg");
var p2 = createPerson("Greg", 25,"doctor");

p1.sayName();
p2.sayName();
console.log(p1.constructor);
console.log(p2.constructor);
工厂模式虽然解决了创建多个相似对象的代码冗余问题,却没有解决对象识别的问题(无法区分对象的类型)
(3)构造函数模式
function Person( name, age, job ){
this.name = name;
this.age  = age;
this.job  = job;

this.sayName = function(){
       console.log( this.name );
    }
}

var p1 = new Person("Nico", 29,"soft eg");
var p2 = new Person("Greg", 25,"doctor");

p1.sayName();
p2.sayName();

console.log(p1.constructor);
console.log(p2.constructor);
console.log(p1 instanceof Person);
console.log(p1 instanceof Object);
对象的constructor属性最初是用来标识对象属性的,但是,提到检测对象类型,还是instanceof更加可靠一点
         虽然构造函数非常好用,但是并非没有缺点,使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍,上述例子中,p1和p2的sayName被创建了两遍,这两个函数并不相等:
console.log(p1.sayName == p2.sayName);//false
创建两个相同的函数并没有必要,可以把sayName函数移到构造函数外部来解决这个问题:
function Person( name, age, job ){
this.name = name;
this.age  = age;
this.job  = job;

this.sayName = sayName;
}

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

var p1 = new Person("Nico", 29,"soft eg");
var p2 = new Person("Greg", 25,"doctor");

p1.sayName();
p2.sayName();
这样又带来了新的问题:全局变量中定义的函数实际上只能被某个对象调用,这让全局作用域名不副实。如果对象需要定义很多方法,那么就需要定义很多全局函数,于是这个自定义的引用类型就毫无封装性可言了。可以用原型模式来解决这些问题。
(4)、原型模式
每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由任何特定类型的所有实例共享的属性和方法。使用原型对象的好处是让所有对象实例共享它所包含的属性和方法。因此,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中:
function Person(){
}

Person.prototype.name = "Nico";
Person.prototype.age  = 29;
Person.prototype.job  = "soft eg";
Person.prototype.sayName = function(){
   console.log( this.name );
}
var p1 = new Person();
p1.sayName();

var p2 = new Person();
p2.sayName();

console.log(p1.sayName == p2.sayName);//true
上述例子中,创建的新对象具有相同的属性和方法,新对象的属性和方法是由所有实例共享的。p1和p2访问的都是同一组属性和同一个sayName函数。
理解原型对象:
无论何时,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获取一个constructor属性,这个属性包含一个指向prototype属性所在函数的指针:
function Person(){

}

Person.prototype.name = "Nico";
Person.prototype.age  = 29;
Person.prototype.job  = "soft eg";
这个例子中,Person.prototype.constructor指向Person。Person的原型对象默认只会取得constructor属性,至于其他方法,则都是从Object中继承而来的。调用函数创建一个实例后,实例的内部将包含一个指针,指向构造函数的原型对象。在很多实现中,这个内部属性的名字是__proto__,而且通过脚本可以访问到(FireFox,Safari,Chrome和Flash的ActionScript中,都可以通过脚本访问__proto__),例如在firefox中:
function Person(){

}

Person.prototype.name = "Nico";
Person.prototype.age  = 29;
Person.prototype.job  = "soft eg";
var p = new Person();
console.log(p);
打印出的结果:
这个连接存在于实例与构造函数的原型对象之间,而不是实例与构造函数之间。下图展示了各个对象之间的关系(分别是Person的构造函数,Person的原型对象和Person的两个实例):
在此,Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person。原型对象除了包含constructor属性外,还包括后来添加的其他属性。Person的每个实例,person1和person2都包含一个内部属性,该属性仅仅指向了Person.prototype,它们与构造函数没有直接的关系。值得注意的是,虽然这两个实例都不包含属性和方法,但是却可以调用person1.sayName,这是通过查找对象属性的过程来实现的。可以通过isPrototypeOf()确定对象之间的关系。
         对象属性的查找过程:每当代码读取某个对象的属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索先从对象实例本身开始,如果在实例中找到了具有给定名字的属性,那么返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找属性,如果在原型对象中找到了这个属性,则返回该属性的值。
         虽然可以通过对象实例访问保存在原型,但却不能通过对象实例重写原型中的值。如果在实例中添加了一个属性,(如果原型对象中存在该属性)那么该属性就会覆盖原型对象中的属性:
function Person(){

}

Person.prototype.name = "Nico";
Person.prototype.age  = 29;
Person.prototype.job  = "soft eg";
Person.prototype.sayName = function(){
   console.log( this.name );
}

var p1 = new Person();
p1.sayName();

var p2 = new Person();
p2.sayName();

p1.name = "new name";
console.log(p1.name);// new name 来自实例p1
console.log(p2.name);// Nico 来自原型对象

为对象添加一个属性时,这个属性就会屏蔽原型对象中的同名属性,换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。即使把这个属性设置为null, 也不会恢复其指向原型的连接。不过,使用delete操作符则可以完全删除实例属性,从而可以从新访问原型中的属性:
var p1 = new Person();
var p2 = new Person();

p1.name = "new name";

delete p1.name;
delete p2.name;

console.log(p1.name);
console.log(p2.name);
         使用hasOwnProperty()方法可以检测一个属性时存在于实例中,还是存在于原型中,这个方法只有在给定属性存在于对象实例中时,才会返回true:
var p1 = new Person();
var p2 = new Person();

console.log(p1.hasOwnProperty("name" ));
console.log(p2.hasOwnProperty("name" ));
p1.name = "new name";

console.log(p1.hasOwnProperty("name" ));
console.log(p2.hasOwnProperty("name" ));
delete p1.name;
delete p2.name;

console.log(p1.hasOwnProperty("name" ));
console.log(p2.hasOwnProperty("name" ));
         in操作符可以检测通过对象能否访问特定的属性,无论这个属性存在于原型中还是存在于实例中。
         for-in循环可以返回所有能够通过对象访问的,可枚举的属性,其中既包含了存在实例中的属性,也包含了存在于原型中的属性:
function Person(){

}

Person.prototype.name = "Nico";
Person.prototype.age  = 29;
Person.prototype.job  = "soft eg";
Person.prototype.sayName = function(){
   console.log( this.name );
}

var p1 = new Person();
p1.aprop = 'a property';
p1.aprop2 = 'another property';

for( var pro in p1 ){
   console.log(pro + ":" + p1[pro]);
}
IE中存在一个bug,即屏蔽不可枚举的实例属性不会出现在for-in属性中。
为了从视觉上更好地封装原型的功能,更常用的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象:
function Person(){
}

Person.prototype = {
         name: "Nico",
         age  : 29,
         job  : "soft eg",
         sayName:function(){
                   console.log(this.name );
         }
};

这种方法创建的对象,其constructor属性不再指向Person了,尽管通过instanceOf还能返回正确的结果,但是constructor已经无法确定对象的类型了:
function Person(){

}

Person.prototype = {
   name : "Nico",
   age  : 29,
   job  : "soft eg",
   sayName: function(){
       console.log( this.name );
    }
};

var p1 = new Person();
console.log(p1.constructor);
console.log(p1 instanceof Person);
如果constructor值特别重要,可以将它设置为适当的值:
Person.prototype = {
   constructor:Person,
   name : "Nico",
   age  : 29,
   job  : "soft eg",

   sayName: function(){
       console.log( this.name );
    }
};
         尽管可以随时为原型添加属性和方法,并且修改能够立即在所有的对象实例中反应出来,但如果是重写整个原型对象,那么情况就不一样了:调用构造函数会为实例添加一个指向最初原型的__proto__指针,而把原型修改为另一个对象就等于切断了构造函数与最初的原型对象之间的联系:实例中的指针仅仅指向原型,而不指向构造函数:
function Person(){

}

var p1 = new Person();

Person.prototype = {
   name : "Nico",
   age  : 29,
   job  : "soft eg",
   sayName: function(){
       console.log( this.name );
    }
};

console.log(p1.__proto__);
var p2 = new Person();
console.log(p2.__proto__);
         原型模式的重要性不仅体现在自定义类型方面,就连所有的引用类型,都是按照这种方式创建的,通过原生对象的原型,不仅可以取得所有默认方法的引用,而且可以定义新方法:
String.prototype.startWith =function(text){
         returnthis.indexOf(text) == 0;
}

var msg = "hello world";
console.log(msg.startWith("hello"));

原型对象的问题:
原型模式的缺点在于:它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认的情况下都取得相同的属性值。原型模式的最大问题是由其共享的本性导致的:
function Person(){

}

Person.prototype = {
   name : "Nico",
   age  : 29,
   job  : "soft eg",
   friends: ["f1", "f2"],
   sayName: function(){

       console.log( this.name );
    }

}

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

p1.friends.push("f3");

console.log(p1.friends);
console.log(p2.friends);
基于以上原因,很少单独使用原型模式。比较常用的方式是组合使用构造函数模式和原型模式:
function Person( name, age, job ){
   this.name = name;
   this.job = job;
   this.age = age;
   this.friends = [ ];
}

Person.prototype = {
    constructor: Person,
     sayName : function(){
         console.log( this.name );
     }
}

var p1 = new Person("Nico", 29,"soft eg");
var p2 = new Person("Gego", 25,"doctor");

p1.friends.push("f1","f2");
p2.friends.push("f2","f3","f4");

console.log(p1.friends);
console.log(p2.friends);
这个实例中,所有的实例属性都是在构造函数中定义的,而所有实例共享的属性则是在原型中定义的。这种构造函数与原型混合而成的模式,是目前ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方式。
动态原型模式:
         可以通过判断某个应该存在的方法是否有效,来决定是否需要初始化原型:
function Person( name, age, job ){
   this.name = name;
   this.job = job;
   this.age = age;

   if( typeof this.sayName != "function" ){
       Person.prototype.sayName = function(){
           console.log( this.name );
       }

    }
}

var p1 = new Person("Nico", 29,"soft eg");
p1.sayName();
寄生构造函数模式:
         这种模式的基本思想与构造函数相似,不同的是创建对象的方式:
function Person( name, age, job ){
   var o = new Object();
   o.name = name;
   o.job = job;
   o.age = age;

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

   return o;

}

var p1 = new Person("Nico", 29,"soft eg");

p1.sayName();
寄生构造函数模式返回的对象与构造函数或者构造函数的原型属性之间没有关系,也就是说,寄生构造函数返回的对象与构造函数外部创建的对象没什么不同,不能依赖于instanceof来确定对象类型。
稳妥构造函数模式:
         稳妥对象,指的是没有公共属性,而且方法也不引用this的对象,稳妥对象使用于一些安全的环境中。稳妥对象与寄生构造函数有两点不同:一是创建对象的实例方法不引用this,二是不适用new操作符调用构造函数:
function Person( name, age, job ){
   var o = new Object();
   o.sayName = function(){
       console.log( name );
   };
   return o;
}

var p1 = Person("Nico", 29,"soft");
var p2 = Person("cc",   22, "soft");
p1.sayName();
p2.sayName();
继承:
         许多OO语言都支持两种继承方式,接口继承和实现继承,接口继承只继承方法签名,实现继承则继承实际的方法。由于ECMA中函数没有签名,因此ECMAScript中无法实现接口继承,只支持实现继承,这是通过原型链来实现的。
         ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方法,其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象包含一个指向构造函数的指针(constructor),而实例则包含一个指向原型对象的内部指针。因此,如果让原型对象等于另一个类型的实例,结果是此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含了指向另一个构造函数的指针。如此层层递进,就构成了实例与原型的链条,这就是所谓原型链的基本概念。
         实现原型链的基本模式:
function SuperType(){
   this.name = "super";
}

SuperType.prototype.getSuperValue =function(){
   console.log( this.name) ;
};

function SubType(){
   this.subName = "sub";
}

SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function(){
   console.log( this.subName );
}

var instance = new SubType();
instance.getSuperValue();
instance.getSubValue();

console.log(instance instanceof Object);
console.log(instance instanceof SuperType);
console.log(instance instanceof SubType);
实现的本质是重写原型对象,代之以一个新类型的实例。原来存在于SuperType实例中的所有属性和方法,现在也存在于SubType.prototype中了。这个例子中的实例以及构造函数和原型之间的关系如下:
通过实现原型链,本质上扩展了前面接扫的原型搜索机制。当读取一个实例属性时,首先会在实例中搜索该属性,如果没有找到该属性,就会继续搜索实例的原型,在通过原型链实现继承的情况下,搜索过程就会沿着原型链继续向上。上面的过程:搜索实例、搜索SubType.prototype, 搜索SuperType.prototype.。
         事实上,上面介绍的原型链还缺少一环,所有的引用类型都继承自Object,而这个继承也是通过原型链实现的。因此,所有的函数的默认原型都是Object的实例,因此默认原型都包含一个内部指针,指向Object.prototype,这也是所有的自定义类型默认都会继承toString, valueOf()等默认方法的根本原因.一句话:SubType继承了SuperType,而SuperType继承了Object,当调用instance.toString时,实际上调用的是保存在Object.prototype中的那个方法。
         确定原型和实例的关系:instanceof操作符和isPrototypeOf函数。
         子类型有时需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后(即SubType.protoType = new SuperType()之后)。而且通过原型链实现继承时,不能使用对象字面量创建原型方法,因为这样做为重写原型链。
         原型链的两个问题:(1)引用类型属性的共享问题。(2)不能向超类型的构造函数中传递参数。由于上述两个原因,实践中很少单独使用原型链。
借用构造函数:
         这种技术的基本思想非常简单,子子类型的构造函数内部调用超类型构造函数。函数只不过是在特定环境中执行代码的对象,因此可以通过apply和call方法在新创建的对象上执行构造函数:
function Super(){
   this.colors = ["red", "green", "blue"];
}

function Sub(){
   Super.call(this);
}

var p1 = new Sub();
p1.colors.push("yellow");
console.log(p1.colors);

var p2 = new Sub();
console.log(p2.colors);
组合继承:组合继承也叫伪经典继承,试讲原型链和构造函数的技术组合在一起,发挥两者之长。基本思路是使用原型链实现对原型属性和方法的继承,而通过构造函数实现对实例属性的继承。这样,即通过原型上定义方法实现了函数复用,又能保证每个实例都有自己的属性:
function Super( name ){
   this.name = name;
   this.colors = ["red", "blue"];
}

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

function Sub(name , age){
   Super.call(this, name);
   this.age = age;
}

Sub.prototype = new Super();
Sub.prototype.sayAge = function(){
   console.log( this.age );
}

var p1 = new Sub("nico", 29);
p1.colors.push("green");
p1.sayName();
p1.sayAge();
组合继承是javascript中最常用的继承模式。
寄生组合式继承时引用类型最理想的继承范式:
function obj( o ){
   function F(){}
   F.prototype = o;
   return new F();
}

function inherit(Sub, Super){
   var proto = obj( Super.prototype );
   proto.constructor = Sub;
   Sub.prototype = proto;
}

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

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

function Sub(name, age){
   Super.call( this, name );

   this.age = age;
}

inherit(Sub, Super);

Sub.prototype.sayAge = function(){
   console.log( this.age );
}
.原型链
ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
可以通过下列方法构造原型链:
        function SuperType(){
            this.property = true;
        }
       
        SuperType.prototype.getSuperValue = function(){
            return this.property;
        };
       
        function SubType(){
            this.subproperty = false;
        }
       
        //inherit from SuperType
        SubType.prototype = new SuperType();
       
        SubType.prototype.getSubValue = function (){
            return this.subproperty;
        };
       
        var instance = new SubType();
        alert(instance.getSuperValue());   //true
可以通过两种方式来确定原型和实例之间的关系:
instanceof
alert(instance instanceof Object);      //true
alert(instance instanceof SuperType);   //true
alert(instance instanceof SubType);     //true
或者是isProtoyepeOf
alert(Object.prototype.isPrototypeOf(instance));    //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance));   //true
原型链的问题:
        function SuperType(){
            this.colors = ["red", "blue", "green"];
        }

        function SubType(){           
        }
       
        //inherit from SuperType
        SubType.prototype = new SuperType();

        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,black"
之前也提到过,引用类型的话会被共享,恩可以这样说吧?

原型链的第二个问题是:在创建子类型的实例是,不能像超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。有鉴于此,再加上前面刚刚讨论股偶的由于原型中包含引用类型值所带来的问题,实践中很少会单独使用原型链。

2.借用构造函数
constructor stealing的技术,有时候也叫作伪造对象或者经典继承。这种技术的基本思想是在子类型构造函数内部调用超类型构造函数。如:
function SuperType(){
            this.colors = ["red", "blue", "green"];
        }

        function SubType(){ 
            //inherit from 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"
代码中SuperType.call(this)这行代码借调了超类型的构造函数。通过使用call()方法,或者apply()方法也可以,我们实际上是在(未来将要)新创建的SubType实例环境下调用了SuperType构造函数。这样一来,就会在新SubType对象上执行SuperType()函数中定义的所有对象初始化代码。结果,SubType的每个实例就都会具有自己的colors属性的副本了。

相对于原型链而言,借用构造函数有一个很大的优势,可以再子类型构造函数中向超类型构造函数传递参数。
function SuperType(name){
            this.name = name;
        }

        function SubType(){ 
            //inherit from SuperType passing in an argument
            SuperType.call(this, "Nicholas");
           
            //instance property
            this.age = 29;
        }

        var instance = new SubType();
        alert(instance.name);    //"Nicholas";
        alert(instance.age);     //29
constructor stealing的问题在于,无法避免构造函数模式存在的问题:方法都在构造函数中定义,因此函数复用就无从谈起了。而且在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。

3.组合继承
combination inheritance,有时候也叫伪经典继承,指的是将原型链和借用构造函数的技术结合到一起,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
        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.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

4.原型式继承
借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。为了达到这个目的,他给出了如下函数
function object(o){
  function F(){}
  F.prototype=o;
  return new F();
}
在object函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后反悔了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。
        function object(o){
            function F(){}
            F.prototype = o;
            return new F();
        }
       
        var person = {
            name: "Nicholas",
            friends: ["Shelby", "Court", "Van"]
        };
       
        var anotherPerson = object(person);
        anotherPerson.name = "Greg";
        anotherPerson.friends.push("Rob");
       
        var yetAnotherPerson = object(person);
        yetAnotherPerson.name = "Linda";
        yetAnotherPerson.friends.push("Barbie");
       
        alert(person.friends);   //"Shelby,Court,Van,Rob,Barbie"
这种原型式继承,要求你必须有一个对象可以作为另一个对象的基础,如果有这么一个对象的话,可以把它传递给object()函数,然后再根据具体需求对得到的对象加以修改即可。
ECMAScript5通过新增Object.create()方法规范花了原型式继承,这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()方法和object()方法的行为相同。
        var person = {
            name: "Nicholas",
            friends: ["Shelby", "Court", "Van"]
        };
       
        var anotherPerson = Object.create(person);
        anotherPerson.name = "Greg";
        anotherPerson.friends.push("Rob");
       
        var yetAnotherPerson = Object.create(person);
        yetAnotherPerson.name = "Linda";
        yetAnotherPerson.friends.push("Barbie");
       
        alert(person.friends);   //"Shelby,Court,Van,Rob,Barbie"


5.寄生式继承
 寄生式(parasitic)继承是与原型式继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是他做了所有工作一样返回对象。一下代码示范了寄生式继承模式。
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:"asd",
  friends:["q","w","e"]
};

var anotherPerson=createAnother(person);
anotherPerson.sayHi();

6.寄生组合式继承
寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非是超类型原型的一个副本而已。本质上们就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式的基本模式如下:
function inheritPrototype(subType,superType){
  var prototype=object(superType.prototype);//创建对象
  prototype.constructor=subType;//增强对象
  subType.prototype=prototype;//指定对象
}

        function object(o){
            function F(){}
            F.prototype = o;
            return new F();
        }
   
        function inheritPrototype(subType, superType){
            var prototype = object(superType.prototype);   //create object
            prototype.constructor = subType;               //augment object
            subType.prototype = prototype;                 //assign object
        }
                               
        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);
        };
       
        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
这个例子的高效率性在于他只调用了一次SuperType构造函数,并且因此避免了在SubType.protorype上面创建不必要的、多余的属性。榆次同时,原型链还能保持不变。
面向对象的程序设计
1. 理解对象
每个对象都是基于引用类型创建.一般创建对象的方式如下:
var person = {
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",

    sayName : function(){
        alert(this.name);
    }
};
1. 属性类型
数据属性:包含一个数据值的位置,在此位置上可以读取和写入值.它具有以下四个特性:
[[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性.
[[Enumerable]]:表示能否通过for-in循环返回属性.
[[Writable]]:表示能否修改属性的值.
[[Value]]:包含这个属性的值.
而我们可以通过Object.defineProperty()方法来修改属性默认的特性.它接收三个参数:属性所在的对象,属性的名字和一个描述符对象.而描述符对象必须是configurable,enumerable,writable和value.
var person = {};
Object.defineProperty(person, "name", {
    writable : false,       //说明不可写
    value : "Nicholas"
});

print(person.name);     //输出:Nicholas
person.name = "voler";  //不可更改
print(person.name);     //输出:Nicholas
特性 Configurable很特殊,一旦修改为false,则无法再次变为可配置(即无法再次设定为true了):
<html><head><meta charset="utf-8" />
<script type="text/javascript">
function print(str) {
    document.write(str + "<br />");
}

var person = {};
Object.defineProperty(person, "name", {
    writable : true,        //说明可写
    configurable : false,
    value : "Nicholas"
});

//writable属性
print(person.name);     //输出:Nicholas
person.name = "voler";  //可更改
print(person.name);     //输出:voler

//configurable属性
delete person.name;     //不可删除
print(person.name);     //输出:voler
try {
    Object.defineProperty(person, "name", {
    configurable : true,
    value : "Nicholas"
    });
} catch(err) {
    print(err);     //输出:TypeError: Cannot redefine property: name
}

</script></head></html>
访问器属性:不包含数据值,但是通过get/set函数来操作数据,通过设定属性名来存储数据.它具有[[Configurable]],[[Enumerable]],[[Get]]:读取数据,[[Set]]:写入数据.设置一个属性导致其他属性发生变化.
var book = {
    _year : 2004,
    edition : 1
};

//通过设定year属性名来操作对象
Object.defineProperty(book, "year",{
    get : function() {
        print("3");
        return this._year;
    },
    set : function(newValue) {
        print("1");
        if (newValue > 2004) {
            this._year = newValue;
            this.edition += newValue - 2004;
        }
    }
});

//对属性year的赋值,实际上会调用set函数
book.year = 2005;
print("2");
//而读取year属性,实际上会调用get函数--通过set/get函数,可以达到数据的私有化
print(book.year);
浏览器显示如下:


2. 定义多个属性
通过Object.defineProperties()方法达到.
<script type="text/javascript">
function print(str) {
    document.write(str + "<br />");
}

var book = {};

Object.defineProperties(book, {
    _year: {
        value: 2004,
        writable: true  //这里必须设置writable属性:否则默认无法进行修改,在定义多个属性的情况下
    },
    edition: {
        value: 1
    },
    year: {
        get: function() {
            print("get");
            return this._year;
        },
        set: function(newValue) {
            print("set");
            if (newValue > 2004) {
                this._year = newValue;
                this.edition += newValue - 2004;
            }
        }
    }
});

book.year = 2006;
print(book.year);   //2006
</script>
3. 读取属性的特性
通过Object.getOwnPropertyDescriptor()方法达到.
<html><head><meta charset="utf-8" />
<script type="text/javascript">
function print(str) {
    document.write(str + "<br />");
}

var book = {};

Object.defineProperties(book, {
    _year : {
        value : 2004
    },
    edition : {
        value : 1
    },
    year : {
        get : function() {
            return this._year;
        },
        set : function(newValue) {
            if (newValue > 2004) {
                this._year = newValue;
                this.edition += newValue - 2004;
            }
        }
    }
});
debugger;
var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
print(descriptor.value);            //2004
print(descriptor.configurable);     //false
print(typeof descriptor.get);       //undefined

//对于访问器属性,是没有value的,所以不能通过descriptor.get来得到值
var descriptor = Object.getOwnPropertyDescriptor(book, "year");
print(descriptor.value);            //undefined
print(descriptor.enumerable);       //false
print(typeof descriptor.get);       //function
</script></head></html>
2. 创建对象
1. 工厂模式
用函数来封装以特定接口创建对象的细节:
function createPerson(name, age, job) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function() {
        print(this.name);
    };
    return o;
}

var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");
2. 构造函数模式
创建自定义的构造函数,从而定义自定义对象类型的属性和方法:
//这里this对应new出来的对象
function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function() {
        print(this.name);
    }
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
与之前的工厂模式不同之处在于:

1. 没有显式的创建对象.
2. 直接将属性和方法赋给了this对象.
3. 没有return语句.
而创建Person的新实例,需要使用new操作符.当调用new的时候,会经历以下四个步骤:
1. 创建一个新对象.
2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
3. 执行构造函数中的代码(为这个新对象添加属性)
4. 返回新对象.
而创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,这正是构造函数胜过工厂模式的地方.
print(person1 instanceof Object);  //true
print(person2 instanceof Person);   //true
print(person1 instanceof Object);   //true
print(person2 instanceof Person);   //true
1. 将构造函数当作函数
构造函数和一般函数没什么区别.函数只要用new操作符来调用,就是构造函数,否则就是一般函数:
//这里this对应new出来的对象
function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function() {
        print(this.name);
    }
}

//this--->person
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName();   //Nicholas

//this--->window
Person("Greg", 27, "Doctor");
window.sayName();   //Greg
sayName();          //Greg

var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName();        //Kristen
2. 构造函数的问题
每个方法都要在每个实例上重新创建一遍.
3. 原型模式
我们创建的每个函数都有一个prototype(原型属性),这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法.
function Person(){}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Enginner";
Person.prototype.sayName = function(){
    print(this.name);
};

var person1 = new Person();
person1.sayName();  //Nicholas

var person2 = new Person();
person2.sayName();  //Nicholas
这里在firebug上有一个很有启发性的调试:

1. 理解原型对象
无论什么时候,只要创建一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象.在默认情况下,所有原型对象都会自动获得一个constructor属性,这个属性包含一个指向prototype属性所在函数的指针.
person1.constructor.prototype === Person.prototype
person2.constructor === person1.constructor

而isPrototypeOf()和getPrototypeOf()方法用来确定原型对象和实例之间的关系:
print(Person.prototype.isPrototypeOf(person1));    //true
print(Person.prototype.isPrototypeOf(person2)); //true
print(Object.getPrototypeOf(person1) == Person.prototype);  //true
print(Object.getPrototypeOf(person1).name);     //Nicholas
虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值.如果我们向实例中添加一个属性,而该属性与实例原型中的一个属性同名,那我们会屏蔽原属性.我们可以用delete来删除掉添加的属性.
<html><head><meta charset="utf-8" />
<script type="text/javascript">
function print(str) {
    document.write(str + "<br />");
}

function Person(){}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Enginner";
Person.prototype.sayName = function(){
    print(this.name);
};

var person1 = new Person();
var person2 = new Person();

//屏蔽掉原属性
person1.name = "Greg";
print(person1.name);    //Greg
print(person2.name);    //Nicholas

//删除添加的属性
delete person1.name;
print(person1.name);    //Nicholas

</script></head></html>
关系如下:


2. 原型与in操作符
in操作符会在通过对象能够访问给定属性时返回true.
<html><head><meta charset="utf-8" />
<script type="text/javascript">
function print(str) {
    document.write(str + "<br />");
}

//hasOwnProperty只有在实例中才返回true
function hasPrototypeProperty(object, name) {
    return !object.hasOwnProperty(name) && (name in object);
}

function Person(){}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Enginner";
Person.prototype.sayName = function(){
    print(this.name);
};

//函数原型中定义name
var person = new Person();
print(hasPrototypeProperty(person, "name"));    //true

//函数实例中定义name
person.name = "Greg";
print(hasPrototypeProperty(person, "name"));    //false

</script></head></html>
而我们可以通过Object.keys()方法来枚举实例属性:
function Person(){}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Enginner";
Person.prototype.sayName = function(){
    print(this.name);
};

var keys = Object.keys(Person.prototype);
print(keys);    //name,age,job,sayName
3. 更简单的原型语法
function Person(){}

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

var person1 = new Person();
这里要注意一点是:constructor属性不再指向Person,而是指向Object.是因为赋值会断开原型链,所以constructor指向新的对象Person.prototype,而此对象为Object类型.


而更具体的解释如下:
<script type="text/javascript">
function print(str) {
    document.write(str + "<br />");
}

function Person(){}

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

var person1 = new Person();

function Func(){}
Func.prototype.name = "lcj";
var person2 = new Func();
</script>
firebug的调试如下:

这里对Person.prototype = ***;的赋值操作,实际上可以理解为继承:
<script type="text/javascript">
function print(str) {
    document.write(str + "<br />");
}

function Person(){}

function tempObject(){};
tempObject.prototype.name = "Nicholas";
tempObject.prototype.age = 29;
tempObject.prototype.job = "Software Engineer";
tempObject.prototype.sayName = function() {
    print(this.name);
};

Person.prototype = new tempObject();

var person1 = new Person();

</script>
这里,tempObject模拟了所有对象的原始父类:Object对象.firebug调试如下:

4. 原型的动态性
实例和原型之间通过prototype指针连接.所以原型的任何修改会时时反应到实例上:
var friend = new Person();
Person.prototype.sayHi = function(){
    print("Hi");
};
friend.sayHi(); //输出:Hi
但是,如果重写原型对象.由于断开了实例和原型之间的关联,则以下代码会出错:
function Person(){}

var friend = new Person();

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

friend.sayName();       //error

5. 原型对象的问题
共享性:
function Person(){}

var friend = new Person();

Person.prototype = {
    constructor : Person,
    name : "Nicholas",
    age : 29,
    job : "Software Enginner",
    friend : ["Shelby", "Court"],
    sayName : function(){
        print(this.name);
    }
};

var person1 = new Person();
var person2 = new Person();

person1.friend.push("Van");
print(person1.friend);  //Shelby,Court,Van
print(person2.friend);  //Shelby,Court,Van
4. 组合使用构造函数模式和原型模式
构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性.结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用.
<html><head><meta charset="utf-8" />
<script type="text/javascript">
function print(str) {
    document.write(str + "<br />");
}

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelly", "Court"];
}

Person.prototype = {
    constructor : Person,
    sayName : function() {
        print(this.name);
    }
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

person1.friends.push("Van");
print(person1.friends);     //Shelly,Court,Van
print(person2.friends);     //Shelly,Court
print(person1.sayName == person2.sayName);  //true

</script></head></html>
5. 动态原型模型
动态原型模型把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点.
function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby", "Court"];

    //只有在sayName不存在的情况下,执行sayName的初始化
    if (typeof this.sayName != "function") {
        Person.prototype.sayName = function() {
            alert(this.name);
        };
    }
}
6. 寄生构造函数模型
思想为创建一个函数,作用仅仅是封装创建对象的代码,然后返回新创建的对象--除了new之外,和工厂函数没什么区别.
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 person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = Person("Nicholas", 29, "Software Engineer");
但是这里有一个疑问是:person1 === person2明显为false,但是它们不同的地方在哪里?
7. 稳妥构造函数模式
稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象:
function Person(name, age, job) {
    var o = new Object();
    o.sayName = function() {
        alert(name);
    };

    return o;
}
这里直接对传入的参数进行操作,所以没有公共属性,也没不引用this对象.
3. 继承
1. 原型链
构造函数,原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针.我们让原型对象包含一个指向构造函数的指针,而实例包含一个指向原型对象的内部指针,这就是原型链的继承方式.
function SuperType() {
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
};

function SubType() {
    this.subproperty = false;
}

SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function() {
    return this.subproperty;
};

var instance = new SubType();
print(instance.getSuperValue());    //true
//这时候,构造函数为SuperType,而非SubType
print(instance.constructor);        //function SuperType() { this.property = true; }



而完整的继承图应该包含Object:

而我们可以通过instanceof操作符和isPrototypeOf方法来测试原型和实例之间的关系:
//通过instanceof来确定原型和实例的关系
print(instance instanceof Object);  //true
print(instance instanceof SuperType);   //true
print(instance instanceof SubType); //true

//通过isPrototypeOf来确定原型和实例的关系
print(Object.prototype.isPrototypeOf(instance));    //true
print(SuperType.prototype.isPrototypeOf(instance)); //true
print(SubType.prototype.isPrototypeOf(instance));   //true
我们需要谨慎的定义方法:给原型(这里为继承的超类型SuperType)添加的方法必须在替换原型的语句(SubType.prototype = new SuperType)的后面,因为原型链为:SubType的实例指向SubType.prototype原型,而 SubType.prototype原型包含一个SuperType的实例(new SuperType),而此实例又指向SuperType的原型(SuperType.prototype).所以,为定义超类(SuperType)的实例之前,为其添加方法是错误的:(但是我觉得这里有点莫名其妙!):
function SuperType() {
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
};

function SubType() {
    this.subproperty = false;
}

SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function() {
    return this.subproperty;
};

//添加新方法
SubType.prototype.getSubValue = function() {
    return this.subproperty;
};

var instance = new SubType();

//重写超类中的方法
SubType.prototype.getSuperValue = function() {
    return "hello";
};

var instance = new SubType();
print(instance.getSubValue());      //false
print(instance.getSuperValue());    //hello
之前自己对函数有一定的认知误区,目前总结如下:
1. 只有对象才能添加其属性和方法.
2. 函数本身是一个对象,函数名仅仅指向此函数,并不能通过函数名来添加属性和方法(即函数名并不是一个对象)
3. 要给函数添加方法,要么通过具有对象的函数原型(FUNC.prototype),要么对new FUNC()出来的对象进行添加.所以以下代码很有参考作用:
function SuperType() {
    this.property = true;
    this.show1 = function() {
        print("show1 function");
    };
}

//不能通过SuperType函数名来添加方法
SuperType.show2 = function() {
    print("show2 function");
};

var superInstance = new SuperType();
superInstance.show3 = function() {
    print("show3 function");
};

superInstance.show1();
//superInstance.show2();    //-此处报异常,show2非函数
superInstance.show3();
但是,原型链有以下两个问题:1.共享机制(跟模型的原理一致)
function SuperType() {
    this.colors = ["red", "blue", "green"];
}

function SubType() {
}

SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
print(instance1.colors);        //red,blue,green,black

var instance2 = new SubType();
print(instance2.colors);        //red,blue,green,black
第二个问题是:不能向超类中传递参数.
2, 借用构造函数
在子类型构造函数的内部调用超类型构造函数(但这种方法谈不上复用,因此很少使用)
function SuperType(name) {
    this.colors = ["red", "blue", "green"];
    this.name = name;

}

function SubType() {
    SuperType.call(this, "Nicholas");
    this.age = 29;
}

SubType.prototype = new SuperType();//此句没有任何的作用,删除也可以

//不共享原型,每个实例均有自己的原型副本
var instance1 = new SubType();
instance1.colors.push("black");
print(instance1.colors);        //red,blue,green,black

var instance2 = new SubType();
print(instance2.colors);        //red,blue,green

print(instance1.name);          //Nicholas
print(instance1.age);           //29
3. 组合继承
组合继承,也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合在一起.其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承.这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性:
function SuperType(name) {
    this.colors = ["red", "blue", "green"];
    this.name = name;
}

SuperType.prototype.sayName = function() {
    print(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() {
    print(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
print(instance1.colors);        //red,blue,green,black
instance1.sayName();            //Nicholas
instance1.sayAge();             //29

var instance2 = new SubType("Greg", 27);
print(instance2.colors);        //red,blue,green
instance2.sayName();            //Greg
instance2.sayAge();             //27
4. 原型式继承
借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型:
function object(o) {
    function F(){}
    F.prototype = o;
    return new F();
}
例子如下:
function object(o) {
    function F(){}
    F.prototype = o;
    return new F();
}

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

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

print(person.friends);  //Shelby,Court,Van,Rob,Barbie
而ECMAScript5新增Object.create()方法,接收两个参数:一个用作新对象原型的对象和一个为新对象定义额外属性的对象.
var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的.以这种方式指定的任何属性都会覆盖原型对象上的同名属性:
var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};

//通过此方法会重新定义name的属性,所以如果name中不编写value: "true",则调用anotherPerson.name为undefined
var anotherPerson = Object.create(person, {
    name: {
        value: "true",
        writable: false
    }
});
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

print(person.friends);  //Shelby,Court,Van,Rob,Barbie
print(anotherPerson.name);  //true
5. 寄生式继承
创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象:
function object(o) {
    function F(){}
    F.prototype = o;
    return new F();
}

function createAnother(original) {
    var clone = object(original);
    clone.sayHi = function() {
        print("Hi");
    };

    return clone;
}

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

var anotherPerson = createAnother(person);
anotherPerson.sayHi();
2. 函数表达式
1, 函数的两种声明方式
function functionName(arg0, arg1, arg2) {
    //函数体
}

var functionName = function(arg0, arg1, arg2) {
    //函数体
};
2. 递归
经典的但是错误的递归函数(在javascript是错误的):
function factorial(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * factorial(num - 1);
    }
}

var anotherFactorial = factorial;
factorial = null;
print(anotherFactorial(4)); //出错!!
而我们应该使用arguments.callee来指向正在执行的函数的指针.
function factorial(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * arguments.callee(num - 1);
    }
}

var anotherFactorial = factorial;
factorial = null;
print(anotherFactorial(4)); //24
而我们通过函数表达式可以优化设计:
var factorial = (function f(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * f(num - 1);
    }
});

print(factorial(4));        //24
var anotherFactorial = factorial;
factorial = null;
print(anotherFactorial(4)); //24
3. 闭包
闭包指的是有权访问另一个函数作用域中的变量的函数.创建闭包的常用方式,就是在一个函数内部创建另一个函数:
function createComparisonFunction(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;
        }
    };
}

var compare = createComparisonFunction("name");
var result = compare({name : "Nicholas"}, {name : "Greg"});
print(result);  //1
作用域链如下:


从上图可以看出,闭包的对象并不会被销毁.闭包的对象会一直存在于声明它的实例中(在compare实例),直到手动设置为null.
//创建函数
var compare = createComparisonFunction("name");
//调用函数
var result = compare({name : "Nicholas"}, {name : "Greg"});
//解除对匿名函数的引用
compare = null;



作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值.闭包所保存的是整个变量对象,而不是某个特殊的变量.
function createFunctions() {
    var result = new Array();

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

    return result;
}

var result = createFunctions();
for (var i = 0; i < result.length; i++) {
    print(result[i]()); //输出10 10 10 10 10 10 10 10 10 10
}
而我们可以强制传递参数进行,让函数符合预期的结果:
function createFunctions() {
    var result = new Array();

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

    return result;
}

var result = createFunctions();
for (var i = 0; i < result.length; i++) {
    print(result[i]);   //输出0 1 2 3 4 5 6 7 8 9
}
而匿名函数的执行环境具有全局性:
var name = "The Window";

var object = {
    name : "My Object",
    getNameFunc : function() {
        return function() {
            //这里this是window
            return this.name;
        };
    }
};

print(object.getNameFunc()());  //The Window
我们可以修改如下:
var name = "The Window";

var object = {
    name : "My Object",
    getNameFunc : function() {
        //这里this是object
        var that = this;
        return function() {
            return this.name + "---" + that.name;
        };
    }
};

print(object.getNameFunc()());  //The Window---My Object
3. 模仿块级作用域
函数具有作用域,并且多次声明一个变量并不起作用:javascript对第一个声明有效,后续声明均视而不见.
function outputNumbers(count) {
    for (var i = 0; i < count; i++) {
        ;
    }
    var i;  //再次声明忽略不计
    print(i);
}

outputNumbers(10);  //10
而我们因此可以使用此特性编写私有作用域:
(function(){
    //这里是块级作用域
})();
4. 私有变量
javascript本身并没有私有变量一说.但是由于存在函数作用域,则在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量.私有变量包括函数参数,局部变量和在函数内部定义的其他函数:
function Person(name) {
    this.getName = function() {
        return name;
    };
    this.setName = function(value) {
        name = value;
    };
}

var person = new Person("Nicholas");
print(person.getName());    //Nicholas
person.setName("Greg");
print(person.getName());    //Greg
我们也可以定义静态私有变量:
(function(){
    var name = "";

    //未使用var,故为全局变量
    Person = function(value){
        name = value;
    };
    Person.prototype.getName = function(){
        return name;
    };
    Person.prototype.setName = function(value){
        name = value;
    };
})();

var person1 = new Person("Nicholas");
print(person1.getName());   //Nicholas
person1.setName("Greg");
print(person1.getName());   //Greg

var person2 = new Person("Michael");
print(person1.getName());   //Michael
print(person2.getName());   //Michael




工厂模式

虽然Object构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。为解决这个问题,人们开始使用
工厂模式的一种变体。


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

var person1=createPerson("Nicholas",29,"Software Engineer");
var person2=createPerson("Grey",27,"Doctor");

函数createPerson()能够根据接受的参数来构建一个包含所有必要信息的Person对象。可以无数次的调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。
工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
构造函数模式


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

var person1=new Person("Nicholas",29,"Software Engineer");
var person2=new Person("Grey",27,"Doctor");

在这个例子中,Person()函数取代了createPerson函数。我们注意到,Person()中的代码除了与createPerson()中相同的部分外,还存在以下不同之处:
(1):没有显式的创建对象;
(2):直接将属性和方法赋给了this对象;
(3):没有return语句
原型模式

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


function Person(){
}

Person.prototype.name="Nicholas";
Person.prototype.age=29;
Person.prototype.sayName=function(){
alert(this.name);
}

var person1=new Person();
person1.sayName(); //"Nicholas"

var person2=new Person();
person2.sayName(); //"Nicholas"

alert(person1.sayName==person2.sayName); //true


虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例
中创建该属性,该属性将会屏蔽原型中的属性。
原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何个修改都能够立即从实例上反映出来--即使先创建了实例后个性原型也照样如此。请看下面的例子:

var friend=new Person();
Person.prototype.sayHi=function(){
alert("HI");
}
friend.sayHi(); //"HI" (没有问题!)
以上代码先创建了Person的一个实例,并将其保存在person中,然后,下一条语句在Person.prototype中添加了一个方法sayHi().即使person实例是在添加新方法之前创建的,
但它仍然可以访问这个新方法。
尽管可以随时为原型添加属性和方法,并且个性能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例
添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型,而不指向构造
函数。看下面的例子:


function Person(){
}
var friend=new Person();

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

动态原型模式

可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。来看下面一个例子:


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

寄生构造函数模式

通常,在前述几种模式都不适用的情况下,可以使用寄生(parasitic)构造函数模式。这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回
新创建的对象;但从表面上看,这个函数又很像是典型的构造函数。下面是一个例子:


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"

在这个例子中,通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。
这个模式可以在特殊情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改Array构造函数,因此可以使用这个模式。


function SpeicalArray(){
var values=new Array(); //创建数组
values.push.apply(values,arguments); //添加值
values.toPipedString=function(){
return this.join("|");
}
return values; //返回数组
}
var colors=new SpeicalArray("red","blue","green");
alert(colors.toPipedString()); //"red|blue|green"

引言:
在ECMAScript中,不存在独立的函数,所有函数都必须是某个对象的方法。而函数也是一个对象。所有对象的属性可在对象创建后动态定义,早绑定(early binding)是指在实例化对象之前定义它的特性和方法,这样编译器或解释程序就能提前转换机器代码。ECMAScript不是强类型语言,所以不支持早绑定。
晚绑定(late binding)指的是编译器或解释程序在运行前,不知道对象的类型。
使用晚绑定,无需检查对象的类型,只需要检查对象是否支持特性和方法即可。ECMAScript中的所有变量都采用晚绑定方法,这样就允许执行大量的对象操作,而无任何惩罚。
构造对象的方式:
1、最普通构造对象方式:
var car=new Object;
car.color=”green”;
car.show=function (){alert(this.color)};
car.run();
2、工厂方式(工厂函数由于在ECMAScript中无法创建类,因此用函数封装以特定接口创建对象。其实现方法非常简单,也就是在函数内创建一个对象,给对象赋予属性及方法再将对象返回即可。)
function generateObj()
{
var obj=new Object;
obj.color=”green”;
obj.show=function (){alert(this.color)};
return obj;
}
var obj1=generateObj();
obj1.show();
var obj2=generateObj();
var obj2.show();
所有代码都包含在generateObj ()函数中,并返回obj对象作为(obj)函数值。
调用此函数时,将创建新对象,并赋予它所有必要的属性,复制出一个前面说明的obj对象。
使用该方法,可以容易地创建obj对象的两个版本(obj1和obj2),它们的属性完全一样。但是每次调用,每次调用函数generateObj (),都要创建新函数show(),意味着每个对象都有自己的show()方法,事实上,每个对象都共享了同一个函数。
优化后的工厂模式:
function generateObj(color,fn)
{
var obj=new Object
obj.color=color;
obj.show=fn;
return obj;
}
var show=function show(){alert(this.color)};

var obj1=generateObj("red",show);
obj1.show();
3、构造函数方式
var show= function(){alert(this.color);};
function GenerateObj(col){
this.color = col;
this.show =show;
}
var obj= new GenerateObj("red");
obj.show();
与工厂方式的区别在于在构造函数内部需创建对象,而是使用this关键字。
3、原型方式
function GenerateObj(){}
GenerateObj.prototype.color = "red";
GenerateObj.prototype.show = function(){alert(this.color);};
var obj = new GenerateObj();
obj.show();
该方式利用了对象的prototype属性,可把它看成创建新对象所依赖的原型。用空构造函数来设置类名,然后所有的属性和方法都被直接赋予prototype属性但是这种原型方式有一种致命的缺点,当属性指向的是对象时,对象也会被共享
function GenerateObj(){}
GenerateObj.prototype.colors = new Array("red","black");
var obj1 = new GenerateObj();
var obj2 = new GenerateObj();
obj1.colors.push("yellow");
alert(obj1.colors) //显示"red,black,yellow"
alert(obj2.colors) //显示"red,black,yellow"
4、混合型(构造函数和原型方式结合)
function GenerateObj(col){
this.color = col;
}
GenerateObj.prototype.show = function(){alert(this.color);};
var obj = new GenerateObj("red");
obj.show();
即用构造函数定义对象的所有非函数属性,用原型方式定义,对象的函数属性(方法)。
5、动态原型(结合prototype推荐)
function Car(col){
this.color = col;
if (typeof Car._initialized == "undefined"){
Car.prototype.show = function(){alert(this.color);};
Car._initialized = true;
}
}
var car = new Car("red");
其实动态原型方法的基本想法与混合的构造函数/原型方式相同,即在构造函数内定义非函数属性,而函数属性则利用原型属性定义。唯一的区别是赋予对象方法的位置,对属性和方法进行了视觉上的封装。该方法使用标志(_initialized)来判断是否已给原型赋予了任何方法。
也许是最优方案:
functon CreatePerson(name,sex,birthday)
{
  this.name =name;
  this.sex = sex;
  this.birthday = birthday;
}
CreatePerson.prototype.sayHi = function ()
{
  alert("Hi ! I am "+this.name);
}

var person1 = new CreatePerson('zs','boy','2001-02-03');
var person2 = new CreatePerson('ls','boy','2001-02-04');
person1.sayHi(); //outputs "Hi ! I am zs"
person2.sayHi(); //outputs "Hi ! I am ls"
一般情况下,一个对象或者类不只一个方法,需要多个方法配合使用,那么
CreatePerson.prototype={
  sayHi:function()
  {
    alert("Hi ! I am "+this.name);
  },
  walk:function()
  {
    alert("walk,walk");
  },
  ……
}
Javascript是一种基于对象(object-based)的语言,你遇到的所有东西几乎都是对象。但是,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有class(类)。
那么,如果我们要把"属性"(property)和"方法"(method),封装成一个对象,甚至要从原型对象生成一个实例对象,我们应该怎么做呢?
一、 生成对象的原始模式
假定我们把猫看成一个对象,它有"名字"和"颜色"两个属性。
  var Cat = {
    name : '',
    color : ''
  }
现在,我们需要根据这个原型对象的规格(schema),生成两个实例对象。
  var cat1 = {}; // 创建一个空对象
    cat1.name = "大毛"; // 按照原型对象的属性赋值
    cat1.color = "黄色";
  var cat2 = {};
    cat2.name = "二毛";
    cat2.color = "黑色";
好了,这就是最简单的封装了,把两个属性封装在一个对象里面。但是,这样的写法有两个缺点,一是如果多生成几个实例,写起来就非常麻烦;二是实例与原型之间,没有任何办法,可以看出有什么联系。
二、 原始模式的改进
我们可以写一个函数,解决代码重复的问题。
  function Cat(name,color){
    return {
      name:name,
      color:color
    }
  }
然后生成实例对象,就等于是在调用函数:
  var cat1 = Cat("大毛","黄色");
  var cat2 = Cat("二毛","黑色");
这种方法的问题依然是,cat1和cat2之间没有内在的联系,不能反映出它们是同一个原型对象的实例。
三、 构造函数模式
为了解决从原型对象生成实例的问题,Javascript提供了一个构造函数(Constructor)模式。
所谓"构造函数",其实就是一个普通函数,但是内部使用了this变量。对构造函数使用new运算符,就能生成实例,并且this变量会绑定在实例对象上。
比如,猫的原型对象现在可以这样写,
  function Cat(name,color){
    this.name=name;
    this.color=color;
  }
我们现在就可以生成实例对象了。
  var cat1 = new Cat("大毛","黄色");
  var cat2 = new Cat("二毛","黑色");
  alert(cat1.name); // 大毛
  alert(cat1.color); // 黄色
这时cat1和cat2会自动含有一个constructor属性,指向它们的构造函数。
  alert(cat1.constructor == Cat); //true
  alert(cat2.constructor == Cat); //true
Javascript还提供了一个instanceof运算符,验证原型对象与实例对象之间的关系。
  alert(cat1 instanceof Cat); //true
  alert(cat2 instanceof Cat); //true
四、构造函数模式的问题
构造函数方法很好用,但是存在一个浪费内存的问题。
请看,我们现在为Cat对象添加一个不变的属性"type"(种类),再添加一个方法eat(吃老鼠)。那么,原型对象Cat就变成了下面这样:
  function Cat(name,color){
    this.name = name;
    this.color = color;
    this.type = "猫科动物";
    this.eat = function(){alert("吃老鼠");};
  }
还是采用同样的方法,生成实例:
  var cat1 = new Cat("大毛","黄色");
  var cat2 = new Cat ("二毛","黑色");
  alert(cat1.type); // 猫科动物
  cat1.eat(); // 吃老鼠
表面上好像没什么问题,但是实际上这样做,有一个很大的弊端。那就是对于每一个实例对象,type属性和eat()方法都是一模一样的内容,每一次生成一个实例,都必须为重复的内容,多占用一些内存。这样既不环保,也缺乏效率。
  alert(cat1.eat == cat2.eat); //false
能不能让type属性和eat()方法在内存中只生成一次,然后所有实例都指向那个内存地址呢?回答是可以的。
五、 Prototype模式
Javascript规定,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。
这意味着,我们可以把那些不变的属性和方法,直接定义在prototype对象上。
  function Cat(name,color){
    this.name = name;
    this.color = color;
  }
  Cat.prototype.type = "猫科动物";
  Cat.prototype.eat = function(){alert("吃老鼠")};
然后,生成实例。
  var cat1 = new Cat("大毛","黄色");
  var cat2 = new Cat("二毛","黑色");
  alert(cat1.type); // 猫科动物
  cat1.eat(); // 吃老鼠
这时所有实例的type属性和eat()方法,其实都是同一个内存地址,指向prototype对象,因此就提高了运行效率。
  alert(cat1.eat == cat2.eat); //true
六、 Prototype模式的验证方法
为了配合prototype属性,Javascript定义了一些辅助方法,帮助我们使用它。,
6.1 isPrototypeOf()
这个方法用来判断,某个proptotype对象和某个实例之间的关系。
  alert(Cat.prototype.isPrototypeOf(cat1)); //true
  alert(Cat.prototype.isPrototypeOf(cat2)); //true
6.2 hasOwnProperty()
每个实例对象都有一个hasOwnProperty()方法,用来判断某一个属性到底是本地属性,还是继承自prototype对象的属性。
  alert(cat1.hasOwnProperty("name")); // true
  alert(cat1.hasOwnProperty("type")); // false
6.3 in运算符
in运算符可以用来判断,某个实例是否含有某个属性,不管是不是本地属性。
  alert("name" in cat1); // true
  alert("type" in cat1); // true
in运算符还可以用来遍历某个对象的所有属性。
  for(var prop in cat1) { alert("cat1["+prop+"]="+cat1[prop]); }

和方法,以及如何从原型对象生成实例。
今天要介绍的是,对象之间的"继承"的五种方法。
比如,现在有一个"动物"对象的构造函数。

  function Animal(){
    this.species = "动物";
  }
还有一个"猫"对象的构造函数。

  function Cat(name,color){
    this.name = name;
    this.color = color;
  }
怎样才能使"猫"继承"动物"呢?
一、 构造函数绑定
第一种方法也是最简单的方法,使用call或apply方法,将父对象的构造函数绑定在子对象上,即在子对象构造函数中加一行:
  function Cat(name,color){
    Animal.apply(this, arguments);
    this.name = name;
    this.color = color;
  }
  var cat1 = new Cat("大毛","黄色");
  alert(cat1.species); // 动物
二、 prototype模式
第二种方法更常见,使用prototype属性。
如果"猫"的prototype对象,指向一个Animal的实例,那么所有"猫"的实例,就能继承Animal了。
  Cat.prototype = new Animal();
  Cat.prototype.constructor = Cat;
  var cat1 = new Cat("大毛","黄色");
  alert(cat1.species); // 动物
代码的第一行,我们将Cat的prototype对象指向一个Animal的实例。
  Cat.prototype = new Animal();
它相当于完全删除了prototype 对象原先的值,然后赋予一个新值。但是,第二行又是什么意思呢?
  Cat.prototype.constructor = Cat;
原来,任何一个prototype对象都有一个constructor属性,指向它的构造函数。如果没有"Cat.prototype = new Animal();"这一行,Cat.prototype.constructor是指向Cat的;加了这一行以后,Cat.prototype.constructor指向Animal。
  alert(Cat.prototype.constructor == Animal); //true
更重要的是,每一个实例也有一个constructor属性,默认调用prototype对象的constructor属性。
  alert(cat1.constructor == Cat.prototype.constructor); // true
因此,在运行"Cat.prototype = new Animal();"这一行之后,cat1.constructor也指向Animal!
  alert(cat1.constructor == Animal); // true
这显然会导致继承链的紊乱(cat1明明是用构造函数Cat生成的),因此我们必须手动纠正,将Cat.prototype对象的constructor值改为Cat。这就是第二行的意思。
这是很重要的一点,编程时务必要遵守。下文都遵循这一点,即如果替换了prototype对象,
  o.prototype = {};
那么,下一步必然是为新的prototype对象加上constructor属性,并将这个属性指回原来的构造函数。
  o.prototype.constructor = o;
三、 直接继承prototype
第三种方法是对第二种方法的改进。由于Animal对象中,不变的属性都可以直接写入Animal.prototype。所以,我们也可以让Cat()跳过 Animal(),直接继承Animal.prototype。
现在,我们先将Animal对象改写:
  function Animal(){ }
  Animal.prototype.species = "动物";
然后,将Cat的prototype对象,然后指向Animal的prototype对象,这样就完成了继承。
  Cat.prototype = Animal.prototype;
  Cat.prototype.constructor = Cat;
  var cat1 = new Cat("大毛","黄色");
  alert(cat1.species); // 动物
与前一种方法相比,这样做的优点是效率比较高(不用执行和建立Animal的实例了),比较省内存。缺点是 Cat.prototype和Animal.prototype现在指向了同一个对象,那么任何对Cat.prototype的修改,都会反映到Animal.prototype。
所以,上面这一段代码其实是有问题的。请看第二行
  Cat.prototype.constructor = Cat;
这一句实际上把Animal.prototype对象的constructor属性也改掉了!
  alert(Animal.prototype.constructor); // Cat
四、 利用空对象作为中介
由于"直接继承prototype"存在上述的缺点,所以就有第四种方法,利用一个空对象作为中介。
  var F = function(){};
  F.prototype = Animal.prototype;
  Cat.prototype = new F();
  Cat.prototype.constructor = Cat;
F是空对象,所以几乎不占内存。这时,修改Cat的prototype对象,就不会影响到Animal的prototype对象。
  alert(Animal.prototype.constructor); // Animal
我们将上面的方法,封装成一个函数,便于使用。
  function extend(Child, Parent) {

    var F = function(){};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
    Child.uber = Parent.prototype;
  }
使用的时候,方法如下
  extend(Cat,Animal);
  var cat1 = new Cat("大毛","黄色");
  alert(cat1.species); // 动物
这个extend函数,就是YUI库如何实现继承的方法。
另外,说明一点,函数体最后一行
  Child.uber = Parent.prototype;
意思是为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。(uber是一个德语词,意思是"向上"、"上一层"。)这等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。
五、 拷贝继承
上面是采用prototype对象,实现继承。我们也可以换一种思路,纯粹采用"拷贝"方法实现继承。简单说,如果把父对象的所有属性和方法,拷贝进子对象,不也能够实现继承吗?这样我们就有了第五种方法。
首先,还是把Animal的所有不变属性,都放到它的prototype对象上。
  function Animal(){}
  Animal.prototype.species = "动物";
然后,再写一个函数,实现属性拷贝的目的。
  function extend2(Child, Parent) {
    var p = Parent.prototype;
    var c = Child.prototype;
    for (var i in p) {
      c[i] = p[i];
      }
    c.uber = p;
  }
这个函数的作用,就是将父对象的prototype对象中的属性,一一拷贝给Child对象的prototype对象。
使用的时候,这样写:
  extend2(Cat, Animal);
  var cat1 = new Cat("大毛","黄色");
  alert(cat1.species); // 动物

今天是最后一个部分,介绍不使用构造函数实现"继承"。
一、什么是"非构造函数"的继承?
比如,现在有一个对象,叫做"中国人"。
  var Chinese = {
    nation:'中国'
  };
还有一个对象,叫做"医生"。
  var Doctor ={
    career:'医生'
  }
请问怎样才能让"医生"去继承"中国人",也就是说,我怎样才能生成一个"中国医生"的对象?
这里要注意,这两个对象都是普通对象,不是构造函数,无法使用构造函数方法实现"继承"。
二、object()方法
json格式的发明人Douglas Crockford,提出了一个object()函数,可以做到这一点。
  function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
  }
这个object()函数,其实只做一件事,就是把子对象的prototype属性,指向父对象,从而使得子对象与父对象连在一起。
使用的时候,第一步先在父对象的基础上,生成子对象:
  var Doctor = object(Chinese);
然后,再加上子对象本身的属性:
  Doctor.career = '医生';
这时,子对象已经继承了父对象的属性了。
  alert(Doctor.nation); //中国
三、浅拷贝
除了使用"prototype链"以外,还有另一种思路:把父对象的属性,全部拷贝给子对象,也能实现继承。
下面这个函数,就是在做拷贝:
  function extendCopy(p) {
    var c = {};
    for (var i in p) {
      c[i] = p[i];
    }
    c.uber = p;
    return c;
  }
使用的时候,这样写:
  var Doctor = extendCopy(Chinese);
  Doctor.career = '医生';
  alert(Doctor.nation); // 中国
但是,这样的拷贝有一个问题。那就是,如果父对象的属性等于数组或另一个对象,那么实际上,子对象获得的只是一个内存地址,而不是真正拷贝,因此存在父对象被篡改的可能。
请看,现在给Chinese添加一个"出生地"属性,它的值是一个数组。
  Chinese.birthPlaces = ['北京','上海','香港'];
通过extendCopy()函数,Doctor继承了Chinese。
  var Doctor = extendCopy(Chinese);
然后,我们为Doctor的"出生地"添加一个城市:
  Doctor.birthPlaces.push('厦门');
发生了什么事?Chinese的"出生地"也被改掉了!
  alert(Doctor.birthPlaces); //北京, 上海, 香港, 厦门
  alert(Chinese.birthPlaces); //北京, 上海, 香港, 厦门
所以,extendCopy()只是拷贝基本类型的数据,我们把这种拷贝叫做"浅拷贝"。这是早期jQuery实现继承的方式。
四、深拷贝
所谓"深拷贝",就是能够实现真正意义上的数组和对象的拷贝。它的实现并不难,只要递归调用"浅拷贝"就行了。
  function deepCopy(p, c) {
    var c = c || {};
    for (var i in p) {
      if (typeof p[i] === 'object') {
        c[i] = (p[i].constructor === Array) ? [] : {};
        deepCopy(p[i], c[i]);
      } else {
         c[i] = p[i];
      }
    }
    return c;
  }
使用的时候这样写:
  var Doctor = deepCopy(Chinese);
现在,给父对象加一个属性,值为数组。然后,在子对象上修改这个属性:
  Chinese.birthPlaces = ['北京','上海','香港'];
  Doctor.birthPlaces.push('厦门');
这时,父对象就不会受到影响了。
  alert(Doctor.birthPlaces); //北京, 上海, 香港, 厦门
  alert(Chinese.birthPlaces); //北京, 上海, 香港

创建对象
Object 构造函数或对象字面量都可以用来创建单个对象。但这个方法的缺点非常明显:同一个接口创建很可耐多对象会产生大量的重复代码。为了解决这个问题,人们开始使用工厂模式的一种变体。

工厂模式(摒弃,不推荐)
这个模式没有解决对象识别的问题(即怎样知道一个对象的类型)。如:

具体的创建单个对象:

var person = {};
person.name = "Oliver";
person.age = 18;
person.sayName = function(){
    return this.Name;
};
改变成工厂模式:

function createPerson(name,age){
    var obj = {};
    obj.name = name;
    obj.age = age;
    obj.sayName = function(){
        return this.name
    };
    return obj; //注意这里要返回obj 对象,这样才能把obj 对象传给createPerson 变量。
}

var newPerson = createPerson("Oliver",18);
构造函数模式
构造函数可以创建特定类型的对象。所以,可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。如:

function Person(name,age){ //注意大小写,构造函数应该把第一个字幕大写化
    this.name = name;
    this.age = age;
    this.sayName = function (){
        return this.name;
    };
}

var newPerson = new Person("Oliver",18);

document.write(newPerson.name); //Oliver
document.write(newPerson.age); //18
document.write(newPerson.sayName()); //Oliver
确实相当方便

这里一定要记住,构造函数都应该以一个大写字母开头,用来区分其他的函数

又如:

function Person(name,age){ //注意大小写,构造函数应该把第一个字幕大写化
    this.name = name;
    this.age = age;
    this.sayName = function (){
        return this.name;
    };
}

var person1 = new Person("Oliver",18);
var person2 = new Person("Troy",24);

document.write(person1.constructor == Person); //true
document.write(person2.constructor == Person); //true
这里的person1 和person2 分别保存着Person 的一个不同的实例。两个对象都有一个constructor(构造函数)属性,该属性指向Person。

在上面这个例子中,person1 和person2 即是Object 的实例,同时也是Person 的实例。可以通过instanceof 操作符来验证:

console.log((person1 instanceof Object) && (person2 instanceof Person)); //true
以这种方式创建构造函数是定义在Global 中的(window 对象)

将构造函数当做函数
任何函数,只要通过new 操作符来调用,那它就可以座位构造函数;而任何函数,如果不通过new 操作符来调用,那它就跟普通函数没区别。如下面这个构造函数:

function Car(name,color,sound){
    this.name = name;
    this.color = color;
    this.sound = function(){
        return sound;
    };
    console.log(this.name + " " + this.color + " " + this.sound());
}
如果当做构造函数来使用:

var benz = new Car("C200","White","Boom Boom"); //C200 White Boom Boom
如果作为普通函数来调用:

Car("Benz","White","Boom!"); //Benz White Boom!
console.log(window.name + window.color + window.sound()); //BenzWhiteBoom!
如果在另一个对象的作用域中调用:

var cars = {};
Car.call(cars,"Benz","White","Boom Boom!");
document.write(cars.sound()); //Boom Boom!
构造函数的问题
问题是每个方法都要在每个实例是重新创建一遍。可用通过把内部的函数转移到外部来解决这些问题。如:

function Car(name,color){
    this.name = name;
    this.color = color;
    this.show = show;
}
function show(){
    console.log(this.name + this.color);
}
var benz = new Car("Benz","white");
benz.show(); //Benzwhite
但这个问题是完全没有了封装性可言。不过可以通过原型模式来解决。

原型模式
function Person(){};
Person.prototype.name = "Oliver";
Person.prototype.age = 18;
Person.prototype.sayName = function(){
    console.log(this.name);
};
var person1 = new Person();
person1.sayName(); //Oliver
var person2 = new Person();
person2.sayName(); //Oliver;
console.log(person1.sayName == person2.sayName); //true
与构造函数不同的是,新对象的这些属性和方法是由所有实例共享的。这里两个新的person 访问的都是同一组属性和同一个sayName() 函数。

理解原型对象
以上面的Person 为例,Person 构造函数里面存在一个prototype 属性,这个属性指向原型对象Person Prototype,该Person Prototype 里面包含了constructor 属性,该属性又指向构造函数Person。构造函数的实例包含了一个[[Prototype]]的内部属性,该内部属性则指向Person Prototype。如:

function Person(){};
Person.prototype.name = "Oliver";
Person.prototype.age = 18;
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
person1.sayName(); //Oliver

var person2 = new Person();
person2.sayName(); //Oliver;

console.log(Person.prototype);
/*
age: 18
constructor: function Person() {}
name: "Oliver"
sayName: function () {
__proto__: Object
*/
console.log(Person.prototype.constructor);
//function Person() {}
console.log(Object.getPrototypeOf(person1));
/*
age: 18
constructor: function Person() {}
name: "Oliver"
sayName: function () {
__proto__: Object
*/
对于构造函数、原型属性以及实例之间的关系,参见《js高级程序设计》一书中第6.2.3 章节。
两个方法:isPrototypeOf()和Object.getProtytypeOf()(ECMAScript 5)。前者是用来确定[[Prototype]];后者是用来返回[[Prototype]]值。如:

console.log(Person.prototype.isPrototypeOf(person1)); //true
console.log(Object.getPrototypeOf(person1).name); //Oliver

console.log(Object.getPrototypeOf(person1));
/*
age: 18
constructor: function Person() {}
name: "Oliver"
sayName: function () {
__proto__: Object
*/
为对象添加一个属性时,这个属性会屏蔽原型对象中的同名属性,但是并不会修改那个属性。如果使用delete 删除这个属性,就可以重新访问原型中的属性。如:

function Person(){};
Person.prototype.name = "Oliver";
Person.prototype.age = 18;
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
person1.sayName(); //Oliver 原型中的Name

person1.name = "Troy";
person1.sayName(); //Troy 实例中的Name

delete person1.name;
person1.sayName(); //Oliver 原型中的Name
每次读取某个对象的某个属性,都会从实例本身开始搜索,如果没有找到给定名字的属性,则会在原型对象中再次搜索。

方法hasOwnProperty()检测属性如果在对象实例中时,返回true。如:

console.log(person1.hasOwnProperty("age")); //false age属性来自于原型
console.log(person1.hasOwnProperty("name")); //true name属性来自于实例
原型与in 操作符
两种方法使用in 操作符:单独使用和for-in 循环中使用。

单独使用时,in 返回true 说明该属性存在于实例或原型中。如:

function Person(){};
Person.prototype.name = "Oliver";
Person.prototype.age = 18;
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
person1.name = "Troy";
person1.sayName(); //Troy 实例中的Name

console.log("name" in person1); //true name属性在实例或原型中
console.log(person1.hasOwnProperty("name")); //true name属性在实例中
//上面两个就说明name属性一定在实例中
又如:


function Person(){};
Person.prototype.name = "Oliver";
Person.prototype.age = 18;
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
person1.name = "Troy";
person1.sayName(); //Troy 实例中的Name

var person2 = new Person();

console.log("name" in person1); //true name属性在实例或原型中
console.log(person1.hasOwnProperty("name")); //true name属性在实例中
//上面两个就说明name属性一定在实例中

console.log("name" in person2); //true
console.log(person2.hasOwnProperty("name")); //false
//上面两个说明name属性一定在原型中
自定义一个函数hasPrototypeProperty(object,name);即同时使用上面两个方法来确定属性到底是不是存在于实例中。如:

function Person(){};
Person.prototype.name = "Oliver";
Person.prototype.age = 18;
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
person1.name = "Troy";
person1.sayName(); //Troy 实例中的Name

var person2 = new Person();

function hasPrototypeProperty(object,name){
    console.log((name in object) && !(object.hasOwnProperty(name)))
}

hasPrototypeProperty(person2,"name"); //true name属性是在原型中
hasPrototypeProperty(person1,"name"); //false name属性是在实例中
用Object.defineProperty()方法定义的属性:

function Person(){};
Person.prototype.name = "Oliver";
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
Object.defineProperty(person1, "age", {
    value: 18
})

console.log(person1.hasOwnProperty("age")); //true age属性是实例属性
关于for-in、[[enumerable]]、defineProperty、hasOwnProperty 的例子:

var person1 = {
    age: 18
};
Object.defineProperty(person1, "name", {
    value: "Oliver",
    enumerable: true
})
for(var x in person1){
    console.log(x);
}
console.log(person1.hasOwnProperty("name")); //true
又如:

function Person(age){
    this.age = age;
}
var person1 = new Person(18);
Object.defineProperty(person1, "name", {
    value: "Oliver",
    enumerable: false
})
for(var x in person1){
    console.log(x); //用defineProperty 定义的name 属性是实例属性,这里不会枚举到
}
console.log(person1.hasOwnProperty("name")); //true
又如:

function Person(){};
Person.prototype.age = 18;
var person1 = new Person();
Object.defineProperty(person1, "name", {
    value: "Oliver",
    enumerable: false
})
for(x in person1){
    console.log(x); //这里仍然不回枚举到自定义的name 实例属性
}
但是:

function Person(){};
Person.prototype.age = 18;
Person.prototype.name = "Oliver";
var person1 = new Person();
Object.defineProperty(person1, "name", {
    enumerable: false
})
for(x in person1){
    console.log(x); //这里则返回枚举到自定义的name 原型属性
}
原型属性的[[enumerable]]设置为false,调用for-in 仍然可以被枚举到。

另外,Object.keys()方法可以返回所有可枚举属性的字符串数组:

function Person(){};
Person.prototype.age = 18;
Person.prototype.name = "Oliver";

var person1 = new Person();
Object.defineProperty(person1, "sound", {
    value: "miao~",
    enumerable: true //可枚举
});
Object.defineProperty(person1, "sound2", {
    value: "wang~",
    enumerable: false //不可枚举
});

console.log(Object.keys(Person.prototype)); //["age", "name"]
console.log(Object.keys(person1)); //["sound"]
Object.getOwnPropertyName()方法,则可以返回无论可否枚举的所有实例属性:

function Person(){};
Person.prototype.age = 18;
Person.prototype.name = "Oliver";

var person1 = new Person();
Object.defineProperty(person1, "sound", {
    value: "miao~",
    enumerable: true //可枚举
});
Object.defineProperty(person1, "sound2", {
    value: "wang~",
    enumerable: false //不可枚举
});

console.log(Object.keys(Person.prototype)); //["age", "name"]
console.log(Object.keys(person1)); //["sound"]
console.log(Object.getOwnPropertyNames(Person.prototype)); //["constructor", "age", "name"]
console.log(Object.getOwnPropertyNames(person1)); //["sound","sound2"]
更简单的原型语法
即字面量方法:


function Person(){};
Person.prototype = {
    name: "Oliver",
    age: 18,
    sayName: function(){
        console.log(this.name);
    }
};

var person1 = new Person();
console.log(Person.prototype.constructor); //不再指向Person()构造函数

function People(){};
People.prototype.name = "Troy";
People.prototype.age = 26;
People.prototype.sayName = function(){
    console.log(this.name);
};

var people1 = new People();
console.log(People.prototype.constructor); //这里则指向People()构造函数
上面第一种就是字面量方法。但是由此带来的问题是,他的原型对象中的constructor 属性将不再指向上个例子中的Person() 构造函数了。(其实我们本质上是重写了prototype对象)

如果constructor 值真的非常重要,则只需要把它设置回适当的值就可以了:

function Person(){};
Person.prototype = {
    constructor: Person,
    name: "Oliver",
    age: 18,
    sayName: function(){
        console.log(this.name);
    }
};

var person1 = new Person();
console.log(Person.prototype.constructor); //重新指向Person()构造函数

function People(){};
People.prototype.name = "Troy";
People.prototype.age = 26;
People.prototype.sayName = function(){
    console.log(this.name);
};

var people1 = new People();
console.log(People.prototype.constructor); //这里则指向People()构造函数
然而用字面量的方法导致的问题仍然没有结束,以上面这种方式重设constructor 属性会导致[[Enumerable]]特性被设置为true。因此在支持ECMAScript 5 的js 引擎中可以用Object.defineProperty()方法把它修改为false:

function Person(){};
Person.prototype = {
    constructor: Person,
    name: "Oliver",
    age: 18,
    sayName: function(){
        console.log(this.name);
    }
};

var person1 = new Person();
console.log(Person.prototype.constructor);
for (var x in person1){
    console.log(x); //这里会出现constructor,但是我们实际上不应该让他能够被枚举出
}

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

for (var x in person1){
    console.log(x); //这里就不会出现constructor 了,但是这种方法只支持ECMAScript 5的js 引擎
}

/*
[Log] function Person() {} (repetition.html, line 130)
[Log] constructor (repetition.html, line 132)
[Log] name (repetition.html, line 132)
[Log] age (repetition.html, line 132)
[Log] sayName (repetition.html, line 132)
[Log] name (repetition.html, line 140)
[Log] age (repetition.html, line 140)
[Log] sayName (repetition.html, line 140)
*/
原型的动态性
我们对原型对象所做的任何修改都能立即从实例上反应出来。因为实例与原型之间的链接只不过是一个指针而不是副本:

function Person(){};
var person = new Person(); //person在Person()构造函数修改之前创建的
Person.prototype.name = "Oliver";
console.log(person.name); //仍然会出现实时的变化
但是如果重写了prototype 则就不同了,因为实例的[[Prototype]]会指向原型对象,如果修改了原来的原型对象,则就是切断了构造函数与最初原型之间的联系:

function Person(){};
var person = new Person();

Person.prototype = { //这里重写了Person.prototype,属于新的Person.prototype
    constructor: Person,
    name: "Oliver"
}

console.log(person.name); //原型对象被修改了,指针仍然指向旧的Person.prototype
从这里就可以很明显看出,Person.prototype={},实际上字面量方法就是重写了原型对象。如果是Person.prototype.name="Oliver",则并不是重写而是修改,不会创建“新的原型对象。”

《js高级程序设计》一书中6.2.3 中的图6-3 很清楚的描述了该原理
原生对象的原型
所有原生的引用类型(Object、Array、String等等)都在其构造函数的原型上定义了方法。同时,我们也可以给原生对象自定义方法:

var array = new Array();
Array.prototype.name = function(){
    console.log("Array")
};

array.push("hello ","there");
console.log(array);

array.name();
也可以修改:

var array = new Array();
Array.prototype.toString = function(){
    return("Array")
};

array.push("hello ","there");
console.log(array.toString());
//这样就抹去了toString()方法
强烈不推荐修改和重写原生对象的原型

原型对象的问题
就是包含引用类型值的属性来说,问题比较严重。具体体现在原型中的属性被实例共享:

function Person(){};
Person.prototype = {
    constructor: Person,
    name: "Oliver",
    age: 18,
    friends: ["Troy","Alice"]
}

var person1 = new Person();
var person2 = new Person();

person1.friends.push("Ellen");
console.log(person1.friends); //["Troy", "Alice", "Ellen"]
console.log(person2.friends); //["Troy", "Alice", "Ellen"]
//两者完全相同,因为原型中的该属性被实例所共享,push()方法只是修改了person1.friends,没有重写

person1.friends = ["Troy", "Alice"];
console.log(person1.friends); //["Troy", "Alice"]
console.log(person2.friends); //["Troy", "Alice", "Ellen"]
//虽然可以通过重写和覆盖来解决该问题,但是仍然非常麻烦

console.log(person1.hasOwnProperty("friends")); //true;
console.log(person2.hasOwnProperty("friends")); //false;
//这里就可以看到,重写能解决问题是因为重写导致它创建了实例属性"friends"
这里可以看出,如果我们的初衷像这样只是想创建一个共享的数组,那么当然不会有什么问题;但是实例一般都会有自己的属性,所以不应该单独使用原型模式。而是组合使用构造函数模式和原型模式。

组合使用构造函数模式和原型模式
这是一种用来定义引用类型的一种默认模式:


function Person(name,age){
    this.name = name;
    this.age = age;
    this.friends = [];
} //独享的部分
Person.prototype = {
    constructor: Person,
    sayName: function(){
        return this.name;
    }
} //共享的部分
var person1 = new Person("Oliver",18);
var person2 = new Person("Troy",24);
person1.friends.push("Alice","Mark");
person2.friends.push("Mac");
console.log(person1.friends.toString());
console.log(person2.friends.toString());
/*
[Log] Alice,Mark (repetition.html, line 228)
[Log] Mac (repetition.html, line 229)
*/
动态原型模式
可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型:

function Person(name,age){
    this.name = name;
    this.age = age;
    if (typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            return (this.name);
        };
    }
}
var person = new Person("Oliver",18);
console.log(person.sayName()); //Oliver
实际上就是把下面代码封装在了构造函数中:

function Person(name,age){
    this.name = name;
    this.age = age;
}
Person.prototype.sayName = function(){
    return(this.name);
};
var person = new Person("Troy",24);
console.log(person.sayName()); //Troy
寄生构造函数模式
世纪撒好难过跟工厂模式一样。建议在可以使用其他模式的情况下,不要使用该模式。

稳妥构造函数模式
稳妥对象,指的是没有公共属性,且其方法也不引用this 的对象如:

function Person(name,age){
    var obj = new Object();
    obj.sayName = function(){
        console.log(name);
    };
    return obj;
}
var person1 = Person("Oliver",18);
person1.sayName(); //Oliver
































猜你喜欢

转载自zhyp29.iteye.com/blog/2304177