每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处是,在它上面定义的属性和方法可以被实例对象共享。原来在构造函数中直接赋值给对象实例的值,可以直接赋值给它们的原型。
function Person() { }
Person.prototype.name = 'Jackson';
Person.prototype.age = 30;
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function(){
console.log(this.name);
};
let person1 = new Person();
person1.sayName(); // 'Jackson'
let person2 = new Person();
person2.sayName(); // 'Jackson'
所有属性和方法都直接添加到了Person的prototype属性上,构造函数体中什么也没有。这里和构造函数模式不同,构造函数模式中新创建出来的实例中它们的属性方法虽然名字都相同,但却不相等,而使用这种原型模式定的属性和方法是由所有实例共享的,因此上面代码中person1和person2访问的属性和方法都是相等的。
理解原型
无论何时,只要创建一个函数,就会按照规则为这个函数创建一个prototype属性,该属性指向原型对象。默认情况下,所有原型对象自动获得一个名为constructor的属性,指向与之关联的构造函数。
// 声明一个构造函数
function Person() {}
// 声明好构造函数之后,该函数就有了一个与之关联的原型对象
console.log(Person.prototype);
/*
{
constructor: f Person(),
_proto_: Object
}
*/
// 构造函数有一个prototype属性引用其原型对象,而原型对象也有一个constructor属性引用这个构造函数,他们两者是循环引用的;
Person.prototype.constructor === Person
// 正常的原型链都会终止于Object的原型对象,Object的原型是null
Person.prototype._proto_ ===Object.prototype;
Person.prototype._proto_.constructor === Object;
Person.prototype._proto_._proto_ === null;
Object._proto_ ===null;
let person1 = new Person(),
person2 = new Person();
// 构造函数Person、原型对象Person.prototype和实例(person1、person2)是三个完全不同的对象
/*
* 实例通过_proto_链接到原型对象,
*构造函数通过prototype属性链接到原型对象
*实例与构造函数没有直接联系,与原型对象有直接联系
*/
person1._proto_ === Person.prototype ;
person1._proto_.constructor === Person;
// instanceof 检查实例的原型链中是否包含指定构造函数的原型:
console.log( person1 instanceof Person ) // true
console.log( person1 instanceof Object ) // true
console.log( Person.prototype instanceof Object ) // true
其它原型语法
在前面的例子中,每次定义一个属性或者方法的时候都需要把Person.prototype重写一遍,为了减少代码的冗余,可以直接通过一个包含所有属性和方法的对象字面量来重写原型。
function Person() {}
Person.prototype = {
name: "Jackson",
age: 30,
sayName(){
console.log(this.name)
}
}
但是这样编写的话,就会存在一个问题,那就是原型对象的constructor属性就不指向Person了,此时它指向了完全不同的新对象(Object构造函数)
let friend = new Person();
friend.constructor == Person //false
friend.constructor == Object //true
这样的话我们也可以直接在重写的时候设置一下它的constructor值
Person.prototype = {
constructor: Person,
name: "Jackson",
age: 30,
sayName(){
console.log(this.name)
}
}
//这样去写又会存在一个问题,那就是此时的constructor是可以枚举出来的
console.log(Object.keys(Person.prototype)) // ["constructor","name","age"]
//但是在原生中constructor是不可被枚举的,因此在这里可以借助 Object.defineProperty()方法来定义constructor
Object.defineProperty(Person.prototype,"constructor",{
enumerable: false,
value: Person
});
console.log(Object.keys(Person.prototype)) // ["name","age"]
原型的问题
原型模式也是存在一些问题的,首先,它弱化了向构造函数传递初始化参数的能力,会导致所有实例对默认都取得相同的属性值,其次,最大的问题是原型的共享特性。有些属性是可以通过在实例上添加同名属性去遮蔽的,但有的属性缺不能,比如包含引用值的属性。
function Person() {}
Person.prototype = {
name: "Jackson",
age: 30,
arr: [1,2,3]
}
let person1 = new Person();
let person2 = new Person();
person1.arr.push(4);
console.log(person1.arr) // 1,2,3,4
console.log(person2.arr) // 1,2,3,4
如上所示,person1与person2实例,修改了person1的arr属性,person2的arr也会被修改,因为他们都指向Person.prototype.arr。