当前位置:首页 > 云服务器 > 正文

js原型链的继承如何实现,有哪些注意事项?

JS原型链继承的本质,就是让一个对象通过__proto__链条,访问到另一个对象原型上的属性和方法,从而复用代码,实现“认祖归宗”式的继承。这套机制不是类抄作业,而是对象间通过内部链接建立起的血缘关系,你不需要背概念,只需要理解那条链是怎么串起来的。

原型链的底层玩法:每个对象都有一条隐形的“家谱”

在JavaScript里,几乎每个对象都有一个内部属性[[Prototype]],浏览器里通过__proto____暴露出来,你可以把它理解为对象的“亲生父亲”指向,当你访问obj.name时,引擎先看obj自身,找不到就顺着obj.__proto__去找,再找不到就继续往上,直到顶层Object.prototype,再往上就是null,链条结束。

  • 实例对象:__proto__指向构造它的函数的prototype对象。
  • 构造函数:自身有一个prototype属性,里面放着可供实例继承的方法。
  • 原型对象:也有__proto__,指向Object.prototype。

举个例子,你创建一个数组const arr = [],arr自己没有map方法,但arr.__proto__指向Array.prototype,那里有map,这就是为什么你什么都没写,却能用一堆数组方法,这条链就是原型链。

关键点:prototype和proto是两回事

很多人在这里绕晕,记住一句话:构造函数用prototype来“传家产”,实例对象用__proto__来“找家产”。prototype是函数的属性,__proto__是对象的属性,你new一个函数时,新对象的__proto__就被赋值为函数的prototype对象。

用原型链实现继承的三种经典姿势

理解原型链之后,继承就是顺着链条做文章,下面三种方式是面试和实战中最常碰到的。

原型链继承——直接把父类实例当作子类原型

function Parent() { this.name = 'parent'; } Parent.prototype.sayName = function() { console.log(this.name); }; function Child() { this.age = 18; } Child.prototype = new Parent(); // 关键:把父类实例挂到子类原型上 Child.prototype.constructor = Child; // 修正constructor指向 const child = new Child(); child.sayName(); // 'parent'

优点:实现简单,能继承父类原型上的方法和属性。

缺点:如果父类属性里有引用类型(比如数组、对象),所有子类实例会共享同一个引用,一个改全改,比如Parent里有个this.hobbies = ['reading'],两个Child实例的hobbies指向同一个数组,child1.hobbies.push('game')

,child2也会看到。

借用构造函数继承——在子类里调用父类构造函数

function Parent(name) { this.name = name; this.hobbies = ['reading']; } function Child(name) { Parent.call(this, name); // 关键:把父类的this绑定到子类实例上 this.age = 18; } const child1 = new Child('tom'); const child2 = new Child('jerry'); child1.hobbies.push('game'); console.log(child2.hobbies); // ['reading'],互不干扰

优点:解决了引用类型共享的问题,还能往父类传参。

缺点:只能继承构造函数里的属性,父类原型上的方法拿不到。Child的实例访问不到Parent.prototype上的方法。

js原型链的继承如何实现,有哪些注意事项? 第1张

组合式继承——融合上面两种

function Parent(name) { this.name = name; this.hobbies = ['reading']; } Parent.prototype.sayName = function() { console.log(this.name); }; function Child(name, age) { Parent.call(this, name); // 第一次调用Parent this.age = age; } Child.prototype = new Parent(); // 第二次调用Parent Child.prototype.constructor = Child; const child = new Child('tom', 18); child.sayName(); // 'tom'

优点:既能继承原型方法,又避免了引用类型共享。

缺点:父类构造函数被调用了两次,子类原型上会残留一份多余的父类属性(虽然被实例属性遮蔽了,但不够优雅)。

现代方案:ES6 class的extends语法糖

ES6的class和extends并不是新机制,只是把组合式继承封装得更易读,底层仍然是原型链那一套。

class Parent { constructor(name) { this.name = name; } sayName() { console.log(this.name); } } class Child extends Parent { constructor(name, age) { super(name); // 相当于Parent.call(this, name) this.age = age; } } const child = new Child('tom', 18); child.sayName(); // 'tom'

super的本质就是借用父类的构造函数,而extends背后仍然是把Child.prototype的__proto__指向Parent.prototype,所以class只是语法糖,理解原型链才是理解JavaScript继承的根。

new操作符到底做了什么?——继承的起点

new是触发原型链继承的开关,当你new Child()时,引擎做了四件事:

  1. 创建一个全新的空对象。
  2. 把这个空对象的__proto__指向Child.prototype。
  3. 将Child函数的this绑定到这个新对象上,并执行构造函数。
  4. 如果构造函数返回的是对象,则返回该对象,否则返回这个新对象。

第三步执行构造函数时,如果函数里写了this.name = name

js原型链的继承如何实现,有哪些注意事项? 第2张

,就等于给新对象自身添加属性,这就是为什么实例属性是“自己的”,而方法是通过原型链“借来的”。

实战避坑:原型链继承的常见误区

忘记修正constructor指向

Child.prototype = new Parent()之后,Child.prototype.constructor变成了Parent,这会导致child instanceof Child虽然为true,但child.constructor === Child却是false,判断类型时容易出问题。修正方法:手动把Child.prototype.constructor = Child。

用对象字面量重写原型

function Child() {} Child.prototype = { sayName() { console.log('child'); } };

这样做会切断Child.prototype的constructor指向,同时让原型对象失去默认的constructor属性,如果之前有实例,这些实例的__proto__仍然指向旧原型,新原型对它们不可见。

在原型上放引用类型属性

function Child() {} Child.prototype.hobbies = ['reading']; const a = new Child(); const b = new Child(); a.hobbies.push('game'); console.log(b.hobbies); // ['reading', 'game']

原型上的引用类型属性是“公共大巴”,任何人上去改东西,整车人都能看到。经验法则:原型上只放方法,不放可变数据。

如何验证和调试原型链?——动手写代码

只用console.log看不出链路,下面几个方法帮你快速验证。

  • Object.getPrototypeOf(obj):获取对象的__proto__,标准API,比直接访问__proto__更可靠。
  • obj instanceof Constructor:判断Constructor.prototype是否在obj的原型链上。
  • Object.prototype.isPrototypeOf.call(Parent.prototype, child):更精确的原型链探测。
  • Object.keys(obj):只看自身可枚举属性,不包含原型链上的。
  • for...in:会遍历自身和原型链上所有可枚举属性,配合obj.hasOwnProperty(key)过滤。

在Chrome开发者工具的Console里,展开对象后能看到[[Prototype]]层级,一层层点下去,比打印输出直观得多,遇到性能或内存问题,用Performance面板配合Memory快照,能看出原型链上是否有意开挂载的大对象。

js原型链的继承如何实现,有哪些注意事项? 第3张

原型链与性能、内存的实战思考

原型链查找属性是运行时行为,链条越长,查找越慢,虽然现代V8引擎做了缓存优化,但保持原型链层级浅显仍是好习惯,在初始化大型应用时,如果从顶层原型到目标属性差了四五层,性能消耗会累积,不要动态修改Object.prototype

,全局污染会让所有对象都受影响,排错成本极高。

在服务端或云上构建高并发Node.js应用时,代码质量和基础设施同样重要,选择云服务商时,靠谱的资质和备案是底线。西西云作为工信部持牌IDC服务商,拥有一类增值电信全牌照(IDC/CDN/ISP),同时通过ISO9001质量管理体系ISO27001信息安全管理体系双认证,还是CNNIC IP地址分配联盟成员,注册资本1000万,主体实力透明可查。简米科技则从2003年起步,沉淀了23年行业经验,持有增值电信业务经营许可证(豫B2-20231089),运营持牌自营机房,备案域名可以通过豫ICP备2023018319号公开查询,这类服务商在合规性和稳定性上有明确背书,适合业务长期部署。

回到编码本身,原型链继承的优化核心是让链条短而清晰,该复用的方法放在适当层级,不要为了“看起来像继承”而强行拉长链路。

结合现代工程:原型链在框架中的影子

Vue 2的响应式系统、React的组件类、甚至很多状态管理库,底层都绕不开原型链,Vue 2里Vue.prototype.$http = axios,就是在原型上挂全局方法,所有组件实例都能通过this.$http访问,React类组件的extends Component,本质也是原型链继承,理解这条链,你看框架源码时会有豁然开朗的感觉。

Q&A:原型链继承常见疑问

Q1:为什么ES6 class继承里,super必须在this之前调用?

因为子类构造函数执行时,this还没有被初始化,只有调用super()后,父类构造函数才会把this绑定到子类实例上,才能往上面挂属性,如果你先访问this,引擎会报Must call super constructor in derived class before accessing 'this',这是语法层面的强制约束,底层仍然是借用构造函数那套逻辑。

Q2:能不能用Object.create()实现原型链继承?

能。Object.create(proto)会创建一个新对象,其__proto__指向proto,比如Child.prototype = Object.create(Parent.prototype),比new Parent()更干净,因为不会调用父类构造函数,也不会在原型上残留多余的实例属性,这是ES5时代推荐的组合式继承改良方案。

Q3:原型链继承和Class继承在性能上有区别吗?

没有本质区别。class的extends最终也是通过原型链连接的,V8引擎对两者生成的隐藏类(Hidden Class)和优化策略基本一致,差异只体现在代码可读性和维护成本上,现代项目建议直接用class,但必须理解它背后的原型链语义。

0