💡JS-初识原型

217 阅读7分钟

前言

这篇文章,我们有三个目标:

  1. 原型存在,是为了解决什么问题
  2. 对象属性查找的过程
  3. 对象,原型对象 和构造函数 的三者之间的关系

在文章的阅读过程中,心中装着这三个问题,读起来才有目标,才更有效率

我们先看一个代码:

const person = {};

console.log(person.toString()); // [object Object]

person 是一个空对象,其中没有任何属性,但是在下面又调用toString属性,toString 属性从哪里来的?

肯定不是来自 person对象的。

来自哪里呢?是来自 person 的原型对象的!


在下面图中,在控制台打印出了 person 对象的结构:

可以看到里面一个属性也没有,但是有一个[[Prototype]]属性,这个属性指向一个对象,我们打开这个属性看看:

这里正好有一个 toString。那代码中的person.toString()是不是来自这里呢?

要解释这个问题,得知道什么是原型对象

原型

概念: 在 Javascript 中,原型对象是用于实现继承和共享属性的机制。每个 Javascript 对象都有一个原型对象,通过原型链可以访问和继承原型对象上的属性和方法

这里回答了文章开头第一个问题

上面有两个关键地方:

  1. 原型对象是用于继承和属性共享的
  2. 每个 JavaScript 对象都有一个原型对象

继承和属性的共享

继承有什么意义呢,继承是为了实现属性的复用。在python(面向对象)中,实现对象继承有什么方法?

用class:

class Animal:
    def __init__(self, name):
        self.name = name

    def get_name(self):
        return self.type

class Dog(Animal):
    def __init__(self):
        super().__init__(name)

dog = Dog('Dog');
print(dog.get_name()); // dog

而js是用什么实现继承呢,用原型。

下面来看一段代码:

const person = {
    name: 'zenos'
};

person.toString(); // [object, Object]

在上面的person对象中,并没有toString属性,为什么可以访问呢?是因为 person 通过原型继承了另一个对象

深入原型

原型是 JavaScript 中实现继承的一种机制。每个对象都有一个内部属性 [[Prototype]],指向它的原型对象。

通过 [[Prototype]] 属性,一个对象可以访问其原型及其原型对象的属性和方法。在对象中查找属性的时候,会从当前对象查找,如果当前对象没有,就会从当前对象的原型对象中查找。

上面的 person 空对象中没有 toString 属性,但 person 能够调用这个方法,就是因为 person 对象的原型对象上有 toString。

你看,每个创建出来的对象,并不用手动添加 toString 属性,但是都可以使用 toString,这不就是继承吗?

[[prototype]]属性指向的是对象的原型对象,但在实际代码中,这样写是会报错的。

如果真的在代码中写person[[prototype]],表示什么意思?

在js 实际开发中,是通过__proto__属性来访问原型对象的。

我们来看一段代码:

const person = {
    name: "zenos",
};

person.__proto__.otherName = 'blue';

上面的代码给 person 的原型对象添加了一个otherName的属性,看看控制台:

原型对象上确实多了otherName的属性。

可以像普通属性一样正常访问:

复杂的例子

下面的例子通过__proto__对 person 对象的原型对象进行覆盖

const person = {
    name: "zenos",
};

person.greet = function() {
    console.log("person");
};

// 覆盖person的原型
person.__proto__ = {
    greet: function(){
        console.log("person.__proto__");
    },
    greetInner: function () {
        console.log("person.__proto__");
    },
};

// 覆盖person的原型的原型
person.__proto__.__proto__ = {
    greetInner: function () {
        console.log("person.__proto__.__proto__");
    },
};

person.greet();
person.greetInner();
console.log(person.toString())
person.greetInner2();

在控制台看看 person 的结构,然后回答下面的问题:

  1. 代码中的person.greet()调用结果是什么?

因为 greet 方法是直接定义在 person 对象上的,因此调用 person.greet() 时,会执行 person 对象自身的 greet 方法,而不会去查找原型链。

  1. 代码中的person.greetInner()调用结果是什么?

因为greetInner 方法没有在 person 对象上直接定义,因此 JavaScript 会沿着原型链查找。person.__proto__ 上定义了 greetInner 方法,所以调用 person.greetInner() 时,会执行 person.__proto__ 上的 greetInner 方法。

  1. person.toString()会正常执行吗?

会!

因为 person.proto.proto 的原型对象上有toString属性

  1. person.greetInner2()会正常执行吗?

会报错!

因为 person 及其原型对象,原型的原型,原型的原型都没有greetInner2属性,那么person.greetInner2得到的是undefined,对undefined执行调用操作,即undefined(),会报错!

小结

从上面的代码中,可以得出几个结论:

  1. 在对象中查找属性的时候,会从当前对象查找,如果当前对象没有,就会从当前对象的原型对象中查找。
  2. 如果对象及其原型对象有相同的属性,优先用当前对象的属性。

如果一直没有找到属性,会一直查下去吗?

不会!当查找到[[prototype]]的值不再是对象,而是 null 的时候,就停止查找,然后返回undefined

上面就是对象属性查找的规则。

这里回答了文章开头的第二个问题

我们会发现查找的过程中,会涉及到多个原型对象,而原型对象之间是由__proto__([[prototype]])链接来的,我们把这个链条,叫做原型链

回顾person 的结构:

图形版的原型链:

画板

有没有醍醐灌顶的感觉呢😄

深入原型,实例对象和函数的关系

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

const obj = new Fn('zenos');

console.log(obj); // { name: 'zenos'}

Fn 是一个构造函数,通过 Fn 创建了一个对象 obj。很简单,我们把 obj 称为构造函数 Fn 的实例对象

构造函数有一个属性prototype,这个属性指向一个对象 A,并且对象 A 会成为构造函数实例对象的原型对象

也就是说 obj 的原型对象是对象 A。用代码表示:

console.log(obj.__proto__ === Fn.prototype); // true

概念辨析

  1. __proto__[[prototype]]prototype这三个属性,有什么关系?

__proto__对应着对象的原型对象,是每个对象都会有的属性。查找属性的时候,如果没有查找到,就会去原型对象上找。__proto__[[prototype]]是同一个东西,只不过__proto__是代码开发的表示方式,[[prototype]]是控制台打印时表示的方式

prototype 属性只存在于函数中,这个属性指向的对象会成为对应实例对象原型对象

  1. 构造函数和普通函数有什么区别?

没什么区别,当普通函数被 new 关键词调用的时候,就称为构造函数。注意箭头函数不能被作为构造函数,即不能被 new 操作。


并且**对象 A **上有一个constructor的属性,这个属性指向构造函数本身

console.log(Fn.prototype.constructor === Fn); // true

有点乱?没关系,我们画个图,就很清晰了

画板

这幅图回答了文章开头的第三个问题

这是 obj 在控制台的打印结构:

偷偷告诉你一个小秘密,对象 A 虽然是 obj 的原型对象,但对象 A 也是对象啊,所以它也有自己的原型对象🤭

console.log(Fn.prototype.__proto__); // ? 试试打印的结果

既然对象 A 会成为 Fn 实例对象的原型对象,那我直接修改对象 A,不就相当于直接修改了 Fn 实例对象的原型对象么

Fn.prototype.hello = function(){ 
  console.log('hello');
}

const obj2 = new Fn('zenos2');

console.log(obj2.name);
obj2.hello();

上面代码执行结果如下:

符合你的预期吗?😼

看看 obj2 的结构:

有了构造函数,我们可以真正地使用继承了!

初试继承

function Animal(name, age) {
        this.name = name;
        this.age = age;
}

Animal.prototype.getName = function () {
    return this.name;
};

Animal.prototype.getAge = function () {
    return this.age;
};

const animal = new Animal('tom',2);

上面有个构造函数 Animal,创建了一个实例对象 animal:

现在我想创建一个 Dog 对象,Dog 的实例对象也有 nameage 两个属性,原型对象上也有 getName,getAge 两个方法,怎么做?

function Dog(name, age) {
    this.name = name;
    this.age = age;
}

Dog.prototype.getName = function () {
    return this.name;
};

Dog.prototype.getAge = function () {
    return this.age;
};

这是最简单的做法,其实 Dog 可以继承 Animal:

function Dog(name, age) {
  // 给 dog 实例对象添加name,age属性
    Animal.call(this, name, age);
}

// 让Dog的实例对象和Animal实例对象的原型对象一致
Dog.prototype = Animal.prototype;

看看效果:

const dog = new Dog('dogName',3);

打印看看 dog 的结构:

该有的属性都有了。这就是继承 Animal 的方式。

当然这种方式比较简陋,其中还有一些小问题,不过没关系,这些小问题我们以后都会解决。这里只需要对继承有一个了解即可

总结

这篇讲了原型存在的目的、对象属性寻找逻辑,还通过原型,我们看到了构造函数和对象的关系。不难,下篇文章我们深入了解原型链