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操作符来确定对象类型。

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

推荐阅读更多精彩内容