news 2026/2/7 13:07:37

JavaScript学习笔记:18.继承与原型链

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaScript学习笔记:18.继承与原型链

JavaScript学习笔记:18.继承与原型链

上一篇用闭包搞定了“函数带状态”的难题,这一篇咱们来解锁JS面向对象的“底层逻辑”——继承与原型链。你肯定写过这样的代码:用构造函数创建“学生”对象,又创建“老师”对象,两者都有“姓名”“年龄”属性,还有“打招呼”的方法,却要重复写两遍逻辑。这就像家族里的每个人都要重新学走路、说话,完全浪费了“传承”的优势。

JS的继承机制,本质是“家族基因传承”:每个对象都有自己的“基因图谱”(原型链),能继承祖先(原型对象)的“天赋技能”(方法)和“家族特质”(属性)。但和Java这类语言的“类继承”不同,JS玩的是“原型继承”——没有真正的“类”,全靠对象之间的“原型关联”实现传承。今天咱们就用“家族族谱”的比喻,把原型、原型链、继承方式这三大核心彻底讲透,让你明白“为什么数组能直接用push方法”“为什么子类能继承父类的属性”。

一、先破案:为什么需要继承?重复代码有多坑?

在没有继承的年代,写相似对象全靠“复制粘贴”,坑点能把人逼疯。咱们先看一个反例:

// 学生构造函数functionStudent(name,age){this.name=name;this.age=age;// 重复的方法:学生和老师都要打招呼this.sayHi=function(){console.log(`我叫${this.name},今年${this.age}`);};}// 老师构造函数functionTeacher(name,age,subject){this.name=name;// 重复的属性this.age=age;// 重复的属性this.subject=subject;// 重复的方法:和学生的sayHi几乎一样this.sayHi=function(){console.log(`我叫${this.name},今年${this.age}岁,教${this.subject}`);};}// 创建实例conststudent=newStudent("张三",18);constteacher=newTeacher("李老师",30,"数学");

这段代码的问题很明显:

  1. 属性方法重复:“姓名”“年龄”和基础版sayHi重复编写,修改时要改两处;
  2. 内存浪费:每个实例都有独立的sayHi方法,100个实例就有100个相同函数,纯属浪费内存;
  3. 无统一管理:学生和老师的“家族关联”没体现,无法统一扩展(比如给所有“人”加“性别”属性,要改两个构造函数)。

而继承的核心作用,就是解决“代码复用”和“关系关联”——让学生和老师都继承“人”的基础属性和方法,再各自扩展自己的特色(比如老师的“科目”),就像家族后代继承祖先的通用能力,再发展自己的特长。

二、核心概念:原型链的“三驾马车”——原型、proto、构造函数

要理解继承,必须先搞懂JS对象的“三角关系”:构造函数(家族老祖宗的“生育手册”)、原型对象(家族的“祠堂”,存着共有的技能)、实例对象(家族后代)。这三者靠prototype__proto__两个属性关联,形成“原型链”。

1. 原型对象(prototype):家族的“祠堂”

每个函数(尤其是构造函数)都有一个prototype属性,指向一个“原型对象”。这个对象就是“家族祠堂”——存着所有实例都能共享的方法和属性,比如“人”的原型对象存着“打招呼”方法,学生和老师的实例都能来“借用”。

关键特性:所有实例共享原型对象的属性和方法,修改原型对象,所有实例都会同步变化。

// 构造函数(生育手册:创建“人”的规则)functionPerson(name,age){this.name=name;// 实例自身属性(每个后代的专属信息)this.age=age;}// 原型对象(祠堂:共享技能)Person.prototype.sayHi=function(){console.log(`我叫${this.name},今年${this.age}`);};// 创建实例(家族后代)constzhangsan=newPerson("张三",18);constlisi=newPerson("李四",20);// 实例共享原型的sayHi方法zhangsan.sayHi();// 我叫张三,今年18岁lisi.sayHi();// 我叫李四,今年20岁// 验证:两个实例的sayHi是同一个函数(共享祠堂的技能)console.log(zhangsan.sayHi===lisi.sayHi);// true(内存优化)

2. 实例的__proto__:后代的“族谱查询权”

每个实例对象都有一个__proto__属性(注意是双下划线,浏览器原生支持,ES6标准化为Object.getPrototypeOf()),这个属性指向创建它的构造函数的prototype——相当于后代手里的“族谱”,用来查找祠堂里的共享技能。

// 实例的__proto__ === 构造函数的prototype(族谱指向祠堂)console.log(zhangsan.__proto__===Person.prototype);// trueconsole.log(lisi.__proto__===Person.prototype);// true// 用ES6方法获取原型(推荐,避免直接操作__proto__)console.log(Object.getPrototypeOf(zhangsan)===Person.prototype);// true

3. 原型对象的constructor:祠堂的“祖宗牌位”

原型对象上有个constructor属性,指向对应的构造函数——相当于祠堂里的“祖宗牌位”,告诉后代“你们的老祖宗是谁”。

// 原型对象的constructor指向构造函数console.log(Person.prototype.constructor===Person);// true// 实例通过原型链访问constructor(查族谱找到祖宗)console.log(zhangsan.constructor===Person);// trueconsole.log(lisi.constructor===Person);// true

总结“三角关系”:

构造函数(Person) → prototype → 原型对象(Person.prototype) 实例(zhangsan) → __proto__ → 原型对象(Person.prototype) 原型对象(Person.prototype) → constructor → 构造函数(Person)

用族谱比喻就是:老祖宗(构造函数)留下生育手册(prototype)指向祠堂(原型对象),后代(实例)拿着族谱(proto)找到祠堂,祠堂里的牌位(constructor)指向老祖宗。

三、原型链的工作原理:“找族谱查技能”

原型链的核心是“链式查找”——实例访问属性或方法时,会按以下顺序查找:

  1. 先找实例自身的属性/方法;
  2. 找不到,就通过__proto__原型对象的属性/方法;
  3. 还找不到,就通过原型对象的__proto__原型的原型(比如Person.prototype.__proto__指向Object.prototype);
  4. 一直找到Object.prototype(原型链的顶端),再找不到就返回undefined

就像后代找一项技能,先查自己会不会,不会就查族谱找父亲,再找爷爷,直到家族老祖宗(Object),还不会就说“不会”。

例子:数组的push方法来自哪里?

constarr=[1,2,3];arr.push(4);// 为什么数组能直接用push?// 查找链:// 1. arr自身有没有push?→ 没有// 2. arr.__proto__ → Array.prototype,有没有push?→ 有(找到!)console.log(arr.__proto__===Array.prototype);// trueconsole.log(Array.prototype.hasOwnProperty('push'));// true// 3. Array.prototype的原型是Object.prototype(族谱继续往上)console.log(Array.prototype.__proto__===Object.prototype);// true// 4. Object.prototype的原型是null(族谱顶端,再往上没有了)console.log(Object.prototype.__proto__===null);// true

原型链结构图(简化):

arr → __proto__ → Array.prototype → __proto__ → Object.prototype → __proto__ → null

四、JS继承的演进:从“手动认亲”到“正规族谱”

JS的继承方式不是一步到位的,经历了从早期“手动修改族谱”到ES6“class语法糖”的演进,核心都是围绕“原型链”做文章。

1. 1.0 原型链继承(早期方案:直接修改__proto__)

核心:让子类实例的__proto__指向父类实例,从而继承父类的属性和方法。

// 父类:PersonfunctionPerson(name,age){this.name=name;this.age=age;}Person.prototype.sayHi=function(){console.log(`我叫${this.name}`);};// 子类:Student(继承Person)functionStudent(name,age,grade){this.grade=grade;// 子类特有属性}// 原型链继承:让Student的原型指向Person实例(手动认亲)Student.prototype=newPerson();// 修复constructor(牌位错位了,要修正)Student.prototype.constructor=Student;// 创建子类实例conststudent=newStudent("张三",18,"高三");student.sayHi();// 我叫undefined(问题1:父类属性没初始化)console.log(student.grade);// 高三(子类属性正常)
缺点:
  • 父类的实例属性会被所有子类实例共享(比如父类有数组属性,一个子类修改会影响所有);
  • 无法向父类构造函数传递参数(上面的name和age是undefined)。

2. 2.0 借用构造函数(解决属性继承问题)

核心:在子类构造函数中用call/apply调用父类构造函数,手动初始化父类属性。

functionPerson(name,age){this.name=name;this.age=age;this.hobbies=["篮球"];// 引用类型属性}Person.prototype.sayHi=function(){console.log(`我叫${this.name}`);};functionStudent(name,age,grade){// 借用父类构造函数:初始化父类属性(每个子类实例独立拥有)Person.call(this,name,age);this.grade=grade;}// 原型链继承:继承父类的方法Student.prototype=Object.create(Person.prototype);Student.prototype.constructor=Student;// 测试:conststudent1=newStudent("张三",18,"高三");conststudent2=newStudent("李四",17,"高二");student1.hobbies.push("游戏");console.log(student1.hobbies);// ["篮球", "游戏"]console.log(student2.hobbies);// ["篮球"](问题解决:属性不共享)student1.sayHi();// 我叫张三(方法继承成功)
优点:
  • 父类属性独立(每个子类实例有自己的属性,引用类型不共享);
  • 能向父类构造函数传递参数。

这就是“组合继承”——借用构造函数(继承属性)+ 原型链继承(继承方法),是早期最常用的继承方案。

3. 3.0 寄生组合继承(优化组合继承)

组合继承的缺点是:父类构造函数会被调用两次(一次是new Person(),一次是Person.call()),导致子类原型上有多余的父类实例属性。寄生组合继承解决了这个问题:

functionStudent(name,age,grade){Person.call(this,name,age);// 只调用一次父类构造函数this.grade=grade;}// 优化:用Object.create创建父类原型的副本,避免调用父类构造函数Student.prototype=Object.create(Person.prototype,{constructor:{value:Student,enumerable:false// 不可枚举,符合原生行为}});// 效果和组合继承一致,但性能更好(少调用一次父类构造函数)conststudent=newStudent("张三",18,"高三");console.log(student.sayHi());// 正常继承

4. 4.0 ES6 class继承(语法糖,底层还是原型链)

ES6的classextends让继承变得更简洁,本质是上面“寄生组合继承”的语法糖,底层依然依赖原型链。

// 父类(用class声明,对应之前的构造函数)classPerson{constructor(name,age){this.name=name;this.age=age;}// 原型方法(对应Person.prototype.sayHi)sayHi(){console.log(`我叫${this.name}`);}// 静态方法(对应Person.staticMethod,不继承给实例)staticcreate(name,age){returnnewPerson(name,age);}}// 子类:用extends继承父类classStudentextendsPerson{constructor(name,age,grade){super(name,age);// 相当于Person.call(this, name, age),必须先调用superthis.grade=grade;}// 子类特有方法study(){console.log(`${this.name}${this.grade}学习`);}}// 创建实例conststudent=newStudent("张三",18,"高三");student.sayHi();// 我叫张三(继承父类方法)student.study();// 张三在高三学习(子类方法)console.log(studentinstanceofStudent);// trueconsole.log(studentinstanceofPerson);// true(原型链关联)// 静态方法继承constperson=Student.create("李四",20);// 继承父类的静态方法
关键说明:
  • extends本质是设置原型链:Student.prototype.__proto__ = Person.prototype
  • super()在构造函数中必须先调用,用来初始化父类的this
  • class的方法默认是原型方法,static修饰的是静态方法(属于类,不继承给实例)。

五、避坑指南:原型链的“常见陷阱”

1. 陷阱1:修改原型对象后,constructor指向错误

直接给子类原型赋值为父类实例/Object.create的结果,会导致constructor指向父类,需要手动修复:

// 反面例子:修改原型后没修复constructorStudent.prototype=Object.create(Person.prototype);console.log(Student.prototype.constructor===Person);// true(牌位错位)conststudent=newStudent();console.log(student.constructor===Person);// true(错误:学生的祖宗变成了Person)// 正面例子:修复constructorStudent.prototype.constructor=Student;console.log(student.constructor===Student);// true(正确)

2. 陷阱2:原型对象是引用类型,导致实例共享属性

如果父类原型上有引用类型属性(比如数组、对象),所有子类实例会共享这个属性,修改一个会影响所有:

// 反面例子:原型上放引用类型属性Person.prototype.hobbies=["篮球"];conststudent1=newStudent("张三",18,"高三");conststudent2=newStudent("李四",17,"高二");student1.hobbies.push("游戏");console.log(student2.hobbies);// ["篮球", "游戏"](被影响,坑!)// 正面例子:引用类型属性放在构造函数中(每个实例独立拥有)functionPerson(name,age){this.hobbies=["篮球"];// 实例自身属性,不共享}

3. 陷阱3:__proto__prototype混淆

很多新手分不清这两个属性,记住一句话:

  • prototype函数的属性,指向原型对象;
  • __proto__实例的属性,指向原型对象;
  • 两者都指向同一个原型对象,只是所属主体不同。
functionPerson(){}constp=newPerson();console.log(Person.hasOwnProperty('prototype'));// true(函数有prototype)console.log(p.hasOwnProperty('prototype'));// false(实例没有prototype)console.log(p.hasOwnProperty('__proto__'));// true(实例有__proto__)

4. 陷阱4:ES6 class的this绑定问题

class方法中的this默认指向实例,但如果单独提取方法调用,this会丢失(指向全局/undefined):

classPerson{constructor(name){this.name=name;}sayHi(){console.log(`我叫${this.name}`);}}constp=newPerson("张三");consthi=p.sayHi;hi();// 报错:Cannot read properties of undefined (reading 'name')(this丢失)// 解决方案1:绑定thisconsthiBind=p.sayHi.bind(p);hiBind();// 正常// 解决方案2:用箭头函数(绑定实例this)classPerson{constructor(name){this.name=name;this.sayHi=()=>console.log(`我叫${this.name}`);}}

六、实战场景:原型链的“实际应用”

场景1:扩展原生对象的方法(谨慎使用)

通过修改原生对象的原型,给所有实例添加方法(比如给数组加去重方法):

// 给数组扩展去重方法Array.prototype.unique=function(){return[...newSet(this)];};constarr=[1,2,2,3];console.log(arr.unique());// [1, 2, 3](所有数组都能使用)

⚠️ 注意:尽量避免修改原生对象原型(可能和其他库冲突),推荐用工具函数替代。

场景2:实现自定义类的继承链(比如UI组件)

前端开发中,UI组件常常用继承实现复用,比如“基础组件”→“按钮组件”→“提交按钮组件”:

// 基础组件classBaseComponent{constructor(el){this.el=el;}render(){console.log(`渲染组件到${this.el}`);}}// 按钮组件(继承基础组件)classButtonextendsBaseComponent{constructor(el,text){super(el);this.text=text;}render(){super.render();// 调用父类renderthis.el.innerText=this.text;}}// 提交按钮组件(继承按钮组件)classSubmitButtonextendsButton{constructor(el,text){super(el,text);this.el.addEventListener('click',()=>this.onClick());}onClick(){console.log("提交按钮点击");}}// 使用组件constbtn=newSubmitButton(document.querySelector('button'),"提交");btn.render();// 渲染组件到button,设置文本为“提交”

七、总结:继承与原型链的核心本质

JS的继承既不是“类继承”,也不是“对象继承”,而是“原型链继承”——所有对象通过__proto__串联成链,共享原型对象的属性和方法,实现代码复用。

核心要点总结:

  1. 原型链是“族谱”,实例访问属性时顺着族谱往上找;
  2. 构造函数、原型对象、实例的“三角关系”是理解的关键;
  3. ES6 class是原型链的语法糖,底层逻辑没变;
  4. 继承的核心价值是“代码复用”,避免重复编写相似逻辑。

原型链是JS的“底层骨架”,理解它不仅能写出更优雅的面向对象代码,还能看懂框架的底层实现(比如React组件的继承、Vue的响应式原理)。这篇笔记也是JS基础部分的重要收尾,后续我们会进入更高级的实战内容(如DOM操作、异步进阶)。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/6 17:55:35

Kotaemon AWS EC2部署实例:国际业务首选

Kotaemon AWS EC2部署实例:国际业务首选 在跨国企业加速数字化转型的今天,客户对智能客服系统的期待早已超越“能回答问题”这一基础要求。他们希望获得准确、连贯且符合本地语境的服务体验——而这背后,是一整套复杂技术栈的协同运作。尤其当…

作者头像 李华
网站建设 2026/2/4 23:59:33

实在没货,简历(软件测试)咋写?

简历咋写,这是很多没有【软件测试实际工作经验】的同学们非常头疼的事情。 简历咋写?首先你要知道简历的作用。 简历的作用是啥呢?一句话就是:让HR小姐姐约你。 如何让HR看你一眼,便相中你的简历,实现在…

作者头像 李华
网站建设 2026/2/5 15:06:01

网约车服务端线上流量巡检与测试验收技术

网约车服务端承接了网约车核心交易流程整体链路串联工作,其涵盖交易细粒度的场景达百万级别,核心交易链路涉及几百个下游服务。这督促我们的质量保障手段要粗细结合,既能保障核心业务的服务可用性,又要保障海量用户场景的正确运行…

作者头像 李华
网站建设 2026/2/5 5:44:36

公考日记7

乘法:

作者头像 李华
网站建设 2026/2/5 21:54:07

科研实验室温湿度监控新范式:以太网 POE 技术全场景解决方案

科研实验室利用以太网POE供电温湿度传感器进行温湿度监控系统的方案设计需要考虑哪些因素?结合科研实验室的场景特殊性、POE 技术特性及温湿度监控的核心需求,方案设计需重点考虑以下8 大核心因素,覆盖技术选型、场景适配、系统稳定性、合规性…

作者头像 李华