深入理解:带着问题看原型和原型链带着问题找答案之 什么是构造函数? 什么是实例? 什么是 `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