JavaScript面向对象的程序设计——“建立对象”的注意要点

建立对象

Object 构造函数或对象字面量均可以用来建立单个对象。但这个方法的缺点很是明显:同一个接口建立很可耐多对象会产生大量的重复代码。为了解决这个问题,人们开始使用工厂模式的一种变体。html

工厂模式(摒弃,不推荐)

这个模式没有解决对象识别的问题(即怎样知道一个对象的类型)。如:数组

具体的建立单个对象:函数

var person = {};
person.name = "Oliver";
person.age = 18;
person.sayName = function(){
    return this.Name;
};

改变成工厂模式:this

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);

构造函数模式

构造函数能够建立特定类型的对象。因此,能够建立自定义的构造函数,从而定义自定义对象类型的属性和方法。如:prototype

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

确实至关方便设计

这里必定要记住,构造函数都应该以一个大写字母开头,用来区分其余的函数指针

又如:code

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。htm

在上面这个例子中,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
相关文章
相关标签/搜索