function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
console.log(this.name)
}
return o;
}
var person1 = createPerson('chuqiao', 29, 'fengyun');
var person2 = createPerson('yuwenyue', 27, 'diezhe');
//工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)
B) 构造函数模式
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
console.log(this.name)
}
}
var person1 = new Person('chuqiao', 29, 'fengyun');
var person2 = new Person('yuwenyue', 27, 'diezhe');
//要创建 Person的新实例,必须使用 new操作符。以这种方式调用构造函数实际会经历以下4个步骤:
//1) 创建一个新对象;
//2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
//3) 执行构造函数中的代码(为这个新对象添加属性);
//4) 返回新对象;
//person1 和 person2分别保存着 Person的一个不同的实例。这两个对象都有一个 constructor(构造函数)属性,该属性指向 Person
console.log(person1.constructor == Person);//true
console.log(person2.constructor == Person);//true
//constructor 最初用来标识对象类型,但是,提到检测对象类型还是 instanceof 操作符要更可靠些。
console.log(person1 instanceof Object);//true
console.log(person1 instanceof Person);//true
console.log(person2 instanceof Object);//true
console.log(person2 instanceof Person);//true
//创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;
1、将构造函数当做函数
构造函数与其他函数的唯一区别,就在于调用它们的方式不同
。任何函数只要通过 new 操作符来调用,那它就可以作为构造函数;
//当做构造函数使用
var person = new Person("chuqiao", 29, "fengyun");
person.sayName();//chuqiao
//作为普通函数调用
Person("yuwenyue", 27, "diezhe");
window.sayName();//yuwenyue
//在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "luohe", 25, "chuqiaotama");
o.sayName();//Greg
2、构造函数的问题
构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("console.log(this.name)");//与声明函数在逻辑上是等价的
}
//从这个角度上看构造函数,更容易明白每个 Person实例都包含一个不同的 Function 实例(以显示 name属性)的本质。
console.log(person1.sayName == person2.sayName) // false 不同实例上的同名函数是不相等的
C) 原型模式
每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象
,而这个对象的用途是包含可以由特定的类型的所有实例共享的属性和方法。
prototype : 通过调用构造函数而创建的那个对象实例的原型对象。
使用原型对象的好处就是可以让所有对象实例共享它所包含的属性和方法。即不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中
function Person(){
}
Person.prototype.name = "chuqiao";
Person.prototype.age = 29;
Person.prototype.job = "fengyun";
Person.prototype.sayName = function(){
console.log(this.name)
};
var person1 = new Person();
person1.sayName();//chuqiao
var person2 = new Person();
person2.sayName();//chuqiao
console.log(person1.sayName == person2.sayName)//true
要理解原型模式的工作原理,必须先理解 EMACScript中原型对象的性质:
1、理解原型对象
无论什么时候,只要创建了一个
新函数
,就会根据一组特定的规则为该函数创建一个prototype 属性
,这个属性指向函数
的原型对象
。在默认情况下,所有原型对象都会获得一个constructor(构造函数)属性
,这个属性是一个指向prototype属性所在函数
的指针
。创建自定义的构造函数之后,其原型对象默认只会取得 constructor 属性;至于其他方法都是从 Object 继承而来的。当调用构造函数创建一个
新实例
后,该实例的内部
将包含一个指针
(内部属性
),指向构造函数
的原型对象
。在 ECMA-262第5版中管这个指针叫[[prototype]]
,脚本中没有标准的方式访问[[prototype]],但是Firefox、Safari、Chrome 都支持一个属性__proto__
;其他实现中这个属性对脚本则是完全不可见的;这个链接存在于实例
与构造函数的原型对象
之间。
isPrototypeOf()
//如果person1的内部属性([[prototype]])指向调用isPrototype()方法的对象(Person.prototype),那么这个方法就返回 true。
console.log(Person.prototype.isPrototypeOf(person1));//true
console.log(Person.prototype.isPrototypeOf(person2));//true
Object.getPrototypeOf():返回某个对象的原型;
console.log(Object.getPrototype(person1) == Person.prototype);//true
读取某个对象的某个属性时,取值顺序:
对象实例本身
>>>>指针指向的原型对象
;
可以通过对象实例访问保存在原型中的值,但不能通过对象实例重写原型中的值
;即当为对象添加一个属性的时候,这个属性只会阻止我们访问原型中的那个属性,不会修改那个属性;即使新增的属性值为 null,也只会在实例中设置这个属性,而不会恢复其指向原型的链接。不过,使用delete
操作符则可以完全删除实例属性
,从而让我们能够重新访问原型中的属性。hasOwnProperty() :
可以检测一个属性是存在于实例中,还是存在于原型中
。这个方法只在给定属性
存在于对象实例
中时,才会返回 true。
function Person(){
}
Person.prototype.name = "chuqiao";
Person.prototype.age = 29;
Person.prototype.job = "fengyun";
Person.prototype.sayName = function(){
console.log(this.name)
};
var person1 = new Person();
var person2 = new Person();
console.log(person1.hasOwnProperty("name"));//false
person1.name = "xinger";
console.log(person1.name);// xinger ---- 来自实例
console.log(person1.hasOwnProperty("name"));// true
console.log(person2.name);//chuqiao ---- 来自原型
console.log(person2.hasOwnProperty("name"));// false
delete person1.name; //删除了实例上的 name 属性
console.log(perosn1.name);//chuqiao ---- 来自原型
console.log(person1.hasOwnProperty("name"));// false
2、原型与 in 操作符
有两种方式使用 in 操作符:单独使用和 for-in 循环使用。
在单独使用时
,无论该属性存在于实例中还是原型中,in操作符会在通过对象能够访问给定属性时返回 true。eg: "name"in
person1。
在使用for-in 循环时
,返回的是所有能够通过对象访问的、可枚举的属性,其中既包括实例中的属性,也包括存在于原型中的属性。
//同时使用 hasOwnProperty()方法和 in 操作符,就可以确定该属性是存在于对象中还是存在于原型中。
function hasPrototypeProperty(obj, name){
return !obj.hasOwnProperty(name) && (name in obj) ;
}
// name存在于obj原型中,返回 true;
//name存在于obj实例中,返回 false;
Object.keys()方法 :取得对象上所有可枚举的
实例属性
;这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组
。Object.getOwnPropertyNames()方法:获取所有实例属性(无论是否可枚举)
function Person(){
}
Person.prototype.name = "chuqiao";
Person.prototype.age = 29;
Person.prototype.job = "fengyun";
Person.prototype.sayName = function(){
console.log(this.name)
};
var keys = Object.keys(Person.prototype);
console.log(keys);//["name", "age", "job", "sayName"]
var p1 = new Person();
p1.name = "xinger";
p1.age = 31;
var p1keys = Object.keys(p1);
console.log(p1keys);//["name", "age"]
var keys2 = Object.getOwnPropertyNames(Person.prototype);
console.log(keys2);//["constructor", "name", "age", "job", "sayName"]
3、更简单的原型语法
用一个包含所有属性和方法的对象字面量来
重写
整个原型对象;
function Person(){
}
Person.prototype = {
//采用对象字面量来重写默认的原型对象,因此constructor属性也就变成了新的对象的 constructor 属性(指向 Object 构造函数),不再指向Person函数
//故 如果constructor的值很重要,可以在此重设constructor
constructor: Person,
name: "chuqiao",
age: 29,
job: "fengyun",
sayName: function(){
console.log(this.name),
};
};
注意,以这种方式重设constructor属性会导致它的[[enumerable]]特性被设置为 true。默认情况下
原生的 constructor 属性是不可枚举的
,因此如果使用 EMACScript5的引擎,可以用Object.defineProperty()
来重设constructor
function Person(){
}
Person.prototype = {
name: "chuqiao",
age: 29,
job: "fengyun",
sayName: function(){
console.log(this.name),
};
};
//重设 constructor,只适用于 EMACScript5兼容的浏览器
Object.defineProperty(Person.prototype,"constructor",{
enumerable: false,//不可枚举
value: Person
});
4、原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此
function Person(){
}
var friend = new Person();
Person.prototype.sayHi = function(){
console.log("hi")
}
friend.sayHi(); // hi
//没有报错,因为实例与原型之间的连接只不过是一个指针,而非一个副本。
尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,
但是如果是重写整个原型对象,那么情况就不一样了。
我们知道,调用构造函数时会为实例添加一个指向最初原型的[[prototype]]指针
,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。记住:实例中的指针指向原型,而不是构造函数
。
function Person(){
}
var friend = new Person();
Person.prototype ={
constructor: Person,
name: "chuqiao",
age: 29,
job: "fengyun",
sayName: function(){
console.log(this.name)
}
}
friend.sayName(); // friend.sayName is not a function
重写原型对象切断了
现有原型
与任何之前已经存在的对象实例
之间的联系,这些实例的引用仍然是最初的原型。
5、原生对象的原型
所有原生引用类型( String、Array、Object 等等)都是在
其构造函数的原型
上定义了方法。
通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。可以像修改自定义对象的原型一样修改原生对象的原型。但是不推荐使用,因为可能重写了原生的方法,还可能引起致命冲突。
//给基本包装类型 String 的原型对象添加了一个startsWith()方法
String.prototype.startsWith = function(text){
return this.indexOf(text) == 0;
}
var msg = "Hello World";
console.log(msg.startsWith("Hello")); //true
6、原型对象的问题
缺点:它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。原型模式的最大问题是由其共享的本性所致。简而言之,对于包含引用类型值的属性来说,就是牵一发而动全身。
function Person(){
}
Person.prototype ={
constructor: Person,
name: "chuqiao",
age: 29,
job: "fengyun",
friends: ["yuwenyue", "yueqi", "wuxiansheng"],
sayName: function(){
console.log(this.name)
}
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push("liangtaizi");
console.log(person1.friends);//["yuwenyue", "yueqi", "wuxiansheng", "liangtaizi"]
console.log(person2.friends);//["yuwenyue", "yueqi", "wuxiansheng", "liangtaizi"]
console.log(Person.prototype.friends);//["yuwenyue", "yueqi", "wuxiansheng", "liangtaizi"]
D) 组合使用构造函数模式和原型模式
创建自定义类型的
最常见方式
,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。
结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["yueqi","yueba"];
}
Person.prototype = {
constructor: Person,
sayName: function(){
console.log(this.name)
}
}
var person1 = new Person("chuqiao", 29,"fengyun");
var person2 = new Person("yuwenyue", 30, "diezhe");
person1.friends.push("wuxiansheng");
console.log(person1.friends);//["yueqi", "yueba", "wuxiansheng"]
console.log(person2.friends);//["yueqi", "yueba"]
console.log(person1.friends === person2.friends);//false
console.log(person1.sayName ===person2.sayName);//true
//实例属性都是在构造函数中定义的,而由所有实例共享的属性和方法都在原型中定义
E) 动态原型模式
动态原型模式:把所有的把所有信息都封装在了构造函数之中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
if(typeof this.sayName != "function"){
Person.prototype.sayName = function(){
console.log(this.name);
}
}
}
var friend = new Person("chuqiao", 29, "fengyun");
friend.sayName(); //chuqiao
F) 寄生构造函数模式
寄生构造函数模式:创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。
function Person(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
console.log(this.name);
};
return o;
}
var friend = new Person("chuqiao", 29, "fengyun");
friend.sayName();//chuqiao
构造函数在不返回值的情况下,默认会返回新对象的实例。而通过在构造函数的末尾添加一个 return 语句,可以
重写
调用构造函数时返回的值。
//由于不能直接修改 Array 构造函数,因此可以使用这个模式
function SpecialArray(){
//创建一个数组实例
var values = new Array();
//初始化数组的值
values.push.apply(values, arguments)
//给数组实例添加了一个 toPipedString()方法,该方法返回以竖线分隔的数组的值
values.toPipedString = function(){
return this.join("|");
}
//将数组以函数值的形式返回
return values;
}
var colors = new SpecialArray("red", "blue", "green");
console.log(colors.toPipedString());// red|blue|green
G) 稳妥构造函数模式
返回的对象与构造函数或者构造函数的原型属性之间没有关系
;也就是说,构造函数返回的对象与构造函数外部创建的对象没有什么不同。为此,不能依赖 instanceof 操作符来确定对象类型。
所谓稳妥对象,
指的是没有公共属性,而且其方法也不引用 this 的对象
。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用 this;二是不使用 new 操作符调用构造函数。
function Person(name, age, job){
//创建要返回的对象
var o = new Object();
//可以在这里定义私有变量和函数
//添加方法
o.sayName = function(){
//在以这种模式创建的对象中,除了使用sayName()方法之外,没有其他办法访问 name 的值。
console.log(name)
}
//返回对象
return o;
}
//像下面使用稳妥的 Person 构造函数
var friend = Person("chuqiao", 29, "fengyun");
friend.sayName();// chuqiao