解密JavaScript面向对象(二):深入原型链,彻底搞懂面向对象精髓

很多同学学了多年JavaScript,却对原型链一知半解。其实,理解了原型链,你才能真正读懂JavaScript的设计哲学!

一、从现实生活理解原型

想象一下制造业的模具生产:有一个标准模具(原型),通过这个模具生产出多个产品(实例),所有产品都继承模具的特性,如果想修改所有产品,只需修改模型。

这就是JavaScript原型思想。

二、3分钟搞懂原型链核心概念

2.1 什么是原型(Prototype)

每个JavaScript对象都有一个隐藏属性[[Prototype]],这就是它的"原型"。

// 创建一个简单的对象
const person = {
    name: "小明",
    age: 25
};
console.log(person.toString()); // [object Object]

为什么person对象可以调用toString方法?这就是原型在起作用!

2.2 原型链查找机制

当我们访问对象的属性或方法时,JavaScript会:

  1. 先在对象自身查找
  2. 如果找不到,沿着原型链向上查找
  3. 直到找到属性或到达原型链尽头(null)
const animal = {
    eats: true
};
const rabbit = {
    jumps: true
};
// 设置rabbit的原型为animal
rabbit.__proto__ = animal;  // 实际开发中不要直接使用__proto__
console.log(rabbit.jumps);  // true (自身属性)
console.log(rabbit.eats);   // true (从原型继承)

Important: __proto__已废弃,推荐使用Object.getPrototypeOf()和Object.setPrototypeOf()。

三、创建对象的方式

3.1:字面量创建(最常用)

const person = {
    name: "小明",
    sayHello() {
        console.log(`你好,我是${this.name}`);
    }
};

优:简单直观
缺:创建多个相似对象时代码重复
该方式创建对象的原型是:Object.prototype;
其原型链是:对象person → Object.prototype → null
其对应关系是:person.proto === Object.prototype

3.2 工厂模式

function createPerson(name) {
    var o = new Object();
    o.name = name;
    o.getName = function () {
        console.log(this.name);
    };
    return o;
}
var person1 = createPerson('大王');
var person2 = createPerson("小王");

优:简单
缺:对象无法识别,因为所有的实例都指向一个原型
该方式创建对象的原型是:Object.prototype;
其原型链是:对象person1 → Object.prototype → null
其对应关系是:person1.proto === Object.prototype

3.3 构造函数模式

function Animal(name) {
    this.name = name;
    this.sayHello = function() {
        console.log(`你好,我是${this.name}`);
    };
}
const cat = new Animal("小猫");
const dog = new Animal("小狗");

优:实例可以识别为一个特定的类型;
缺:每次创建实例时都要创建新的函数,造成内存浪费;
该方式创建对象的原型是:Animal.prototype;
其原型链是:对象cat→ Animal.prototype → Object.prototype → null
其对应关系是:cat.proto === Animal.prototype

3.4 原型模式(解决内存浪费问题)

function Animal(name) {
    this.name = name;
}
Animal.prototype.canEat = []; //所有实例共享同一个数组
// 方法定义在原型上,所有实例共享
Animal.prototype.sayHello = function() {
    console.log(`你好,我是${this.name}`);
};
const cat = new Animal("小猫");
cat.canEat.push("carrot");
const dog = new Animal("小狗");
console.log(cat.sayHello === dog.sayHello); // true 
console.log(dog.canEat); //['carrot']  问题!

优:方法共享,节省内存
缺:所有实例共享方法(引用类型属性),可能产生意外

3.5 组合方式(构造函数+原型模式)

function Person(name, age) {
    // 实例特有属性放在构造函数
    this.name = name;
    this.age = age;
    this.friends = []; // 引用类型,每个实例独立
}
// 共享方法放在原型
Person.prototype.sayHello = function() {
    console.log(`你好,我是${this.name}`);
};

👍 最常用方式:结合了前两种方式的优点,初始化简单,方法共享,节省内存。

3.6 Class语法糖(ES6+)

本质:Class就是构造函数的语法糖,底层还是基于原型

class Animal {
    constructor(name) {
        this.name = name;
    }
    // 自动添加到原型
    sayHello() {
        console.log(`你好,我是${this.name}`);
    }
    // 静态方法
    static createAnonymous() {
        return new Animal("匿名");
    }
}
//使用
const Cat = new Animal("小猫");
Cat.sayHello(); //你好,我是小猫
const annoymous = Animal.createAnonymous();
annoymous.name = "小狗";
annoymous.sayHello(); //你好,我是小狗

四、面向对象的继承方式

继承指的是一个类(子类)基于另一个类(父类)来构建,自动获得父类的属性和方法,并可以添加新的功能或修改现有功能。原型链则是继承的实现基础。

4.1 原型链继承

function Parent() {
    this.name = "父级";
    this.colors = ["red", "blue"];
}
function Child() {
    this.age = 10;
}
// 关键:设置原型
Child.prototype = new Parent();
// 定义两个实例 
const child1 = new Child();
const child2 = new Child();
child1.colors.push("green");
console.log(child2.colors); // ["red", "blue", "green"] 问题!

缺:所有子类实例共享引用类型的属性

4.2 构造函数继承

// 父类构造函数
function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
  this.sayHello = function() {
    console.log('Hello, ' + this.name);
  };
}
// 父类原型上的方法
Parent.prototype.sayName = function() {
  console.log('My name is ' + this.name);
};
// 子类构造函数
function Child(name, age) {
  // 关键步骤:借用父类构造函数
  // 在子类实例的上下文中执行父类构造函数
  Parent.call(this, name);
  this.age = age;
}
// 使用子类创建实例
var child1 = new Child('大王', 20);
var child2 = new Child('小王', 10);
console.log(child1.name); // "大王"
console.log(child2.name); // "小王"
// 引用类型属性是独立的
child1.colors.push('black');
console.log(child1.colors); // ['red', 'blue', 'green', 'black']
console.log(child2.colors); // ['red', 'blue', 'green'](没有相互影响)
child1.sayHello(); // Hello, 大王 
child1.sayName(); // TypeError: child1.sayName is not a function (无法访问原型上的方法)

缺:无法继承父类原型上的方法和属性,方法无法复用

4.3 组合继承(最常用)

function Parent(name) {
    this.name = name;
    this.colors = ["red", "blue"];
}
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("小明", 10);

缺:虽完美解决两种方案缺陷,但构造函数被调用了2次

4.4 原型式继承

// 核心函数:基于已有对象创建新对象
function object(o) {
    function F() {}  // 创建一个临时构造函数
    F.prototype = o; // 将传入对象作为原型
    return new F();  // 返回这个构造函数的实例
}
const person = {
    name: '匿名',
    colors: ["red", "blue"],
    sayName: function() {
        console.log(this.name);
    }
};
// 创建新对象
var person1 = object(person); 
// 或用现代的 Object.create()
var person2 = Object.create(person);
person1.name = '大王';
person2.name = '小王';
person1.colors.push("green");
console.log(person1.name); // "大王"
console.log(person2.name); // "小王"
console.log(person2.colors); // ['red', 'blue', 'green']

缺:同原型链继承,所有子类实例共享引用类型的属性

4.5 寄生式继承

称之为寄生式继承的原因:它不直接创建新对象,而是基于现有对象进行扩展,通过"寄生"在已有对象上,添加新的属性和方法本质上是对原有对象的增强和改造。

function createChild(parent) {
    const clone = Object.create(parent);
    clone.sayHello = function() {
        console.log("Hello");
    };
    return clone;
}

缺:函数方法无法复用,内存效率极低(与构造函数继承的问题相同);共享引用类型(parent)的属性。

4.6 寄生组合式继承(最理想)

// 父类
function Parent(name) {
    this.name = name;
}
Parent.prototype.sayName = function() {
    console.log(this.name);
};
// 子类 
function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}
// 寄生组合式继承 


function inheritPrototype(child, parent) {
    // 创建父类原型的纯净副本,建立原型链连接
    const prototype = Object.create(parent.prototype);
    // 修复构造函数的指向,保持正确的继承关系
    prototype.constructor = child;
    // 将修复好的原型对象设置为子类的原型
    child.prototype = prototype;
}
// 关键步骤
inheritPrototype(Child, Parent);
const child = new Child("小明", 10);

👍 最佳实践:只调用一次父类构造函数,原型链保持正确

4.7 Class继承(ES6+)

底层原理:基于寄生组合式继承

class Parent {
    constructor(name) {
        this.name = name;
        this.colors = ["red", "blue"];
    }
    sayName() {
        console.log(this.name);
    }
    static staticMethod() {
        console.log("父类静态方法");
    }
}
class Child extends Parent {
    constructor(name, age) {
        super(name); // 必须调用super
        this.age = age;
    }
    // 方法重写
    sayName() {
        super.sayName(); // 调用父类方法
        console.log(`年龄: ${this.age}`);
    }
}

五、实际项目应用场景

场景1:Vue组件扩展(原型链的应用)

// 基础组件
const BaseComponent = {
    data() {
        // ***
    },
    methods: {
        // ***
    }
};
// Vue2的mixin机制就是基于原型链的
// 具体页面组件
const UserPage = {
    mixins: [BaseComponent],
    data() {
        // ***
    },
    methods: {
        // ***
    }
};

Vue2 在处理 Mixin 和组件选项的合并时,其核心的覆盖和查找逻辑,完全遵循并利用了 JavaScript 对象原型链的规则——即“自身属性优先于原型上的属性”。它没有创造新规则,只是巧妙地将 JavaScript 语言本身的特性应用在了框架的代码组织上。

场景2: 工具类继承体系

class ApiClient {
    constructor(baseURL) {
        this.baseURL = baseURL;
    }
    async request(endpoint, options = {}) {
        const response = await fetch(`${this.baseURL}${endpoint}`, options);
        return this.handleResponse(response);
    }
    async handleResponse(response) {
        if (!response.ok) {
            throw new Error(`HTTP错误: ${response.status}`);
        }
        return response.json();
    }
}
// UserApi 继承 ApiClient 
class UserApi extends ApiClient {
    constructor() {
        super('/api');

    }
    async getUsers() {
        return this.request('/users');
    }
    async createUser(userData) {
        return this.request('/users', {
            method: 'POST',
            body: JSON.stringify(userData)
        });
    }
}

场景3: 错误处理继承体系

class AppError extends Error {
    constructor(message, code = 'UNKNOWN_ERROR') {
        super(message);
        this.name = this.constructor.name;
        this.code = code;
        this.timestamp = new Date();
    }
    // JSON格式化
    toJSON() {
        return {
            name: this.name,
            message: this.message,
            code: this.code,
            timestamp: this.timestamp
        };
    }
}
// ValidationError 继承 AppError
class ValidationError extends AppError {
    constructor(field, message) {
        super(message, 'VALIDATION_ERROR');
        this.field = field;
    }
}
// 使用
try {
    throw new ValidationError('email', '邮箱格式不正确');
} catch (error) {
    if (error instanceof AppError) {
        console.error(error.toJSON());
    }
}

最佳实践说明:

  • 方法共享:将公共方法放在原型上,节省内存
  • 继承层次不宜过深:最多2-3层继承,否则难以维护
  • 合理使用Class语法:ES6 Class让原型操作更直观

六、面试常见问题解析

问题1: instanceof的原理是什么?

instanceof原理是检查对象的原型链上是否存在构造函数的 prototype属性。
它会沿着对象的原型链向上查找,若找到了某个原型等于构造函数的 prototype,就返回 true,直到原型链尽头(null)还没找到就返回 false。

// instanceof 检查原型链
console.log([] instanceof Array);  // true
console.log([] instanceof Object); // true

// 手动实现instanceof
function myInstanceof(obj, constructor) {
    let proto = Object.getPrototypeOf(obj);
    while (proto) {
        if (proto === constructor.prototype) {
            return true;
        }
        proto = Object.getPrototypeOf(proto);
    }
    return false;
}

问题2: 如何实现深拷贝?

深拷贝指的是创建一个全新的对象,并递归地复制原对象的所有属性(包括嵌套的对象和数组),使得新对象与原对象完全独立,修改新对象不会影响原对象。

function deepClone(obj, hash = new WeakMap()) {
    // 非对象直接返回 
    if (obj === null || typeof obj !== 'object') {
        return obj;
    }
    // has(WeakMap):用于缓存已拷贝对象,解决循环引用
    // 若已拷贝过该对象,返回缓存 
    if (hash.has(obj)) {
        return hash.get(obj);
    }
    //辨别是数组或普通对象,创建对应的空容器;并缓存原对象和缓存结果
    const result = Array.isArray(obj) ? [] : {};
    hash.set(obj, result);
    // 递归复制所有属性 
    for (let key in obj) {
      //只处理对象自身的属性(不处理原型链上的属性)
        if (obj.hasOwnProperty(key)) {
            result[key] = deepClone(obj[key], hash);
        }
    }
    return result;
}
// 使用
const obj = {
    name: '大王',
    hobbies: ['reading', 'swimming'],
}
const cloneObj = deepClone(obj);

七、总结

原型链是JavaScript面向对象的灵魂,理解它能帮助我们

  • 更好地使用框架(React/Vue的组件系统基于原型思想)
  • 编写更优雅的代码(合理的继承体系)
  • 深入理解语言特性(包括ES6 Class的本质)
  • 解决复杂问题(如深拷贝、类型判断等)
    记住:Class只是语法糖,原型是根本

八、扩展

JavaScript面向对象思想除了继承(作用:代码复用,层次关系)外,还包括以下思想:

  • 🎯 封装 - 信息隐藏,访问控制
  • 🧩 组合 - 灵活组装,功能混合
  • 🌈 多态 - 接口统一,实现多样
  • 📨 消息传递 - 事件驱动,松耦合通信
  • 🤝 委托 - 职责转移,行为代理
  • 📝 抽象 - 隐藏细节,暴露本质
  • 💉 依赖注入 - 控制反转,解耦管理

这些思想共同构成了 JavaScript的完整体系,以及丰富且灵活的面向对象编程范式,方便开发者根据具体需求选择适合的模式来构建可维护、可扩展的应用程序。

下期预告

下一次我们将深入探讨作用域链和闭包,这是JavaScript的另一个核心概念。

如果觉得有帮助,请关注+点赞,这是对我最大的鼓励!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序媛小王ouc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值