对象的创建

150 阅读9分钟

引言:对象就是一系列属性的集合,一个属性包含一个名和一个值(键值对)。

一、单个对象的创建

!!!不适合创建大量相似的对象,代码复用率低,且会占据大量的内存,性能低

1、Object() 构造函数

2、对象字面量

示例:

// Object() 构造函数创建一个对象
let obj = new Object({
  name: 'tom',
  age: 18,
  sayName() {
    console.log(this.name);
  },
});

// 对象字面量创建一个对象
let obj2 = {
  name: 'jack',
  age: 19,
  sayName() {
    console.log(this.name);
  },
};

二、可复用的对象的创建

1、工厂模式

1.实现步骤如下:

  • 用函数来封装创建对象的步骤
  • 通过调用函数创建对象
/ 利用函数封装创建对象的步骤
function createPerson(name, age) {
  // 用构造函数创建对象person类
  let person = new Object({
    name,
    age,
    sayName() {
      console.log(this.name); // 谁调用的,this指向的就是那个对象
    },
  });
  return person;
}

function createDog(name, age) {
  // 用对象字面量创建对象dog类
  let dog = {
    name,
    age,
    sayHello() {
      console.log('汪汪~~~~~');
    },
  };
  return dog;
}

// 通过调用函数来创建对象,达到复用
let p1 = createPerson('tom', 18);
let p2 = createPerson('jack', 19);

let d1 = createDog('小黑', 3);
let d2 = createDog('小白', 5);

2.问题或者缺陷

  • 创建出来的对象无法和某个类型联系起来,只是简单的封装了可复用代码,但没有建立起对象和类型间的关系,即无法直观的区分创建出来的对象是Person类型的还是Dog类型的
console.log(p1); // { name: 'tom', age: 18, sayName: [Function: sayName] }
console.log(p2); // { name: 'jack', age: 19, sayName: [Function: sayName] }
console.log(d1); // { name: '小黑', age: 3, sayHello: [Function: sayHello] }
console.log(d2); // { name: '小白', age: 5, sayHello: [Function: sayHello] }

2、构造函数模式。

js 中的函数都可以作为构造函数,只要是通过 new 来调用的函数,就可以把它称为构造函数。

执行构造函数

  • 首先会创建一个对象
  • 然后将对象的原型指向构造函数的 prototype 属性
  • 然后将执行上下文中的 this 指向这个对象
  • 最后再执行整个函数,如果返回值不是对象,则返回新建的对象。
  • 因为 this 的值指向了新建的对象,因此我们可以使用 this 给对象赋值
  • 构造函数模式的优点是,所创建的对象和构造函数建立起了联系,可以通过原型来识别对象的类型。
  • 构造函数存在一个缺点就是,造成了不必要的函数对象的创建(工厂函数也有这个问题)
    • 因为函数也是一个对象,对象属性值是函数,每次new的时候都会新建一个函数对象,浪费了不必要的内存空间,因为函数是所有的实例都可以通用的。
// 构造函数模式
// 创建Person的构造函数
function Person(name, age) {
// this指向新创建的实例对象,可以用this给实例赋值
  this.name = name;
  this.age = age;
  // 这个函数在每次new的时候都会重新创建,即使它是一个通用的函数
  this.sayName = function () {
    console.log(this.name);
  };
}
// 创建Dog的构造函数
function Dog(name, age) {
  this.name = name;
  this.age = age;
  this.sayHello = function () {
    console.log('汪汪~~~~~');
  };
}
let p3 = new Person('lucy', 20);
let p4 = new Person('wendy', 20);
let d3 = new Dog('小灰', 6);
let d4 = new Dog('小黄', 7);
// 可以通过原型来识别对象的类型
console.log(p3); // Person { name: 'lucy', age: 20, sayName: [Function (anonymous)] }
console.log(p4); // Person { name: 'wendy', age: 20, sayName: [Function (anonymous)] }
console.log(d3); // Dog { name: '小灰', age: 6, sayHello: [Function (anonymous)] }
console.log(d4); // Dog { name: '小黄', age: 7, sayHello: [Function (anonymous)] }

3、原型模式

因为每一个函数都有一个 prototype 属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法。因此我们可以使用原型对象来添加公用属性和方法,从而实现代码的复用。

这种方式相对于构造函数模式来说,解决了函数对象的复用问题。但是这种模式也存在一些问题

  • 一是所有的值一开始就都确定好了,没有办法通过传入参数来初始化值(原型上无法使用this指向实例)
  • 二是如果属性值为对象,那么所有的实例将共享这个对象,任何实例上对该对象的改变都会影响到所有的实例

!!! 注意:给原型赋值的时候可以使用对象字面量的形式,但是使用字面量相当于重写原型,也就是改变了原型的地址值,可能会带来一些问题

// 原型模式
function Person2() {
  // 在构造函数内部不做任何初始化的操作,实例的属性都来自于原型
}
// 所有初始化的操作都放到原型上
Person2.prototype.name = 'bob';
Person2.prototype.age = 18;
Person2.prototype.sayHello = function () {
  console.log('你好呀');
};
Person2.prototype.children = { name: '张三', age: 3 };

let p5 = new Person2();
// 构造函数未做初始化操作,实例上无任何属性,所以也无法传入参数来初始化
console.log(p5); // Person2 {}  
// 实例上没有属性,沿原型链寻找,找到可以输出
console.log(p5.name); // bob
// 所有实例共享一个函数,解决了仅使用构造函数时创建多个函数方法的问题
p5.sayHello(); // 你好呀

// 属性值为对象时,一个实例做出的改变会影响到所有实例
console.log(p5.children.name); // 张三 未修改前
let p6 = new Person2();
p6.children.name = '李四';// 进行修改操作
console.log(p5.children.name); // 李四
console.log(p6.children.name); // 李四

4、构造函数+原型模式,最常用的。

  • 用构造函数来初始化对象的属性(每个实例自己所独有的属性)
  • 用原型对象来共享属性或者方法(每个实例都需要且一样的属性或方法)
  • 问题:对于代码的封装性不够好,因为往原型上添加属性和方法是在构造函数的外面进行的
// 构造函数 + 原型 模式
function Person3(name, age) {
  // 初始化实例独有的属性(用this)
  this.name = name;
  this.age = age;
}
// 给原型添加共享的方法
Person3.prototype.sayHello = function () {
  console.log('你好呀');
};
let p7 = new Person3('tony', 28);
console.log(p7); // Person3 { name: 'tony', age: 28 }
p7.sayHello(); // 你好呀

5、动态原型模式

将给原型赋值的过程移动到构造函数的内部,通过使用if判断是否为第一次执行构造函数,即需要添加到原型上的方法是否存在,来决定是否执行添加的代码,可以实现仅在第一次调用函数时对原型对象赋值一次的效果。这一种方式很好地对上面的混合模式进行了封装。

!!! 不要在构造函数内部使用对象字面量的形式为原型添加属性和方法

// 动态原型模式
function Person4(name, age) {
  this.name = name;
  this.age = age;
  // 使用if判断是否为第一次执行构造函数,即需要添加到原型上的方法是否存在
  if (typeof this.sayName !== 'function') {
    Person4.prototype.sayName = function () {
      console.log(this.name);
    };

    // 。。。其他需要添加的属性或者方法
  }
}
let p8 = new Person4('jack', 28);
p8.sayName();

6、寄生构造函数模式(和 es6中的Class的继承相似)

  • 在一个函数类型内部调用另外一个函数类型,获得这个函数类型的所有属性和方法后,再进行额外的初始化操作,以达到不用修改构造函数,但却能扩展对象的目的
  • 使用Object.create()方式重写扩展后的构造函数(Student)的原型为需要被扩展的构造函数(Person)的原型,以获得Person原型上的共享的属性和方法
// 寄生构造函数模式
// 创建Person类型
function Person(name, age) {
  this.name = name;
  this.age = age;
  if (typeof this.sayName !== 'function') {
    Person.prototype.sayName = function () {
      console.log(this.name);
    };
  }
}

// 在不修改Person类型的基础上扩展为Student类型
function Student(name, age, grade) {
  // 继承Person实例上的应有属性和方法
  Person.call(this, name, age);
  // 需要扩展的属性和方法
  this.grade = grade;

  // 扩展新的共享属性和方法到原型上
  if (typeof this.study !== 'function') {
    Student.prototype.study = function () {
      console.log(`我读${grade}年级了`);
    };
  }
}
// 以Person类型的原型为基础重写Student类型的原型,以获取原型上的属性和方法
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;

let s1 = new Student('lucy', 12, 6);
console.log(s1); // Student { name: 'lucy', age: 12, grade: 6 }
console.log(s1.name); // lucy
console.log(s1.age); // 12
console.log(s1.grade); // 6
s1.sayName(); // lucy
s1.study(); // 我读6年级了

7、对象创建的进阶 es6中的Class

1.如何使用class关键字声明一个类

// 方式一 
class Person {};

// 方式二
const Person2 = class {};

2.类的构造函数 constructor

每个类都会有自己的构造函数,使用 new 调用一个类的时候,这个类就会调用自己的构造函数,即constructor

  • 作用:创建对象的时候给类传递参数
  • !!!注意:每个类只能有一个构造器
  • 类中的this指向实例对象
  • 在类中使用 函数名(){} 形式定义的方法都会被定义到原型上,例如构造器 constructor(){}
  • 直接定义在构造器内属性和方法,即定义在this上的属性和方法,都属于实例的属性和方法(推荐)
  • 类的本身和类的构造函数是相等的,所以在类中以变量名 = 值 的形式定义的属性和方法也都属于实例的属性和方法(不推荐使用)
  • !!! 实例自身的属性和方法都应该放到构造器constructor中去

3.类的继承

  • extends关键字用于扩展子类,创建一个类作为另外一个类的一个子类。
  • 它会将父类中的属性和方法一起继承到子类的,减少子类中重复的业务代码。
  • 子类的原型的__proto__ 属性所指向的原型对象就是父类的原型对象
class Person {
  // 类的构造方法  用于接收参数

  // 实例自身的属性都应该放到构造器中去
  constructor(name, age) {
    // 在构造器内的属性和方法属于实例的属性和方法
    // 实例的属性
    this.name = name;
    this.age = age;
    // 实例的方法
    this.sayAge = function () {
      console.log(this.age);
    };
  }
  // 定义到实例的原型上的方法
  sayHello() {
    console.log(this);
  }

  // 在类中以 变量名 = 值  的形式定义的属性和方法也都属于实例的属性和方法
  sex = '男';
  sayName = () => {
    console.log(this.name);
  };
}

let p = new Person();

// 在类中以 函数名(){}  形式定义的函数,都属于原型的方法
console.log(p.hasOwnProperty('constructor')); // false
console.log(p.hasOwnProperty('sayHello')); // false

// 在构造器中定义的属性和方法,都属于实例的属性和方法
console.log(p.hasOwnProperty('name')); // true
console.log(p.hasOwnProperty('sayAge')); // true

// 直接在类中以  变量名 = 值  的形式定义的属性和方法,都属于实例的属性和方法
console.log(p.hasOwnProperty('sex')); // true
console.log(p.hasOwnProperty('sayName')); // true

// 类的本身和类的构造函数是相等的
console.log(Person === Person.prototype.constructor); // true

// --------------------------------------------------------------

// 类的继承
class Student extends Person {
  constructor(name, age, grade) {
  // 使用super关键字给父类构造器传参,让父类帮子类构造这些属性到实例上
    super(name, age);
    this.grade = grade;
  }
  // 在Student的原型上
  sayGrade() {
    console.log('1004', this);
  }
}
let s = new Student();
s.sayHello(); // 可以执行
console.log(s.hasOwnProperty('sayHello')); // false Person类的原型上的方法
console.log(s.hasOwnProperty('sayName')); // true
console.log(s.hasOwnProperty('sayAge')); // true
console.log(s.hasOwnProperty('sayGrade')); // false  Student类的原型上的方法

// 子类的原型的__proto__ 属性所指向的原型对象就是父类的原型对象
console.log(Student.prototype.__proto__ === Person.prototype);// true

总结:以上就是我所知晓的创建对象的方法啦,希望看到这里的小伙伴能有所收获!!!