Gahing's blog Gahing's blog
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Gahing / francecil

To be best
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 前端基础

    • 编程语言

      • CSS

      • HTML

      • JavaScript

        • ECMAScript6入门
        • JS 手写题

        • JS技巧
        • JS 学习笔记
        • Promise then 原理分析
        • async 函数编译原理
        • 你不知道的JavaScript(上)
        • 再谈闭包
        • 浏览器剪切板协议
        • 前端实现相对路径转绝对路径的几种方法
        • 为什么 0.._ 等于 undefined
        • 前端项目中常用的位操作技巧
        • 如何利用前端剪切板实现文件上传
        • 快速理解 JS 装饰器
        • 趣味js-只用特殊字符生成任意字符串
        • 重学 JS 原型链
          • 前言
          • 原型对象 prototype
          • \_proto_
          • 原型链
          • 继承
            • class
            • 原型链继承法
            • 借用构造函数法
            • 组合继承法
            • 寄生组合式继承
          • 参考
        • 面试官问:怎么避免函数调用栈溢出
      • Rust

      • TypeScript

      • WebAssembly

    • 开发工具

    • 前端调试

    • 浏览器原理

    • 浏览器生态

  • 应用框架

  • 工程能力

  • 应用基础

  • 专业领域

  • 业务场景

  • 大前端
  • 前端基础
  • 编程语言
  • JavaScript
gahing
2019/12/31
目录

重学 JS 原型链

# 前言

不考虑箭头函数等特异函数,本文所说的函数都是指的构造函数

之前对原型,原型链,构造函数等都是零零散散的知识碎片,这次写个文章记录一下

# 原型对象 prototype

创建一个函数 F 时,会自动为该函数 F 创建一个 prototype 属性,指向一个原型对象 F.prototype

默认情况下,函数 F 的原型对象 F.prototype 会自动获得一个 constructor (构造函数)属性,并指向 F

F.prototype.constructor === F // true
1

而原型对象也是对象,可以对其添加属性和方法

# __proto__

每个对象都有一个 __proto__ 属性,指向生成该对象的构造函数的原型对象

如 F.prototype 原型对象是 Object 的一个实例, __proto__ 指向 Object.prototype

F.prototype.__proto__ === Object.prototype // true
1

Object.prototype 这个原型对象也是对象,那么其 __proto__ 指向哪?其实指向的是 null

Object.prototype 为万物起源

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

当实例化 F 得到对象 f 时,f 的 __proto__ 属性指向 F 的原型对象 F.prototype

const f = new F()
f.__proto__ === F.prototype // true
1
2

这个指向再创建后就确定了,后续修改 F 的原型对象指向也没有影响

function F(){}
const f1 = new F
F.prototype = {}
const f2 = new F
// 指向不一致
f1.__proto__ !== f2.__proto__ // true
// 新的原型对象 {} 没有构造函数,可以为其赋值
F.prototype.constructor !== F // true
1
2
3
4
5
6
7
8

# 原型链

上述的关系,通过原型串接起来,可以得到下面这个图

image

如果问什么是原型,就这么回答:

每个构造函数有个原型对象
每个实例对象有个 __proto__ 属性,指向该实例的构造函数的原型对象
1
2

对对象属性或方法的访问,采用的是原型链搜索,先搜索对象实例 f 自身,没找到的话,搜索原型对象,再没找到,搜素该原型对象的原型对象,直到不存在原型对象

比如访问 f.name 将按以下顺序搜索值

f.name
f.__proto__.name / F.prototype.name
f.__proto__.__proto__.name / Object.prototype.name
1
2
3

判断实例 f 中是否含有 name 属性,可以通过以下方法

f.hasOwnProperty("name")
1

判断原型中是否含有 name 属性,可以通过以下方法

!f.hasOwnProperty("name") && "name" in f
1

# 继承

# class

在 es6 中,我们是这样做的

class Engineer {
  constructor(name){
    this.name = name
    this.skills = ['linux','network','os']
  }
  coding(){
    console.log(`${this.name} 正在编码`)
  }
}

class FeEngineer extends Engineer{
  constructor(name){
    super(name)
    this.skills.push('web')
  }
  croping(){
    console.log(`${this.name} 正在切图`)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

每个 Engineer 都有一套基础技能 skills 和一个编码方法 coding

FeEngineer 继承了 Engineer 的基础技能和编码方法,并添加了自己的技能

实例化一个 FeEngineer 看下效果

const fe = new FeEngineer("gahing")
console.log(fe)
/**
{
  name: "gahing",
  skills: ["linux", "network", "os", "web"],
  __proto__: Engineer {
    constructor: class FeEngineer,
    croping: ƒ croping(),
    __proto__: Object {
      coding: ƒ coding()
      constructor: class Engineer
      __proto__: Object
    }
  }
}
*/
Engineer.prototype.constructor === Engineer // true
fe.__proto__ === FeEngineer.prototype // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

然后说说实例化 FeEngineer 执行的过程

子类中的构造函数必须调用 super ,原因在于 js 是这样处理的:

  • 实例化一个普通的构造函数时,会创建一个空对象作为 this ,然后继续运行
  • 而实例化一个派生(子类)的构造函数时,需要利用父构造函数来完成上面这件事 -- 执行 super(...)。于是,父构造函数创建了一个空对象作为 this,然后继续运行父构造函数的代码。而后利用该 this 对象继续执行子类的构造函数中的代码

注意创建 this 的位置,所以下面代码是会报错的。

class Super{}
class Sub extends Super{
  constructor(){
    this.name = "1"
    super()
  }
}
new Sub() // Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
1
2
3
4
5
6
7
8

提示 this 的访问必须在执行 super 之后,所以和等等要谈到的借用构造函数法还是有点不一样的,这里先简单的上个组合继承的代码

function Super(){
  this.age = 1
}
function Sub(){
  this.name = "test"
  Super.call(this)
}
new Sub() // {age:1,name:"test"}
1
2
3
4
5
6
7
8

下面我们看看用 es5 怎么实现上面的效果

# 原型链继承法

创建构造函数时,原型对象会自动创建,也可以将其指向其他对象,实现原型链继承

function Engineer (name){
  this.name = name
  this.skills = ['linux','network','os']
}
Engineer.prototype.coding = function(){
  console.log(`${this.name} 正在编码`)
}

function FeEngineer(){

}
var engineer = new Engineer("gahing")
FeEngineer.prototype = engineer
FeEngineer.prototype.croping = function(){
  console.log(`${this.name} 正在切图`)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这样实例化 FeEngineer 后,该对象就能访问父类对象的所有属性和方法了

var fe = new FeEngineer('gahing')
console.log(fe)
/**
FeEngineer {
  __proto__: Engineer
    croping: ƒ ()
    name: "gahing"
    skills: (3) ["linux", "network", "os"]
    __proto__: Object
}
*/
1
2
3
4
5
6
7
8
9
10
11

但是看到没有, __proto__ 把 fe 实例和原型对象 engineer 连接起来了,

修改 fe 的属性和方法可能会改动到 engineer

如上面的例子,本来 engineer 是没有 croping 方法的,后面却因为要给 fe 添加原型方法,导致 engineer 也有这个方法

# 借用构造函数法

由于不用原型继承,需要把父类的原型方法转到实例方法

function Engineer (name){
  this.name = name
  this.skills = ['linux','network','os']
  this.coding = function() {
    console.log(`${this.name} 正在编码`)
  }
}

function FeEngineer(name){
  Engineer.call(this,name)
  this.skills.push('web')
}
FeEngineer.prototype.croping = function(){
  console.log(`${this.name} 正在切图`)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这样实例化 FeEngineer 后,该对象就拥有了父类对象的所有属性和方法了

var fe = new FeEngineer('gahing')
console.log(fe)
/**
FeEngineer {
  coding: ƒ ()
  name: "gahing"
  skills: (4) ["linux", "network", "os", "web"]
  __proto__:
    croping: ƒ ()
    constructor: ƒ FeEngineer(name)
    __proto__: Object
}
*/
1
2
3
4
5
6
7
8
9
10
11
12
13

但是这样会产生一个问题, FeEngineer 实例的 coding 方法和 Engineer 实例的 coding 方法不一样,并且每个 FeEngineer 实例的 coding 方法也不一样

因为该方法是每次实例化的时候生成的,所以也非常占内存

var engineer = new Engineer()
var fe = new FeEngineer()
fe.coding === engineer.coding //false
1
2
3

# 组合继承法

借助原型链继承法实现原型方法的继承,借助借用构造函数法 实现父类实例属性和方法的继承

function Engineer (name){
  this.name = name
  this.skills = ['linux','network','os']
}
Engineer.prototype.coding = function(){
  console.log(`${this.name} 正在编码`)
}

function FeEngineer(name){
  Engineer.call(this,name)
  this.skills.push('web')
}
var engineer = new Engineer("gahing")
FeEngineer.prototype = engineer
FeEngineer.prototype.croping = function(){
  console.log(`${this.name} 正在切图`)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这样实例化 FeEngineer 后,该对象就拥有了父类对象的所有属性和方法了

var fe = new FeEngineer('gahing')
console.log(fe)
/**
FeEngineer {
  name: "gahing"
  skills: (4) ["linux", "network", "os", "web"]
  __proto__: Engineer
    croping: ƒ ()
    name: "gahing"
    skills: (3) ["linux", "network", "os"]
    __proto__: Object
      coding: ƒ ()
      constructor: ƒ Engineer(name)
      __proto__: Object
}
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

父类实例属性和方法被 fe 继承了,且父类的的方法还是挂在父类的原型对象上

但是由于父类实例 engineer 被挂载到 fe 的原型对象上,修改 fe 的属性和方法可能会改动到 engineer

同时,父类构造函数被调用了两次,fe 的原型对象 engineer 多了一些不必要的实例属性和方法

# 寄生组合式继承

其实只要父类构造函数 Engineer 中的东西不被挂载到子类实例对象 fe 的原型对象上,就可以实现我们要的效果

再上一次 es6 执行后的效果

/**
{
  name: "gahing",
  skills: ["linux", "network", "os", "web"],
  __proto__: Engineer {
    constructor: class FeEngineer,
    croping: ƒ croping(),
    __proto__: Object {
      coding: ƒ coding()
      constructor: class Engineer
      __proto__: Object
    }
  }
}
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

思路就是将子类实例对象 fe 的原型对象指向一个空对象,该空对象的构造函数的原型对象指向父类构造函数 Engineer 的原型对象

然后调整下组合继承的代码,变成如下

// 创建 Engineer 的一个副本,区别在于没有任何实例属性
function Temp(){}
Temp.prototype = Engineer.prototype
const temp = new Temp() 

function Engineer (name){
  this.name = name
  this.skills = ['linux','network','os']
}
Engineer.prototype.coding = function(){
  console.log(`${this.name} 正在编码`)
}

function FeEngineer(name){
  Engineer.call(this,name)
  this.skills.push('web')
}
// 指向 temp
FeEngineer.prototype = temp
FeEngineer.prototype.croping = function(){
  console.log(`${this.name} 正在切图`)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

实例化 FeEngineer 看下效果

var fe = new FeEngineer('gahing')
fe.__proto__.constructor === FeEngineer // false
console.log(fe)
/**
FeEngineer {
  name: "gahing"
  skills: (4) ["linux", "network", "os", "web"]
  __proto__: Engineer {
    croping: ƒ ()
    __proto__: {
      coding: ƒ ()
      constructor: ƒ Engineer(name)
      __proto__: Object
    }
  }
}
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

由于实例化 Temp 时不会产生多余的属性和方法,因此这种做法基本能达到效果

剩下的就是一些构造器方法的调整

// fe 的原型对象的构造器方法应该指向 FeEngineer ,并且是不可修改的
Object.defineProperty(temp,'constructor',{
  enumerable: false,
  value: FeEngineer
})
1
2
3
4
5

综合一下,得到如下的寄生组合式继承的代码

function Temp(){}
Temp.prototype = Engineer.prototype
const temp = new Temp() 
Object.defineProperty(temp,'constructor',{
  enumerable: false,
  value: FeEngineer
})

function Engineer (name){
  this.name = name
  this.skills = ['linux','network','os']
}
Engineer.prototype.coding = function(){
  console.log(`${this.name} 正在编码`)
}

function FeEngineer(name){
  Engineer.call(this,name)
  this.skills.push('web')
}
// 指向 temp
FeEngineer.prototype = temp
FeEngineer.prototype.croping = function(){
  console.log(`${this.name} 正在切图`)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

再看下效果

var fe = new FeEngineer('gahing')
fe.__proto__.constructor === FeEngineer // true
console.log(fe)
/**
FeEngineer 
  name: "gahing"
  skills: (4) ["linux", "network", "os", "web"]
  __proto__: Engineer
    croping: ƒ ()
    constructor: ƒ FeEngineer(name)
    __proto__:
      coding: ƒ ()
      constructor: ƒ Engineer(name)
      __proto__: Object
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

最后,我们希望把 Temp 相关的代码做个封装,实现 SubClass 和 SuperClass 之间的寄生组合式继承

function createTemp(SuperClass){
  function Temp(){}
  Temp.prototype = SuperClass.prototype
  return new Temp() 
}
function createTemp2(SuperClass){
  let obj = {}
  obj.__proto__ = SuperClass.prototype
  return obj
}
function inheritPrototype (SubClass, SuperClass){
  // 也可以直接用 Object.create(SuperClass.prototype)
  // 即 SuperClass.prototype 指向创建 temp 的构造函数的原型对象
  // temp.__proto__ === SuperClass.prototype
  // 这里也可以采用 createTemp2
  const temp = createTemp(SuperClass)
  Object.defineProperty(temp,'constructor',{
    enumerable: false,
    value: SubClass
  })
  SubClass.prototype = temp
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

所以上面的代码又可以写成

function Engineer (name){
  this.name = name
  this.skills = ['linux','network','os']
}
Engineer.prototype.coding = function(){
  console.log(`${this.name} 正在编码`)
}

function FeEngineer(name){
  Engineer.call(this,name)
  this.skills.push('web')
}
inheritPrototype(FeEngineer,Engineer)
FeEngineer.prototype.croping = function(){
  console.log(`${this.name} 正在切图`)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 参考

  1. ES6 Class 继承与 super (opens new window)
  2. 回忆杀:JavaScript的继承 (opens new window)
编辑 (opens new window)
#ECMAScript
上次更新: 2024/09/01, 23:56:56
趣味js-只用特殊字符生成任意字符串
面试官问:怎么避免函数调用栈溢出

← 趣味js-只用特殊字符生成任意字符串 面试官问:怎么避免函数调用栈溢出→

最近更新
01
浅谈代码质量与量化指标
08-27
02
快速理解 JS 装饰器
08-26
03
Vue 项目中的 data-v-xxx 是怎么生成的
09-19
更多文章>
Theme by Vdoing | Copyright © 2016-2024 Gahing | 闽ICP备19024221号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式