引言:对象就是一系列属性的集合,一个属性包含一个名和一个值(键值对)。
一、单个对象的创建
!!!不适合创建大量相似的对象,
代码复用率低,且会占据大量的内存,性能低
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