Langchain系列文章目录
01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
08-【万字长文】MCP深度解析:打通AI与世界的“USB-C”,模型上下文协议原理、实践与未来
Python系列文章目录
PyTorch系列文章目录
机器学习系列文章目录
深度学习系列文章目录
Java系列文章目录
JavaScript系列文章目录
01-【JavaScript-Day 1】从零开始:全面了解 JavaScript 是什么、为什么学以及它与 Java 的区别
02-【JavaScript-Day 2】开启 JS 之旅:从浏览器控制台到 <script>
标签的 Hello World 实践
03-【JavaScript-Day 3】掌握JS语法规则:语句、分号、注释与大小写敏感详解
04-【JavaScript-Day 4】var
完全指南:掌握变量声明、作用域及提升
05-【JavaScript-Day 5】告别 var
陷阱:深入理解 let
和 const
的妙用
06-【JavaScript-Day 6】从零到精通:JavaScript 原始类型 String, Number, Boolean, Null, Undefined, Symbol, BigInt 详解
07-【JavaScript-Day 7】全面解析 Number 与 String:JS 数据核心操作指南
08-【JavaScript-Day 8】告别混淆:一文彻底搞懂 JavaScript 的 Boolean、null 和 undefined
09-【JavaScript-Day 9】从基础到进阶:掌握 JavaScript 核心运算符之算术与赋值篇
10-【JavaScript-Day 10】掌握代码决策核心:详解比较、逻辑与三元运算符
11-【JavaScript-Day 11】避坑指南!深入理解JavaScript隐式和显式类型转换
12-【JavaScript-Day 12】掌握程序流程:深入解析 if…else 条件语句
13-【JavaScript-Day 13】告别冗长if-else:精通switch语句,让代码清爽高效!
14-【JavaScript-Day 14】玩转 for
循环:从基础语法到遍历数组实战
15-【JavaScript-Day 15】深入解析 while 与 do…while 循环:满足条件的重复执行
16-【JavaScript-Day 16】函数探秘:代码复用的基石——声明、表达式与调用详解
17-【JavaScript-Day 17】函数的核心出口:深入解析 return
语句的奥秘
18-【JavaScript-Day 18】揭秘变量的“隐形边界”:深入理解全局与函数作用域
19-【JavaScript-Day 19】深入理解 JavaScript 作用域:块级、词法及 Hoisting 机制
20-【JavaScript-Day 20】揭秘函数的“记忆”:深入浅出理解闭包(Closure)
21-【JavaScript-Day 21】闭包实战:从模块化到内存管理,高级技巧全解析
22-【JavaScript-Day 22】告别 function
关键字?ES6 箭头函数 (=>
) 深度解析
23-【JavaScript-Day 23】告别繁琐的参数处理:玩转 ES6 默认参数与剩余参数
24-【JavaScript-Day 24】从零到一,精通 JavaScript 对象:创建、访问与操作
25-【JavaScript-Day 25】深入探索:使用 for...in
循环遍历 JavaScript 对象属性
26-【JavaScript-Day 26】零基础掌握JavaScript数组:轻松理解创建、索引、长度和多维结构
27-【JavaScript-Day 27】玩转数组:push
, pop
, slice
, splice
等方法详解与实战
28-【JavaScript-Day 28】告别繁琐循环:forEach
, map
, filter
数组遍历三剑客详解
29-【JavaScript-Day 29】数组迭代进阶:掌握 reduce、find、some 等高阶遍历方法
30-【JavaScript-Day 30】ES6新特性:Set与Map,让你的数据管理更高效!
31-【JavaScript-Day 31】对象的“蓝图”详解:构造函数、new
与 instanceof
完全指南
32-【JavaScript-Day 32】深入理解 prototype、__proto__ 与原型链的奥秘**
文章目录
前言
在 JavaScript 的世界中,对象无处不在。而理解 JavaScript 的原型(Prototype)和原型链(Prototype Chain)机制,是深入掌握这门语言面向对象编程特性、理解继承方式以及高效编写代码的关键。上一篇文章我们初步探讨了构造函数如何创建对象,但构造函数本身并不能完全解决代码复用和共享的问题。今天,我们将深入挖掘对象的“共享空间”——原型,以及它如何构建起强大的原型链。准备好了吗?让我们一起揭开 JavaScript 原型机制的神秘面纱!
一、回顾:为什么需要原型?
在深入原型之前,我们先回顾一下单纯使用构造函数创建对象时可能遇到的问题,这将帮助我们理解引入原型的必要性。
1.1 构造函数创建对象的局限性
我们在【JavaScript-Day 31】中学习了如何使用构造函数来创建具有相似属性和方法的对象。例如,创建一个 Person
构造函数:
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHello = function() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};
this.species = "Homo sapiens"; // 假设所有 Person 实例都有这个属性
}
const person1 = new Person("Alice", 30);
const person2 = new Person("Bob", 25);
person1.sayHello(); // Hello, my name is Alice and I am 30 years old.
person2.sayHello(); // Hello, my name is Bob and I am 25 years old.
console.log(person1.sayHello === person2.sayHello); // false
console.log(person1.species === person2.species); // true (对于基本类型值是这样,但如果是对象就不一样了)
这段代码看起来不错,每个实例都有自己的 name
和 age
,以及一个 sayHello
方法。但这里存在一些潜在的问题。
1.1.1 内存浪费问题
观察 console.log(person1.sayHello === person2.sayHello);
的输出是 false
。这意味着 person1
的 sayHello
方法和 person2
的 sayHello
方法是两个独立存在于内存中的不同函数。如果创建了成百上千个 Person
实例,那么内存中就会有成百上千个功能完全相同的 sayHello
函数副本。这显然是一种内存资源的浪费。
对于像 species
这样的共享属性,如果它是一个复杂对象而不是简单的字符串,同样会存在每个实例都复制一份的问题。
1.1.2 代码共享与维护问题
如果所有 Person
实例的 sayHello
方法逻辑都是一样的,那么将这个方法在每个实例中都创建一遍,不仅浪费内存,也不利于代码的维护。如果我们想修改 sayHello
的行为,就需要遍历所有实例去修改,这几乎是不可能的。
思考: 有没有一种方法,可以让所有实例共享同一个方法,从而节省内存并方便维护呢?答案就是——原型!
二、深入理解 prototype
属性
为了解决上述问题,JavaScript 引入了原型机制。核心概念之一就是构造函数的 prototype
属性。
2.1 什么是函数的 prototype
属性?
在 JavaScript 中,每个函数(不仅仅是构造函数,但主要与构造函数配合使用才有意义)在创建时都会自动获得一个特殊的属性,叫做 prototype
。这个 prototype
属性的值是一个对象,我们通常称之为原型对象。
你可以把它想象成一个“模板”或者“共享仓库”。
function Dog(name) {
this.name = name;
}
// Dog 函数一被创建,就自动拥有了 prototype 属性
console.log(typeof Dog.prototype); // "object"
console.log(Dog.prototype); // 输出一个对象,通常包含 constructor 属性
2.2 prototype
的作用:共享方法与属性
prototype
对象的核心作用就是存放那些希望被该构造函数创建的所有实例所共享的属性和方法。
当我们通过 new
关键字调用构造函数创建实例时,实例内部会有一个机制(我们稍后会讲到的 __proto__
)指向构造函数的 prototype
对象。这样,实例就可以访问到定义在 prototype
对象上的属性和方法了。
2.2.2 代码示例:将方法添加到 prototype
让我们改造之前的 Person
构造函数,将 sayHello
方法和 species
属性放到 Person.prototype
上:
function Person(name, age) {
this.name = name; // 实例独有的属性
this.age = age; // 实例独有的属性
}
// 将共享的属性和方法添加到 Person.prototype 对象上
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};
Person.prototype.species = "Homo sapiens";
const person1 = new Person("Alice", 30);
const person2 = new Person("Bob", 25);
person1.sayHello(); // Hello, my name is Alice and I am 30 years old.
person2.sayHello(); // Hello, my name is Bob and I am 25 years old.
console.log(person1.sayHello === person2.sayHello); // true 🎉 方法共享了!
console.log(person1.species); // "Homo sapiens"
console.log(person2.species); // "Homo sapiens"
console.log(person1.species === person2.species); // true
现在,person1.sayHello
和 person2.sayHello
指向的是内存中同一个函数,即 Person.prototype.sayHello
。同样,它们访问的 species
也是共享的 Person.prototype.species
。这就大大节省了内存,并且如果需要修改 sayHello
的行为,只需要修改 Person.prototype.sayHello
一处即可。
关键点: 实例自身通常只存储那些独有的属性(如 name
, age
),而共享的方法和属性则存储在构造函数的 prototype
对象中。
2.3 prototype
对象中的 constructor
属性
每个原型对象(Foo.prototype
)都会自动获得一个 constructor
属性,这个属性是一个指针,默认指向其关联的构造函数本身(即 Foo
)。
function Cat(name) {
this.name = name;
}
console.log(Cat.prototype.constructor); // ƒ Cat(name) { ... }
console.log(Cat.prototype.constructor === Cat); // true
这个 constructor
属性有什么用呢?
- 身份识别:可以用来判断一个对象的构造函数是谁。例如,
person1.constructor === Person
会返回true
(如果constructor
未被修改)。但要注意,constructor
属性是可以被修改的,所以不总是绝对可靠的身份判断依据。 - 对象创建:虽然不常用,但理论上可以通过
instance.constructor()
来创建同类型的新对象。
场景:
假设我们有一个对象 obj
,我们想知道它是由哪个构造函数创建的。
const fluffy = new Cat("Fluffy");
console.log(fluffy.constructor); // ƒ Cat(name) { ... }
console.log(fluffy.constructor === Cat); // true
const genericObj = {};
console.log(genericObj.constructor); // ƒ Object() { [native code] }
const arr = [];
console.log(arr.constructor); // ƒ Array() { [native code] }
注意: 当我们完全重写构造函数的 prototype
对象时,需要手动修复 constructor
属性的指向,否则它会指向 Object
构造函数。
function Bird(name) {
this.name = name;
}
Bird.prototype = { // 完全重写了 prototype 对象
// constructor: Bird, // 如果不手动指定,constructor 会丢失或指向 Object
fly: function() { console.log(this.name + ' is flying.'); }
};
const sparrow = new Bird("Sparrow");
// console.log(sparrow.constructor === Bird); // false,因为 prototype 被重写且未指定 constructor
// 正确的做法是:
Bird.prototype = {
constructor: Bird, // 手动将 constructor 指回 Bird
fly: function() { console.log(this.name + ' is flying.'); }
};
const eagle = new Bird("Eagle");
console.log(eagle.constructor === Bird); // true
三、揭秘对象的 __proto__
与原型链
我们已经知道,实例可以访问到构造函数 prototype
对象上的共享成员。那么,实例是如何与构造函数的 prototype
对象关联起来的呢?这就引出了 __proto__
属性和原型链的概念。
3.1 什么是对象的 __proto__
属性?
当通过 new Constructor()
创建一个实例对象时,该实例对象内部会拥有一个特殊的属性(在一些旧的 JavaScript 引擎中或非标准实现里,这个属性被称为 __proto__
,发音 “dunder proto”)。这个 __proto__
属性指向创建该对象的构造函数的 prototype
对象。
也就是说:
instance.__proto__ === Constructor.prototype
重要提示: __proto__
属性曾经是一个非标准但被广泛实现的属性。ES6 之后,推荐使用 Object.getPrototypeOf()
方法来获取一个对象的原型,以及使用 Object.setPrototypeOf()
来设置一个对象的原型。尽管如此,理解 __proto__
对于理解原型链仍然很有帮助。
function Animal(sound) {
this.sound = sound;
}
Animal.prototype.makeSound = function() {
console.log(this.sound);
}
const lion = new Animal("Roar!");
// 实例 lion 的 __proto__ 指向 Animal.prototype
console.log(lion.__proto__ === Animal.prototype); // true (在支持 __proto__ 的环境中)
所以,当 lion.makeSound()
被调用时:
- JavaScript 引擎首先在
lion
对象自身查找是否有makeSound
属性。 - 如果找不到,它就会通过
lion.__proto__
找到Animal.prototype
对象。 - 然后在
Animal.prototype
对象上查找makeSound
属性。 - 找到了,就执行它。此时,方法内的
this
仍然指向调用该方法的实例lion
。
3.2 __proto__
与 Object.getPrototypeOf()
如前所述,__proto__
不是标准写法。获取一个对象原型的标准方法是 Object.getPrototypeOf()
。
function Car(brand) {
this.brand = brand;
}
Car.prototype.getBrand = function() {
return this.brand;
};
const myCar = new Car("Toyota");
// 使用 Object.getPrototypeOf()
console.log(Object.getPrototypeOf(myCar) === Car.prototype); // true
// 效果等同于 (在支持 __proto__ 的环境中)
// console.log(myCar.__proto__ === Car.prototype); // true
Object.getPrototypeOf(obj)
返回 obj
的内部 [[Prototype]]
属性的值。
3.3 什么是原型链?
现在我们理解了 instance.__proto__
指向 Constructor.prototype
。但 Constructor.prototype
本身也是一个对象,那么它有没有自己的 __proto__
呢?答案是肯定的!
几乎所有的 JavaScript 对象在创建时都会被赋予一个原型对象,而这个原型对象自身也可能拥有原型,这样一层一层地链接起来,就形成了一个原型链(Prototype Chain)。
3.3.1 原型链的查找机制
当你试图访问一个对象的属性或方法时,JavaScript 引擎会执行以下查找过程:
- 首先在对象自身查找:看对象实例本身是否有该属性/方法。
- 如果自身没有,则沿着
__proto__
向上查找:如果实例自身找不到,就会通过其__proto__
指针(即Object.getPrototypeOf()
返回的对象)在其原型对象上查找。 - 继续向上查找:如果原型对象上还是没有,就继续通过原型对象的
__proto__
向上查找,直到链的末端。 - 直到
Object.prototype
:大部分原型链的顶端是Object.prototype
对象。 - 原型链终点:
Object.prototype
对象的__proto__
是null
,表示原型链的结束。 - 未找到:如果在整个原型链上都没有找到该属性/方法,则属性读取返回
undefined
,方法调用会抛出TypeError
。
这个属性/方法的查找过程,就是原型链的核心工作机制。
3.3.2 原型链的终点:Object.prototype
我们知道,在 JavaScript 中,几乎“万物皆对象”(除了原始类型,但它们在进行属性访问时也会被临时包装成对象)。而所有普通对象的原型链最终都会指向 Object.prototype
。
Object.prototype
包含了一些所有对象都通用的方法,比如 toString()
、hasOwnProperty()
、valueOf()
等。
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function() {
console.log(this.name);
};
const alice = new Person("Alice");
// alice.__proto__ is Person.prototype
// Person.prototype.__proto__ is Object.prototype
// Object.prototype.__proto__ is null
console.log(alice.toString()); // 调用的是 Object.prototype.toString()
// 查找过程:
// 1. alice 对象自身没有 toString()
// 2. alice.__proto__ (Person.prototype) 上也没有 toString() (假设我们没重写)
// 3. Person.prototype.__proto__ (Object.prototype) 上找到了 toString(),执行它。
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null
3.3.3 图解原型链
让我们用 Mermaid 语法来可视化一个更完整的原型链:
解读上图:
instance1
是由MyConstructor
创建的。instance1
的__proto__
指向MyConstructor.prototype
。MyConstructor.prototype
是一个普通对象,所以它的__proto__
指向Object.prototype
。Object.prototype
的__proto__
是null
。
当我们调用 instance1.methodA()
时,它会在 MyConstructor.prototype
中找到。
当我们调用 instance1.toString()
时,它会在 Object.prototype
中找到。
当我们访问 instance1.nonExistentProperty
时,最终会返回 undefined
。
3.4 hasOwnProperty()
的妙用
既然属性可以存在于实例自身,也可以存在于原型链上,那么如何区分一个属性到底是实例自身的还是继承自原型的呢?
Object.prototype
提供了一个非常有用的方法:hasOwnProperty(propertyName)
。
object.hasOwnProperty(prop)
方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的OwnProperty);该方法不会检查原型链。
function Fruit(name) {
this.name = name; // 实例自身属性
}
Fruit.prototype.color = "unknown"; // 原型属性
const apple = new Fruit("Apple");
apple.taste = "sweet"; // 给实例添加自身属性
console.log(apple.hasOwnProperty("name")); // true (name 是 apple 自身的)
console.log(apple.hasOwnProperty("color")); // false (color 是继承自 Fruit.prototype 的)
console.log(apple.hasOwnProperty("taste")); // true (taste 是 apple 自身的)
console.log(apple.hasOwnProperty("toString"));// false (toString 继承自 Object.prototype)
// 如何判断属性是否存在于对象上(包括原型链)?
console.log("name" in apple); // true
console.log("color" in apple); // true
console.log("taste" in apple); // true
console.log("toString" in apple); // true
in
操作符会检查属性是否存在于对象自身或其原型链上。
四、原型继承的简单示例
原型链最强大的应用之一就是实现继承。子构造函数的原型对象可以指向父构造函数的实例,或者更常见的是将子构造函数的原型设置为通过父构造函数原型创建的对象,从而让子构造函数的实例能够访问父构造函数原型上的方法和属性。
4.1 如何通过原型实现继承?
这通常涉及到几个步骤,最基本的一种方式(ES6 class
出现之前的经典方式之一)是:
- 让子构造函数的
prototype
指向一个父构造函数的实例。 - (可选但推荐)修复子构造函数
prototype
的constructor
指向。
下面是一个非常简化的例子来说明核心思想(更完善的继承方式比较复杂,会在后续专门讨论)。
// 父构造函数
function Animal(name) {
this.name = name;
this.kingdom = "Animalia";
}
Animal.prototype.eat = function() {
console.log(this.name + " is eating.");
}
// 子构造函数
function Dog(name, breed) {
// Animal.call(this, name); // 调用父构造函数,继承实例属性 (关键步骤1)
this.name = name; // 简化处理,实际继承需要调用父构造函数
this.breed = breed;
}
// 核心:实现原型继承 (关键步骤2)
// 创建一个 Animal 的实例作为 Dog.prototype 的原型
// 这样 Dog.prototype 就能通过其 __proto__ 访问到 Animal.prototype 上的方法
Dog.prototype = Object.create(Animal.prototype); // 更推荐的方式,避免了直接 new Animal() 可能带来的副作用
// 修复 constructor 指向 (关键步骤3)
Dog.prototype.constructor = Dog;
// 给子构造函数添加自己的原型方法
Dog.prototype.bark = function() {
console.log(this.name + " says Woof!");
}
const myPet = new Dog("Buddy", "Golden Retriever");
myPet.eat(); // Buddy is eating. (继承自 Animal.prototype)
myPet.bark(); // Buddy says Woof! (Dog.prototype 自身的方法)
console.log(myPet.kingdom); // undefined (如果没用 Animal.call,则实例属性未继承)
// 如果用了 Animal.call(this, name),则会输出 Animalia
console.log(myPet instanceof Dog); // true
console.log(myPet instanceof Animal); // true
代码解析:
Dog.prototype = Object.create(Animal.prototype);
这一行是实现原型继承的关键。Object.create()
方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
。这意味着Dog.prototype
的原型是Animal.prototype
。因此,Dog
的实例可以通过原型链访问到Animal.prototype
上的方法。Dog.prototype.constructor = Dog;
重置constructor
属性,使其正确指向Dog
构造函数。- 在
Dog
构造函数内部,通常还需要通过Animal.call(this, name);
(或Animal.apply(this, [name]);
) 来调用父构造函数,以便继承父构造函数中定义的实例属性(如this.name
、this.kingdom
)。在上面的简化例子中,为了突出原型链,我们暂时省略了这一步对kingdom
的继承,仅在注释中提及。
这种继承方式使得 myPet
能够访问 Animal.prototype
上的 eat
方法,同时也拥有 Dog.prototype
上的 bark
方法。
注意: JavaScript 中有多种实现继承的方式,包括原型链继承、借用构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承,以及 ES6 引入的 class
语法糖。class extends
是目前最推荐和简洁的方式,但其底层仍然是基于原型机制。
五、constructor
属性再探
我们之前提到,每个 prototype
对象都有一个 constructor
属性,默认指向其关联的构造函数。
function Vehicle() {}
console.log(Vehicle.prototype.constructor === Vehicle); // true
const car = new Vehicle();
console.log(car.constructor === Vehicle); // true (通过原型链找到)
这个属性的主要作用是:
- 确定对象类型:
instance.constructor
可以帮助我们(不完全可靠地)判断一个实例是由哪个构造函数创建的。 - 创建相似对象:理论上可以用
instance.constructor()
来创建与instance
同类型的新对象,但这依赖于constructor
属性没有被错误地修改。
5.1 constructor
的易变性
需要注意的是,prototype
对象是可写的,这意味着 prototype.constructor
属性也是可以被修改的。
function Gadget() {}
const widget = new Gadget();
console.log(widget.constructor === Gadget); // true
// 修改 Gadget.prototype
Gadget.prototype = {
use: function() { console.log("Using gadget"); }
// 注意:这里没有显式设置 constructor,所以 widget.constructor 将不再是 Gadget
};
const newWidget = new Gadget(); // Gadget.prototype 已被替换
console.log(newWidget.constructor === Gadget); // false!
console.log(newWidget.constructor === Object); // true,因为新对象 {use: ...} 的 constructor 默认是 Object
// 正确的做法是在重写 prototype 时,手动指定 constructor
Gadget.prototype = {
constructor: Gadget, // 保持 constructor 指向正确
use: function() { console.log("Using gadget"); }
};
const anotherWidget = new Gadget();
console.log(anotherWidget.constructor === Gadget); // true
因此,在完全覆盖一个构造函数的 prototype
对象时,务必记得手动将 constructor
属性指回原来的构造函数,以保持其语义的正确性。
5.2 constructor
与 instanceof
虽然 constructor
可以用于类型判断,但 instanceof
操作符通常是更可靠的选择,因为它检查的是整个原型链。
object instanceof Constructor
:判断Constructor.prototype
是否存在于object
的原型链上。
function A() {}
function B() {}
B.prototype = Object.create(A.prototype);
B.prototype.constructor = B; // 修复 constructor
const objB = new B();
console.log(objB.constructor === B); // true
console.log(objB.constructor === A); // false
console.log(objB instanceof B); // true
console.log(objB instanceof A); // true (因为 B.prototype 的原型是 A.prototype)
console.log(objB instanceof Object); // true
instanceof
更能反映对象在继承体系中的关系。
六、常见问题与最佳实践
6.1 prototype
vs __proto__
的混淆
初学者最容易混淆这两个概念:
prototype
:是函数(特指构造函数)才拥有的属性。它指向一个对象,这个对象包含了希望被该构造函数创建的所有实例共享的属性和方法。它是“蓝图”的一部分。__proto__
(或通过Object.getPrototypeOf()
访问的内部[[Prototype]]
):是实例对象拥有的(内部)属性。它指向创建该实例的构造函数的prototype
对象。它是实例与“蓝图”之间的链接。
简单记:函数有 prototype
,实例有 __proto__
(内部链接)。
6.2 修改内置对象的原型?谨慎!
理论上,你可以修改 JavaScript 内置对象的原型,比如 Array.prototype
、Object.prototype
等,从而为所有数组或对象添加新的默认方法。
// 不推荐这样做!
Array.prototype.myCustomMethod = function() {
console.log("This is my custom array method!");
};
const arr = [1, 2, 3];
arr.myCustomMethod(); // "This is my custom array method!"
为什么不推荐?
- 命名冲突:你添加的方法名可能与未来 JavaScript 版本中新增的标准方法名冲突,或者与第三方库添加的方法名冲突,导致不可预期的行为。
- 可维护性差:这种全局性的修改使得代码行为不透明,其他开发者(或未来的你)可能不知道这个方法是从哪里来的。
- 污染全局:修改了所有数组(或对象)的行为,可能会对项目中其他不期望这种行为的部分产生副作用。
最佳实践:通常避免修改内置对象的原型。如果需要扩展功能,可以考虑使用继承创建子类,或者使用包装函数/辅助函数。
6.3 原型链的性能考量
属性/方法的查找是沿着原型链进行的。如果原型链过长或过于复杂,理论上查找时间会增加,可能会对性能产生微小的影响。
然而,在现代 JavaScript 引擎中,这种查找通常被高度优化,对于大多数应用来说,原型链的深度很少成为性能瓶颈。更重要的是代码的清晰度和可维护性。
建议:
- 保持原型链的相对扁平,避免不必要的深度。
- 关注代码逻辑和算法优化,这些通常对性能的影响更大。
6.4 避免在 prototype
上使用箭头函数定义需要 this
的方法
当在 prototype
上定义方法时,如果这些方法需要使用 this
来引用调用该方法的实例对象,那么应该使用普通函数,而不是箭头函数。
function Counter() {
this.count = 0;
}
// 正确:使用普通函数,this 指向实例
Counter.prototype.increment = function() {
this.count++;
console.log(this.count);
};
// 错误:使用箭头函数,this 会捕获其定义时的上下文(通常是全局对象或 undefined)
Counter.prototype.decrement = () => {
// 'this' 在这里不会指向 Counter 的实例
// this.count--; // 这会导致错误或意外行为
// console.log(this.count);
console.error("Arrow function 'this' context issue!", this);
};
const c1 = new Counter();
c1.increment(); // 1
c1.decrement(); // Error or logs window/undefined depending on strict mode
箭头函数没有自己的 this
绑定,它会捕获其词法作用域中的 this
值。在原型方法中,我们通常希望 this
指向调用该方法的具体实例。
七、总结
原型和原型链是 JavaScript 中实现对象间共享属性和方法、以及实现继承的核心机制。理解它们对于写出高效、可维护的 JavaScript 代码至关重要。
本次我们学习了:
- 构造函数的局限性:单纯使用构造函数在每个实例中创建方法会导致内存浪费和维护困难。
prototype
属性:- 每个函数都有一个
prototype
属性,它是一个对象(原型对象)。 - 用于存放实例间共享的属性和方法,从而节省内存。
- 原型对象有一个
constructor
属性,默认指回其关联的构造函数。
- 每个函数都有一个
__proto__
属性与Object.getPrototypeOf()
:- 实例对象有一个内部的
[[Prototype]]
链接(常通过非标准的__proto__
访问,或标准的Object.getPrototypeOf()
获取),指向其构造函数的prototype
对象。 instance.__proto__ === Constructor.prototype
。
- 实例对象有一个内部的
- 原型链 (Prototype Chain):
- 对象通过
__proto__
链接起来形成一个链状结构。 - 属性和方法的查找会沿着原型链向上进行,直到找到或者到达链的末端 (
Object.prototype.__proto__
为null
)。 Object.prototype
是大多数对象原型链的顶端,包含如toString()
、hasOwnProperty()
等通用方法。
- 对象通过
hasOwnProperty()
:用于检查属性是对象自身的还是继承自原型链的。- 原型继承的初步认识:通过操作子构造函数的
prototype
使其指向父构造函数原型链上的对象(如Object.create(Parent.prototype)
),可以实现继承。 constructor
属性的重要性:在重写prototype
对象时,需要手动维护constructor
的正确指向。- 常见问题与最佳实践:区分
prototype
与__proto__
,谨慎修改内置对象原型,注意原型链性能(通常不是主要瓶颈),以及在原型方法中正确使用this
(避免箭头函数)。
原型机制是 JavaScript 语言的精髓之一。虽然 ES6 的 class
语法让面向对象编程看起来更像传统的类式语言,但其底层依然是基于原型和原型链的。透彻理解原型,能让你更好地驾驭 JavaScript,写出更优雅、更高效的代码。在下一篇中,我们将正式学习 ES6 中引入的 class
语法,看看它是如何简化基于原型的面向对象编程的。