《javascript设计模式》笔记之第四章:继承

一:首先,一个简单的继承实例
首先是创建一个父类Person:

function Person(name) {
  this.name = name;
}

Person.prototype.getName = function() {
  return this.name;
}
下面是创建一个子类Author,用于继承Person:
分两步,第一步继承父类的属性,第二部继承父类的公共方法

function Author(name, books) {
  Person.call(this, name); // 通过这一行代码可以继承父类的属性
  this.books = books; // 子类增添自己的属性
}

Author.prototype = new Person(); // 继承父类方法的第一行代码
Author.prototype.constructor = Author; // 继承父类方法的第二行代码
Author.prototype.getBooks = function() { // 子类增添自己的方法
  return this.books;
};

这样就完成了一个简单的继承
二:编写一个extend函数来简化类的声明
extend函数:
function extend(subClass, superClass) {
  var F = function() {};
  F.prototype = superClass.prototype;
  subClass.prototype = new F();
  subClass.prototype.constructor = subClass;
}
extend的函数作用有两个,一为简化代码,二就是使继承父类方法的时候不用运行父类的构造函数然后Author这个子类的定义就变成了这样:
function Author(name, books) {
  Person.call(this, name);
  this.books = books;
}
extend(Author, Person);

Author.prototype.getBooks = function() {
  return this.books;
};
到目前为止,我们要调用超类的方法,是要以Person开头来调用的(例如:Person.call。。。。等),如果我们想要通过Author开头来调用父类的方法,我们就要对extend函数做一些改动改进的extend函数:
function extend(subClass, superClass) {
  var F = function() {};
  F.prototype = superClass.prototype;
  subClass.prototype = new F();
  subClass.prototype.constructor = subClass;

  subClass.superclass = superClass.prototype;
  if(superClass.prototype.constructor == Object.prototype.constructor) {
    superClass.prototype.constructor = superClass;
  }
}
利用上面的倒数第五行代码,我们就可以使子类可以调用通过Author.这种方法调用父类的方法了(这样做是为了不让Person出现在Author类的定义中),然后我们就可以把Author的定义改成以下:
function Author(name, books) {
  Author.superclass.constructor.call(this, name);
  this.books = books;
}
extend(Author, Person);

Author.prototype.getBooks = function() {
  return this.books;
};

Author.prototype.getName = function() {
  var name = Author.superclass.getName.call(this);
  return name + ', Author of ' + this.getBooks().join(', ');
};
这样的话,Author的定义中,除了extend函数中有出现过Person,其他地方都没有出现Person了~三: 原型式继承接下来,我们使用原型式继承来重新设计Person和Author:首先,clone函数(按照书上的顺序,这个可以先不看):
function clone(object) {
    function F() {}
    F.prototype = object;
    return new F;
}
步骤一(定义Person原型):
var Person = {
  name: 'default name',
  getName: function() {
    return this.name;
  }
};
步骤二(通过原型式继承,创建Author原型):
var Author = clone(Person);
Author.books = []; // Default value.
Author.getBooks = function() {
  return this.books;
}
步骤三(使用):
var author = [];

author[0] = clone(Author);
author[0].name = 'Dustin Diaz';
author[0].books = ['JavaScript Design Patterns'];

author[1] = clone(Author);
author[1].name = 'Ross Harmes';
author[1].books = ['JavaScript Design Patterns'];

author[1].getName();
author[1].getBooks();
通过这样的方法,传统意义的类就变成这样一个个的对象了!(虽然在javascript中类也是对象。。。)这种方法的特点就是所有对象共用一个对象原型,其实就是这么简单。但是这导致的一个问题就是读和写的不对等。下面例子说明这个问题:
var authorClone = clone(Author);
alert(authorClone.name); // 调用的其实是Person.name 
                         // 所以输出的是字符串'default name'.
authorClone.name = 'new name'; // 这样做其实就是在自己的对象中创建一个name属性
alert(authorClone.name); // 这样再使用name属性的时候,就会变成使用自己的name属性,而不是原型上的Person.name了
                         // 所以这时候的值是'new name'
authorClone.books.push('new book'); // 对于数组也就是一样的道理,这行代码没有创建自身的books属性,等于直接操作原型里面的属性,所以就会导致原型的默认值编程‘new book’了,这会作用到其他使用原型继承的其他对象当中
authorClone.books = []; // 所以解决的方法就是在自身的对象中新建一个books数组
authorClone.books.push('new book'); // 然后再进行对数组的操作
提示,通过hasOwnProperty可以去分对象的实际成员和它继承而来的成员
此外,上述的这种情况对于拥有对象属性的原型来说也是一样,其实我们也可以用同一种办法来解决,例如下面这个实例:


var CompoundObject = {
  string1: 'default value',
  childObject: {
    bool: true,
    num: 10
  }
}

var compoundObjectClone = clone(CompoundObject);

// 这样做的话就会破坏原型的默认值了
compoundObjectClone.childObject.num = 5;

// 这个解决方法和上面数组那个一样,在自身的原型里面创建一个心的childObject对象
compoundObjectClone.childObject = {
  bool: true,
  num: 5
};
但是,问题又来了,我想要知道默认值的情况下创建自身的childObject对象要怎样做呢?下面就是一种解决方法,利用工厂方法来创建自身的childObject

var CompoundObject = {};
CompoundObject.string1 = 'default value',
CompoundObject.createChildObject = function() {//这个函数是关键,返回一份副本
  return {
    bool: true,
    num: 10
  }
};
CompoundObject.childObject = CompoundObject.createChildObject();//这里就在原型里面先获取一次这个对象副本,原型就有了childObject属性

var compoundObjectClone = clone(CompoundObject);
compoundObjectClone.childObject = CompoundObject.createChildObject();//这里就创建新对象自己的childObject属性
compoundObjectClone.childObject.num = 5;
四: 类式继承和原型式继承的对比类式继承的优点就是很多人都知道原型式继承的优点就是代码简洁,并且节约内存(因为如果克隆出来的对象不新建自己的属性的话,那么就只有原型上的一份公共副本而已)五: 继承与封装
如果要继承的话,最好父类使用门户大开方法
六: 掺元类javascript中只能继承一个父类,所以如有一些不需要严格继承,但是很多类都用到的方法,我们就通过扩充的方式让这些类共享这些方法,例子:一个类(其实就是包含一个简单的toJSONString方法):
var Mixin = function() {};
Mixin.prototype = {
  serialize: function() {
    var output = [];
    for(key in this) {
      output.push(key + ': ' + this[key]);
    }
    return output.join(', ');
  }
};
我们很多类都需要用到上面这个类的serialize方法,所以我们就用扩充函数来扩充需要这个方法的类(扩充函数的具体代码下面会给出):
augment(Author, Mixin);
让后就可以使用了:
var author = new Author('Ross Harmes', ['JavaScript Design Patterns']);
var serializedString = author.serialize();
现在给出augment函数的具体代码:
function augment(receivingClass, givingClass) {
  for(methodName in givingClass.prototype) { 
    if(!receivingClass.prototype[methodName]) {
      receivingClass.prototype[methodName] = givingClass.prototype[methodName];
    }
  }
}
其实就是遍历有没有同名的方法,没有的话就添加到要扩展的类中。如果想要只扩展某些方法,而不是全部方法,那么就改进一下augment函数:
function augment(receivingClass, givingClass) {
  if(arguments[2]) { // Only give certain methods.
    for(var i = 2, len = arguments.length; i < len; i++) {
      receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]];
    }
  } 
  else { // Give all methods.
    for(methodName in givingClass.prototype) { 
      if(!receivingClass.prototype[methodName]) {
        receivingClass.prototype[methodName] = givingClass.prototype[methodName];
      }
    }
  }
}
改进的augment函数可以选传第三个以后的参数,这些参数都是方法名。然后函数会检测有没有选传的参数,如果有的话就只扩展与这些参数同名的方法。否则扩展全部方法。七: 示例:就地编辑
下面就是使用一个例子,来演示上所说的三种方法。
例子说明:假设你的任务就是编写一个用于创建和管理就地编辑域的可重用的模块化API(就地编辑是指网页上的一段普通文本被点击后就变成一个配有一些按钮的表单域,以便用户对这段文本进行编辑)。使用这个API,用户应该能够为对象分配一个唯一的ID值,能够为它提供一个默认值,并且能够指定其在页面上的目标位置。用户还应该在任何时候都可以访问到这个域的当前值,并且可以选择具体使用的编辑域(比如多行文本框或单行文本框)。
也就是说,平时是这个样子的:

点击之后会变成这样在的:   

文本框中填写"改变文本",save后:

接下来,我们的需求更改了,要把input框变成textarea框,怎么办了?也就是说
点击之后要变成这样子:


方法一:类式继承解决方案
首先创建一个父类EditInPlaceField以及演示它的使用:
<html>

<head>
    <meta http-equiv="charset" content="utf-8"></head>

<body>
    <script>
        //类的定义
        function EditInPlaceField(id, parent, value) {
            this.id = id;
            this.parentElement = parent;
            this.value = value || 'default value';

            this.createElements(this.id);
            this.attachEvents();
        }

        EditInPlaceField.prototype = {
            createElements: function (id) {
                this.containerElement = document.createElement('div');
                this.parentElement.appendChild(this.containerElement);

                this.staticElement = document.createElement('span');
                this.containerElement.appendChild(this.staticElement);
                this.staticElement.innerHTML = this.value;

                this.fieldElement = document.createElement('input');
                this.fieldElement.type = 'text';
                this.fieldElement.value = this.value;
                this.containerElement.appendChild(this.fieldElement);

                this.saveButton = document.createElement('input');
                this.saveButton.type = 'button';
                this.saveButton.value = 'Save';
                this.containerElement.appendChild(this.saveButton);

                this.cancelButton = document.createElement('input');
                this.cancelButton.type = 'button';
                this.cancelButton.value = 'Cancel';
                this.containerElement.appendChild(this.cancelButton);

                this.convertToText();
            },
            attachEvents: function () {
                var that = this;
                this.staticElement.addEventListener('click', function () {
                    that.convertToEditable();
                },false);
                this.saveButton.addEventListener('click', function () {
                    that.save();
                },false);
                this.cancelButton.addEventListener('click', function () {
                    that.cancel();
                },false);
            },
            convertToEditable: function () {
                this.staticElement.style.display = 'none';
                this.fieldElement.style.display = 'inline';
                this.saveButton.style.display = 'inline';
                this.cancelButton.style.display = 'inline';

                this.setValue(this.value);
            },
            save: function () {
                this.value = this.getValue();
                this.convertToText();
            },
            cancel: function () {
                this.convertToText();
            },
            convertToText: function () {
                this.fieldElement.style.display = 'none';
                this.saveButton.style.display = 'none';
                this.cancelButton.style.display = 'none';
                this.staticElement.style.display = 'inline';

                this.setValue(this.value);
            },

            setValue: function (value) {
                this.fieldElement.value = value;
                this.staticElement.innerHTML = value;
            },
            getValue: function () {
                return this.fieldElement.value;
            }
        }
        
        //类的使用
        var body = document.body;
        var titleClassical = new EditInPlaceField('titleClassical', body, 'Title Here');
    </script></body>

</html>
接下来,就演示如何通过继承来把input装换成textarea框,增添的代码如下:
        function extend(subClass, superClass) {
            var F = function () {};
            F.prototype = superClass.prototype;
            subClass.prototype = new F();
            subClass.prototype.constructor = subClass;

            subClass.superclass = superClass.prototype;
            if (superClass.prototype.constructor == Object.prototype.constructor) {
                superClass.prototype.constructor = superClass;
            }
        }

        //继承
        function EditInPlaceArea(id, parent, value) {
            EditInPlaceArea.superclass.constructor.call(this, id, parent, value);
        };
        extend(EditInPlaceArea, EditInPlaceField);

        EditInPlaceArea.prototype.createElements = function (id) {
            this.containerElement = document.createElement('div');
            this.parentElement.appendChild(this.containerElement);

            this.staticElement = document.createElement('p');
            this.containerElement.appendChild(this.staticElement);
            this.staticElement.innerHTML = this.value;

            this.fieldElement = document.createElement('textarea');
            this.fieldElement.value = this.value;
            this.containerElement.appendChild(this.fieldElement);

            this.saveButton = document.createElement('input');
            this.saveButton.type = 'button';
            this.saveButton.value = 'Save';
            this.containerElement.appendChild(this.saveButton);

            this.cancelButton = document.createElement('input');
            this.cancelButton.type = 'button';
            this.cancelButton.value = 'Cancel';
            this.containerElement.appendChild(this.cancelButton);

            this.convertToText();
        };
        EditInPlaceArea.prototype.convertToEditable = function () {
            this.staticElement.style.display = 'none';
            this.fieldElement.style.display = 'block';
            this.saveButton.style.display = 'inline';
            this.cancelButton.style.display = 'inline';

            this.setValue(this.value);
        };
        EditInPlaceArea.prototype.convertToText = function () {
            this.fieldElement.style.display = 'none';
            this.saveButton.style.display = 'none';
            this.cancelButton.style.display = 'none';
            this.staticElement.style.display = 'block';

            this.setValue(this.value);
        };
        
        var AreaObject = new EditInPlaceArea('AreaObject', body, '多行文本框默认值');
继承之后,再重写相应的方法,就完成了所有需求了。
方法二:原型式继承解决方案
第一步:
<html>

<head>
    <meta http-equiv="charset" content="utf-8"></head>

<body>
    <script>
        function clone(object) {
            function F() {}
            F.prototype = object;
            return new F;
        }

        var EditInPlaceField = {
            configure: function (id, parent, value) {
                this.id = id;
                this.value = value || 'default value';
                this.parentElement = parent;

                this.createElements(this.id);
                this.attachEvents();
            },
            createElements: function (id) {
                this.containerElement = document.createElement('div');
                this.parentElement.appendChild(this.containerElement);

                this.staticElement = document.createElement('span');
                this.containerElement.appendChild(this.staticElement);
                this.staticElement.innerHTML = this.value;

                this.fieldElement = document.createElement('input');
                this.fieldElement.type = 'text';
                this.fieldElement.value = this.value;
                this.containerElement.appendChild(this.fieldElement);

                this.saveButton = document.createElement('input');
                this.saveButton.type = 'button';
                this.saveButton.value = 'Save';
                this.containerElement.appendChild(this.saveButton);

                this.cancelButton = document.createElement('input');
                this.cancelButton.type = 'button';
                this.cancelButton.value = 'Cancel';
                this.containerElement.appendChild(this.cancelButton);

                this.convertToText();
            },
            attachEvents: function () {
                var that = this;
                this.staticElement.addEventListener('click', function () {
                    that.convertToEditable();
                }, false);
                this.saveButton.addEventListener('click', function () {
                    that.save();
                }, false);
                this.cancelButton.addEventListener('click', function () {
                    that.cancel();
                }, false);
            },

            convertToEditable: function () {
                this.staticElement.style.display = 'none';
                this.fieldElement.style.display = 'inline';
                this.saveButton.style.display = 'inline';
                this.cancelButton.style.display = 'inline';

                this.setValue(this.value);
            },

            save: function () {
                this.value = this.getValue();
                this.convertToText();
            },


            cancel: function () {
                this.convertToText();
            },
            convertToText: function () {
                this.fieldElement.style.display = 'none';
                this.saveButton.style.display = 'none';
                this.cancelButton.style.display = 'none';
                this.staticElement.style.display = 'inline';

                this.setValue(this.value);
            },

            setValue: function (value) {
                this.fieldElement.value = value;
                this.staticElement.innerHTML = value;
            },
            getValue: function () {
                return this.fieldElement.value;
            }
        };

        var body = document.body;
        var titlePrototypal = clone(EditInPlaceField);
        titlePrototypal.configure('titlePrototypal ', body, 'Title Here');
    </script></body>

</html>
第二步继承,添加的代码:
        var EditInPlaceArea = clone(EditInPlaceField);

        EditInPlaceArea.createElements = function (id) {
            this.containerElement = document.createElement('div');
            this.parentElement.appendChild(this.containerElement);

            this.staticElement = document.createElement('p');
            this.containerElement.appendChild(this.staticElement);
            this.staticElement.innerHTML = this.value;

            this.fieldElement = document.createElement('textarea');
            this.fieldElement.value = this.value;
            this.containerElement.appendChild(this.fieldElement);

            this.saveButton = document.createElement('input');
            this.saveButton.type = 'button';
            this.saveButton.value = 'Save';
            this.containerElement.appendChild(this.saveButton);

            this.cancelButton = document.createElement('input');
            this.cancelButton.type = 'button';
            this.cancelButton.value = 'Cancel';
            this.containerElement.appendChild(this.cancelButton);

            this.convertToText();
        };
        EditInPlaceArea.convertToEditable = function () {
            this.staticElement.style.display = 'none';
            this.fieldElement.style.display = 'block';
            this.saveButton.style.display = 'inline';
            this.cancelButton.style.display = 'inline';

            this.setValue(this.value);
        };
        EditInPlaceArea.convertToText = function () {
            this.fieldElement.style.display = 'none';
            this.saveButton.style.display = 'none';
            this.cancelButton.style.display = 'none';
            this.staticElement.style.display = 'block';

            this.setValue(this.value);
        };
        
        //使用
        var editInPlaceArea = clone(EditInPlaceArea);
        editInPlaceArea.configure('editInPlaceArea ', body, 'editInPlaceArea');
这样就完成了,其实,相对于第一种方法,改动是很少的~。
方法三:掺元类解决方案
先把扩展类复制进代码中:
function augment(receivingClass, givingClass) {
        if (arguments[2]) { // Only give certain methods.
                for (var i = 2, len = arguments.length; i < len; i++) {
                        receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]];
                }
        } else { // Give all methods.
                for (methodName in givingClass.prototype) {
                        if (!receivingClass.prototype[methodName]) {
                                receivingClass.prototype[methodName] = givingClass.prototype[methodName];
                        }
                }
        }
}
然后创建一个包含了所有要共享的方法的类:
var EditInPlaceMixin = function() {};
EditInPlaceMixin.prototype = {
    createElements: function(id) {
        this.containerElement = document.createElement('div');
        this.parentElement.appendChild(this.containerElement);

        this.staticElement = document.createElement('span');
        this.containerElement.appendChild(this.staticElement);
        this.staticElement.innerHTML = this.value;

        this.fieldElement = document.createElement('input');
        this.fieldElement.type = 'text';
        this.fieldElement.value = this.value;
        this.containerElement.appendChild(this.fieldElement);

        this.saveButton = document.createElement('input');
        this.saveButton.type = 'button';
        this.saveButton.value = 'Save';
        this.containerElement.appendChild(this.saveButton);

        this.cancelButton = document.createElement('input');
        this.cancelButton.type = 'button';
        this.cancelButton.value = 'Cancel';
        this.containerElement.appendChild(this.cancelButton);

        this.convertToText();
    },

    attachEvents: function() {
        var that = this;
        this.staticElement.addEventListener('click', function() {
            that.convertToEditable();
        }, false);
        this.saveButton.addEventListener('click', function() {
            that.save();
        }, false);
        this.cancelButton.addEventListener('click', function() {
            that.cancel();
        }, false);
    },

    convertToEditable: function() {
        this.staticElement.style.display = 'none';
        this.fieldElement.style.display = 'inline';
        this.saveButton.style.display = 'inline';
        this.cancelButton.style.display = 'inline';

        this.setValue(this.value);
    },

    save: function() {
        this.value = this.getValue();
        this.convertToText();
    },

    cancel: function() {
        this.convertToText();
    },
    convertToText: function() {
        this.fieldElement.style.display = 'none';
        this.saveButton.style.display = 'none';
        this.cancelButton.style.display = 'none';
        this.staticElement.style.display = 'inline';

        this.setValue(this.value);
    },

    setValue: function(value) {
        this.fieldElement.value = value;
        this.staticElement.innerHTML = value;
    },
    getValue: function() {
        return this.fieldElement.value;
    }
};
下一步就是创建一个EditInPlaceField类,然后扩展它
function EditInPlaceField(id, parent, value) {
    this.id = id;
    this.value = value || 'default value';
    this.parentElement = parent;

    this.createElements(this.id);
    this.attachEvents();
};
augment(EditInPlaceField, EditInPlaceMixin);
像第一种方法一样使用
var body = document.body;
var titleClassical = new EditInPlaceField('titleClassical', body, 'Title Here');
将input框变成textarea的做法如下:首先创建EditInPlaceArea构造方法:
        function EditInPlaceArea(id, parent, value) {
            this.id = id;
            this.value = value || 'default value';
            this.parentElement = parent;

            this.createElements(this.id);
            this.attachEvents();
        };
添加要重写的方法:
        EditInPlaceArea.prototype.createElements = function (id) {
            this.containerElement = document.createElement('div');
            this.parentElement.appendChild(this.containerElement);

            this.staticElement = document.createElement('p');
            this.containerElement.appendChild(this.staticElement);
            this.staticElement.innerHTML = this.value;

            this.fieldElement = document.createElement('textarea');
            this.fieldElement.value = this.value;
            this.containerElement.appendChild(this.fieldElement);

            this.saveButton = document.createElement('input');
            this.saveButton.type = 'button';
            this.saveButton.value = 'Save';
            this.containerElement.appendChild(this.saveButton);

            this.cancelButton = document.createElement('input');
            this.cancelButton.type = 'button';
            this.cancelButton.value = 'Cancel';
            this.containerElement.appendChild(this.cancelButton);

            this.convertToText();
        };
        EditInPlaceArea.prototype.convertToEditable = function () {
            this.staticElement.style.display = 'none';
            this.fieldElement.style.display = 'block';
            this.saveButton.style.display = 'inline';
            this.cancelButton.style.display = 'inline';

            this.setValue(this.value);
        };
        EditInPlaceArea.prototype.convertToText = function () {
            this.fieldElement.style.display = 'none';
            this.saveButton.style.display = 'none';
            this.cancelButton.style.display = 'none';
            this.staticElement.style.display = 'block';

            this.setValue(this.value);
        };
最后才是扩展:
augment(EditInPlaceArea, EditInPlaceMixin);
使用:
var AreaObject = new EditInPlaceArea('AreaObject', body, '多行文本框默认值');
其实这种方法用在这个例子上是不是很适合的,因为上面那两个类都很相似,导致要先重写一些方法再扩展。












posted @ 2015-03-20 16:00  oadaM92  阅读(172)  评论(0编辑  收藏  举报