JS深入理解对象

1.JavaScript工厂模式   

    1、什么是工厂模式?

       工厂模式是一种众所周知的设计模式,广泛应用于软件工程领域,用于抽象创建特定对象的过程。工厂模式是一种创建型模式,简单来说,工厂模式就是创建对象的一种方式。 

    2、工厂模式有什么用?

        作用:创建对象;降低代码冗余度。

        应用场景:当你想要批量生产同种类的对象的时候;比如,你想生成一个班级的40个学生,每个学生都有姓名、年龄等特征。这时候你创建一个“工厂”,把信息丢到工厂里,工厂就给你造一个人出来,非常方便。

    3、为什么用工厂模式?

        从工厂模式的作用出发来看,工厂模式的主要作用就是用来产生对象的。那么别的创建对象的模式有什么缺点?

         a.用字面量的方式创建对象

  • 字面量就是用来描述变量的;一般来说,给变量赋值的时候,等号右边的都可以看作是字面量(因为等号右边的都是用来描述这个变量的,比如描述一个变量为字符串(字符串字面量)、一个数组(数组字面量)、一个对象(对象字面量),等等)。
  •     const obj={
          name:"芭比Q啦",
          age:18,
          sayName:function(){
            console.log(this.name);
          }
        }
        obj.sayName()//芭比Q啦

    缺点:这种方式可以创建一个对象,那么缺点也是显而易见的:创建具有同样接口的多个对象需要编写很多重复的代码。

         b.new Object()创建对象

  • Object是JavaScript提供的构造函数;new Object()就是利用JavaScript提供的构造函数实例化了一个对象;
  • var person = new Object();
    //为这个实例化的对象添加属性
    person.name = "zhangsan";
    person.age = 18;
    person.gender = 'male';
    person.sayName = function(){
        console.log(this.name)
    }

    缺点:可以发现它是先实例化了一个对象,然后再为对象添加属性,这样就看不出来是个整体(像上面的用字面量来创建,属性都包在一个大括号里面,这样就很好看出这是个整体)。

    因此,我们为了使创建对象更加方便(不像字面量创建那样一次性),也为了写的代码更像个整体,就可以交给工厂模式来做  。

    4、使用工厂模式创建对象

      function Person(name, age) {
        const obj = new Object();

        obj.name = name;
        obj.age = age;
        obj.sayName = function () {
          console.log(this.name);
        };
        return obj;
      }

      const person1 = Person("靓仔", 21);
      person1.sayName(); //靓仔

      const person2 = Person("芭比Q啦", 20);
      person2.sayName(); //芭比Q啦

优点:只要我们往工厂函数里面塞参数,工厂函数就会像生产产品一样造个人出来。

缺点:但是这个模式存在一个问题 -->不知道创建的新对象是什么类型。

//我们需要的是打印Person
console.log(typeof person1);//object

所以,我们可以使用自定义构造函数模式。

2.构造函数模式

ECMAScript 中的构造函数是用于创建特定类型对象的。像 Object 和 Array 这样的原生构造函数,运行时可以直接在执行环境中使用。当然也可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法。

JavaScript中可以自定义构造函数,从而自定义对象类型的属性和方法,构造函数本身也是函数,只不过可以用来创建对象。

自定义构造函数:

        前面提过,构造函数是用于创建特定类型对象的,当然我们也可以自己自定义构造函数来创建自定义类型的对象

         比如,上面的例子可以这样写:

      function Person(name, age) {
        this.name = name;
        this.age = age;
        this.sayName = function () {
          console.log(this.name);
        };
      }

      const person1 = new Person("欢迎靓仔", 20);
      const person2 = new Person("欢迎靓女", 18);

      person1.sayName();
      person2.sayName();

在这个例子中,构造函数于之前的工厂模式的区别主要在于下面三点:

  1. 没有显示的创建对象
  2. 属性和方法直接赋值给了this
  3. 没有return

那么看到这里,您应该会存在一个疑问:

使用new创建实例的过程经历了什么?

要创建Person类的实例,应该使用new操作符

  1. 创建一个对象
  2. 将对象的[[prototype]]赋值构造函数prototype属性
  3. 将this指向新创建的对象
  4. 执行构造函数中的代码,给新对象添加属性
  5. 如果构造函数返回了其他对象,那么这个对象作废,否则返回新创建的这个对象。

下面来简单实现一下new:

      function NewPerson(name, age) {
        this.name = name;
        this.age = age;
      }

      function TestNew(name, age) {
        const obj = Object.create(NewPerson.prototype); //Object.create的原理
        //将构造函数中的this指向obj并调用构造函数进行赋值
        NewPerson.apply(obj, [name, age]);
        return obj;
      }

      const newPerson1 = TestNew("靓仔", 18);
      console.log(newPerson1.__proto__ === NewPerson.prototype); //true
      console.log(newPerson1.name);

 NewPerson.apply(obj, [name, age]);这一句代码相当于,这样就给obj添加上了属性。

 function NewPerson(name, age) {
     obj.name = name;
     obj.age = age;
 }

 NewPerson(name,age)

构造函数可以确定对象的类型,相比于工厂模式会有一个很大的提升。但是构造函数也有缺点 ,如果要创建多个实例,每一个方法都要重新创建一遍。

      function Constr(name) {
        this.name = name;
        this.sayName = function () {
          console.log(this.name);
        };
      }
      const conStr1 = new Constr("mmm");
      const conStr2 = new Constr("靓仔");
      conStr1.sayName(); //mmm
      conStr2.sayName(); //靓仔
      console.log(conStr1.sayName === conStr2.sayName); //false

      //方法都是相同的,那么每次去创建就会造成内存浪费

这里conStr1.sayName === conStr2.sayName返回false说明两个实例的sayName方法是不同的。

都是做一样的事,所以没必要定义两个不同的 Function 实例。况且,this 对象可以把函数与对象的绑定推迟到运行时。

要解决这个问题,可以把函数定义转移到构造函数外部:

      function Constr1(name) {
        this.name = name;
        this.sayName = sayName;
      }
      function sayName() {
        console.log(this.name);
      }
      const conStr3 = new Constr1("靓仔");
      const conStr4 = new Constr1("嘻嘻嘻");
      conStr3.sayName(); //靓仔
      conStr4.sayName(); //嘻嘻嘻
      console.log(conStr3.sayName===conStr4.sayName);//true

这样虽然解决了问题,但是又出现了一个新的问题:函数实际上只能在一个对象上调用,如果这个对象需要多个方法,那么就需要在全局作用域中定义多个函数,这会导致自定义类型的代码不能很好的凝聚在一起。这个问题可以通过原型模式来解决。

3.原型模式

每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。原来在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型。

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

      Person.prototype.sayName = function () {
        console.log(this.name);
      };
      Person.prototype.sayAge = function () {
        console.log(this.age);
      };

      const person1 = new Person("靓仔?", 21);
      const person2 = new Person("笑嘻嘻", 21);

      person1.sayName(); //靓仔
      person2.sayName(); //笑嘻嘻

      console.log(person1.sayName === person2.sayName); //true

看到这里,肯定又有一些疑问了


为什么实例能访问到sayName属性呢

每次创建一个新实例的时候,我们在文章之前也提到过将对象的[[prototype]]赋值构造函数prototype属性,而我们当时使用的是Object.create(),这个方法的作用是创建一个对象,将参数对象的prototype赋值给这个对象,然后返回这个对象的[[prototype]]属性,请看下面这个例子:

      function Person() {}
      Person.prototype.say = function () {
        console.log("Person");
      };

      const obj2 = Object.create(Person.prototype);

      console.log(obj2);

      console.log(obj2.__proto__ === Person.prototype);//true

实例内部的[[prottype]]是不可以直接访问的,但是Firefox,Chrome,Safari会在每个对象上暴露__proto__属性通过这个属性可以访问对象的原型

这里我们打印了一下obj对象和Person.prototype,我们会发现,obj.__proto__和Person.prototype是相等的!这就是Object.create()的作用。

image.png

person1.sayName === person2.sayName为啥是true?

因为sayName方法定义在Person的原型上,两个实例并没有自己的sayName方法,都是引用这一个共享的sayName方法,所以在===的时候会返回true啦

原型的问题

原型模式也不是没有问题。首先,它弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值。虽然这会带来不便,但还不是原型的最大问题。原型的最主要问题源自它的共享特性。

我们知道,原型上的所有属性是在实例间共享的,这对函数来说比较合适。另外包含原始值的属性也还好,如前面案例中所示,可以通过在实例上添加同名属性来简单地遮蔽原型上的属性。真正的问题来自包含引用值的属性。来看下面的案例:

function Person() { }
Person.prototype = {
  constructor: Person,
  name: "zhangsan",
  friends: ["lisi", "wangwu"],
  sayName() {
    console.log(this.name);
  }
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("zhaoliu");
console.log(person1.friends); // [ 'lisi', 'wangwu', 'zhaoliu' ]
console.log(person2.friends); // [ 'lisi', 'wangwu', 'zhaoliu' ]
console.log(person1.friends === person2.friends); // true

这里,Person.prototype 有一个名为 friends 的属性,它包含一个字符串数组。然后这里创建了两个Person 的实例。person1.friends 通过 push 方法向数组中添加了一个字符串。由于这个friends 属性存在于 Person.prototype 而非 person1 上新加的这个字符串也会在(指向同一个数组的)person2.friends 反映出来。如果这是有意在多个实例间共享数组,那没什么问题。但一般来说,不同的实例应该有属于自己的属性副本。这就是实际开发中通常不单独使用原型模式的原因。

4.组合模式

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

构造函数用于定义实例属性,原型模式用于定义方法和共享属性。这种模式是目前在ECMAScript中使用最广泛,认同度最高的一种创建自定义类型的方法。

function Person(name, age, gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;
  this.firends = ['zhangsan', 'lisi'];
}
Person.prototype = {
  constructor: Person,
  sayName: function () {
    console.log(this.name);
  }
};
var p1 = new Person('larry', 44, 'male');
var p2 = new Person('terry', 39, 'male');

p1.firends.push('robin');
console.log(p1.firends); // [ 'zhangsan', 'lisi', 'robin' ]
console.log(p2.firends); // [ 'zhangsan', 'lisi' ]
console.log(p1.firends === p2.firends); // false
console.log(p1.sayName === p2.sayName); // true

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值