创建对象
- 虽然用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("Greg", 27, "Doctor");
person1.sayName(); //"Nicholas"
person2.sayName(); //"Greg"
- 每次要创建Person对象只要调用createPerson()方法传入三个参数,方法即可返回一个Person对象。不过这个模式的弊端是无法识别对象的类型。(即使用该方法创建的两个对象并无联系)
构造函数模式
- Object和Array都是原生构造函数。故也可以创建自定义构造函数,从而定义自定义对象类型的方法和属性。来看下面的例子:
//构造方法习惯大写字母开头
//与createPerson的区别在于
//1.没有new Object
//2.使用this
//3.没有return
//此处创建了原型对象(constructor指向该函数) 在原型模式会有详细说明
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
//注意此处使用了new操作符 创建对象经历以下4个步骤
//1.创建一个新对象
//2.将构造函数的作用域赋给新对象(因此this就指向了该对象)
//3.执行构造函数中的代码(为这个对象添加属性和方法)
//4.返回新对象。
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); //"Nicholas"
person2.sayName(); //"Greg"
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true 使用构造方法可以使用instanceof方法判断
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true
alert(person1.constructor == Person); //true 使用new创建的对象有个属性constructor指向构造方法
alert(person2.constructor == Person); //true
alert(person1.sayName == person2.sayName); //false 想想为什么?
构造函数也是普通函数
- 构造函数和其他函数的唯一区别,就在于调用的他的方式不同。不过构造函数毕竟也是函数,不存在定义构造函数的特殊语法。如下:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
Person("hange", 23, "cxy");//此时this指代window
alert(age);//23
sayName();//hange
//这里复习一下call的用法。使用call,使this指向o
var o = new Object();
Person.call(o, "hange", 23, "cxy");
o.sayName();//hange
构造函数的问题
- alert(person1.sayName == person2.sayName); //false
- 使用构造函数,会使每个方法都要在每个实例上创建一遍。所以上面的代码会弹出false。但是创建两个完成同样任务的Function实例的确没有必要(因为使用了this,结果会因为调用的环境而异),故可以按照下面的方式修改:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); //"Nicholas"
person2.sayName(); //"Greg"
alert(person1.sayName == person2.sayName); //true
- 上面这种做法的确解决了方法实例不需要多次创建的问题,但是却带来了新的问题:全局作用域内定义的sayName()函数,却只能被某个对象调用。并且大大破坏了封装性,难道每有定义一个方法,就得跑去全局作用域下定义这个方法吗。来看原型模式:
原型模式
- 我们创建的每个函数(例如构造函数)都有一个prototype(原型)属性,这个属性指向一个对象,而这个对象的用途就是包含可以由特定类型的所有实例共享的属性和方法。(按照我的理解就像java中的类方法和类变量一样)
function Person(name){
this.name = name;
}
//这里只是改变了sayName的定义位置,并且不再需要在Person构造函数中定义明function
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person("hange");
person1.sayName(); //"hange"
var person2 = new Person("miaoch");
person2.sayName(); //"miaoch"
alert(person1.sayName == person2.sayName); //true
- 上面这段代码就显得好多了,要理解原型模式的工作原理,先来理解原型对象的性质:
理解原型对象
- 无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。默认情况下,所有的原型对象都会自动获得constructor(构造函数)属性,通过这个构造函数,我们可以继续为原型对象添加其他属性和方法。
- 通常[[Prototype]]指针对于脚本是不可见的,但是在Firefox、Safari、Chrome上支持__proto__属性获得这个指针。
//接着上面的例子
var name = "全局作用域";
person1.__proto__.sayName();//undefined
person1.__proto__.sayName.call(window);//"全局作用域"
- 原型对象有一个方法:isPrototypeOf()接收一个对象,如果该对象的[[Prototype]]指针指向该原型对象则返回true。
//接着上面的例子
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
- 在ECMAScript 5 增加了一个方法:Object.getPrototypeOf(),这个方法接收一个对象,返回该对象的[[Prototype]]所指向的原型对象。
- 那么当请求一个对象的属性或者方法时,对象是如何返回相应的属性或者方法呢?首先先从对象实例开始,如果找到了,则返回。否则继续从对象实现的原型对象中搜索,如果找到了,则返回。
function Person() {
}
Person.prototype.name = "miaoch";
var p1 = new Person();
p1.name = "hange";
var p2 = new Person();
alert(p1.name);//hange
alert(Object.getPrototypeOf(p1).name);//miaoch
alert(p2.name);//miaoch
p1.name = null;
alert(p1.name);//null
p1.name = undefined;
alert(p1.name);//undefined
delete p1.name;
alert(p1.name);//miaoch
- 利用对象的hasOwnProperty()方法,传入一个字符串,可以判断该属性是否存在于对象实例中。如果该属性存在于对象实例中则返回true,否则即使原型对象中含有该属性也会返回false。
原型与in操作符
- in操作符有两种用法,一是for-in,第二个是在能够通过对象访问给定属性时返回true。无论该属性存在于实例还是原型对象中。
function Person() {
}
Person.prototype.name = "miaoch";
var p1 = new Person();
p1.name = "hange";
alert("name" in p1);//注意name必须加引号 否则会达到 undefined in p1的效果
delete p1.name;
alert("name" in p1);//true
/*delete p1.prototype.name;//无效 会报错
alert("name" in p1);//true*/
//delete Person.prototype.name;//有效
delete p1.constructor.prototype.name;//有效
alert("name" in p1);//false
- 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 p = new Person();
p.name = "hange";
alert(Object.keys(p)); //name
var array = new Array();
for (var propo in p) { //in会搜到原型对象里面的可枚举属性
array.push(propo);
}
alert(array);//"name,age,job,sayName"
更简单的原型语法
function Person(){
}
Person.prototype = {
//constructor:Person, //这么做会导致constructor可枚举,可考虑使用defineProperty()设置[[enumerable]]为true
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
- 这种做法虽然简洁,但是这里有一个要注意的点。前面提到在创建一个函数的时候,就会自动创建该函数的原型对象,并且该原型对象有一个constructor属性指向该函数。而利用对象字面量形式设置Person.prototype其实相当于完全重写了Person的原型对象。故此时Person.prototype的constructor属性就不再指向Person了(当然你可以手动指定constructor属性),而是指向了Object构造函数(因为利用对象字面量创建对象赋给原型对象,此处的对象字面量也就相当于是Object构造函数创建的)。虽然constructor属性已经变化了,但是不会影响instanceof操作符的结果,这也说明了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
- 另外提到的字面还有一个弊端,这样会重新设置Person构造函数的[[prototype]]指向一个新的对象。什么意思呢?就是说假设你new了一个friend。然后你又用这个方式修改了Person构造函数的[[prototype]],你再new一个person2,此时person2的[[prototype]]指向的对象和person1可是不一样的。见下图:friend指向的依旧是老的原型对象。
原生对象的原型
- 可以给原生对象定义新的方法。但是这种方式并不推荐,可能会导致命名冲突,或者意外的重写原生方法。
String.prototype.startsWith = function(text) {
return this.indexOf(text) == 0;
};
var msg = "Hello world!";
alert(msg.startsWith("Hello")); //true
原型对象的问题
- 见下面的例子:
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共享了一个属性(如果是引用类型,修改一个对象的值还会影响到别的对象),但是通常情况下,实例一般都是要有属于自己的全部属性。而这个问题正是我们很少看到有人单独使用原型模型的原因所在。
组合使用构造函数模式和原型模式
- 其实这种方法显而易见啊,这就像java中定义一个类一样,有成员变量和成员方法,也有静态变量和静态方法。在js中一般这样定义:
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
动态原型模式
- 这种方式不需要在其他位置再重新设置Person.prototype,而且因为不是使用字面量形式,故也不会切断已经创建的对象和原型对象的联系。看以下例子:
function Person(name, age, job){
//properties
this.name = name;
this.age = age;
this.job = job;
if (!("type" in Person.prototype)){
Person.prototype.type = age;
}
//methods
if (typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
};
}
}
var friend1 = new Person("Nicholas", 29, "Software Engineer");
var friend2 = new Person("Nicholas", 29, "Software Engineer");
friend1.sayName();
alert(friend1.type);//29
alert(friend2.type);//29
friend1.type = 28;
alert(friend1.type);//28 此处屏蔽了prototype中的29
alert(friend2.type);//29
寄生构造函数模式
- 这种模式和工厂模式没有什么不同,唯一的区别就是在调用的时候前面加了一个new操作符。构造函数在不返回值的情况下,默认会返回新对象实例,并且把构造函数的作用域赋给新对象实例。而当构造函数拥有返回值的时候,可以重写调用构造函数时返回的值。
- 假设我们想创建一个具有额外方法的特殊数组,由于不能直接修改Array构造函数,可以使用这个模式。其实在我看来和工厂模式毫无区别,去掉new操作符结果相同。这么做只是为了使代码变得更优雅 = =!,强行将他理解为构造函数而不是普通的函数。
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);//false 寄生构造函数模式创建的类型取决于函数内部new的类型。
alert(colors instanceof Array);//true
稳妥构造函数模式
- 稳妥对象(durable objects)指的是没有公共属性,而且方法也不引用this的对象。稳妥构造函数模式和寄生构造函数模式类似,但有两点不同:一是不引用this,二是不使用new操作符调用构造函数。
function Person(name) {
var o = new Object();
o.sayName = function () {
alert(name);
}
return o;
}
var p = Person("hange");//这里不使用new 但是你用了new其结果也一样 还是为了优雅~~
p.sayName();//hange
alert(p.name);//undefined
alert(p instanceof Person);//false