一文详解如何用原型链的方式实现JS继承

目录
  • 原型链是什么
  • 通过构造函数创建实例对象
  • 用原型链的方式实现继承
    • 方法1:Object.create
    • 方法2:直接修改 [[prototype]]
    • 方法3:使用父类的实例
  • 总结

今天讲一道经典的原型链面试题。

原型链是什么

JavaScript 中,每当创建一个对象,都会给这个对象提供一个内置对象 [[Prototype]] 。这个对象就是原型对象,[[Prototype]] 的层层嵌套就形成了原型链。

当我们访问一个对象的属性时,如果自身没有,就会通过原型链向上追溯,找到第一个存在该属性原型对象,取出对应值。

当然原型链不是无止境的,和单链表一样,最后一个原型对象的值是 null,原型链的所有对象都找不到指定的属性时,我们会拿到 undefined。

[[Prototype]] 虽然无法通过脚本进行访问,但大多数浏览器提供了 __proto__ 属性来访问这个内置对象,但它并不是标准,无法兼容所有浏览器。

下面来举几个例子,让读者对原型链有一个直观的认识:

  • 通过对象字面量声明 a = {} 时, a 的 [[prototype]] 就是 Object.prototype。此时的原型链是:a -> Object.prototype -> null。这里有个易错点,就是以为 a 的上一个原型对象是 Object,其实并不对,Object 其实只是一个构造函数。
  • 声明数组 arr = [1, 2, 4],它的原型链则是 arr -> Array.prototype -> Object.prototype -> null。
  • Object.create(null) 甚至能够创建一个连 [[prototype]] 都没有的真正的空对象,一般用于做字符串哈希表,比如 vue 源码里就能经常看到。

通过构造函数创建实例对象

在 JavaScript 中,一个函数会在 new 关键字的配合下成为构造函数。也就是说,任何一个函数都可以成为构造函数。

当声明一个构造函数时,它会有一个属性名为 prototype 的对象(和 [[prototype]] 是不同的东西),这个对象就是 原型对象。这个对象的 constructor 又反过来指向构造函数。

当我们对使用 new 关键字创建对象,被创建的对象的 [[prototype]] 会指向这个 prototype。

function Rect() {}
const rect = new Rect()
rect.__proto__ === Rect.prototype // true
Rect.prototype.constructor === Rect // true

只要是通过 new Rect() 创建的对象,无论多少次,它的 [[prototype]] 都是指向 Rect.prototype。另外,Rect.prototype.prototype 指向的是 Object.prototype。

这样,通过给构造函数的原型对象(Rect.prototype)添加一些方法(如 Rect.prototype.draw),就能让创建的多个实例对象共享同一个方法,减少内存的使用。

用原型链的方式实现继承

理解了构造函数如何影响创建的实例的原型链后,我们来探讨一下核心问题,如何使用原型链来实现继承。

假设我们有一个 Shape 构造函数(父类)和 Rect 构造函数(子类)。代码如下:

// 父类
function Shape() {}
Shape.prototype.draw = function() {
  console.log('Shape Draw')
}
Shape.prototype.clear = function() {
  console.log('Shape Clear')
}
// 子类
function Rect() {}
/**
 实现继承的代码放这里
**/
Rect.prototype.draw = function() {
  console.log('Rect Draw')
}

通过前面的学习,我们知道,正常情况下使用 new Rect 创建的实例对象,它的原型链是这样的:

rect -> Rect.prototype -> Object.protoype -> null

现在我们要实现的继承,其实就是在原型链中间再加一个原型对象 Shape.prototype。对此我们需要对 Rect.prototype 进行特殊的处理。

方法1:Object.create

Rect.prototype = Object.create(Shape.prototype)
Rect.prototype.constructor = Rect // 选用,如果要用到 constructor

Rect.prototype.constructor = Rect // 选用,如果要用到 constructor

Object.create(proto) 是个神奇的方法,它能够创建一个空对象,并设置它的 [[prototype]] 为传入的对象。

因为我们无法通过代码的方式给 [[prototype]] 属性赋值,所以使用了 Object.create 方法作为替代。

因为 Rect.prototype 指向了另一个新的对象,所以把 constructor 给丢失了,可以考虑把它放回来,如果你要用到的话。

缺点是替换掉了原来的对象。

方法2:直接修改 [[prototype]]

如果就是不想使用新对象,只想修改原对象,可以使用 废弃 的 __proto__ 属性,但不推荐。

不过另外还有一个方法 Object.setPrototypeOf() 可以修改对象的 [[prototype]],但因为性能的问题,也不推荐使用。

Object.setPrototypeOf(Rect.prototype, Shape.prototype)
// 或
Rect.prototype.__proto__ = Shape.prototype

都不推荐使用,但确实能用。

方法3:使用父类的实例

Rect.prototype = new Shape()

形成的原型链为:

rect -> shape(替代掉原来的 Rect.prototype) -> Shape.prototype -> Object.prototype -> null

基本能用,缺点是会产生副作用,就是执行 new Shap() 可能会出现副作用,比如给创建的对象添加了一些属性、发送了请求之类的,完全取决于构造函数内的代码。

某种意义上,这个缺点是致命的。不推荐使用。

总结

用原型链的方式实现一个 JS 继承,其实就是希望构造函数 Son 创建出来的对象 son,它的原型链上加上父类 Parent.prototype,所以最后就是要修改 Son.prototype 的 [[prototype]]。

鉴于性能、兼容性、副作用等考虑,推荐使用方法 1,即通过 Object.create(Parent.prototype) 创建一个指定了 [[prototype]] 的新对象,替换掉原来的 Son.prototype 指向的对象。

总结几个核心知识点:

  • 任何对象都有 [[prototype]] 属性,读写对象属性发现当前对象不存在时,会访问 [[prototype]] 指向的对象尝试访问属性,于是原型链形成了。
  • 函数创建时,它的 prototype 属性会拿到一个原型对象。当函数作为构造函数,通过 new 创建一个新对象时,这个新对象的 [[prototype]] 会指向这个原型对象。
  • JS 要实现 “类” 继承,本质是通过处理构造函数的 prototype 对象来修改原型链。

以上就是一文详解如何用原型链的方式实现JS继承的详细内容,更多关于JS 原型链 继承的资料请关注我们其它相关文章!

(0)

相关推荐

  • js原型链与继承解析(初体验)

    首先定义一个对象obj,该对象的原型为obj._proto_,我们可以用ES5中的getPrototypeOf这一方法来查询obj的原型,我们通过判断obj的原型是否与Object.prototype相等来证明是否存在obj的原型,答案返回true,所以存在.然后我们定义一个函数foo(),任何一个函数都有它的prototype对象,即函数的原型,我们可以在函数的原型上添加任意属性,之后通过new一个实例化的对象可以共享其属性(下面的两个例子会详细介绍). function foo(){} fo

  • 深入理解JS继承和原型链的问题

    对于那些熟悉基于类的面向对象语言(Java 或者 C++)的开发者来说,JavaScript 的语法是比较怪异的,这是由于 JavaScript 是一门动态语言,而且它没有类的概念( ES6 新增了class 关键字,但只是语法糖,JavaScript 仍旧是基于原型). 涉及到继承这一块,Javascript 只有一种结构,那就是:对象.在 javaScript 中,每个对象都有一个指向它的原型(prototype)对象的内部链接.这个原型对象又有自己的原型,直到某个对象的原型为null 为止

  • 一篇文章让你看懂Js继承与原型链

    目录 继承与原型链 继承属性 继承方法 在 JavaScript 中使用原型 性能 附:原型链是实现继承的主要方法 总结 继承与原型链 当谈到继承时,JavaScript 只有一种结构:对象.每个实例对象(object)都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype).该原型对象也有一个自己的原型对象(proto),层层向上直到一个对象的原型对象为 null.根据定义,null 没有原型,并作为这个原型链中的最后一个环节. 几乎所有 JavaScript

  • JavaScript基于原型链的继承

    Javascript并不是一门面向对象的语言,没有提供传统的继承方式,但是它提供了一种原型继承的方式,利用自身提供的原型属性来实现继承. 原型链是JavaScript中继承的主要方法. 原型链的基本思想是:利用原型让一个引用类型继承另一个引用类型的属性和方法. 构造函数.原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针. 如果让原型对象等于另一个对象的实例,这样原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中

  • 浅谈javascript原型链与继承

    js原型链与继承是js中的重点,所以我们通过以下三个例子来进行详细的讲解. 首先定义一个对象obj,该对象的原型为obj._proto_,我们可以用ES5中的getPrototypeOf这一方法来查询obj的原型,我们通过判断obj的原型是否与Object.prototype相等来证明是否存在obj的原型,答案返回true,所以存在.然后我们定义一个函数foo(),任何一个函数都有它的prototype对象,即函数的原型,我们可以在函数的原型上添加任意属性,之后通过new一个实例化的对象可以共享

  • JavaScript原型链与继承操作实例总结

    本文实例讲述了JavaScript原型链与继承操作.分享给大家供大家参考,具体如下: 1. JavaScript继承 JavaScript继承可以说是发生在对象与对象之间,而原型链则是实现继承的主要方法: 1.1 原型链 利用原型让一引用类型继承另一个引用类型的属性和方法. 构造函数中有个prototype(每个函数中都有),指向他的原型对象,每个原型对象中也有一个constructor属性,指向原构造函数.通过构造函数创建的新对象中都有一个无法直接访问的[[proto]]属性,使得对象也指向构

  • 一文详解如何用原型链的方式实现JS继承

    目录 原型链是什么 通过构造函数创建实例对象 用原型链的方式实现继承 方法1:Object.create 方法2:直接修改 [[prototype]] 方法3:使用父类的实例 总结 今天讲一道经典的原型链面试题. 原型链是什么 JavaScript 中,每当创建一个对象,都会给这个对象提供一个内置对象 [[Prototype]] .这个对象就是原型对象,[[Prototype]] 的层层嵌套就形成了原型链. 当我们访问一个对象的属性时,如果自身没有,就会通过原型链向上追溯,找到第一个存在该属性原

  • 一文详解typeScript的extends关键字

    目录 前言 extends 的几个语义 extends 与 类型组合/类继承 extends 与类型约束 extends 与条件类型 extends 与 {} extends 与 any extends 与 never extends 与 联合类型 extends 判断类型严格相等 extends 与类型推导 总结 前言 声明: 以下文章所包含的结论都是基于 typeScript@4.9.4 版本所取得的. extends 是 typeScript 中的关键字.在 typeScript 的类型编

  • 详解JavaScript中的链式调用

    链模式 链模式是一种链式调用的方式,准确来说不属于通常定义的设计模式范畴,但链式调用是一种非常有用的代码构建技巧. 描述 链式调用在JavaScript语言中很常见,如jQuery.Promise等,都是使用的链式调用,当我们在调用同一对象多次其属性或方法的时候,我们需要多次书写对象进行.或()操作,链式调用是一种简化此过程的一种编码方式,使代码简洁.易读. 链式调用通常有以下几种实现方式,但是本质上相似,都是通过返回对象供之后进行调用. this的作用域链,jQuery的实现方式,通常链式调用

  • 一文详解 OpenGL ES 纹理颜色混合的方法

    目录 一.混合API 二.参数含义 2.1 举个栗子 2.2 参数含义 三. 几种常用混合方式效果 3.1 混合(GL_ONE, GL_ZERO) 3.2 混合(GL_ONE, GL_ONE) 3.3 混合(GL_ONE, GL_ONE_MINUS_DST_ALPHA) 3.4 混合 (GL_SRC_ALPHA, GL_ONE) 3.5 混合 (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) 在OpenGL中绘制的时候,有时候想使新画的颜色和已经有的颜色按照一定的方式

  • 一文详解JS私有属性的6种实现方式

    目录 _prop Proxy Symbol WeakMap #prop ts private 总结 class 是创建对象的模版,由一系列属性和方法构成,用于表示对同一概念的数据和操作. 有的属性和方法是对外的,但也有的是只想内部用的,也就是私有的,那怎么实现私有属性和方法呢? 不知道大家会怎么实现,我梳理了下,我大概用过 6 种方式,我们分别来看一下: _prop 区分私有和公有最简单的方式就是加个下划线 _,从命名上来区分. 比如: class Dong { constructor() {

  • 一文详解Spring Security的基本用法

    目录 1.引入依赖 2.用户名和密码在哪里设置 3.UserDetailsService接口详解 3.1JdbcDaoImpl实现类 3.2InMemoryUserDetailsManager实现类 3.3自定义实现类实现UserDetailsService接口 4.如何修改登录页面 Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架, 提供了完善的认证机制和方法级的授权功能.是一款非常优秀的权限管理框架.它的核心是一组过滤器链,不同的功能经由不同的过滤器. 今天通

  • 一文详解Java中Stream流的使用

    目录 简介 操作1:创建流 操作2:中间操作 筛选(过滤).去重 映射 排序 消费 操作3:终止操作 匹配.最值.个数 收集 规约 简介 说明 本文用实例介绍stream的使用. JDK8新增了Stream(流操作) 处理集合的数据,可执行查找.过滤和映射数据等操作. 使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询.可以使用 Stream API 来并行执行操作. 简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式. 特点 不是数据结构

  • 一文详解Golang中net/http包的实现原理

    目录 前言 http包执行流程 http包源码分析 端口监听 请求解析 路由分配 响应处理 前言 Go语言自带的net/http包提供了HTTP客户端和服务端的实现,实现一个简单的http服务非常容易,其自带了一些列结构和方法来帮助开发者简化HTTP服务开发的相关流程,因此我们不需要依赖任何第三方组件就能构建并启动一个高并发的HTTP服务器,net/http包在编写web应用中有很重要的作用,这篇文章会学习如何用 net/http 自己编写实现一个 HTTP Server 并探究其实现原理,具体

  • 一文详解JavaScript闭包典型应用

    目录 1.应用 1.1 模拟私有变量 1.2 柯里化 1.3 偏函数 1.4 防抖 1.5 节流 2.性能问题 2.1 内存泄漏 2.2 常见的内存泄漏 3.闭包与循环体 3.1 这段代码输出啥 3.2 改造方法 4.总结 1.应用 以下这几个方面是我们开发中最为常用到的,同时也是面试中回答比较稳的几个方面. 1.1 模拟私有变量 我们都知道JS是基于对象的语言,JS强调的是对象,而非类的概念,在ES6中,可以通过class关键字模拟类,生成对象实例. 通过class模拟出来的类,仍然无法实现传

  • 一文详解Golang的中间件设计模式

    目录 背景 Demo 验证结论 背景 记录一下自己在go开发和学习上的一些笔记 最近在看一些rpc框架的使用原理和源码的时候,对中间件的实现非常感兴趣,然后也看了一下grpc的中间件的用法,也看了别的框架的中间件的设计,感觉grpc的还算是比较容易弄懂,于是记录一下这个常用中间件的实现的一个原理的demo(吐槽一下其他的rpc框架分为inbound和outbound的middleware感觉好像有点复杂化了,所以我也不知道哪种设计会比较好,楼主是java出身,所以对反射走aop的那种模式比较熟悉

随机推荐