JS基础—学JavaScript不看看这个吗?大佬必备——继承和原型链

122 阅读7分钟

Class之"没落"

说起继承,由于之前学的是CJava这些基于类的语言,我总能想到 class ,在JavaScript中,尽管类(class)自从ECMAScript 6(ES6)引入以来已经成为语言的一部分,但在某些情况下,你可能会发现它们使用得并不频繁。

  • 历史原因

    • 函数原型继承:在ES6之前,JavaScript通过函数和原型链来实现继承和面向对象编程。许多开发者在ES6之前已经习惯了这种方法,并且已有大量的代码库是基于这种风格编写的。
    • 类的概念是相对较新的:对于那些已经熟悉和习惯了函数原型继承的开发者,可能没有强烈的动机去改用class
  • 灵活性和功能性

    • 函数和闭包:JavaScript的函数和闭包提供了很强的灵活性,可以实现类和对象的许多功能。例如,模块模式和工厂函数都是常见的设计模式,它们在很多情况下可以代替类。
    • 原型继承:原型继承在某些场景下更加灵活,并且性能上可能更优一些,特别是在创建大量对象时。
  • 简洁性和可读性

    • 简单的对象字面量:对于简单的对象结构,直接使用对象字面量({})比定义一个类要更简单、更直接。例如,配置对象和静态数据结构通常使用对象字面量。
    • 小工具函数:对于一些简单的功能,定义一个工具函数(utility function)可能比定义一个类和实例化它要更简洁。
  • 函数式编程的流行

    • 函数式编程范式:JavaScript也支持函数式编程范式,随着函数式编程的流行,许多开发者倾向于使用纯函数、不可变数据和高阶函数等函数式编程概念,这些在某种程度上减少了对类的使用。
很明显,函数比类更为的灵活且适用,所以呢 JavaScript 只有一种结构:对象。

原型链

每一个对象都有一个私有属性,指向另一个名为原型(prototype) 的对象

设计模式牵手大模型文章中有浅浅提到原型和原型链,但还是属于理解,因此在这里详细理一下。

官方文档中我们知道了。每一个对象都有一个私有属性指向原型,原型(prototype)也是一个对象。

prototype

  • 定义: prototype 是每个函数(包括构造函数)独有的属性,它是一个对象。这个对象用于构造函数创建的所有实例对象共享的方法和属性。
  • 作用: 当我们使用构造函数创建对象时,实例对象会继承构造函数的 prototype 对象上的属性和方法。
  • 访问方式: 通过构造函数可以访问 prototype 属性:someConstructor.prototype
function Person(name) {
    this.name = name;
}
Person.prototype.greet = function() {
    console.log("Hello, " + this.name);
};
let alice = new Person("Alice");
alice.greet();  // 输出:Hello, Alice

在上述代码中,greet 方法被添加到 Person 构造函数的 prototype 上,因此所有使用 Person 构造函数创建的实例(如 alice)都可以访问这个方法。

那么我们再来看一个 __proto__(鄙人之前对二者理解地模糊不清)

__proto__

  • 定义: __proto__ 是每个对象(包括函数对象)都有的内部属性,它指向该对象的构造函数的 prototype 属性。
  • 作用: __proto__ 用于实现对象的原型链,它使得对象可以访问构造函数的 prototype 上的属性和方法。
  • 访问方式: 通过实例对象可以访问 __proto__ 属性,例如:someObject.__proto__
console.log(alice.__proto__ === Person.prototype);  // 输出:true

在上述代码中,alice.__proto__ 指向 Person.prototype,这说明 alice 的原型是 Person.prototype

不知道大家懂了没有,我们来看一个例子:

function doSomething() {}
console.log(doSomething.prototype);

输出

{
constructor: ƒ doSomething()
[[Prototype]]: Object 
}

我们来分析一下

  • doSomething.prototype:

    • 当你定义一个函数时,JavaScript会自动给这个函数分配一个prototype属性。这个属性是一个对象,默认情况下包含一个constructor属性,这个constructor属性指向函数本身。
    • 因此,doSomething.prototype默认是一个对象,它的constructor属性指向doSomething函数本身。
  • constructor:

    • 这个属性是指向创建该原型对象的函数,即这里的doSomething函数。
    • 在默认情况下,这个constructor属性会自动添加到prototype对象中。
  • [[Prototype]] :

    • 这表示doSomething.prototype对象的原型,它是Object.prototype。这是因为所有的对象(除了通过Object.create(null)创建的对象)最终都继承自Object.prototype

问题:doSomething.prototype对象的原型是如何与Object.prototype相关联的?

原型链和继承

在JavaScript中,对象是通过原型链来实现继承的。每个对象都有一个内部链接,称为[[Prototype]],指向另一个对象。如果对象在查找某个属性时找不到该属性,它会沿着这个链接去查找,直到找到该属性或到达原型链的末尾。

Function.prototype

当你定义一个函数,如doSomething时,这个函数对象会自动获得一个prototype属性,该属性是一个对象。这个对象有一个隐式的[[Prototype]]属性(可以用__proto__访问),指向Object.prototype

因此才会有

  constructor: doSomething,
  [[Prototype]]: Object.prototype

图示化

doSomething (function)
|
|__ __proto__ --> Function.prototype
|                |
|                |__ __proto__ --> Object.prototype
|                                   |
|                                   |__ __proto__ --> null
|
|__ prototype (property)
    |
    |__ constructor: doSomething
    |
    |__ __proto__ --> Object.prototype
                      |
                      |__ hasOwnProperty
                      |__ toString
                      |__ ... 
                      |__ __proto__ --> null

小结一下吧

  • __proto__ 存在于所有对象上, prototype 只存在于函数上;

  • 每个对象都对应一个原型对象, 并从原型对象继承属性和方法, 该对应关系由 __proto__ 实现(访问对象内部属性[[Prototype]]);

  • prototype 用于存储共享的属性和方法, 其作用主要体现在 new 创建对象时, 为 __proto__ 构建一个对应的原型对象(设置实例对象的内部属性[[Prototype]]);

  • __proto__ 不是 ECMAScript 语法规范的标准, 是浏览器厂商实现的一种访问和修改对象内部属性 [[Prototype]] 的访问器属性(getter/setter), 现常用 ECMAScript 定义的 Object.getPrototypeOf 和 Object.setPrototypeOf 代替;

  • prototype 是 ECMAScript 语法规范的标准;

最后呢 我们再来看一个简单的例子,也是官方文档中的例子。

const o = {
  a: 1,
  b: 2,
  // __proto__ 设置了 [[Prototype]]。它在这里被指定为另一个对象字面量。
  __proto__: {
    b: 3,
    c: 4,
  },
};

这里定义了一个对象 o,它有三个属性:

-   `a`,值为 `1`
-   `b`,值为 `2`
-   `__proto__`,它是一个特殊属性,用于设置对象的原型(`[[Prototype]]`)。
在这个例子中,它被设置为另一个对象 `{ b: 3, c: 4 }`
  • 原型链

在这个例子中,o 的原型链如下:

  1. o 对象本身 { a: 1, b: 2 }
  2. o[[Prototype]],即 { b: 3, c: 4 }
  3. o[[Prototype]][[Prototype]],即 Object.prototype
  4. Object.prototype[[Prototype]],即 null

让我们一步步来看:

  • 第一级:o 对象

o 对象自身有属性 ab

{
  a: 1,
  b: 2
}
  • 第二级:o 的 [[Prototype]]

通过 __proto__,我们知道 o 的原型是这个对象:

{
  b: 3,
  c: 4
}

第三级:Object.prototype

{ b: 3, c: 4 } 对象的原型是 Object.prototype。这是 JavaScript 中所有普通对象的默认原型。

第四级:null

Object.prototype 的原型是 null,这是原型链的末端。

  • 属性查找过程

当你访问 o 的属性时,JavaScript 引擎会按照原型链查找:

  1. 如果访问 o.a,它在 o 对象本身找到,值为 1
  2. 如果访问 o.b,它在 o 对象本身找到,值为 2,所以不会查找原型中的 b: 3
  3. 如果访问 o.co 对象本身没有 c,于是会沿着原型链查找,在 o.__proto__ 找到 c: 4

完整的原型链如下:

{ a: 1, b: 2 } ---> { b: 3, c: 4 } ---> Object.prototype ---> null

本篇最后(未完)留一个问题:大家想想当我们实例化对象时候的new是从何而来?不知道是否有了一些新的想法。

当然看相信各位都能看懂,但还是需要和老黄牛一般消化才好,建议各位再去翻阅相关资料加深理解,本文若有不足之处还请指证,Thanks♪(・ω・)ノ。