继承的意义
为了方便描述,以下内容以“类”的概念进行说明。
类与对象
封装一类事物共有的属性和行为,其目的是用于创建具体实例,也就是对象。可以说,类是创建对象的模板,没有类也就没有对象。因此,由类到对象,是一种声明变量和分配内存的过程,并非完全意义上的继承。
父类与子类
提到继承,标准意义上是指子类的继承性。
当我们编写类时,发现某个类中有我们所需要的成员,利用继承,就可以直接复用这个类中的成员,避免了再次定义。那么,正在编写的这个类称为子类,被继承的类称为父类,或超类。总而言之,继承是一种由已有的类创建新类的机制。
一个类可以有多个子类,但只有一个父类(Object 除外);Object 是所有类的祖先类,所有类默认都是Object 的子孙类。
instanceof 运算符
当左侧的对象是右侧的类或子类所创建的,返回 true。
例子:
//类
function Person(name) {
this.name = name;
}
//创建对象
var p = new Person("Tom");
console.log(p instanceof Person); //输出 true
console.log(p instanceof Object); //输出 true
类式继承
将父类的实例对象赋值给子类的原型,就是类式继承。
例子:
//父类构造函数
function F_class(name) {
//父类实例属性
this.name = name;
}
//父类原型属性
F_class.prototype.getName = function() {
return this.name;
};
//子类构造函数
function C_class(age) {
//子类实例属性
this.age = age;
}
//子类原型属性
C_class.prototype.getAge = function() {
return this.age;
};
//类式继承
C_class.prototype = new F_class("Tom");
//创建对象
var p = new C_class(20);
类式继承的关键点在于父类 F_class 实例化并赋值给子类 C_class 的原型,从而使得原先存在于父类的实例属性和原型属性现在也存在于子类的原型中。
子类的原型会被其创建的所有实例对象共享,如果恰巧从父类继承的属性中包含一个引用类型值,那么改变某一个实例对象中的此属性,就会对其他实例对象产生影响。
例子:
//父类构造函数
function F_class() {
//父类实例属性
this.tels = [133, 152, 177];
}
//子类构造函数
function C_class() {}
//类式继承
C_class.prototype = new F_class("Tom");
//创建对象 item_1
var item_1 = new C_class();
//item_1 新增值
item_1.tels.push(188);
//创建对象 item_2
var item_2 = new C_class();
console.log(item_2.tels);
//输出:[133, 152, 177, 188]
//说明 item_2 受到 item_1 新增值的影响
类式继承实现的本质是重写子类原型,这也造成了实例对象 constructor 指向不正确的问题。
例子:
//父类构造函数
function F_class() {
//父类实例属性
this.tels = [133, 152, 177];
}
//子类构造函数
function C_class() {}
//类式继承
C_class.prototype = new F_class("Tom");
//创建对象 item
var item = new C_class();
console.log(item.constructor.name);
//输出:F_class
//正常情况下,constructor 属性指向创建此对象的构造函数
可以通过修改子类原型的 constructor 属性,让实例对象重新指向子类的构造函数。
例子:
//父类构造函数
function F_class() {
//父类实例属性
this.tels = [133, 152, 177];
}
//子类构造函数
function C_class() {}
//类式继承
C_class.prototype = new F_class("Tom");
//指向子类构造函数
C_class.prototype.constructor = C_class;
//创建对象 item
var item = new C_class();
console.log(item.constructor.name);
//输出:C_class
借用构造函数式继承
为了解决类式继承中子类原型包含引用类型值所带来的问题,引入了借用构造函数式的继承方式。
实现方式就是在子类内部调用父类的构造函数。
例子:
//父类构造函数
function F_class(name) {
//父类实例属性
this.name = name;
}
//父类原型属性
F_class.prototype.getName = function() {
return this.name;
};
//子类构造函数
function C_class(age) {
//借用构造函数式继承
F_class.call(this, "Tom");
//子类实例属性
this.age = age;
}
//子类原型属性
C_class.prototype.getAge = function() {
return this.age;
};
//创建对象
var p = new C_class(20);
借用构造函数式继承的关键点就是将父类 F_class 中的 this 重新绑定在子类 C_class 的构造函数中,这就相当于直接在子类中定义父类的实例属性。
这类继承由于没有设计到原型,所以父类的原型属性,对于子类而言不可见。
组合式继承
将类式继承和借用构造函数式继承组合到一起,从而发挥二者之长的继承方式。
这样,原型上定义方法实现了函数的复用,构造函数中定义属性保证了每个实例对象都有自己的数据。
例子:
//父类构造函数
function F_class(name) {
//父类实例属性
this.name = name;
}
//父类原型属性
F_class.prototype.getName = function() {
return this.name;
};
//子类构造函数
function C_class(age) {
//借用构造函数式继承
F_class.call(this, "Tom");
//子类实例属性
this.age = age;
}
//子类原型属性
C_class.prototype.getAge = function() {
return this.age;
};
//类式继承
C_class.prototype = new F_class();
//指向子类构造函数
C_class.prototype.constructor = C_class;
//创建对象
var p = new C_class(20);
组合式继承虽然融合了两种继承方式的优点,但也造成了父类构造函数被调用两次的问题,可以看到,例子中原型和实例都存在 name 属性。
原型式继承
基于已有的对象创建新对象,ES5 中的 Object.create() 就是标准的原型式继承。
例子:
//原型对象
var person = {
name: "Tom",
getName: function() {
return this.name;
}
}
//原型式继承,创建对象
var p = Object.create(person);
寄生式继承
对原型式继承进行封装,并根据需要增加自己的属性。
例子:
//原型对象
var person = {
name: "Tom",
getName: function() {
return this.name;
}
}
//寄生式继承
function createObject(obj) {
var new_obj = Object.create(obj);
new_obj.age = "20";
return new_obj;
}
//创建对象
var p = createObject(person);
寄生组合式继承
将寄生式继承和借用构造函数式继承组合在一起,就是寄生组合式继承。
与组合式继承唯一不同的就是使用寄生式继承替换类式继承,避免了父类构造函数被调用两次。
例子:
//父类构造函数
function F_class(name) {
//父类实例属性
this.name = name;
}
//父类原型属性
F_class.prototype.getName = function() {
return this.name;
};
//子类构造函数
function C_class(age) {
//构造函数式继承
F_class.call(this, "Tom");
//子类实例属性
this.age = age;
}
//寄生式继承
(function(c_class, f_class) {
//保存父类原型
var item = Object.create(f_class.prototype);
//指向子类构造函数
item.constructor = c_class;
//赋值给子类的原型
c_class.prototype = item;
})(C_class, F_class);
//子类原型属性
C_class.prototype.getAge = function() {
return this.age;
};
//创建对象
var p = new C_class(20);
多继承的实现
一个类只能有一个父类,因此 ES 本身是不能继承多个父类的。但是,jQuery 中的 $.extend() 方法以及 ES6 中的 Object.assign() 方法都通过一些技巧实现了继承多个对象的属性。
例子:
var obj_1 = {
name: "Tom"
},
obj_2 = {
age: 20
};
var p = Object.assign({}, obj_1, obj_2);
我们也可以简单编写一个多继承的函数:
例子:
//自定义 extend() 方法
Object.prototype.extend = function() {
var target = arguments[0];
for (var i = 1, len = arguments.length; i < len; i++) {
var sources = arguments[i];
for (key in sources) {
target[key] = sources[key];
}
}
return target;
}
//测试
var obj_1 = {
name: "Tom"
},
obj_2 = {
age: 20
};
var p = Object.extend({}, obj_1, obj_2);
如有错误,欢迎指正,本人不胜感激。