三种创建对象的模式深入理解对象

三种创建对象的模式深入理解对象

虽然使用Object构造函数或字面量形式可以很方便的创建对线,但存在着明显的不足:

创建具有同样接口的多个对象需要重复编写很多代码

a.用字面量创建对象
var person = {
  name: "zhangsan",
  age: 18,
  gender: 'male',
  sayName: function(){
      console.log(this.name);
  }
}

缺点:用字面量的方式来创建对象,最大的缺点就是,这个对象是一次性的,如果有四十个同学,这个代码就要写四十次,有点小麻烦。

b.用构造函数的形式创建对象
var person = new Object();
//为这个实例化的对象添加属性
person.name = "zhangsan";
person.age = 18;
person.gender = 'male';
person.sayName = function(){
    console.log(this.name)
}

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

1.javaScript工厂模式

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

//将创建对象的代码封装在一个函数中
function createPerson(name, age, gender) {
  var person = new Object();
  person.name = name;
  person.age = age;
  person.gender = gender;
  person.sayName = function () {
    console.log(this.name);
  }
  return person;
}
//利用工厂函数来创建对象
var person1 = createPerson("zhangsan", 18, 'male');
var person2 = createPerson("lisi", 20, 'female');
  • 工厂模式有什么用?

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

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

  • 工厂模式的优缺点

    优点:只要我们往工厂函数里面塞参数,工厂函数就会像生产产品一样造个人出来。
    缺点:这种方式本质上是将创建对象的过程进行了封装,本质并没有改变,我们创建一个student时无法知道其具体的数据类型,只知道这是一个对象,往往实际开发中我们需要确定这个对象到底是个Person的实例还是Dog的实例。

2.构造函数模式

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

// 自定义构造函数
function Person(name, age, gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;
  this.sayName = function () {
    console.log(this.name);
  }
}

var person1 = new Person('zhangsan', 29, 'male');
var person2 = new Person('lisi', 19, 'female');

person1.sayName(); // zhangsan
person2.sayName(); // lisi

在这个案例中,Person()构造函数代替了 createPerson()工厂函数。实际上,Person()内部的代码跟 createPerson()基本是一样的,只是有如下区别。

  1. 没有显示地创建对象

  2. 属性和方法直接赋值给了this

  3. 没有return

    另外,要注意函数名 Person 的首字母大写了。按照惯例,构造函数名称的首字母都是要大写的,非构造函数则以小写字母开头。

  • 创建Person实例

    var person1 = new Person('zhangsan', 29, 'male');
    var person2 = new Person('lisi', 19, 'female');
    

    创建Person的实例时,应使用new操作符。以这种方式调用构造函数会有如下特性。

    (1)在内存中创建一个对象

    (2)这个对象内部的__proto__特性被赋值为构造函数的prototype属性

    (3)构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)

    (4)执行构造函数内部的代码(给新对象添加属性)。

    (5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

    person1 和 person2 分别保存着 Person 的不同实例。所有对象都会从它的原型上继承一个 constructor 属性,这两个对象的constructor 属性指向 Person,

    console.log(person1.constructor === Person); // true 
    console.log(person2.constructor === Person); // true
    
  • 构造函数也是一个函数

    var Person = function(name,age,gender){
    	this.name = name
        this.age = age
        this.gender = gender
        this.sayName = function(){
            console.log(this.name)
        }
    }
    // 作为构造函数
    var person = new Person("jacky",29,'male')
    person,sayName() //jacky
    //作为函数使用
    Person('lisi',27,'female')
    global.sayName() //lisi
    // 在另一个对象的作用域中调用
    var o = new Object()
    Person.call(o,'wangwu',25,'male')
    o.sayName() //wangwu
    

    ​ 这个案例展示了,当我们对构造函数使用new操作符,即创建了一个对象,发挥了构造函数的作用。

    ​ 当我们没有使用new操作符调用Person(),结果会将属性和方法添加到全局对象。这里,在调用一个函数但没有明确设置它的this指向(即没有作为对象的方法调用,或者没有使用call()、apply()、bind()),this的指向始终为global对象(在浏览器中就是window对象)。因此当我们调用了函数后,global上面就会有一个sayName()方法,调用返回传入的name属性为‘lisi’。

    ​ 最后我们通过call()方法,将对象o的this指向指定为Person()内部的this值,所有属性和sayName()方法又将添加到对象o上。

  • 构造函数的问题

    function Person(name, age, gender) {
      this.name = name;
      this.age = age;
      this.gender = gender;
      this.sayName = new Function("console.log(this.name)"); // 逻辑等价
    }
    var person1 = new Person()
    var person2 = new Person()
    console.log(person1.sayName === person2.sayName); // false
    

    从上面案例可以看到,虽然两个实例同名却不相等,是因为每个实例创建方法时,都会创建一个自己实例的方法,以这种方式创建函数会带来不同的作用域链和标识符解析。但是都是做一样的事,所以没必要定义两个不同的 Function 实例。况且,this 对象可以把函数与对象的绑定推迟到运行时。

3.原型模式

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

function Person(){}
Person.prototype.name = 'zhangsan'
Person.prototype.age = 29
Person.prototype.gender = 'male'
Person.prototype.sayName = function(){
  console.log(this.name);
}
// 创建Person实例
var person1 = new Person()
person1.sayName() //zhangsan
var person2 = new Person()
person2.sayName() //zhangsan
console.log(person1.sayName === person2.sayName); //true

上述代码中所有属性和sayName()方法都添加至Person的prototype属性中。这种模式下定义的属性和方法都是Person实例所共享的,因此person1和person2都有相同的属性和相同的sayName方法

更简单的原型模式

在上面案例中,每次定义一个方法都要写一遍Person.prototype。为减少代码冗余,也为从视觉上更好的体现封装原型的功能,直接通过一个包含所有属性和方法的对象字面量来重写原型。如下

function Person(){}
// 使用字面量形式封装原型
Person.prototype ={
  name:'zhangsan',
  age:'20',
  gender: 'male',
  sayName:function(){
    console.log(this.name);
  }
}
// 在这个案例中,Person.prototype被设置等于一个通过字面量创建的新对象。最终结果是一样的,只有一个问题:这样重写之后,Person.prototype 的constructor 属性就不指向Person了。在创建函数时,也会创建它的prototype对象,同时会自动给这个原型的constructor属性赋值。而上面的写法完全重写了默认的prototype对象,因此其constructor属性也指向了完全不同的新对象(Object构造函数),不再指向原来的构造函数。
var person1 = new Person()
console.log(person1.constructor === Person); //false
console.log(person1.constructor === Object); //true
解决这个问题?

可以在重写原型对象时,专门设置constructor的值;

但是这种方法会创建一个[[Enumerable]]为true的属性。而原生constructor属性默认是不可枚举的。

function Person(){}
Person.prototype ={
  // 这种方法恢复constructor属性会创建一个[[Enumerable]]为true的属性
  constructor:Person,
  name:'zhangsan',
  age:'29',
  gender:'male',
  sayName(){
    console.log(this.name);
  }
}
var person1 = new Person()
console.log(person1.constructor == Person);//true
console.log(person1.constructor == Object);//false
  • 原型模式的问题

    原型模式的问题在于弱化了构造传递初始化参数的能力,会导致所有实例默认都取得相同的属性值。再就是所有属性在实例之间是共享的,对于方法而言比较合适,另外对于包含原始值的属性影响也不大,毕竟可以通过在实例上添加同属性名来简单地遮蔽原型上的属性。真正的问题在于引用值的属性。

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

    上述代码中,创建两个Person的实例。person1.friend通过push方法向数组中添加了一个字符串,由于这个friend属性存在于Person.prototype上,新加入的字符串也会在实例person2.friend中显示出来。但一般来说,不同的实例应该有属于自己的属性副本。这也是实际开发中不单独使用原型模式的原因。

    4.组合模式

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

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

    // 组合模式
    function Person(name,age,gender){
      this.name = name
      this.age = age
      this.gender = gender
      this.friends = ['zhangsan','lisi']
    }
    Person.prototype ={
      constructor:Person,
      sayName:function(){
        console.log(this.name);
      }
    }
    var p1 = new Person('larry',44,'male')
    var p2 = new Person('tarry',39,'female')
    
    p1.friends.push('robinson')
    console.log(p1.friends);//[ 'zhangsan', 'lisi', 'robinson' ]
    console.log(p2.friends);//[ 'zhangsan', 'lisi' ]
    console.log(p1.friends === p2.friends); // 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、付费专栏及课程。

余额充值