继承


  在基于类的语言中,对象是类的实例,并且类可以从另一个类继承。 JavaScript 是一门基于原型的语言,这意味着对象直接从其他对象继承。

伪类(Pseudoclassical)

  JavaScript 原型机制存在诸多矛盾。它不直接让对象从其他对象继承,反而插入了一个多余的间接层:通过构造器函数产生对象。
  当一个函数对象被创建时,Function 构造器产生的函数对象会运行类似这样的一些代码:

this.prototype = {constructor:this};

  新函数被赋予一个 prototype 属性,它的值是一个包含 constructor 属性且属性值为该新函数的对象。这个 prototype 对象是存放继承特征的地方。因为 JavaScript 没有提供一种方法去确定哪个函数是打算用来做构造器的,所以每个函数都会得到一个 prototype 对象。

  当采用构造器调用模式,即用 new 前缀去调用一个函数时,函数执行的方式会被修改。如果 new 运算符是一个方法而不是一个运算符,它可能会像这样执行:

//先前添加的 method 函数
Function.prototype.method = function (name,func) {
    this.prototype[name] = func;
    return this;
};

Function.method('new',function () {
    //创建一个新对象,它继承自构造器函数的原型对象
    var that = Object.create(this.prototype);

    //调用构造器函数,绑定 this 到新对象上
    var other = this.apply(that,arguments);

    //如果它返回值不是一个对象,就返回该新对象
    return (typeof other === 'object' && other) || that;
});

其中,Object.create 方法为之前定义的。
我们可以定义一个构造器并扩充它的原型:

var Mammal = function (name) {
    this.name = name;
};
Mammal.prototype.get_name = function () {
    return this.name;
};
Mammal.prototype.says = function () {
    return this.saying || '';
};

现在,我们可以构造一个实例:

var myMammal = new Mammal('Herb the Mammal');
var name = myMammal.get_name(); //-->Herb the Mammal

  我们可以构造另一个伪类来继承 Mammal ,这是通过定义它的 constructor 函数并替换它的 prototype 为一个 Mammal 的实例来实现的:

var Cat = function (name) {
    this.name = name;
    this.saying = 'meow';
};

//替换 Cat.prototype 为一个新的 Mammal 实例
Cat.prototype = new Mammal();

//扩充原型对象,新增 purr 和 get_name 方法
Cat.prototype.purr = function (n) {
    var i,s = '';
    for(i = 0; i < n; i++){
        if(s){
            s += '-';
        }
        s += 'r';
    }
    return s;
};
Cat.prototype.get_name = function () {
    return this.says() + ' ' + this.name + ' ' + this.says();
};

var myCat = new Cat('Henrietta');
var says = myCat.says();    //--> meow
var purr = myCat.purr(5);   //--> r-r-r-r-r
var name = myCat.get_name();    //--> meow Henrietta meow

  伪类模式本意是想向面向对象靠拢,但它看起来格格不入。我们可以隐藏一些丑陋的细节,通过使用 method 方法来定义一个 inherits 方法实现:

Function.method('inherits',function (Parent) {
    this.prototype = new Parent();
    return this;
});

  我们的 inherits 方法和 method 方法都返回 this ,这样允许我们可以采用级联的形式编程。现在,我们可以只用一行语句构造我们的 Cat 对象。

var Cat = function (name) {
    this.name = name;
    this.saying = 'meow';
}
    .inherits(Mammal)
    .method('purr',function (n) {
        var i,s = '';
        for(i = 0; i < n; i++){
            if(s){
                s += '-';
            }
            s += 'r';
        }
        return s;
    })
    .method('get_name',function () {
        return this.says() + ' ' + this.name + ' ' + this.says();
    });

  通过隐藏那些无谓的 prototype 细节,现在它看起来没那么怪异了。但它存在几个糟糕的问题:没有私有环境,所有属性都是公开的。无法访问 super (父类)的方法。并且,使用构造器函数存在一个严重的危害:如果你在调用构造器函数时忘了在前面加上前缀 new ,那么 this 将不会被绑定到一个新对象上,而是被绑定到全局对象上,这样一来,你不但没扩充新对象,反而破坏了全局变量环境。发生那样的情况时,既没有编译时警告,也没有运行时警告。
  为了降低这个风险,所有构造器函数都约定命名成首字母大写的形式,且不以首字母大写的形式命名其他的东西。

  在基于类的语言中,类的继承是代码重用的唯一方式,而 JavaScript 有更多且更好的选择。

对象说明符(Object Specifiers)

  有时候,构造器需要接受一大堆参数。在这种情况下,如果我们在编写构造器时让它接受一个简单的对象说明符,可能会更加友好些。那个对象包含了将要构造的对象规格说明。
所以,与其这么写:

var myObject = maker(f,l,mlc,s);

不如这么写:

var myObject = maker({
    first:f,
    middle:m,
    last:l,
    state:s,
    city:c
});

  现在多个参数可以按任意顺序排序,如果构造器会聪明地使用默认值,一些参数可以忽略掉,并且代码也更容易阅读。

  当与 JSON 一起工作时,这种形式还有一个间接的好处:JSON 文本只能描述数据,但有时候数据表示的是一个对象,把该数据与它的方法关联起来是有用的。如果构造器取得一个对象说明符,就能让它轻松实现,因为我们可以简单地把 JSON 对象传递给构造器,而它将返回一个构造完全的对象。

原型(Prototype)

  在一个纯粹的原型模式中,我们会摒弃类,转而专注于对象。基于原型的继承相比于基于类的继承在概念上更为简单:一个新对象可以继承一个旧对象的属性。

让我们先用对象字面量去构造一个有用的函数:

var myMammal = {
    name : 'Herb the Mammal',
    get_name : function () {
        return this.name;
    },
    says : function () {
        return this.saying || '';
    }
};

  一旦有了一个想要的对象,我们就可以利用 之前定义的 Object.create 方法构造出更多实例来:

var myCat = Object.create(myMammal);
myCat.name = 'Henrietta';
myCat.saying = 'meow';
myCat.purr = function (n) {
    var i,s = '';
    for(i = 0; i < n; i++){
        if(s){
            s += '-';
        }
        s += 'r';
    }
    return s;
};
myCat.get_name = function () { 
    return this.says + ' ' + this.name + ' ' + this.says;
};

  这是一种 “差异化继承” 。通过定制一个新的对象,我们指明它与所基于的基本对象的区别。

  有时候,它对某些数据结构继承于其他数据结构的情形非常有用:假定我们要解析一门类似 JavaScriptTEX 那样用一对花括号指示作用域的语言。定义在某个作用域里定义的条目在该作用域之外是不可见的。但在某种意义上,一个内部作用域会继承它的外部作用域。JavaScript 在表示这样的关系上做得非常好。当遇到一个左花括号时 block 函数被调用。parse 函数将从 scope 中寻找符号,并且当它定义了新的符号时扩充 scope

var block = function () {
	//记住当前作用域。构造一个包含了当前作用域中所有对象的新作用域
	var oldScope = scope;
	scope = Object.create(scope);
	
	//传递左花括号作为参数调用 advance
	advance('{');
	
	//使用新的作用域进行解析
	parse(scope);
	
	//传递右花括号作为参数调用 advance 并抛弃新作用域,恢复原来老的作用域
	advance('}');
	scope = oldScope;
};

函数化(Functional)

  迄今为止,我们所看到的继承模式的一个弱点就是没办法保护隐私。对象的所有属性都是可见的。对这种情况,我们有一个解决方案,那就是应用模块模式
  我们以小写字母开头来命名它,因为不需要使用 new 前缀。该函数包括 4 个步骤。

  1. 创建一个新对象。可以使用很多方法去构造一个对象:它可以构造一个字面量对象,或者可以和 new 前缀连用去调用一个构造器函数,或者它可以使用 Object.create 方法去构造一个已经存在的对象的新实例,或者它可以调用任何一个会返回一个对象的函数。
  2. 有选择的定义私有实例变量和方法。这些就是函数中通过 var 语句定义的普通变量。
  3. 给这个新对象扩充方法。这些方法有权去访问参数,以及通过 var 语句定义的变量。
  4. 返回那个新对象。

以下是一个函数化构造器的伪代码模板:

var constructor = function(spec,my){
	var that,其他的私有变量;
	my = my || {};
	
	把共享的变量和函数添加到 my 中

	that = 一个新的对象;
	
	添加给 that 的特权方法
	
	return that;
};

  其中, spec 对象包含构造器需要构造一个新实例的所有信息。spec 的内容可能会被复制到私有变量中,或是被其他函数改变,或者方法可以在需要的时候访问 spec 的信息。

  my 是一个为继承链中的构造器提供秘密共享的容器。 my 对象可以选择性地使用。如果没有传入一个 my 对象,那么会创建一个 my 对象。

函数内部给 my 对象添加共享的变量和函数是通过赋值语句实现的:

my.member = value;

  在我们给 that 添加特权方法时,我们可以分配一个新函数成为 that 的成员方法。或者,更安全地,我们可以先把函数定义为私有方法,然后再把它们分配给 that

var methodical = function(){
	···
};

that.methodical = methodical;

  分两步去定义 methodical 的好处是,如果其他方法想要调用 methodical ,它们可以直接调用 methodical() 而不是 that.methodical 。如果该实例被破坏或篡改,甚至 that.methodical 被替换掉了,调用 methodical 的方法同样会继续工作,因为它们私有的 methodical 不受该实例被修改的影响。

  以前面的 mammal 的例子来试着使用这种模式。不过此处不需要 my ,所以我们先抛开它不管。

var mammal = function (spec) {
    var that = {};
    that.get_name = function () {
        return spec.name;
    };
    that.says = function () {
        return spec.saying || '';
    };
    
    return that;
};

var mammal = mammal({name:'Herb'});

  在伪类模式中,构造器函数 Cat 不得不重复构造器 Mammal 已经完成的工作。在函数化模式中不再需要了,因为构造器 Cat 会调用构造器 Mammal ,让 Mammal 去做对象创建中的大部分工作,所以 Cat 只需关注自身的差异即可。

var Cat = function (spec) {
    spec.saying = spec.saying || 'meow';
    var that = mammal(spec);
    that.purr = function (n) {
        var i,s = '';
        for(i = 0; i < n; i++){
            if(s){
                s += '-';
            }
            s += 'r';
        }
        return s;
    };
    that.get_name = function () {
        return that.says() + ' ' + spec.name + ' ' + that.says();
    };

    return that;
};

var myCat = Cat({name:'Henrietta'});

  函数化模式还给我们提供了一个处理父类方法的方法。我们会构造一个 superior 方法,它取得一个方法名并返回调用那个方法的函数。该函数会调用原来的方法,尽管属性已经变化了。

Object.method('superior',function (name) {
    var that = this;
    var method = that[name]; 
    return function () {
        return method.apply(that,arguments);
    };
});

  让我们在 coolcat 上试一下上面申明的函数,coolcat 相比于 cat 有一个更酷的调用父类方法的 get_name 方法。我们会申明一个 super_get_name 变量,并把调用 superior 方法所返回的结果赋值给它。

var coolcat = function (spec) {
    var that = cat(spec);
    var super_get_name = that.superior('get_name');
    that.get_name = function (n) {
        return 'like ' + super_get_name() + ' baby';
    };
    return that;
};

var myCoolCat = coolcat({name:'Bix'});
var name = myCoolCat.get_name();    //-->like meow Bix meow baby

  函数化模式有很大的灵活性。它相比伪类模式不仅带来的工作更少,还让我们得到更好的封装和信息隐藏,以及访问父类的能力。

  如果对象的所有状态都是私有的,那么该对象称为一个 “防伪” 对象。该对象的属性可以被替换或删除,但该对象的完整性不会受到损害。如果我们用函数化的样式去创建一个对象,并且该对象的所有方法都不使用 thisthat ,那么该对象就是持久的一个持久性对象就是一个简单功能的函数集合。

  一个持久性的对象不会被入侵。访问一个持久性的对象时,除非有方法授权,否则攻击者不能访问对象的内部状态。

部件(Parts)

  我们可以从一套部件中把对象组装出来。例如,我们可以构造一个给任何对象添加简单事件处理特性的函数。它会给对象添加一个 on 方法、一个 fire 方法和一个私有的事件注册表对象:

var eventuality = function (that) {
    var registry = {};
    that.fire = function (event) {
        /*
        在一个对象上触发一个事件。该事件可以是一个包含事件名称的字符串,或是一个
        拥有包含事件名称的 type 属性的对象。
        通过 on 方法注册的事件处理程序中匹配事件名称的函数将被调用
         */
        var array,func,handler,i,
            type = typeof event === 'string' ? event : event.type;
        
        //如果这个事件存在一组事件处理程序,那么就遍历它们并按顺序依次执行
        if(registry.hasOwnProperty(type)){
            array = registry[type];
            for(i = 0; i < array.length; i++){
                handler = array[i];
                
                //每个处理程序包含一个方法和一组可选的参数
                //如果该方法是一个字符串形式的名字,那么寻找到该函数
                func = handler.method;
                if(typeof func === 'string'){
                    func = this[func];
                }
                
                //调用一个处理程序。如果该条目包含参数,那么传递它们过去。否则,传递该事件对象。
                func.apply(this,
                    handler.parameter || [event]);
            }
        }
        return this;
    };
    
    that.on = function (type,method,parameters) {
        //注册一个事件。构造一条处理程序条目。将它插入到处理程序数组中
        //如果这种类型的事件还不存在,就构造一个
        var handler = {
            method : method,
            parameters : parameters
        };
        if(registry.hasOwnProperty(type)){
            registry[type].push(handler);
        }else{
            registry[type] = [handler];
        }
        return this;
    };
    return that;
};

  我们可以在任何单独的对象上调用 eventuality ,授予它事件处理方法。我们也可以赶在 that 被返回前在一个构造器函数中调用它。

eventuality(that);

  用这种方式,一个构造器函数可以从一套部件中把对象组装出来。如果我们想要 eventuality 访问该对象的私有状态,可以把私有成员集 my 传给它。


参考书籍:《JavaScript 语言精粹》 - - - Douglas Crockford 著、赵泽欣等译

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值