关于原型/原型链,你想知道的都在这里 | 青训营笔记

173 阅读8分钟

关于原型/原型链,你想知道的都在这里

这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天

一、原型

1. 什么是原型?

原型的本质是一个对象,Javascript中每一个对象都会拥有原型对象(包括原型对象本身)。

对于函数而言:JavaScript会为每个函数额外创建一个对象,创建的这个对象就叫做函数的显式原。对于对象而言:每个对象都会拥有一个原型对象,称为隐式子原型。

因为函数也是对象,所以函数既有显式原型,也有隐式原型(下方会详细讲解)。

2. 原型的作用是什么?

因为Javascript中,可以使用 {} 或者 new Object() 创建对象,如果有多个对象都有功能一样或者差不多的方法,在每一个对象里面都写是不合适的,在其他语言(比如Java)中,我们一般都会使用 继承 ,而在ES6之前,想要实现继承的效果,Javascript都是使用原型链(下面会讲)实现的。

原型对象是一个公共的对象,其好处是可以让所有对象实例共享它所包含的属性和方法。

3. 原型对象的访问及其特点

Javascript中,一切都是对象,函数也是对象,而所有对象都有一个对象类型的属性 [[Prototype]] ,如下图。

image.png

从这个属性的名字就能知道,这个属性不简单。它是对象的内部属性,不能够直接访问,下图尝试了两几种方法直接访问,都访问失败。

image.png

那么我们应该如何访问呢?

对于原型对象的访问,有如下两种方式:

  1. 使用非标准属性 __proto__

Firefox和Chrome提供了 __proto__ 这个非标准属性(不是所有浏览器都支持)访问

image.png

  1. 使用ECMA引入的标准原型访问器 Object.getPrototypeOf(object )

image.png

4. 原型中的特殊属性 constructor

原型对象中的constructor属性是一个函数属性(属性值为函数),其值指向该原型对象所属的函数,也就是说,这个属性表示的是函数本身。

比如说:小君养了只猫,到哪儿都会带着这只猫,关系很好,这只猫脖子上有一根项链,项链上写了一行字,“主人:小君”。 在这里,小猫就相当于是原型对象,而小君则是一个函数,小猫是小君的原型对象,而小君是小猫的constructor。

因为函数可以作为构造函数创建对象,而原型中指向函数的属性又叫constructor(中文翻译:构造器),所以直接称原函数为原型的构造函数。

下图中,直接通过原型对象取出constructor,然后调用,证明constructor属性确实指向原函数。

image.png

5. 隐式原型和显式原型

前面提到,所有对象都拥有原型,但是其原型是不可直接访问的属性 [[Prototype]] ,这个即为 隐式原型(Chrome和Firefox可以用 __proto__ 访问, __proto__ 也表示隐式原型)。

对于函数而言,其本身也属于对象,但和普通对象不同的是,其拥有一个属性 prototype 这个属性指向一个对象,即 JavaScript为这个函数创建的原型对象。此时,称这个 prototype 的值为函数的 显示原型

对于对象而言,其不存在 prototype 属性(只有函数对象拥有),只有隐式原型 [[Prototype]]

注意:函数虽然拥有显式原型prototype属性,但是作为一个对象(函数也是对象),仍然拥有 [[Prototype]] 隐式属性,且二者不是同一个对象,对于隐式属性,其来源于原型链的“上方”。

image.png

6. 原型对象中的内容是什么?

对于函数而言,其显式原型中的内容很简单,除了构造函数和原型对象本身的原型之外,没有别的内容。

image.png

所以,此处讲解的原型内容指的都是隐式原型,即原型对象指向哪里。

(1)普通函数的原型指向。

首先,我们先了解以下Javascript中的 Function 对象。

image.png

上方是MDN文档描述,我们此处直接进行讲解,感兴趣可以看看文档,和咱们这篇文章相关的,下面都会提到,不看也行。

Function是一个构造函数,所有函数都是其实例,其显式原型和隐式原型指向同一个对象。

image.png

首先,我们新建一个函数,然后看看其原型。

image.png

通过上方的运行结果我们可以得出结论:

  • 普通函数的隐式原型 proto 和显式原型 prototype 不是 同一个对象。
  • Function对象的隐式原型和显式原型 同一个对象。
  • 普通函数的隐式原型指向Function的显式原型(或者说隐式原型)。

JavaScript中函数也是对象,那么函数有对象的性质,也有函数的性质

作为对象:拥有 proto 属性,对应的内容即其作为函数的 prototype 属性值

作为函数:拥有 prototype 属性,其 prototypeconstructor 对应 function(){}

每个函数都有自己的函数原型prototype,但是其作为对象,也会存在__proto__,这个__proto__指向的是Function的prototype,可以理解为声明的函数本质是通过new Function得到的。

(2)通过 new + 构造函数() 的方式创建的对象。

通过 new + 构造函数() 创建的对象,其原型指向构造函数的显式原型(prototype)。

image.png

注意:普通对象没有显式原型prototype属性,只有隐式属性[[Prototype]]。

执行过程:

let l = {}; // 初始化一个对象
l.__proto__ = Lucky.prototype // 将函数的原型赋值给对象原型
Lucky.call(l, 'LuoKing', 22); // 调用函数(构造函数)初始化对象

(3)使用字面量 {} 创建对象。

使用字面量创建对象时,其原型指向的是 Object 的显式原型(Object是一个构造函数)。

var July = { 
	name: "张三", 
	age: 28, 
	getInfo: function(){ 
		console.log(this.name + " is " + this.age + " years old"); 
		}
	} 
console.log(July.getInfo());

image.png

7. Object对象

Object对象和Function对象一样,本质上是一个构造函数,因为Javascript中函数也是对象,故Object和Function也都是对象,既然Object是函数对象,那么其肯定拥有 prototype 属性,如下图。

image.png

Object的原型的原型是null(Object的原型是最顶层的原型<终点>)

image.png

因为Object本身是一个构造函数,所以其隐式原型和所有普通函数一样,指向的是Function的显式原型。

二、原型原型链

1. 属性(包含方法)查找

当查找一个属性时,会向上遍历原型链,当对象实例中不存在该属性时,就会顺着原型链向上寻找,找不到则为 undefined

注意:原型链寻找是优先显式原型寻找( prototype > __proto__),对于函数而言,原型链是顺着 prototype,而对于对象而言,没有 prototype属性,所以使用 __proto__

2. 原型链示例

image.png

上图中,青色部分表示构造函数,橙色部分表示构造函数对应的原型对象。

3. 通过原型链实现继承

既然提到通过原型链能够实现继承,现在我们做一个实例。

定义一个Animal类,其有两个子类,Cat、Dog。 Animal拥有属性 weight和行为 eat。 Cat拥有特殊的属性name和行为run。 Dog拥有特殊的属性color和drink。

定义父类

function Animal(weight){
	this.weight = weight;
}
Animal.prototype.eat = function(){
	console.log('animal is eating');
}

子类实现继承(Cat)

// 子类构造函数
function Cat(weight){
    // 通过绑定this指向的方式,将父类构造函数中的this指向为子类实现属性的继承
    Animal.call(this, weight);
    this.name = 'Tom';

}
// 手动指定子类的原型为一个父类对象,这一步是为了利用原型链实现对父类方法的继承
Cat.prototype = new Animal();
// 由于原型是我们自己指定的(覆盖了原有的),而指定的对象的constructor是指向Animal,我们这里需要改成正确的子类的构造函数
Cat.prototype.constructor = Cat;
// 定义子类专有的方法
Cat.prototype.run = function(){
    console.log(this.name + ' is running');
}
var cat = new Cat(88);
// 调用父类方法
cat.eat(); // animal is eating
// 调用子类专有方法
cat.run(); // Tom is running
// 获取父类属性
console.log(cat.weight); // 88

子类实现多态继承(Cat)

// 子类构造函数
function Dog(weight, color) {
    // 通过绑定this指向的方式,将父类构造函数中的this指向为子类实现属性的继承
    Animal.call(this, weight);
    this.color = color;

}
// 手动指定子类的原型为一个父类对象
Dog.prototype = new Animal();
// 由于原型是我们自己指定的(覆盖了原有的),而指定的对象的constructor是指向Animal,我们这里需要改成正确的子类的构造函数
Dog.prototype.constructor = Dog;
// 指定子类专有的方法
Dog.prototype.drink = function () {
    console.log('a dog which color is ' + this.color + ' and weight is ' + this.weight + ' is drinking');
}
// 重写父类方法
Dog.prototype.eat = function () {
    console.log('dog is eating');
}
var dog = new Dog(88, 'yellow');
// 调用父类方法
dog.eat(); // dog is eating
// 调用子类专有方法
dog.drink(); // a dog which color is yellow and weight is 88 is drinking
// 获取父类属性
console.log(dog.weight); // 88

以Dog为例,当dog调用 eat方法时,因为Dog本身的原型中没有这个函数,所有Javascript会从Dog的 原型的原型 里面去找,不断再向上找,最终在我们指定的Dog的 原型的原型里面找到了 eat 方法。

Cat内容

image.png

Dog内容

image.png