【笔记】面向对象 / 原型 / 原型链 / 继承

一. 面向对象

1. 什么是类?

类是对一类具有共同特征的事物的抽象,是对象的模板。类的内部封装了属性和方法,用于操作自身的成员。类是对某种对象的定义,具有行为,它描述一个对象能够做什么以及做的方法,它们是可以对这个对象进行操作的程序和过程。它包含有关对象行为方式的状态,包括它的名称、属性、方法和事件。

2. 什么是对象?
  • 对象对于单个物体的简单抽象
  • 对象是一个容器,封装了属性(对象的特征/状态)和方法(对象的行为)
3. 为什么要面向对象?
  • 面向过程:是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的依次调用,即按照步骤编程 (函数和变量)。
  • 面向对象:面向对象是把事务分解成为一个个对象,然后由对象之间分工与合作。将需求分析出一个一个的对象,然后在分析出对象中的属性和方法,最后按照步骤编程(方法和属性)。特点:逻辑迁移更加灵活、代码复用性高、高度模块化的体现
  • 面向过程和面向对象对比
面向过程面向对象
优点性能比面向对象高,适合跟硬件联系很紧密的东西,例如单片机就采用的面向过程编程。易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
缺点不易维护、不易复用、不易扩展性能比面向过程低

二. 创建对象

1. 对象字面量

使用花括号{ }包含表达这个对象的属性和方法。{ }里面采用键值对的方式表示。
缺点:创建具有同样接口的多个对象需要重复编写很多代码。且创建出来的对象是开放性的,无法管控,外部可以随意更改。

// 简单对象 - 本身开放, 外部可以随意更改
var person = {
    name: '老王',
    age: 30,
    sayName: function() {
        console.log(this.name);
    }
}
2. 工厂模式

工厂模式是设计模式的一种,是一种用于抽象创建特定对象的过程
缺点:可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)

function crearePerson(name, age) { // 创建对象的工厂
    let o = new Object(); // 借助Object创建一个对象
    o.name = name;
    o.age = age;
    o.sayName = function () {
        console.log(this.name);
    }
    return o; // 返回创建完的对象
}

let person1 = crearePerson('老王', 30);
let person2 = crearePerson('小红', 20);

person1.sayName(); // 老王
person2.sayName(); // 小红
3. 构造函数 / new

是一种特殊的函数,主要用来初始化对象,为对象成员变量赋初始值。
构造函数可以是函数表达式,也可以是函数声明。
:任何函数只要使用 new 操作符调用就是构造函数,而不使用 new 操作符调用的函数就是普通函数。

  • 构造函数名称首字母要大写
  • new操作符组合使用创建实例
  • 相比于工厂模式,构造函数可以确保实例被标识为特定类型,可以使用 instanceof 操作符确定对象类型
function Person(name, age) { // 与工厂模式对比,不显式创建对象
    this.name = name; // 属性和方法直接赋值给this
    this.age = age;
    this.sayName = function () {
        console.log(this.name);
    }
    // 没有 return 语句
}
const person = new Person('老王', 30);
  • new 操作符干了什么事情?
    1. 结构上:在内存中创建一个新对象。
    2. 属性上:新对象的原型([[Prototype]]特性)指向构造函数的 prototype。
    3. 关系上:构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
    4. 生命周期上:执行构造函数内部的代码(给新对象添加属性)。
    5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
  • 构造函数的缺点
    构造函数定义的方法会在每个实例上都创建一遍,即使将共同的方法提取出来,可以解决相同逻辑的函数重复定义的问题,但全局作用域也因此变得混乱且会造成资源浪费,因为如果对象中需要多个方法,就要在全局作用域中定义多个函数。
function Person(name, age) {
    this.name = name;
    this.age = age;
    // 每个实例的sayName方法逻辑等价,但不同实例上的sayName函数同名不相等
    this.sayName = function () { 
        console.log(this.name);
    }
}

// 优化: 利用this对象,可以把函数与对象的绑定推迟到运行时。
function Person(name, age){ 
     this.name = name; 
     this.age = age; 
     this.sayName = sayName; 
} 

function sayName() { // 引起全局作用域混乱和资源浪费
     console.log(this.name); 
}
4. 原型模式

每个函数都有一个 prototype 属性,这个属性是一个对象(原型对象)。这个对象就是通过调用构造函数创建的对象的原型。
每个原型对象(prototype)都有一个 constructor 属性,指回它的构造函数。
每个对象(函数也是对象)都有一个 _proto_属性,这个属性是一个对象,指向它的构造函数的原型。
原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。原来在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型。

function Person() {}

Person.prototype.name = '老王'; // 所有实例共享的属性和方法
Person.prototype.age = 30;
Person.prototype.sayName = function() {
    console.log(this.name);
}

var dan = new Person('丹丹', 24); // 传参没用的
5. Object.create()

Object.create() 方法,它创建一个对象,其中第一个参数就是这个对象的原型,Object.create()提供第二个可选参数,用以对对象的属性进行进一步描述。

var p1 = Object.create(null) // 相当于空对象,任何属性都没有
var p2 = Object.create(Person.prototype) // 相当于var p2 = {}
var p3 = Object.create({ name: '丹丹' }) // 相当于var p3 = {}; p3.prototype = { name: '丹丹' }

三. 原型和原型链

1. 构造函数,原型对象和实例
  • 无论何时,只要创建一个函数,就会为这个函数创建一个 prototype 属性,指向这个函数的原型对象。因此定义构造函数的时候(构造函数可以是函数表达式,也可以是函数声明),构造函数就自动有了一个与之关联的原型对象。
  • 所有的原型对象都自动获得一个 constructor 属性,指回与之关联的构造函数。
    function Person() {}              // 函数声明构造函数
    let Person = function() {};       // 函数表达式构造函数
    // 构造函数
    var person = new Person();
    // 构造函数的原型对象
    typeof Person.prototype;     // 'object'
    // 原型对象的 constructor 属性,指回构造函数
    Person.prototype;            // { constructor: Person() }
    
    从上面的代码段不难看出,构造函数和原型对象通过 prototype 和 constructor 实现了循环引用,如下图所示
  • 每个通过构造函数创建的新实例内部的 [[Prototype]] 指针指向了其构造函数的原型对象(注意,不是prototype属性,实例的 prototype 属性为 undefined)。[[Prototype]] 指针对对象来说是隐藏的,但是在Firefox、Safari 和 Chrome中会暴露__proto__属性,通过这个属性可以访问到这个对象的原型。
  • 实例通过__proto__ ([[Prototype]]特性)链接到原型对象,构造函数通过 prototype 属性链接到原型对象。实例与构造函数没有直接关联,与原型对象有直接关联。
    如下图,① 的关系实际上是通过 ② 建立起来的

由构造函数创建一个实例为例,每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。根据前文说明,对原型和原型链的关系绘制如下图:

function Person() {} // 相当于 var Person = new Function()
var person = new Person();
按颜色分组,其中的等量关系有 `5 + 4 + 2 = 11` 个
function Person() {
    this.name = '丹丹';
}
var person = new Person();

// 蓝色组 5
1. person.__proto__ === Person.prototype;                 // true
2. person.__proto__.__proto__ === Object.prototype;       // true
3. person.__proto__.__proto__.__proto__ === null;         // true
4. Person.prototype.constructor === Person;               // true
5. Object.prototype.constructor === Object;               // true

// 橙色组 4
6. Person.constructor === Function;                       // true
7. Object.constructor === Function;                       // true
8. Person.__proto__ === Function.prototype;               // true
9. Object.__proto__ === Function.prototype;               // true

// 紫色组 2
10. Function.prototype.constructor === Function;           // true
11. Function.prototype.__proto__ === Object.prototype      // true
2. 原型链

当一个对象实例的原型指向了另一个对象的实例,在实例和原型之间就构造成了一条原型链。

function Parent() {
    this.parentMoney = 5000;
}

Parent.prototype.getParentMoney = function() {
    return this.parentMoney;
}

function Child () {
    this.childMoney = 5;
}

Child.prototype = new Parent(); // 原型指向了另一个对象的实例,实现了继承
Child.prototype.getChildMoney = function() {
    return this.childMoney;
}

var peron = new Child();
console.log(peron.getParentMoney()); // 5000
console.log(peron.getChildMoney());  // 5

在读取实例上的属性时,首先会在实例上搜索这个属性,如果没找到,就会去这个实例的原型上找。再通过原型链实现继承后,搜索就可以继承向上,搜索原型的原型,这样层层向上就形成了一个链式结构,这就是原型链。

原型搜索机制
  • 以搜索 getParentMoney() 方法为例
    1. 首先搜索 person 实例本身有没有这个方法,没有。
    2. 通过__proto__访问 person 实例的原型 Child.prototype,搜索原型上有没有这个方法,没有。
    3. person 的原型 Child.prototype 指向了 Parent 的实例 new Parent(),实现了继承。因此搜索可以继续向上。
    4. 搜索 Parent 的实例 new Parent() 上有没有这个方法,没有。
    5. 通过__proto__访问 new Parent() 这个实例的原型 Parent.prototype,搜索原型上有没有这个方法,找到啦。

搜索 getChildMoney() 方法的机制也是一样的道理,不过getChildMoney() 方法在 person 实例的原型上就找到了。

原型链存在的问题
  1. 原型中包含的引用值会在所有实例之间共享,会导致意想不到的错误。且父类的属性一旦赋值给子类的原型,这些属性就属于子类共享属性了 (继承者的实例间篡改)
  2. 子类型在实例化时不能给父类型的构造函数传参。

四. 继承

为了解决原型链继承包含引用值导致的共享属性问题,不同的继承方法被提出和改进。

1. 盗用构造函数继承 / 经典继承 / 对象伪装
  • 在子类的构造函数中调用父类的构造方法。
  • 使用 apply()call() 以新创建的对象为上下文执行构造函数。
优点
  1. 可以在子类构造函数中向父类构造函数传参。
  2. 解决了原型的引用值属性的共享问题。
缺点
  1. 必须在构造函数中定义方法,否则函数不能被重用。
  2. 子类无法访问父类原型中的方法 ,如下面挂载在 Parent 原型上的 getName()。
function Parent(name, age) {
    this.name = name;
    this.age = age;
    this.sex = '男';
}

Parent.prototype.getName = function () {   // 这个函数无法被子类访问到
    return this.name;
}

function Child(name, age) {
    Parent.call(this, name, age); // 继承 Parent 并传参
    this.friends = ['Mark']; // 自己的实例属性
}

var person = new Child('小明', 10);  // { "name": "小明", "age": 10, "sex": "男" , friends: ['Mark']}
2. 组合继承 / 伪经典继承

组合继承综合了原型链继承和盗用构造函数继承的优点。

  • 使用原型链继承原型上的属性和方法
  • 使用盗用构造函数继承实例的属性。

这样既可以把公用方法挂载在原型上以实现重用,又可以让每个实例都有自己的属性。

优点

弥补了原型链和盗用构造函数的不足,并且保存了 instanceof 操作符和 isPrototypeOf() 方法识别合成对象的能力。

缺点

为了实现原型链继承和盗用构造函数的继承,父类的构造函数会被执行两次,一次是在创建子类的时候,另一次是在子类构造函数中调用,造成资源的浪费。

function Parent(name, age) {
    this.name = name;
    this.age = age;
    this.sex = '男';
}
// 挂载在父类原型上的公用方法
Parent.prototype.getName = function () {  
    return this.name;
}

function Child(name, age, frineds) {
    Parent.call(this, name, age); // 继承属性, 执行了第二次 Parent 构造方法
    this.friends = frineds; // 自定义属性
}
Child.prototype = new Parent(); // 实现继承, 执行了第一次 Parent 构造方法, 非必要
Child.prototype.constructor = Child; // constructor 丢失,指回构造函数
Child.prototype.getFriends = function() { // 挂载在子类原型上的公用方法
    return this.friends;
}

var person1 = new Child('小明', 10, ['Mark']);
var person2 = new Child('小强', 8, ['Lisa'])
// 调用父类原型上的方法
console.log(person1.getName()); // 小明
// 调用子类原型上的方法
console.log(person1.getFriends()); // ['Mark']

person1.friends.push('Tom');  // 实例属性互不影响
console.log(person1.friends); // ['Mark', 'Tom']
console.log(person2.friends); // ['Lisa']

console.log(person1 instanceof Parent); // true
console.log(person1 instanceof Child);  // true
3. 寄生组合继承
  • 不直接通过调用父类构造函数给子类原型赋值,而是通过 Object.create() 取得父类原型的副本。
  • 是引用类型继承的最佳模式。
function Parent(name, age) {
    this.name = name;
    this.age = age;
    this.sex = '男';
}

Parent.prototype.getName = function () {  // 挂载在父类原型上的公用方法
    return this.name;
}

function Child(name, age, frineds) {
    Parent.call(this, name, age); // 继承属性
    this.friends = frineds;
}
// 使用 Object.create 继承 Parent
Child.prototype = Object.create(Parent.prototype) 
Child.prototype.constructor = Child; // constructor 丢失,指回构造函数
Child.prototype.getFriends = function() { // 挂载在子类原型上的公用方法
    return this.friends;
}

var person = new Child('小明', 10, ['Mark']);
怎么实现多重继承?
  • 使用 Object.create() 实现单个继承
  • 使用 Object.assign() 实现多重继承
function Father(name) {
    this.eyeColor = 'blue'; // 蓝眼睛的爸爸
    this.name = name;
}

Father.prototype.getEyeColor = function () {
    return this.eyeColor;
}

function Mother(age) {
    this.blood = 'A';  // A 血型的妈妈
    this.age = age;
}

Mother.prototype.getAge = function () {
    return this.age;
}

function Child(name, age) {
    Father.call(this, name); // 继承爸爸的属性
    Mother.call(this, age);  // 继承妈妈的属性
}
// 继承爸爸的原型方法
Child.prototype = Object.create(Father.prototype);
// 继承妈妈的原型方法
Object.assign(Child.prototype, Mother.prototype);
// 原型的consturctor丢失,指回自己的构造函数
Child.prototype.constructor = Child;

var person = new Child('小明', 15);
console.log(person); // { "eyeColor": "blue", "name": "小明", "blood": "A", "age": 15 }
console.log(person.getEyeColor()); // 'blue'
console.log(person.getAge()); // 15 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值