JavaScript 的面向对象 (OOP) 与传统面向对象语言 (如 Java、C++) 差异显著 —— 它没有类 (class) 的原生概念 (ES6 class 是语法糖),而是基于原型 (Prototype) 实现面向对象核心特性 (封装、继承、多态)
面向对象的核心基石
万物皆对象?不完全是
-
基本类型:string、number、boolean、null、undefined、symbol、bigint (非对象,无属性 / 方法);
-
引用类型:object (包括普通对象、数组、函数、日期、正则等)—— 核心是 「键值对集合」,且每个对象都关联一个 「原型对象」;
原型(Prototype):OOP 的核心
-
每个对象 (除 Object.create(null) 创建的纯对象) 都有一个隐藏的 [[Prototype]] 内置属性 (可通过 __proto__ 访问,标准方式是 Object.getPrototypeOf()),指向它的原型对象;
-
原型对象本身也是对象,也有自己的原型,形成 「原型链」,最终指向 null (原型链的终点);
const obj = {}; console.log(Object.getPrototypeOf(obj) === Object.prototype); // true console.log(Object.getPrototypeOf(Object.prototype)); // null(原型链终点)
构造函数(Constructor)
-
构造函数是普通函数 (首字母大写约定),通过 new 调用时:
- 创建一个空对象;
- 该对象的 [[Prototype]] 指向构造函数的 prototype 属性;
- 构造函数的 this 绑定到这个空对象;
- 执行构造函数代码 (初始化属性);
- 若构造函数无显式返回对象,则返回这个新对象;
-
示例:
// 构造函数 function Person(name, age) { this.name = name; // 实例属性 this.age = age; } // 原型方法(所有实例共享) Person.prototype.sayHi = function() { console.log(`Hi, I'm ${this.name}`); }; // 创建实例 const p1 = new Person("张三", 20); p1.sayHi(); // Hi, I'm 张三 console.log(p1.__proto__ === Person.prototype); // true
面向对象的三大特性
封装:隐藏内部细节,暴露公共接口
-
封装的核心是「控制属性 / 方法的访问权限」,但 JS 没有原生的 private/protected 修饰符 (ES6+ 提供了模拟方案);
-
实现:
function Counter() { // 实例私有属性(外部无法直接访问) let count = 0; // 公共方法(暴露接口) this.increment = function() { count++; return count; }; this.getCount = function() { return count; }; } const c = new Counter(); c.increment(); // 1 console.log(c.count); // undefined(私有属性不可访问)class Counter { #count = 0; // 私有实例字段 increment() { this.#count++; return this.#count; } #log() { // 私有方法 console.log("Count changed"); } getCount() { this.#log(); return this.#count; } } const c = new Counter(); c.increment(); // 1 c.getCount(); // 打印 "Count changed",返回 1 console.log(c.#count); // 报错:私有字段不可访问function createPerson(name) { // 私有变量 let age = 0; return { getName: () => name, getAge: () => age, setAge: (newAge) => { if (newAge >= 0) age = newAge; } }; } const p = createPerson("李四"); p.setAge(25); console.log(p.getName(), p.getAge()); // 李四 25 console.log(p.age); // undefined
继承:复用已有对象的属性 / 方法
JS 继承的核心是「原型链继承」,ES6 之前需手动拼接原型链,ES6 class 简化了语法 (但底层仍是原型);
组合继承(原型链 + 构造函数继承)
-
核心逻辑:
- 用
构造函数.call(this)继承父类实例属性 (解决原型链继承的属性共享问题); - 用
子类.prototype = new 父类()继承父类原型方法; - 修复子类构造函数指向 (避免 constructor 错乱);
- 用
-
示例代码:
// 父类 function Animal(name) { this.name = name; this.eat = () => console.log(`${name} 吃食物`); } // 父类原型方法 Animal.prototype.run = function() { console.log(`${this.name} 跑`); }; // 子类 function Dog(name, breed) { // 1. 构造函数继承(继承父类实例属性,避免引用类型共享) Animal.call(this, name); this.breed = breed; // 子类自有属性 } // 2. 核心:让子类原型指向父类实例(建立原型链) Dog.prototype = new Animal(); // 修复构造函数指向(否则 Dog 实例的 constructor 是 Animal) Dog.prototype.constructor = Dog; // 子类原型方法 Dog.prototype.bark = function() { console.log(`${this.name} 汪汪叫`); }; // 使用 const dog = new Dog("旺财", "中华田园犬"); dog.eat(); // 旺财 吃食物 dog.run(); // 旺财 跑 dog.bark(); // 旺财 汪汪叫 console.log(dog.breed); // 中华田园犬 -
图解:
寄生组合继承(优化组合继承)
-
核心优化点:
- 组合继承会调用 2 次父类构造函数 (Animal.call + new Animal),导致子类原型冗余;
- 寄生组合继承通过
Object.create(父类.prototype)直接继承父类原型,仅调用 1 次父类构造函数;
-
示例代码:
// 父类:动物(同组合继承) function Animal(name) { this.name = name; this.skills = ["跑", "吃"]; console.log("Animal 构造函数被调用"); // 用于验证调用次数 } Animal.prototype.eat = function() { console.log(`${this.name} 正在吃食物`); }; // 子类:猫 function Cat(name, color) { // 仅这 1 次调用父类构造函数(核心优化) Animal.call(this, name); this.color = color; } // 寄生继承核心:创建空对象,原型指向父类原型(避免调用父类构造函数) Cat.prototype = Object.create(Animal.prototype); // 修复构造函数指向 Cat.prototype.constructor = Cat; // 子类原型方法 Cat.prototype.meow = function() { console.log(`${this.name}(${this.color})喵喵叫`); }; // 测试示例 const cat1 = new Cat("小白", "白色"); const cat2 = new Cat("小黑", "黑色"); // 1. 验证父类构造函数仅调用 1 次(控制台只打印 1 次 "Animal 构造函数被调用") // 2. 验证实例属性不共享 cat1.skills.push("抓老鼠"); console.log(cat1.skills); // ["跑", "吃", "抓老鼠"] console.log(cat2.skills); // ["跑", "吃"] // 3. 验证继承父类方法 cat2.eat(); // 小黑 正在吃食物 // 4. 验证子类自有方法 cat1.meow(); // 小白(白色)喵喵叫 // 5. 验证原型链干净(无冗余的父类实例属性) console.log(Cat.prototype.name); // undefined(组合继承会是 undefined 吗?组合继承中是 undefined,因为 new Animal() 没传参,但调用了构造函数) // 对比:组合继承中 Dog.prototype.name 是 undefined(因为 new Animal() 无参),但寄生组合继承更彻底 -
图解
ES6 class 继承(语法糖)
-
class + extends + super 简化继承逻辑,底层仍是原型链;
-
示例代码:
class Animal { constructor(name) { this.name = name; } eat() { console.log(`${this.name} 吃食物`); } run() { console.log(`${this.name} 跑`); } } // 子类继承父类 class Dog extends Animal { constructor(name, breed) { super(name); // 调用父类构造函数(必须在 this 前) this.breed = breed; } bark() { console.log(`${this.name} 汪汪叫`); } // 重写父类方法(多态) run() { super.run(); // 调用父类 run 方法 console.log(`${this.name} 飞快地跑`); } } const dog = new Dog("来福", "金毛"); dog.eat(); // 来福 吃食物 dog.run(); // 来福 跑 → 来福 飞快地跑 dog.bark(); // 来福 汪汪叫
多态:同一方法,不同对象表现不同行为
-
JS 是弱类型语言,多态无需像强类型语言那样显式定义,核心是 「方法重写」 和 「动态绑定」;
-
示例代码:
class Shape { area() { return 0; // 基类默认实现 } } class Circle extends Shape { constructor(radius) { super(); this.radius = radius; } area() { return Math.PI * this.radius **2; // 重写面积计算 } } class Rectangle extends Shape { constructor(width, height) { super(); this.width = width; this.height = height; } area() { return this.width * this.height; // 重写面积计算 } } // 多态体现:统一接口,不同实现,子类重写父类的方法,调用时根据实例类型执行对应逻辑 function calculateArea(shape) { console.log(shape.area()); } calculateArea(new Circle(5)); // 78.5398... calculateArea(new Rectangle(4, 6)); // 24 calculateArea(new Shape()); // 0// 无需继承关系,只要有 fly 方法即可,体现 “鸭子类型”(如果它走起来像鸭子、叫起来像鸭子,那它就是鸭子) const Bird = { fly: () => console.log("鸟飞") }; const Plane = { fly: () => console.log("飞机飞") }; function letItFly(obj) { obj.fly(); // 多态:不同对象执行不同 fly 逻辑 } letItFly(Bird); // 鸟飞 letItFly(Plane); // 飞机飞
ES6 class 深度解析(语法糖的本质)
-
ES6 引入的 class 是原型继承的语法糖,没有改变 JS 基于原型的本质,但让代码更接近传统 OOP 风格;
-
以下两段代码实现了同样的继承关系:
class Person { // 静态属性(ES7+) static species = "Human"; // 实例属性(ES7+,也可写在 constructor 里) gender = "unknown"; // 构造函数 constructor(name, age) { this.name = name; this.age = age; } // 原型方法(实例方法) sayHi() { console.log(`Hi, ${this.name}`); } // 静态方法(通过类调用,无 this) static createAdult(name) { return new Person(name, 18); } // 访问器属性(get/set) get fullInfo() { return `${this.name}, ${this.age}岁`; } set fullInfo(info) { const [name, age] = info.split(","); this.name = name; this.age = Number(age); } } // 使用 const p = Person.createAdult("王五"); // 静态方法创建实例 p.sayHi(); // Hi, 王五 console.log(p.fullInfo); // 王五, 18岁 p.fullInfo = "赵六, 22"; console.log(p.age); // 22 console.log(Person.species); // Humanfunction Person(name, age) { this.gender = "unknown"; this.name = name; this.age = age; } Person.species = "Human"; Person.prototype.sayHi = function() { console.log(`Hi, ${this.name}`); }; Person.createAdult = function(name) { return new Person(name, 18); }; // 访问器属性 Object.defineProperty(Person.prototype, "fullInfo", { get() { return `${this.name}, ${this.age}岁`; }, set(info) { const [name, age] = info.split(","); this.name = name; this.age = Number(age); } });
常见误区与注意事项
混淆 __proto__ 和 prototype
-
prototype:构造函数的属性,指向原型对象 (供实例继承);
-
__proto__:实例的属性,指向构造函数的 prototype (即 Object.getPrototypeOf(实例));
function Foo() {} const foo = new Foo(); console.log(Foo.prototype); // Foo 的原型对象 console.log(foo.__proto__ === Foo.prototype); // true console.log(Foo.__proto__ === Function.prototype); // true(函数也是对象)
原型方法 vs 实例方法
-
原型方法:定义在 构造函数.prototype 上,所有实例共享 (节省内存);
-
实例方法:定义在构造函数内部 (this.方法),每个实例独立拥有 (占用更多内存);
function A() { this.fn1 = () => {}; // 实例方法(每个实例一个副本) } A.prototype.fn2 = () => {}; // 原型方法(所有实例共享) const a1 = new A(); const a2 = new A(); console.log(a1.fn1 === a2.fn1); // false console.log(a1.fn2 === a2.fn2); // true
避免修改原生对象的原型
-
如修改 Array.prototype、Object.prototype,可能导致全局污染和兼容性问题;
-
示例代码:
// 不推荐! Array.prototype.add = function(val) { this.push(val); }; // 可能与其他库冲突,或影响 for...in 遍历
面试题
手写简易版 Object.create
-
核心目标:创建一个新对象,让其原型指向传入的对象 (忽略 null 原型、第二个参数等细节);
-
核心逻辑:通过空构造函数关联原型,再实例化得到新对象;
function Animal(kind) {
this.kind = kind;
}
function Dog(name) {
this.name = name;
}
function _create(proto) {
// 1. 定义空构造函数
function F() {}
// 2. 让构造函数的原型指向传入的 proto
F.prototype = proto;
// 3. 实例化构造函数,返回新对象(新对象 __proto__ 指向 proto)
return new F();
}
// 测试
Dog.prototype = _create(Animal);
let dog = new Dog('花花');
console.log(dog.__proto__.__proto__ === Animal.prototype); // true
console.log(dog.__proto__ === Dog.prototype); // true
手写简易版 new
-
核心目标:模拟 new 的核心流程 (创建对象→绑定原型→执行构造函数→返回对象),忽略边界校验、显式返回对象等细节;
-
核心逻辑:创建空对象 → 绑定原型 → 改变构造函数 this 指向 → 执行构造函数 → 返回新对象;
/***
* @param {*} Func 操作的那个类
* @param {...any} args new 的时候传入的实参集合
* @return 实例 或者 自己返回的对象
*/
function _new(Func, ...args) {
// 1. 创建空对象,让其 __proto__ 指向构造函数的 prototype
// let obj = {};
// obj.__proto__ == Func.prototype;//=> IE大部分浏览器不支持直接操作 __proto__
// 等价于
let objInstance = Object.create(Func.prototype);
// 2. 执行构造函数,将 this 绑定到新对象
let result = Func.call(objInstance, ...args);
// 3. 若客户自己返回引用值,则以自己返回的为主,否则返回创建的实例
if ((result !== null && typeof result === 'object') || typeof result === 'function') {
return result;
}
// 4. 返回创建的新对象
return objInstance;
}
function Dog(name) {
this.name = name;
}
Dog.prototype.bark = function () {
console.log('wangwang');
}
Dog.prototype.sayName = function () {
console.log('my name is ' + this.name);
}
let sanmao = _new(Dog, '三毛');
sanmao.sayName();
sanmao.bark();
console.log(sanmao instanceof Dog);
实现 n.plus(10).minus(5)
~ function anonymous(proto) {
const checkNum = function checkNum(num) {
// isNaN 是否是 非有效数字
return isNaN(Number(num)) ? 0 : num;
};
proto.plus = function plus(num) {
// this:要操作的那个数字实例(对象)
// 返回 Number 类的实例,实现链式写法
return this + checkNum(num);
};
proto.minus = function minus(num) {
return this - checkNum(num);
};
}(Number.prototype);
let n = 10;
console.log(n.plus(10).minus(5)); //=>15(10+10-5)
画图计算下面的结果
function fun() {
this.a = 0;
this.b = function () {
alert(this.a);
}
}
fun.prototype = {
b: function () {
this.a = 20;
alert(this.a);
},
c: function () {
this.a = 30;
alert(this.a);
}
}
var my_fun = new fun();
my_fun.b();
my_fun.c();
写出下面代码执行输出的结果
function C1(name) {
// name:undefined 没有给实例设置私有的属性 name
if (name) {
this.name = name;
}
}
function C2(name) {
this.name = name;
// 给实例设置私有属性 name => this.name=undefined
}
function C3(name) {
this.name = name || 'join';
// 给实例设置私有属性 name =>this.name=undefined || 'join'
}
C1.prototype.name = 'Tom';
C2.prototype.name = 'Tom';
C3.prototype.name = 'Tom';
alert((new C1().name) + (new C2().name) + (new C3().name));
//=> (new C1().name) 找原型上的 'Tom'
//=> (new C2().name) 找私有属性 undefined
//=> (new C3().name) 找私有属性 'join'
写出下面代码执行输出的结果(阿里)
function Foo() {
//没有 this.XXX() 就不是私有方法
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();// 1
// new无参列表 优先级为18
// new有参列表 优先级为19
// 成员访问 优先级为19
// 先Foo.getName(),在new
new Foo.getName();// 2
// 先new,在调用.getName()
// Foo中没有this.getName,所以根据原型链向上查找
new Foo().getName();// 3
// 先 new Foo(),再.getName(),再 new 实例.getName,new 原型上的getName
new new Foo().getName();// 3
下面代码输出结果是什么?为什么?
// 题目
let obj = {
2: 3,
3: 4,
length: 2,
push: Array.prototype.push
}
obj.push(1);
obj.push(2);
console.log(obj);
// 解析
// 扩展:模拟 array.push 的实现
Array.prototype.push = function push(num) {
// this:arr
this.length = this.length || 0;
// 拿原有 length 作为新增项的索引
this[this.length] = num;
// length 的长度会自动跟着累加 1
this.length++
};
let arr = [10, 20]; //=>{ 0: 10, 1: 20, length: 2 }
arr.push(30);
let obj = {
2: 3,
3: 4,
length: 2,
push: Array.prototype.push
};
obj.push(1); // obj[2]=1 obj.length=3
obj.push(2); // obj[3]=2 obj.length=4
console.log(obj); // {2: 1, 3: 2, length: 4 ...}
let obj = {
1: 10,
push: Array.prototype.push
};
// this.length 默认值为 0
obj.push(20); // obj[obj.length]=20 obj[0]=20
console.log(obj); // {0:20,1:10,length:1...}
剑指 Offer 58 - II.左旋转字符串
上一篇