😄
前端学习
  • 👋Welcome
  • 📖前端基础
    • HTML
      • 基础知识
      • 进阶知识
      • HTML5
    • CSS
      • 基础知识
      • 进阶知识
      • CSS 专题
        • CSS 选择器
        • CSS 布局
        • CSS 动画
        • CSS 画图
        • 响应式方案
        • CSS BEM 规范
        • CSS 案例
    • JavaScript
      • 基础知识
      • 进阶知识
      • 常用内置对象
        • Array 对象
        • String 对象
        • Number 对象
        • Boolean 对象
        • Math 对象
        • Date 对象
        • RegExp 对象
        • Object 对象
      • JS 专题
        • 数据类型
        • 原型链/继承
        • 对象赋值与拷贝
        • this 的指向
        • 异步操作
        • 模块化
        • 设计模式
    • 浏览器
      • 浏览器模型
      • 事件
      • 位置属性
      • Web 缓存
      • 本地存储
    • 综合内容
      • 前端跨域
      • 登录鉴权
      • 文件上传与下载
  • 🏗️前端框架
    • Vue.js
      • 基础知识
      • Vue 组件通信
      • Vuex 使用指南
      • Vue 动画
      • 静态网站框架 VuePress
    • React.js
      • 基础知识
      • 组件通信
  • 📦计算机基础/后端
    • 图解计算机网络
    • HTTP/HTTPS
    • TCP/UDP
    • Node.js
    • MongoDB
  • 🛠️开发工具
    • 版本控制工具-Git
      • git submodule
    • 构建工具-Webpack
    • 错误监控工具-Sentry
    • 单元测试工具-Jest
    • 包管理工具-NPM
    • 代码编辑器-VSCode
  • 🤔专题内容
    • 前端工程化
    • 代码规范
      • JavaScript 代码规范
      • CSS 代码规范
      • Vue 代码规范
      • Git Commit 规范
      • 代码规范配置
    • 网络安全与防御
    • 性能优化
    • 算法编程
    • 数据可视化
  • 🧑‍💻 面试相关
    • 面试知识总结
    • 面试问题总结
    • 面试常见编程
    • 面试资源汇总
  • 🍭其他
    • 项目经验❗️
    • 踩坑指南❗️
      • JavaScript 踩坑指南
      • CSS 踩坑指南
      • Vue 踩坑指南
    • 学习资源
    • 综合收藏夹
由 GitBook 提供支持
在本页
  • 1. 原型链
  • 1.1 基础介绍
  • 1.2 核心知识*
  • 2. 继承
  • 2.1 ES5 继承
  • 2.2 ES6 Class 继承

这有帮助吗?

  1. 前端基础
  2. JavaScript
  3. JS 专题

原型链/继承

上一页数据类型下一页对象赋值与拷贝

最后更新于3年前

这有帮助吗?

1. 原型链

参考资料:

1.1 基础介绍

每次利用构造函数生成对象,则部分重复的内容则会占用不必要的内存,因此利用原型prototype,则可以减少这些内存开销,每一个生成的对象都有一个隐式的_proto_指向原型,如下图所示。

如上图所示的原型代码,则会形成一个原型链,如下图所示。

属性访问:属性访问会沿着原型链进行查找,首先在自身属性上查找,若没有则沿着原型链一层一层往下找。

tom.name; // "Tom"
tom.job; // "teacher"
tom.toString(); // 内置Object对象含有该属性

属性删除:属性删除永远删除的是对象的自身属性,除非指定删除prototype的属性。

del tom.job; // 只删除tom的job属性,对原型无影响
tom.job; //再访问job则会获得原型的job属性值 

属性修改:属性修改永远修改的是对象的自身属性,除非指定修改prototype的属性。

tom.job = "policeman"; // 此时只会在tom对象上生成job属性,并且Teacher的prototype上的job值不改变
Teacher.prototype.job = "assistant"; // 此时则会修改Teacher的prototype上的job值

如下图所示,bill 对象继承了 Teacher 原型,bill 对象调用 addCourse 方法时,改变了 teacher 的 course。实际为 bill.__proto__.add("haha"); 是 bill 的原型调用了 add,this 指的是原型 Teacher。

1.2 核心知识*

  • 我们需要牢记两点:①__proto__ 和 constructor 属性是对象所独有的;② prototype 属性是函数所独有的,但因为函数也是一种对象,所以函数也拥有 __proto__ 和 constructor属性。

  • __proto__ 属性的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的 __proto__ 属性所指向的那个对象(父对象)里找,一直找,直到 __proto__ 属性的终点 null,再往上找就相当于在 null 上取值,会报错。通过 __proto__ 属性将对象连接起来的这条链路即我们所谓的原型链.

  • prototype 属性的作用就是让该函数所实例化的对象们都可以找到公用的属性和方法,即 f1.__proto__ === Foo.prototype。

  • constructor 属性的含义就是指向该对象的构造函数,所有函数(此时看成对象了)最终的构造函数都指向 Function。

简单来说

  • __proto__和constructor属性是对象所独有的;prototype属性是函数所独有的,函数也是对象,因此拥有3个属性。

  • __proto__对象实例指向父对象,constructor原型指向构造函数,prototype构造函数指向其原型。

  • __proto__最终会到达Object.prototype至null,constructor最终会达Function()。

2. 继承

2.1 ES5 继承

function Animal(name) {
    // 实例属性
    this.name = name;
    // 实例方法
    this.sleep = function () {
        console.log(this.name + '正在睡觉!');
    }
}
​
Animal.prototype.eat = function (food) {
    console.log(this.name + '正在吃:' + food);
};

2.1.1 原型链继承

子类的原型是父类的实例。

function Cat() {}
​
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';

缺点:

  1. 创建子类实例时无法向父类构造函数传参,const cat = new Cat('name');

  2. 来自原型对象的所有属性被所有实例共享,如果是引用类型则更改会影响其他子类实例;

  3. 无法实现多继承。

2.1.2 构造继承

使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类。

function Cat(name) {
    Animal.call(this, name);
}

解决了原型继承的缺点1和缺点2(因为每一次都创建了新的父类实例)。缺点:

  1. 不能继承父类原型的属性与方法;

  2. 每个子类都有父类实力函数的副本,影响性能。

2.1.3 实例继承

为父类实例添加新特性,作为子类实例返回。

function Cat3(name) {
    const cat = new Animal(name);
    cat.age = 18 // 实例添加属性
    return cat
}

缺点:

  1. 子类的实例其实是父类的实例;

  2. 不能多继承。

2.1.4 拷贝继承

为父类实例添加新特性,作为子类实例返回。

function Cat(name) {
    const animal = new Animal(name);
    for (let pro in animal) {
        Cat.prototype[pro] = animal[pro];
    }
}

缺点:

  1. 效率低,需要拷贝父类的属性;

  2. 无法获取父类不可枚举的属性。

2.1.5 组合继承

通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用。

function Cat(name) {
    Animal.call(this, name);
}
​
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat5;

缺点:

  1. 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)。

2.1.6 寄生组合继承

通过寄生方式,砍掉父类的实例属性。

function Cat(name) {
    Animal.call(this, name);
}
​
(function () {
    // 创建一个没有实例方法的类
    const Super = function () {};
    // 将实例作为子类的原型
    Super.prototype = Animal.prototype;
    Cat.prototype = new Super();
})();
类型
原理
父类实例属性继承
父类原型属性继承
多继承

原型链继承

子类的原型是父类的实例

+

+

-

构造继承

调用父类构造函数

+

-

+

实例继承

子类实例是父类实例

+

+

-

拷贝继承

子类拷贝父类实例的属性和方法

+

+

+

组合继承

调用父类构造,子类原型是父类实例

+

+

?

寄生组合继承

/

+

+

?

2.2 ES6 Class 继承

class Point { /* ... */ }
​
class ColorPoint extends Point {
  constructor(x, y, color) {
    this.y = y // ReferenceError
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }
​
  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

父类的静态方法,也会被子类继承,而父类的实例对象不能使用父类的静态方法。

class A {
    static hello() {
        console.log('hello world');
    }
}

class B extends A {
}

B.hello();  // hello world

const b = new A();
b.hello(); // TypeError: newB.hello is not a function

继承关系说明

class A { }
class B extends A { }
console.log(B.__proto__ === A);  // true
console.log(B.prototype.__proto__ === A.prototype);  // true

super关键词的用法

  • 当作函数调用时,代表父类的构造函数

class A {}

class B extends A {
  constructor() {
    super();
  }
}
  • 当作对象使用时,在普通方法中,指向父类的原型对象(简单说就是父类中没有static的部分),也就是说在父类实例上的方法或属性是无法通过super调用。在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。

class A {
  constructor() {
    this.p = 2;
  }
  print() {
    console.log(this.x);
  }
}

A.prototype.x = 2;

class B extends A {
  constructor() {
    super();
    this.x = 4;
  }
  get m() {
    return super.p;
  }
  get x(){
    return super.x;
  }
  y() {
    super.print();
  }
}

let b = new B();
b.m // undefined
b.x // 2
b.y() // 4
  • 当作对象使用时,在静态方法中,指向父类(简单说就是父类中static的部分)。类相当于实例的原型对象。

// super在静态方法之中指向父类,在普通方法之中指向父类的原型对象。
class Parent {
  static myMethod(msg) {
    console.log('static', msg);
  }

  myMethod(msg) {
    console.log('instance', msg);
  }
}

class Child extends Parent {
  static myMethod(msg) {
    super.myMethod(msg);
  }

  myMethod(msg) {
    super.myMethod(msg);
  }
}

Child.myMethod(1); // static 1

var child = new Child();
child.myMethod(2); // instance 2

参考链接:

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。完整内容详见 。

如果你对内容有任何疑问,欢迎提交 或

📖
JS继承的实现方式
Class 的继承
❕issues
✉️ email
帮你彻底搞懂 JS 中的 prototype、__proto__ 与 constructor