翻译 Secrets of the JavaScript Ninja - 6.原型与面向对象(6.Object-orientation with prototypes)

翻译 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(){  
        returnthis.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
1.jpg

在图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),请勿用于任何商业用途)

猜你喜欢

转载自blog.csdn.net/YannHack/article/details/43916045