6、面向对象的程序设计2(《JS高级》笔记)

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

function Person(){
}

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

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

alert(person1.hasOwnProperty("name"));  //false
alert("name" in person1);  //true

person1.name = "Greg";
alert(person1.name);   //"Greg" ?from instance
alert(person1.hasOwnProperty("name"));  //true
alert("name" in person1);  //true

alert(person2.name);   //"Nicholas" ?from prototype
alert(person2.hasOwnProperty("name"));  //false
alert("name" in person2);  //true

delete person1.name;
alert(person1.name);   //"Nicholas" - from the prototype
alert(person1.hasOwnProperty("name"));  //false
alert("name" in person1);  //true

说明:可以看到无论属性是存在于实例中还是原型中,同时使用hasOwnProperty()方法和in操作符,就可以确定该属性到底是存在于实例中还是原型中:

function hasPrototypeProperty(object, name){
  return !object.hasOwnProperty(name) && (name in object);
}

说明:使用上述方法时,只有属性存在于原型中时才返回true。而如果此时实例中也存在此属性,那么此方法返回false

在使用for-in循环时,返回的是所有能通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举的属性(即将[[Enumerable]]标记为false的属性)的实例属性也会在 for-in循环中返回。因为根据规定,所有开发人员定义的属性都是可枚举的。

ECMAScript 5中,将constructorprototype属性设置为了不可枚举,但并不是所有浏览器都照此实现。要取得对象上所有可枚举的实例属性,可以使用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"

3、更简单的原型语法
之前定义原型对象时,要依次定义每个属性,比较麻烦,可以使用更简单的方式:

function Person(){
}

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

说明:上述代码中,将Person.prototype设置为等于一个以对象字面量形式创建的新对象,最终结果一样,但是constructor属性不再指向Person了。前面讲过,每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获得constructor属性。但是这里的语法完全重写了默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数。此时,尽管instanceof操作符还能返回正确结果,但是通过constructor已经无法确定对象的类型了:

var friend = new Person();

alert(friend instanceof Object);  //true
alert(friend instanceof Person);  //true
alert(friend.constructor == Person);  //false
alert(friend.constructor == Object);  //true

说明:如果constructor属性比较重要,则可以修改:

function Person(){
}

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

注意:以这种方式重设constructor属性会导致它的[[Enumerable]]特性被设置为true。默认情况下,原生的constructor属性是不可枚举的。因此如果想兼容ECMAScript 5JS引擎,可以使用下面的方式:

function Person(){
}

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

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

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

var friend = new Person();
Person.prototype.sayHi = function(){
    alert("hi");
};
friend.sayHi();   //"hi" ?works!

说明:可以看到上面的sayHi()方法可以其作用。尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的[[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

说明:我们通过图解的方式进行说明。

1

5、原生对象的原型
原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有的原生引用类型(Object、Array、String等)都在其构造函数的原型上定义了方法。如:

alert(typeof Array.prototype.sort);//function
alert(typeof String.prototype.substring);//function

说明:通过原生对象不仅可以取得所有默认方法的引用,而且可以定义新方法。

String.prototype.startsWith = function (text){
  return this.indexOf(text) == 0;
};
var msg = "Hello World";
alert(msg.startsWith("Hello"));//true

说明:尽管可以添加方法,但是不推荐在产品化的程序中修改原生对象的原型。

6、原型对象的问题
原型模式会省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。但这不是最主要的问题,对包含引用类型值的属性来说吗,问题较为突出:

function Person(){
}

Person.prototype = {
    constructor: Person,
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    friends : ["Shelby", "Court"],
    sayName : function () {
        alert(this.name);
    }
};
var person1 = new Person();
var person2 = new Person();

person1.friends.push("Van");

alert(person1.friends);    //"Shelby,Court,Van"
alert(person2.friends);    //"Shelby,Court,Van"
alert(person1.friends === person2.friends);  //true

说明:可以看到,虽然我们是向person1对象中添加值,但是在person2中的属性也包含新添加的值,但是实例一般都是要有属于自己的全部属性的,而这个问题正是我们很少有人单独使用原型模式的原因所在。

2.2.4 组合使用构造函数模式和原型模式

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

 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,Court,Van"
 alert(person2.friends);    //"Shelby,Court"
 alert(person1.friends === person2.friends);  //false
 alert(person1.sayName === person2.sayName);  //true

2.2.5 动态原型模式

动态原型模式就是将所有信息都封装在构造函数中,而通过构造函数初始化原型(进在必要的情况下),也就是如果不存在某个方法的时候才将其初始化为原型属性:

 function Person(name, age, job){
 
     //properties
     this.name = name;
     this.age = age;
     this.job = job;
     
     //methods
     if (typeof this.sayName != "function"){
         Person.prototype.sayName = function(){
             alert(this.name);
         };
     }
 }

 var friend = new Person("Nicholas", 29, "Software Engineer");
 friend.sayName();

说明:可以看到只有当friend实例对象不存在sayName方法时才会将其初始化为原型属性,但是也要注意,不能在使用对象字面量重写原型。

2.2.6 寄生构造函数模式(不推荐)

通常,在前述的几种模式都不适用的情况下,可以使用寄生构造函数模式。

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

var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();  //"Nicholas"

说明:除了使用new操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一样的。通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。

这个模式可以在特殊的情况下用来创建构造函数。假设我们向创建一个具有额外方法的特殊数组。由于不能直接修改Array构造函数,因此可以使用此模式:

function SpecialArray(){       

    //create the array
    var values = new Array();
    
    //add the values
    values.push.apply(values, arguments);
    
    //assign the method
    values.toPipedString = function(){
        return this.join("|");
    };
    //return it
    return values;        
}

var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); //"red|blue|green"
alert(colors instanceof SpecialArray);

说明:虽然定义的构造函数没有参数,但是如果向其传入参数,会保存在argument数组中。关于寄生构造函数模式,有一点需要说明:首先,返回的对象于构造函数或者构造函数的原型属性之间没有任何关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。所以,不能依赖instanceof操作符来确定对象类型。

2.2.7 稳妥构造函数模式

所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this对象。稳妥对象最适合在一些安全的环境中(这些环境中禁止使用thisnew),或者在防止数据被其他应用程序改动时使用。其和寄生构造函数类似,但是有两点不同:一是新创建对象的实例方法不引用this;而是不使用new操作符调用构造函数。

function Person(name, age, job){
  //创建要返回的对象
  var o = new Object();
  //可以在这里定义私有变量和函数
  
  //添加方法
  o.sayName = function(){
    alert(name);
  };
  //返回对象
  return o;
}

说明:以这种模式创建的对象中,除了使用sayName()方法之外,没有其他办法访问name的值:

var friend = Person("Tom", 29, "Software Engineer");
friend.sayName();//"Tom"

说明:和寄生模式一样,这种模式也不能使用instanceof操作符来确定对象类型。

推荐阅读更多精彩内容