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和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。
将六行代码转化为复杂的关系图是理解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.
