原型、原型链详解

1,534 阅读7分钟

认识原型

认识对象的原型

JavaScript当中每个对象都有一个特殊的内置属性 [[prototype]],这个特殊的对象可以指向另外一个对象。

那么这个对象有什么用呢?

  • 当我们通过引用对象的属性key来获取一个value时,它会触发 [[Get]]的操作;
  • 这个操作会首先检查该属性是否有对应的属性,如果有的话就使用它;
  • 如果对象中没有改属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性;
  • 这个 [[prototype]] 我们通常会将其称之为隐式原型;

那么如果通过字面量直接创建一个对象,这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢?

  • 答案是有的,只要是对象都会有这样的一个内置属性;
  • 获取的方式有两种:
    • 方式一:通过对象的 __proto__ 属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题);
    • 方式二:通过 Object.getPrototypeOf 方法可以获取到;
var obj = {}  
// 方式一: __proto__(有浏览器兼容问题)  
console.log(obj.__proto__)  
  
// 方式二: Object.getPrototypeOf  
console.log(Object.getPrototypeOf(obj))

那么我们就可以进行如下的测试了:

// 定义一个obj对象  
var obj = {}  
  
// 直接给对象添加address属性  
// obj.address = "北京市"  
  
// 直接给隐式原型上添加address属性  
// 给__proto__上添加address属性  
obj.__proto__.address = "齐齐哈尔市"  
  
// 通过Object.setPrototypeOf来设置隐式原型  
Object.setPrototypeOf(obj, { address"上海市"name"setPrototypeOf" })  
  
console.log(obj.address)

认识函数的原型

 函数的prototype

那么我们知道上面的东西对于我们的构造函数创建对象来说有什么用呢?

  • 它的意义是非常重大的,接下来我们继续来探讨;

这里我们又要引入一个新的概念:所有的函数都有一个prototype的属性:

function foo() {  
  
}  
  
// 所有的函数都有一个属性, 名字是 prototype   
console.log(foo.prototype)

我们前面讲过new关键字的步骤如下:

  • 1.在内存中创建一个新的对象(空对象);
  • 2.这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性;(后面详细讲);
  • 3.构造函数内部的this,会指向创建出来的新对象;
  • 4.执行函数的内部代码(函数体代码);
  • 5.如果构造函数没有返回非空对象,则返回创建出来的新对象;

我们将重心放到步骤一和二中:

  • 在内存中创建一个对象;
  • 将对象的[[prototype]]属性赋值为该构造函数的prototype属性;

那么也就意味着:

function Person() {  
  
}  
var p1 = new Person()  
  
// 上面的操作相当于会进行如下的操作:  
p = {}  
p.__proto__ = Person.prototype

那么也就意味着我们通过Person构造函数创建出来的所有对象的[[prototype]]属性都指向Person.prototype:

function Person() {  
  
}  
  
var p1 = new Person()  
var p2 = new Person()  
var p3 = new Person()  
  
console.log(p1.__proto__ === p2.__proto__)  
console.log(p1.__proto__ === Person.prototype)

创建对象的内存

d0cc1f2a0aab7f57b44f85571739d8c.jpg

prototype属性

如果我们在函数的prototype中添加属性,那么创建的对象是否可以访问到呢?

function Person() {  
  
}  
  
Person.prototype.name = "word"  
Person.prototype.age = 22  
  
var p1 = new Person()  
var p2 = new Person()  
  
console.log(p1.name, p1.age)  
console.log(p2.name, p2.age)

constructor属性

事实上原型对象上面是有一个属性的:constructor

  • 默认情况下原型上都会添加一个属性叫做constructor,这个constructor指向当前的函数对象;
console.log(Person.prototype.constructor// [Function: Person]  
console.log(p1.__proto__.constructor// [Function: Person]  
console.log(p1.__proto__.constructor.name// Person

重写原型对象

如果我们需要在原型上添加过多的属性,通常我们会重新整个原型对象:

function Person() {  
  
}  
  
Person.prototype = {  
  name"word",  
  age22,  
  eatingfunction() {  
    console.log(this.name + "在跳舞~")  
  }  
}

前面我们说过, 每创建一个函数, 就会同时创建它的prototype对象, 这个对象也会自动获取constructor属性;

  • 而我们这里相当于给prototype重新赋值了一个对象, 那么这个新对象的constructor属性, 会指向Object构造函数, 而不是Person构造函数了
console.log(Person.prototype.constructor// [Function: Object]  
// 为什么是Object呢? 因为对象的字面量是由Object函数产生的  
var obj = {}  
console.log(obj.constructor// // [Function: Object]

如果希望constructor指向Person,那么可以手动添加:

Person.prototype = {  
  constructorPerson,  
  name"word",  
  age22,  
  eatingfunction() {  
    console.log(this.name + "在跳舞~")  
  }  
}

上面的方式虽然可以, 但是也会造成constructor的[[Enumerable]]特性被设置了true.

  • 默认情况下, 原生的constructor属性是不可枚举的.
  • 如果希望解决这个问题, 就可以使用我们前面介绍的Object.defineProperty()函数了.
Person.prototype = {  
  name"word",  
  age22,  
  eatingfunction() {  
    console.log(this.name + "在跳舞~")  
  }  
}  
  
Object.defineProperty(Person.prototype"constructor", {  
  enumerablefalse,  
  valuePerson  
})

组合构造函数和原型

我们在上一个构造函数的方式创建对象时,有一个弊端:会创建出重复的函数,比如running、eating这些函数

那么有没有办法让所有的对象去共享这些函数呢?

  • 可以,将这些函数放到Person.prototype的对象上即可;
function Person(name, age, height, address) {  
  this.name = name  
  this.age = age  
  this.height = height  
  this.address = address  
}  
  
Person.prototype.eating = function() {  
  console.log(this.name + "在跳舞~")  
}  
  
Person.prototype.running = function() {  
  console.log(this.name + "在打球~")  
}  
  
  
var p1 = new Person("word"221.67"北京市")  
var p2 = new Person("bier"231.78"上海市")  
  
p1.eating()  
p2.running()

类、原型链

JS中的类和对象

当我们编写如下代码的时候,我们会如何来称呼这个Person呢?

  • 在JS中Person应该被称之为是一个构造函数;
  • 从很多面向对象语言过来的开发者,也习惯称之为类,因为类可以帮助我们创建出来对象p1、p2;
  • 如果从面向对象的编程范式角度来看,Person确实是可以称之为类的;
function Person() {  
  
}  
  
var p1 = new Person()  
var p2 = new Person()

为什么需要继承

面向对象有三大特性:封装、继承、多态

  • 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程;
  • 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中);
  • 多态:不同的对象在执行时表现出不同的形态;

那么这里我们核心讲继承。

比如下面的这段代码,如果我们不使用继承,那么会存在大量的重复代码:

function Student(name, age, sno) {  
  this.name = name  
  this.age = age  
  this.sno = sno  
}  
  
Student.prototype.eating = function() {  
  console.log(this.name + "在吃饭~")  
}  
  
Student.prototype.running = function() {  
  console.log(this.name + "在跑步~")  
}  
  
Student.prototype.studying = function() {  
  console.log(this.name + "在学习~")  
}  
  
function Teacher(name, age, title) {  
  this.name = name  
  this.age = age  
  this.title = title  
}  
  
Teacher.prototype.eating = function() {  
  console.log(this.name + "在吃饭~")  
}  
  
Teacher.prototype.running = function() {  
  console.log(this.name + "在跑步~")  
}  
  
Teacher.prototype.teaching = function() {  
  console.log(this.name + "上课~")  
}

那么继承是做什么呢?继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可。

那么JavaScript当中如何实现继承呢?

  • 不着急,我们先来看一下JavaScript原型链的机制;
  • 再利用原型链的机制实现一下继承;

JavaScript原型链

在真正实现继承之前,我们先来理解一个非常重要的概念:原型链。

我们知道,从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取:

var obj = {  
  name"word",  
  age22  
}  
  
obj.__proto__ = {  
  address"北京市"  
}  
  
console.log(obj.address)

但是如果obj的原型上也没有对应的address属性呢?必然还是获取不到的。

那么如果我们配置的原型对象上,继续配置原型呢?

var obj = {  
  name"word",  
  age22  
}  
  
obj.__proto__ = {  
}  
  
obj.__proto__.__proto__ = {  
  
}  
  
obj.__proto__.__proto__.__proto__ = {  
  address"北京市"  
}  
  
console.log(obj.address)

Object的原型

那么什么地方是原型链的尽头呢?比如第三个对象是否也是有原型__proto__属性呢?

console.log(obj.__proto__.__proto__.__proto__.__proto__// [Object: null prototype] {}

我们会发现它打印的是 [Object: null prototype] {}

  • 事实上这个原型就是我们最顶层的原型了

我们来研究一下默认字面量的原型是什么:

var obj = { name"word" }  
console.log(obj.__proto__// [Object: null prototype] {}  
  
var obj1 = new Object()  
console.log(obj1.__proto__// [Object: null prototype] {}  
console.log(Object.prototype// [Object: null prototype] {}  
  
console.log(obj.__proto__ === Object.prototype// true  
console.log(obj1.__proto__ === Object.prototype// true

我们可以知道,从Object直接创建出来的对象的原型都是 [Object: null prototype] {}

那么我们可能会问题: [Object: null prototype] {} 原型有什么特殊吗?

  • 特殊一:该对象不再继续有原型属性了,也就是已经是顶层原型了;
  • 特殊二:该对象上有很多默认的属性和方法;

6458ef02bf499d675f52a2ee6763d5b.jpg 那么我们回到刚才创建的原型链中,它们最终也会找到Object的prototype的:

image.png