掌握 JavaScript:理解原型和原型链

原型和原型链是学习 JavaScript 过程中极为关键的概念,是深入理解 JavaScript 面向对象编程的核心要点。

一、理解原型

(一)原型的基本概念

在 JavaScript 中,几乎所有的对象都有一个__proto__属性,这个属性指向了该对象的原型对象。可以把原型对象想象成一个 "模板",它为对象提供了一些默认的属性和方法。当对象本身没有某个特定属性或方法时,JavaScript 引擎就会沿着__proto__所指的方向,到原型对象中去寻找。

比如,创建一个简单的对象:

javascript 复制代码
let animal = {
    name: '小狗',
    speak() {
        console.log('我会叫');
    }
};
console.log(animal.__proto__); 

这里的animal对象就有一个__proto__属性,它指向了一个隐藏的原型对象。

原型对象的创建

1. 构造函数方式

构造函数是创建对象的一种模板。当我们定义一个构造函数时,JavaScript 会自动为其创建一个与之关联的原型对象。例如:

javascript 复制代码
function Animal(name) {
    this.name = name;
}
// Animal.prototype 即为与 Animal 构造函数关联的原型对象
Animal.prototype.speak = function() {
    console.log(`${this.name} makes a sound.`);
};

在上述代码中,Animal 是一个构造函数。当我们定义 Animal.prototype.speak 方法时,实际上是在为 Animal 构造函数的原型对象添加一个 speak 方法。之后,通过 new Animal('Lion') 创建的所有 Animal 实例都能访问到这个 speak 方法。

2. Object.create () 方式

Object.create() 方法为我们提供了一种更为灵活的创建原型对象的途径。它允许我们基于一个已有的对象来创建新的对象,新对象的原型将指向传入的对象。例如:

javascript 复制代码
const animalPrototype = {
    speak: function() {
        console.log(`${this.name} makes a sound.`);
    }
};
const dog = Object.create(animalPrototype);
dog.name = 'Buddy';
dog.speak(); // Buddy makes a sound.

这里,我们首先定义了一个 animalPrototype 对象,然后使用 Object.create(animalPrototype) 创建了 dog 对象。dog 对象的原型就是 animalPrototype,所以它能够调用 speak 方法。

3. 字面量方式

使用对象字面量创建对象时,该对象会默认继承 Object.prototype 作为其原型对象。对象字面量是一种简洁创建对象的方式。

示例代码如下:

javascript 复制代码
// 使用对象字面量创建对象 cat
const cat = {
    name: 'Cat'
};
// cat 对象的原型是 Object.prototype
console.log(cat.__proto__ === Object.prototype); 

在这段代码中,cat 对象是通过对象字面量创建的,它的 __proto__ 属性指向 Object.prototype

4. 类语法(ES6 及以后)

ES6 引入了类语法,虽然本质上类还是基于原型实现的,但它提供了更接近传统面向对象语言的语法。类中的 prototype 可以用来定义共享的方法。

示例代码如下:

javascript 复制代码
// 定义一个类 Animal
class Animal {
    constructor(name) {
        this.name = name;
    }
    // 定义共享方法 speak
    speak() {
        console.log(`${this.name} makes a sound.`);
    }
}
// 通过类创建实例
const tiger = new Animal('Tiger');
tiger.speak(); 

Animal 类有一个 prototype 属性,也就是 Animal.prototype,它是一个对象,这个对象就是通过 Animal 类创建的实例的原型对象。

(二)函数的 prototype 属性

在 JavaScript 中,函数具有独特的性质。它和普通对象一样拥有 __proto__ 属性,此外还具备特有的 prototype 属性。prototype 是函数独有的一个属性,指向一个对象,用于实现构造函数的原型继承。

当使用构造函数创建新对象时,实际上是新对象的内部 [[Prototype]] 属性会指向构造函数的 prototype 属性所对应的对象。__proto__ 是访问这个内部 [[Prototype]] 属性的一种方式(不过 __proto__ 并非标准属性,在现代 JavaScript 里,更推荐使用 Object.getPrototypeOf() 方法来访问对象的原型)。这种机制构建起了新对象与构造函数原型之间的关键联系。

例如:

javascript 复制代码
function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.sayHello = function() {
    console.log(`大家好,我是${this.name},今年${this.age}岁`);
};
let tom = new Person('Tom', 25);
console.log(tom.__proto__ === Person.prototype); 
tom.sayHello(); 

在上述代码里,Person 作为构造函数,其 prototype 是一个对象,我们在上面定义了 sayHello 方法。使用 new 关键字创建 tom 对象后,tom[[Prototype]] 指向 Person.prototype__proto__ 反映了这一关系。调用 tom.sayHello() 时,JavaScript 引擎会先在 tom 自身查找该方法,找不到就会顺着 [[Prototype]]Person.prototype 中查找并执行。

这种特性让通过同一构造函数创建的对象能共享 prototype 上的属性和方法,避免为每个对象单独创建副本,实现代码复用与内存节省。

__proto__prototype[[Prototype]] 的区别

  • __proto__ 是一个实际存在于对象中的属性,它是对对象内部 [[Prototype]] 的引用。
    • 借助 __proto__,你能够在代码里直接访问对象的原型。
    • __proto__ 并非标准属性,尽管大多数浏览器都支持它,但不建议在生产环境中使用,因为它可能会引发性能问题,并且在某些环境下可能不被支持。例如:
javascript 复制代码
let obj = {};
console.log(obj.__proto__ === Object.prototype); // true

`

  • prototype 是函数特有的属性。当一个函数作为构造函数使用时,通过 new 关键字创建的对象,其 __proto__ 属性会指向构造函数的 prototype 属性。
    • 也就是说,prototype 是为了实现构造函数的原型继承而设计的。例如:
javascript 复制代码
function MyClass() {}
MyClass.prototype.someMethod = function() {
    console.log('This is a method on the prototype');
};
let instance = new MyClass();
console.log(instance.__proto__ === MyClass.prototype); // true
instance.someMethod(); // This is a method on the prototype
  • [[Prototype]] 是 JavaScript 内部的一个隐式属性,它体现了对象之间的原型关联。
    • JavaScript 引擎在查找对象的属性与方法时,会依据 [[Prototype]] 构建的原型链来进行搜索。
    • 不过,[[Prototype]] 无法直接在代码里访问,它是一个抽象的概念,用来描述对象的原型继承关系。

(三)原型对象的作用

属性与方法继承

原型对象最核心的作用之一就是实现属性和方法的继承。当我们访问一个对象的属性或方法时,如果该对象自身没有这个属性或方法,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的顶端(Object.prototype)。例如:

javascript 复制代码
function Dog(name) {
    this.name = name;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
    console.log(`${this.name} barks.`);
};
const myDog = new Dog('Max');
myDog.speak(); // Max makes a sound.
myDog.bark(); // Max barks.

在这个例子中,Dog 构造函数的原型继承自 Animal 构造函数的原型。因此,myDog 作为 Dog 的实例,不仅能够访问 Dog.prototype 上定义的 bark 方法,还能访问 Animal.prototype 上的 speak 方法。

节省内存

通过原型对象,多个对象可以共享相同的属性和方法,从而节省内存空间。假设我们有一个 Circle 构造函数,并且为其原型定义了一个 draw 方法:

javascript 复制代码
function Circle(radius) {
    this.radius = radius;
}
Circle.prototype.draw = function() {
    console.log(`Drawing a circle with radius ${this.radius}`);
};
const circle1 = new Circle(5);
const circle2 = new Circle(10);

circle1circle2 这两个对象共享 Circle.prototype 上的 draw 方法,而不是每个对象都拥有一份独立的 draw 方法副本。这在创建大量对象时,能够显著减少内存的占用。

二、原型链的奥秘

(一)原型链的形成机制

  • 在 JavaScript 里,原型链通过对象原型层层相连构成。

  • 引擎首先会在对象本身的属性列表中进行查找,检查目标属性或方法是否直接存在于该对象上。若对象自身拥有此属性或方法,引擎会立即使用它。

  • 如果对象自身并不包含目标属性或方法,引擎就会借助对象的 __proto__ 属性来深入探索原型链。__proto__ 提供了对对象内部 [[Prototype]] 的访问途径,[[Prototype]] 指向该对象的原型对象。引擎会顺着这个链接进入原型对象,继续在原型对象的属性和方法中搜索目标内容。

  • 若在当前原型对象中仍未找到目标,引擎不会停止,而是会继续沿着原型链向上查找,不断通过每个原型对象的 __proto__(即其自身的 [[Prototype]])访问上一级原型对象,持续搜索,直至到达原型链的顶端(Object.prototype,其 [[Prototype]]null)。如果遍历完整个原型链都未找到目标属性或方法,最终会返回 undefined

以数组为例:

javascript 复制代码
let arr = [1, 2, 3];
arr.push(4); 

数组对象arr自身没有push方法。但arr__proto__指向Array.prototype,而Array.prototype上定义了push方法。借助原型链,arr便能调用push方法。

(二)原型链的结构特性与查找终点

原型链有个重要特性:它的终点是null

以数组arr来说,arr__proto__指向Array.prototypeArray.prototype__proto__指向Object.prototypeObject.prototype__proto__null,这样就形成了完整的原型链。

这一特性影响着查找过程。当在arr中查找属性或方法时,从arr自身开始,沿__proto__依次在Array.prototypeObject.prototype中查找。一旦到达Object.prototype,且其__proto__null时还未找到目标,就可确定该属性或方法不存在 。null作为原型链终点,为查找提供了明确的终止信号,保证了查找的完整性。

(三)原型链的作用

  1. 代码复用 :在原型对象上定义属性和方法,可实现多个对象对这些内容的共享,有效减少代码冗余。如上述Person构造函数,所有通过Person创建的对象都能共享Person.prototype上的sayHello方法。
  2. 实现继承 :JavaScript 没有传统面向对象语言的类继承语法,而是借助原型链实现继承。例如创建一个继承自PersonStudent对象:
javascript 复制代码
function Student(name, age, grade) {
    Person.call(this, name, age);
    this.grade = grade;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
Student.prototype.study = function() {
    console.log(`${this.name}正在学习,年级是${this.grade}`);
};
let lucy = new Student('Lucy', 18, '高三');
lucy.sayHello(); 
lucy.study(); 

在这段代码中,Student通过原型链继承了Person的属性和方法,同时添加了自身特有的study方法,充分展示了原型链在实现对象间继承关系方面的重要作用 。

三、原型和原型链的常见问题

(一)原型污染

如果不小心修改了原型对象上的属性,可能会影响到所有基于该原型创建的对象,这就是原型污染。例如:

javascript 复制代码
Object.prototype.newProp = '新属性';
let obj1 = {};
let obj2 = {};
console.log(obj1.newProp); 
console.log(obj2.newProp); 

这里在Object.prototype上添加了一个新属性newProp,导致所有对象都有了这个属性,这可能会带来意想不到的问题。所以在开发中要避免对原型对象进行不必要的修改。

(二)理解constructor属性

每个原型对象都有一个constructor属性,它指向创建该原型对象的构造函数。但是在修改原型对象时,要注意constructor属性的指向是否正确。比如在上面Student继承Person的例子中,修改Student.prototype后,需要手动将Student.prototype.constructor指向Student,否则constructor属性会指向错误的构造函数。

javascript 复制代码
Student.prototype = Object.create(Person.prototype);
// 手动设置 constructor 属性
Student.prototype.constructor = Student;

let student = new Student('Lucy', 18, '高三');
console.log(student.constructor === Student); // true

希望通过这篇文章,大家能对 JavaScript 的原型和原型链有更清晰的认识,在 JavaScript 的学习道路上迈出更坚实的步伐。

相关推荐
zru_960216 分钟前
Vue 常用组件介绍博客
前端·javascript·vue.js
ConardLi1 小时前
MCP + 数据库,一种比 RAG 检索效果更好的新方式!
javascript·数据库·人工智能
m0_616188491 小时前
PDF预览-搜索并高亮文本
开发语言·javascript·ecmascript
勘察加熊人2 小时前
vue猜词游戏
前端·vue.js·游戏
且心2 小时前
【问题处理】webpack4升webpack5,报错Uncaught ReferrnceError: process is not defined
前端·webpack5·process·uncaught·referrnceerror
美美打不死2 小时前
webpack js 逆向 --- 个人记录
开发语言·javascript·webpack
我是哈哈hh2 小时前
【Vue】 核心特性实战解析:computed、watch、条件渲染与列表渲染
前端·javascript·vue.js·前端框架·vue·语法基础
龙在天2 小时前
“手速太快,分页翻车?”,前端分页竞态问题,看这一篇就够了
前端
前端Hardy2 小时前
HTML&CSS:超好看的收缩展开菜单
javascript·css·html
Riesenzahn3 小时前
你使用过css3的:root吗?说说你对它的理解
前端·javascript