首页 > 其他分享 >深入原型链与继承(详解JS继承原理)

深入原型链与继承(详解JS继承原理)

时间:2022-12-20 19:58:32浏览次数:73  
标签:__ 继承 JS VIP 详解 原型 new prototype 构造函数

目录

原型链与继承


new 关键字的执行过程

让我们回顾一下,this 指向里提到的new关键字执行过程。

  1. 创建一个新的空对象
  2. 将构造函数的原型赋给新创建对象(实例)的隐式原型
  3. 利用显式绑定将构造函数的 this 绑定到新创建对象并为其添加属性
  4. 返回这个对象

手写new关键字的执行过程:

function myNew(fn, ...args) { // 构造函数作为参数
    let obj = {}
    obj.__proto__ = fn.prototype
    fn.apply(obj, args)
    return obj
}

这里提到了__proto__ prototype:前者被称为隐式原型,后者被称为显式原型


构造函数、实例对象和原型对象

三者的概念

构造函数:用于生成实例对象。构造函数可分为两类:

  • 自定义构造函数:function foo () {}
  • 原生构造函数:function Function () {}function Object () {}

原型对象:每个构造函数都有自己的原型对象,可通过prototype访问。

实例对象:可由构造函数通过new关键字生成的对象。

三者的关系

构造函数可以通过prototype访问其原型对象,而原型对象可通过constructor访问其构造函数。构造函数可通过new关键字创建实例对象,实例对象可通过__proto__ 访问其原型对象。

我们来看一段代码的输出结果:

function Foo(name, age) {
    this.name = name
    this.age = age
}
let a = new Foo('小明', 22)
console.log('构造函数:', Foo)
console.log('原型对象', Foo.prototype)
console.log('实例对象', a)
// 可以输出一下,看看它们都是什么样子

可以看出实例对象内部的第一个[[Prototype]]的展开内容等于原型对象的展开内容,可构建一个等式如下:

// 实例对象可通过 __proto__ 访问其原型对象
a.__proto__ === Foo.prototype // true
// 原型对象可通过 constructor 访问其构造函数
Foo.prototype.constructor === Foo // true

原型链的概念及图解

来看一张关于原型链的经典图:

上面这张图的箭头乍一看能让人头疼,我们对图中的元素进行分类并划分层次,可有以下三层:

第一层__proto__指向:实例对象

  1. 通过构造函数生成的实例对象
// 生成实例对象
function Foo() {}
let obj1 = new Foo()

// __proto__指向验证
obj1.__proto__ === Foo.prototype // true
  1. 通过new Object()对象字面量生成的实例对象
// 生成实例对象
let obj2 = new Object()

// __proto__指向验证
obj2.__proto__ === Object.prototype // true
  1. 通过functionclass声明生成的实例对象
// 生成实例对象
function Foo(){}
// 原生构造函数
// function Function(){}
// function Object(){}

// __proto__指向验证
Foo.__proto__ === Function.prototype // true
Function.__proto__ === Function.prototype // true
Object.__proto__ === Function.prototype // true

说明:其实我们自己定义的函数也是由Function构造函数生成的实例对象。

第二层__proto__指向:Function.prototypeFoo.prototype

Foo.prototype.__proto__ === Object.prototype // true

Function.prototype.__proto__ === Object.prototype // true

第三层__proto__指向:Object.prototype

Object.prototype.__proto__ === null // true

我们自己再画一张图看一下:

自底向上有三层的__proto__构成基本的JavaScript原型模式生态,最后再总结一下规则:

  1. 实例对象都会指向其构造函数原型
  2. 构造函数原型都会指向Object.prototype
  3. Object.prototype最终指向null

总结:其实我们的原型链指的就是__proto__的路径。

注意:这里只是为了原型链能更加直观,请不要忘了构造函数原型的constructor属性,它会指回对应的构造函数。


原型链继承

我们利用任务驱动型的方法去学习继承方式,考虑这样一个类结构:

  1. 普通用户:作为父类
  2. VIP 用户:作为子类

说明:VIP用户需要继承普通用户。其中,VIP用户的武器列表可以添加屠龙宝刀。

原型链搜索机制:若要访问当前对象所没有的属性和方法,则会首先以当前对象为起点沿着原型链__proto__向上寻找每个对象内部的属性和方法。直到找到对应的属性和方法,没有则会直接走到原型链尽头null

来看这样一段代码:

function USER(username, password) {
    this.username = username
    this.password = password
    this.weapon = ['水果小刀']
}

VIP.prototype = new USER() // 为什么要放到中间?
// 注意:改写原型,要记得把 constructor 指会原构造函数
VIP.prototype.constructor = VIP

function VIP() { }
VIP.prototype.addWeapon = function (weaponName) {
    this.weapon.push(weaponName)
}

let a = new VIP('小明')
// 缺陷1:无法给父类构造函数传参,只能在 VIP 中自行添加相应参数。无法实现父类属性重用

let b = new VIP()
b.addWeapon('屠龙宝刀')
console.log(b.weapon)

let c = new VIP()
console.log(c.weapon)
// 缺陷2: 我们想要单独给实例 b 的武器列表添加一把屠龙宝刀,结果是实例 c 的武器列表也会增加屠龙宝刀

原型链__proto__实现继承会经过的对象(从子类实例到父类原型):

  1. 子类构造函数实例 :new VIP()

  2. 父类构造函数实例new USER()

  3. 父类构造函数原型 :USER.prototype

我们可以构建两个表达式去验证:

new VIP().__proto__ === new USER() // true
new USER().__proto__ === USER.prototype // true

再进一步提炼以上两个表达式,可获得最终表达式。以下为实现继承关键的完整原型链:

// VIP 构造函数所生成的实例会经过两层__proto__找到父类原型
new VIP().__proto__.__proto__ === USER.prototype
// 接下来,由 VIP 构造函数生成的实例所没有的属性和方法,都会去父类原型找到属性和方法。

原型链继承缺点:

  1. 父类原型中若存在的引用值则会在所有实例间共享。
  2. 子类构造函数在实例化时不能给父类构造函数传参,即我们的父类属性无法重用。

为什么 VIP.prototype = new USER() 这一步要放到两个构造函数中间?

如果这一步表达式放到后面,我们的VIP.prototype是其原本构造函数 VIP 的原型。在这个原本的构造函数原型上添加方法,不会有继承效果。

我们的想法是通过父类构造函数生成实例,利用它实例的__proto__去实现继承效果。要想在子类构造函数添加方法,我们实际做了这样的操作。如下:

// new USER()就是我们父类构造函数生成的实例
new USER().addWeapon = function (weaponName) {
    this.weapon.push(weaponName)
}

但是上面这样会出现问题,我们子类构造函数怎么办?他想new一个实例,还是会根据原来的原型。

因此,我们需要将new USER()传递给VIP.prototype。这样VIP构造函数生成实例才会有继承效果,如下:

VIP.prototype = new USER() // 传递__proto__实现继承

VIP.prototype.addWeapon = function (weaponName) {
    this.weapon.push(weaponName)
}

盗用构造函数

来看这样一段代码:

function USER(username, password) {
    this.username = username
    this.password = password
    this.weapon = ['水果小刀']
}

function VIP(username, password) {
    USER.call(this, username, password) // 调用父类构造函数,为其属性赋值
} // 这里的 this 指向子类构造函数生成的新实例

// 1. 接下来我们可以向父类构造函数传参
let a = new VIP('小红')
console.log(a.username) // 小红

// 2. 也可以解决引用值产生的问题
let b = new VIP()
b.weapon.push('屠龙宝刀')
console.log(b.weapon) // ['水果小刀', '屠龙宝刀']

let c = new VIP()
console.log(c.weapon) // ['水果小刀']
// 这样实例 b 和 c 的武器列表的数据都是独立的

过程解析:new VIP('小红')传入了一个“小红”参数。

第一次绑定操作new执行过程会执行一次绑定操作,将this指向实例对象。

第二次绑定操作:VIP构造函数内部的call方法再次绑定实例对象,调用父类构造函数

总结:我们通过传参实际调用了两次绑定操作,最终使得子类构造函数的新实例也能拥有父类的属性和值。

盗用构造函数缺点:

  1. 只能在构造函数内部定义方法使用,不能访问父类原型定义的方法。即我们的父类方法不能重用

组合继承( = 原型链继承 + 盗用构造函数 )

如果你已经清楚的知道上面两种继承方式的优点和缺陷,那么我们可以利用1 + 1 > 2 的方法实现组合继承。

function USER(username, password) {
    this.username = username
    this.password = password
    this.weapon = ['水果小刀']
}

VIP.prototype = new USER()
// 注意:改写原型,要记得把 constructor 指会原构造函数
VIP.prototype.constructor = VIP

function VIP(username, password) {
    USER.call(this, username, password) // 调用父类构造函数,为其属性赋值
} // 这里的 this 指向子类构造函数生成的新实例
VIP.prototype.addWeapon = function (weaponName) {
    this.weapon.push(weaponName)
}

// 我们尝试给父类构造函数传参
let a = new VIP('小红')
console.log(a.username) // 小红

// 看看添加屠龙宝刀,有没有相互影响
let b = new VIP()
b.addWeapon('屠龙宝刀')
console.log(b.weapon)

let c = new VIP()
console.log(c.weapon)

以上的组合继承方式输出了正确的答案,算是完美解决了原型链继承和盗用构造函数继承出现的问题。我们将以上代码放入浏览器打断点分析。如下:

很明显,我们第一次new USER()会调用父类构造函数,而后子类构造函数每一次生成新实例都会调用父类构造函数。也就是说,多了第一次会调用父类构造函数的情况。


原型继承

在 JavaScirpt 高级程序设计 8.3.4 中提到了这种方式,来看这样一段代码

function object(obj) {
    function Fn() { }
    Fn.prototype = obj
    return new Fn() // 返回一个空函数,其内部原型改写为 obj
}

有没有熟悉的感觉,其实正是我们之前手写bind函数利用的继承方法。与ES6中的Object.create()方法效果相同。它适于在原有对象的基础上再克隆一个对象。此外,对象属性值若为原始值则可以进行改写,若为引用值则会产生引用值的特点。即多个克隆对象会共享同一个引用值,也就是说这个“克隆”操作相当于我们的浅拷贝操作。


寄生继承

function createAnother(obj) {
    let clone = object(obj)
    clone.sayHello = () => {
        console.log('Hello World')
    }
}

这种方式可以使克隆对象在原基础上增强,即添加属性和方法,

注意:原型继承和寄生继承都重点关注对象的使用,而不考虑构造函数的使用


寄生组合继承( = 组合继承 + 原型继承 + 寄生继承 )

我们可以再利用浏览器打断点试试,是不是不会发生像组合继承那样首次调用构造函数的情况。

// 寄生组合继承
function inheritPrototype(subType, superType) {
    subType.prototype = Object.create(superType.prototype) // 创建对象
    subType.prototype.constructor = subType // 增强对象
}

function USER(username, password) {
    this.username = username
    this.password = password
    this.weapon = ['水果小刀']
}

// 验证表达式时,下面这一条语句要加上注释。
inheritPrototype(VIP, USER) // 调用继承函数,

// 验证表达式时,把下面这两条语句注释去掉。
// VIP.prototype = Object.create(USER.prototype) // 创建对象
// VIP.prototype.constructor = VIP // 增强对象

function VIP(username, password) {
    USER.call(this, username, password) // 调用父类构造函数,为其属性赋值
} // 这里的 this 指向子类构造函数生成的新实例
VIP.prototype.addWeapon = function (weaponName) {
    this.weapon.push(weaponName)
}

// 我们尝试给父类构造函数传参
let a = new VIP('小红')
console.log(a.username) // 小红

// 看看添加屠龙宝刀,有没有相互影响
let b = new VIP()
b.addWeapon('屠龙宝刀')
console.log(b.weapon)

let c = new VIP()
console.log(c.weapon)

原型链__proto__实现继承会经过的对象(从子类实例到父类原型):

  1. 子类构造函数实例 :new VIP()
  2. 空构造函数实例Object.create(USER.prototype)
  3. 父类构造函数原型 :USER.prototype

我们同样构建两个表达式去验证:

new VIP().__proto__ === Object.create(USER.prototype) // true
Object.create(USER.prototype).__proto__ === USER.prototype // true

再进一步提炼以上两个表达式,可获得最终表达式。以下为实现继承关键的完整原型链:

new VIP().__proto__.__proto__ === USER.prototype // true
// 接下来,由 VIP 构造函数生成的实例所没有的属性和方法,都会去父类原型找到属性和方法。

原型链和寄生组合的继承区别比较

原型链的继承实现:利用new USER()作为跳板实现继承。

VIP.prototype = new USER() // 传递__proto__实现继承
new VIP().__proto__ === new USER() // true
new USER().__proto__ === USER.prototype // true

寄生组合的继承实现:利用Object.create(USER.prototype)作为跳板实现继承。

VIP.prototype = Object.create(USER.prototype) // 传递__proto__实现继承
new VIP().__proto__ === Object.create(USER.prototype) // true
Object.create(USER.prototype).__proto__ === USER.prototype // true

注意:Object.create(USER.prototype)会返回一个空函数实例,这个实例的__proro__指向USER()构造函数。


class继承(ES6 语法)( ≈ 寄生组合继承 )

在 ES5 之前我们都是利用构造函数实现面向对象编程。ES6 的class作为语法糖,其实内部也是利用了构造函数实现面向对象编程。

class USER {
    constructor(username, password) {
        this.username = username
        this.password = password
        this.weapon = ['水果小刀']
    }
}
class VIP extends USER {
    constructor(username, password) {
        super(username, password) // 调用父类构造函数,相当于执行 call 方法
    }
    addWeapon(weaponName) {
        this.weapon.push(weaponName)
    }
}

// 我们尝试给父类构造函数传参
let a = new VIP('小红')
console.log(a.username) // 小红

// 看看添加屠龙宝刀,有没有相互影响
let b = new VIP()
b.addWeapon('屠龙宝刀')
console.log(b.weapon)

let c = new VIP()
console.log(c.weapon)

// 以上同样可运行我们的测试代码

总结 JavaScript 继承方式:

  1. 原型链继承
  2. 盗用构造函数继承
  3. 组合继承( = 原型链继承 + 盗用构造函数 )
  4. 原型式继承
  5. 寄生继承
  6. 寄生组合继承( = 组合继承 + 原型继承 + 寄生继承 )
  7. class继承( ≈ 寄生组合继承 )

以上可以看出 JavaScript 对与继承方式的优化是一个多次迭代不断优化的过程。


参考

JavaScript高级程序设计(第4版)

标签:__,继承,JS,VIP,详解,原型,new,prototype,构造函数
From: https://www.cnblogs.com/chscript/p/16994957.html

相关文章

  • JS数组和字符串方法(API总结与应用)
    目录ArrayAPI静态方法数组首尾元素处理数组遍历(重要)数组查找数组过滤(重要)数组合并数组删除与截取数组排序StringAPI字符串查找与匹配字符串替换字符串合并字符串首尾空格......
  • JS值和类型(必学知识点总结)
    目录值和类型八种数据类型原始值和引用值访问对象的方式相等与全等运算符typeof和instanceof深拷贝与浅拷贝值和类型八种数据类型undefined、null、boolean、number......
  • JS闭包和作用域(必学知识点总结)
    目录闭包和作用域变量声明变量和函数的声明提升作用域和作用域链执行上下文闭包垃圾回收机制闭包和作用域变量声明var声明特点在使用var声明变量时,变量会被自动添......
  • JS混淆加密的作用
    在软件开发过程中,有时会使用代码混淆技术来使代码难以被阅读或破解。这种技术通常被用于防止恶意使用或盗用代码。在JavaScript中,有许多工具可以用来混淆代码,例如Google......
  • vue.nextTick()方法的使用详解
    1,什么是Vue.nextTick()理解:nextTick(),是将回调函数延迟在下一次dom更新数据后调用,简单的理解是:当数据更新了,在dom中渲染后,自动执行该函数,1<template>2<divclass......
  • Docker daemon.json 的配置项目合集
    vim/etc/docker/daemon.json{"authorization-plugins":[],"data-root":"",#设置docker运行时的根目录"dns":[],#设置容器的DNS地址......
  • JS 的 9 种作用域
    作用域想必大家都知道,就是变量生效的范围,比如函数就会生成一个作用域,声明的变量只在函数内生效。而这样的作用域一共有9种,其中几种绝大多数前端都说不出来。下面我们就......
  • 记录--可视化大屏-用threejs撸一个3d中国地图
    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助不想看繁琐步骤的,可以直接去github下载项目,如果可以顺便来个star哈哈本项目使用vue-cli创建,但不影响使......
  • 开关量、模拟量、脉冲量分不清楚?PLC最全编程算法详解,看完彻底懂了!
    PLC中无非就是三大量:开关量、模拟量、脉冲量。只在搞清楚三者之间的关系,你就能熟练的掌握PLC了。开关量的计算1、开关量也称逻辑量,指仅有两个取值,0或1、ON或OFF。它是最......
  • .Net7 自动拷贝appsettings.json到debug文件下
    IDERider在配置json时遇到路径的问题Theconfigurationfile'appsettings.json'wasnotfoundandisnotoptional.TheexpectedphysicalpathwasIConfiguration......