JavaScript-工厂模式&&构造函数模式&&原型模式

本文介绍了JavaScript中创建对象的三种模式:工厂模式、构造函数模式和原型模式。讨论了各自的特点和应用场景,如工厂模式解决创建多个相似对象的问题,但无法识别对象类型;构造函数模式能创建自定义类型,但可能导致全局污染;原型模式通过共享原型对象的方法和属性节省内存,允许动态扩展。此外,还探讨了原型的动态性、原生对象的原型问题以及如何组合使用构造函数和原型模式来平衡实例属性和共享方法的需求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ECMAScript是中没有类的概念,ECMA-262 把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。”严格来讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。

Object构造函数方法

创建自定义对象的最简单方式就是创建一个 Object 的实例,然后再为它添加属性和方法。这里的Object实际上是默认的构造函数

   var zw=new Object();
   zw.name="zw";
   zw.age=20;
   zw.job="student";
   zw.getName=function(){
       console.log(this.name);
   }
   zw.getName();//zw

使用字面量方法创建对象

字面量用来为变量赋值时的常数量

对象字面值是封闭在花括号对({})中的一个对象的零个或多个”属性名:值”列表。

   var zw={
        name:"zw",
        age:20,
        job:"student",
        getName:function(){
            console.log(this.name);
        }
   }
   console.log(zw);//{name: "zw", age: 20, job: "student", getName: ƒ}

控制台依次输出zw的属性和值,getName:f表示getName是一个函数。

虽然 Object 构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。

工厂模式

 ECMAScript 中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节。

  function my(name,age,job){
       var zw=new Object();
       zw.name=name;
       zw.age=age;
       zw.job=job;
       zw.getName=function(){
           console.log(this.name);
       }
       return zw;
   }
   var per=my("zw",20,"student");
   console.log(per instanceof my);//false
   console.log(per instanceof Object);//true,因为在js中函数也是对象
   per.getName();//zw

工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型,譬如怎样测试出per这个对象是person这个类型的。

构造函数模式

  function my(name,age,job){
       this.name=name;
       this.age=age;
       this.job=job;
       this.getName=function(){
           console.log(this.name);
       }
      
   }
   var per=new my("zw",20,"student");
   console.log(per instanceof my);//true
   console.log(per instanceof Object);//true
   per.getName();//zw

与上面工厂模式不同的是:

  1. 没有显式地创建对象;
  2. 直接将属性和方法赋给了 this 对象;
  3. 没有 return 语句。

要创建 my 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4个步骤:
    (1) 创建一个新对象;
    (2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
    (3) 执行构造函数中的代码(为这个新对象添加属性);
    (4) 返回新对象。

而且这种模式下per保存了my的一个实例,per对象有一个constructor属性,该属性指向my。对象的 constructor 属性最初是用来标识对象类型的。但是,提到检测对象类型,还是用上面的 instanceof 操作符要更可靠一些。

console.log(per.constructor==my);//true

       创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。在这个例子中, 测试per是 Object 的实例,是因为所有对象均继承自 Object。

       而且创建多个实现相同功能的对象是没有多大意义的,但是把getName移出构造函数外,又导致了一个全局作用域中的函数只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。

原型模式

我们创建的每个函数都有一个 prototype (原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。

举例:

   function person(){
   }

   person.prototype.name="zw";
   person.prototype.age=20;
   person.prototype.job="student";
   person.prototype.getName=function(){
       console.log(this.name);
   }
   var zw=new person();
   zw.getName();//zw
   
   var zz=new person();
   zz.getName();//zw

   console.log(zz.name==zw.name);//true
   console.log(zz.getName==zw.getName);//true

zz和zw的访问的是同一个name属性和getName()函数,可见原型对象中的属性和方法是所有实例所共享的。

1.原型对象的性质

       无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。就拿前面的例子来说,person.prototype. constructor 指向 person 。而通过这个构造函数,我们还可继续为原型对象添加其他属性和方法。

       创建了自定义的构造函数之后,其原型对象默认只会取得 constructor 属性;至于其他方法,则都是从 Object 继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。

        虽然在所有实现中都无法访问到 [[Prototype]] ,但可以通过 isPrototypeOf() 方法来确定对象之间是否存在这种关系。从本质上讲,如果 [[Prototype]] 指向调用 isPrototypeOf() 方法的对象(person.prototype ),那么这个方法就返回 true ,如下所示:

   console.log(person.prototype.isPrototypeOf(zw));//true
   console.log(person.prototype.isPrototypeOf(zz));//true

ES5中增加了一个新方法Object.getPrototypeOf() ,在所有支持的实现中,这个方法返回 [[Prototype]] 的值。

   console.log(Object.getPrototypeOf(zz) == person.prototype); //true
   console.log(Object.getPrototypeOf(zz).name);//zw

每当代码要寻找一个对象某个属性时,先会在这个对象实例中寻找它的属性,找到则返回该值,如果没有找到,则去这个对象所指向的原型对象中去寻找,然后返回该值。

   function person(){
   }

   person.prototype.name="zw";
   person.prototype.age=20;
   person.prototype.job="student";
   person.prototype.getName=function(){
       console.log(this.name);
   }
   
   var zw=new person();
   var zz=new person();
   zz.name="zz";
   console.log(zw.name);//zw
   console.log(zz.name);//zz 

zz对象实例的name确是存在,所以不用去原型中继续寻找值,返回的就是zz,相当于zw被屏蔽掉了!而且对原型中的值没有任何影响。

使用 delete 操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性。

   function person(){
   }

   person.prototype.name="zw";
   person.prototype.age=20;
   person.prototype.job="student";
   person.prototype.getName=function(){
       console.log(this.name);
   }
   
   var zw=new person();
   var zz=new person();
   zz.name="zz";
   console.log(zw.name);//zw
   console.log(zz.name);//zz 
   
   delete zz.name;
   console.log(zz.name);//zw

 使用hasOwnProperty() 方法可以检测一个属性是存在于实例中,还是存在于原型中。

   function person(){
   }

   person.prototype.name="zw";
   person.prototype.age=20;
   person.prototype.job="student";
   person.prototype.getName=function(){
       console.log(this.name);
   }
   
   var zw=new person();
   var zz=new person();
   zz.name="zz";
   console.log(zw.hasOwnProperty("name"));//false来自原型
   console.log(zz.hasOwnProperty("name"));//true来自实例
   delete zz.name;
   console.log(zw.hasOwnProperty("name"));//false来自原型
   console.log(zz.hasOwnProperty("name"));//false来自原型

2.原型与in操作符

in操作符单独使用:in 操作符会在通过对象能够访问给定属性时返回 true ,无论该属性存在于实例中还是原型中。

   function person(){
   }

   person.prototype.name="zw";
   person.prototype.age=20;
   person.prototype.job="student";
   person.prototype.getName=function(){
       console.log(this.name);
   }
   
   var zw=new person();
   var zz=new person();
   zz.name="zz";
   console.log("name" in zw);//true
   console.log("name" in zz);//true
   delete zz.name;
   console.log("name" in zw);//true
   console.log("name" in zz);//true

同时使用 hasOwnProperty() 方法和 in 操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中

    function hasPrototypeProperty(object, name) {
        return !object.hasOwnProperty(name) && (name in object);
    }
    console.log(hasPrototypeProperty(zz,"name"));//true

在 for-in 循环中使用in操作符

返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性(即将[[Enumerable]] 标记为 false 的属性)的实例属性也会在 for-in 循环中返回。

要取得对象上所有可枚举的实例属性,可以使用 ECMAScript 5 的 Object.keys() 方法。

    function person() {}

    person.prototype.name = "zw";
    person.prototype.age = 20;
    person.prototype.job = "student";
    person.prototype.getName = function () {
        console.log(this.name);
    }

    console.log(Object.keys(person.prototype));// ["name", "age", "job", "getName"]
    var zz = new person();
    zz.name = "zz";
    zz.age = 21;
    console.log(Object.keys(zz));//["name", "age"]
    var zw=new person();
    console.log(Object.keys(zw));//[]

可以看到,原型对象作为参数则返回的是原型对象的所有属性,实例对象返回的是其实例属性,没有则返回空数组。

 

如果想要得到所有实例属性,无论它是否可枚举,都可以使用 Object.getOwnPropertyNames()方法

console.log(Object.getOwnPropertyNames(person.prototype));//["constructor", "name", "age", "job", "getName"]

结果中包含了不可枚举的 constructor 属性。 Object.keys() 和 Object.getOwnPropertyNames() 方法都可以用来替代 for-in 循环。

更简单的原型语法

对象字面量形式创建

    function person() {}
    person.prototype = {
        name :"zw",
        age : 20,
        job : "student",
        getName : function () {
            console.log(this.name);
        }
    };

这种情况下 constructor 属性不再指向 Person 了。前面曾经介绍过,每创建一个函数,就会同时创建它的 prototype 对象,这个对象也会自动获得 constructor 属性。在这里使用的语法,本质上完全重写了默认的 prototype 对象,因此 constructor 属性也就变成了新对象的 constructor 属性(指向 Object 构造函数),不再指向 Person 函数。

测试一下:

    var zw = new person();
    console.log(zw instanceof Object); //true
    console.log(zw instanceof person); //true
    console.log(zw.constructor == person); //false
    console.log(zw.constructor == Object); //true

3.原型的动态性

由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此。

    var zw = new person();
    person.prototype.weight="88kg";
    console.log(zw.weight);//88kg

先创建了zw对象,然后再在原型中添加值,之后zw依旧可以找到height这个属性,原因可以归结为实例与原型之间的松散连接关系。因为实例与原型之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到新的 weight 属性。

调用构造函数时会为实例添加一个指向最初原型的[[Prototype]] 指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型,而不指向构造函数。

    function person() {}
    var zw = new person();
    person.prototype = {
        constructor:person,
        name :"zw",
        age : 20,
        job : "student",
        getName : function () {
            console.log(this.name);
        }
    };
    console.log(zw.name);//undefined
    console.log(zw.getName());//error

修改了整个原型,所以现在zw找不到原来的原型了。重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系,它们引用的仍然是最初的原型。

4.原生对象的原型

所有原生引用类型( Object 、 Array 、 String ,等等)都在其构造函数的原型上定义了方法。通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。可以像修改自定义对象的原型一样修改原生对象的原型,因此可以随时添加方法。//不推荐,有些情况下会导致命名冲突

5.原生对象的问题

  • 所有实例在默认情况下都将取得相同的属性值
  • 共享导致实例不能拥有属于自己的全部属性的。

6.组合使用构造函数模式和原型模式

构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。

例如:

   function Person(name, age, job) {
        this.name = name;
        this.age = age;
        this.job = job;
        this.classmates=["xx","yy"];
    }
    Person.prototype = {
        getName: function () {
            console.log(this.name);
        }
    }

    var zw = new Person("zw", 20, "student");
    var zz = new Person("zz", 21, "student");
    console.log(zw.name==zz.name);//false
    console.log(zw.getName==zz.getName);//true

每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。

7.动态原型模式

    function Person(name, age, job) {
        this.name = name;
        this.age = age;
        this.job = job;
        this.classmates = ["xx", "yy"];
        if (typeof getName != "function") {
            Person.prototype.getName = function () {
                console.log(this.name);
            }
        }
    }

这里只在 getName() 方法不存在的情况下,才会将它添加到原型中。

!!!使用动态原型模式时,不能使用对象字面量重写原型。如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。下面这个例子zw找不到getName这个方法了,显示error

   function Person(name, age, job) {
        this.name = name;
        this.age = age;
        this.job = job;
        this.classmates = ["xx", "yy"];
        if (typeof getName != "function") {
            Person.prototype={//使用对象字面量初始化
                getName:function () {
                console.log(this.name);
            }
          }
         }
    }
    
    var zw = new Person("zw", 20, "student");
    var zz = new Person("zz", 21, "student");
    zw.getName();//error

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值