原型链 面向对象和继承

202 阅读8分钟

原型链总结架构图

image.png

对象是什么?为什么要面向对象?

特点:面向对象(OOP): 逻辑迁移灵活、代码可复用性高、高度模块化


对象的理解

  • 对象是对于单个物体的简单抽象

  • 对象是一个容器,封装了属性&方法 ** 属性: 对象的状态

** 方法: 对象的行为

//简单对象
const Course = {
    teacher: 'AA',
    leader: 'BB',
    startCourse: function (name) {
        return `开始${name}课程`;
    }
}
//函数对象
function Course () {
    this.teacher = 'AA';
    this.leader = 'BB';
    this.startCourse = function (name) {
        return `开始${name}课程`;
    }
}

构造函数 - 生成对象

**需要一个模板 - 表征了一类物体的共同特征,从而生成对象

**类即对象模板

**js其实本质上不是基于类,而是基于构造函数 + 原型链

**constructor + prototype

    function Course () {
        this.teacher = 'AA';
        this.leader = 'BB';
    }
    const course = new Course();

Course本质就是构造函数

  • 1.函数里用this来指向实例对象
  • 2.生成对象通过new来实例化
  • 3.可以做初始化传参

追问:

**构造函数,不初始化,可以使用么 - 无法使用

**如果需要使用,如何做兼容

    function Course () {
        const isClass = this instanceof Course; //刚开始的this指向window
        if(!isClass) {
            return new Course(); // 这里再次调用Course方法,把this指向Course
        }
        this.teacher = 'AA';
        this.leader = 'BB';
    }
    const course = Course();

追问:new 是什么 / new的原理 / new做了什么


  funtion Course() {}

  const course = new Course();

  1. 创建了一个空对象,作为返回的对象实例
  2. 将生成的空对象的__proto__指向构造函数的prototype属性
  3. 将构造函数中的this指向当前实例对象
  4. 执行构造函数的初始化代码

image.png

如果new一个箭头函数会怎么样?

箭头函数是ES6中的提出来的,它没有prototype,也没有自己的this指向,更不可以使用arguments参数,所以不能New一个箭头函数。

箭头函数的 this 指向哪⾥?

箭头函数不同于传统JavaScript中的函数,箭头函数并没有属于⾃⼰的this,它所谓的this是捕获其所在上下⽂的 this 值,作为⾃⼰的 this 值,并且由于没有属于⾃⼰的this,所以是不会被new调⽤的,这个所谓的this也不会被改变。 可以⽤Babel理解⼀下箭头函数:

// ES6 
const obj = { 
  getArrow() { 
    return () => { 
      console.log(this === obj); // true
    }; 
  } 
}

追问: 实例属性影响

    function Course (teacher, leader) {
        this.teacher = teacher;
        this.leader = leader;
    }
    const course1 = new Course('AA', 'BB');
    cosnt course2 = new Course('AA', 'CC');
    course2.leader = '可可'; // course1.leader不会受到影响

constructor 是什么


function Course(teacher, leader) {
  this.teacher = teacher;
  this.leader = leader;
}

const course = new Course('AA', 'BB');

    1. 每个对象创建时会自动拥有一个构造函数属性constructor
    1. constructor继承自原型对象,指向构造函数的引用 course.constructor === course.proto.constructor === Course.prototype.constructor (constructor继承自原型对象)

course.constructor === Course (指向构造函数的引用)

追问:使用构造函数 没有问题么? / 会有什么性能问题?


  function Course(name) {
    this.teacher = 'AA',
    this.leader = 'BB',
    this.startCourse = function(name) {
      return `开始${name}课`;
    }
  }
  const course1 = new Course('es6');
  const course2 = new Course('OOP');
  // 构造函数中的方法,会存在于每个生成的实例中,重复挂载会导致资源浪费

原型对象图:

image.png

image.png Object.prototype.proto = null


原型对象

  function Course() {}
  const course1 = new Course();
  const course2 = new Course();
    1. 构造函数:用来初始化创建对象的函数 - Course ** 自动给构造函数赋予一个属性prototype,该属性实际等于实例对象的__proto__属性
    1. 实例对象:course1就是实例对象,根据原型创建出来的实例 ** 每个对象中都有个__proto__

** 每个实例对象都有个constructor属性

** constructor由继承而来,并指向当前构造函数

  • 3.原型对象:Course.prototype


  // 对上篇原型对象做优化

  function Course(name) {
    this.teacher = 'AA';
    this.leader = 'BB';
  }
  //避免了构造函数中的方法被重复挂载,并且在原型对象的所有属性和方法,都能被实例所共享
  Course.prototype.startCourse = function(name) { 
    return `开始${name}课`;
  }

  const course1 = new Course('es6');
  const course2 = new Course('OOP');


继承

在原型对象的所有属性和方法,都能被实例所共享

// Game类
  function Game() {
    this.name = 'lol';
  }
  Game.prototype.getName = function() {
    return this.name;
  }
  // LOL类
  function LOL() {}
  // LOL继承Game类
  LOL.prototype = new Game();
  LOL.prototype.constructor = LOL;
  const game = new LOL();
  // 本质: 重写原型对象,将父对象的属性方法,作为子对象原型对象的属性和方法

image.png

image.png

LOL.prototype = new Game()这句话的意义?

根据图: game要拿到game.name, 通过找game的__proto__找到LOL.prototype LOL.prototype因为指向了new Game的实例,因此通过继承构造函数的this指向实例可以拿到name的值。又或者更深一步考虑,指向new Game,因此会去找new Game的__proto__,因此找到Game的prototype. Game的prototype没找到又找到了Game的构造函数,里面包含name,因此找到了name的值。 因此通过LOL.prototype = new Game(),重写了原型对象,将父对象的属性方法,作为子对象原型对象的属性和方法。

追问:原型链继承有什么缺点

function Game() {
  this.name = 'lol';
  this.skin = ['s'];
}
Game.prototype.getName = function() {
  return this.name;
}
// LOL类
function LOL() {}
// LOL继承Game类
LOL.prototype = new Game();
LOL.prototype.constructor = LOL;
const game1 = new LOL();
const game2 = new LOL();
game1.skin.push('ss');

此时game1.skin与game2.skin相同,都是['s','ss']

    1. 父类属性一旦赋值给子类的原型属性,此时属性属于子类的共享属性了
    1. 实例化子类时,不能向父类传参

解决方案: 构造函数继承

经典继承: 在子类构造函数内部调用父类构造函数

function Game(arg) {
  this.name = 'lol';
  this.skin = ['s'];
}
Game.prototype.getName = function() {
  return this.name;
}
// LOL类
function LOL(arg) {
  Game.call(this, arg);
}

// LOL继承Game类
const game3 = new LOL(1);
const game4 = new LOL(2);
console.log(game3.arg, game4.arg);
game3.skin.push('1', '2');
console.log(game3.skin,game4.skin)
//输出结果
1 2
['s', '1', '2'] ['s']
// 解决了共享属性问题&传参问题

但是调用原型链上的方法时,显示not a function image.png

追问:原型链上的共享方法无法被读取继承,如何解决?

解决方案: 组合继承

function Game(arg) {
  this.name = 'lol';
  this.skin = ['s'];
}
Game.prototype.getName = function() {
  return this.name;
}
// LOL类
function LOL(arg) {
  Game.call(this, arg);
}
LOL.prototype = new Game();
LOL.prototype.constructor = LOL;
// LOL继承Game类
const game3 = new LOL();

追问: 组合继承就没有缺点么? 问题就在于:无论何种场景,都会调用两次父类构造函数。

    1. 初始化子类原型时
    1. 子类构造函数内部call父类的时候

解决方案: 寄生组合继承

function Game(arg) {
  this.name = 'lol';
  this.skin = ['s'];
}
Game.prototype.getName = function() {
  return this.name;
}
// LOL类
function LOL(arg) {
  Game.call(this, arg);
}
// Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
LOL.prototype = Object.create(Game.prototype); 
LOL.prototype.constructor = LOL;
// LOL继承Game类
const game3 = new LOL();

提高:看起来完美解决了继承。js实现多重继承?

function Game(arg) {
  this.name = 'lol';
  this.skin = ['s'];
}
Game.prototype.getName = function() {
  return this.name;
}
function Store() {
  this.shop = 'steam';
}
Store.prototype.getPlatform = function() {
  return this.shop;
}
// LOL类
function LOL(arg) {
  Game.call(this, arg);
  Store.call(this, arg);
}
LOL.prototype = Object.create(Game.prototype);
// LOL.prototype = Object.create(Store.prototype);
Object.assign(LOL.prototype, Store.prototype);
LOL.prototype.constructor = LOL;
// LOL继承Game类
const game3 = new LOL();

Object.create 和 new 有什么区别?

js中创建对象的方式一般有两种Object.create和new

image.png

在讲述两者区别之前,我们需要知道:

  • 构造函数Foo的原型属性Foo.prototype指向了原型对象。
  • 原型对象保存着实例共享的方法,有一个指针constructor指回构造函数。
  • js中只有函数有 prototype 属性,所有的对象只有 **proto** 隐式属性。

Object.create

image.png

Object.create是内部定义一个对象,并且让F.prototype对象 赋值为引进的对象/函数 o,并return出一个新的对象。

new

image.png

new做法是新建一个obj对象o1,并且让o1的__proto__指向了Base.prototype对象。并且使用 call 进行强转作用环境。从而实现了实例的创建。

区别

image.png

小结

image.png

如何获取到一个实例对象的原型对象?

  • 从 构造函数 获得 原型对象:
构造函数.prototype
  • 从 对象实例 获得 父级原型对象
方法一: 对象实例.__proto__        【 有兼容性问题,不建议使用】
方法二:Object.getPrototypeOf( 对象实例 )

如何确保你的构造函数只能被new调用,而不能被普通调用?

明确函数的双重用途

JavaScript 中的函数一般有两种使用方式:

  • 当作构造函数使用: new Func()
  • 当作普通函数使用: Func() 人为规定构造函数名首字母要大写作为区分。也就是说,构造函数被当成普通函数调用不会有报错提示。

使用 instanceof 实现

instanceof 基础知识

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

使用 instanceof 检测某个对象是不是另一个对象的实例,例如 new Person() instanceof Person --> true

new 绑定/ 默认绑定

  • 通过 new 来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的 this 。
  • 如果普通调用函数,非严格模式 this 指向 window,严格模式指向 undefined

image.png

new 调用函数和普通调用函数最大的区别在于函数内部 this 指向不同:
new 调用后 this 指向实例,
普通调用则会指向 window

instanceof 可以检测某个对象是不是另一个对象的实例。
如果为 new 调用, this 指向实例,this instanceof 构造函数 返回值为 true ,普通调用返回值为 false

image.png

new.target

new.target 属性,该属性一般用在构造函数之中,返回 new 命令作用于的那个构造函数。 如果构造函数不是通过 new 命令或 Reflect.construct() 调用的,new.target 会返回 undefined ,这个属性可以用来确定构造函数是怎么调用的

function Person () {
    console.log(new.target);
}
console.log('new:',  new Person()) // new: Person {}
console.log('not new:', Person()) // not new: undefined

所以我们就可以使用 new.target 来非常简单的实现对构造函数的限制。

function Person () {
    if(!(new.target)) {
        throw new TypeError('Function constructor A cannot be invoked without "new"')
    }
}
// Uncaught TypeError: Function constructor A cannot be invoked without "new"
console.log('not new:', Person())

使用ES6 Class

类也具备限制构造函数只能用 new 调用的作用。
ES6 提供 Class 作为构造函数的语法糖,来实现语义化更好的面向对象编程,并且对 Class 进行了规定:类的构造器必须使用 new 来调用

因此后续在进行面向对象编程时,强烈推荐使用 ES6 的 Class
Class 修复了很多 ES5 面向对象编程的缺陷,例如类中的所有方法都是不可枚举的;类的所有方法都无法被当作构造函数使用等。

new.target 实现抽象类

image.png Class 内部调用 new.target,会返回当前 Class需要注意的是,子类继承父类时,new.target会返回子类

image.png 通过上面案例,我们可以发现子类调用和父类调用的返回结果是不同的,我们利用这个特性,就可以实现父类不可调用而子类可以调用的情况——面向对象中的抽象类

抽象类实现

抽象类也可以理解为不能独立使用、必须继承后才能使用的类。

image.png 这里的抽象类Animal不能直接调用,必须备继承后才能使用

image.png