您的位置:首页 > Web前端 > JavaScript

JavaScript学习系列之深入原型链与继承的实现

2017-02-22 14:21 429 查看
JavaScript是一种解释型语言,它不需要编译成二进制文件,是一种动态语言。不同于Java中继承的实现方式,JavaScript实现继承主要依靠强大的原型链机制来实现。

一 原型链

访问一个对象的属性时,先在基本属性中查找,如果没有,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止,到查找到达原型链的顶部(也就是 Object.prototype)仍然没有找到属性,就会返回undefined。

var obj = {};
obj.a //输出undefined
var another = { a : 2};
obj.__proto__ = another;
obj.a //输出2


对象属性屏蔽问题

当读取一个对象obj的属性a时候,如果obj本身不存在属性名为a的属性,就去原型链中查找属性。但是在obj中写入一个属性a的时候,即执行赋值语句obj.a = 4时候,则分如下几种情况:

如果obj对象中包含名为a的属性,那么它会屏蔽所有原型链上的名为a的属性。这条赋值语句只会修改已有的属性。

var another = { a : 2};
var obj = {a : 4};
obj.__proto__ = another;
obj.a //输出4
obj.a = 5;
another.a //输出2
obj.a //输出5


如果obj对象中不包含名为a的属性,其原型链上包含名为a的属性且没有被标记为只读,那就会直接在obj中添加一个名为a的新属性。

var another = { a : 2};
var obj = {};
obj.__proto__ = another;
obj.a //输出2
obj.hasOwnProperty('a')//输出false
obj.a = 5;
another.a //输出2
obj.a //输出5
obj.hasOwnProperty('a')//输出true


如果obj对象中不包含名为a的属性,其原型链上包含名为a的属性且被标记为只读,那么无法修改已有属性,也无法直接在obj中添加一个名为a的新属性。代码运行在严格模式下会报错。

如果obj对象中不包含名为a的属性,其原型链上包含名为a的属性并且它是一个setter,那就会调用这个setter,a不会被添加到obj中。

有些情况下会产生隐式屏蔽。

var another = { a : 2};
var obj = {};
obj.__proto__ = another;
another.a//输出2
obj.a//输出2
obj.hasOwnProperty('a')//输出false
obj.a++;
another.a//输出2
obj.a//输出3
obj.hasOwnProperty('a')//输出true


因为obj.a++相当于obj.a = obj.a + 1,也是一个赋值语句。

二 原型链继承与类式继承结合(借用构造函数)

需要注意的一点是原型继承中的原型是指函数的原型,即每个函数都有一个prototype属性与之对应。这需要与原型链中的proto属性区分开来。原型继承最重要的就是将父类的原型继承到子类上来。

//父类Animal
function Animal(name) {
this.name = name || 'Animal';
}
//Animal公有方法
Animal.prototype.breath = function() {
console.log(this.name + ' is breathing!');
};

// 子类Bird
function Bird(name) {
//通过借用构造函数来实现对实例属性的继承
Animal.call(this, name);
}
//使用原型链实现对原型属性和方法的继承
Bird.prototype = Object.create(Animal.prototype);
//Bird公有方法
Bird.prototype.fly = function(){
console.log(this.name + ' is flying!');
}
//实例
var bird = new Bird('birdman');
bird.breath();//birdman is breathing!
bird.fly();//birdman is flying!


上述代码中最重要的一句就是:

Bird.prototype = Object.create(Animal.prototype);。Object.create()方法会创建一个新的对象并把对象内部的proto属性(原型链)关联到指定的对象(本例中是Animal.prototype)。

注意:最好不要把这句话换成下面两种形式

Bird.prototype = Animal.prototype;

这种形式并不会创建一个新对象,只是让Bird.prototype指向Animal.prototype,它们其实是一个对象。当我们为子类Bird的原型添加新的属性时,这个属性也相当于添加到Animal的原型中。这就导致其他类继承于Animal时,有了多余的属性。

Bird.prototype = new Animal();

这种形式的确可以创建一个关联到Animal.prototype的新对象。但是它有许多副作用,比如给Bird的原型添加多余的属性,或者可能做了一些不必要的操作,甚至可能影响Bird的“后代”。

三 模拟多重继承的混入机制

JavaScript本身不提供多重继承的功能,使用多重继承的代价也太高。但是我们可以模拟实现多重继承,也就是混入机制。在jQuery中把混入叫做extend,我们一般把它叫做mixin。下面看非常简单的mixin函数。

1.显式混入

function mixin(sourceObj, targetObj){
for(var key in sourceObj){
if(!(key in targetObj)){
targetObj[key] = sourceObj[key];
}
}
return targetObj;
}
var Animal = {
name : 'Animal',
breath : function() {
console.log(this.name + ' is breathing!');
}
};
//创建一个空对象把Animal的内容复制进去
var Bird = mixin(Animal, {});
//把新内容复制到bird中
mixin({
name : 'birdman',
fly : function(){
console.log(this.name + ' is flying!');
}
}, Bird);


如果你向目标对象中显式混入超过一个对象,就可以模仿多重继承行为。但是仍没有直接的方式处理函数和属性的同名问题。有些库提出“晚绑定”技术,但是这些技术可能会降低性能得不偿失。

2.寄生继承

显式混入的一种变体称为“寄生继承”。

//父类Animal
function Animal(name) {
this.name = name || 'Animal';
}

Animal.prototype.breath = function() {
console.log(this.name + ' is breathing!');
};

// 寄生类Bird
function Bird(name) {
var bird = new Animal();

bird.name = name || 'bird';
bird.fly = function(){
console.log(this.name + ' is flying!');
};
return bird;
}

//实例
var bird = new Bird('birdman');
bird.breath();//birdman is breathing!
bird.fly();//birdman is flying!


3.隐式混入

var Animal = {
name : 'Animal',
breath : function() {
console.log(this.name + ' is breathing!');
}
};
var Bird ={
name : 'birdman',
breath : function() {
//隐式把breath混入Bird中
Animal.breath.call(this);
}
};
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息