探索JavaScript原型链设计——详解prototype、__proto__及constructor三者之间的关系

原型链,相信使用过JavaScript的同道中人都或多或少的听说过,它是JavaScript语言中相当重要的核心概念,有了它,我们才能在遍地都是对象的JavaScript中实现继承,大大提升了我们编程的便利性和程序的精简度。理解prototype__proto__constructor以及它们三者之间的关系是掌握原型链的必经之路。

1、原型链设计的始末

1.1 历史背景

  • 1990年代初期,互联网刚刚兴起,但是当时的只能查看静态的网页,只能显示html和图片,缺乏与用户之间的交互。
  • 1994年网景公司退出Netscape Navigator浏览器,为了更好的占据市场,它们希望网页可以"动"起来,所以急需一门直接嵌入html的语言。
  • 1995年5月网景公司招募的程序员Brendan Eich仅用了十天时间设计出了JavaScript语言,但在设计它的继承机制的时候确实花了点功夫。

1.2 借鉴java和c++语言的new操作符

在设计JavaScript语言的继承机制的时候,Eich为了满足轻便、易上手 的设计理念,没有选择引入类(calss) 的概念。但是它借鉴了javac++语言实例化类的机制:使用new操作符,但是没有类,new出来的是什么呢?

  • 了解一点javac++语言的同学们都知道,在类被实例化的时候都会触发类的构造函数constructor ,既然没有类,Eich就直接让new操作构造函数。

1.3 引入prototype原型对象

但是new操作符是有缺陷的,通过new创建的对象没法共享公共的方法和属性,这样只是创建了一个新对象,没有达到和传统面向对象语言一样的继承目的。于是prototype原型对象就此诞生,它专门用于存放公共的属性与方法。

2、prototype

  • 属于构造函数(函数对象) :只有构造函数才有prototype属性

  • 作用:为构造函数定义公共的属性和方法,方便实例对象使用。

  • 例子

    javascript 复制代码
    function animal(name) {
        this.name = name;
    }
    animal.prototype.showName = funciton() {
        console.log(`我是:${this.name}`)
    }
    const cat = new animal('猫');
    const fish = new animal('鱼');
    cat.showName(); // 我是:猫
    fish.showName(); // 我是:鱼

    在例子中,animal是构造函数,animal.prototype原型对象,所有animal的实例对象(cat和fish)共享了原型对象中的showName方法。

3、__proto__

  • 属于实例对象 :所有对象(包括函数对象)都有 __proto__ 属性。

  • 作用 :指向创建该对象的构造函数的原型对象(即 constructor.prototype)。

  • 例子

    javascript 复制代码
    // cat 是 animal 的实例
    console.log(cat.__proto__ === animal.prototype); // true
    • cat.__proto__ 指向 animal.prototype
  • 注意

    • __proto__ 是浏览器实现的非标准属性,现代代码中应使用 Object.getPrototypeOf(obj) 替代。
    • 原型链的终点是 Object.prototype.__proto__(即 null)。

4、constructor

  • 属于原型对象 :每个原型对象(prototype)都有一个 constructor 属性。

  • 作用:指向该原型对象关联的构造函数。

  • 例子

    javascript 复制代码
    console.log(animal.prototype.constructor === animal); // true
    console.log(cat.constructor === animal); // true(通过原型链继承)
    • 实例对象本身没有 constructor 属性,但会通过原型链找到 animal.prototype.constructor

5、它们之间的关系

如上图所示,它们之间的关系是这样的:

  • 构造函数的prototype属性指向原型对象;

  • 实例对象的__proto__也指向原型对象;

  • 原型对象的constructor属性又指向构造函数本身

    javascript 复制代码
    cat.__protp__ === animal.prototype;  // 实例对象的__protp__指向构造函数的原型对象
    animal.prototype.constructor === animal; // 原型对象的constructor指向构造函数本身
    ​
    // 构造函数的原型对象的__proto__指向内置对象Object的原型对象
    animal.prototype.__proto__ === Object.prototype;
    ​
    Object.prototype.__proto__ === null; //(原型链的终点)

6、原型链的优缺点

优点

  1. 内存高效:共享属性和方法,通过原型链所有的实例共享原型对象中的方法和属性,避免重复创建,节约内存。

    javascript 复制代码
    function animal(name) {
       this.name = name;
    }
    animal.prototype.showName = funciton() {
       console.log(`我是:${this.name}`)
    }
    const cat = new animal('猫');
    const fish = new animal('鱼');
    cat.showName(); // 我是:猫
    fish.showName(); // 我是:鱼
  2. 动态扩展性:运行时修改原型,原型对象可以动态修改,一次修改,所有实例受益,无论新旧实例。

    javascript 复制代码
    animal.prototype.sayHello = function() {
        console.log('Hello')
    }
    cat.sayHello(); // Hello  虽然实例对象已经被创建了,但也能使用原型对象中的新方法
    fish.sayHello(); // Hello
  3. 灵活的继承模式:根据不同的场景,可通过多种方式实现继承。

    javascript 复制代码
    // 组合继承
    function dog(name, grade) {
      animal.call(this, name); // 构造函数继承属性
      this.grade = grade;
    }
    dog.prototype = Object.create(animal.prototype); // 原型链继承方法

缺点

  1. 性能问题:深层次原型链查找,属性和方法的查找需要逐层遍历原型链,直至顶层原型链为止,过长的原型链会影响其性能

    javascript 复制代码
    cat.toString(); 查找线路:cat -> animal.prototype -> Object.prototype -> null
  2. 共享属性的副作用:原型对象的共享属性可能会被所有实例对象意外修改

    javascript 复制代码
    animal.prototype.family = [];
    cat.family.push('dog'); // 其实修改的是原型对象中的family
    console.log(fish.family); // ["dog"]
  3. 污染作用域 :构造函数不规范的使用,即不通过new操作符调用,this指向全局作用域,会导致污染全局作用域。

    javascript 复制代码
    const dog = animal('dog');// this.name 泄露到全局作用域
    console.log(window.name); // "dog"
  4. 理解成本高 :隐式引用,原型对象中的额公共属性很难被发现,容易混淆prototype、__proto__、constructor这三者之间的关系

    javascript 复制代码
    console.log(cat.__proto__ === animal.prototype); // true
    console.log(animal.prototype.constructor === animal); // true

总结

原型链的设计主要是为了让JavaScript语言支持继承机制,但是JavaScript的继承机制又与传统面向对象语言不太一样,它没有设计"类"的概念,继承是基于原型对象(prototype)实现,实例对象通过__proto__对象找到创建该实例的构造函数的原型对象,原型对象的constructor属性又指向构造函数本身,确实有点绕,容易混淆,可以看一下这个表格,会清晰一些:

属性 归属 作用 示例关系
prototype 构造函数 定义实例共享的原型对象 Person.prototype
__proto__ 实例对象 指向构造函数的原型对象(形成原型链) alice.__proto__ === Person.prototype
constructor 原型对象 指向关联的构造函数 Person.prototype.constructor === Person

JavaScript的继承机制也是相当灵活,在运行状态下,能修改通过同一构造函数(或指向同一原型对象)创建的多个实例对象的共享属性,这是其他面向对象语言办不到的。

但没有完美的语言设计,JavaScript的原型链设计也会有很多问题,如原型链过长导致查找的性能问题,理解成本高、共享属性副作用、污染全局变量。

最后,运用JavaScript,我们应当去其糟粕,取其精华,将它的优点最大化。

相关推荐
夕水19 分钟前
这个提升效率宝藏级工具一定要收藏使用
前端·javascript·trae
会飞的鱼先生33 分钟前
vue3 内置组件KeepAlive的使用
前端·javascript·vue.js
斯~内克1 小时前
前端浏览器窗口交互完全指南:从基础操作到高级控制
前端
Mike_jia1 小时前
Memos:知识工作者的理想开源笔记系统
前端
前端大白话1 小时前
前端崩溃瞬间救星!10 个 JavaScript 实战技巧大揭秘
前端·javascript
loveoobaby1 小时前
Shadertoy着色器移植到Three.js经验总结
前端
蓝易云1 小时前
在Linux、CentOS7中设置shell脚本开机自启动服务
前端·后端·centos
浩龙不eMo1 小时前
前端获取环境变量方式区分(Vite)
前端·vite
一千柯橘2 小时前
Nestjs 解决 request entity too large
javascript·后端
土豆骑士2 小时前
monorepo 实战练习
前端