2

创建对象

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

JS菌
6.4k 声望2k 粉丝