大白话理解JavaScript原型和继承

188 阅读7分钟

前言

prototype 对我来说一直都是比较难理解的东西,它给我的感觉就像凭空冒出来的一样,又是面试出现频率很高的问题,每次都是死记硬背式的去理解,结果根本没学会什么,一段时间之后又会忘记。

就像我一开始看到这种图,是有点晕的:

这篇我想分享下我学习原型的过程和收获,尽量使用简单的大白话,争取让每个看到这篇文章的新手能最快的理解

原型 & 原型链

prototype

prototype: object that provides shared properties for other objects

从这句话可以知道

  1. prototype 它就是一个对象!(指向原型)
  2. 它用于连接两个对象之间的属性,实行共享。

你可以把 prototype 比作婴儿身上的脐带, 连接了母亲和孩子,孩子可以汲取母亲的营养长大, balabala....

talk is simple, see my code:

// 创建 Child 构造函数
function Child() {

}
// prototype是函数才会有的属性
Child.prototype.name = 'fuck';
var child = new Child();
console.log(child.name) // fuck

现在把以前的理解全部扔掉,我们就认为prototype是一个对象,把Child.prototype = { ... }就看成是obj.a = { .... }, 而 prototype 这个对象它是指向了构造函数而创建的实例的原型。也就是child的原型. 也就是说 Childchild 就是孩子, 母亲就是 原型(Child.prototype 指向的对象), 通过prototype 以及 下面说的 __proto__ 连接了它们的原型。

那么~ 到底什么是原型。你可以这样理解:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性。

__proto__

Every object has an implicit reference (called the object's prototype)

就是说 __proto__ 是一个隐式引用, 它也是指向了对象的原型,看下面的 code:

function Child() {}
var child = new Child();
console.log(child.__proto__ === Child.prototype); // true

可能这个还不能完全搞懂隐式的意思, 那我们先看下这张图

我们创建obj对象,但展开之后发现多了一个 __proto__ 属性,这意味着 obj 被隐式地挂载了另一个对象的引用,置于 __proto__ 属性中。

也就是说,所谓的隐式,是指不是由开发者(你和我)亲自创建/操作。

注意:

  1. 可以通过 Object.getPrototypeOf(obj) 间接访问指定对象的 prototype 对象。
console.log(Object.getPrototypeOf(child) === Child.prototype) // true
  1. 通过 Object.setPrototypeOf(obj, anotherObj) 间接设置指定对象的 prototype 对象。

constructor

每个原型都有一个 constructor 属性指向关联的构造函数。

function Person() {
    
}
console.log(Person === Person.prototype.constructor); // true

这里有一个东西需要注意:

function Person() {
    
}
var person = new Person()
console.log(person.constructor === Person); // true

为什么是 true 呢? 其实就是因为 person 的隐式引用, 如下图

person.constructor -> person.__proto__.constructor -> Person.prototype.constructor === Person

到这里,应该就对最上面那张图的最上面一部分理解了

原型链

a prototype may have a non-null implicit reference to its prototype, and so on; this is called the prototype chain.

就是说, 一个原型对它的原型可能都有一个隐式的引用,就是说 prototype对象也有自己的prototype, 再简单了说就像是个族谱,我要找你的祖宗十八代,需要从你爸爸开始,你爸爸也有爸爸,爸爸的爸爸也有爸爸, balabala..., 最后找到了你的祖宗十八代。

说人话其实就是: 当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。下面看个例子:

function Person() {

}
Person.prototype.name = 'Kevin';

var person = new Person();

person.name = 'Daisy';
console.log(person.name) // Daisy

delete person.name;
console.log(person.name) // Kevin 当读取实例的属性找不到 name,  就去 Person.prototype 中找
  1. 这个时候就会问了,如果在原型里还找不到,那原型的原型又是什么?其实我们打印出person就可以知道

我们最开始讲了, prototype就是个对象,既然是对象,那它就是通过 Object 构造函数生成的,结合之前所讲,实例的 __proto__ 指向构造函数的 prototype, 所以

person.__proto__.__proto__ === Object.prototype // true
  1. 而问题又来了, 我怎么知道我找的是祖宗十八代呢,不是使其代或者十九代。

看到上面的简介,non-null, 表示我找到原型为null之后停止

Object.prototype.__proto__ === null

这一路查找的路径所连成的线,就是原型链。

继承

Object.create & new

在学习如何继承之前,我们要先了解两个概念

Object.create()

var obj ={a: 1}
var b = Object.create(obj)
console.log(obj.a) // 1
console.log(b.a) // 1
b.a = 2
console.log(obj.a) // 1

从上面的例子就可以看出Object.create解决了对象的拷贝问题,它创建一个新对象,使用现有的对象来提供新创建的对象的__proto__, 其实内部就是

function create(o) {
    let F = function () {}
    F.prototype = o
    return new F()
}

new

new 一直面试问的很多的问题,它具体做了什么呢?

  1. 创建了一个全新的对象。
  2. 这个对象会被执行[[Prototype]](也就是__proto__)链接, 链接到这个函数的prototype对象上。
  3. 生成的新对象会绑定到函数调用的this
  4. 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。

转化成代码就是

function create() {
  // 1. 创建空对象
  let obj = {}
  // 截取数组,并获取第一个参数
  let Con = [].shift.call(arguments)
  // 2. 链接到原型 
  obj.__proto__ = Con.prototype
  // 3. 绑定 this
  let result = Con.apply(obj, arguments)
  // 4. 返回
  return result instanceof Object ? result : obj
}

那我们现在借用 Object.create 的特性,可以更改为:

function create() {
  let Con = [].shift.call(arguments)
  let obj = Object.create(Con.prototype)
  let result = Con.apply(obj, arguments)
  return result instanceof Object ? result : obj
}

为什么要说 new 呢?因为在我的理解中,new 方法其实就像是一种继承。

继承历程

首先说下这两种方法

  1. Object.setPropertyOf,给我两个对象,我把其中一个设置为另一个的原型。
  2. Object.create,给我一个对象,它将作为我创建的新对象的原型。

如何使用,上面都有介绍,不在赘述。

下面介绍下我们日常工作中,实现继承的一路历程。首先我们创建一个构造函数

function SuperPerson (phone) {
    this.name = 'xiatian'
    this.age = 24
    this.hobby = ['lady', 'female', 'woman', 'girl']
    this.phone = phone
}
SuperPerson.prototype.getName = function() {
    return this.name
}

类式继承

// 声明子类
function SubPerson (phone) {
    this.sex = 'male'
}
// 继承父类
SubPerson.prototype = new SuperPerson()

类式继承的原理就是将子类的原型指向父类的实例,这样子类的prototype就可以访问到new Person()__proto__, 从而访问到它的属性和方法。

但是,这样的继承是有两个问题的:

  1. 如果父类的属性里有引用类型,那么就会在子类的所有实例里共用
var instance1 = new SubPerson()
var instance2 = new SubPerson()
console.log(instance2.hobby) // ['lady', 'female', 'woman', 'girl']
instance1.hobby.push('man')
console.log(instance2.hobby) // ['lady', 'female', 'woman', 'girl', 'man']
  1. 如果子类有参数,则无法传递给父类,实现初始化。如此🌰,phone无法传递给父类
function SuperPerson (phone) {
    this.name = 'xiatian'
    this.age = 24
    this.hobby = ['lady', 'female', 'woman', 'girl']
    this.phone = phone
}

// 声明子类
function SubPerson (phone) {
    this.sex = 'male'
}
// phone 无法传递
SubPerson.prototype = new SuperPerson()

构造函数继承

为了解决类式继承的问题,使用了构造函数式继承

function SuperPerson (phone) {
    this.name = 'xiatian'
    this.age = 24
    this.hobby = ['lady', 'female', 'woman', 'girl']
    this.phone = phone
}

// 声明子类
function SubPerson (phone) {
    this.sex = 'male'
    // 重点!!
    // call 绑定this, 并把 phone 传递
    SuperPerson.call(this, phone)
}

但这也存在问题,prototype 没有涉及也就无法继承,除非把他们都写在构造函数中,但这样每个实例都只此一份,无法共享

组合继承

顾名思义,就是组合了类式继承和构造函数式继承

function SuperPerson (phone) {
    this.name = 'xiatian'
    this.age = 24
    this.hobby = ['lady', 'female', 'woman', 'girl']
    this.phone = phone
}
SuperPerson.prototype.getName = function() {
    return this.name
}

// 声明子类
function SubPerson (phone) {
    this.sex = 'male'
    SuperPerson.call(this, phone)
}
// 类式继承
SubPerson.prototype = new SuperPerson()

var instance1 = new SubPerson()
var instance2 = new SubPerson()

instance1.hobby.push('man')
console.log(instance1.hobby) // ['lady', 'female', 'woman', 'girl', 'man']
console.log(instance2.hobby) // ['lady', 'female', 'woman', 'girl']

但即使这样,还是存在问题的,它调用了两次父类的构造函数,导致父类构造函数中的属性重复的两次, 如图所示

寄生式继承

为了消除组合继承的问题,我们只需要继承父类的prototype, 这就要用到上面说的Object.create()

SubPerson.prototype = Object.create(SuperPerson.prototype)

// 这样也是有个问题,SubPerson.prototype.constructor !== SubPerson, 因为对照Object.create的内部代码

function create(o = 'SuperPerson.prototype') {
    let F = function () {}
    F.prototype = o ('SuperPerson.prototype')
    return new F() ('SubPerson.prototype')
}
// 所以 
// SubPerson.prototype.constructor 
// = F.prototype.constructor
// = SuperPerson.prototype.constructor
// = SuperPerson

所以我们要改造一下

function inherit(subClass, superClass) {
    var p = Object.create(superClass.prototype)
    p.constructor = subClass
    subClass.prototype = p
}

寄生式组合继承

到这里就很简单了, 跟上面的一起连起来

function SuperPerson (phone) {
    this.name = 'xiatian'
    this.age = 24
    this.hobby = ['lady', 'female', 'woman', 'girl']
    this.phone = phone
}
SuperPerson.prototype.getName = function() {
    return this.name
}

// 声明子类
function SubPerson (phone) {
    this.sex = 'male'
    SuperPerson.call(this, phone)
}
// 寄生式继承
inherit(SubPerson, SuperPerson)

// 添加子类 prototype
SubPerson.prototype.getSex = function() {
    return this.sex
}

var instance = new SubPerson('13776020154')
instance.getName() // 'xiatian'
instance.phone // '13776020154'
instance.getSex() // 'male'