JavaScript 原型及原型链:深入解析核心机制

引言

在 JavaScript 的世界中,原型(Prototype)与原型链(Prototype Chain)是实现对象继承和属性查找的核心机制。它们不仅是这门语言的独特特性,更是理解 JavaScript 面向对象编程的关键所在。

与传统的基于类的面向对象语言不同,JavaScript 采用了基于原型的继承模式,这使得它在对象创建和继承方面具有独特的灵活性。本文将从基础概念出发,通过丰富的代码示例和图解,深入剖析原型与原型链的运作原理,帮助你彻底掌握这一重要知识体系。

理解两种对象创建模式

在深入学习 JavaScript 原型之前,我们需要理解两种不同的对象创建模式,这有助于我们更好地理解 JavaScript 的设计理念。

基于类的对象创建(Class-based)

大多数主流编程语言(如 Java、C#、Python)采用基于类的模式:

// Java 示例
class Person {
    private String name;
    private int age;
    
    // 构造函数
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public void introduce() {
        System.out.println("我是 " + name + ",今年 " + age + " 岁");
    }
}

// 创建对象实例
Person john = new Person("John", 25);
Person mary = new Person("Mary", 30);

在这种模式下:

  • 类是模板:定义了对象的结构和行为
  • 对象是实例:根据类模板创建的具体实体
  • 继承通过类实现:子类继承父类的属性和方法

基于原型的对象创建(Prototype-based)

JavaScript 采用了不同的方式——基于原型的模式:

// 创建一个原型对象
const personPrototype = {
    introduce() {
        console.log(`我是 ${this.name},今年 ${this.age}`);
    },
    walk() {
        console.log(`${this.name} 正在走路`);
    }
};

// 基于原型创建新对象
const john = Object.create(personPrototype);
john.name = "John";
john.age = 25;

const mary = Object.create(personPrototype);
mary.name = "Mary";
mary.age = 30;

// 两个对象都可以使用原型上的方法
john.introduce(); // "我是 John,今年 25 岁"
mary.introduce(); // "我是 Mary,今年 30 岁"

// 验证原型关系
console.log(john.__proto__ === personPrototype); // true
console.log(mary.__proto__ === personPrototype); // true

在原型模式下:

  • 没有类的概念:直接使用对象作为模板
  • 对象可以直接继承对象:新对象以现有对象为原型
  • 动态性更强:可以随时修改原型,影响所有继承该原型的对象

两种模式的对比

特性基于类基于原型
模板类(Class)对象(Object)
创建方式new Class()Object.create()
继承方式类继承类对象继承对象
灵活性相对固定高度灵活
性能较好稍差(需要原型链查找)

JavaScript 的对象创建机制

当布兰登·艾奇在 1995 年设计 JavaScript 时,他选择了基于原型的方式来创建对象。这个决定使得 JavaScript 在对象创建和继承方面具有了独特的特性。

原型的基本概念

在 JavaScript 中,每个对象都有一个原型。原型本身也是一个对象,它为其他对象提供共享的属性和方法。我们可以通过 __proto__ 属性来访问一个对象的原型:

// 创建一个简单对象
const obj = { name: "测试对象" };

// 查看对象的原型
console.log(obj.__proto__); // 指向 Object.prototype
console.log(obj.__proto__ === Object.prototype); // true

在 ES5 中提供了 Object.create() 方法,让我们可以显式地指定新对象的原型:

// 定义一个原型对象
const personPrototype = {
    species: '人类',
    arms: 2,
    legs: 2,
    walk() {
        console.log(`${this.name} 正在走路`);
    },
    introduce() {
        console.log(`大家好,我是 ${this.name},今年 ${this.age}`);
    }
};

// 使用 Object.create() 创建新对象
const john = Object.create(personPrototype);
john.name = 'John';
john.age = 25;
// 验证原型关系
console.log(john.__proto__ === personPrototype); // true
// 调用原型上的方法
john.introduce(); // "大家好,我是 John,今年 25 岁"

构造函数的引入

基于原型的构想固然很美好,但是现实也很残酷,由于当时基于类的编程模式更为流行,加上网景公司管理层对 Java 的偏好,布兰登·艾奇被迫对 JavaScript 进行改造。添加了 this、new 这些关键字,使其看上去像是基于类生产的对象。不过早期没有 class 关键字,就需要使用 function 来模拟类,也就是我们熟知的构造函数,为了区分普通函数,也随之出现了一个不成文的规定,构造函数名称首字母大写

function Person(name, age) {
    this.name = name;
    this.age = age;
}

这里的构造函数本身是普通的函数,但当我们使用 new 的方式来调用,JavaScript 引擎会执行以下步骤:

  1. 创建一个空的简单 JS 对象(即 { } )
  2. 为创建的对象添加属性 _proto_,将该属性链接至构造函数的原型对象
  3. 将 this 绑定到创建的对象
  4. 如果该函数没有返回对象,则返回 this
function Person(name, age) {
    // 1. 创建一个新的空对象
    // const newObj = {};
    
    // 2. 将新对象的 __proto__ 指向构造函数的 prototype
    // newObj.__proto__ = Person.prototype;
    
    // 3. 将构造函数的 this 绑定到新对象
    // this = newObj;
    
    this.name = name;
    this.age = age;
    
    // 4. 如果构造函数没有显式返回对象,则返回新创建的对象
    // return this;
}

// 使用 new 创建实例
const john = new Person('John', 25);
console.log(john.name); // "John"

为了更好地理解 new 的工作原理,我们可以手动实现一个:

function myNew(constructor, ...args) {
    // 1. 创建一个新对象
    const obj = {};
    
    // 2. 设置新对象的原型
    obj.__proto__ = constructor.prototype;
    
    // 3. 执行构造函数,并将 this 绑定到新对象
    const result = constructor.apply(obj, args);
    
    // 4. 如果构造函数返回了对象,则返回该对象;否则返回新创建的对象
    return result instanceof Object ? result : obj;
}

const john2 = myNew(Person, 'John2', 26);
console.log(john2.name); // "John2"

这其实也就是 JS 中函数二义性的由来

不过,无论 JavaScript 如何模拟面向对象的特性,即便在 ES6 中引入了 class 关键字。

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    introduce() {
        console.log(`大家好,我是 ${this.name},今年 ${this.age}`);
    }
}

const john = new Person('John', 25);

// 验证:class 本质上还是函数
console.log(typeof Person); // "function"
console.log(john.__proto__ === Person.prototype); // true

但这些也只是语法糖,JavaScript 底层仍然是一门基于原型的语言

原型对象与原型链

核心三角关系

在 JavaScript 中,构造函数、实例对象和原型对象之间存在一个重要的三角关系。理解这个关系是掌握原型链的关键:

请添加图片描述

这三角指的是 构造函数、实例对象、原型对象 之间的关系。在 JS 中,只要是由构造函数 new 出来的对象,都满足这样的关系

function Person(name) {
    this.name = name;
}
const john = new Person('John');
// 1. 实例的 __proto__ 指向构造函数的 prototype
console.log(john.__proto__ === Person.prototype); // true
// 2. 实例的 constructor 指向构造函数
console.log(john.constructor === Person); // true
// 3. 原型对象的 constructor 指向构造函数
console.log(Person.prototype.constructor === Person); // true
// 4. 构造函数的 prototype 指向原型对象
console.log(Person.prototype === john.__proto__); // true

这种关系不仅适用于自定义构造函数,也适用于所有内置构造函数:

const arr = [];
console.log(arr.__proto__ === Array.prototype); // true
console.log(arr.constructor === Array); // true
console.log(Array.prototype.constructor === Array); // true

原型链的完整结构

原型链是 JavaScript 中对象之间的继承关系链条。全貌图如下:

请添加图片描述

  • JS 中的对象大体上分为两大类:普通对象构造器对象(函数)

  • 无论是 普通对象 还是 构造器对象,都会有自己的原型对象,可以通过 _proto_ 这个隐式属性找到自己的原型对象,而原型对象也会自己的原型对象,就这样一直向上找,最终会到达 null.

    const obj = {};
    console.log(obj.__proto__); // Object.prototype
    console.log(obj.__proto__.__proto__); // null
    
  • 普通对象构造器对象 的区别在于是否能够实例化,构造器对象可以通过 new 的形式创建新的实例对象,这些实例对象的原型对象一直往上找最终仍然是到达 null.

    // 构造器对象
    function Animal(name) {
        this.name = name;
    }
    // 创建实例
    const dog = new Animal('旺财');
    console.log('dog.__proto__:', dog.__proto__); // Animal.prototype
    console.log('dog.__proto__.__proto__:', dog.__proto__.__proto__); // Object.prototype
    console.log('dog.__proto__.__proto__.__proto__:', dog.__proto__.__proto__.__proto__); // null
    
  • 只有 构造器对象 才有 prototype 属性,其 prototype 属性指向实例对象的原型对象

    function MyFunction() {}
    const myFunc = new MyFunction()
    console.log(myFunc.__proto__ === MyFunction.prototype); // true
    
  • 所有 构造器对象 的原型对象均为 Function.prototype。

    function MyFunction() {}
    console.log(MyFunction.__proto__); // Function.prototype
    
  • 无论是 普通对象 还是 构造器对象,最终的 constructor 指向 Function,也就是说所有构造器对象都是有 Function 的实例,包括 Function 自己。

    function MyFunction() {}
    console.log(MyFunction.constructor === Function); // true
    console.log(Function.constructor === Function); // true
    
  • Object 这个 构造器对象 比较特殊,实例化出来的对象的原型对象直接就是 Object.prototype,而其他的构造器对象,其实例对象的原型对象为对应的 xxx.prototype,再往一层才是 Object.prototype.

    const obj = new Object()
    console.log(obj.__proto__ === Object.prototype); // true
    
    function MyFunction() {}
    const myFunc = new MyFunction()
    console.log(myFunc.__proto__.__proto__ === Object.prototype); // true
    

原型链的实际应用

理解原型链不仅是理论知识,更重要的是它在实际开发中的应用。让我们通过具体例子来看原型链如何解决实际问题。

为什么方法要放在原型上?

错误的做法:将方法放在构造函数内部

function Person(name, age) {
    this.name = name;
    this.age = age;
    
    // 错误:每个实例都会创建一个新的函数
    this.introduce = function() {
        console.log(`我是 ${this.name},今年 ${this.age}`);
    };
    
    this.walk = function() {
        console.log(`${this.name} 正在走路`);
    };
}

const person1 = new Person('张三', 25);
const person2 = new Person('李四', 30);

// 每个实例的方法都是不同的函数对象
console.log(person1.introduce === person2.introduce); // false
console.log(person1.walk === person2.walk); // false

// 这意味着创建了多个相同功能的函数,浪费内存

正确的做法:将方法放在原型上

function Person(name, age) {
    this.name = name;
    this.age = age;
}

// 正确:将方法放在原型上,所有实例共享
Person.prototype.introduce = function() {
    console.log(`我是 ${this.name},今年 ${this.age}`);
};

Person.prototype.walk = function() {
    console.log(`${this.name} 正在走路`);
};

const person1 = new Person('张三', 25);
const person2 = new Person('李四', 30);

// 所有实例共享同一个方法
console.log(person1.introduce === person2.introduce); // true
console.log(person1.walk === person2.walk); // true

// 方法调用时,this 会正确指向调用的实例
person1.introduce(); // "我是 张三,今年 25 岁"
person2.introduce(); // "我是 李四,今年 30 岁"
动态添加方法

原型的一个强大特性是可以动态添加方法,所有实例都会立即获得新方法:

function Car(brand) {
    this.brand = brand;
}

const car1 = new Car('Toyota');
const car2 = new Car('Honda');

// 动态添加方法到原型
Car.prototype.start = function() {
    console.log(`${this.brand} 汽车启动了`);
};

Car.prototype.stop = function() {
    console.log(`${this.brand} 汽车停止了`);
};

// 之前创建的实例也能使用新方法
car1.start(); // "Toyota 汽车启动了"
car2.stop();  // "Honda 汽车停止了"
扩展内置对象(谨慎使用)

虽然可以扩展内置对象的原型,但这通常不被推荐,这样的做法往往被称之猴子补丁(monkey-patching):

// 不推荐:直接修改内置对象原型
Number.prototype.isEven = function() {
    return this % 2 === 0;
};

Number.prototype.isOdd = function() {
    return this % 2 === 1;
};

const num1 = 42;
const num2 = 13;

console.log(num1.isEven()); // true
console.log(num2.isOdd());  // true
// 问题:可能与其他库冲突,污染全局环境

更好的做法:使用继承或工具函数

// 推荐:创建自定义类继承内置类
class ExtendedNumber extends Number {
    isEven() {
        return this % 2 === 0;
    }
    isOdd() {
        return this % 2 === 1;
    }
    isPrime() {
        if (this < 2) return false;
        for (let i = 2; i <= Math.sqrt(this); i++) {
            if (this % i === 0) return false;
        }
        return true;
    }
}

const num = new ExtendedNumber(17);
console.log(num.isOdd());   // true
console.log(num.isPrime()); // true

// 或者使用工具函数
const NumberUtils = {
    isEven: (num) => num % 2 === 0,
    isOdd: (num) => num % 2 === 1,
    isPrime: (num) => {
        if (num < 2) return false;
        for (let i = 2; i <= Math.sqrt(num); i++) {
            if (num % i === 0) return false;
        }
        return true;
    }
};

console.log(NumberUtils.isEven(42)); // true
console.log(NumberUtils.isPrime(17)); // true
原型链在框架中的应用

许多 JavaScript 框架都大量使用原型链:

// 模拟一个简单的事件系统
function EventEmitter() {
    this.events = {};
}

EventEmitter.prototype.on = function(event, callback) {
    if (!this.events[event]) {
        this.events[event] = [];
    }
    this.events[event].push(callback);
};

EventEmitter.prototype.emit = function(event, ...args) {
    if (this.events[event]) {
        this.events[event].forEach(callback => callback(...args));
    }
};

EventEmitter.prototype.off = function(event, callback) {
    if (this.events[event]) {
        this.events[event] = this.events[event].filter(cb => cb !== callback);
    }
};

// 使用事件系统
const emitter = new EventEmitter();

emitter.on('message', (data) => {
    console.log('收到消息:', data);
});

emitter.emit('message', 'Hello World!'); // "收到消息: Hello World!"

原型链相关方法

Object.getPrototypeOf( )

该方法用于查找一个对象的原型对象。示例代码如下:

function Computer() {}
const c = new Computer()
console.log(Object.getPrototypeOf(c) === c.__proto__)

instanceof 操作符

判断一个对象是否是一个构造函数的实例。如果是返回 true,否则就返回 false。示例代码如下:

function Computer() {}
const c = new Computer()
console.log(c instanceof Computer) // true
console.log(c instanceof Array) // false
console.log([] instanceof Array) // true

isPrototypeOf( )

主要用于检测一个对象是否是一个另一个对象的原型对象,如果是返回 true,否则就返回 false。示例代码如下:

function Computer() {}
const c = new Computer()
console.log(Computer.prototype.isPrototypeOf(c)) // true
console.log(Computer.prototype.isPrototypeOf([])) // false
console.log(Array.prototype.isPrototypeOf([])) // true

hasOwnProperty( )

判断一个属性是定义在对象本身上面还是从原型对象上面继承而来的。如果是本身的,则返回 true,如果是继承而来的,则返回 false。示例代码如下:

const person = {
  arm: 2,
  legs: 2,
  walk() {
    console.log('walking')
  },
}
const john = Object.create(person, {
  name: {
    value: 'John',
    enumerable: true,
  },
  age: {
    value: 18,
    enumerable: true,
  },
})
console.log(john.hasOwnProperty('name')) // true
console.log(john.hasOwnProperty('arms')) // false

总结

原型与原型链是 JavaScript 中实现对象继承和属性查找的核心机制。通过原型链,对象可以继承其他对象的属性和方法,实现代码的复用和共享。希望本文能帮助你深入理解 JavaScript 的原型与原型链,并在开发中更好地应用它们。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值