探索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,我们应当去其糟粕,取其精华,将它的优点最大化。

相关推荐
归于尽11 分钟前
async/await 从入门到精通,解锁异步编程的优雅密码
前端·javascript
陈随易12 分钟前
Kimi k2不行?一个小技巧,大幅提高一次成型的概率
前端·后端·程序员
猩猩程序员18 分钟前
Rust 动态类型与类型反射详解
前端
杨进军19 分钟前
React 实现节点删除
前端·react.js·前端框架
晓131328 分钟前
JavaScript加强篇——第六章 定时器(延时函数)与JS执行机制
开发语言·javascript·ecmascript
yanlele41 分钟前
【实践篇】【01】我用做了一个插件, 点击复制, 获取当前文章为 Markdown 文档
前端·javascript·浏览器
爱编程的喵44 分钟前
React useContext 深度解析:告别组件间通信的噩梦
前端·react.js
LeeAt1 小时前
手把手教你构建自己的MCP服务器并把它连接到你的Cursor
javascript·cursor·mcp
前端风云志2 小时前
TypeScript枚举类型应用:前后端状态码映射的最简方案
javascript
望获linux2 小时前
【实时Linux实战系列】多核同步与锁相(Clock Sync)技术
linux·前端·javascript·chrome·操作系统·嵌入式软件·软件