搞懂__proto__与prototype

2,040 阅读5分钟

前言

对一个知识点是否完全把握,最好的检验方式就是能否用自己的语言将其表述出来。

原型与原型链一直是学习JS绕不过的知识点,其中__proto__与prototype最为让人头疼,这里简单的写下自己的理解,从原型与原型链中拆解__proto__与prototype,希望能对大家有所帮助。

一,原型

1,定义

在JavaScript中,函数可以有属性。每个函数都有一个特殊的属性叫做原型(prototype)

**原型(prototype)**是什么东西呢,里面又有哪些属性呢?来,让我们拿个具体例子看下:

function fn () {};
console.dir(fn);

这里我们可以看出**原型(prototype)**是一个对象,对于当前例子来说,里面有constructor与__proto__两个属性。那这个原型对象有什么作用呢?来,让我们继续往下看~

2,使用

现在我们知道了原型是一个对象,那原型对象有什么作用呢?实际上,原型是ECMAScript实现集成的过程中产生的一个概念。这里我们简单拿ES5的对象来举例子:

function Person (name) {
    this.name = name;
}

Person.prototype.say = function () {
    console.log(`My name is ${this.name}`);
}

let person = new Person('小明');
person.say(); // My name is 小明

让我们来逐步解释下这个例子: 1. 声明了一个构造函数Person, 其中一个属性name 2. 在其原型上声明了一个函数say 3. 实例一个Person类 --- person 4. 调用person的say方法 我们发现,person可以调用其构造函数原型里的say方法,why?让我们看下person里都有什么:

实例person虽然自身没有say方法,但是通过__proto__属性访问到了其原型中的say方法。

为什么__proto__属性会指向其构造函数的原型prototype呢,他们之间是什么关系呢?让我们继续往下看~

二,原型链

1,__proto__

在介绍原型链之前,让我们先看一个属性:__proto__,这是一个与原型prototype很相似的属性,这次让我们来彻底搞懂他们之间的关系。

让我们来看MDN上的定义:

Object.prototype的__proto__属性是一个访问器属性(一个getter函数和一个setter函数),暴露了通过它访问的对象的内部[[prototype]](一个对象或null)

注:函数也是一个对象,所以其同时具有prototype与__proto__两个属性

看不懂定义没关系,让我们举例说明:

let obj = {a: 1};
console.log(obj);

我们可以看出__proto__指向了一个对象,这个对象是什么呢?来,让我们继续看

class Parent {
    constructor (name) {
        this.name = name;
    }
    print () {
        console.log(this.name);
    }
}

let parent = new Parent('小明');

console.dir(parent);
console.dir(Parent);

咦,有没有发现parent.__proto__ 与 Parent.prototype所指向的对象很相似,它们是不是同一个对象呢?

parent.__proto__ === Parent.prototype; // true

结果是同一个引用,这个时候我们可以得出一个结论:实例的__proto__属性指向其结构函数的原型。那他们之间的这种关联关系有什么作用?这便涉及到了原型链。

2,原型链

Javascript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

注:因为对象没有prototype属性,所以通过__proto__属性与原型prototype进行关联

文字描述很抽象,让我们通过ES5的继承来具体分析:

function Parent (name) {
    this.name = name;
}

Parent.prototype.print = function () {
    console.log(this.name);
}

function Child (name) {
    Parent.call(this, name);
}

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

let child = new Child('小明');
console.log(child);

让我们来看下child里面都有什么

这里可能有的同学对Object.create()这个函数不太熟悉,不明白其做了什么,不要急,让我们来看下其源码实现:

function create(proto) {
    function F();
    F.prototype = proto;
    return new F();
}

声明了一个构造函数F,然后将其原型指向参数proto,返回构造函数的实例。好,让我们将整个过程串起来看一下:

1, 声明Parent构造函数
2, 声明Child构造函数,并手动绑定this指向
3, 执行Object.create(Parent.prototype): 声明一个构造函数F,更改其原型prototype指向(F.prototype = Parent.prototype),然后返回F的实例f,注意这一步,实际上是f.__proto__ === F.prototype
4, 将Child的prototype指向f.prototype
5, 绑定Child的构造函数

让我们来看一下child.print()的调用过程

1, child对象里没有print函数,于是便在其原型上寻找:child.__proto__ --> f.prototype
2, 进入f.prototype中寻找print函数,发现没有,于是去其原型上寻找:f.__proto__ --> F.prototype
3, F.prototype == Parent.prototype, 于是便进入Parent.prototype中寻找print函数,有print函数,调用成功

怎么样,是不是豁然开朗!原型链其实就是通过__proto__与prototype的关联关系连接起来的,这样对象便可以寻找其原型上的方法与属性。详细的关系描述如下图:

实战

让我们来看一道面试题:

var F = function () {};
Object.prototype.a = function () {
    console.log('a');
};

Function.prototype.b = function () {
    console.log('b');
};

var f = new F();

f.a();
f.b();

F.a();
F.b();

解题思路如下:

1. f.a() --> 实例f调用a方法,自身没有,从其原型中查找:f.__proto__ == F.prototype
2. F.prototype中没有a方法,于是继续在其原型中查找,F.prototype.__proto__ == Object.prototype
3. Obejct.prototype中有a方法,无b方法,所以f.a()结果为a,f.b()调用会报错:f.b is not a function
4. F.a(): 构造函数调用a方法。自身没有,从其原型中查找: F.__proto__ == Function.prototype
5. Function.prototype中有b方法,所以F.b()的输出为b。没有a方法,继续在其原型中查找,Function.prototype.__proto__ == Object.prototype
6. Object.prototype中有a方法,所以F.a()的输出为a

三,总结

本文对__proto__与prototype之间的关系进行了简单的梳理,写在了笔者自己的理解,给大家理解提供一个思路,当然可能存在描述不准确或错误的地方,欢迎大家留言交流。

本文为转载,转载原因是想把自己看到的,对自己有帮助的博文整理到一起。帮助自己有效的学习。

原文地址为:segmentfault.com/a/119000002…