继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

轻松理解javascript继承——细致又简单

恩言
关注TA
已关注
手记 41
粉丝 316
获赞 3231
轻松理解javascript继承

原型继承是javascript中继承最传统的做法,通过操作伪类和prototype,你很容易就能实现继承的效果:将父类的实例赋给子类的prototype,再创建子类实例即可。

如果你不知道原型的基础知识,我强烈推荐你阅读我的上一篇文章《轻松理解javascript原型》,文章中,作者把原型部分的知识近乎完美的剥离了出来,目的是为这篇文章做好铺垫。

但实话实说,我和《javascript语言精粹》的作者“老道”有着近乎相同的想法——我们希望能够避免new关键字的出现和伪类带来的种种问题,而采取另一种产生对象的设计模式——让函数直接返回对象。

这篇文章会为大家详细的介绍原型继承的应用、问题和需要注意的地方,更会花上一定的篇幅为大家推荐应用模块差异化继承。

伪类与原型继承

一个例子

你知道,在javascript中,没有纯粹的类的概念,就像“老道”说的那样:javascript只关心对象能做什么,并不关心它是怎么来的。

所谓原型继承,其实就是利用js访问对象属性(或方法)的时候会去查找它的原型链这一特点:你把父类对象的实例赋值给子类构造函数的原型属性,这样子类的所有实例在创建时都会通过__proto__子类构造函数.prototype的隐秘连接“获得”那个对象的所有属性和方法——子类对象从父类对象继承的属性并不真正直接存在于子类对象上,而是存在于子类构造函数的prototype(原型)对象中或者说子类实例的__proto__属性上,你可以在子类对象上通过原型链访问到它们。如果你还不知道是原型链查找是怎么运作的,《轻松理解javascript原型》里讲得很清楚。

下面的例子会更好地帮助你理解:

// 动物类
var Animal = function () {
    this.kind = "animal";
};

// 哺乳动物类
var Mammal = function (name) {
    this.name = name;
    this.sayHello = function () {
        alert('hello I am' + this.name + " and I am a kind of" + this.kind);
    }
};

Mammal.prototype = new Animal();

var mammal = new Mammal('Tom');
mammal.sayHello();

输出:

hello I am Tom and I am a kind of animal

当Mammal的实例mammal调用sayHello()方法时,里面需要找到this.kind,但是对象本身并没有this.type,于是javascript开启"大招"——原型链查找。去查找mammal构造函数Mammal的prototype对象,结果在这里面发现了“赃物”属性——kind,返回结果,游戏结束。

弄丢了什么

这么做并不是完美无缺的,不知道你还记不记得我们曾经说过的关于构造器的事,我不介意让这段代码再次呈现在这里:

this.prototype = {constuctor : this}

是不是想起什么来了,(构造)函数的prototype并非一个任何意义上的空对象——在这个函数对象被创建的时候,它就被它的构造器Function强行刻下了烙印,一个名叫constructor的属性,指的正是它自己。

如果用另一个实例去替换函数的prototype的话,显然,对象会丢失原本的constuctor属性,我们输出一下mammal的constuctor:

function () {
    this.kind = "animal";
}

是Animal的构造函数,不难想象,Mammal的prototype被Animal的实例替换掉了,原本的constuctor属性也随之丢失了。而Animal构造器的prototype里也有一个叫做construcor的属性,值是上面输出的结果。

所以,如果需要的话,为了保留住真正的构造器,你可以在替换了Mammal的prototype之后再做些额外的工作:

Mammal.prototype.constructor = Mammal;

此时再访问mammal的constructor属性,会返回Mammal构造函数。

更新

当构造器的prototype对象更新时,之前产生的所有实例对象都会做出立即更新。我们把上面动物的例子稍作改变:

// 动物类
var Animal = function () {};

// 哺乳动物类
var Mammal = function (name) {
    this.name = name;
    this.sayHi = function () {
        console.log('hello I am ' + this.name + " and I am from " + this.country);
    }
};

Mammal.prototype = new Animal();

var mammal = new Mammal('Tom');

mammal.sayHi();

Mammal.prototype.country = 'USA'; // 显然 Animal.prototype.country = 'USA'效果相同

mammal.sayHi();

先后输出:

hello I am Tom and I am from undefined
hello I am Tom and I am from USA

但是,你要注意我说的是更新,如果你完全将prototype换成了别对象,那么引用关系将被破坏,之前生成的实例对象将会不为所动。

伪类的缺陷

伪类加原型的方式的的确确能够解决对象与继承的问题,但这种方式存在缺陷:

(1) 首先,它没有私有环境,所有属性都是公开的

function Cat() {
    this.name='Tom';
    this.age = 2;
}

var cat = new Cat();

打开控制台,输入cat.name,就可以轻而易举的知道我们的小猫叫做Tom,在控制台输入cat.name = 'Jerry',糟糕,我们小猫的名字被修改了!!

(2)其次,它无法像java那样通过super.属性轻易的访问父类名字相同的属性和方法,除非使用cat.__proto__这样不友好的方式,这是应该禁止的。(事实上,直接访问属性都是应该被禁止的)

function Animal () {
    this.name = "animal"
}

function Cat() {
    this.name='Tom';
    this.age = 2;
}

Cat.prototype = new Animal();
var cat = new Cat();

console.log(cat.name); // =>Tom
console.log(cat.__proto__.name); // =>animal

(3)最可怕的如果你在调用构造器函数时忘记了在前面加上new前缀,那么this将不会被绑定到那个新对象上,反倒被绑定到全局对象上,从而破坏了全局环境。既没有编译时警告,也没有运行时警告。

或许,我是说或许,是时候换一个设计模式以实现相同并更强大的功能。

应用模块模式继承

差异化继承

差异化继承的本质是先复制一份与父对象含有相同属性特征的对象(注意是深复制,不是简单的复制一个引用),然后再在这个基础上创建我们自己的对象:

var animal = {
    name : "animal",
    age : 15
};

var cat = Object.create(animal);

cat.name = "Tom";
cat.sayMeow = function () {
    console.log('meow')
};

console.log(cat.name);
console.log(cat.age);
cat.sayMeow();

Object.create是ES5的新特性,作用是创建一个具有指定原型且可选择性地包含指定属性的对象(我们不讨论它的第二个作为属性描述符集合的可选参数)。在父对象的这个基础上,我们再为子对象添加新属性。

在ES3的那个原始的时代,Object.create()大概是像下面这样工作的:

function create (obj) {
    var F = function () {};
    F.prototype = obj;
    return new F();
}

实际上,父对象的属性与伪类继承模式类似,被放置在了原型上,而不是真正的存在与子对象上。

函数化

想想伪类模式的缺陷吧,访问父元素的同名属性的问题解决了(直接父对象.属性就可以),遗漏new关键字造成的威胁解决了,但私有属性仍然暴漏无疑。我们使用函数化的方式解决这个问题,先看一个简单的例子:

function animal() {
    var name = 'animal';
    var getName = function () {
        return name
    };
    return {
        getName : getName
    }
}

var myAnimal = animal();
console.log(myAnimal.name);  // =>undefined
console.log(myAnimal.getName()); //=>animal

这里我们利用闭包的特性来解决私有变量的问题。我在想或许我应该写一篇叫做《轻松理解javascript闭包的文章》。

现在好了,除非调用我们的getName特权方法,谁也别想知道name是什么,更别想修改它。看那,animal()已经不是new的小跟班,它自己返回一个对象,它的首字母再也不用大写了。

应用模块继承

干得不错,现在,我们试着融合两种方法,实现模块化继承。为了更明显些,我们为animal构造器增加一个food属性和eat方法。嗯,我已经尽力把情况做的丰富些:

function animal(info) {

    var that = {};

    that.getName = function () {
        var name = info.name|| 'animal';
        console.log('(super method) my name is ' + name);
        return name ;
    };

    that.eat = function () {
        console.log('I am eating ' + info.food);
    };

    return that;
}

function cat(info) {

    var that = animal(info), // 获得父级对象,并在此基础上创建子对象
        super_getName = that.getName; // 直接获得父级对象的方法

    that.sayMeow = function () {    // 子对象特有的方法
        console.log('Meow')
    };

    that.super_getName = super_getName;

    that.getName = function () {  // 子对象与父对象重名的方法
        var name = info.name|| 'Tom';
        console.log('(child method) my name is ' + name);
        return name ;
    };

    return that;
}

var Tom = cat({name:'Tom', food:'fish'});

Tom.sayMeow(); // =>Meow
Tom.eat(); // =>I am eating fish
Tom.getName(); // =>(child method) my name is  Tom
Tom.super_getName();// =>(super method) my name is  Tom

“构造函数”接收一个参数info,存放对象的所有数据信息。这种设计模式下,私有变量受到保护,可以轻易的访问父级对象的方法(需要时使用apply改变this),更解决了new关键字容易造成的问题。

小结

通过今天的学习,不知道你有没有走出原型、继承的谜团呢? 如果还是有疑惑,我推荐你再读一遍。《javascript语言精粹》、《javascript启示录》中对相关的知识点有更细致的讲解,没事的话就去看看这些书吧,的确挺好的。

打开App,阅读手记
5人推荐
发表评论
随时随地看视频慕课网APP