likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 47

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

总体比较:

  1. 属性定义位置

    • Person 类中,属性 name 是类字段,定义在类体内部,直接绑定到实例对象上。
    • Person1Person2 中,name属性是通过构造函数中的 this 明确绑定到实例上的。
  2. 方法定义

    • Person 类的 action() 是原型方法,所有实例共享。
    • Person1action 是局部变量,外部无法访问。
    • Person2action 函数也是局部变量,外部无法访问。
  3. 作用域

    • PersonPerson1 都是 ES6 类语法,Person2 是传统的构造函数语法。
    • Person1Person2 都有局部函数(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

区别总结

  1. action 的定义位置

    • Person 中,action() 是一个普通的原型方法,定义在 Person.prototype 上,所有实例共享同一个方法。
    • Person1 中,action() 是一个箭头函数,直接绑定在每个实例上。每次实例化都会创建一个新的 action 方法。
  2. 内存占用

    • Person 中,action() 是共享的原型方法,所以所有实例共享一个 action() 方法,节省内存。
    • Person1 中,action() 是每个实例独立的,导致每个实例都有自己的一份副本,内存开销更大。
  3. this 的绑定

    • Person 中,action() 方法的 this 取决于调用方式。如果调用方法时改变了上下文(例如通过 callapply),this 可能会指向其他对象。
    • Person1 中,action() 是箭头函数,箭头函数的 this 绑定在定义时的词法作用域中,因此无论如何调用 action()this 始终指向当前实例对象。
  4. 调用方式的影响

    • 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

于是乎我有了下面的总结

  1. [[Prototype]] 就是 __proto__
  2. 构造函数就是我们最初始创建的 Person
  3. prototype 是构造函数才有的属性
  4. person1Person 的实例
  5. __proto__ 是实例才有的属性,它指向的就是构造函数的 prototype

用GPT判断一下我的总结是否正确

  1. [[Prototype]] 就是 __proto__基本正确[[Prototype]] 是 JavaScript 规范中的术语,用来表示对象的原型(Prototype)。__proto__ 是访问 [[Prototype]] 的方式之一,因此 __proto__ 就是 [[Prototype]] 的实际体现。不过 [[Prototype]] 是内部属性,不能直接通过代码访问。
  2. 构造函数就是我们最初始创建的 Person正确。 构造函数是用于创建实例对象的函数,例如 function Person(),它就是用来创建 Person 类型对象的构造函数。
  3. prototype 是构造函数才有的属性正确。 只有构造函数(即函数对象)才拥有 prototype 属性,它指向一个原型对象。这个原型对象上定义的方法和属性,供通过该构造函数创建的实例共享使用。
  4. person1Person 的实例正确。 当你使用 new Person() 时,你创建了 Person 构造函数的一个实例对象,person1 是这个实例。
  5. __proto__ 是实例才有的属性,它指向的就是构造函数的 prototype正确,但稍微修改一下措辞。 是的,实例对象才有 __proto__ 属性,它指向创建这个实例的构造函数的 prototype 对象。注意,__proto__ 并不是构造函数本身的属性,而是实例对象的属性。

结尾

我们对 JavaScript 原型链机制的理解已经非常清晰了!如果想要深入了解,推荐进一步研究原型链的查找机制,以及如何使用 Object.create()Object.getPrototypeOf()Object.setPrototypeOf() 这些函数来操作原型。


如果大家有任何疑问,欢迎随时在评论区提出,或者直接私聊我探讨交流!我非常愿意和大家一起讨论这些技术问题。如果我的解答有任何不准确的地方,也请大家及时指出,我会积极改正并完善内容。

你们的支持如同璀璨星光,照亮我前行的创作之路,让我更有信心和热情为大家带来更多优质的内容。

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

转载自:https://juejin.cn/post/7426954878680498210
评论
请登录