想象你出生在一个古老的大家族。每个新生儿都会收到两样东西:一本记载着全家族共同规矩的《传家手册》,以及族谱上一笔“父亲是谁”的暗记。手册让后代不用把家训抄写无数份,暗记让后代随时能顺着线索找到祖先。JavaScript 的对象世界就是这样一个家族——prototype 是传家手册,__proto__ 是认父暗记,constructor 是手册扉页上“编者签名”。这三样东西,构成了 JS 面向对象的灵魂:原型链。
当你访问一个对象的属性时,JS 会像翻抽屉一样去查找:先翻自己的抽屉,没有就打开爸爸的抽屉,再没有就打开爷爷的抽屉,一路翻到家族祠堂(Object.prototype),实在没有才摊手返回 undefined。本文就用这个家族故事,把 prototype、__proto__、constructor 的三角关系、四种继承方式的优劣、以及 ES6 class 的糖衣本质,一层层剥给你看。
吃透原型链,你才能真正读懂 JS 中九成的“奇怪行为”,这也是前端面试、框架源码阅读、大型应用架构设计的立足之本。
目录
可视化演示
本节配有交互动效(进入视口后自动加载,约 3KB)。
原型链:属性查找与继承机制 prototype指向原型对象,原型对象的constructor指回构造函数,实例的proto指向原型对象,三者构成三角关系 属性查找沿原型链向上追溯:先在实例自身查找,未找到则沿proto向上查找直到Object.prototype 寄生组合继承是最优模式:用call继承实例属性,用Object.create继承原型方法,避免父类构造函数调用两次 原型链是JS实现继承的核心,ES6 class是其语法糖
原型三件套:手册、暗链与签名
家族里管事的老人会告诉你:每一代新生儿都要在三样东西上留下印记。第一样是《传家手册》,它会郑重地写到“本族子弟共享以下规矩”——在 JS 里这就是 prototype,一个专门挂公共属性和方法的对象。第二样是族谱上的认父暗记,新生儿出生时家族自动记一笔“此子之父为谁”——这就是 __proto__。第三样是手册扉页的编者签名,写着手册是谁编的——这就是 constructor。三者共同回答了“我是谁、我爸是谁、家里的规矩在哪”。
翻译成技术语言:每个函数(除了箭头函数)天生带一个 prototype 属性,它是一个普通对象。当用 new Foo() 创建实例时,引擎做了三件事:新建一个对象、把它的 __proto__ 指向 Foo.prototype、最后执行 Foo 函数体把 this 上的属性填进去。而 Foo.prototype 上默认有一个 constructor 属性,指回 Foo 自己,形成闭环。
这套三角关系是后面所有继承方式的基石。记住:prototype 是“传给后代的东西”,__proto__ 是“我爹是谁”,constructor 是“手册谁编的”。新手最容易把前两个弄混,但用家族故事记就永远不会:手册是规矩本身,暗记是血缘线索,两者一物一链,天然不同。
__proto__ 找到手册,手册通过 constructor 找到编者,三者互为印证。- new Foo 诞生实例执行构造函数,得到一个全新对象 f,相当于家族里登记了一个新成员。
- 实例的认父暗记新对象的
__proto__被自动指向构造函数的prototype,也就是“我爸的传家手册”。 - 手册的编者签名
prototype对象上的constructor属性默认指回构造函数本尊。
属性查找:翻箱倒柜的巡查路线
新生儿想用一件传家宝,他并不知道东西放在哪。家族的规矩是:先翻自己房间的抽屉,没有就去爸爸房间翻,再没有就去爷爷房间翻,最后去祠堂的公共箱子里翻,翻到 null 这个“虚空”为止。JS 的属性访问 obj.x 走的正是这条巡查路线,术语叫“沿原型链查找”。路上如果某一层抽屉里有同名的物件,就立刻使用它并停下,不再往上翻——这叫遮蔽(shadowing),像你爸在自家抽屉放了一把和你爷爷一样的钥匙,你用的时候当然拿近的。
技术上的细节是:obj.x 先检查对象自身属性,找不到就沿 obj.__proto__ 逐级上溯。大部分普通对象的链都会汇合到 Object.prototype(家族祠堂),祠堂再往上就是 null,查找到此结束。特别地,obj.x = 1 这种赋值操作永远不会去改原型上的属性,它只会在自己身上新建或覆盖,所以遮蔽是“我自己的东西优先”。而 Object.create(null) 能造出一个连祠堂都不认的“孤儿对象”,彻底没有原型链。
理解这条巡查路线后,很多“怪现象”迎刃而解:为什么给实例加同名属性就能“覆盖”原型方法?因为遮蔽。为什么 hasOwnProperty 能区分“自己的”和“祖传的”?因为它不走这条路。属性查找是 JS 面向对象的地基,后面的继承方案全都是围绕“让这条链更长、更合理”做文章。
- obj[x] 起点访问属性时永远先查自身,相当于先翻自己的抽屉。
- 未命中就沿暗链上溯自身没有,就顺着
__proto__去父级对象身上继续找。 - 直到公共祖先逐层上溯,最终会到达所有普通对象共享的 Object.prototype。
- null 终点祠堂里也没有,就触到 null,查找终止并返回 undefined。
构造函数继承:借钱式复制家产
有一种继承思路很直接:孩子出生时,父母直接塞给他一份家产清单,把名下所有物件都复制一份给他,从此各过各的、互不相欠。在 JS 里,这叫构造函数继承(constructor stealing)。做法是在子构造函数里调用 Parent.call(this, ...)——call 的作用是“借”父构造函数来用,并强行把它的 this 改成子实例,于是父构造函数里所有 this.xxx = ... 的语句,属性全都落在了子实例自己身上。
这种方式的优点很实在:子实例的属性完全独立,改自己的不会影响别人;而且可以在调用时传入参数,每个孩子拿到不同的家产。但它有一个致命短板:父构造函数里 this.method = function(){} 这样定义的方法也被复制了一份到每个实例身上——家族明明有一本公共手册(Parent.prototype),却没人用,每个孩子都自己抄了一份家训,内存浪费严重。而且父原型上的方法,子实例通过这条链根本够不着。
用家族话说,构造函数继承是“分家产式继承”:孩子拿到一笔独立财产,却和家族祠堂断了血脉。所以它适合“只要属性、不要公共方法”的场景,更多时候它只是作为一个零件,被组合进更完善的方案里,就像拼盘里的一味主料。
- Parent.call 借用父构造把父构造函数当作普通函数“借”过来调用,它内部的 this 被替换。
- 属性落到子实例父函数体里
this.xxx = ...全部写进子实例自己的属性表。 - 原型链未接通子构造函数的 prototype 仍是默认对象,没有连到父原型上。
原型链继承:血缘式共享家传
另一条路是血缘式继承:不复制任何家产,孩子通过认祖归宗自动获得家族血脉。做法很粗暴:Child.prototype = new Parent()——把子构造函数的传家手册整个换成“一个父实例”。这样一来,子实例沿链走两步就能摸到 Parent.prototype 上的方法:实例 → Child.prototype(父实例)→ Parent.prototype。公共方法天然共享,不用复制,内存省了。
但血缘共享有个大坑:如果家产清单(原型)上放了引用类型的传家宝,比如一个数组 this.hobbies = [],所有子实例共用的是同一份数组。一个孩子往数组里加东西,其他孩子全看到了——这不是继承,这是“连坐”。更麻烦的是,创建 Child.prototype 时 new Parent() 的参数是写死的,子实例出生时没办法通过 new Child(args) 把参数传给父构造函数,每个孩子拿到的祖传属性都一样。
回看家族比喻:血缘继承打通了血脉、共享了祠堂,但祖产(引用属性)公有化导致一损俱损,且新生儿的个人参数传不进父辈。它适合“方法共享、无引用属性”的简单场景,但作为完整继承方案,它和构造继承一样只是半成品,所以才有了下面的拼盘。
- 子原型替换把 Child 的手册换成父实例,相当于让“孩子的爸爸”直接当手册。
- 两跳抵达父原型子实例沿链先到父实例,再到父原型。
- 方法共享父原型上的方法被子实例和子原型同时读到,不需要复制。
组合继承:拼盘方案的两难
聪明的工匠把前两种方案拼在一起:构造函数继承负责“分家产”——属性各自独立;原型链继承负责“通血脉”——方法公共共享。于是就有了组合继承(combination inheritance):在子构造函数里 Parent.call(this, args) 复制属性,再在外面 Child.prototype = new Parent() 接通血脉。它属性独立、方法共享、还能传参,看起来非常完美,一度是 JS 教科书上的“标准答案”。
但拼盘有个隐蔽的浪费:父构造函数被执行了两次。第一次在 Parent.call(this) 里,把属性复制到子实例上;第二次在 new Parent() 里,又把同样的属性做进了 Child.prototype。结果每个子实例身上有两份父属性:自己身上一份(用得上),原型上还有一份(永远被自身的遮蔽,白占内存)。名字里的“组合”二字,既是优点也是代价——它什么都好,就是不够干净。
用家族的话说,父母既塞了一份家产清单给孩子,又在祠堂里多存了一份一模一样的清单——孩子永远用自己那份,祠堂那份成了文物。组合继承是“能用但有点浪费”的方案,理解了它的冗余,你才能真正体会到寄生组合继承的巧妙。
- call 复制属性子构造函数内借父构造执行一次,实例获得自己的属性。
- new 再造父实例外面又 new 一个父实例当子原型,属性再次被创建。
- 父构造共两次同一份属性逻辑被执行两遍,实例和原型各存一份。
寄生组合继承:复印手册的最优解
能不能既接通血脉,又不在祠堂里多存一份冗余家产?工匠想出一个妙招:不直接 new 一个父实例当子原型,只用一台“复印机”把父原型的空壳复印一份,让子原型以父原型为模板诞生,再在上面写子类自己的方法。这台复印机就是 Object.create:它以参数为原型,返回一个全新对象,全程不调用任何构造函数。
于是寄生组合继承(parasitic combination inheritance)诞生了:
Object.create(Parent.prototype) 造出一个以父手册为模板的干净副本,子手册换上去后血脉通了;再把副本上的 constructor 修正回 Child(因为复印机会把 constructor 也继承成 Parent,得手动改签)。父构造函数只执行一次(在子构造函数里 call 的那次),没有冗余属性,属性独立、方法共享、能传参——四全其美。ES5 时代,这就是公认的最优继承方案。
回到家族故事:寄生组合继承等于只复印了《传家手册》的空白封面,封面签上“本册属 Child 支系”,里面的公共家训仍指向 Parent 祠堂,却不占用任何多余人手、不执行多余仪式。这个方案把原型链的灵活发挥到了极致,也是后面 ES6 class 内部悄悄做的事——先懂它,你才算真正看穿了继承的底牌。
- Object.create 复印模板以父原型为原型,创建一个空的新对象,不执行父构造函数。
- 换上新手册子构造函数的 prototype 换成这个干净副本,血脉接通。
- 修正 constructor 签名副本上原有的 constructor 要手动指回 Child。
function inherit(Child, Parent) { Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child;}
class 语法糖:同一套族谱的新装
很多从 Java 转过来的同学看到 class 关键字欣喜若狂,以为 JS 终于有了“标准面向对象”。其实 class 只是给原型链家族穿上的一件西装:内里还是那套手册、暗链、签名。class Foo {} 的本质是一个构造函数,typeof Foo === 'function';class Child extends Parent 的本质就是寄生组合继承那两行代码;子类里的 super() 本质就是 Parent.call(this),而且因为 this 必须等父构造初始化完毕才能使用,所以 super() 必须写在子构造函数第一行——这是“先认祖,再自立”的仪式感。
class 语法解决了老写法“容易手滑”的问题:方法自动挂在 prototype 上、继承关系一行声明、super 语法清晰,但它没有引入任何新的对象模型。new Child() 之后的对象,child.__proto__.constructor 依然是 Child,child.__proto__.__proto__ 依然是 Parent.prototype,一切照旧。换句话说,class 改的是笔迹,没改族谱。
用家族故事收个尾:class 是把散落在各处的认祖仪式、签到流程装订成一本《家规手册》,新人照着念就行,不用懂背后的族谱逻辑。但如果你想排查线上问题、读框架源码、或者手写 polyfill,就必须把糖衣剥开,看见里面那条朴素的 __proto__ 链——这正是本节的用意。
- class 声明本质上是定义了一个带 prototype 的函数,typeof 仍是 function。
- extends 自动接链引擎自动完成
Child.prototype与Parent.prototype的连接。 - super 先调用子构造函数里必须先 super(),相当于先执行父构造初始化 this。
实战排查三招:查谱、自查、孤儿
掌握了原理,还要会“破案”。前端排查继承相关问题,有三件趁手的工具。第一件是查族谱:a instanceof B 会从 a 出发沿原型链走,途中只要遇到 B.prototype 就返回 true,它回答的是“a 家谱上有没有 B 这一支”。第二件是自查:obj.hasOwnProperty('x') 只问“这个属性是不是 obj 自己身上的”,完全不看族谱,专门用来区分“我自己的”和“祖传的”。第三件是造孤儿:Object.create(null) 创建一个没有原型的对象,适合做纯字典,防止 __proto__ 被恶意污染,也不会被 for...in 意外翻出祖先的属性。
这三招常配合使用。比如遍历对象时,for...in 会把原型链上可枚举的属性也捞出来,所以标准写法是 for (const k in obj) { if (obj.hasOwnProperty(k)) ... };判断数组、日期等内置类型时用 arr instanceof Array 会受跨 iframe 影响,更稳妥的是 Object.prototype.toString.call(arr)。这些技巧的本质,都是对“属性查找路径”的精确控制——你知道链怎么走,就知道在哪一步设卡。
侦探三件套的家族版本:查出身看族谱(instanceof)、查私产看房本(hasOwnProperty)、要绝对干净就收养一个不认任何祖先的孤儿(Object.create(null))。这三招覆盖了日常九成的原型链排查需求,配合前面几节对链的理解,你已经能像一个“懂族谱的老族长”一样,从容解释任何属性查找的来龙去脉。
- obj 出发从对象自身开始,沿认父暗链逐层上跳。
- 寻找 F.prototype每跳一层,都比较当前层的原型手印是否等于构造函数的 prototype。
- 存在即真只要某一步命中,表达式为 true;走到 null 也没命中则为 false。
AI 应用场景
浏览器端 AI 模型封装与基类扩展
阶段:模型部署与前端集成
解决的问题:在浏览器里跑 TensorFlow.js、transformers.js 等 AI 推理库时,直接修改第三方库源码会在升级时被覆盖;传统做法是复制粘贴改代码,维护成本极高。利用 JS 的原型继承,可以优雅地继承官方基类,只覆写预处理、后处理等局部方法。
具体案例:某 AI 对话产品需要把用户输入统一加上 prompt 模板再交给模型。工程团队定义 class MyPipeline extends TextClassificationPipeline,在子类里重写 tokenizer 预处理逻辑,公共的模型加载、推理调度全部复用父原型上的实现。后续升级依赖库,只需重新测试子类,父类行为零改动。
统一 AI 错误处理与降级体系
阶段:应用工程与容错
解决的问题:AI 应用中错误类型繁杂(模型加载失败、推理超时、流式中断、鉴权过期),传统做法是到处 try/catch 再用字符串匹配错误码,极易写错且难以维护。借助原型链继承,可以建立统一的错误家族,再用 instanceof 沿链精准分流。
具体案例:一个流式对话服务定义了 class AIError extends Error {},再派生出 AITimeoutError、AIModelError、AIStreamError。全局错误处理器里用 err instanceof AITimeoutError 走重试、err instanceof AIModelError 走降级模型切换、其余走兜底提示。因为 instanceof 沿原型链查找,捕获子类错误时父类判断也能兜住,容错体系既灵活又严谨。
AIGC 低代码平台的组件继承
阶段:AI 应用构建与平台化
解决的问题:AIGC 搭建平台有大量画布组件(文本生成、图像生成、视频生成),若各自为政,公共的拖拽、缩放、状态管理逻辑会重复实现。把公共方法挂到组件基类的原型上,子组件按需继承和覆写,是典型的原型链应用。
具体案例:某 AI 绘图平台定义 class BaseNode { render() {...} bindEvents() {...} },ImageNode、TextNode、VideoNode 分别 extends 它并覆写 render。所有节点的公共生命周期方法都放在基类原型上,实例对象通过原型链共享,内存占用小、扩展新节点只需写差异代码。
Python 演示
"""Use Python to simulate JS prototype chain: lookup, shadowing, inheritance""" class JSObject: """A JS-like object: __dict__ stores own props, proto points to parent""" def __init__(self, proto=None): self.__dict__['proto'] = proto def set(self, name, value): self.__dict__[name] = value def get(self, name): node = self while node is not None: if name in node.__dict__: return node.__dict__[name] node = node.__dict__.get('proto') raise AttributeError('not found: ' + name) def make_person(name, age): p = JSObject() p.set('name', name) p.set('age', age) return p def make_student(name, age, school): """Combination inheritance: copy props first, then link parent on chain""" parent = make_person(name, age) s = JSObject() for key, value in parent.__dict__.items(): if key == 'proto': continue s.__dict__[key] = value s.__dict__['proto'] = parent s.set('school', school) return s if __name__ == '__main__': person_proto = JSObject() person_proto.set('species', 'human') alice = make_student('Alice', 10, 'No.1 School') print(alice.get('name')) print(alice.get('school')) print(alice.get('species')) bob = JSObject(person_proto) bob.set('species', 'robot') print(bob.get('species')) print(alice.get('species'))
while node is not None:从实例出发沿链走,直到 null 终点才停,对应 JS 原型链的终止条件。if name in node.__dict__:先看当前对象的自有属性表,相当于先翻自己的抽屉。node = node.__dict__.get('proto')没找到就取出认父暗记,跳到父级继续查。s.__dict__[key] = value模拟构造函数继承:把父实例的属性复制到子对象身上。s.__dict__['proto'] = parent模拟原型链继承:把父实例挂到子对象的链上。alice.get('species')实例自身没有 species,顺着链在父级身上找到,输出 human。bob.set('species', 'robot')遮蔽效应:bob 在自己身上新建同名属性,只影响自己。print(alice.get('species'))依然输出 human,证明遮蔽不影响链上的其他兄弟。
常见误区
误区我一直以为 `prototype` 和 `__proto__` 是一回事,都是“原型”。比如 `fn.prototype` 和 `fn.__proto__` 应该都指向同一个对象吧?写代码时经常混着用。
正解两者完全不同。prototype 是函数身上“传给后代的手册”,只有函数才有,new 出来的实例没有 prototype;__proto__ 是每个对象都有的“认父暗记”,指向构造它的函数的 prototype。用家族比喻:手册是家族公共规矩,暗记是你爸爸是谁,一物一链,方向相反。
为何常见:两个词都带“原型”字样,中文都翻译成“原型”,加上控制台打印时经常出现 [[Prototype]] 隐藏属性,初学者极易混淆。
误区既然函数都能 new 或者调用,那箭头函数应该也有 `prototype` 属性吧?我试过箭头函数 `() => {}`,感觉它也能当构造函数用。
正解普通函数(function 声明和表达式)都有 prototype,但箭头函数没有。箭头函数继承了外层作用域的 this,不能作为构造函数,不能 new,因此引擎不给它配 prototype。可以试试 typeof (() => {}).prototype,结果是 "undefined"。这就像家族里“入赘女婿”不传家族姓氏一样——没有后代手册,自然没有传家手册。
为何常见:大多数教程只讲“函数有 prototype”,没强调“普通函数”这个限定语;实际开发中箭头函数用得又多,踩坑后才反应过来。
误区JS 有了 class 关键字,应该和 Java 一样是编译期静态类型、类定义不可变、`this` 绑定和 Java 完全相同吧?以后可以完全按 Java 那套思维写 JS 了。
正解class 只是寄生组合继承的语法糖,底层依然是函数加原型链。typeof class Foo {} 是 "function",child.__proto__ 依然存在,实例方法还是挂在 prototype 上。它既没有引入新的内存模型,也没有静态类型检查,this 依旧由调用方式决定。糖衣再甜,拨开还是那条族谱链。
为何常见:class 关键字与 Java/C++ 同名同形,极易触发“语言相似性错觉”,加上 ES6 普及后很少有人去读转译产物。
误区我觉得组合继承调用了 `Parent.call(this)`,属性都复制到实例上了,所以组合继承不存在引用类型被共享的问题,只有原型链继承才有这个坑。
正解组合继承照样有共享问题。因为 Child.prototype = new Parent() 创建出来的原型对象上,本身就有一份父构造函数的引用属性(比如数组)。虽然实例自身的同名属性会遮蔽它,但如果子类代码通过 Child.prototype 去操作这个数组,或者在某些边界场景下,这隐藏的共享引用仍然存在。所以真正的根治方案是寄生组合继承:用 Object.create 造干净原型,完全不执行父构造函数,从源头消灭冗余和共享。
为何常见:学习者记住了“组合继承属性独立”这个结论,却忽略了它内部还藏着一个 new Parent(),那一次调用留下的原型属性正是隐患。
面试题
Q1. const f = new Foo(),请问 f 的 __proto__ 指向谁?
- A.Foo 的原型对象← 正确答案
- B.Foo 的实例对象
- C.f 自身的原型表
- D.Object 的原型对象
答案:A。new 的过程中,引擎会把实例的 __proto__ 指向构造函数的 prototype,所以 f.__proto__ === Foo.prototype。用家族话说,实例的“认父暗记”指向父亲手上的“传家手册”,而不是指向爸爸本人,更不是指向 Object 原型。
Q2. 要判断对象 obj 上是否有“自有属性 name”,最可靠的方法是?
- A.直接判断 name in obj
- B.调用 hasOwnProperty 检测← 正确答案
- C.判断 obj.name 不为空值
- D.遍历原型链上的所有属性
答案:B。name in obj 会沿原型链查找,把祖传属性也算进去;obj.name !== undefined 在属性值为 undefined 时误判;遍历全链效率低且把非自有也算入。hasOwnProperty 只看自身属性表,就像查房本只认户主名下的资产,不认祖上传下来的。
Q3. 关于组合继承(构造函数继承 + 原型链继承),下列说法正确的有?
- A.父构造函数会被调用两次← 正确答案
- B.子实例上存在冗余属性← 正确答案
- C.子类无法继承父类方法
- D.原型链上找不到父属性
答案:AB。组合继承里 Parent.call(this) 一次、Child.prototype = new Parent() 又一次,父构造函数总共执行两次,第二次在原型上留下冗余属性(被实例自身遮蔽但白占内存)。方法沿原型链正常继承,父属性也在链上,所以 C、D 错误。
Q4. 想让 Child 继承 Parent,且不产生冗余属性,最佳做法是?
- A.复制一份 Parent 的所有方法
- B.用 new Parent 当子类原型
- C.用 Object.create 建新原型← 正确答案
- D.把 Child 原型指针改写
答案:C。Object.create(Parent.prototype) 只以父原型为模板创建干净副本,不执行父构造函数,属性不冗余。new Parent() 会执行构造并留下冗余;复制方法是构造继承的写法,方法无法复用;“改写指针”指向不明,且直接 Child.prototype = Parent.prototype 会让子父原型共享,修改互相污染。
Q5. hasOwnProperty 方法的作用是?
- A.检查属性是否在原型链上
- B.检查属性是否为自有属性← 正确答案
- C.检查属性能否被枚举出来
- D.检查属性是否可以被删除
答案:B。hasOwnProperty 只检查属性是否存在于对象自身的属性表中,完全不沿原型链查找。它就像查房本——只认自己名下的资产,不认祖传财产。是否可枚举是 propertyIsEnumerable 管的事,能否删除是 delete 的事,都不属于它。
Q6. 下列哪些方式能判断 obj 是 Foo 的实例?
- A.使用 obj instanceof Foo← 正确答案
- B.用 Foo 原型 isPrototypeOf← 正确答案
- C.判断 obj 的 constructor 指向
- D.判断 obj 自身存在 prototype
答案:AB。instanceof 沿 obj 的原型链查找是否出现 Foo.prototype,是标准判断;Foo.prototype.isPrototypeOf(obj) 语义相同。obj.constructor 可以被修改或继承,不可靠;实例对象自身没有 prototype 属性,D 本身就不成立。
Q7. 关于箭头函数与 prototype,下列说法正确的是?
- A.箭头函数自带 prototype 表
- B.只有普通函数有 prototype← 正确答案
- C.箭头函数 prototype 为 null
- D.两者都没有 prototype 属性
答案:B。箭头函数没有 prototype 属性,(() => {}).prototype 是 undefined。因为它不能作为构造函数使用,没有“传家手册”的必要。但普通函数(function 声明和表达式)都有 prototype。注意“普通函数”也包括 class 的构造函数,它是带 prototype 的特殊函数。
Q8. 采用原型链继承(Child.prototype = new Parent()),会带来哪些问题?
- A.引用类型的属性被实例共享← 正确答案
- B.创建子实例时无法传参← 正确答案
- C.子实例完全无法继承方法
- D.父构造函数被调用两次
答案:AB。原型上的引用类型属性被所有实例共用,一个实例改动全部实例可见;new Parent() 的参数写死,实例创建时无法向父构造传参。方法是可以沿链继承的,所以 C 错;“父构造调用两次”是组合继承的缺点,原型链继承只调用一次,D 错。
Q9. ES6 的 class 继承本质是什么?
- A.与 Java 类完全相同的机制
- B.语法糖,底层仍是原型链← 正确答案
- C.全新的一套内存存储模型
- D.与普通函数没有任何关联
答案:B。class 是寄生组合继承的语法糖,extends 底层仍是 Object.create 连接原型链,super 底层是 Parent.call(this)。它没有引入新内存模型,typeof class {} 是 "function",和 Java 的类机制有本质区别。
Q10. Function.prototype 的 __proto__ 指向什么?
- A.Function 构造函数的原型
- B.Object 构造函数的原型← 正确答案
- C.指向 null 表示链的终点
- D.指向自身成环的闭环
答案:B。Function.prototype 虽然是个函数对象,但它的 __proto__ 指向 Object.prototype,因为一切对象(包括函数对象)最终都要汇入 Object 这条“家族主线”。Function.prototype.__proto__ === Object.prototype 为 true。它并不指向自己,也不直接指向 null——链的终点在 Object.prototype 之上才是 null。