《JS高程》实现继承的6种方式(完整版)
许多OO语言都支持 两种继承方式:
(1)接口继承:只继承方法签名;
(2)实现继承:继承实际的方法。
ECMAScript
由于函数没有签名,无法实现接口继承,因此只支持实现继承,而且主要是依靠原型链来实现的。
1. 原型链 —— 实现继承的主要方法,但是由于其问题,实践中很少会单独使用
基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法。
原型链的实现:让原型对象等于另一个类型的实例,层层递进,构成实例与原型的链条。
实现本质:重写原型对象,代之以一个新类型的实例
实现原型链的基本模式:
// 原型链继承--基本模式
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
/*
* 继承实现方式:通过创建 SuperType 的实例,并将该实例赋给 SubType.prototype
* 实现本质:重写原型对象,代之以一个新类型的实例
*/
SubType.prototype = new SuperType();
// 添加新方法
SubType.prototype.getSubValue = function() {
return this.subproperty;
}
// 创建实例并操作
var instance = new SubType();
alert(instance.getSuperValue()); // true
// instance.constructor 现在指向的是 SuperType
alert(instance.constructor); // SuperType
/*
* 所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针,指向 Object.prototype
*/
// SuperType 继承了 Object,能够调用保存在 Object.prototype 中的那个方法
alert(intance.toString()); //"[object Object]"
/*
* 确定原型和实例的关系
*/
// 方法1:使用 instanceof 操作符
alert(instance instanceof Object); // true
alert(instance instanceof SuperType); // true
alert(instance instanceof SubType); // true
// 方法2:使用 isPrototypeOf() 方法
alert(Object.prototype.isPrototypeOf(instance)); // true
alert(SuperType.prototype.isPrototypeOf(instance)); // true
alert(SubType.prototype.isPrototypeOf(instance)); // true
注意点:
(1)图中,
getSuperValue()
方法仍然还在SuperType.prototype
中,但property
则位于SubType.ptototype
中。这是因为property
是一个实例属性,而getSuperValue()
则是一个原型方法。
(2)
instance.constructor
现在指向的是SuperType
,这是因为原来SubType.prototype
中的constructor
指向了SuperType
的原型,而这个原型对象的contructor
属性指向的是SuperType
。
通过实现原型链,本质上扩展了原型搜索机制。
- 例:调用 instance.getSuperValue() 会经历三个搜索步骤:1)搜索实例;2)搜索 SubType.prototype;3)搜索 SuperType.prototype,最后一步才会找到该方法。
别忘记默认的原型
所有引用类型默认都继承了
Object
所有函数的默认原型都是
Object
的实例,因此默认原型都会包含一个内部指针,指向Object.prototype
。因此,所有自定义类型都会继承
toString()
、valueOf()
等默认方法。
确定实例与原型的关系:1)instanceof
操作符;2)isPrototypeOf()
方法
谨慎地定义方法:给原型添加方法的代码一定要放在替换原型的语句之后
/*
* 谨慎地定义方法:给原型添加方法的代码一定要放在替换原型的语句之后
*/
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
// 继承了 SuperType
SubType.prototype = new SuperType();
// 添加新方法
SubType.prototype.getSubValue = function() {
return this.subproperty;
}
// 重写超类型中的方法,会屏蔽原来的那个方法
SubType.prototype.getSuperValue = function() {
return false;
}
var instance = new SubType();
alert(instance.getSuperValue()); // false
注意:在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样会重写原型链。
/*
* 不能使用对象字面量创建原型方法,因为这样会重写原型链
*/
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
// 继承了 SuperType
SubType.prototype = new SuperType();
/*
* 使用字面量添加新方法,会导致上一行代码无效
* 现在 SubType.prototype 是一个 Object 的实例
*/
SubType.prototype = {
getSubValue : function() {
return this.subproperty;
},
someOtherMethod : function() {
return false;
}
}
var instance = new SubType();
console.log(SubType.prototype); // Object {}
alert(instance.getSuperValue()); // error!
原型链的问题:
主要问题:来自包含引用类型值的原型。
—— 在通过实例来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。
问题2:在创建子类型的实例时,不能向超类型的构造函数中传递参数(在不影响所有对象实例的情况下)。
/*
* 原型链的问题
*/
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {
}
// 继承了 SuperType
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // ["red", "blue", "green", "black"]
var instance2 = new SubType();
console.log(instance2.colors); // ["red", "blue", "green", "black"]
2. 借用构造函数(伪造对象或经典继承)—— 实践中很少单独使用
优点:
解决原型中包含引用类型值所带来的问题。
可以在子类型构造函数中向超类型构造函数传递参数。
基本思想:在子类型构造函数的内部调用超类型构造函数。
// 借用构造函数实现继承
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {
// 继承了 SuperType
SuperType.call(this); // 在新创建的对象上执行构造函数
}
var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // ["red", "blue", "green", "black"]
var instance2 = new SubType();
console.log(instance2.colors); // [red", "blue", "green"]
通过
call()
方法或apply()
方法,实际上是在(未来将要)新创建的 SubType 的实例的环境下调用了 SuperType 构造函数。
这样一来,会在新 SubType 对象上执行 SuperType() 函数中定义的所有对象初始化代码
结果,SubType 的每个实例都会有自己的 colors 属性的副本了
传递参数:
// 传递参数
function SuperType(name) {
this.name = name;
}
function SubType() {
// 继承了 SuperType,同时还传递了参数
SuperType.call(this, "Nicholas");
// 实例属性
this.age = 29;
}
var instance = new SubType();
alert(instance.name); // "Nicholas"
alert(instance.age); // 29
// 实例的 constructor 属性指向 SubType,因此无法使用原型中定义的方法
console.log(instance.constructor); // function SubType() {}
借用构造函数的问题:
1)无法避免构造函数模式存在的问题——方法都在构造函数中定义,无法实现函数复用。
2)在超类型的原型中定义的方法,对子类型而言是不可见的,结果所有类型都只能使用构造函数模式。
3. 组合继承(伪经典继承)—— 最常用的继承模式
优点:将原型链和借用构造函数的技术组合到一块,发挥二者之长。
基本思想:使用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承。
不足:无论什么情况下,都会调用两次超类型构造函数,一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。——>子类型最终会包含超类型对象的全部实例属性,我们不得不在调用子类型构造函数时重写这些属性。
// 组合继承
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
alert(this.name);
}
function SubType(name, age) {
// 继承属性
SuperType.call(this, name); // 第二次调用 SuperType(),子类型构造函数内部调用
this.age = age;
}
// 继承方法
SubType.prototype = new SuperType(); // 第一次调用 SuperType(),创建子类型原型时调用
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
alert(this.age);
}
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); // ["red", "blue", "green", "black"]
instance1.sayName(); // "Nicholas"
instance1.sayAge(); // 29
var instance2 = new SubType("Greg", 27);
console.log(instance2.colors); // [red", "blue", "green"]
instance2.sayName(); // "Greg"
instance2.sayAge(); // 27
// 只是改变了 constructor 的指向,__proto__ 的指向没有变,还是通过原型链进行连接
alert(instance1 instanceof Object); // true
alert(instance1 instanceof SuperType); // true
alert(instance1 instanceof SubType); // true
alert(Object.prototype.isPrototypeOf(instance1)); // true
alert(SuperType.prototype.isPrototypeOf(instance1)); // true
alert(SubType.prototype.isPrototypeOf(instance1)); // true
instnceof
和isPrototypeOf()
方法能够用于识别基于组合继承创建的对象
第一次调用
SuperType
构造函数时,SubType.ptototype
会得到两个属性:name
和colors
;它们都是 SuperType 的实例属性,只不过现在位于 SubType 的原型中。
当调用
SubType
构造函数时,又会调用一次SuperType
构造函数,这一次又在新对象上创建了实例属性name
和colors
。于是,这两个属性就屏蔽了原型中的两个同名属性。
4. 原型式继承
没有严格意义上的构造函数
借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
// 原型式继承
function object(o) {
// 先创建一个临时性的构造函数
function F() {}; // 将传入的对象作为这个构造函数的原型
F.prototype = o; // 返回这个临时类型的新实例
return new F();
} // 本质上来讲,object() 对其中的对象执行了一次浅复制
var person = {
name : "Nicholas", // 基本类型值属性
friends : ["Shelby", "Court"] // 引用类型值属性
}; var person1 = object(person);
person1.name = "Greg";
person1.friends.push("Rob"); var person2 = object(person);
person2.name = "Linda";
person2.friends.push("Barbie"); console.log(person.friends); // ["Shelby", "Court", "Rob", "Barbie"] /*
* Object.create() 方法:规范化了原型式继承
*/
var person3 = Object.create(person);
person3.name = "Allie";
person3.friends.push("Anna"); var person4 = Object.create(person);
person4.name = "John";
person4.friends.push("Mark"); console.log(person.friends); // ["Shelby", "Court", "Rob", "Barbie", "Anna", "Mark"] // Object.create() 传入第二个参数,会覆盖原型对象上的同名属性
var person5 = Object.create(person, {
name: {
value: "Don"
}
}) console.log(person5.name); // Don
这种原型式继承,必须要有一个对象可以作为另一个对象的基础。如果有一个对象的话,可以把它传递给
object()
函数,然后再根据具体需求对得到的对象加以修改即可。在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。
注意:同使用原型模式一样,包含引用类型值的属性
始终都会共享相应的值。
ECMAScript 5 通过新增的 Object.create()
方法 规范化了原型式继承:
两个参数:1)用作新对象原型的对象;2)为新对象定义额外属性的对象(可选)。
- 在传入一个参数的情况下,
Object.create()
与object()
方法的行为相同。 - 第二个参数与
Object.defineProperties()
方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。
5. 寄生式继承
思路:与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。
用处:在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式
不足:类似构造函数模式,使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率
// 寄生式继承
function object(o) {
function F() {}; // 先创建一个临时性的构造函数
F.prototype = o; // 将传入的对象作为这个构造函数的原型
return new F(); // 返回这个临时类型的新实例
}
/*
* 利用原型式继承所创建的 object() 函数,但不是必须的
* 任何能够返回新对象的函数都是用于此模式
*/
function createAnother(original) {
var clone = object(original); // 通过调用函数创建一个新对象
clone.sayHi = function() { // 以某种方式来增强这个对象
alert("hi");
};
return clone; // 返回这个对象
}
// 使用
var person = {
name : "Nicholas",
friends : ["Shelby", "Court"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // hi
6. 寄生组合式继承 —— 实现基于类型继承的最有效方式
寄生组合式继承:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
基本思路:不必为了指定子类型的原型二调用超类型的构造函数。我们所需要的无非就是超类型原型的一个副本而已。
本质:就是使用寄生式继承来继承超类型的原型,然后将结果指定给子类型的原型。
优点:解决组合式继承两次调用超类型构造函数的问题。
// 寄生组合式继承
function object(o) {
function F() {}; // 先创建一个临时性的构造函数
F.prototype = o; // 将传入的对象作为这个构造函数的原型
return new F(); // 返回这个临时类型的新实例
}
function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype); // 创建对象,即创建超类型原型的副本
prototype.constructor = subType; // 增强对象,即为创建的副本添加 contructor 属性
subType.prototype = prototype; // 指定对象,即将新创建的对象(副本)赋值给子类型的原型
}
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
alert(this.name);
}
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
alert(this.age);
}
var instance = new SubType("Nicholas", 29);
instance.sayName(); // "Nicholas"
instance.sayAge(); // 29
小结
JavaScript 主要通过原型链实现继承。
原型链的构建:通过将一个类型的实例赋值给另一个构造函数的原型实现的。这样子,子类型就能够访问超类型的所有属性和方法,这一点与基于类的继承很相似。
原型链的问题:对象实例共享所有继承的属性和方法,因此不适宜单独使用。
解决技术:借用构造函数,即在子类型构造函数的内部调用超类型构造函数。这样可以做到每个实例都具有自己的属性,同时还能保证只使用构造函数模式来定义类型。
使用最多的继承模式是组合继承,这种模式使用原型链继承共享的属性和方法,而通过借用构造函数继承实例属性。
此外,还存在下列可供选择的继承模式:
原型式继承,可以在不必预先定义构造函数的情况下实现继承,其本质是执行对给定对象的浅复制。而复制得到的副本还可以得到进一步改造。
寄生式继承,与原型式继承非常相似,也是基于某个对象或某些信息创建一个对象,然后增强对象,最后返回对象。为了解决组合继承模式由于多次调用超类型构造函数而导致的低效率问题,可以将这个模式与组合继承一起使用。
寄生组合式继承,集寄生式继承和组合继承的优点与一身,是实现基于类型继承的最有效方式。
《JS高程》实现继承的6种方式(完整版)的更多相关文章
- 《JS高程》创建对象的7种方式(完整版)
一.理解对象 ECMA-262定义对象:无序属性的集合,其属性可以包含基本值.对象或者属性. 我们可以把 ECMAScript 的对象想象成 散列表:无非就是一组 名值对,其中值可以是数据或函数. 创 ...
- js中实现继承的几种方式
首先我们了解,js中的继承是主要是由原型链实现的.那么什么是原型链呢? 由于每个实例中都有一个指向原型对象的指针,如果一个对象的原型对象,是另一个构造函数的实例,这个对象的原型对象就会指向另一个对象的 ...
- js中原型继承的三种方式
- js 实现继承的几种方式
//js中实现继承的几种方式 //实现继承首先要有一个父类,先创造一个动物的父类 function Animal(name){ this.name = name; this.shoot = funct ...
- js实现继承的5种方式 (笔记)
js实现继承的5种方式 以下 均为 ES5 的写法: js是门灵活的语言,实现一种功能往往有多种做法,ECMAScript没有明确的继承机制,而是通过模仿实现的,根据js语言的本身的特性,js实现继承 ...
- javascript(js)创建对象的模式与继承的几种方式
1.js创建对象的几种方式 工厂模式 为什么会产生工厂模式,原因是使用同一个接口创建很多对象,会产生大量的重复代码,为了解决这个问题,产生了工厂模式. function createPerson(na ...
- js 实现继承的6种方式(逐渐优化)
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8&quo ...
- JavaScript学习12 JS中定义对象的几种方式【转】
avaScript学习12 JS中定义对象的几种方式 转自: http://www.cnblogs.com/mengdd/p/3697255.html JavaScript中没有类的概念,只有对象. ...
- 前端知识体系:JavaScript基础-原型和原型链-实现继承的几种方式以及他们的优缺点
实现继承的几种方式以及他们的优缺点(参考文档1.参考文档2.参考文档3) 要搞懂JS继承,我们首先要理解原型链:每一个实例对象都有一个__proto__属性(隐式原型),在js内部用来查找原型链:每一 ...
- JavaScript学习12 JS中定义对象的几种方式
JavaScript学习12 JS中定义对象的几种方式 JavaScript中没有类的概念,只有对象. 在JavaScript中定义对象可以采用以下几种方式: 1.基于已有对象扩充其属性和方法 2.工 ...
随机推荐
- 在浏览器中将表格导入到本地的EXCEL文件,注意控制内存
if ($export_flag == 1) { $rr = $this->mdl->test($test); header("Content-Type: application ...
- Windows获取文件大小
Windows最初的设计允许我们处理非常大的文件,所以最初的设计者选用64位值来表示文件大小.但是我们在日常处理过程中文件大小一般不会超过4GB.故Windows提供了两个联合类型的数据结构表示文件大 ...
- 从BlackHat2013中我们收获了什么
拉斯维加斯-BlackHat全球黑客大会是每年围观革新安全技术的最好机会,还能和那些 在这个行业里聪明至极的家伙交谈并从中得到些关于前沿技术的动向和启示.今年的会议无论参会人数还是议题数量是历届规模最 ...
- 用C#操作vss、msbuild、reactor
一.命令行 凡是支持命令行的工具,都可以通过cmd.exe操作.如下: var p = new Process(); p.StartInfo.FileName = "cmd.exe" ...
- bzoj 1036 Tree Count
题目大意:给出一棵树,每个点有一个权值,要求三种操作:1.修改某个点的权值,2.询问x到y路径上各点的权值最大值,3.询问x到y路径上各点的权值之和. #include <cstdio> ...
- hdu 1034 (preprocess optimization, property of division to avoid if, decreasing order process) 分类: hdoj 2015-06-16 13:32 39人阅读 评论(0) 收藏
IMO, version 1 better than version 2, version 2 better than version 3. make some preprocess to make ...
- SOD 精选细节--常用工具
要明白一个思想: SOD 只是帮你拼接sql语句, 用简单的方式来帮你实现. 不要理解错了.这很重要的! 查询: TB table=new TB(); table.Name="111& ...
- C语言实例代码
绘制余弦曲线和直线 #include #include int main() { double y; int x,m,n,yy; for(yy=0;yy<=20;yy++) {y=0.1*yy; ...
- Ubuntu 13.04设置root用户
1 .设置root用户密码:passwd root 输入密码 2 .编辑lightdm.conf gedit /etc/lightdm/lightdm.conf 最后加: greeter-show-m ...
- NABCD分析
NABCD——今日事 N(Need):开创的成就系统可以在一定程度上督促用户坚持下来. A(Approach):做一个APP软件,是在android平台构建. B(Benefit):可以逐步改变用户的 ...