JavaScript语言精粹之继承

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

1 伪类

JavaScript的原型存在着诸多矛盾,某些看起来有点像基于类讹点语言的复杂语法问题遮蔽了它的原型机制,它不让对象直接从其他对象继承,反而插入了一个多余的间接层,从而使构造器函数产生对象

当一个函数对象被创建时,Function构造器产生的函数对象会运行类似这样的一个代码:

this.prototype = {constructor: this};

新函数对象被赋予一个prototype属性,其值是包含一个constructor属性且属性值为该新函数对象,该prototype对象是存放继承特征的地方

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

Function.method('new',function(){//创建一个新对象,它继承自构造器函数的原型对象var that = Object.beget(this.prototype);//调用构造器函数,绑定 -this- 到新对象上var other = this.apply(that, arguments);//如果它的返回值不是一个对象,就返回该新对象return (typeof other === 'object' && other) || that;
});

我们可以定义一个构造器并扩充它的原型

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();
});

我们现在有了行为像 “类” 的构造器函数,但仔细去看,它们可能有着令人惊讶的行为:没有私有环境,所有的属性都是公开的,无法访问 super(父类)的方法

更糟糕的是,使用构造器函数存在一个严重的危害,如果你在调用构造器函数时忘记了在前面加上new 前缀,那么this 将不会被绑定到一个新对象上,可悲的是,this将被绑定到全局对象上,所以你不但没有扩充新对象,反而将破坏全局变量

为了降低这个问题带来的风险,所有的构造器函数都约定命名成首字母大写的形式,并且不以首字母大写的形式拼写任何其他的东西,这样我们至少可以通过目视检查去发现是否缺少了new前缀,一个更好的备选方案就是根本不使用new

5.2 对象说明符

我们在编写构造器时使其接受一个简单的对象说明符可能会更加友好,那个对象包含了将要构建的对象规格说明,所以,与其这么写

var myObject = maker(f,l,m,c,s);

不如这么写

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

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

当与 json 一起工作时,这还可以有一个间接地好处, json 文本直能描述数据,但有时数据表示的是一个对象,将该数据与它的方法关联起来是有用处的,如果构造器取得一个对象符,可以容易做到,因为我们可以简单地传递该 json 对象给构造器,而它将返回一个构造完全的对象

5.3 原型 

在一个纯粹的原型模式中,我们会摒弃类,转而专注于对象,基于原型的继承相比于基于类的继承在概念上更为简单:一个新对象可以继承一个旧对象的属性。你通过构造一个有用的对象开始,接着可以构造更多和那个对象类似的对象,可以完全避免把一个应用拆解成一系列嵌套抽象类的过程

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

一旦有了想要的对象,接下来定制新的实例

var myCat = Object.beget(myMammal);
myCat.name = 'Herietta';
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;
};

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

类似于 Java 里对继承对象方法的重写

假定我们要解析一门类似JavaScript 或 TEX 那样用一对花括号指示作用域的语言,定义在一个作用域中的条目在该作用域是不可见的,从某种意义上来理解,也就是说一个内部作用域会继承它的外部作用域,JavaScript 在表示这样的关系上做得非常好,当遇到一个做花括号时 block 函数被调用,parse 函数将从scope中寻找符号,并且当它定义了新的符号扩充scope

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

5.4 函数化

我们从构造一个将产生对象的函数开始,给它起的名字将以一个小写字母开头,因为它并不需要使用 new 前缀,该函数包括四个步骤

1. 它创建一个新对象,有很多的方式去构造一个对象,它可以构造一个对象字面量,或者它可以和 new 前缀连用去调用一个构造器函数,或者它可以使用 Object.beget 方法去构造一个已经存在的对象的新实例,或者它可以调用任意一个会返回一个对象的函数

2.  他选择性地定义私有实例变量和方法,这些就是函数通过var语句定义地普通变量

3. 它给这个新对象扩充方法,那些方法将拥有特权去访问参数,以及在第二步中通过var语句定义地变量

4. 它返回那个新对象

这是一个函数化构造器地伪代码模板

var constructor = function(spec, my){var that,其他的私有实例变量;my = my || {};把共享的变量和函数添加到my中that = 一个新对象添加给 that 的特权方法return that;
}

让我们将这个模式应用到 mammal 例子里,此处不需要my

name 和 saying 属性现在时完全私有地,它们只有通过 get_name 和 says 两个特权方法才可以访问

var mammal = function(spec) {var that = {};that.get_name = function() {return spec.name;};that.says = function() {return spec.saying || '';};return that;
};var myMammal = 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,method = that[name];return function() {return method.apply(that, arguments);};
});

让我们在 coolcat 上试验一下,除了它有一个更酷的调用父类的方法的 get_name 方法,它只需要一点点的准备工作,我们将声明一个 super_get_name 变量,并且把调用 superior 方法所返回的结果赋值给它

var coolcat = function(spec) {var that = cat(spec),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'

函数化模式有很大灵活性,它不仅不像伪类模式那样需要很多功夫,还让我们得到更好的封装和信息隐藏,以及访问父类方法的能力

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

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

5.5部件

我么可以从一套组件中组合出对象来,例如,我们可以构造一个能添加简单事件处理特性到任何对象上的函数

var eventuality = function(that) {var registry = {};thar.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.parameters || [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)

用这种方式,一个构造器函数可以从一套组件中组装出对象来,JavaScript 的弱类型在此处是一个巨大的优势,因为我们无须花费精力去关注一个类型系统中的类谱系,相反,我们可以专注于它们的个性化内容

如果我们想要 eventuality 去访问该对象的私有状态,则可以把私有成员集 my 传递给它


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部