首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

深入理解:带着问题看原型和原型链

前言

学而时习之,不亦说乎;

吾日三省吾身;

学而不思则罔,思而不学则殆;

今天还是研究Promsie的一天,然后就在脑中响起了关于原型和原型链的知识。。越想越觉得这些概念像浆糊一样,变得混乱不清。

什么是构造函数? 什么是实例? 什么是 prototype? 什么是 __proto__? [[Prototype]] 又是什么?

大致都懂,又不太清晰,不太确定,于是我决定请出 GPT-4 来解答我的疑问。并带大家一同梳理回顾其中的所有知识点。Let's go

First problem:先来三段代码看看区别

JS 代码解读复制代码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 类语法)

js 代码解读复制代码class Person {

   name = 1;

   action() {

       console.log('action');

  }

}特点:

类字段: name = 1 是通过类字段的形式定义的,它直接属于类的实例属性。每次实例化 Person,实例都会有 name 属性,其初始值为 1。

原型方法: action() 方法定义在 Person 类的原型上 (Person.prototype)。所有通过 new Person() 创建的实例共享这一个方法,这样节省内存。

行为:

创建实例时,name 会直接被绑定在实例对象上。

action() 是挂载在 Person.prototype 上的,可以被所有实例共享。

js 代码解读复制代码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 类(构造函数内定义)

js 代码解读复制代码class Person1 {

   constructor() {

       this.name = 'alice';

       action = () => {

           console.log('action');

      };

  }

}特点:

构造函数定义的属性: this.name = 'alice' 是在构造函数中定义的,属于实例对象的属性。每次实例化 Person1 时,都会在实例上创建该属性。

局部变量 action: action 是一个箭头函数,但它在构造函数中没有通过 this 绑定,而是直接定义的一个局部变量,所以它只存在于构造函数的作用域中,外部无法访问。

行为:

this.name 是实例属性。

action 是构造函数内部的局部变量,不是实例方法或原型方法,无法在实例上访问。

js 代码解读复制代码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 构造函数(传统函数语法)

js 代码解读复制代码function Person2() {

   this.name = 'alice;

   const action = () => {

       console.log('action');

  };

}特点:

构造函数定义的属性: this.name = alice 是实例属性,类似于 Person1 中的 name,直接绑定到实例对象上。

局部变量 a: 这里 action 是一个箭头函数,但它是通过 const 定义在构造函数的作用域内,所以它也是一个局部变量,外部无法访问。它不会成为实例或原型上的方法。

行为:

this.name 是实例属性。

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的有什么区别

JS 代码解读复制代码class Person {

   name = 'alice';

   action() {

       console.log('action');

  }

}

class Person1 {

   constructor() {

       this.name = 'alice';

       this.action = () => {

           console.log('action');

      };

  }

}

虽然它们看上去类似,都定义了 name 属性和 action 方法,但它们之间有一些关键的区别,主要体现在方法的定义方式以及其影响的行为上,尤其是在内存分配this 的绑定方面。

1. Person 类 (方法定义在原型上)

js 代码解读复制代码class Person {

   name = 'alice';

   action() {

       console.log('action');

  }

}特点:

实例属性: name 是类字段,直接属于实例。每次实例化 Person 时,name 属性会被添加到实例对象上。

原型方法: action() 是定义在 Person 类的原型 (Person.prototype) 上的普通方法。所有实例共享同一个 action() 方法,不会为每个实例创建新的方法副本。

重要行为:

内存效率: 因为 action() 是定义在原型上的,所有实例对象共享这个方法,不会为每个实例单独创建一个方法副本,这样节省了内存。

this 绑定: action() 方法在调用时,this 指向取决于调用方式。通常,this 指向调用方法的对象(即实例),但如果方法被单独调用或者通过其他方式调用,this 的指向可能会发生变化。

js 代码解读复制代码let p = new Person();

p.action(); // 输出 'action'

由于 action() 是定义在原型上的,所有 Person 实例都会共享同一个 action 方法。

2. Person1 类 (箭头函数作为方法)

js 代码解读复制代码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 也不会改变。

js 代码解读复制代码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:

js 代码解读复制代码let john = new Person('John');

console.log(john.__proto__ === Person.prototype); // trueFourth 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://page.om.qq.com/page/O38gOw0PEJF7lGXeXEMY07aw0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券