javascript实现继承

254 阅读6分钟

这是我参与8月更文挑战的第30天,活动详情查看:8月更文挑战

JavaScript 在编程语言界是个异类,它和其他编程语言很不一样,JavaScript 可以在运行的时候动态地改变某个变量的类型。

比如你永远也没法想到像isTimeout这样一个变量可以存在多少种类型,除了布尔值true和false,它还可能是undefined、1和0、一个时间戳,甚至一个对象。

又或者你的代码跑异常了,打开浏览器开始断点,发现InfoList这个变量第一次被赋值的时候是个数组[{name: 'test1', value: '11'}, {name: 'test2', value: '22'}],过了一会竟然变成了一个对象{test1:'11', test2: '22'}

除了变量可以在运行时被赋值为任何类型以外,JavaScript 中也能实现继承,但它不像 Java、C++、C# 这些编程语言一样基于类来实现继承,而是基于原型进行继承。

这是因为 JavaScript 中有个特殊的存在:对象。每个对象还都拥有一个原型对象,并可以从中继承方法和属性。

提到对象和原型,你曾经是否有过这些疑惑:

  • JavaScript 的函数怎么也是个对象?

  • __proto__和prototype到底是啥关系?

  • JavaScript 中对象是怎么实现继承的?

  • JavaScript 是怎么访问对象的方法和属性的?

下面我们一起结合问题,来探讨下 JavaScript 对象和继承。

  • 原型对象和对象是什么关系 在 JavaScript 中,对象由一组或多组的属性和值组成:
{
  key1: value1,
  key2: value2,
  key3: value3,
}

在 JavaScript 中,对象的用途很是广泛,因为它的值既可以是原始类型(number、string、boolean、null、undefined、bigint和symbol),还可以是对象和函数。

不管是对象,还是函数和数组,它们都是Object的实例,也就是说在 JavaScript 中,除了原始类型以外,其余都是对象。

这也就解答了疑惑 1:JavaScript 的函数怎么也是个对象?

在 JavaScript 中,函数也是一种特殊的对象,它同样拥有属性和值。所有的函数会有一个特别的属性prototype,该属性的值是一个对象,这个对象便是我们常说的“原型对象”。

我们可以在控制台打印一下这个属性:

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

打印结果显示为:

image.png

可以看到,该原型对象有两个属性:constructor和__proto__。

到这里,我们仿佛看到疑惑 “2:__proto__和prototype到底是啥关系?”的答案要出现了。在 JavaScript 中,__proto__属性指向对象的原型对象,对于函数来说,它的原型对象便是prototype。函数的原型对象prototype有以下特点:

默认情况下,所有函数的原型对象(prototype)都拥有constructor属性,该属性指向与之关联的构造函数,在这里构造函数便是Person函数;

Person函数的原型对象(prototype)同样拥有自己的原型对象,用__proto__属性表示。前面说过,函数是Object的实例,因此Person.prototype的原型对象为Object.prototype。

在 JavaScript 中,__proto__属性指向对象的原型对象;

对于函数来说,每个函数都有一个prototype属性,该属性为该函数的原型对象。

这是否就是疑惑 2 的完整答案呢?并不全是,在 JavaScript 中还可以通过prototype和__proto__实现继承。

  • 使用 prototype 和 proto 实现继承 前面我们说过,对象之所以使用广泛,是因为对象的属性值可以为任意类型。因此,属性的值同样可以为另外一个对象,这意味着 JavaScript 可以这么做:通过将对象 A 的__proto__属性赋值为对象 B,即A.proto = B,此时使用A.__proto__便可以访问 B 的属性和方法。

这样,JavaScript 可以在两个对象之间创建一个关联,使得一个对象可以访问另一个对象的属性和方法,从而实现了继承,此时疑惑 “3. JavaScript 中对象是怎么实现继承的?”解答完毕。

那么,JavaScript 又是怎样使用prototype和__proto__实现继承的呢?

继续以Person为例,当我们使用new Person()创建对象时,JavaScript 就会创建构造函数Person的实例,比如这里我们创建了一个叫“Lily”的Person:

var lily = new Person("Lily");

上述这段代码在运行时,JavaScript 引擎通过将Person的原型对象prototype赋值给实例对象lily的__proto__属性,实现了lily对Person的继承,即执行了以下代码:

// 实际上 JavaScript 引擎执行了以下代码
var lily = {};
lily.__proto__ = Person.prototype;
Person.call(lily, "Lily");

可以看到,lily作为Person的实例对象,它的__proto__指向了Person的原型对象,即Person.prototype。

每个函数的原型对象(Person.prototype)都拥有constructor属性,指向该原型对象的构造函数(Person);

使用构造函数(new Person())可以创建对象,创建的对象称为实例对象(lily);

实例对象通过将__proto__属性指向构造函数的原型对象(Person.prototype),实现了该原型对象的继承。

那么现在,关于疑惑 2 中__proto__和prototype的关系,我们可以得到这样的答案:

每个对象都有__proto__属性来标识自己所继承的原型对象,但只有函数才有prototype属性;

对于函数来说,每个函数都有一个prototype属性,该属性为该函数的原型对象;

通过将实例对象的__proto__属性赋值为其构造函数的原型对象prototype,JavaScript 可以使用构造函数创建对象的方式,来实现继承。

现在我们知道,一个对象可通过__proto__访问原型对象上的属性和方法,而该原型同样也可通过__proto__访问它的原型对象,这样我们就在实例和原型之间构造了一条原型链。这里我用红色的线将lily实例的原型链标了出来。

下面一起来进行疑惑 4 “JavaScript 是怎么访问对象的方法和属性的?”的解答:在 JavaScript 中,是通过遍历原型链的方式,来访问对象的方法和属性。

通过原型链访问对象的方法和属性 当 JavaScript 试图访问一个对象的属性时,会基于原型链进行查找。查找的过程是这样的:

首先会优先在该对象上搜寻。如果找不到,还会依次层层向上搜索该对象的原型对象、该对象的原型对象的原型对象等(套娃告警);

JavaScript 中的所有对象都来自Object,Object.prototype.proto === null。null没有原型,并作为这个原型链中的最后一个环节;

JavaScript 会遍历访问对象的整个原型链,如果最终依然找不到,此时会认为该对象的属性值为undefined。