Video thumbnail

    Dhananjay Kumar - 揭秘 JavaScript 原型

    Valuable insights

    1.所有函数皆有原型对象: JavaScript中,每一个函数在创建时都会自动拥有一个与之关联的原型对象,这是实现继承的基础机制。

    2.箭头函数不具备原型: 与传统函数不同,ES6引入的箭头函数不包含原型对象,因此它们无法被用作实例化对象的构造函数。

    3.构造函数调用模式的重要性: Douglas Crockford定义了四种调用模式,其中构造函数调用模式涉及使用'new'关键字来初始化对象实例。

    4.实例与原型的深层联系: 对象实例(如P1)不仅通过构造函数关联,还直接链接到构造函数的原型对象,形成继承链条。

    5.读取操作遵循原型链: 当访问对象属性时,JavaScript首先在实例自身查找;若未找到,则沿着原型链向上递归搜索,直到找到或返回undefined。

    6.写入操作的局部性: 属性写入操作通常在实例对象上创建或修改属性,此操作会覆盖原型链上的同名属性,不影响原型本身。

    7.使用__proto__直接访问原型: 与需要通过'.constructor.prototype'的冗长路径相比,'__proto__'属性提供了从实例对象直接指向其原型对象的快捷方式。

    引言与函数原型基础

    本次探讨的核心议题是JavaScript的原型对象机制,一个自1994年以来就存在的概念。演讲者首先要求听众准备好记录工具,以便捕捉关键信息。在深入技术细节之前,通过一个简单的提问环节旨在重新激发听众对JavaScript的兴趣,明确接下来的目标是深入理解原型对象,从而重新爱上这门语言。

    函数的原型对象特性

    JavaScript中的每一个函数都拥有一个原型对象(prototype object)。当打印一个普通函数时,返回的是一个空对象。这一特性是JavaScript面向对象模型的基础,意味着函数本身承载了可供其实例继承的属性和方法集合。

    箭头函数例外与构造器限制

    ES6引入的箭头函数(arrow function)打破了这一惯例,它们不具备原型对象,打印其原型会得到'undefined'。由于缺乏原型对象,箭头函数不能用于创建新对象,JavaScript引擎会报错指出箭头函数不是一个构造器。这源于箭头函数缺少一个名为'[[Constructor]]'的内部属性,该属性是函数能否充当构造器的关键标识。

    箭头函数不能用于创建对象,因为它不具备原型对象。
    • 普通函数/函数声明:.prototype 返回空对象。
    • 箭头函数:.prototype 返回 undefined。
    • 普通函数:拥有内部构造器属性,可用作构造器。
    • 箭头函数:缺少内部构造器属性,不可用作构造器。

    调用模式与对象实例化

    理解函数如何被调用至关重要,因为调用方式决定了函数内部'this'关键字的指向。根据Douglas Crockford在《JavaScript: The Good Parts》中阐述的理论,JavaScript函数存在四种调用模式:函数调用、构造函数调用、方法调用和间接调用。这些模式的存在是为了处理JavaScript函数可能具有的四种'this'对象值。

    构造函数调用模式详解

    构造函数调用模式是通过使用'new'操作符来调用函数,例如'a = new Product()'。这种方式会创建一个新对象,并将该新对象的内部'[[Prototype]]'链接到构造函数自身的'Product.prototype'上。因此,当实例P1和P2访问'P1.rating'时,即使'rating'不在P1自身上,也能成功从'Product.prototype'中获取到值9,这证明了实例与原型之间的关系。

    我们都认为我们了解JavaScript,直到我们深入探究其底层机制。
    表达式
    预期结果
    P1.rating
    9 (来自Product.prototype)
    P1.constructor
    Product 函数
    P1.constructor === P2.constructor
    true
    P1.constructor.prototype === P2.constructor.prototype
    true

    核心观点是:对象P1和P2虽然看起来是从'Product'函数创建的,但它们实际上是链接到了该函数的'Product.prototype'对象上。每一个新创建的对象都必须链接到一个已存在的对象,这是JavaScript继承模型的本质所在,而非简单地从函数创建。

    原型链的可视化与内部结构

    理解P1、P2与Product.prototype之间的关联是掌握原型的关键。当打印P1.constructor和P2.constructor时,结果均指向Product函数,这似乎与它们链接到Product.prototype的机制存在矛盾,但实际上两者描述的是同一继承结构的不同侧面。当深入探究'P1.constructor.prototype.constructor'时,会发现它最终会指向Product函数本身,证实了这种循环引用和链接的正确性。

    函数与对象的符号表示

    为了简化理解,函数被表示为圆形,对象则表示为方形或矩形。一旦函数被定义,无论是否被调用,JavaScript都会立即为其创建一个原型对象。实例对象P1和P2作为方形,通过指向Product函数原型的箭头(表示'prototype'链接)与圆形关联起来。

    • 定义Product函数(圆形)。
    • JavaScript自动创建Product.prototype对象(一个未命名的矩形)。
    • 实例P1和P2被创建(两个新的矩形)。
    • P1和P2通过'__proto__'链接到Product.prototype。

    在面试场景中,如果不对原型链结构有深刻理解,面对复杂的属性查询(如P1.constructor.prototype.constructor),很容易产生猜测性的错误答案。掌握这种图示化的结构,可以确保所有查询结果的准确性,因为在标准继承结构中,这些关系查询的结果几乎全部为'true'。

    __proto__与原型链的直接访问

    在早期的JavaScript实现中,要从实例P1追溯到其构造函数Product的原型对象,必须经过'P1.constructor.prototype'这条路径。然而,为了优化开发体验,Mozilla等浏览器厂商引入了'__proto__'属性。该属性提供了一个捷径,允许对象实例直接指向其被链接到的原型对象。

    __proto__如何简化路径

    每个对象都拥有一个'__proto__'属性,它直接指向创建该对象时所链接的那个原型对象。这意味着'P1.__proto__'的结果与'P1.constructor.prototype'的结果是相等的,极大地简化了原型链的遍历复杂度。掌握这一机制后,所有基于该结构的关系查询都将得到肯定的答案,例如P1.__proto__等于Product.constructor.prototype。

    目标属性
    传统路径
    快捷路径
    实例的原型对象
    P1.constructor.prototype
    P1.__proto__
    构造函数
    P1.constructor
    P1.__proto__.constructor

    将六行代码转化为复杂的关系图是理解JavaScript精髓的有效方法。当代码量增加时,这种结构化的图示对于追踪属性的来源至关重要。演讲者布置了一项挑战:实现'Animal'、'Dog'和'Cat'构造函数之间的继承,要求仅使用'prototype'、'constructor'和'__proto__'这三个核心关键字。

    原型链上的读写操作差异

    读取操作的机制

    读取操作(Read Operation)在原型链上是安全且直接的。当请求'P1.rating'时,JavaScript引擎首先在P1对象自身中查找'rating'。如果找不到,它会沿着'__proto__'链接继续搜索,直到找到属性值或到达原型链的顶端(即'Object.prototype'的'__proto__',通常为null)。读取操作永远不会导致错误,最多返回'undefined'。

    写入操作的陷阱

    写入操作(Write Operation)则更为微妙,因为它允许在运行时向对象添加或修改属性。如果对实例P2执行'P2.getDetails = ...',该属性会被直接设置在P2对象上,从而有效地“遮蔽”(shadowing)了原型链上'Product.prototype.getDetails'的定义。因此,P2调用'getDetails'时,会执行其自身定义的版本,而不是原型链上的版本,这导致了不同的执行结果。

    • 读取:遍历原型链,直到找到属性或到达链尾。
    • 写入:属性直接设置在当前实例对象上,不影响原型。
    • 结果:读取操作返回找到的值或undefined;写入操作成功创建或覆盖实例属性。

    Questions

    Common questions and answers from the video to help you understand the content better.

    JavaScript中箭头函数与普通函数在原型对象方面存在哪些根本区别?

    普通函数在定义时自动拥有一个可供实例继承的.prototype对象,而ES6的箭头函数则没有.prototype属性,其结果为undefined。

    为什么说箭头函数不能用作构造函数?

    箭头函数不能用作构造函数的原因在于它们缺少一个内部的'[[Constructor]]'属性,这是JavaScript引擎用来识别和执行构造函数调用的关键标记。

    在JavaScript中,如何通过实例对象直接访问其构造函数的原型对象?

    可以通过'实例.__proto__'属性直接访问实例对象所链接的原型对象,这比使用'实例.constructor.prototype'的路径更为快捷和直接。

    原型链上的读取操作和写入操作在行为上有何不同?

    读取操作会沿着原型链向上搜索属性,直到找到为止;而写入操作则在实例对象上直接设置属性,该操作会遮蔽原型链上的同名属性,而不会修改原型本身。

    Useful links

    These links were generated based on the content of the video to help you deepen your knowledge about the topics discussed.

    This article was AI generated. It may contain errors and should be verified with the original source.
    VideoToWordsClarifyTube

    © 2025 ClarifyTube. All rights reserved.