对象以及继承的总结(二)创建对象

223 阅读5分钟

什么是对象?

对象是一组属性的无序集合。每一个属性或者方法都是由一个名称标识来映射一个值。可以把对象想成一张散列表,内容就是一堆的名/值对。

对象的创建

通常,我们会使用Object构造函数或者对象的字面量可以很方便的创建对象,但是如果要创建具有相同接口的多个对象就需要重复编写很多代码。 以下列举了3个创建对象的方式

工厂模式

工厂模式就是利用一个函数来创建对象,如下所示:

function createObject(name,age,job) {
    let obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.job = job;
    obj.sayName = function() {
        console.log(this.name);
    }
    return obj;
}

上述函数利用函数返回值来创建一个对象,但是该方式创建的对象无法得知它的类型。

构造函数模式

类似于Object、Array等,这些都是原生的构造函数。我们可以自己自定义一个构造函数。

    function Person(name, age, job) {
      this.name = name;
      this.age = age;
      this.job = job;
      this.sayname = function () {
        console.log(this.name);
      };
    }
    let person1 = new Person("Nicholas", 29, "software Engineer");
    let person2 = new Person("Greg", 27, "Doctor");
    person1.sayname(); // nicholas
    person1.sayname(); // greg

与工厂模式相比,有以下区别:

  • 没有显示地创建对象
  • 属性和方法直接赋值给了this
  • 没有return

构造函数模式创建对象需要通过new操作符,实际上这种方式调用会执行下面操作:

  1. 在内存中创建一个新对象
  2. 在新对象的内部的[[Prototype]]特性被赋值为构造函数的prototype属性
  3. 构造函数内部的this指向新对象
  4. 执行构造函数内部的代码,给新对象添加属性
  5. 如果构造函数返回非空对象,则返回该对象;否则返回创建的新对象

当然,构造函数也存在一些问题。比如:构造函数内部定义的方法,会在每个实例上都创建一遍。

原型模式

每个函数都会有一个prototype属性,这个属性是一个对象,通过调用构造函数创建的对象的原型,包含共享的属性和方法。

function Person() {}
Person.prototype.name = "peopleName";
Person.prototype.age = 29;
Person.prototype.job = "peopleJob";
Person.prototype.sayName = function() {
    console.log(this.name);
};
let person1 = new Person();
person1.sayName(); // "peopleName"
let person2 = new Person();
person2.sayName(); // "peopleName"
console.log(person1.sayName == person2.sayName); // true

原型

只要创建一个函数,就会根据相关的规则为这个函数创建一个prototype属性(指向原型对象),所有的原型对象都会自动获得一个constructor的属性,指回关联的构造函数。

自定义构造函数中,原型对象默认只会有一个constructor属性,其他的所有方法都是继承自Object。创建了一个新实例时,这个新实例的内部有一个[[Prototype]]指针,会指向构造函数的原型对象,该指针无法直接访问,但是有一些游览器可以通过__proto__属性可以访问到对象的原型。

所以可以得出:实例和构造函数原型之间有直接的联系,实例与构造函数之间没有直接的联系。

至于可以通过实例访问到实例的构造函数,是因为实例能够访问到原型的属性,而原型中存在一个constructor属性指向构造函数。

  function People(name, age) {
    this.name = name;
    this.age = age;
  }
  People.prototype.say = function () {
    console.log("hello", this.name);
  };
  const p1 = new People("张三", 18); // p1 Person实例
  const p2 = new People("李四", 20); // p2 Person实例
  console.log(p1); // 如下所示,存在一个[[Prototype]]指向构造函数原型
  console.log(p1.__proto__ === People.prototype); // true

image.png

当我们访问实例的constructor时,并没有在实例对象的属性中发现constructor,因此,会通过原型链去进行查找,首先会查[[Prototype]]指向的构造函数原型,这是能够发现constructor,因此会将该属性值返回。

  console.log(p1.constructor); // 即指向People
  console.log(p1.constructor === People); // true

image.png

当我们把实例的[[Prototype]]展开时,能够发现原型上也有一个[[Prototype]]属性,这个属性指向Object构造函数的原型

image.png

    console.log(p1.__proto__.__proto__ === Object.prototype); // true

完整的原型链如下所示:

image.png

Object类型方法

isPrototypeOf():用于确定两个对象之间的原型关系。如下所示,p1的原型指向的是People.prototype

  console.log(People.prototype.isPrototypeOf(p1)); // true

getPrototypeOf():返回参数的内部特性[[prototype]]值。

Object.getPrototypeOf(p1) // 就是指向Person的原型

setPrototypeOf():向实例的私有特性[[Prototype]]写入一个新值(可能造成性能下降,可以通过Object.create()创建新对象,同时为其指定原型),可以重写一个对象的原型继承关系。

let biped = {
    numLegs: 2
};
let person = Object.create(biped);
person.name = 'Matt';
console.log(person.name); // Matt
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true

原型层级

在访问对象的属性时,会先访问这个对象实例上是否存在这个属性,如果不存在,会沿着指针进入原型对象,然后再原型对象上找到属性后再返回对应的值,类似于上述访问constructor属性。

如果在实例上和原型上有一个同名属性,则实例上的属性会遮蔽原型对象上的同名属性。可以使用delete操作符完全删除实例上的这个属性,从而可以继续搜索原型对象上的这个属性。 hasOwnProperty()用于确定某个属性是在实例上还是在原型上,在属性则返回true。

in则与上述不同,只要通过对象能够访问到某个属性时,都会返回true,无论是在原型还是实例上。

原型模式存在的问题,弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性最主要的问题是共享特性

原型上的所有属性都是在实例间共享的,如果原始值属性还好,但是如果是引用值可能就会存在问题。因为他们都指向了同一个内存区域的值,不同的实例上进行操作会同时对其他实例造成影响,因此实际上不会单独使用原型模式。