原型与原型链、继承

66 阅读3分钟

原型(Prototype)

每个函数在创建时,都会自动获得一个 prototype 属性,这个属性指向一个对象,称为该函数的原型对象。当使用 new 操作符基于这个函数创建对象实例时,实例的内部 [[Prototype]] (在大多数现代浏览器中,可以通过 __proto__ 访问,但不推荐直接使用)会指向该函数的原型对象。

原型对象的主要用途是实现属性和方法的共享与复用。在原型对象上定义的属性和方法,可以被通过该构造函数创建的所有实例访问和使用。

function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, I'm ${this.name}`);
};

let person1 = new Person('Alice');
let person2 = new Person('Bob');

person1.sayHello(); 
person2.sayHello(); 

在上述示例中,sayHello 方法定义在 Person 函数的原型对象上,但 person1person2 实例都可以调用它。

原型链(Prototype Chain)

当访问一个对象的属性或方法时,如果在对象本身找不到,JavaScript 引擎会沿着对象的 __proto__ 所指向的原型对象继续查找。如果在当前原型对象中仍然找不到,就会继续沿着当前原型对象的 __proto__ 向上查找,如此形成一个链式结构,这就是原型链。

原型链的顶端是 Object.prototype,如果在整个原型链中都没有找到要访问的属性或方法,最终会返回 undefined

function Animal() {}
Animal.prototype.eat = function() {
  console.log('Eating...');
};

function Dog() {}
Dog.prototype = new Animal();

let dog = new Dog();
dog.eat(); 

在这个例子中,创建的 dog 对象本身没有 eat 方法,但通过原型链在 Animal 的原型对象上找到了 eat 方法。

prototype与proto

  • prototype:这是函数对象所特有的属性。当使用 new 操作符基于一个函数创建对象实例时,新创建的对象的 __proto__ 会指向该函数的 prototype 对象。例如,上面的构造函数 PersonPerson.prototype 上定义的属性和方法可以被通过 new Person() 创建的实例访问和继承。
  • __proto__:这是大多数对象都具有的一个内部属性(在 ES6 中,更推荐使用 Object.getPrototypeOf()Object.setPrototypeOf() 方法来操作对象的原型)。它用于建立对象与原型对象之间的链接,从而实现属性的查找和继承。

简单来说,prototype 用于定义构造函数的原型,而 __proto__ 用于连接对象和其对应的原型。

继承

说到原型就不得不说一下js中的继承。

继承的主要目的是代码复用和实现对象之间的层次关系。通过继承,可以创建具有相似特征和行为的对象类,减少重复代码,并使代码结构更清晰、更易于维护。

js中继承分为两种:基于原型的继承、基于class的继承

基于原型的继承方式

原型链继承

原理:将子类的原型对象设置为父类的实例

不足:

  1. 引用类型属性被共享
  2. 无法向父类构造函数传参。因为父类构造函数被挂载在prototype上, 所以子类new时传递进来的参数无法直接传递到Parent上
function Parent() {
    this.colors = ["red", "blue"];
}

function Child() {}

Child.prototype = new Parent();

let child1 = new Child();
child1.colors.push("green");

let child2 = new Child();
console.log(child2.colors); // ["red", "blue", "green"]  引用类型被共享

借用构造函数继承

原理:在子类构造函数中通过 call()apply() 方法调用父类构造函数来继承属性,解决了引用类型共享和传参问题.

不足:函数无法复用。child1、child2中都单独有一个独立的sayHello方法

function Parent(name) {
  this.name = name;
  this.colors = ["red", "blue"];
  this.sayHello = function() {
    console.log(`Hello, I'm ${this.name}`);
  };
}

function Child(name) {
  Parent.call(this, name);
}

let child1 = new Child("John");
child1.colors.push("green");


let child2 = new Child(“bob”);

组合式继承

原理:结合原型链继承和借用构造函数继承,用原型链继承方法,用借用构造函数继承实例属性

不足:父类构造函数会被调用两次

function Parent(name) {
    this.name = name;
    this.colors = ["red", "blue"];
}

Parent.prototype.sayName = function() {
    console.log(this.name);
};

function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

let child = new Child("John", 10);

原型式继承

原理:创建一个函数, 在函数内部创建一个临时的构造函数,将函数传进来的对象作为构造函数的原型,然后返回这个临时构造的对象实例。

不足:引用类型属性被共享

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

let person = {name: "John"};
let anotherPerson = object(person);

寄生式继承

原理:基于原型式继承创建一个新对象,然后为新对象添加额外的属性和方法。

function createAnother(original) {
    let clone = object(original);
    clone.sayHi = function() {
        console.log("Hi");
    };
    return clone;
}

let person = {name: "John"};
let anotherPerson = createAnother(person);

寄生组合式继承

原理:被认为是 JavaScript 中最理想的继承方式。它通过创建一个空的中间对象,将中间对象的原型指向父类的原型,然后将子类的原型指向这个中间对象,避免了父类构造函数的重复调用。

function inheritPrototype(subType, superType) {
    let prototype = Object.create(superType.prototype);
    prototype.constructor = subType;
    subType.prototype = prototype;
}

function Parent() {}
Parent.prototype.someMethod = function() {}

function Child() {}
inheritPrototype(Child, Parent);

基于class的继承方式

extends-推荐实际使用

  1. 语法更清晰和简洁,更接近传统面向对象编程语言的语法,易于理解和使用。
  2. extends 关键字明确表示了继承关系。
  3. 通过 super 关键字方便地调用父类的构造函数和方法。
  4. 支持方法的重写和扩展,使得代码的结构更加清晰和可维护。
// 父类
class Parent {
  constructor(name) {
    this.name = name;
  }

  sayHello() {
    console.log(`Hello, I'm ${this.name}`);
  }
}

// 子类继承父类
class Child extends Parent {
  constructor(name, age) {
    // 调用父类的构造函数
    super(name); 
    this.age = age;
  }

  // 子类可以重写父类的方法
  sayHello() {
    console.log(`Hello, I'm ${this.name} and I'm ${this.age} years old`);
  }
}

let child = new Child('John', 10);
child.sayHello(); 

call、apply、bind的实现

上面我们了解了原型、原型链以及继承的一些知识点。知道通过call、apply改变指针也能实现继承。那call、apply、bind是如果改变指针指向的呢?这个又跟原型、原型链有什么关系呢?

我们自己来实现一下这三个方法就明白了

call

Function.prototype.myCall = function(context, ...args){
  // 判断调用对象是否为函数
  if(typeof this !== 'function'){
    throw new TypeError('not a function')
  }
  // 判断context是否传入,如果未传入则设置为window
  context = context || window
  // 给context创建一个Symbol属性以免覆盖原有的属性
  const fn = Symbol()
  context[fn] = this
  // 调用函数
  let res = context[fn](...args)
  // 将属性删除
  delete context[fn]
  // 返回结果
  return res
}

apply

apply与call的区别在于参数格式不同

Function.prototype.myApply = function (context, args) {
  if(typeof this !== 'function'){
    throw new TypeError('not a function');
  }
  context = context || window;
  const fn = Symbol()
  context[fn] = this
  let res = context[fn](args);
  delete context[fn];
  return res;
}

bind

bind方法执行后会返回一个函数,结合apply可以改变返回的函数的指针指向

Function.prototype.myBind = function(context,...args){
  let self = this;
  console.log(args);
  return function(){
    return self.myApply(context,args)
  }
}