当前位置: 首页 > ai >正文

JavaScript 面向对象 原型和原型链 继承

 面向对象 原型和原型链

一、原型对象

  1. 定义:所有构造函数在初始化时,会自动生成一个特殊的实例化对象,构造函数的prototype属性指向该对象,这个对象就是原型对象(prototype 对象)。

  2. 核心特点

    • 实例化对象通过__proto__属性指向构造函数的原型对象
    • 实例访问属性 / 方法时,先查自身,再通过__proto__查原型对象
    • 原型对象的主要作用是实现继承和共享方法
  3. 原型对象默认属性

    • constructor:指向构造函数本身
    • __proto__:指向其父级构造函数的原型对象
function Student(name) {this.name = name;
}// 原型对象
console.log(Student.prototype); 
// constructor指向构造函数
console.log(Student.prototype.constructor === Student); // true
// 实例对象的__proto__指向原型对象
const s = new Student('Tom');
console.log(s.__proto__ === Student.prototype); // true

二、原型链

  1. 定义:由一系列__proto__属性串联起来的原型对象链称为原型链。

  2. 查找机制

    • 访问对象属性 / 方法时,先查自身
    • 若无,则通过__proto__查原型对象
    • 若无,则继续查原型的原型,直至Object.prototype
    • Object.prototype.__proto__null,查找终止
  3. 示意图

    plaintext

    实例对象 -> 构造函数.prototype -> 父构造函数.prototype -> ... -> Object.prototype -> null
    

三、典型面试题解析

数组去重方法添加

Array.prototype.unique = function() {const res = [];for (let i = 0; i < this.length; i++) {if (!res.includes(this[i])) {res.push(this[i]);}}return res;
};const arr = [1, 2, 2, 3, 3, 3];
console.log(arr.unique()); // [1, 2, 3]

异步循环输出问题

for(var i = 0; i < 10; i++){setTimeout(function(){console.log(i, 'inner'); // 10个10 'inner'}, 0);
}
console.log(i, 'outer'); // 10 'outer'// 原因:var声明的i是函数作用域,循环结束后i=10,定时器回调访问的是同一个i

this 指向问题

var x = 1234;
function test(){var x = 4567;console.log(this.x);  
}
test(); // 1234(this指向window)
var o = {x:5678, m:test};
o.m(); // 5678(this指向o)

函数与原型方法优先级

function Foo() {getName = function () { console.log(1); };return this;
}
Foo.getName = function () { console.log(2); };
Foo.prototype.getName = function () { console.log(3); };
var getName = function () { console.log(4); };
function getName() { console.log(5); }Foo.getName(); // 2(调用静态方法)
getName(); // 4(函数表达式覆盖函数声明)
Foo().getName(); // 1(全局getName被重写)
new Foo().getName(); // 3(调用原型方法)

原型链查找案例

function A(){};
function B(a){ this.a = a};
function C(a){ if(a){this.a = a;}}
A.prototype.a = 1;
B.prototype.a = 1;
C.prototype.a = 1;console.log(new A().a); // 1(来自原型)
console.log(new B().a); // undefined(实例无a属性)
console.log(new C(2).a); // 2(实例有a属性)

静态方法与实例方法

function Cat(name){this.name = name;
}
Cat.say = function(){ // 静态方法console.log(this.name1);
};
Cat.name1 = "kmf";const cat1 = new Cat('cat1');
cat1.say(); // 报错(实例无法访问静态方法)
Cat.say(); // "kmf"(静态方法中this指向构造函数)

面向对象 - 继承

一、继承基础:call () 与 apply ()

JS 继承的底层依赖 call() 和 apply() 方法,二者的核心作用是改变函数内 this 的指向,从而让子类构造函数能 “借用” 父类的构造逻辑,实现构造属性的继承。

1. 语法对比

方法语法特点
call()父类构造函数.call(thisObj, arg1, arg2, ...)参数需逐个传入
apply()父类构造函数.apply(thisObj, [arg1, arg2, ...])参数需以数组形式传入

2. 核心作用

在子类构造函数中调用 父类.call(this, 参数),可将父类构造函数的 this 指向子类实例,使子类实例能继承父类的构造属性 / 方法。

二、7 种常见继承方式

根据继承的对象(构造属性 / 原型属性)和实现逻辑,JS 继承可分为 7 种核心方式,各有优缺点,需根据场景选择。

1. 构造继承(借助 call/apply)

实现逻辑

仅通过 call() 或 apply() 借用父类构造函数,让子类继承父类的构造属性 / 方法,不涉及原型属性的继承。

代码示例
// 父类:Person(含构造属性和构造方法)
function Person(name, sex, age) {this.name = name; // 构造属性this.sex = sex;this.age = age;this.play = function() { // 构造方法console.log(`${this.name}在玩`);};
}
// 父类原型属性(构造继承无法继承)
Person.prototype.eat = function() {console.log(`${this.name}在吃饭`);
};// 子类:Student(继承Person的构造属性,新增学号)
function Student(name, sex, age, stuID) {// 核心:通过apply改变this指向,继承Person的构造属性/方法Person.apply(this, [name, sex, age]); this.stuID = stuID; // 子类新增构造属性this.study = function() { // 子类新增构造方法console.log(`${this.name}在学习`);};
}// 测试
const stu1 = new Student('Jack', '男', 18, '1001');
console.log(stu1.name); // Jack(继承自Person的构造属性)
stu1.play(); // Jack在玩(继承自Person的构造方法)
stu1.eat(); // 报错!构造继承无法继承原型方法
特点
  • 优点:实现简单,能传参,子类实例的构造属性是独立的(无共享问题)。
  • 缺点:仅能继承父类的构造内容,无法继承父类的原型属性 / 方法(如上述 eat());子类每个实例会重复创建父类的构造方法(如 play()),浪费内存。

2. 原型继承

实现逻辑

通过重写子类的原型对象,让子类的 prototype 指向父类的实例,从而使子类继承父类的原型属性 / 方法(父类实例会携带父类的构造属性和原型属性)。

代码示例
// 父类:Person(同上)
function Person(name, sex, age) {this.name = name;this.sex = sex;this.age = age;
}
Person.prototype.eat = function() {console.log(`${this.name}在吃饭`);
};// 子类:Student(原型继承)
function Student() {}
// 核心:子类原型指向父类实例,继承父类的原型属性/方法
Student.prototype = new Person('Jack', '男', 18); // 测试
const stu1 = new Student();
console.log(stu1.name); // Jack(继承自父类实例的构造属性)
stu1.eat(); // Jack在吃饭(继承自父类的原型方法)
特点
  • 优点:能继承父类的原型属性 / 方法,实现方法复用(所有子类实例共享原型方法)。
  • 缺点
    1. 子类无法新增构造属性(需在原型上添加,不灵活);
    2. 父类的构造属性会被所有子类实例共享(如上述 stu1.name 是固定的 Jack,无法传参修改);
    3. 无法实现多继承。

3. 实例继承

实现逻辑

在子类构造函数内部直接实例化父类对象,并为其添加子类特有的属性 / 方法,最后返回该父类实例。本质是 “包装父类实例”,而非真正的子类。

代码示例
// 父类:Person(同上)
function Person(name, sex, age) {this.name = name;this.sex = sex;this.age = age;
}
Person.prototype.eat = function() {console.log(`${this.name}在吃饭`);
};// 子类:Student(实例继承)
function Student(name, sex, age) {// 1. 实例化父类对象const per = new Person(name, sex, age);// 2. 为父类实例添加子类特有属性/方法per.stuID = '1001';per.study = function() {console.log(`${this.name}在学习`);};// 3. 返回父类实例(此时Student的实例本质是Person实例)return per;
}// 测试(调用方式不限制new,有无new结果一致)
const stu1 = new Student('Rose', '女', 17);
const stu2 = Student('Tom', '男', 19);
console.log(stu1 instanceof Student); // false(本质是Person实例)
console.log(stu1 instanceof Person); // true
stu1.eat(); // Rose在吃饭(继承父类原型方法)
stu1.study(); // Rose在学习(子类新增方法)
特点
  • 优点:调用灵活(可加 new 也可不加),能继承父类的构造和原型属性。
  • 缺点
    1. 子类实例本质是父类实例(instanceof 检测不成立);
    2. 无法给子类添加独立的原型属性 / 方法;
    3. 无法实现多继承。

4. 拷贝继承

实现逻辑

通过循环遍历父类实例的属性 / 方法,将其逐个拷贝到子类的原型对象上,从而实现继承。支持多继承(拷贝多个父类)。

代码示例
// 父类:Person(同上)
function Person(name, sex, age) {this.name = name;this.sex = sex;this.age = age;
}
Person.prototype.eat = function() {console.log(`${this.name}在吃饭`);
};// 子类:Student(拷贝继承)
function Student(name, sex, age, stuID) {// 1. 实例化父类const per = new Person(name, sex, age);// 2. 循环拷贝父类的所有属性/方法到子类原型for (const key in per) {Student.prototype[key] = per[key];}// 3. 子类新增属性/方法this.stuID = stuID;Student.prototype.study = function() {console.log(`${this.name}在学习`);};
}// 测试
const stu1 = new Student('Jack', '男', 18, '1001');
console.log(stu1.name); // Jack(拷贝自父类构造属性)
stu1.eat(); // Jack在吃饭(拷贝自父类原型方法)
stu1.study(); // Jack在学习(子类新增方法)
特点
  • 优点:支持多继承(可拷贝多个父类的属性);能继承父类的构造和原型属性。
  • 缺点
    1. 效率低(需遍历所有属性,属性多时性能差);
    2. 内存占用高(拷贝会生成重复属性 / 方法);
    3. 无法获取父类中不可枚举的方法(如 Object.prototype 上的隐藏方法)。

5. 组合继承(构造 + 原型)

实现逻辑

结合构造继承call/apply 继承构造属性)和原型继承(子类原型指向父类实例),同时继承父类的构造属性和原型属性,是最常用的继承方式之一。

代码示例
// 父类:Person(同上)
function Person(name, sex, age) {this.name = name;this.sex = sex;this.age = age;
}
Person.prototype.eat = function() {console.log(`${this.name}在吃饭`);
};// 子类:Student(组合继承)
function Student(name, sex, age, stuID) {// 1. 构造继承:继承父类构造属性(传参灵活)Person.call(this, name, sex, age); this.stuID = stuID; // 子类新增构造属性
}
// 2. 原型继承:继承父类原型属性/方法
Student.prototype = new Person(); 
// 修复构造器指向(关键!否则Student.prototype.constructor指向Person)
Student.prototype.constructor = Student; 
// 子类新增原型方法
Student.prototype.study = function() {console.log(`${this.name}在学习`);
};// 测试
const stu1 = new Student('Jack', '男', 18, '1001');
console.log(stu1.name); // Jack(构造继承)
console.log(stu1.stuID); // 1001(子类新增)
stu1.eat(); // Jack在吃饭(原型继承)
stu1.study(); // Jack在学习(子类原型方法)
console.log(stu1.constructor); // Student(构造器已修复)
特点
  • 优点
    1. 兼顾构造继承(传参灵活、实例属性独立)和原型继承(方法复用);
    2. 能继承父类的构造和原型属性,支持子类新增属性 / 方法。
  • 缺点:父类构造函数被调用两次(一次在 Person.call(this),一次在 new Person()),造成内存浪费(父类构造属性会在子类实例和子类原型中各存一份)。

6. 寄生继承

实现逻辑

基于 “寄生” 思想:创建一个中间空类(作为 “寄生载体”),让中间类的原型指向父类的原型,再让子类的原型指向中间类的实例,从而避免父类构造函数被重复调用(解决组合继承的缺点)。

代码示例
// 父类:Person(同上)
function Person(name, sex, age) {this.name = name;this.sex = sex;this.age = age;
}
Person.prototype.eat = function() {console.log(`${this.name}在吃饭`);
};// 子类:Student(寄生继承)
function Student(name, sex, age, stuID) {// 1. 构造继承:仅调用一次父类构造Person.call(this, name, sex, age); this.stuID = stuID;
}// 2. 寄生逻辑:通过中间类实现原型继承(避免父类构造重复调用)
(function() {// 中间空类(无自身构造逻辑,仅作为原型桥梁)const Super = function() {}; // 中间类的原型指向父类原型(继承父类原型属性)Super.prototype = Person.prototype; // 子类原型指向中间类实例(避免调用父类构造)Student.prototype = new Super(); // 修复构造器指向Student.prototype.constructor = Student; 
})();// 子类新增原型方法
Student.prototype.study = function() {console.log(`${this.name}在学习`);
};// 测试
const stu1 = new Student('Jack', '男', 18, '1001');
console.log(stu1.name); // Jack(构造继承)
stu1.eat(); // Jack在吃饭(原型继承,父类构造仅调用一次)
特点
  • 优点:解决组合继承中父类构造函数被调用两次的问题,内存更高效;同时继承构造和原型属性。
  • 缺点:实现逻辑复杂(需借助自调用函数和中间类);仍无法实现多继承。

7. ES6 class 继承(语法糖)

ES6 引入 class 关键字和 extends 语法,本质是原型继承的语法糖,简化了继承代码的编写,逻辑与寄生组合继承一致。

代码示例
// 父类:Person(class语法)
class Person {// 构造方法(对应ES5的构造函数)constructor(name, sex, age) {this.name = name;this.sex = sex;this.age = age;}// 原型方法(自动挂载到Person.prototype)eat() {console.log(`${this.name}在吃饭`);}
}// 子类:Student(extends继承)
class Student extends Person {// 子类构造方法(必须调用super(),对应ES5的Person.call(this))constructor(name, sex, age, stuID) {super(name, sex, age); // 调用父类构造,继承构造属性this.stuID = stuID; // 子类新增属性}// 子类原型方法study() {console.log(`${this.name}在学习`);}
}// 测试
const stu1 = new Student('Jack', '男', 18, '1001');
console.log(stu1.name); // Jack(继承父类构造属性)
stu1.eat(); // Jack在吃饭(继承父类原型方法)
stu1.study(); // Jack在学习(子类原型方法)
特点
  • 优点:语法简洁直观,与传统面向对象语言(如 Java)一致;底层自动解决父类构造重复调用问题,无需手动修复构造器。
  • 缺点:本质仍是原型继承,无法摆脱原型链的底层逻辑(如 instanceof 检测仍基于原型)。

http://www.xdnf.cn/news/20147.html

相关文章:

  • 西嘎嘎学习-day 1
  • 栈:有效的括号
  • Dify-CHATflow案例
  • JS中的String的常用方法
  • Process Explorer 学习笔记(第三章3.2.3):工具栏与参考功能
  • 知微集:Python中的线程(三)
  • JavaScript 中的并发编程实践与误区:一次深入的探讨
  • 软考高级 — 系统规划与管理师考试知识点精要
  • 电脑活动追踪全解析:六款软件助企业实现数字化精细管理
  • whl编译命令作用解释
  • 【完整源码+数据集+部署教程】加工操作安全手套与手部检测系统源码和数据集:改进yolo11-cls
  • mysq集群高可用架构之组复制MGR(单主复制-多主复制)
  • 2025 年 8 个最佳网站内容管理系统(CMS)
  • 小迪安全v2023学习笔记(七十八讲)—— 数据库安全RedisCouchDBH2database未授权CVE
  • LeetCode 刷题【65. 有效数字】
  • 机器学习算法介绍二
  • postgresql 通过dblink实现 跨库查询
  • PostgreSQL收集pg_stat_activity记录的shell工具pg_collect_pgsa
  • zoho crm notes add customer fields
  • 数字人打断对话的逻辑
  • 本地 Ai 离线视频去水印字幕!支持字幕、动静态水印去除!
  • python-虚拟试衣
  • LVS、Nginx与HAProxy负载均衡技术对比介绍
  • 任意齿形的齿轮和齿条相互包络工具
  • Linux常见命令总结 合集二:基本命令、目录操作命令、文件操作命令、压缩文件操作、查找命令、权限命令、其他命令
  • Process Explorer 学习笔记(第三章3.2.5):状态栏信息详解
  • PyTorch 训练显存越跑越涨:隐式保留计算图导致 OOM
  • 机器学习周报十二
  • 基于Echarts+HTML5可视化数据大屏展示-旅游智慧中心
  • CC-Link IE FB 转 DeviceNet 实现欧姆龙 PLC 与松下机器人在 SMT 生产线锡膏印刷环节的精准定位控制