前言
继承也是我去年面试中支支吾吾答不上来的一个题,现在虽说还没有完全吃透它,不过一点一滴地去接触、思考、理解、实践,总能吃透它的。我为什么把创建对象和继承放在一起说呢?那是因为对象的创建是理解继承的前提,继承,那当然是对象的继承。
一、创建对象
虽然Object构造函数和对象字面量都可以用来创建单个对象,但是它们都有一个明显的缺点:使用同一个接口创建很多对象,会产生大量重复的代码。所以,产生了下面几种方式:
1、工厂模式(Factory)
工厂模式是软件工程领域广为人知的一种设计模式,它抽象了创建具体对象的过程,用函数来封装以特定接口创建对象的细节。
function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = createPerson("Nicholas", 23, "Software Engineer");
var person2 = createPerson("Greg", 32, "Doctor");
console.log(person1 instanceof Object); // true
console.log(person2 instanceof Object); // true
这种模式解决了创建多个相似对象的问题,但是却无法识别对象具体的类型。person1和person2的类型都是Object,无法区分。
2、构造函数模式(Constructor)
ECMAScript中的构造函数可用来创建特定类型的对象。像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中,也可以创建自定义的构造函数。由此改写前面的例子如下:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var person1 = new Person("Nicholas", 23, "Software Engineer");
var person2 = new Person("Greg", 32, "Doctor");
console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true
这种方式与工厂模式的不同之处在于:
(1)没有显式地创建对象;
(2)直接将属性和方法赋给了this对象;
(3)没有return语句。
(4)函数名Person首字母大写
要创建Person的实例,必须使用new操作符。这种方式,实例person1和person2既是Object的实例,又是Person的实例(所有的对象均继承自Object),可说它们是Person类型,这正是它胜过工厂模式的地方。但是它并非没有缺点,就是每个方法都要在每个实例上重新创建一遍。把sayName稍微更改一下就看的一清二楚了:
this.sayName = new Function("alert(this.name)");
所以
console.log(person1.sayName == person2.sayName); // false
两个实例完成同一任务却创建两个Function实例是没有必要的;况且有this对象在,根本没有必要在执行代码前就把函数绑定到特定对象上。不过若是把sayName放到全局作用域中,就避免了这个问题。可是如果对象有很多方法,那就要在全局作用域中定义很多函数,并且只能为某个对象调用,这样就毫无封装性可言了。所以另寻他法吧!
3、原型模式(Prototype)
我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。按照字面意思理解就是prototype就是通过调用构造函数而创建的那个对象的原型对象。那么使用原型模式创建对象的好处是可以让所有对象实例共享它所包含的属性和方法,换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。
function Person(){}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); // "Nicholas"
var person2 = new Person();
person2.sayName(); // "Nicholas"
console.log(person1.sayName == person2.sayName); // true
由此可见,person1和person2都有相同的属性和方法。不过与构造函数不同的是,这种方式创建的新对象的属性和方法是由所有实例共享的。
这种方式也是有缺点的,主要体现在有引用类型值得属性上。看例子:
function Person(){}
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
friends: ["Shelby", "Court"],
sayName : function(){
alert(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.name = "Greg";
person1.friends.push("Van");
console.log(person1.friends); // ["Shelby", "Court", "Van"]
console.log(person2.friends); // ["Shelby", "Court", "Van"]
console.log(person1.name); // "Greg"
console.log(person2.name); // "Nicholas"
console.log(person1.friends === person2.friends); // true
更改其中一个实例的无引用类型的属性,不会影响其他实例的此属性。但是更改了有引用类型的属性后就会影响其他实例的此属性。就如例子中所示,明明更改的是person1的friends,但是person2的friends属性值随之更改,并且两者完全相等。于是组合使用构造函数模式和原型模式就出现了。
4、组合使用构造函数模式和原型模式(Constructor and Prototype)
这种方法的思想是:构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。而且,它支持向构造函数传递参数。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype = {
constructor: Person,
sayName : function(){
alert(this.name);
}
};
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 23, "Doctor");
person1.friends.push("Van");
console.log(person1.friends); // ["Shelby", "Court", "Van"]
console.log(person2.friends); // ["Shelby", "Court"]
console.log(person1.friends == person2.friends); // false
console.log(person1.sayName == person2.sayName); // true
这种模式目前使用最广泛、认同度最高的一种创建自定义类型的方法。
二、继承
JavaScript中的继承都属于实现继承,它主要是依靠原型链来完成的。
1、原型链(Prototype Chain)
原型链的基本思想是:利用原型让一个引用类型继承另一个引用类型的属性和方法,它描述的是实例与原型的关系链条。其实现继承的基本模式如下:
function A(){
this.property = true;
}
A.prototype.getProperty = function(){
return this.property;
};
function B(){
this.name = "Nicholas";
}
// 继承了A
B.prototype = new A();
B.prototype.getName = function(){
return this.name;
};
var instance = new B();
instance.getProperty(); // true
本例的原型链如下图所示:(别忘了所有函数的默认原型都是Object的实例)
这种方式有两个问题:(1)同原型模式创建对象一样,包含引用类型值的原型属性会被所有实例共享,因此某个实例的此属性更改会影响其他的实例;(2)在创建子类型的实例时,不能像超类型的构造函数中传递参数。
2、借用构造函数(Constructor)
这种方式的思想是:在子类型构造函数的内部调用超类型的构造函数。
function A(){
this.colors = ["red", "green", "yellow"];
}
function B(){
A.call(this);
}
var instance1 = new B();
instance1.colors.push("black");
console.log(instance1.colors); // ["red", "green", "yellow", "black"]
var instance2 = new B();
console.log(instance2.colors); // ["red", "green", "yellow"]
这种模式的问题是:方法都在构造函数中定义,无法做到函数复用。而且,在超类型的原型上定义的方法,对于子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。
3、组合继承(Combination)
组合继承,有时候也叫伪经典继承,指的是将原型链和借用构造函数的技术结合到一块,从而发挥二者之长的一种模式。它的思想是:使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
function A(name){
this.name = name;
this.colors = ["red", "green", "yellow"];
}
A.prototype.sayName = function(){
console.log(this.name);
};
function B(name, age){
// 继承属性
A.call(this, name);
this.age = age;
}
B.prototype = new A();
B.prototype.constructor = B;
B.prototype.sayAge = function(){
console.log(this.age);
};
var instance1 = new B("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); // ["red", "green", "yellow", "black"]
instance1.sayName(); // "Nicholas"
instance1.sayAge(); // 29
var instance2 = new B("Greg", 27);
console.log(instance2.colors); // ["red", "green", "yellow"]
instance2.sayName(); // "Greg"
instance2.sayAge(); // 27
组合继承是JavaScript中最常用的继承模式。