JavaScript 面向对象 原型和原型链 继承
面向对象 原型和原型链
一、原型对象
定义:所有构造函数在初始化时,会自动生成一个特殊的实例化对象,构造函数的
prototype
属性指向该对象,这个对象就是原型对象(prototype 对象)。核心特点:
- 实例化对象通过
__proto__
属性指向构造函数的原型对象 - 实例访问属性 / 方法时,先查自身,再通过
__proto__
查原型对象 - 原型对象的主要作用是实现继承和共享方法
- 实例化对象通过
原型对象默认属性:
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
二、原型链
定义:由一系列
__proto__
属性串联起来的原型对象链称为原型链。查找机制:
- 访问对象属性 / 方法时,先查自身
- 若无,则通过
__proto__
查原型对象 - 若无,则继续查原型的原型,直至
Object.prototype
Object.prototype.__proto__
为null
,查找终止
示意图:
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在吃饭(继承自父类的原型方法)
特点
- 优点:能继承父类的原型属性 / 方法,实现方法复用(所有子类实例共享原型方法)。
- 缺点:
- 子类无法新增构造属性(需在原型上添加,不灵活);
- 父类的构造属性会被所有子类实例共享(如上述
stu1.name
是固定的Jack
,无法传参修改); - 无法实现多继承。
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
也可不加),能继承父类的构造和原型属性。 - 缺点:
- 子类实例本质是父类实例(
instanceof
检测不成立); - 无法给子类添加独立的原型属性 / 方法;
- 无法实现多继承。
- 子类实例本质是父类实例(
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在学习(子类新增方法)
特点
- 优点:支持多继承(可拷贝多个父类的属性);能继承父类的构造和原型属性。
- 缺点:
- 效率低(需遍历所有属性,属性多时性能差);
- 内存占用高(拷贝会生成重复属性 / 方法);
- 无法获取父类中不可枚举的方法(如
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(构造器已修复)
特点
- 优点:
- 兼顾构造继承(传参灵活、实例属性独立)和原型继承(方法复用);
- 能继承父类的构造和原型属性,支持子类新增属性 / 方法。
- 缺点:父类构造函数被调用两次(一次在
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
检测仍基于原型)。