如何理解JavaScript中的原型链?

原型链是JavaScript实现继承和属性查找的机制,通过对象的[[Prototype]]链接形成链条,当访问属性时会沿链向上查找直至找到或到达null。原型(prototype)是函数特有的属性,指向实例共享方法的原型对象;原型链则是由__proto__连接构成的查找路径,二者共同实现对象间的方法共享与继承。利用原型链可优化内存、实现继承并提升性能,ES6的class本质仍是基于原型链的语法糖,提供更清晰的继承写法但底层机制不变。

如何理解JavaScript中的原型链?

JavaScript中的原型链,简单来说,就是一套对象间继承属性和方法的机制。当你试图访问一个对象的某个属性或方法时,如果它本身没有,JavaScript引擎就会沿着一条由

__proto__

(或内部的

[[Prototype]]

)连接起来的链条向上查找,直到找到该属性或者到达链的末端(

null

)。这条查找路径,就是我们所说的原型链。它让不同的对象能够共享同一套行为,避免了重复定义,也使得JavaScript的继承模式显得既灵活又有些“不走寻常路”。

理解原型链,我觉得就像理解一棵家族树,但不是从上往下看,而是从你自身往上追溯你的祖先。每个对象都有一个“父母”对象(它的原型),如果父母没有,就去找爷爷奶奶(父母的原型),以此类推,直到找到“始祖”(

Object.prototype

)或者发现没人有这个属性。

解决方案

要深入理解原型链,我们得从几个核心概念入手。首先是每个JavaScript对象内部都有一个

[[Prototype]]

属性,它指向这个对象的原型。在旧的浏览器环境或为了方便调试,我们常常通过

__proto__

这个非标准但广泛实现的属性来访问它。而对于函数(它们也是对象),还有一个

prototype

属性,这可不是一回事。

prototype

属性指向的是一个对象,这个对象会作为通过该函数构造出来的实例的

[[Prototype]]

。听起来有点绕?别急,我们慢慢捋。

我们通常创建对象的方式,其实都在默默地利用原型链。

立即学习Java免费学习笔记(深入)”;

1. 对象字面量: 当你写

{}

new Object()

时,你创建的对象会默认继承

Object.prototype

const myObj = {}; console.log(myObj.__proto__ === Object.prototype); // true console.log(myObj.toString()); // "[object Object]" - 这个方法就来自Object.prototype
myObj

本身并没有

toString

方法,但它能调用,就是因为JavaScript引擎沿着

myObj.__proto__

找到了

Object.prototype

上的

toString

2. 构造函数: 这是JavaScript实现“类”或“蓝图”的传统方式。

function Person(name) {     this.name = name; }  // 在Person.prototype上添加方法,所有Person实例共享 Person.prototype.sayHello = function() {     console.log(`Hello, my name is ${this.name}`); };  const alice = new Person('Alice'); console.log(alice.name); // 'Alice' alice.sayHello(); // 'Hello, my name is Alice'  // 关键来了:alice的原型是Person.prototype console.log(alice.__proto__ === Person.prototype); // true // Person.prototype的原型是Object.prototype console.log(Person.prototype.__proto__ === Object.prototype); // true // Object.prototype的原型是null,链的终点 console.log(Object.prototype.__proto__ === null); // true

alice.sayHello()

被调用时,引擎先在

alice

对象自身找

sayHello

,没找到。接着,它会沿着

alice.__proto__

找到

Person.prototype

,在这里找到了

sayHello

方法并执行。这就是原型链在实际工作中的一个经典体现。

3.

Object.create()

这个方法允许你显式地指定一个新对象的原型。

const animal = {     eats: true,     walk() {         console.log("Animal walks.");     } };  const rabbit = Object.create(animal); // rabbit的原型是animal rabbit.jumps = true; rabbit.walk(); // "Animal walks."  console.log(rabbit.__proto__ === animal); // true console.log(animal.__proto__ === Object.prototype); // true
rabbit

对象自身没有

walk

方法,但它能通过原型链找到

animal

对象上的

walk

方法。

原型链的魔力在于它实现了属性和方法的共享,这意味着内存效率更高,因为方法只存储在一个地方(原型对象上),所有实例都通过原型链去访问它。同时,它也是JavaScript实现“继承”的基石。

JavaScript原型(prototype)和原型链(prototype chain)之间究竟有什么关联与区别

这确实是初学者最容易混淆的地方,也是我当年踩过不少坑的地方。要我说,它们俩是紧密相连但又有着明确分工的两个概念。

原型(

prototype

): 这个词在JavaScript里通常特指函数对象上的一个特殊属性。记住,只有函数(包括构造函数和类)才会有这个

prototype

属性。它是一个普通的对象,我们称之为“原型对象”。这个原型对象的作用非常关键:当你用这个函数作为构造器(比如

new MyFunction()

)创建实例时,新创建的实例的

[[Prototype]]

(也就是

__proto__

)就会指向这个

MyFunction.prototype

对象。

举个例子,

Array.prototype

就是

Array

构造函数的原型对象。所有数组实例,比如

[1, 2, 3]

,它们的

__proto__

都指向

Array.prototype

,因此它们都能访问

Array.prototype

上定义的

push

pop

等方法。

原型链(

prototype chain

): 原型链则是一个机制,是当JavaScript引擎在查找对象的属性或方法时,所遵循的一条路径。这条路径是由对象的

[[Prototype]]

(或

__proto__

)属性串联起来的。每个对象都有一个

[[Prototype]]

,它指向其原型对象,而这个原型对象也可能有自己的

[[Prototype]]

,这样一层一层向上,就形成了一条链。这条链的终点通常是

null

(即

Object.prototype

[[Prototype]]

)。

所以,关联在于:原型(

prototype

属性所指向的原型对象)是构成原型链上的一个个节点。 每个对象的

__proto__

指向的,就是它在原型链上的“上一站”——它的原型对象。

区别在于:

  • prototype

    是一个属性,存在于函数对象上,指向一个对象。它的作用是为通过该函数构造的实例提供共享的属性和方法。

  • 原型链 是一种查找机制/结构,由一系列对象通过
    __proto__

    (内部的

    [[Prototype]]

    )连接而成。它的作用是实现对象的继承和属性查找。

你可以把

prototype

想象成一个工厂的“蓝图库”,里面存放着所有新产品(实例)应该具备的共享部件。而原型链则是当你拿到一个产品(实例)后,发现某个部件没有,就去它的“蓝图库”找,如果蓝图库里也没有,就去蓝图库的“蓝图库”找,直到找到或者放弃,这个查找过程就是原型链。

在实际开发中,我们如何有效利用JavaScript原型链来优化代码结构和性能?

原型链在实际开发中的应用远不止于理论,它确实能帮助我们写出更优雅、更高效的代码。我个人觉得,最直接和最常见的应用场景就是共享方法和属性,这直接关系到内存和性能。

1. 共享方法,减少内存开销: 这是原型链最核心的优势之一。想象一下,如果你有1000个

User

对象,每个对象都有一个

greet()

方法。如果这个方法直接定义在每个实例上:

function UserBad(name) {     this.name = name;     this.greet = function() { // 每个实例都会有自己的greet方法副本         console.log(`Hi, I'm ${this.name}`);     }; } const user1 = new UserBad('Alice'); const user2 = new UserBad('Bob'); // user1.greet !== user2.greet // 它们是不同的函数对象

这样,每个

UserBad

实例都会在内存中存储一个

greet

函数的副本,如果方法很多,实例也很多,内存消耗会非常大。

而利用原型链,我们可以将方法定义在构造函数的

prototype

对象上:

function Usergood(name) {     this.name = name; }  UserGood.prototype.greet = function() { // 所有实例共享同一个greet方法     console.log(`Hi, I'm ${this.name}`); };  const userA = new UserGood('Charlie'); const userB = new UserGood('David'); // userA.greet === userB.greet // 它们引用的是同一个函数对象

这样,

greet

方法只在

UserGood.prototype

上存在一份,所有

UserGood

的实例都通过原型链来访问它。这显著减少了内存占用,尤其是在处理大量对象时,性能提升非常明显。

2. 实现继承: 虽然ES6有了

class

语法糖,但其底层依然是原型链。通过原型链,我们可以实现经典的“原型式继承”。

function Animal(name) {     this.name = name; } Animal.prototype.eat = function() {     console.log(`${this.name} eats.`); };  function Dog(name, breed) {     Animal.call(this, name); // 继承Animal的属性     this.breed = breed; }  // 核心:让Dog的原型继承Animal的原型 // 避免直接赋值Animal.prototype,那样会覆盖Dog.prototype,导致所有Dog实例的原型都指向Animal.prototype // Object.create() 是一个很好的方式来创建一个新对象,并指定其原型 Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog; // 修复constructor指向  Dog.prototype.bark = function() {     console.log(`${this.name} barks!`); };  const myDog = new Dog('Buddy', 'Golden Retriever'); myDog.eat(); // Buddy eats. (来自Animal.prototype) myDog.bark(); // Buddy barks! (来自Dog.prototype)

这里,

myDog

实例的原型链是:

myDog

->

Dog.prototype

->

Animal.prototype

->

Object.prototype

->

null

。这样

Dog

实例就能访问

animal

原型上的方法了。

如何理解JavaScript中的原型链?

可图大模型

可图大模型(Kolors)是快手大模型团队自研打造的文生图AI大模型

如何理解JavaScript中的原型链?33

查看详情 如何理解JavaScript中的原型链?

3. 扩展内置对象(需谨慎): 虽然不推荐大规模修改内置对象的原型(因为它可能导致命名冲突和难以调试的问题),但在某些特定且受控的环境下,为内置对象添加一些辅助方法确实能提高开发效率。

// 示例:给Array.prototype添加一个求和方法 // 实际项目中应避免直接修改内置原型,除非你完全控制环境 if (!Array.prototype.sum) { // 检查是否已存在,避免重复定义     Array.prototype.sum = function() {         return this.reduce((acc, current) => acc + current, 0);     }; }  const numbers = [1, 2, 3, 4, 5]; console.log(numbers.sum()); // 15

这样,所有数组实例都能直接调用

sum()

方法,非常方便。但再次强调,这种做法应极度谨慎。

通过合理利用原型链,我们能够构建出模块化、可维护且高效的JavaScript代码,这对于大型应用尤其重要。

ES6中的

class

语法糖,它背后和JavaScript原型链的工作机制有何异同?

ES6引入的

class

关键字,无疑让JavaScript的面向对象编程看起来更“传统”了,它提供了更清晰、更简洁的语法来创建构造函数和处理继承。然而,这只是一个语法糖(syntactic sugar)。它并没有引入新的继承机制,其底层依然是基于我们刚才讨论的原型链机制。

相同点:

  1. 基于原型链的继承:

    class

    的继承机制,即

    extends

    关键字,本质上还是通过操作原型链来实现的。当你写

    class Dog extends Animal {}

    时,JavaScript引擎会在幕后将

    Dog.prototype

    [[Prototype]]

    设置为

    Animal.prototype

    ,从而建立了原型链上的继承关系。

    class Animal {     constructor(name) {         this.name = name;     }     eat() {         console.log(`${this.name} eats.`);     } }  class Dog extends Animal {     constructor(name, breed) {         super(name); // 调用父类的构造函数         this.breed = breed;     }     bark() {         console.log(`${this.name} barks!`);     } }  const myDog = new Dog('Buddy', 'Golden Retriever'); myDog.eat(); // Buddy eats. myDog.bark(); // Buddy barks!  // 底层原理: console.log(Dog.prototype.__proto__ === Animal.prototype); // true console.log(myDog.__proto__ === Dog.prototype); // true

    可以看到,

    Dog.prototype

    确实继承自

    Animal.prototype

    ,这正是原型链在起作用。

  2. 方法共享:

    class

    中定义的所有实例方法(非静态方法),都会被放置在类的

    prototype

    对象上,从而实现所有实例共享这些方法,节省内存。这与传统构造函数将方法定义在

    Function.prototype

    上是完全一致的。

  3. constructor

    属性:

    class

    中的

    constructor

    方法,其实就是传统构造函数。它在创建实例时被调用,

    this

    指向新创建的实例。

不同点(主要是语法和一些细节行为):

  1. 语法更清晰: 这是最显而易见的区别。

    class

    语法避免了手动操作

    prototype

    Object.create()

    来设置继承,让代码看起来更像传统的面向对象语言,更容易理解和维护。

  2. super

    关键字: 在子类的构造函数中,必须先调用

    super()

    才能使用

    this

    super()

    实际上就是调用了父类的构造函数。在方法中,

    super

    可以用来调用父类的方法。这是

    class

    特有的语法糖,在传统原型继承中,你需要手动调用

    Parent.call(this, ...)

    Parent.prototype.method.call(this, ...)

  3. class

    是函数: 尽管看起来像一个特殊的结构,但

    class

    本身仍然是一个函数。

    class MyClass {} console.log(typeof MyClass); // "function"

    不过,

    class

    函数与普通函数有一些关键区别:

    • 不能被
      new

      调用: 普通函数如果不用

      new

      调用,

      this

      可能指向全局对象;

      class

      必须用

      new

      调用。

    • 没有
      arguments

      对象:

      class

      内部没有

      arguments

      对象。

    • 严格模式:
      class

      内部的代码默认运行在严格模式下,即使你没有在文件开头声明

      "use strict"

  4. 静态方法和属性:

    class

    提供了

    static

    关键字来定义静态方法和属性,这些方法和属性直接属于类本身,而不是类的实例,也无法通过实例访问。

    class Utils {     static add(a, b) {         return a + b;     } } console.log(Utils.add(1, 2)); // 3 // const util = new Utils(); // util.add(1, 2); // TypeError: util.add is not a function

    在传统原型链中,静态方法通常是直接作为构造函数的属性来添加的,比如

    Function.staticMethod = ...

  5. 私有字段(提案中或已实现): 现代JavaScript的

    class

    语法还引入了私有字段(如

    #privateField

    ),这在传统原型继承中是难以直接实现的(通常需要闭包或

    Symbol

    来模拟)。

总结来说,

class

是JavaScript语言在语法层面为了迎合更广泛的编程范式而做出的改进,它让开发者能够以更直观的方式使用原型链的强大功能。但无论你使用

class

还是传统的构造函数加原型,理解原型链的底层机制始终是深入掌握JavaScript面向对象编程的关键。它就像是冰山下面的部分,虽然不总被直接看到,但支撑着整个上层结构。

javascript es6 java go 浏览器 ai 面向对象编程 区别 内存占用 red JavaScript es6 Static Array Object NULL 面向对象 父类 子类 构造函数 继承 class 闭包 symbol function 对象 constructor 严格模式 this prototype

上一篇
下一篇
text=ZqhQzanResources