Js面向对象漫谈(1) 工厂模式,构造函数模式,原型模式

理解js的对象,下面定义一个对象:

var Person = {
            name: "gao",
            age: 12,
            say: function () {
                document.write(this.name);
            }
        }

以上定义了一个Person对象,有 name 和age属性,和 say()方法.那么这些属性在创建时 都带有一些特征值, Javascript通过这些特征值来定义它们的行为.

属性类型:

ECMAScript-262 第五版 在定义只有内部采用的特性时,描述了 属性的各种特征. ECMAScript-262定义这些特性是为了 实现

Javascript引擎用的. 因为在 javascript中不能直接访问它们,  为了表示特性时 内部值, 用如下表示法: [[Enumerable]].

ECMAScript中有两种属性:

  1. 数据属性
  2. 访问器属性

数据属性:

  • [[Configurable]]:  表示能否通过 delete删除属性从而重新定义属性.能否修改属性的特性,或者能否把属性改为访问器属性.
  • [[Enumerable]] : 表示能否通过for-in循环 返回属性.
  • [[Writable]]  : 表示是否可以修改属性的值.
  • [[Value]] :  包含这个属性的数据值. 这个特性的默认值是  undefined
var Person={
             name:"gao"
                }

定义如上 的Person对象,  那么 定义一个name属性,  也就说 [[Value]]特性,将被设置为 gao,而对这个值的任何修改,都将反映在这个位置.

要修改属性的默认特性,必须使用 ECMAScript 5 的 Object.defineProperty() 方法,看下面实例:

 var Person = {};
        Object.defineProperty(Person, "name", {
        writable: false,
            value:"gao"
        });

        document.write(Person.name); //gao
        Person.name = "new name";
        document.write(Person.name); //gao

访问器属性:

访问器属性不包含 数据值,它们包含 一对儿 Getter 和Setter函数, 如下四个特性:

  1. [[Cofigurable]]:与 数据属性一样
  2. [[Enumerable]] : 与数据属性一样
  3. [[Get]]: 在读取属性时 调用的函数 .默认值为 undefined
  4. [[Set]] : 在写入时调用的函数  .默认值 为 undefined

访问器属性不能直接定义,而需要 object.defineProperty()来定义,看下面例子:

var book = {
            _year: 2010,
            edition: 1
        };

        Object.defineProperty(book, "year", {
            get: function () {
                return this._year;
            }
            ,
            set: function (newValue) {
                if (newValue > 2010) {
                    this._year = newValue;
                    this.edition += newValue - 2010;
                }
            }
        });
        book.year = 2012;
        document.write(book.edition); //3

一次定义多个属性:

 var book = {};
        Object.defineProperties(book, {
            _year: {
                value: 2010
            },
            edition: {
                value: 1
            },
            year: {
                get: function () {
                    return this._year;
                }
            ,
                set: function (newValue) {
                    if (newValue > 2010) {
                        this._year = newValue;
                        this.edition += newValue - 2010;
                    }
                }
            }
        });
        book.year = 2012;
        document.write(book.edition); //1

这样定义之后,发现book.editon的值变成1了,原因就在于,使用Object.defineProperties()定义属性时,会自动设置 属性的特性值 为false,

即 _year和edition的特性值 writable 都为false , 即 _year属性和edition属性的的Value将不会被改变. 所以即使在访问器属性中改变了edition的值,却是不生效的.

如下定义,将会解决这个问题:

var book = {};
        Object.defineProperties(book, {
            _year: {
                writable: true,
                value: 2010,
                
            },
            edition: {
 writable:true,
                value: 1
               
            },
            year: {
                get: function () {
                    return this._year;
                }
            ,
                set: function (newValue) {
                    if (newValue > 2010) {
                        this._year = newValue;
                        this.edition += newValue - 2010;
                    }
                }
            }
        });
        book.year = 2012;
        document.write(book.edition); //3

发现又输出3了.

读取属性的特性

使用 ECMAScript 5的 Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符.

var book = {};
        Object.defineProperties(book, {
            _year: {
                writable: true,
                value: 2010,

            },
            edition: {
                writable: true,
                value: 1

            },
            year: {
                get: function () {
                    return this._year;
                }
            ,
                set: function (newValue) {
                    if (newValue > 2010) {
                        this._year = newValue;
                        this.edition += newValue - 2010;
                    }
                }
            }
        });
        var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
        document.write(descriptor.value);  //2010
        document.write(descriptor.writable);  //true
        document.write(typeof descriptor.get);  //undefined

        descriptor = Object.getOwnPropertyDescriptor(book, "year");
        document.write(descriptor.value);  //  undefined
        document.write(descriptor.enumerable);  //false
        document.write(typeof descriptor.get);   //function

 

工厂模式:

工厂模式 是软件工程领域的一种广为人知的设计模式.这种模式 抽象了创建具体对象的过程.下面看一个例子:

function createPerson(name, age) {
           
            var o = new Object();
            o.name = name;
            o.age = age;
            o.say = function () {
                document.write(this.name);
            }
            return o;
        }

        var p1 = createPerson("小明", 12);
        var p2 = createPerson("小刚", 14);

函数 createPerson()可以根据不同的参数,创建不同的person对象,可以无数次的调用这个函数.

每次调用该函数,都换返回一个 包含两个属性和一个方法的对象.工厂模式虽然解决了创建多个相似对象的问题.

但是却没有解决 对象识别的问题.(即怎样知道一个对象的类型.),那就看 下面的新模式.

构造函数模式:

ECMAScript中的构造函数 可用来创建特定类型的对象. 像 Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中,

此外,我们可以自己定义构造函数,从而定义自定义对象类型的属性和方法,看下面例子:

 function Person() {

            this.name = arguments[0];
            this.age = arguments[1];
            this.say = function () {
                document.write(this.name);
            }

        }

        var p3 = new Person("小丽", 18);
        p3.say();

页面会输出  小丽.

创建Person实例,必须使用new操作符.以这种方式调用构造函数实际会经历一下4步:

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象(因此this指向了这个新对象)
  3. 执行构造函数内的代码(为这个新对象添加属性)
  4. 返回新对象

在这个例子中.p3保存着Person的一个实例.这个对象都有 一个 constructor(构造函数)属性,该属性指向Person,如下所示:

 document.write(p3.constructor==Person); //true

对象的 constructor属性 最初是用来标示 对象类型的.但是检测对象类型,还是用 instanceof来比较可靠.如下:

   document.write(p3 instanceof Object);   //true
        document.write(p3 instanceof Person);   //true

创建自定义的构造函数 意味着将来可以将它的实例标示为一种特定的类型.而这正是 工厂模式的不足之处.  p3是 Object的实例,是因为

所有的对象均继承自 Object.

将构造函数当做函数使用.

构造函数与其他函数的唯一区别 ,就在于调用它们的方式不同.下面看 如下 调用 Person构造函数:

 //当做构造函数使用
        var p3 = new Person("小丽", 18);
        p3.say();
        //当做普通函数使用
        Person("小李", 15);  //添加到 window
        window.say();   //小李
        //在另一个对象的作用于中调用
        var o = new Object();
        Person.call(o, "小寒", 20);
        o.say();  //小寒

 

构造函数的问题

构造函数的问题在于每个方法都要在 每个实例上重复创建一遍. 下面实例:

 var per1 = new Person("小", 1);
        var per2 = new Person("大", 2);
        document.write(per1.say == per2.say);  //false

 

说明,per1 和per2的 say()方法 是不同的,说明是 不同的Function实例(以显示不同的name属性)的本质.

 

然而创建完全相同任务的 Function实例的确没有必要,况且this对象在,根本不用来 执行代码前 就 把函数绑定到特定的对象上.

因此,可以如下定义构造函数.来解决这个问题:

 function Person() {

            this.name = arguments[0];
            this.age = arguments[1];
            this.say =  say;
               
            }
        function say() {
            document.write(this.name);
        }

下载把 say方法的函数定义在构造函数的外面.在 构造函数内部,把say属性设置成一个全局的 say函数.

这样一来,由于 say包含了一个指向函数的指针,因此per1和per2对象就共享了 全局作用域中定义的同一个say函数,

但是这样 , 在全局作用域中定义的函数实际上只能被某个对象调用,有点奢侈. 下面 就用 原型模式 解决这个问题.

 

原型模式

每一个创建的函数都有一个 prototype(原型)属性,这个属性是个指针,指向一个对象.而这个对象的用途是 包含可以由特定类型的所有实例

共享的属性和方法. 字面的意思就是, prototype就是通过调用构造函数而创建的那个对象实例的原型对象.

使用原型对象的好处是 可以让所有对象共享它的所包含的属性和方法. 通俗的说就是 不必再构造函数中定义对象实例的信息,

而是可以将这些信息 直接添加到 原型对象中,下面的例子所示:

function Person() {
        }

        Person.prototype.name = "gao";
        Person.prototype.age = 12;
        Person.prototype.say = function () {
            document.write(this.name);
        }
        var p = new Person();
        p.say();   //gao

        var p2 = new Person();
        p2.say();   //gao
    document.write(p.say == p2.say);  //true

虽然构造函数是空的,但是也可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法.  与构造函数模式不同的是

新对象的这些属性和方法是由 所有实例 共享的. 通俗的讲就似乎  p 和p2访问的都是 同一组属性 和同一个 say()函数.

下面看一下 原型对象的性质

只要创建一个对象,就会根据一组特定的规则为 该函数创建一个 prototype属性,这个属性 指向函数的原型对象.

默认情况下,所有 原型对象 都会自动获得 一个 constructor(构造函数)属性,这个属性 包含一个指向 prototype属性所在的函数指针.

比如前面的例子:   Person.prototype.constructor指向 Person,而通过这个构造函数,还可以继续为 原型对象添加其他属性和方法.

 

创建了自定义函数后,其原型对象默认只会取得 constructor属性,至于其他的方法,则都是从Object继承来的. 当调用构造函数创建一个新实例后,该实例的内部包含一个指针(内部属性),指向构造函数的原型对象.ECMAScript-261第五版 管这个指针叫 [[Prototype]].

下面看一个图例:

image

 

此外 ECMAScript提供 getPrototypeOf(), 可以 找到实例的原型对象.

   document.write(Object.getPrototypeOf(p).name); //gao

虽然我们可以通过对象实例 访问 保存在原型中的值,但是却不能通过实例重写原型中的值.如果 实例的属性和原型的属性同名,那么

将会屏蔽原型的那个属性.

function Person() {
        }

        Person.prototype.name = "gao";
        Person.prototype.age = 12;
        Person.prototype.say = function () {
            document.write(this.name);
        }
        var p = new Person();
        p.name = "new name";
        p.say();  //new name 来自实例
    var p2 = new Person();
        p2.say(); //gao
    delete p.name;  //删除 实例的name属性
        p.say();   //gao 来自原型
 
 

可以使用hasOwnProperty()方法 检测一个属性 是 来自原型还是来自实例.

function Person() {
        }

        Person.prototype.name = "gao";
        Person.prototype.age = 12;
        Person.prototype.say = function () {
            document.write(this.name);
        }
        var p = new Person();
       document.write( p.hasOwnProperty("name")); //false
        
       p.name = "new name";
       document.write(p.hasOwnProperty("name"));//true
        p.say();

        var p2 = new Person();
        p2.say();

        delete p.name;
        document.write(p.hasOwnProperty("name"));//fasle
        p.say();


ECMAScript  还提供一个函数,用于返回 对象上 所有可枚举 的实例属性.

function Person() {
        }

        Person.prototype.name = "gao";
        Person.prototype.age = 12;
        Person.prototype.say = function () {
            document.write(this.name);
        }
        document.write(Object.keys(Person.prototype));  //name,age,say

Object.keys() 接受一个 对象参数,  返回的是一个 数组.

还有一种方法: 可以列出所有属性不管是否可枚举:

document.write(Object.getOwnPropertyNames(Person.prototype));  //age,name,say,constructor

 

更简单的原型语法:

function Person() {
        }

        Person.prototype = {
            name: "gao",
            age: 12,
            say: function () {
                alert(this.name);
            }

        }
        var p = new Person();
        document.write(p instanceof Person);  //true
        document.write(p.constructor);  //function Object() { [native code] }

可以看到 这样的写法,会使  实例的 构造函数 变成 Object ,而不再是 Person.如想想强制改变,那么看如下代码:

function Person() {
        }

        Person.prototype = {
            name: "gao",
            age: 12,
            say: function () {
                alert(this.name);
            }

        }
        Object.defineProperty(Person.prototype, "constructor", {  // 重设 Person 原型对象的 constructor属性
            enumerable: false,  // 由于这种重设的方式,会导致 constructor属性的 [[Enumerable]]特性变为true,即可以枚举,顾显示设置成 false,即屏蔽枚举 此属性
            value: Person   //设置 构造函数的名字

        });

        var p = new Person();
        document.write(p instanceof Person);  //true
        document.write(p.constructor); //Person

原型模式的问题:

由于原型模式的原因,  原型对象的属性和方法是 所有实例共享的.那么问题就来了,看下面的实例:

 function Person() { };
        Person.prototype = {
            constructor: Person,
            name: "gao",
            age: 14,
            friends: ["lin", "feng"],
            say: function () {
                alert(this.friends);
            }
        }
        var p1 = new Person();
        var p2 = new Person();
        p1.friends.push("newFriend");
        p1.say();  //lin  feng  newFriend 3为朋友
        p2.say();   //由于p1多了一位朋友,所以 在Person的原型对象中也相应加了一位朋友,而p2是Person的实例,所以也变了
 

下面介绍一中方法解决这种 连带的问题,  混合模式(构造函数模式+原型模式);

混合模式

构造函数用于定义实例属性, 而原型模式用于定义方法和 共享的属性. 结果每个实例都有自己的一份实例属性的副本,但同时

又共享着对方法的引用. 从而最大限度的节省了 内存.  另外,混合模式还支持 传递参数.看下例:

function Person() {

            this.name = arguments[0];
            this.age = arguments[1];
            this.friends = arguments[2];
        }
        Person.prototype.say = {
            constructor: Person,
            say: function () {
                alert(this.name);
            }
        }
        var p1 = new Person("p1", 12, ["p11", "p12"]);
        var p2 = new Person("p2", 18, ["p21"]);
        p1.friends.push("p13");
        alert(p1.friends);  //p11 p12 p13
        alert(p2.friends);   //p21

动态原型模式

function Person() {
            this.name = arguments[0];
            this.age = arguments[1];
            if (typeof this.say != "function") {

                Person.prototype.say = function () {
                    alert(this.name);
                }
            }
        }
        var p1 = new Person("p1", 14);
        var p2 = new Person("p2", 16);

创建p1时 this.say()由于不是函数,所以会 实例化一个say()函数,当实例化p2时,person的原型对象已经存在函数say()了,那么将共享say(),而无需再创建一个 say()函数的实例了. 这样就节省了内存.

寄生构造函数模式

基本思想就是 : 创建一个函数,该函数的作用仅仅是 封装创建对象的代码,然后在返回对象的实例.看下面例子:

function Person(name) {
            var o = new Object();
            o.name = name;
            o.say = function () { alert(this.name); };
            return o;
        }

        var p = new Person("gao");
        p.say();  //gao

 

这个例子中,Person函数创建了一个对象o,给o附加属性和方法,在返回该对象.  除了使用new操作符并把使用的包装函数叫做构造函数之外,和工厂模式没有什么区别.

构造函数在不返回值的情况下,默认返回一个新对象实例. 而通过在 构造函数 末尾添加一个return 语句,可以重写 调用构造函数时返回的值.

这个模式可以再 特殊的情况下 用来为对象创建构造函数.看下面例子:

function CustomArray() {

            var value = new Array();

            value.push.apply(value, arguments);

            value.toCutomString = function () {
                return this.join('|');
            }

            return value;
        }

        var array = new CustomArray("1", "2", "3");
        alert(array.toCutomString()); // 1|2|3
    alert( array instanceof CustomArray); //false

使用这个模式需要注意一下几点:

返回的对象与构造函数或者与构造函数原型之间没有关系. 为此,不能依赖instanceof操作符查看对象类型.

posted @ 2012-05-21 09:16  高捍得  阅读(856)  评论(0编辑  收藏  举报