创建对象
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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。