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

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);
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值