面向对象-对象的创建方式

188 阅读7分钟

在程序中所有的对象都被分成了两个部分:数据和功能。以人为例,人的姓名、性别、年龄、身高、体重等属于数据,人可以说话、走路、吃饭、睡觉这些属于人的功能。数据在对象中被称为属性,而功能就被称为方法

对象的创建方法及优缺点

  1. 字面量创建对象

    var box = {
      name:'张三',
      run:function () {
        console.log(this.name + '在跑')
      }
    }
    box.run();
    

    缺点:无法复用

  2. 内置构造函数创建对象

    var p1 = new Object()
    p1.name = '张三'
    p1.run = function () {
      console.log(this.name+'在跑路')
    }
    p1.run()
    

    缺点:无法知道创建对象的类型,人类?狗类?虽然 Object 构造函数或对象字面量都可以用来创建单个对象,但这些方式有一个明显的缺点:使用同一个接口创建很多对象会产生大量的重复代码。为解决这个问题,人们开始使用工厂模式的一种变体。

    原文链接 juejin.cn/post/684490…

  3. 工厂模式
    可以无数次地调用这个函数,而每次它都会返回包含属性或方法的对象。解决了创建多个相似对象的问题。如同构造函数创建对象,只不过工厂模式显式的创建了一个对象并显式返回了它。

    function createObject(name,age) {
      var obj = new Objecct() // 创建对象
      obj.name = name // 添加属性
      obj.age = age
      obj.run = function() { // 添加方法
        return obj.name + obj.age + '运行中'
      }
      return obj // 返回对象引用
    }
    
    var person1 = createObject('lxx',26)
    var person2 = createObject('xxx',27)
    

    缺点:没有解决对象识别问题,无法知道创建的对象的类型,都是 Objecct 类型。

    // 对象识别问题
    person1 instanceof createObject // false
    person2 instanceof Object // true
    
  4. 自定义构造函数模式
    通过 new 关键字去调用一个函数,这个函数就是一个构造函数。

    function Dog(option) {
      this.name = option.name
      this.age = option.age
      // 意味着每个实例有自己的函数对象
      this.run = function () {
        console.log(this.name + this.age + '在跑步')
      }
    }
    
    var xg = new Dog({name:'小狗',age:25})
    xg.run()
    
    var dg = new Dog({name:'大狗',age:24})
    dg.run()
    

    下面比较两个实例中的方法是否是同一个方法,结果是 false ,因为是引用类型,比较的是引用地址,是不可能相等的。

    xg.run === dg.run // false
    

    缺点:虽然方法都一样,但因为是各自创建的方法,所以不相等,造成内存浪费。 👎 方法共享:通过构造函数外面绑定同一个函数的办法,来保证引用地址的一致性。

    function Dog(option) {
      this.name = option.name;
      this.age = option.age;
      this.run = run;
    }
    
     // 全局方法
    function run() {
      console.log(this.name + this.age + '在跑步');
    }
    

    但是这种做法没有必要且违背封装原则,容易被恶意调用。虽然使用了全局的函数 run 来解决了保证引用地址的一致性问题,但带来了新的问题,全局中的 this 在对象调用时是 Dog 本身,而作为普通函数调用时,this 又代表了 window

  5. 组合构造函数+原型模式 解决了构造函数传参和引用类型值共享问题,是创建对象比较好的方法。
    构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性。每个实例都有自己的一份实例属性副本,但同时又共享着对方法的引用,最大限度的节省内存。另外,这种混合模式还支持向构造函数传递参数,可谓是集两种模式之长。

    function Box(name,age) { // 不共享的属性放到构造函数中
      this.name = name
      this.age = age
      this.family = ['哥哥','姐姐','弟弟']
    }
    Box.prototype = { // 共享的方法放到原型中
      constructor:Box,
      run:function() {
        return this.name + this.age + this.family
      }
    }
    

    缺点:1.原型模式不管是否调用了原型中的共享方法,它都会初始化原型中的方法;2.原型被重写,导致 constructor 指向变了。

  6. 动态原型模式 原型中的方法只会在第一次调用时初始化,并且把构造函数和原型封装到一起,保持了同时使用构造函数和原型的优点。下面代码只会在 run 方法不存在的情况下才会执行,避免了每次都创建原型方法。

    function Box(name,age) {
      this.name = name;
      this.age = age;
    
      if(typeof this.run != 'function') {
        Box.prototype.run = function() {
          ...
        }
      }
    }
    

    💣 使用动态原型模式时,不能使用对象字面量重写原型。如果在已经创建了实例的情况下重写原型,就会切断现有实例与新原型之间的联系!

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

    function Person(name,age,height){
      var o = new Object()
      o.name = name
      o.age = age
      o.height = height
      o.friends = ['aaa','bbb']
      o.sayName = function (){
        console.log(this.name)
      }
      return o
    }
    var person1 = new Person('lxx',25,1.88)
    person1.sayName() // lxx
    

    💣 在可以使用其他模式的情况下,不要使用这种模式。

  8. 稳妥构造函数模式 新创建对象的实例方法不引用 this,不使用 new 操作符调用构造函数。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用 thisnew),或者在防止数据被其他应用程序改动时使用。

    function Person(name, age, job) {
      var o = new Object();
    
      // 私有成员
      var nameUC = name.toUpperCase();
    
      // 公有成员
      o.sayName = function() {
        alert(name);
      };
      o.sayNameUC = function() {
        alert(nameUC);
      };
    
      return o;
    }
    
    var person = Person("Nicholas", 32, "software Engineer");
    
    person.sayName(); // "Nicholas"
    person.sayNameUC(); // "NICHOLAS"
    
    alert(person.name);  // undefined
    alert(person.nameUC);  // undefined
    

优化后的自定义构造函数+原型创建模式

// 创建构造函数
function Dog(option) {
  // 实例属性
  this.name = option.name
  this.age = option.age
  this.dogFriends = option.dogFriends

  // 原型方法
  Dog.prototype.eat = function(someThing) {
    console.log(this.name + '在吃' + someThing)
  }
  Dog.prototype.run = function(someThing) {
    console.log(this.name + '在' + someThing + '跑步')
  }
}

// 实例化
var smallDog = new Dog({name:'小花',age:10})
smallDog.eat('奶') // 小花在吃奶
console.log(smallDog.name,smallDog.age) // 小花 10

var bigDog = new Dog({name:'大花',age:10,dogFriends:['托马斯','柯基']})
console.log(bigDog.name) // 大花
console.log(bigDog.name+'的朋友是:'+bigDog.dogFriends) // 大花的朋友是:托马斯,柯基
bigDog.run('中山公园') // 大花在中山公园跑步

bigDog.run === smallDog.run // true  

把方法写在原型中比写在构造函数中消耗的内存更小,因为在内存中一个类的原型只有一个,写在原型中的行为可以被所有实例共享,实例化的时候并不会在实例中复制一份。而写在类中的方法,实例化的时候会在每一个实例对象中再复制一份,所以消耗的内存更高。因此没有特殊原因,一般把属性定义在类中,方法定义在原型中。
在构造函数中定义的属性和方法要比原型中定义的属性和方法优先级高,如果定义了同名称的属性和方法,构造函数中的将会覆盖原型中的。这是作用域链的原因,就近原则会优先查找构造函数中的属性和方法。

new 之后发生了什么

函数也是一个对象,它也具有 [[prototype]] (隐式原型)。因为构造函数是一个函数,所以它还多出一个显式原型属性:prototype

function Person(name,age,sex){
  // 1、在内存中自动隐式创建一个空对象
  obj = new Object()

  // 2、该对象内部的 [[prototype]] 属性会被赋值为该构造函数的 prototype
  // p.__proto__ = Person.prototype 隐式原型对象被赋值为构造函数的显式原型对象

  // 3、构造函数内部的this,会指向创建出来的新对象
  this = obj
	
  // 4、执行构造函数的内部代码(函数体代码)
  // 给空对象添加属性、方法
  this.name = name
  this.run = function() {}

  // 5、默认返回this,指向该空对象obj
  return this
}

var p = new Person()

p.__proto__Person.prototype 两属性指向内存空间中的同一个对象。

自定义构造函数中的 return

  1. 如果不写 return 或显式返回 this 亦或显式返回一个原始值,则返回默认创建的新对象
  2. 如果显式返回一个引用类型值,则直接将该引用类型值返回给外界
function Person(name,age) {
  this.name = name
  this.age = age
  return []
}

new Person('lxx',25) // []