2016.9.9
第6章 原型与面向对象
原型虽然是定义对象的一种很方便的方式,但它的本质依然是函数特性。
使用原型所定义的属性和功能会自动应用到对象的实例上。一旦进行了定义,原型的属性就会变成实例化对象的属性,从而作为复杂对象创建的概览。
js原型的主要用途就是使用一种类风格的面向对象和继承技术进行编码。
6.1 实例化和原型
所有的函数在初始化的时候都有一个prototype属性,该属性的初始值是一个空对象。只有函数在作为构造器的时候,prototype属性才会发挥更大的作用。
使用new关键字调用一个函数,使得该函数可以作为构造器进行实例化,并产生一个新的空对象实例作为其上下文。
6.1.1 对象实例化
创建新对象最简单的方法
var o={};
通过赋值语句可以再给它添加一些属性
o.name = 'satio';
o.occupation = 'marksman';
o.cyberization = 20;
1)原型作为对象概览
示例6.1 使用原型方法创建一个新实例
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function Ninja(){}
Ninja.prototype.swingSword = function(){
return true;
}
var ninja1 = Ninja();
assert(ninja1 === undefined,'No instance of Ninja created.')
var ninja2 = new Ninja();
assert(ninja2 && ninja2.swingSword && ninja2.swingSword(),'Instance exists and method is callable.')
首先,我们像普通函数一样调用,并将其结果存储在变量ninja1中,由于该函数体没有返回任何东西,所以ninja1的值是undefined。
接着,使用new操作符将其作为构造器进行调用,此时,新创建了一个对象,并且成为函数的上下文。new操作符返回的结果是该新对象的引用。ninja2引用的是新创建对象,并且该新对象有一个swingSword()方法可以让我们进行调用。
这说明,函数作为构造器进行调用时,函数的原型是新对象的一个概览。
原型可以让我们预定义属性,包括方法,这些属性和方法会自动应用在新对象实例上。
我们并没有在构造器里显式做任何事情,通过将swingSword()方法添加到构造器的prototype属性上,swingSword()方法就可以附加到新对象上了。
2)实例属性
使用new操作符将函数作为构造器进行调用的时候,其上下文被定义为新对象实例。这意味着,除了通过原型给函数附加属性的形式以外,我们还可以在构造器函数内通过this参数初始化值。
示例6.2 观察初始化活动的优先级
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function Ninja(){
this.swung = false;
this.swingSword = function(){
return !this.swung;
}
}
Ninja.prototype.swingSword = function(){
return this.swung;
}
var ninja = new Ninja();
assert(ninja.swingSword(),'Called the instance method, not the prototype method.')
以上示例表明,在构造器内创建的实例方法会阻挡在原型上定义的同名方法。
在构造器内的绑定操作优先级永远都高于在原型上的绑定操作优先级。因为构造器的this上下文指向的是实例自身,所以我们可以在构造器内对核心内容执行初始化操作。
3)协调引用
示例6.3 观察原型变化时的行为
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function Ninja(){
this.swung = true;
}
var ninja = new Ninja();
Ninja.prototype.swingSword = function(){
return this.swung;
}
assert(ninja.swingSword(),'Method exists, even out of order.')
测试证明原型的改变会影响其构造器已经创建的对象
在引用对象的一个属性时,首先检查该对象本身是否拥有该属性,如果有,则直接返回,否则,再查看对象的原型,检查该原型上是否有所要的属性,如果有则直接返回,如果没有,则该值是undefined.
对象不仅与其构造器有关,还与构造器所创建对象的原型相关
js中的每个对象,都有一个名为constructor的隐式属性,该属性引用的是创建该对象的构造器。由于prototype是构造器的一个属性,所以每个对象都有一种方式可以找到自己的原型。
原型是实时附加在对象上的,可以很好的与该对象的自身属性协调在一起。引用时,可以随时使用原型。
示例6.4 进一步观察原型改变时的行为
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function Ninja(){
this.swung = true;
this.swingsSword = function(){
return !this.swung;
}
}
var ninja = new Ninja();
Ninja.prototype.swingsSword = function(){
return this.swung;
}
assert(ninja.swingsSword(),'Called the instance method,not the prototype method.')
示例测试不通过表示:
查找属性时,首先查找对象自身,如果没找到,再查找构造器的原型
6.1.2 通过构造器判断对象类型
对象的构造器可以通过constructor属性获得,任何时候我们都可以重新引用该构造器,甚至可以使用它进行类型检查。
示例6.5 判断一个实例的类型以及其构造器
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function Ninja(){}
var ninja = new Ninja();
assert(typeof ninja == 'object','The type of the instance is object.')
assert(ninja instanceof Ninja,'Instanceof identifies the constructor.')
assert(ninja.constructor == Ninja,'The ninja object was create by the Ninja function')
instanceof操作符告诉我们确定一个实例是否是由特定的函数构造器所创建。
利用constructor属性可以验证实例的起源。
示例6.6 使用constructor属性实例化一个新对象
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function Ninja(){}
var ninja = new Ninja();
var ninja2 = new ninja.constructor();
assert(ninja2 instanceof Ninja,'It\'s a Ninja.')
assert(ninja !== ninja2,'But not the same Ninja.')
示例定义了一个构造器,然后使用该构造器创建一个实例。接着使用该实例的constructor属性再创建第二个实例。测试结果表明,成功创建了第二个Ninja对象,并且该变量指定的并不是同一个实例。
我们不用知道原有的构造器函数就可以再次创建一个新实例,即使是原始的构造器在作用域内已经不存在了,我们也完全可以在幕后使用该引用。
9.12
6.1.3 继承与原型链
可以利用instanceof作为对象继承的一种形式。
示例6.7 尝试用原型实现继承
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function Person(){}
Person.prototype.dance = function(){};
function Ninja(){}
Ninja.prototype = {dance:Person.prototype.dance};
var ninja = new Ninja();
assert(ninja instanceof Ninja,'ninja receives functionality from the ninja prototype.')
assert(ninja instanceof Person,'... and the Person prototype.')
assert(ninja instanceof Object,'... and the Object prototype.')
测试结果,第二个没有通过
即使没做任何事情,所有的对象也都会是object的实例。
创建原型链最好的方式 ,使用一个对象的实例作为另一个对象的原型:
Subclass.prototype = new Superclass();
Ninja.prototype = new Person();
这将保持原型链,是因为Subclass实例的原型将是Superclass的一个实例,该实例不仅拥有原型,还持有Superclass的所有属性,并且该原型指向其自身超类的一个实例,以此类推。
示例6.8 用原型实现继承
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function Person(){}
Person.prototype.dance = function(){};
function Ninja(){}
Ninja.prototype = new Person();
var ninja = new Ninja();
assert(ninja instanceof Ninja,'ninja receives functionality from the ninja prototype.')
assert(ninja instanceof Person,'... and the Person prototype.')
assert(ninja instanceof Object,'... and the Object prototype.')
assert(typeof ninja.dance == 'function','... and can dance.')
测试结果,通过
通过执行instanceof操作,可以判断函数是否继承了其原型链中任何对象的功能。
dance()方法在原型链上被所有实例继承。
所有原生js对象构造器(如Object,Array,String,Number,RegExp,Function)都有可以被操作和扩展的原型属性,因为每个对象构造器自身就是一个函数。这证明原型是这门语言的一个非常强大的特性。利用原型,我们可以扩展这门语言自身的功能,从而给这门语言引用新的特性或丢失的特性。
示例6.9 javascript 1.6 forEach()方法的一种不过时的实现
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
if(!Array.prototype.forEach){
Array.prototype.forEach = function(callback,context){
for(var i=0; i
callback.call(context || null, this[i], i, this);
}
}
}
['a','b','c'].forEach(function(value,index,array){
assert(value,array[index]+' is in position '+index+' out of '+(array.length-1))
})
在定义一个可能已经存在的方法之前,需要检查一下,使得代码可以向前兼容。
示例将方法添加到Array原型上,并使用传统的for循环遍历该数组,为每一个条目都调用下callback方法。传递给callback的是当前所遍历的条目、索引以及原始数组。context||null表达式可以防止我们将undefined传递给call()。
所有的内置对象,比如Array,包括其原型,我们都可以按照自己的意愿来扩展它,但需要记住,在原始对象上引入新的属性和方法,与在全局作用域内声明一个变量一样危险,因为原生对象的原型只有一个实例,所以有发生命名冲突的重大可能性。
6.1.4 HTML DOM原型
在现代浏览器中,所有DOM元素都继承于HTMLElement构造器。通过访问HTMLElement的原型,浏览器可以为我们提供扩展任意HTML节点的能力。
示例6.10 通过HTMLElement的原型,给所有HTML元素都添加一个新方法
test suite
#results .pass{color:green;}
#results .fail{color:red;}
I'm going to be removed.
Me too!
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
HTMLElement.prototype.remove = function(){
if(this.parentNode){
this.parentNode.removeChild(this);
}
}
var a = document.getElementById('a');
a.parentNode.removeChild(a);
document.getElementById('b').remove();
assert(!document.getElementById('a'),'a is gone.')
assert(!document.getElementById('b'),'b is gone too.')
在示例中,通过增强HTMLElement构造器的原型,我们为所有的DOM元素都添加了一个新方法remove()。
尽管浏览器暴露了基构造器和原型,但它们选择性地禁用了通过构造器创建元素的能力。
6.2 疑难陷阱
js有几个和原型、实例化以及继承相关的陷阱。
6.2.1 扩展对象
我们也许会犯的极其严重的错误就是去扩展原生Object.prototype。因为,在扩展该原型时,所有的对象都会接收这些额外的属性。在我们遍历对象属性的时候,新属性的出现可能会导致各种意想不到的行为。
示例6.11 给Object原型添加额外属性会发生意想不到的行为
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
Object.prototype.keys = function(){
var keys = [];
for(var p in this) keys.push(p);
return keys;
}
var obj = {a:1,b:2,c:3};
assert(obj.keys().length == 3,'There are three properties in this object.')
console.log(obj.keys())
示例测试没有通过
问题出在了给Object添加的keys()方法上,我们给所有的对象都添加了这一属性,这会影响到所有对象,并迫使所有的代码都必须要考虑到这个额外的属性,这可能会破坏页面开发人员的合理代码。
js提供一个名为hasOwnProperty()方法,使用该方法可以确定一个属性是在对象实例上定义的,还是从原型里导入的。
示例6.12 使用hasOwnProperty()方法辨别Object原型扩展
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
Object.prototype.keys = function(){
var keys = [];
for(var i in this)
if(this.hasOwnProperty(i))
keys.push(i);
return keys;
}
var obj = {a:1,b:2,c:3};
assert(obj.keys().length == 3,'There are three properties in this object.')
console.log(obj.keys())
重新定义了方法,忽略了非实例属性,这样测试就通过了。
使用hasOwnProperty()忽略原型对象上的属性,从而跳过原型上的属性。
但是,不能仅仅因为它能解决这个问题,就滥用它,从而使其成为我们代码的用户的一个负担。
6.2.2 扩展数字
除了Object,对其他原生对象的原型进行扩展相对来说比较安全。但是,另外一个有问题的原生对象是Number。
示例6.13 在Number原型上添加一个方法
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
Number.prototype.add = function(num){
return this+num;
}
var n=5;
assert(n.add(3) == 8,'It works if a number is in a variable.')
assert((5).add(3) == 8,'Also works if a number is wrapped in parentheses.');
assert(5.add(3)==8,'What about a simple literal?')
在浏览器中加载页面时,页面不会加载,会报错。语法解析器不能处理字面量这种情况。
9.13
6.2.3 子类化原生对象
原生对象的子类化,一个对象是Object的子类(因为它是所有原型链的根)
示例:6.14 子类化Array对象
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function myArray(){}
myArray.prototype = new Array();
var mine = new myArray();
mine.push(1,2,3);
assert(mine.length == 3,'All the items are in out sub-classed array.')
assert(mine instanceof Array,'Verify that we implement Array functionality.')
示例在有些浏览器可能不支持。
更好的策略是单独实现原生对象的各个功能,而不是扩展出子类。
示例6.15 模拟Array功能,而不是扩展出子类
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function myArray(){}
myArray.prototype.length = 0;
(function(){
var methods = ['push','pop','shift','unshift','slice','splice','join'];
for(var i=0; i
(function(name){
myArray.prototype[name] = function(){
return Array.prototype[name].apply(this,arguments);
}
})(methods[i]);
}
})()
var mine = new myArray();
mine.push(1,2,3);
assert(mine.length == 3,'All the items are in out sub-classed array.')
assert(!(mine instanceof Array),'Verify that we implement Array functionality.')
我们使用了一个即时函数,使用apply()方法,将从Array中选中的方法复制到新类上。使用数组里的方法名是为了保持整洁性,且易于扩展。
6.2.4 实例化问题
函数有两种用途:作为“普通”的函数,以及作为构造器。因为如此,用户可能并不很清楚在代码中用的到底是哪个。
示例6.16 函数调用时不使用new操作符时的结果
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function User(first,last){
this.name = first + ' ' + last;
}
var user = User('Ichigo','Kurosaki');
assert(user,'User instantiated.')
assert(user.name == 'Ichigo Kurosaki','User name correctly assigned')
测试失败。原因是忘记在User()函数上调用new操作符了。这种情况下,new操作符的缺失,将会导致函数是作为普通函数进行调用的,而不是构造器,而且并没有实例化一个新对象。新手用户很容易误入这个陷阱,不使用操作符调用该函数,将导致意想不到的结果。
注意:约定,函数作为构造器的时候,其名称以一个大写字符开头,而非构造器函数则不必。此外,构造器往往是名词,用于描述类的概念,如Ninja,samurai,tachikoma这样的“类”,而普通函数则命名为动词,或动词/对象,用于描述所做的事情,如swingSword,hideBehindAplant这样。
示例6.17 不小心将变量引入到全局命名空间中
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function User(first,last){
this.name = first + ' ' + last;
}
var name = 'Rukia';
var user = User('Ichigo','Kurosaki');
assert(name == 'Rukia','Name was set to Rukia')
测试失败,这个也犯了错误,忘记使用new操作符。
函数作为构造器调用时,函数调用的上下文是新分配的对象。但作为普通函数调用时,其上下文是全局作用域,也就是this.name引用指向的不是新创建对象的name属性,而是全局作用域内的name变量。
示例6.18 判断函数是否是作为构造器进行调用的
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function Test(){
return this instanceof arguments.callee;
}
assert(!Test(),'We didn\'t instantiate , so it return false.');
assert(new Test(),'We dis instantiate , returning true')
回想一些重要的概念:
.通过arguments.callee可以得到当前执行函数的引用。
.“普通”函数的上下文是全局作用域(除非强制修改)。
.利用instanceof 操作符测试已构建对象是否构建于指定的构造器。
基于这些,函数在作为构造器进行执行的时候,表达式 this instanceof arguments.callee 的结果是true,如果作为普通函数执行,则返回false。
示例6.19 在调用者上修复该问题
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function User(first,last){
if(!(this instanceof arguments.callee)){
return new User(first,last);
}
this.name = first+' '+last;
}
var name = 'Rukia';
var user = User('Ichigo','Kurosaki');
assert(name == 'Rukia','Name was set to Rukia');
assert(user instanceof User,'User instantiated');
assert(user.name == 'Ichigo Kurosaki','User name correctly assigned')
判断函数是否按不正确的方法进行了调用,如果是,则再实例化User自身,将其作为函数的结果进行返回。这样的结果是,无论函数调用的时候是否是作为普通函数进行调用的,最终都返回一个User实例。
callee属性即将在新版本js中废弃,并且不支持严格格式。
还要再思考下,使用这种方法是否就一定是正确的,我们只是想出一个巧妙的解决办法,但不代表就一定要用。
9.14
6.3 编写类风格的代码
js可以让我们通过原型实现继承。
类是面向对象开发人员所期望的内容,尽管js本身不支持传统的类继承。
开发人员希望有如下特性:
.一套可以构建新构造器函数和原型的轻量级系统。
.一种简单的方式来执行原型继承
.一种可以访问被函数原型所覆盖的方法的途径
示例6.20 经典继承语法示例
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
(function(){
var initializing = false,superPattern = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
Object.subClass = function(properties){
var _super = this.prototype;
initializing = true;
var proto = new this();
initializing = false;
for(var name in properties){
proto[name] = typeof properties[name] == 'function' && typeof _super[name] == 'function' && superPattern.test(properties[name]) ?
(function(name,fn){
return function(){
var tmp = this._super;
this._super = _super[name];
var ret = fn.apply(this,arguments);
this._super = tmp;
return ret;
}
})(name,properties[name]) : properties[name];
}
function Class(){
if(!initializing && this.init){
this.init.apply(this,arguments);
}
}
Class.prototype = proto;
Class.constructor = Class;
Class.subClass = arguments.callee;
return Class;
}
})()
var Person = Object.subClass({
init:function(isDancing){
this.dancing = isDancing
},
dance:function(){
return this.dancing;
}
});
var Ninja = Person.subClass({
init:function(){
this._super(false);
},
dance:function(){
return this._super();
},
swingSword:function(){
return true;
}
})
var person = new Person(true);
assert(person.dance(),'The person i sdancing.');
var ninja = new Ninja();
assert(ninja.swingSword(),'The sword is swinging');
assert(!ninja.dance(),'The ninja is not dancing');
assert(person instanceof Person,'Person is a Person')
assert(ninja instanceof Ninja && ninja instanceof Person,'Ninja is a Ninja and a Person')
关于本例,有几点重要的注意事项:
.通过调用现有的构造器函数的subClass()方法可以创建一个新“类”,例如,通过Object创建一个Person类,以及通过Person创建一个Ninja类。
.为了让构造器的创建更加简单。我们建议的语法是,为每个类只提供一个init()方法,就像为Person和Ninja提供的init()方法一样。
.我们所有的“类”最终都继承于一个祖先:Object。因此,如果要创建一个新类,它必须是Object的一个子类,或者是一个在层级上继承于Object的类(完全模仿当前的原型系统)
.该语法的最大挑战是访问被覆盖的方法,而且有时这些方法的上下文也有可能被修改了。通过this._super()调用Person超类的原始init()和dance()方法,我们就可以了解这种用法。
9.18
6.3.1 检测函数是否可序列化
函数序列化就是简单接收一个函数,然后返回该函数的源码文本。
一个函数在其上下文中序列化成字符串,会导致它的toString方法被调用。可以用这种方式测试函数是否可以序列化。
/xyz/.test(function(){xyz;}) 该表达式创建一个包含xyz的函数,将该函数传递给正则表达式的test()方法,该正则表达式对字符串“xyz”进行测试。如果函数能够正常序列化(test()方法将接收一个字符串,然后将触发函数的toString()方法),最终结果返回true。
superPattern =/xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; 建立一个名为superPattern的变量,稍后用它来判断一个函数是否包含字符串"_super"。只有函数支持序列化才能进行判断,所以在不支持序列化的浏览器上,我们使用一个匹配任意字符串的模式进行代替。
6.3.2 子类的实例化
Object.subClass = function(properties){
var _super = this.prototype;
}
给Object添加一个subClass()方法,该方法接收一个参数,该参数是我们期望添加到子类的属性集。
为了用函数原型模拟继承:创建父类的一个实例,并将其赋值给子类的原型。
我们在代码中定义了一个initializing变量,每当我们想使用原型实例化一个类的时候,都将该变量设置为true。
因此,在构造实例时,我们可以确保不在实例化模式下进行构建实例,并可以相应地运行或跳过init()方法。
if(!initializing && this.init){
this.init.apply(this,argument);
}
init()方法可以运行各种昂贵的启动代码(连接到服务器、创建DOM元素,还有其他未知内容),所以如果只是创建一个实例作为原型的话,你们要规避任何不必要的昂贵启动代码。
6.3.3 保留父级方法
在我们的特定实现中,我们创建一个名为_super的临时新方法,该方法只能从子类方法内部进行访问,并且该方法引用的是父类中的原有方法。
var Person = Object.subClass({
init:function(){
this.dancing = isDancing;
}
});
var Ninja = Person.subclass({
init:function(){
this._super(false);
}
})
在Ninja构造器内,我们调用了Person的构造器,并传入了一个相应的值。这可以防止重新复制代码——我们可以重用父类中已经编写好的代码。
为了增强子类,我们向subClass()方法传入了一个对象哈希,只需要将父类的属性和传入的属性合并在一起就可以了。
首先,使用如下代码,创建一个超类的实例作为一个原型:
initizlizing = true;
var proto = new this();
initizlizing = false;
将传入的属性合并到proto对象(一个原型的原型)中。如果不在意父类函数,合并代码将非常简单:
for(var name in properties) proto[name] = properties[name];
检测即将被包装的子类函数,可以使用如下条件表达式:
typeof properties[name] == 'function' && typeof _super[name] == 'function' && superPattern.test(properties[name]);
此表达式包含三个检测条件:子类属性是否是一个函数?超类属性是否是一个函数?子类函数是否包含一个_super()引用?
如果条件表达式表明我们必须包装功能,我们通过给即时函数的结果进行赋值,将该结果作为子类的属性:
(function(name,fn){
return function(){
var tmp = this._super;
this._super = _super[name];
var ret = fn.apply(this,arguments);
this._super = tmp;
return ret;
}
})(name,properties[name])
该即时函数创建并返回了一个新函数,该新函数包装并执行了子类的函数,同时可以通过_super属性访问父类函数。
网友评论