JavaScript(7) 继承

javascript中实现继承有多种方法,各有优劣,具体还是看需求。

构造函数实现继承

使用构造函数的方式实现继承比较常见。

call和apply实现继承

使用call和apply方法实现继承

原型链(Prototype Chain)实现继承

每个函数都是基于一个prototype创建。

查看函数创建对象的属性可以看到prototype,不过不同的浏览器对其命名不一样,chrome命名为_proto_(但是在编程时prototype可以访问),下面是在chrome种查看其属性时的截图。可以看到,prototype包含一个指向构造函数的引用,一些自己添加的属性和方法(sayName),还有指向另一个原型的引用。


所以现在可以理解prototype chain了,原型本身也是有原型的,这样就组成了链!这像极了java中的继承,父类也有可能有父类的,java中的终极父类是Object,javascript也是Object,在上图中你看到最下面那个_proto_了吗?它指向Object。

prototype可以修改,在上面的代码中为People的prototype添加了一个sayName的函数,那么所有People的实例都能访问此函数。扩展下,如果我们指定People的实例为另一个构造函数的prototype,则模拟出继承:

var People = function(name,age){
	this.name = name;
	this.age = age;
}
People.prototype.sayName=function(){alert(this.name);};
//
var Lucy = function(id,address){
	this.id = id;
	this.address = address;
}
//指定Lucy的原型为People的一个实例
Lucy.prototype = new People("Lucy",12);

var lucy1 = new Lucy("23265466","UK");
//原型的属性
alert(lucy1.age);//12
//原型的原型的方法
lucy1.sayName();//Lucy


原型链与作用域链

你可能会思考如果构造函数和原型中有同样的属性和方法,那么调用次序是什么样的?答案是从实例方法开始,到实例的原型,再到原型的原型,依次...是不是感觉很像作用域链?但这不是作用域链,一条原型链存在于作用域链中的一个节点,它们只不过相似罢了。程序的设计中处处存在这样微妙,令人迷惑的地方,相似但不是,就跟从一叶脉络可见整棵树,从一粒沙子表面可见山川河谷,伟大只是局部的重复。

var FuncA = function(){
	this.name = "FuncA";
	this.sayName = function(){
		return this.name;
	}
}

var FuncB = function(){
	this.name = "FuncB";
}

FuncB.prototype = new FuncA();
var b = new FuncB();
alert(b.sayName());//FuncB,调用对象层中的name
delete b.name;//删除对象中的name
alert(b.sayName());//FuncA 调用对象原型中的name

注意,创建的对象,其中的property的configurable默认为true(比如b),可以使用delete删除,但是构造函数中的property的configurable是false(比如FuncA),是无法删除的。在代码运行过程中可以对构造函数创建的对象进行修改,但是无法修改构造函数的定义,详细请参阅 JavaScript№6 Attributes


原型链继承的陷阱

程序语言是计算机科学家为程序员制作的工具,工具自然有很多功能,这些功能组合会满足大部分需求,但是组合的可能非常之多,包括巨多的没有意义的破坏性的组合(很多变态面试官喜欢用这些可能来面试程序员,以此来考察程序员的熟练程度,这类似于厨师面试时问“如果把屎放在锅里炒会出现什么情况?”——因为这也是一种可能)。

可是在用一些一知半解的技术时会非常危险,比如这个沧海一粟的原型链继承,请看下面的例子。

//原始人类
function OriginHuman(){
	this.DNA = ["A","B","C"];
	this.eyeCount = 3;
}

function CurrentHuman(){}
//近代人的原型是原始人
CurrentHuman.prototype = new OriginHuman();

//中国人
var chinese = new CurrentHuman();
//中国人进化出了抗污染基因
chinese.DNA.push("Ant-pollution");
chinese.eyeCount = 2;

//日本人
var japanese = new CurrentHuman();
alert(japanese.DNA);//A,B,C,Ant-pollution
alert(japanese.eyeCount);//3

这是为什么呢,日本人的基因发生变化了,但是眼睛数量没有发生变化?因为chinese.eyeCount=2实际上在chinese对象中添加了一个新属性,覆盖了prototype中的同名属性。而对DNA的push操作是修改,它能直接访问到prototype中的DNA数组。 


改进的继承方法

下面使用的方法称为构造窃取(Constructor Stealing),在子构造函数执行环境中执行父构造函数方法:

//原始人类
function OriginHuman(){
	this.DNA = ["A","B","C"];
	this.eyeCount = 3;
}

function CurrentHuman(){
	OriginHuman.call(this);//Contructor Stealing
}

//中国人
var chinese = new CurrentHuman();
//中国人进化出了抗污染基因
chinese.DNA.push("Ant-pollution");
chinese.eyeCount = 2;
alert(chinese.DNA);//A,B,C,Ant-pollution

//日本人
var japanese = new CurrentHuman();
alert(japanese.DNA);//A,B,C
alert(japanese.eyeCount);//3

Constructor Stealing 已经和原型链没有关系了。

故事到这里,并没有结束。。。很伤感,偷窃也有不能解决问题的时候。在上面的例子中,每个 CurrentHuman 的实例都有一个 DNA 数组引用,它们指向的并不是同一个数组,这是解决问题的关键,也是问题的根源。我们希望改变一个 CurrentHuman 实例的 DNA 时希望日本人不要继承抗污染的基因,于是对于每个实例都创建一个DNA数组对象。但是如果是函数呢,比如一个显示DNA的函数:

//原始人类
function OriginHuman(){
	this.DNA = ["A","B","C"];
	this.eyeCount = 3;
	this.displayDNA = function(){
		alert(this.DNA);
	}
}

function CurrentHuman(){
	OriginHuman.call(this);//Contructor Stealing
}

//中国人
var chinese = new CurrentHuman();
//中国人进化出了抗污染基因
chinese.DNA.push("Ant-pollution");
chinese.displayDNA();//A,B,C,Ant-pollution

//日本人
var japanese = new CurrentHuman();
japanese.displayDNA();//A,B,C

alert(chinese.displayDNA == japanese.displayDNA );//false
可以看到创建了两个完全一样的函数,如果有多个实例则会创建多个,这是一种浪费。善良的程序员总是要钻牛角尖的,擅长把简单的问题复杂化,这是程序员的荣耀,就像《幽灵公主》中的野猪王,我们的冲锋一定要从正面。


组合型继承

”我们要造一栋大楼,它是要有天安门的顶,五角大楼的中腰,以及白宫的基。“一位土豪对建筑设计人员道。

这位土豪深谙取长补短之道,值得我辈借鉴。

原型链继承和构造窃取各有长短,原型链可以保证引用类型不会因为实例而大量被深度复制(deep clone),而构造窃取则可以保证引用类型不会被破坏。现在我们把那些一直要保持原始值的引用类型放在构造窃取实现部分,而将函数类的需要将改变应用到所有实例的对象放在原型中,这样的结合堪称完美。

//原始人类
function OriginHuman(){
	this.DNA = ["A","B","C"];
	this.eyeCount = 3;
}

//Contructor Stealing
function CurrentHuman(){
	OriginHuman.call(this);
}
//prototype
CurrentHuman.prototype.displayDNA = function(){
		alert(this.DNA);
}

//中国人
var chinese = new CurrentHuman();
//中国人进化出了抗污染基因
chinese.DNA.push("Ant-pollution");
chinese.displayDNA();//A,B,C,Ant-pollution

//日本人
var japanese = new CurrentHuman();
japanese.displayDNA();//A,B,C

alert(chinese.displayDNA == japanese.displayDNA );//true

一点延伸

在上面代码的末尾加上如下代码:

//修改displayDNA
chinese.displayDNA = function(){
	alert("DNA:"+this.DNA);
}

japanese.displayDNA()//A,B,C
可见japanese的displayDNA没有被修改。原因是上行所谓的修改chinese的displayDNA只是覆盖了prototype中的同名函数,这种语法操作时如果对象中不存在则添加,否则修改。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值