原型与继承
原型和原型链
在 JavaScript 中,我们可以这样理解,每个实例对象都是通过 new 构造函数()生成的,而构造函数也是对象,因此构造函数也有属性。其中有一个 prototype 属性,我们称之为函数原型。
// 声明一个构造函数Person
function Person(name) {
// this指向实例
this.name = name;
}
// 给构造函数Person的原型上添加sayMyName方法
Person.prototype.sayMyName = function() {
console.log('My name is ' + this.name);
}
// 创建实例并赋值给变量Jack
var jack = new Person('Jack');
jack.sayMyName(); // My name is Jack默认情况下,函数原型也是一个对象,其中有一个 constructor 属性,就是构造函数。
console.log(Person.prototype.constructor === Person); // true另外需要补充一点,每个实例对象都有一个__proto__属性,我们称之为隐式原型,它指向的是创建该实例的构造函数的原型。
console.log(jack.__proto__ === Person.prototype); // true注意
我们不推荐使用__proto__属性,因为它不存在 web 标准中,随时有被删除的可能,我们更推荐使用 Object.getPrototypeOf 方法。
console.log(Object.getPrototypeOf(jack) === Person.prototype); // true通过使用 hasOwnProperty 方法,我们能够知道哪些属性是实例属性。
// name属性来自实例
console.log(jack.hasOwnProperty('name')); // true
// sayName方法不属于实例
console.log(jack.hasOwnProperty('sayName')); // false当我们访问一个实例对象的属性时,会首先查看这个对象本身是否拥有该属性,如果没有,则会从原型中查找,如果找不到,就会从原型的原型中查找,像这样一层一层向上查找,就形成了一条原型链。
关于原型链有两种特殊情况:
- Function 构造函数的隐式原型指向自身的原型
console.log(Object.getPrototypeOf(Function) === Function.prototype); // true- 所有原型链的终点是 Object 构造函数的原型,因此 Object 构造函数的原型上没有隐式原型
console.log(Object.getPrototypeOf(Object)); // null总结一下构造函数、原型和实例的关系:
每个构造函数都有一个 prototype 属性指向原型
每个原型都有一个 constructor 属性执行构造函数
每个实例都是通过 new 构造函数生成
每个实例都有一个内部指针指向原型

继承
原型链继承
原型链作为实现继承的主要方法,它的基本思想就是利用原型让一个引用类型继承另一个引用类型的属性和方法。也就是说,如果我们让原型等于一个父级构造函数的实例,那么此时的原型就有一个内部指针指向父级原型,相应的,父级原型有一个 constructor 属性指向父级构造函数,如此层层递进,就形成了原型链。

原型链继承的代码大致如下:
function Person(gender) {
this.genderList = ['male', 'female'];
this.gender = gender;
}
Person.prototype.sayMyGender = function() {
console.log('I am a ' + this.gender);
}
function Male(name) {
this.name = name;
}
// 继承了Person
Male.prototype = new Person('male');
Male.prototype.sayMyName = function() {
console.log('My name is' + this.name);
}
var tom = new Male('Tom');
tom.genderList.push('unknown');
tom.sayMyGender(); // I am a male
tom.sayMyName(); // My name is Tom
var mike = new Male('Mike');
console.log(mike); // ['male', 'female', 'unknown']
console.log(tom.genderList === mike.genderList); // true优点:
- 父级原型的属性方法可以复用
缺点:
- 父级构造函数的引用类型会被所有子实例共享,子实例修改其中一个引用类型,其他子实例都会受到影响
- 子实例在创建时,不能传递参数给父级构造函数
构造函数继承(经典继承)
为了解决原型链继承的缺点,我们可以使用构造函数继承,它的基本思想就是在子构造函数中调用父构造函数。
function Person(gender) {
this.genderList = ['male', 'female'];
this.gender = gender;
this.sayMyGender = function() {
console.log('I am a ' + this.gender);
}
}
function Female(name) {
Person.call(this, 'female');
this.name = name;
}
Female.prototype.sayMyName = function() {
console.log('My name is ' + this.name);
}
var amy = new Female('Amy');
amy.sayMyGender(); // I am a female
amy.sayMyName(); // My name is Amy
amy.genderList.push('unknown');
var lisa = new Female('Lisa');
console.log(amy.genderList); // output: ['male', 'female', 'unknown']
console.log(lisa.genderList); // output: ['male', 'female']优点:
- 子实例在创建时可以传递参数给父级构造函数
- 每个子实例继承自父级构造函数的引用类型相互独立
缺点:
- 由于借用了父级构造函数,导致所有属性和方法都在构造函数中定义,导致了实例上的方法都是一个新的值,并不是复用(一般情况下属性在构造函数中定义,保证独立性,方法在原型上定义,保证复用性)
组合继承
组合继承是将原型链继承和构造函数继承的技术组合到一起的继承模式,它的基本思想就是使用原型链继承实现方法的继承,使用构造函数继承实现属性的继承。
function Person(gender) {
this.genderList = ['male', 'female'];
this.gender = gender;
}
Person.prototype.sayMyGender = function() {
console.log('I am a ' + this.gender);
}
function Female(name) {
// 继承属性
Person.call(this, 'female'); // 第二次调用父构造函数
this.name = name;
}
// 继承方法
Female.prototype = new Person('female'); // 第一次调用父构造函数
Female.prototype.constructor = Female;
Female.prototype.sayMyName = function() {
console.log('My name is ' + this.name);
}
var amy = new Female('Amy');
amy.sayMyGender(); // I am a female
amy.sayMyName(); // My name is Amy
amy.genderList.push('unknown');
var lisa = new Female('Lisa');
console.log(amy.genderList); // ['male', 'female', 'unknown']
console.log(lisa.genderList); // ['male', 'female']
console.log(amy.sayMyGender === lisa.sayMyGender); // true优点:
- 父级原型的方法可以复用
- 父级构造函数中的引用类型不会被共享
缺点:
- 使用子级构造函数创建实例时,会调用两次父级构造函数,产生两份一样的属性,一份在实例属性上,一份在原型属性上,这造成了不必要的消耗
- 第一次调用时,给子级原型添加父级构造函数的两个属性gender,genderList
- 第二次调用时,给实例Amy添加父级构造函数的两个属性gender,genderList

原型式继承
原型式继承的本质是浅克隆,它会先创建一个临时的构造函数,然后将传入的对象作为这个构造函数的原型,然后返回这个临时构造函数的实例。ES5新增的Object.create方法可以代替这个方法。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
};
var anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');
var yetAnotherPerson = object(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');
console.log(person.friends); // ['Shelby', 'Court', 'Van', 'Rob', 'Barbie']优点:
- 写法简单,不用像写构造函数那样把所有属性写一遍
缺点:
- 与原型链继承一样,引用类型会被所有子实例共享
寄生式继承
寄生式继承其实就是将原型式继承进行了封装,在内部给对象添加函数。
function createAnother(origin) {
var clone = Object.create(origin);
clone.sayHi = function() {
console.log('Hi');
}
}
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // output: Hi优点:
- 写法简单,不用像写构造函数那样把所有属性写一遍
缺点:
- 与构造函数继承一样,方法无法复用
寄生组合式继承(圣杯模式)
寄生组合式继承是将寄生式继承和组合式继承的技术组合到一起的继承模式。主要是为了解决组合继承中,调用了两次父级构造函数的问题。它的基本思想是,使用构造函数继承来实现属性的继承,但不必使用原型链继承来实现方法的继承,我们需要的只是父级原型的副本,通过寄生式继承就可以完成。
function inherit(Son, Father) {
Son.prototype = Object.create(Father.prototype);
Son.prototype.constructor = Son;
}
function Person(gender) {
this.genderList = ['male', 'female'];
this.gender = gender;
}
Person.prototype.sayMyGender = function() {
console.log('I am a ' + this.gender);
}
function Female(name) {
// 继承属性
Person.call(this, 'female'); // 使用构造函数继承来实现属性的继承
this.name = name;
}
// 继承方法
inherit(Female, Person); // 使用寄生式继承来实现方法的继承
Female.prototype.sayMyName = function() {
console.log('My name is ' + this.name);
}
var amy = new Female('Amy');
amy.sayMyGender(); // I am a female
amy.sayMyName(); // My name is Amy
amy.genderList.push('unknown');
var lisa = new Female('Lisa');
console.log(amy.genderList); // ['male', 'female', 'unknown']
console.log(lisa.genderList); // ['male', 'female']
console.log(amy.sayMyGender === lisa.sayMyGender); // true
console.log(amy);
优点:
- 解决了组合式继承的问题,只调用了一次父级构造函数,避免了创建多余的属性