JS高级 ES5实现继承(八千字详解)

本文详细讲解了JavaScript中的对象原型、函数原型、面向对象的继承特性,以及各种实现继承的方式,包括原型链、借用构造函数、组合继承和寄生组合式继承。重点讨论了每种继承方式的优缺点,帮助读者深入理解JavaScript的继承机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

一、对象的原型

1.1 概念

1.2 作用

1.3 获取方式

二、函数的原型 prototype

2.1 概念

2.2 作用

2.2.1 new操作符

2.3 constructor属性

三、面向对象的特征—继承

3.1 面向对象三大特性

3.2 继承是做什么的?

3.3 JavaScript中如何实现继承

四、JavaScript原型链

五、Object

5.1 Object的原型

5.2 Object是所有类的父类

六、实现继承的方式(重点)

6.1 通过原型链实现继承

原型链继承的弊端

6.2 借用构造函数继承

优缺点:

6.3 组合继承(仍不够完美,但是基本上没用问题了)

优缺点:

6.4 寄生组合式继承(最终继承方案)

优缺点:

七、原型继承关系

7.1 解释

7.2 图解


一、对象的原型

1.1 概念

JavaScript当中每个对象都有一个特殊的内置属性 [[prototype]] ,这个特殊的对象可以指向另外一个对象。

1.2 作用

  • 当通过引用对象的属性key来获取一个value时,它会触发[[Get]]的操作
  • 这个操作会首先检查该对象是否有对应的属性,如果有的话就使用它
  • 如果对象中没有该属性,那么会访问对象[[prototype]] 内置属性指向的对象上的属性

💠只要是对象都会有这样的一个内置属性!

1.3 获取方式

🔹方式一:(不常用)

通过对象的__proto__属性可以获取到 (早期浏览器自己添加的,存在一定的兼容性问题)

🔹方式二:(常用)

通过Object.getPrototypeOf方法可以获取到

二、函数的原型 prototype

2.1 概念

所以的函数都有一个prototype的属性(注意:不是__proto__)

(🏷️箭头函数没有原型)

💠虽然函数也是一个对象,但是对象上面没有prototype的属性;

💠因为是函数,所以才有这个特殊的属性;

💠(隐式全都要,显式只有函数有)

2.2 作用

用来构建对象时,给对象设置隐式原型的

2.2.1 new操作符

步骤:1. 在内存中创建一个新的对象(空对象)

           2. 这个对象内部的[ [ prototype ] ] 属性会被赋值为该构造函数的prototype属性

因此,也就意味着通过Person构造函数创建出来的所有对象的[ [ prototype ] ] 属性都指向Person.prototype

function Person() {
    
}
//函数的显式原型
console.log(Person.prototype)

//new操作
var p1 = new Person()
var p2 = new Person()
var p3 = new Person()
//实例的隐式原型  指向  函数的显式原型
console.log(p1.__proto__)
console.log(p1.__proto__ === Person.prototype)  // true
//实例的隐式原型都共同指向构造函数的显式原型
console.log(p1.__proto__ === p3.__proto__)  //true

2.3 constructor属性

函数的显式原型上都会存在一个属性叫做constructor,这个constructor指向当前的函数对象

function Person() {
    
}

console.log(Person.prototype.constructor) // [Function: Person]
console.log(p1.__proto__.constructor)   // [Function: Person]
console.log(p1.__proto__.constructor.name)  // Person

三、面向对象的特征—继承

3.1 面向对象三大特性

封装、继承、多态

  • 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程
  • 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态的前提(纯面向对象中)
  • 多态:不同的对象在执行时表现出不同的形态

3.2 继承是做什么的?

继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可。

继承也是多态的前提。

3.3 JavaScript中如何实现继承

使用JavaScript原型链的机制

四、JavaScript原型链

从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取

五、Object

5.1 Object的原型

那么什么地方是原型链的尽头:Object的原型对象

console.log(obj.__proto__.__proto__.__proto__) //[Object: null prototype] {}

这时会发现打印出来的结果是 [Object: null prototype] {},事实上这个原型就是最顶层的原型了

💠从Object直接创建出来的对象的原型都是 [Object: null prototype] {}

🗯️[Object: null prototype] {} 原型有什么特殊吗?

  • 特殊一:该对象有原型属性,但是它的原型属性已经指向的是null,也就是已经是顶层原型了
  • 特殊二:该对象上有很多默认的属性和方法

5.2 Object是所有类的父类

从上面Object原型可以得出一个结论:原型链最顶层的原型对象就是Object的原型对象

六、实现继承的方式(重点)

JavaScript想要实现继承的目的:重复利用另一个对象的属性和方法

6.1 通过原型链实现继承

主要代码:子类构造函数.prototype = new 父类构造函数()

//1.定义父类构造函数
function Person() {
    this.name = "xiaomimg"
}
//2.父类原型上添加内容
Person.prototype.running = function() {
    cosole.log(this.name + "running~")
}
//3.定义子类构造函数
function Student() {
    this.sno = 111
}
//4.创建父类对象,并且作为子类的原型对象
var p new Person()
Student.prototype = p
//5.在子类原型上添加内容
Student.prototype.studying = function() {
    cosole.log(this.name + "studying~")
}

目前stu的原型是p对象,而p对象的原型是Person默认的原型,里面包含running等函数

🔺注意:步骤4和步骤5不可以调整顺序,否则会有问题

原型链继承的弊端

缺点:某些属性其实是保存在p对象上的

  • 第一,通过直接打印对象是看不到这个属性
  • 第二,这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题
  • 第三,不能给Person传递参数(让每个stu有自己的属性),因为这个对象是一次性创建的(没有办法定制化)

优点:好理解,逻辑清晰,可以继承父类属性和方法

6.2 借用构造函数继承

主要代码:在子类构造函数中使用 call() / apply()方法调用父类构造函数

因为函数可以在任意时刻被调用,所以通过apply()和call()方法也可以在新创建的对象上执行构造函数

function Student(name, friends, sno) {
    Person.call(this, name, friends)
    this.sno = sno
}
Student.prototype = Person.pertotype

优缺点:

优点:可以先父类传参,定制对象,且不会造成属性共享的问题

缺点:

  • 虽然可以继承属性和方法,但方法必须写在构造函数中,创建出来的对象都自带方法,无法进行方法的复用,浪费空间
  • 父类的原型上绑定的属性和方法无法被子类使用

6.3 组合继承(仍不够完美,但是基本上没用问题了)

组合继承 = 原型链继承 + 借用构造函数继承

主要代码:1. 子类构造函数.prototype = new 父类构造函数()

                  2. 在子类构造函数中使用 call | apply 调用父类构造函数

优缺点:

优点:用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承,弥补了各自的缺点

缺点:

  • 组合继承最大的问题就是无论在什么情况下,都会调用两次父类构造函数

(1)一次在创建子类原型的时候

(2)另一次在子类构造函数内部(也就是每次创建子类实例的时候)

  • 所以的子类实例事实上会拥有两份父类的属性

一份在当前的实例自己里面(也就是person本身的),另一份在子类对应的原型对象中(也就是person.__proto__里面)。

🏷️当然,这两份属性我们无需担心访问出现问题,因为默认一定是访问实例本身这一部分的。

  //工具函数
    // function createObject(o) {
    //   function F() {}
    //   F.prototype = o
    //   return new F()
    // }

    //将Subtype和Supertype联系在一起
    //寄生式函数
    function inherit(Subtype, Supertype) {
      //不担心兼容性的写法
      // Subtype.prototype = Object.create(Supertype.prototype)

      Subtype.prototype = createObject(Supertype.prototype)
      Object.defineProperty(Subtype.prototype, "constructor", {
        enumerable: false,
        configurable: true,
        writable: true,
        value: Subtype
      })
    }

    //定义Person的构造函数
    function Person(name, age, height, address) {}
    Person.prototype.running = function() {
      console.log('person running')
    }

    function Student(name, age, height, address, sno, score) {}

    inherit(Student, Person);

    console.log(new Student());

    (new Student()).running()

  /**
   * 满足什么条件
   * 1.必须创建出来一个对象
   * 2.这个对象的隐式原型必须指向父类的显式原型
   * 3.将这个对象赋值给子类的显式原型
  */

  /*
    之前的做法:这个不是我们想要的  不推荐
  */
  // var p = new Person()  // p.__proto__ === Person.prototype
  // Student.prototype = p 

  //方案一:
  // var obj = {}  // __proto__   Object
  // // obj.__proto__ = Person.prototype  (可能存在兼容性问题, 所以用下面的方法)
  // Object.setPrototypeOf(obj, Person.prototype)
  // Student.prototype = obj

  //方案二:
  // 兼容性最好 
  // function F() {}
  // F.prototype = Person.prototype
  // Student.prototype = new F()


  //方案三:
  // var obj = Object.create(Person.prototype)
  // console.log(obj.__proto__ === Person.prototype)  //true
  // Student.prototype = obj

6.4 寄生组合式继承(最终继承方案)

寄生式继承与原型式继承紧密相关的一种思想,并且同样由道格拉斯·克罗克福德(Douglas Crockford)提出和推广的;

寄生式继承的思路就是结合原型类继承和工厂模式的一种方式,即创建一个封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再将这个对象返回

寄生组合式继承 = 原型链继承 + 借用构造函数继承 + 寄生函数

优缺点:

优点:这是最成熟的方法,也是现在库实现的方法

function Person(name, age, height) {
  this.name = name
  this.age = age
  this.height = height
}
Person.prototype.running = function() {
  console.log("running")
}
Person.prototype.eating = function() {
  console.log("eating")
}

function Student(name, age, height, sno, score) {
  Person.call(this, name, age, height)
  this.sno = sno
  this.score = score
}

function createObject(aaa) {
  function F() {}
  F.prototype = aaa
  return new F()
}

function inherit(Subtype, Supertype) {  
  Subtype.prototype = createObject(Supertype.prototype)
  Object.defineProperty(Subtype.prototype, "constructor", {
    enumerable: false,
    configurable: true,
    writable: true,
    value: Subtype
  })
}

inherit(Student, Person)
Student.prototype.studying = function() {
  console.log("studying")
}

var stu1 = new Student("ming", 18, 88, 100, 20)
console.log(stu1)
console.log(stu1.__proto__)
console.log(Student.prototype)
console.log(Person.prototype)

输出结果:

七、原型继承关系

7.1 解释

1. 系统自带的构造函数 function Object()

创建对象无论是采用字面量的形式还是实例化Object构造函数来创建,本质上字面量的形式还是会从Object构造函数来进行创建。

字面量:var obj1 = {}

实例化:var obj2 = new Object()

(1)当执行 var obj2 = new Object() 时,会将构造函数的prototype复制给创建对象的__proto__ ,即 obj2.__proto__ = Object.prototype ,所以图中01,02对象的__proto__指向Object.prototype。

(2)Object构造函数原型就是本身的原型 Object.prototype

Object.prototype中的constructor指向本身构造函数Object

(3)因为Object.prototype是原型对象,是对象就会有__proto__,又因为它是最顶层的,所有对象的起源都源自于它,在它之上就没有原型了,所以它的__proto__指向null

2. 自己定义的构造函数 function Foo()

(1)由1(1)可知,图中f1,f2对象的__proto__指向 Foo.prototype

(2)prototype与constructor指向同1(2)

(3)因为函数既是函数也是对象,所以是对象的话就会有__proto__,是函数的话就会有prototype。又在这个函数被创建的时候,函数里面就会创建一个叫prototype的属性,所以会产生Foo.prototype。又它的值是一个对象,所以它内部创建肯定是通过实例化了Object构造函数创建所得。所以它的指向毫无疑问是指向Object.prototype

Foo.prototype = new Object()

Foo.prototype.__proto__ = Object.prototype

3. 系统自带的构造函数 function Function()

创建函数也可以通过创造函数来创建,和创建对象原理一样

var fn = new Function()

(1)Function的prototype和constructor指向同1(2)一致

(2)Function.prototype指向Object.prototype和2(3)中Foo.prototype指向原理一致

作为函数来说,有自己的显式原型prototype对象,显式原型对象相当于是被new Object() 创建出来的,所以又有自己的__proto__,指向Object的显式原型 Object.prototype

4. 构造函数本身的__proto__指向

对象的隐式原型指向构造函数的显式原型

实例对象 -> new 构造函数

函数对象 -> new Function

原型对象 -> new Object

(1)Foo函数的__proto__ -> Function.prototype

function Foo() {} ===> var Foo = new Function()

所以执行上面代码时,会执行 Foo.__proto__ = Function.prototype

相当于调用了构造函数来创建,所以 Foo()的__proto__ -> Function.prototype

(2)Object函数的__proto__ -> Function.prototype

原理同(1)

function Object() {} ===> var Object = new Function

所以执行上面代码时,会执行 Object.__proto__ = Function.prototype

(3)Function函数的__proto__ -> Function.prototype

相当于Function.__proto__ === Function.prototype (只有这一个满足!)

function Function() {} ===> var Function = new Function()

所以执行上面代码时会执行 Function.__proto__ === Function.prototype

作为对象来说,有自己的隐式原型__proto__对象,比较特殊的事,相当于它被自己创建了,即function Function相当于被 new Function() 创建的,所以function Function().__proto__ 也指向Function函数的显式原型 Function.prototype

7.2 图解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值