深入理解:带着问题看原型和原型链带着问题找答案之 什么是构造函数? 什么是实例? 什么是 `prototype`? 什么

前言
- 学而时习之,不亦说乎;
- 吾日三省吾身;
- 学而不思则罔,思而不学则殆;
今天还是研究Promsie的一天,然后就在脑中响起了关于原型和原型链的知识。。越想越觉得这些概念像浆糊一样,变得混乱不清。
什么是构造函数? 什么是实例? 什么是 prototype? 什么是 __proto__? [[Prototype]] 又是什么?
大致都懂,又不太清晰,不太确定,于是我决定请出 GPT-4 来解答我的疑问。并带大家一同梳理回顾其中的所有知识点。Let's go
First problem:先来三段代码看看区别
class Person {
name = 'alice';
action() {
console.log('action');
}
}
class Person1 {
constructor() {
this.name = 'alice';
action = () => {
console.log('action');
};
}
}
function Person2() {
this.name = 'alice;
const action = () => {
console.log('action');
};
}
1. Person 类(ES6 类语法)
class Person {
name = 1;
action() {
console.log('action');
}
}
特点:
- 类字段:
name = 1是通过类字段的形式定义的,它直接属于类的实例属性。每次实例化Person,实例都会有name属性,其初始值为1。 - 原型方法:
action()方法定义在Person类的原型上 (Person.prototype)。所有通过new Person()创建的实例共享这一个方法,这样节省内存。
行为:
- 创建实例时,
name会直接被绑定在实例对象上。 action()是挂载在Person.prototype上的,可以被所有实例共享。
let p1 = new Person();
console.log(p1.name); // 1
p1.action(); // 输出 'action'
console.log(p1.hasOwnProperty('action')); // false (说明 action 是原型方法)
console.log(Person.prototype.action); // [Function: action]
总结:
- 类字段定义的属性直接属于实例。
- 方法定义在原型上,所有实例共享。
2. Person1 类(构造函数内定义)
class Person1 {
constructor() {
this.name = 'alice';
action = () => {
console.log('action');
};
}
}
特点:
- 构造函数定义的属性:
this.name = 'alice'是在构造函数中定义的,属于实例对象的属性。每次实例化Person1时,都会在实例上创建该属性。 - 局部变量
action:action是一个箭头函数,但它在构造函数中没有通过this绑定,而是直接定义的一个局部变量,所以它只存在于构造函数的作用域中,外部无法访问。
行为:
this.name是实例属性。action是构造函数内部的局部变量,不是实例方法或原型方法,无法在实例上访问。
let p1 = new Person1();
console.log(p1.name); // 'alice'
p1.action(); // TypeError: p1.action is not a function (action 不是实例上的方法)
总结:
name是实例属性。action是构造函数的局部变量,外部不可访问。注意,这里你可能是想写this.action = () => {...}来让它成为实例的方法。
3. Person2 构造函数(传统函数语法)
function Person2() {
this.name = 'alice;
const action = () => {
console.log('action');
};
}
特点:
- 构造函数定义的属性:
this.name = alice是实例属性,类似于Person1中的name,直接绑定到实例对象上。 - 局部变量
a: 这里action是一个箭头函数,但它是通过const定义在构造函数的作用域内,所以它也是一个局部变量,外部无法访问。它不会成为实例或原型上的方法。
行为:
this.name是实例属性。action作为局部变量,外部不可访问。
let p2 = new Person2();
console.log(p2.name); // alice
p2.action(); // TypeError: p2.action is not a function (action 是局部函数,外部不可访问)
总结:
this.name是实例属性。action是局部变量,外部不可访问,同理可以使用this来让它成为实例的方法。
总体比较:
-
属性定义位置:
- 在
Person类中,属性name是类字段,定义在类体内部,直接绑定到实例对象上。 - 在
Person1和Person2中,name属性是通过构造函数中的this明确绑定到实例上的。
- 在
-
方法定义:
Person类的action()是原型方法,所有实例共享。Person1的action是局部变量,外部无法访问。Person2的action函数也是局部变量,外部无法访问。
-
作用域:
Person和Person1都是 ES6 类语法,Person2是传统的构造函数语法。Person1和Person2都有局部函数(action),它们只存在于构造函数的作用域中,而不是实例或原型链上的方法。
推荐用法:
在类中定义实例方法时,建议使用 this.action = () => {...} 或原型方法(如 action() {...}),而不要定义局部变量作为函数,这样可以确保方法对外部可用,实例可以正确调用这些方法。
Second problem: 我们改了一下Person1之后它和Person这个没用constructor的有什么区别
class Person {
name = 'alice';
action() {
console.log('action');
}
}
class Person1 {
constructor() {
this.name = 'alice';
this.action = () => {
console.log('action');
};
}
}
虽然它们看上去类似,都定义了 name 属性和 action 方法,但它们之间有一些关键的区别,主要体现在方法的定义方式以及其影响的行为上,尤其是在内存分配和**this 的绑定**方面。
1. Person 类 (方法定义在原型上)
class Person {
name = 'alice';
action() {
console.log('action');
}
}
特点:
- 实例属性:
name是类字段,直接属于实例。每次实例化Person时,name属性会被添加到实例对象上。 - 原型方法:
action()是定义在Person类的原型 (Person.prototype) 上的普通方法。所有实例共享同一个action()方法,不会为每个实例创建新的方法副本。
重要行为:
- 内存效率: 因为
action()是定义在原型上的,所有实例对象共享这个方法,不会为每个实例单独创建一个方法副本,这样节省了内存。 this绑定:action()方法在调用时,this指向取决于调用方式。通常,this指向调用方法的对象(即实例),但如果方法被单独调用或者通过其他方式调用,this的指向可能会发生变化。
let p = new Person();
p.action(); // 输出 'action'
- 由于
action()是定义在原型上的,所有Person实例都会共享同一个action方法。
2. Person1 类 (箭头函数作为方法)
class Person1 {
constructor() {
this.name = 'alice';
this.action = () => {
console.log('action');
};
}
}
特点:
- 实例属性:
name是通过构造函数直接定义在实例上的,每次实例化时,name会被绑定到实例对象上。 - 实例方法:
action()是通过箭头函数直接定义在实例对象上的。每次实例化Person1时,都会为每个实例创建一个新的箭头函数作为action方法。
重要行为:
- 内存使用: 每次实例化时,
action()都会在每个实例上创建一份新的方法副本,因此内存占用比原型方法要高。这意味着每个Person1实例都有自己独立的action函数,而不是共享的。 this绑定: 箭头函数没有自己的this,它会从定义时的词法作用域中继承this。在这种情况下,this始终指向当前实例对象,因此即使在回调或其他上下文中调用action(),this也不会改变。
let p1 = new Person1();
p1.action(); // 输出 'action'
- 由于
action是箭头函数,它的this永远指向定义时的实例,即p1。
区别总结:
-
action的定义位置:- 在
Person中,action()是一个普通的原型方法,定义在Person.prototype上,所有实例共享同一个方法。 - 在
Person1中,action()是一个箭头函数,直接绑定在每个实例上。每次实例化都会创建一个新的action方法。
- 在
-
内存占用:
Person中,action()是共享的原型方法,所以所有实例共享一个action()方法,节省内存。Person1中,action()是每个实例独立的,导致每个实例都有自己的一份副本,内存开销更大。
-
this的绑定:Person中,action()方法的this取决于调用方式。如果调用方法时改变了上下文(例如通过call或apply),this可能会指向其他对象。Person1中,action()是箭头函数,箭头函数的this绑定在定义时的词法作用域中,因此无论如何调用action(),this始终指向当前实例对象。
-
调用方式的影响:
- 在
Person中,如果把action()方法传递给其他函数或事件处理程序,this的指向可能会丢失,除非明确绑定。 - 在
Person1中,由于action()是箭头函数,this永远不会改变,因此它在任何上下文中都能正确引用实例。
- 在
什么时候使用哪种方式?
- 共享方法(如
Person的原型方法):如果你希望所有实例共享同一个方法,并且该方法不依赖于特定的this绑定,可以选择这种方式,节省内存。 - 实例独立方法(如
Person1的箭头函数):如果方法依赖于this并且可能在不同上下文中调用(如回调函数或事件处理),那么使用箭头函数会确保this始终指向正确的实例。但这种方式会增加内存开销,因为每个实例都有独立的函数副本。
Third problem: prototype 和 __proto__有什么区别
1、prototype
- 是什么:
prototype是一个函数对象特有的属性,它是构造函数的一个属性,指向该构造函数的原型对象。 - 作用:当通过构造函数
new创建对象时,生成的对象会继承构造函数的prototype属性中的所有方法和属性。
2、__proto__
-
是什么:
__proto__是所有对象(除了null)都有的一个内部属性,指向该对象的构造函数的prototype。它表示对象的隐式原型(即该对象从哪个原型继承)。 -
作用:
__proto__用来实现对象的原型链机制,通过它可以访问对象的原型,从而实现继承。当访问对象的属性时,如果对象自身没有该属性,JavaScript 引擎会通过__proto__去查找它的原型,直到找到或者返回undefined。
区别总结
-
prototype是函数(尤其是构造函数)独有的属性,它是定义实例共享方法和属性的地方。 -
__proto__是对象的内部属性,指向它的构造函数的prototype,用于实现继承机制。 -
prototype是一个构造函数的属性,只有函数才有。 -
__proto__是对象的属性,几乎所有对象都有。
关系
当我们用构造函数创建一个对象时,生成的对象的 __proto__ 会指向该构造函数的 prototype:
let john = new Person('John');
console.log(john.__proto__ === Person.prototype); // true
Fourth problem:[[Prototype]]和它俩的关系
-
概念:
[[Prototype]]是 JavaScript 对象的内部属性,表示对象的原型。每个对象都有一个[[Prototype]]属性,它指向另一个对象,这个对象就是它的原型。这个概念是规范中的名称,无法直接通过代码访问它。- 在现代 JavaScript 中,
[[Prototype]]的概念对应的是__proto__,尽管在实际编程中我们使用的是__proto__或Object.getPrototypeOf()来操作和查看这个内部属性。
-
作用:
[[Prototype]]代表对象的原型,它与__proto__表示的是同一个原型链的概念,区别在于[[Prototype]]是底层语言机制,而__proto__是该机制的实现方式之一。
解释:
__proto__是对[[Prototype]]的暴露,它允许我们在代码中查看和操作对象的原型链。- 而
[[Prototype]]是内部实现,不能直接通过代码访问它,但它的作用体现在原型链的运作上。
总结:[[Prototype]]就是__proto__
[[Prototype]] :这是对象的内部属性,是 JavaScript 语言规范中的名称,表示对象的原型。它不能被直接访问,但它的作用通过 __proto__ 或 Object.getPrototypeOf() 间接体现。
Last problem
于是乎我有了下面的总结
[[Prototype]]就是__proto__- 构造函数就是我们最初始创建的
Person prototype是构造函数才有的属性person1是Person的实例__proto__是实例才有的属性,它指向的就是构造函数的prototype
用GPT判断一下我的总结是否正确
[[Prototype]]就是__proto__✅ 基本正确。[[Prototype]]是 JavaScript 规范中的术语,用来表示对象的原型(Prototype)。__proto__是访问[[Prototype]]的方式之一,因此__proto__就是[[Prototype]]的实际体现。不过[[Prototype]]是内部属性,不能直接通过代码访问。- 构造函数就是我们最初始创建的
Person✅ 正确。 构造函数是用于创建实例对象的函数,例如function Person(),它就是用来创建Person类型对象的构造函数。 prototype是构造函数才有的属性 ✅ 正确。 只有构造函数(即函数对象)才拥有prototype属性,它指向一个原型对象。这个原型对象上定义的方法和属性,供通过该构造函数创建的实例共享使用。person1是Person的实例 ✅ 正确。 当你使用new Person()时,你创建了Person构造函数的一个实例对象,person1是这个实例。__proto__是实例才有的属性,它指向的就是构造函数的prototype✅ 正确,但稍微修改一下措辞。 是的,实例对象才有__proto__属性,它指向创建这个实例的构造函数的prototype对象。注意,__proto__并不是构造函数本身的属性,而是实例对象的属性。
结尾
我们对 JavaScript 原型链机制的理解已经非常清晰了!如果想要深入了解,推荐进一步研究原型链的查找机制,以及如何使用 Object.create()、Object.getPrototypeOf()、Object.setPrototypeOf() 这些函数来操作原型。
如果大家有任何疑问,欢迎随时在评论区提出,或者直接私聊我探讨交流!我非常愿意和大家一起讨论这些技术问题。如果我的解答有任何不准确的地方,也请大家及时指出,我会积极改正并完善内容。
你们的支持如同璀璨星光,照亮我前行的创作之路,让我更有信心和热情为大家带来更多优质的内容。

转载自:https://juejin.cn/post/7426954878680498210