面对对象、原型链、继承

172 阅读6分钟

面向对象编程

面向对象的体现

Javascript是一种基于对象(object-based)的语言,你遇到的所有东西几乎都是对象。但是它又不是一种面向对象编程(OOP)语言,因为在ES6之前,它的语法中没class(类)的概念,封装定义类基于构造函数(constructor)和原型链(prototype)。

function Person (name,age,sex){
    this.name = name;
    this.age = age;
    this.sex = sex;
    sayHell:function () {
    consolo.log("Hello")
    }
}
Person.prototype.sayHello = function () {
    consolo.log("Hello")
}

在2015年6分发布的ES6中,开始引入了class这个概念。

class Person{
  // 类的静态方法,相当于Person.test = function(){console.log("类的静态方法");}
  static test() {
    console.log("类的静态方法");
  }
  //constructor构造函数
  constructor(name,age){
    console.log("调用构造函数");
    this.name = name;
    this.age = age;
  }
  //类的一般方法,定义在实例对象的原型对象上,相当于Person.prototype.show = function(){console.log("this.name,this.age");}
  show(){
    console.log(this.name,this.age);
  }
}

为什么要面对对象

逻辑迁移更加灵活:逻辑是封装在固定的模块内的,当外部需要用到模块时不需要关心实现方式,只需要关心提供的变量和方法,例如element-ui,Ant-design组件库。
代码复用性高:可以多次使用,不用重复实现,纵使功能不一样,我们可以通过外部的方法或者变量的传入去实现不同场景下的效果。
高度模块化体现:随着软件的进步,我们写的代码越来越多越来越复杂,不得不对语言规范做出更严格的控制,规范性、可读性才能让软件规模进一步扩大。

什么是对象

现实生活中:对于单个事物的简单抽象

一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个与远程服务器的连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。

JavaScript中:是一个容器,封装了属性和方法 对象是一组无需的相关属性和方法的集合,所有的事物都是对象,例如字符串、数值、数组、函数等。

对象是由属性和方法组成的:属性:事物的 特征,在对象中用 属性 来表示(常用名词),方法:事物的 行为,在对象中用 方法 来表示(常用动词)

创建对象的几种方式

// 第一种
let a = new Object();
// 第二种
let b = {}
// 第三种,使现有的对象来提供新创建对象的__proto__
Object.create();
let Fn = {};
let fn = Object.create(Fn); // fn.__proto__ === Fn
// 第四种,使用构造函数来创建一个对象并且返回
new Function();

构造函数

 //简单对象
 const obj = {
     name:"小李"age:23,
     sex:"男"
 }
 // 函数对象
 function Person(name,age,sex){
     this.name = name;
     this.age = age;
     this.sex = sex
 }
 let person = new Person("jiuamy",23,"男")

构造函数,就是提供一个生成对象的模板,并描述对象的基本结构的函数。一个构造函数,可以生成多个对象,每个对象都有相同的结构。

new 操作符

  • 创建了一个对象;
  • 对象的原型,指向了这个Function(构造函数)的prototype;
  • 该对象实现了这个构造函数的方法;
  • 根据一些特定情况返回对象:
    • 如果没有返回值,则返回创建的对象;
    • 如果有返回值,不是一个对象,则返回创建的对象;
    • 如果有返回值,是一个对象,则返回该对象。
function Mynew (Fn,...args){
    if(typeof Fn !== "function"){
        throw new Error("new operator function the frist param must be a function")
    }
    const obj = Object.create(Fn.prototype);
    const result = Fn.apply(obj,args)
    return result && typeof result === "object" ? result : obj
}

function Person(name,age){
    this.name = name;
    this.age = age;
}
let person = Mynew(Person,"jiuamy",23)

原型、prototype、构造函数三者关系

person.__proto__ === Person.prototype;
Person.prototype.constructor === Person;
person.constructor === Person

Snipaste_2022-06-17_16-02-40.png

继承

实现一个继承,主要就是两个部分:

  • 使用父类的构造函数的方法;
  • 让对象的原型链指向父类。

原型链继承

function Parent (name){
    this.name = name
}
Parent.prototype.getName = function(){
    console.log(this.name)
}
function Child(){
}

如何让Child继承Parent的方法?
也就是说我们需要让Child成为Parent的实例。

// Child.prototype.__proto__ === Object.prototype  原本

// Child.prototype.__proto__ === Parent.prototype  现在

Child.prototype = new Parent();
Child.prototype.constructor = Child; // 指针(this)还原

隐含问题

  • 如果有属性是引用类型,一旦某个实例修改了这个属性,所有的都会修改;
  • 创建Child的时候不能传参数。
function Parent (actions,name){
    this.actions = [1,2,3,4,5,6];
    this.name = name;
}
Parent.prototype.setAction = function(){
    this.actions.push(7);
}
function Child(){
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
let child1 = new Child();
child1.setAction();
let child2 = new Child();
console.log(child2.actions,child2.actions) // child2实例上的actions属性被篡改

构造函数继承

把Parent上的属性和方法,添加在Child上,而不是都存在原型对象上,防止被实例共享。

    function Parent(actions,name){
        this.actions = actions;
        this.name = name;
    }
    function Child(play){
        Parent.apply(this,Array.prototype.slice.call(arguments,1));
        this.play = play;
    }
    let child = new Child('game',['eat','exercise','sleep'],'childName');

隐含问题

  • 属性或者方法想被继承的话,只能在构造函数中定义;
  • 如果方法在构造函数中定义了,那么每次创建实例都会创建一遍方法。

组合继承

组合继承的出现在一定程度上解决原型链继承和构造函数继承的隐含问题。

function Parent(actions,name){
    this.actions = actions;
    this.name = name;
}
Parent.prototype.work = function() {
    console.log(`${this.name} coding evenyday...`);
}
function Child(play){
    //继承Parent构造函数内的属性和方法.
    Parent.apply(this,Array.prototype.slice.call(arguments,1));
    this.play = play
}
// 继承Parent构造函数原型上的属性和方法.
Child.prototype = new Parent();
// 在new Child())的情况下,将指针重新指回Child.
Child.prototype.constructor = Child;

隐含问题

  • Child.prototype = new Parent()会对Parent构造函数进行初始化,可能会对Child.prototype产生副作用。

组合寄生式继承

function inheritPrototype(child, parent) {
    let prototype = Object.create(parent.prototype); // 创建对象
    prototype.constructor = child; // 增强对象
    child.prototype = prototype; // 赋值对象
}
function Parent(actions,name){
    this.actions = actions;
    this.name = name;
}
Parent.prototype.work = function() {
    console.log(`${this.name} coding evenyday...`);
}
function Child(play){
    Parent.apply(this,Array.prototype.slice.call(arguments,1));
    this.play = play
}
inheritPrototype(Child,Parent)
// Child.prototype = Object.create(Parent.prototype);
// Child.prototype.constructor = Child;

inheritPrototype

inheritPrototype()函数实现了寄生式组合继承的核心逻辑。这个函数接收两个参数:子类构造函数和父类构造函数。在这个函数内部,第一步是创建父类原型的一个副本。给返回的prototype 对象设置 constructor 属性,解决由于重写原型导致默认 constructor 丢失的问题。最后将新创建的对象赋值给子类型的原型。

Object.create

Object.create()  方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype),创建的新对象跟之前的值不会存在相同地址值。

错误问题

在调试代码的时候我发现如果使用Object()去创建一个新对象时,会出现地址值指向同一个地址的情况。
也就是说,我在重写Child构造函数原型和constructor指向的同时,Parent构造函数的原型和constructor也一并被修改了。并且红宝书寄生式组合继承的代码片段就是用的就是Object(),可能是红宝书的小bug吧。

image.png

function inheritPrototype(child, parent) {
    let prototype = Object(parent.prototype); // 创建对象
    prototype.constructor = child; // 增强对象
    child.prototype = prototype; // 赋值对象
}

function Parent(actions,name){
    this.actions = actions;
    this.name = name;
}

Parent.prototype.work = function() {
    console.log(`${this.name} coding evenyday...`);
}

function Child(play){
    Parent.apply(this,Array.prototype.slice.call(arguments,1));
    this.play = play
}

inheritPrototype(Child,Parent)

let child = new Child('game',['eat','exercise','sleep'],'childName');

let parent = new Parent('eat','parentName')

console.log(child,"child",parent,"parent")

image.png

ES6继承

class Parent{
}

class Child extends Parent {
    constructor(){
        super()
        // this.xxx
    }
}
  • super作为函数调用时,要求子类必须执行一次。
  • 子类自己的this对象,必须通过父类的构造函数完成。
  • 会继承静态的方法和属性