构造函数、原型对象与实例
抛开「构造」二字,只要创建了一个函数,就会有与其对应的原型对象。其关系如下:
/* 函数 */
function Person () { ... }
/* 原型对象 */
Person.prototype = {
constructor: Person
}
复制代码
函数 Person
的 prototype
属性指向其原型对象,原型对象 Person.prototype
的 constructor
属性又反过来指向了函数。二者通过这种关系彼此关联起来。
回到构造函数上来。如果此时我们使用构造函数 Person
新建了一个实例 person1
,实例与构造函数间是没有直接联系的,实例的 __proto__
属性指向构造函数的原型对象 Person.prototype
。因此,访问实例的构造函数,只能通过间接地在原型对象中拿到 constructor
属性。
var person1 = new Person();
console.log( person1.__proto__ === Person.prototype ); /* true */
console.log( person1.__proto__.constructor === Person ); /* true,此处仅仅是为了演示,实际上不必显式地写 __proto__ ,JS会自动地去原型链上寻找 constructor 属性 */
复制代码
创建对象
1. 工厂模式
除了使用字面量创建对象以外,工厂模式是最简单的创建对象的方法。
function createPerson (name, age) {
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function () {
console.log(this.name);
};
return o;
}
var person1 = createPeron('Nicholas', 29);
var person2 = createPeron('Grey', 27);
复制代码
使用工厂模式,每次总是新建并返回一个全新的对象。可以看出,person1
与 person2
仅仅是有相同名字的属性和函数,但二者之间没有任何关联,与 createPerson
工厂函数不存在关系,更没有原型对象,因此我们无法识别其类型。
2. 构造函数模式
使用 new
操作符,可以将普通函数用作构造函数。实际上是将 Person
在 new
的空白对象作用域中执行。
function Person (name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
};
}
var person1 = new Person('Nicholas', 29);
var person2 = new Person('Grey', 27);
复制代码
这里正是我们在第一节中描述的构造函数、原型对象和实例的关系。构造函数 Person
有一个原型对象 Person.prototype
,两个实例 person1
和 person2
的 __proto__
均指向了这个原型对象,实例通过原型对象访问到构造函数 Person
,借助于这个关系可以解决新建对象的类型识别问题。
但在构造函数中的 sayName
方法,其实是每次 new
时新建的,两个函数是在内存中独立存在。
console.log( person1.sayName === person2.sayName ); /* false */
复制代码
由于其完成相同的功能,应将其视为一个「公共函数」。完成相同功能的「公共函数」,没必要在内存中存在两个副本。把 sayName
的定义转移到构造函数外可以解决这个问题。
function sayName () {
console.log(this.name);
}
function Person (name, age) {
this.name = name;
this.age = age;
this.sayName = sayName;
}
var person1 = new Person('Nicholas', 29);
var person2 = new Person('Grey', 27);
console.log( person1.sayName === person2.sayName ); /* true */
复制代码
公共函数 sayName
现在在内存中仅有一个副本了。但新的问题出现了,它被暴露在全局环境下,任何对象都可调用它。这不该是一个对象的内部函数该有的情况。继续改进,使用原型模式!
3. 原型模式
前面我们提到,每个构造函数都有一个原型对象,实例可以通过 __proto__
访问原型对象。当访问实例的属性时,JS 会借助 __proto__
自动地在原型链上一层层地向上寻找目标属性名对应的属性值。
由于构造函数在内存中只有一份,所以原型对象也只有一份。原型模式就是把属性和方法都定义在原型对象中。
function Person () {
}
Person.prototype.name = 'Nicholas';
Person.prototype.age = age;
Person.prototype.sayName = function () {
console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();
复制代码
基于原型链的特性,当我们访问实例 person1
的 name
属性时,返回的是原型对象中的 name
属性。由于原型对象只有一份,所以使用构造函数 Person
创建的实例共享同一份属性和函数。
显然,我们希望每个实例有自己独立的属性。可以在实例中创建同名属性,在原型链的最前端「屏蔽」原型对象中的属性,这样创建的属性就是属于每个实例的了。
console.log(person1.name); /* Nicholas */
console.log(person2.name); /* Nicholas */
person1.name = 'Rob';
console.log(person1.name); /* Rob - 实例属性 */
console.log(person2.name); /* Nicholas - 原型对象属性 */
delete person1.name; /* delete 用于删除实例属性 */
console.log(person1.name); /* Nicholas - 原型对象属性 */
console.log(person2.name); /* Nicholas - 原型对象属性 */
复制代码
这种「添加同名属性」来「隐藏原型属性」的方法对于基本类型来讲勉强可以使用。然而,在这个问题上,给引用类型带来的麻烦更为突出。
function Person () {
}
Person.prototype.name = 'Nicholas';
Person.prototype.friends = ['Shelby', 'Court'];
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push('Van');
console.log(person1.friends); /* 'Shelby, Court, Van' */
console.log(person2.friends); /* 'Shelby, Court, Van' */
复制代码
我们使用原型模式的初衷是,让需要共享的方法在实例间共享。令人苦恼的是,不需要共享的属性也被共享了。有没有一种方法可以自由地决定属性的共享/不共享呢?
4. 组合使用构造函数模式与原型模式
我们在使用构造函数时,内部函数 sayName
是非共享的,每个实例拥有一份 sayName
函数的副本。准确的讲,构造函数模式下所有的属性和方法都是在实例中定义的,因此是非共享的。我们利用这一点来改造原型模式。
核心思想是,将非共享属性放在构造函数中定义,最终这些非共享属性将各自属于自己的实例;将方法和共享属性放在原型对象中,所有实例共同使用一份副本。
function Person (name, age) {
this.name = name;
this.age = age;
this.friends = ['Shelby', 'Court'];
}
Person.prototype.sayName = function () {
console.log(this.name);
};
var person1 = new Person('Nicholas', 29);
var person2 = new Person('Greg', 27);
复制代码
这是目前 ECMAScript 中使用最广泛、认同度最高的一种创建自定义类型的方法。
原型模式的另一种简单写法
上面我们介绍的原型模式,都是在 Person.prototype
直接添加属性/方法,写法是这样的:
Person.prototype.name = 'Nicholas';
Person.prototype.age = 27;
Person.prototype.sayName = function () {
console.log(this.name);
};
复制代码
如果我们使用字面量的方式重写,代码会简介得多:
Person.prototype = {
name: 'Nicholas',
age: 27,
sayName: function () {
console.log(this.name);
};
}
复制代码
但回想第一节介绍的知识,原型对象 Person.prototype
的属性 constructor
指向了构造函数 Person
,实例 person1
的 __proto__
属性指向了原型对象 Person.prototype
,这是从实例寻找构造函数的唯一途径。当我们用字面量的方式重写原型对象时,原型对象中的 constructor
会指向字面量创建对象的构造函数 Object
。这是我们不希望看到的,我们再手动把 constructor
的指向修正为 Person
。
Person.prototype = {
constructor: Person,
name: 'Nicholas',
age: 27,
sayName: function () {
console.log(this.name);
};
}
复制代码
按照这种方式使用原型模式就比较稳妥了。
欢迎大家指正、补充!