翻译 Secrets of the JavaScript Ninja (JavaScript忍者禁术)
第六章.原型与面向对象(6.Object-orientation with prototypes)
本章重点:
1.利用函数实现构造器
2.解释prototyes
3.利用prototypes实现对象的扩展
4.avoiding common gotchas
5.利用inheritace构建classes
看到prototypes你可能会觉得他与object是紧密联系的,但是,再次提醒各位,我们的重点还是function,Prototypes是一种很方便的定义classes的途径,但是它的本质是属于function的特性
总的来说,如果你很明显的在使用prototypes就是在使用一种classical-style形式的面向对象编程和继承的技术。
让我们开始看看如何使用prototypes
6.1 实例化和原型(Instantiation and prototypes)
所有functions都拥有一个属性:prototype,
初始状态中prototype指向一个空的object。
这个属性只是在这个函数被当作构造器来调用的时候,
才有作用,其余的情况下,没啥用。
在第三章中,我们利用关键字new来调用一个函数,这个函数就成为了构造器,
它会产出一个新的对象实例作为他的函数上下文(function context)
鉴于对象实例化这个部分很重要,下面我们详细的讨论一下,以便我们能真正理解这个知识点。
6.1.1 对象实例化(Object instantiation)
通常最简单的一种创建对象的方式如下:
var o = {};
但是这种形式有些缺陷,如果是一个面向对象背景的视角来看,这样做是不便利,并且缺少封装。
JavaScript提供了一种途径,虽然和大多数的语言都不用一一样,例如面向对象阵营中的Java,C++。
JavaScript利用new这个关键字,通过构造器函数来实例化一个对象,
Prototypes作为对象的蓝图(PROTOTYPES AS OBJECT BLUEPRINTS)
让我们来看看利用函数,用和不用new关键字会造成什么结果,注意观察prototype属性是否添加到了新的对象实例中
例6.1
Listing 6.1: Creating an new instance with prototyped method
function Ninja(){}
Ninja.prototype.swingSord = function(){
return true;
}
var ninja1 = Ninja();
assert(ninja1 === undefined , "No instance of Ninja created.");
var ninja2 = new Ninja();
assert(ninja2 && ninja2.swingSword(), "Instance exists and method is callable.");
这个例子说明,函数的prototype作为new出来的对象的蓝图,但是前题是这个函数是被当作构造函数来调用才行。
实例的属性(INSTANCE PROPERTIES)
当使用构造函数通过new关键字来构建一个对象实例的时候,这就意味着我们可以在构造函数中通过prototype来初始化一些值。
让我们来看例6.2
Listing 6.2:Observing the precedence of initialization activities
function Ninja(){
thisswung = 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.");
1.对象的属性从prototype来
2.对喜爱那个的属性从构造函数里面来
当这两者冲突的时候,构造函数优先于prototype。
因为构造函数中的this就是对象实例本身,我们可以在构造函数中做任何初始化的事情。
关联问题(RECONCILING REFERENCES)
这里有个很重要的问题要理解,就是JavaScript如何处理对象实例的属性来关联prototype的时序问题。
在上个例子中,时序问题我们可以能会如此解读:首先一个新的实例对象被创建出来,并传递给了构造函数,然后构造函数的prototype属性会被复制到对象实例中。
然后事实是构造函数内部的逻辑覆盖了prototype的值。
我们会知道,这并不是像我们想的那样,简单的将protype拷贝
例如,如果prototype的值仅仅是简单的拷贝,在这个对象之后对prototype的任何改变都不应该再来影响这个对象才对。但事实是这样吗?
让我们来看例6.3
Listing 6.3: Observing the behavior of changes to the prototype
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.”);
很明显,prototype不是简单的拷贝
简单的时序如下:
1.当你查看一个对象的属性,首先,这个对象检查真身的属性中是否存在,如果存在则使用这个值,如果不存在…
2.他会检查与他有关的那个prototype,如果prototype存在这个属性,则使用此值,如果不存在…
3.这个值就是undefined
我们后面会看到,真正的时序会比现在这个复杂一些,不过我们先这么理解。
这一切是如何工作的?
JavaScript中每个对象丢有一个名字叫做constructor的属性,这个属性关联的是创建此对象的构造函数。由于prototype是构造函数的一个属性,所以每个对象都能有方法找到与他相关的prototype。
在本例中,如果我们查看执行此例子时候的console(in Chrome),我们会看到对象的结构如下:
>ninja.constructor
function Nunja(){
this.swung = true;
}
>ninja.constructor.prototype.swingSword
function(){
return this.swung
}
这里的更新了prototype就同时更新object中的属性值,我们叫他“同步更新”,
这个特性会给我带来很多的用处,这个特点在其他语言中是很少见到的。
利用这个特性,我们可以构建一个函数化的框架,使用这个框架的用户可以使用到未来的函数实现,哪怕对象已经被创建出来了。
Listing 6.4: Fruther observing the behavior of changes to the prtotype
function Ninja(){
this.swung = true;
this.swingSword = function(){
return !this.swung;
}
}
var ninja = new Ninja();
Ninja.prototype.swingSword = function(){
return this.swung;
}
asswert(ninja.swingSwrod(), "Called the instance method, not the prototype method.");
本例中我们看到,在ninja被创建之后,通过更改prototype的值可以影响到ninja中的对应属性。
6.1.2 通过构造器归类对象(Object typing via constuctors)
例6.5
Listing 6.5:Examining the type of an instance and its constructor
function Ninja(){}
var ninja = new Ninja();
assert (typeof ninja == "object", "The type of the instance is object.");
assert (ninja instanceof Ninja, "instance of identifies the constructor.");
assert (ninja.constructor == Ninja, "The ninja object was created by the Ninja function.")
在本例中,我们定义了一个构造函数,然后利用它创建了一个对象实例.
然后我们利用type of来检验这个实例的类型。这里并不很明显,因为一个实例指定是对象,结果指定是“object”.相比起typeof,更有意思的是instanceof,通过instanceof我们可以清晰的看出一个对象实例的构造函数。
另外,我们知道对象都有一个constructor属性,这个属性关联的就是当初创建这个对象实例的构造函数。
注意,constructor属性除了可以查询到原始的构造函数,我们还可以利用这个属性来创建一个新的Ninja对象实例.
例:6.6
Listing 6.6: Instantiating a new object using a reference to a construcor
function Ninja(){}
var ninja = new Ninja();
var ninja2 = new ninja.constructor();
assert(ninja2 instanceof Ninja, "It's a Ninja!");
注意:一个对象实例的constructor属性是可以被更改的,
到这里我们只是接触了一下prototypes的皮毛,下面我们来看看prototypes真正的有趣的地方。
6.1.3 继承和原型链(Inheritance and the prototype chain)
我们之前看到instanceof这个关键字,
如果想要应用它,我们需要理解JavaScript的继承机制,还有prototype chain扮演着什么样的角色。
让我们看一下例6.7,在这个例子中我们会试图将一个继承加入到对象实例中。
Listing 6.7: Trying to achieve inheritance with prototypes
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 Object prototype");
本例中,目的是让Ninja继承Person的dance属性,但是运行的结果发现,ninja instanceof Person这句是false。
说明ninja并不是一个Person
虽然Ninja复用了Person的dance属性,但是他也不意味着就是一个Person
如果我们想让Ninja复用Person的所有属性,那么就需要copy多次,这绝不是继承。
注意:即便什么也不做,每个对象都会继承Object,你可以通过这句话来检验一下:
console.log({}.constructor)
我们的真正目的是prototype chain。
例如一个Ninja可以是一个person,
一个person可以是Mammal,一个Mammal可以是一个Animal,最终都成为一个Object。
创建prototyp chain的一个方式是通过其他对象的prototype,例如:
SubClass.prototype = new SuperClass();
这样通过SubClass创建出来的对像,会拥有SuperClass的所有属性。
另外它的prototype还会指向SuperClass的prototype
我们来更改一下上面的例子,看例6.8:
Listing 6.8 Achieving inheritance with prototypes
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!");
注意,不要用Ninja.prototype = Person.prototype;
如果这样做,那么更改Ninja prototype的时候同样会更改Person prototype(因为他们是同一个对象)
这里要注意,prototype继承模式中,所有继承链中的函数都是实时更新的(live update)。
我们用一个图来解释prototype chain
图6.6
在图6.6中,我们注意到,对象的所有属性都是继承自Object的prototype。
所有自然对象(native objects)的constructors属性(例如Object,Array,String,Number,RegExp, and Function)都拥有prototype属性,并且它是可以被更改和继承的。
这样一来,每个上面提到的对象的constructors都是functions本身.
在语言层面,这是一个很有用的特点,利用它,我们可以扩展这门语言本身。
例如,JavaScript1.6版本会加入一些有用的方法,例如Arrays的forEach().
如果我们想在1.6版本之前就是用forEach(),并且当JavaScript升级到1.6之后能同步使用新的特点,那么请看下面这个例子,我们针对旧的浏览器,实现一个forEach()
Listing 6.9: Implementing the JavaScrript 1.6 array forEach method in a future-compatible manner
if (!Array.prototype.forEach){
Array.prototype.forEach = function(fn, callback){
for (var i = 0; i < this.length; i++){
fn.call(callback || null, this[i], i, this);
}
}
}
["a", "b", "c"].forEach(function(value, index, array){
assert(value, "Is in position" + index + "out of " + (array.length - 1))
})
我们已经看到,我们可以同那个prototypes来增强native JavaScript objects;
现在,让我们来关注一下Dom
6.1.4 HTML DOM prototypes
浏览器的一个有趣的地方是所有的DOM元素都是继承于HTMLElement构造器.
通过操作HTMLElement的prototype,浏览器就会提供我们扩展任何HTML节点的能力。
让我们来看例6.10
Listing 6.10: Adding a new method to all HTML elemeents via the HTMLElement prototype
<div id="a">I'm going to be removed.</div>
<div id="b">Me too!</div>
<script>
HTMLElement.prototype.remove = function(){
if (this.parentNode)
this.parentNode.removeChild(this);
};
var a = document.getEleementById("a");
a.parentNonde.removeChild(a);
document.getElementById("b").remove();
assert(!document.getElementById("a"), "a is gone.");
assert(!document.getElementById("b"), "b is gone too.");
我们添加了一个新的remove()方法,通过更改HTMLElement构造器的prototype。
6.2 The Gotchas!
6.2.1 对象的扩展(Extending Object)
我们可能会犯的及其严重的错误就是去扩展native Object.prototype.
难点在于一旦我们扩展了这个prototype,所有的对象将会受到影响。
让我们来看一个例子
这里我们想在Object上增加一个keys()方法,它会返回一个包含所有属性名的列表
看例子6.11
Listing 6.11: Unexpected behavior of adding extra properties to the Object prototype
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.");
结果是报错的,我们需要hasOwnProperty(),它能区别哪些是真正属于对象实例的属性,并不是引用的prototype.让我们来看例6.12
Listing 6.12: Using the hasOwnProperty() method to tame Object prototype extensions
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.");
6.2.2 Number的扩展(Exending Number)
Listing 6.13: Adding a method to the Number prototype.
Number.prototype.add = function(num){
return this + num;
}
var n = 5;
assert(n.add(3) == 8, "It works when the number is in a variable");
assert((5).add(3) == 8, "Also worrks if a number is wrapped in parentheses.");
assert(5.add(3) == 8, "What about a simple literal?");
本例运行后,浏览器会报错。
It turns out that the syntax parser can’t handle the literal case.
6.2.3 Subclassing native objects
Listing 6.14: Subclassing the Array object
function MyArray(){}
MyArray.prototype = new Array();
var mine = new MyArray();
mine.push(1,2,3);
assert(mine.length == 3, "All the items are on our sub-classed array.");
assert(mine instanceof Array, "Verify that we implement Array functionality.");
这种模式就是制造子类,但是在IE中,浏览器不允许Array被子类化,所以这样做是危险的。
让我用另一种方式来实现,例6.15
Listing 6.15: Simulating Array functionality but without the true sub-classing.
function MyArray(){}
MyArray.prototype.length = 0;
(function(){
var methods = ['push', 'pop', 'shift', 'unshift', 'slice', 'splice', 'join'];
for (var i = 0; i< methods.length; 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 on our sub-classed array.");
assert(!(mine instanceof Array), "We aren't subclassing Array, though.");
6.2.4 实例化的问题(Instantiation issues)
我们已经看到函数可以被当作普通函数调用,也可以作为构造器被调用,也许你还是很模糊,不知道哪个代码是哪个,让我们用些例子来详细解释一下。
Listing 6.16: The result of leavving off thee new operator from a function call.
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");
运行会报错
Listing 6.17: An example of accidentally introducing a variablee into the global namespace
function User(first, last){
this.name = first + " " + last;
}
var name = "Rukia";
var user = User("Ichigo", "Kurosaki");
assert(name == "Rukia", "name was set to Rukia.");
Listing 6.18: Determining if we're called as constructor
function Test(){
return this instanceof arguments.callee;
}
assert(!Test(), "We didn't instantiate, so it returns false.");
assert(new Test(), "We did instantiate, returning true.");
复习一下以前学过的概念:
1.我们可以获得当期被调用的函数的的引用,通过arguments.calle(我们在第四章学过)
2.普通函数的函数上下文是全局域的
3.instanceof关键字可以判断一个对象的构造器
在本例中我们看到这样的表达:
this instanceof arguments.callee
如果为true表示是作为构造函数被调用的,如果为false则表示是作为普通函数被调用的。
这就是所,在函数中,我们可以判断出,它是否作为构造函数被调用。
如果我们不是忍者,如果这个函数没有作为构造函数被调用,我们会抛出一个异常来提醒用户,下次正确使用。但是我们可以做的更好,让我们看看如何修复这个问题
Listing 6.19: Fixing things on the caller's behalf
function User(first, last){
if (! (this instanceof arguments.callee)){
return new User(first, last);
}
this.name = first + " " + last;
}
var name = "Rukia";
var user = User("Ichiggo", "Kurosaki");
assert(name == "Rukia", "Name was set to Rukia.");
assert(user instanceof User, "user instantiated");
assert(user.name == "Ichigo Kurosaki", "User name correctly assigned");
6.3 编写像类一样的代码(Writing class-like code)
JavaScript可以允许我们通过prototype来实现集成,针对于大多数面向对象背景的开发者来说,JavaScript的继承系统是比较熟悉的。
一般来说,这些开发者会渴求几点:
1.一套可以比较轻量的构建新的构造函数和属性的系统
2.一个简单的方式可以执行prototype的继承
3.一个路径可以利用函数的prototype来覆盖methods
有2个比较突出的JavaScript类库实现了类的继承:base2和Prototype。
我们会提取其中的精华部分来展示。
Listing 6.20: An example of somewhat classical-style inheritance syntax
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(){
// Ninja-specific stuff here
return this._super();
},
swingSword: function(){
return true;
}
});
var person = new Person(true);
assert(person.dance(), "The person is dancing.");
var ninja = new Ninja();
assert(ninja.swingSword(), "The sword is swinging.");
assert(!ninja.ance(), "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()方法来构建了一个子类出来,现在我们需要实现这个方法。
Listing 6.21: A sub-classing method
(function(){
//#1 Determines if functions can be serialized
var initializing = false,
fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
//#2 Creates a subclass
Object.subClass = function(prop){
var _super = this.prototype;
//#3 Instantiates the superclass
initializing = true;
var proto = new this();
initializing = false;
//#4 Copies properties into prototype
for (var name in prop){
proto[name] = typeof prop[name] == "function" &&
typpeof _super[name] == "function" && fnTest.test(prop[name]) ?
//#5 Defines overriding function
(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, prop[name]) :
prop[name]
}
//#6 Creates a dummy class constructor
function Class(){
if (!initializing && this.init)
this.init.apply(this, arguments);
}
//#7 Populates class prototype
Class.prototype = proto;
//#8 Overrides constructor reference
Class.constructor = Class;
//#9 Makes class extendable
Class.subClass = arguments.callee;
return Class;
};
})();
这里最重要的两处实现为initialization和super-method protions,
我们下面来一步步的分析这些代码。
让我们开始看看可能以前你们见过的东西。
6.3.1 检查函数是否可序列化(Checking for function serializability)
很不幸,我们代码的一开始就是一段秘传的代码,它可能会让人迷惑。
这段代码的目的是检查浏览器是否支持函数的序列化。
函数序列化是获取函数的代码文本资源。
在大多数浏览器中,函数的toString()方法就可以搞定。
在我们的代码中,我们这样写的:
/xyz/.test(function(){xyz;})
如何函数可以被序列化,那么结果就是true(关于正则表达式我们后面会讨论)
我们写出这样的代码,为了后面用到:
superPattern= /xyz/.test(function(){ xyz; }) ? /\b_super\b/ : /.*/;
这里的superPattern变量,我们后面会用于检验一个函数是否包含”_super”.
现在让我们看看sub-classing方法的代码。
6.3.2 subClasses的实例化(Initialization of subclasses)
在这里,我们会声明一个subclass的方法,我们这样写的:
Object.subClass = function(properties){
var _super = this.prototype;
这里添加到Object上的subClass方法,接受唯一的一个参数,这个参数就是一个属性组,我们需要遍历这个属性组并将它们加到subclass中。
普通的代码一般会写成这样:
function Person(){}
function Ninja(){}
NInja.prototype = new Person();
assert((new Ninja()) instanceof Person, "Ninjas are peeople too!");
6.3.3 保留父级的方法(Preserving super methods)
大多数语言都支持继承,一个方法被重写后我们可以访问重写后的方法。
这是很有用的,有时候我们会重写一个方法,但大部分时候我们只是想要加强一个方法。
在我们的代码中我们创建了一个新的方法叫做_super,它是关联着父类的方法。
例如例6.20,当我们想调用父类方法的构造函数的时候,我们这样写的:
var Person = Object.subclass({
init: function(isDancing){
this.dancing = isDancing;
}
})
var Ninja = Person.subclass({
init: function(){
this._super(false);
}
})
利用_super我们可以省去重新写父类代码的麻烦。
实现这个方法需要多个步骤。
简单来说我们需要merge父类和传递进来的属性。
在开始,我们创建了一个实例,我们将此实例作为prototype,
代码如下:
initializing = true;
var proto = new this();
initializing = false;
记得之前我们讨论过的如何保护初始化吗?对,是通过initializing这个变量标识。
如果我们不考虑父类的函数,我们可以这么写。
for(var name in properties) proto[name] = properties[name];
但是我们要考虑父类的函数,我们会通过_super来引用父类的函数。
我们首先要查明我们是否需要wrap子类函数。我们可以通过下面的表达式:
typeof properties[name] == "function" &&
typeof _super[name] == "function" &&
superPattern.test(properties[name])
这个表示包括了如下的检查项:
1.子类的这个属性是否是个函数?
2.父类的这个属性是否是个函数?
3.子类函数中是否包含_super()?
只有所有条件都满足的时候我们才开始wrap这个函数,具体代码如下:
(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])
6.4 总结(Summary)
在这一章中,我们看到通过prototype我们将面向对象带进了JavaScript。
我首先介绍了prototype的概念,他所扮演的角色。我们也介绍了是否用new关键字来调用一个函数的区别。
接下来,我们学习了如何辨别一个对象的类型。
我们还学习了面向对象中的继承概念,以及学习了如何运用prototype链来影响继承。
我们实现了supclass方法来构建一个子类。
在最后我们还一睹了正则表达式,在下一个章节我们会深入的学习它。
(转载本文章请注明作者和出处 Yann (yannhe.com),请勿用于任何商业用途)