详细解析ES6中原型和原型链

609 阅读13分钟

前言

JavaScript是一门基于原型的语言,在软件设计模式中,有一种模式叫做原型模式,JavaScript正是利用这种模式而被创建出来。

原型模式是用于创建重复的对象,同时又能保证性能,这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。原型模式的目的是用原型实例指定创建对象的种类,利用已有的一个原型对象,可以快速地生成和原型对象一样的新对象实例。

一个可以被复制(或者叫克隆)的一个类,通过复制原型可以创建一个一模一样的新对象,也可以说原型就是一个模板,在设计语言中更准确的说是一个对象模板。

JavaScript的原型是为了实现对象间的联系,解决构造函数无法数据共享而引入的一个属性,而原型链是一个实现对象间联系即继承的主要方法。

构造函数

构造函数和普通函数本质上没什么区别,只不过使用了new关键字创建对象的函数,被叫做了构造函数。构造函数的首字母一般是大写,用以区分普通函数,当然不大写也不会有什么错误。
1、原型定义了一些公用的属性和方法,利用原型创建出来的新对象实例会共享原型的所有属性和方法。

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.species = '人类';
    this.say = function () {
        console.log("Hello");
    }
}
let per1 = new Person('xiaoming', 20);
// 创建原型
var Person = function(name) {
    this.name = name;
};
// 原型的方法
Person.prototype.sayHello = function() {
    console.log(this.name+",hello");
};
// 实例化创建新的原型对象,新的原型对象会共享原型的属性和方法
var person1 = new Person("zhangsan");
var person2 = new Person("lisi");
// zhangsan,hello
person1.sayHello();
// lisi,hello
person2.sayHello();

2、通过原型创建的新对象实例是相互独立的,为新对象实例添加的方法只有该实例拥有这个方法,其它实例是没有这个方法的

// 创建原型
var Person = function(name){
    this.name = name;
};
// 原型的方法
Person.prototype.sayHello = function() {
    console.log(this.name+",hello");
};
// 实例化创建新的原型对象,新的原型对象会共享原型的属性和方法
var person1 = new Person("zhangsan");
var person2 = new Person("lisi");
// zhangsan,hello
person1.sayHello();
// lisi,hello
person2.sayHello();
// 为新对象实例添加方法
// 通过原型创建的新对象实例是相互独立的
person1.getName = function() {
    console.log(this.name);
}
// zhangsan
person1.getName();
// Uncaught TypeError: person2.getName is not a function
person2.getName();

原型(对象)

1、在js中,每一个函数类型的数据,都有一个叫做prototype的属性,这个属性指向的是一个普通的对象,就是所谓的原型对象。 函数才有prototype属性,prototype是一个对象,指向了当前构造函数的原型地址。 5.png 2、对于原型对象来说,它有个constructor属性,指向它的构造函数。 6.png 那么这个原型对象有什么用呢?最主要的作用就是用来存放实例对象的公有属性和公有方法。 在上面那个例子里species属性和say方法对于所有实例来说都一样,放在构造函数里,那每创建一个实例,就会重复创建一次相同的属性和方法,显得有些浪费。这时候,如果把这些公有的属性和方法放在原型对象里共享,就会好很多。

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

Person.prototype.species = '人类';
Person.prototype.say = function () {
    console.log("Hello");
}

let per1 = new Person('xiaoming', 20);
let per2 = new Person('xiaohong', 19);

console.log(per1.species); // 人类 
console.log(per2.species); // 人类

per1.say(); // Hello
per2.say(); // Hello

可是这里的species属性和say方法不是实例对象自己的,为什么可以直接用点运算符访问?这是因为在js中,对象如果在自己的这里找不到对应的属性或者方法,就会查看构造函数的原型对象,如果上面有这个属性或方法,就会返回属性值或调用方法。所以有时候,我们会用per1.constructor查看对象的构造函数。

console.log(per1.constructor); // Person()

这个constructor是原型对象的属性,在这里能被实例对象使用,原因就是上面所说的。那如果原型对象上也没有找到想要的属性呢?这就要说到原型链了。

原型链

说原型链之前,先来了解两个概念:

1. 显式原型

显示原型就是利用prototype属性查找原型,只是这个是函数类型数据的属性。

2. 隐式原型

隐式原型是利用__proto__属性查找原型,这个属性指向当前对象的构造函数的原型对象,这个属性是对象类型数据的属性,所以可以在实例对象上面使用。所有对象都有__proto__属性, 当用构造函数实例化(new)一个对象时,会将新对象的__proto__属性指向 构造函数的prototype

console.log(per1.__proto__ === Person.prototype); // true
console.log(per2.__proto__ === Person.prototype); // true

根据上面,就可以得出constructor、prototype和__proto__之间的关系了: 7.png 所有引用类型(对象)都有一个__proto__(隐式原型)属性,属性值是一个普通的对象 综上可得:

1、所有引用类型(函数,数组,对象)都拥有__proto__属性(隐式原型) 。

2、所有函数除了有_proto_属性之外还拥有prototype属性(显式原型)。

3、原型对象:每创建一个函数,该函数会自动带有一个prototype属性,该属性是一个指针,指向了一个对象,我们称之为原型对象。

3、原型链

既然这个是对象类型的属性,而原型对象也是对象,那么原型对象就也有这个属性,但是原型对象的__proto__又是指向哪呢?

我们来分析一下,既然原型对象也是对象,那我们只要找到对象的构造函数就能知道__proto__的指向了。而js中,对象的构造函数就是Object(),所以对象的原型对象,就是Object.prototype。既然原型对象也是对象,那原型对象的原型对象,就也是Object.prototype。不过Object.prototype这个比较特殊,它没有上一层的原型对象,或者说是它的__proto__指向的是null。

所以上面的关系图可以拓展成下面这种: 8.png 到这里,就可以回答前面那个问题了,如果某个对象查找属性,自己和原型对象上都没有,那就会继续往原型对象的原型对象上去找,这个例子里就是Object.prototype,这里就是查找的终点站了,在这里找不到,就没有更上一层了(null里面啥也没有),直接返回undefined。

可以看出,整个查找过程都是顺着__proto__属性,一步一步往上查找,形成了像链条一样的结构,这个结构,就是原型链。所以,原型链也叫作隐式原型链。

正是因为这个原因,我们在创建对象、数组、函数等等数据的时候,都自带一些属性和方法,这些属性和方法是在它们的原型上面保存着,所以它们自创建起就可以直接使用那些属性和方法。

属性读取规则

解释一:对象之间通过原型关联到一起,就好比用一条锁链将一个个对象链接在一起,这样就形成了一条原型链。在读取对象的属性时,会先在对象中查找自有属性 如果不存在,再沿着原型链向上搜索继承属性,直到到达原型链的顶端,才停止搜索。

解释二:原型链是原型对象创建过程的历史记录,当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会去它的__proto__隐式原型上查找,即它的构造函数的prototype,如果还没有找到就会再在构造函数的prototype的__proto__中查找,这样一层一层向上查找就会形成一个链式结构。

解释三:当查找一个对象的属性时,JavaScript 会根据原型链向上遍历对象的原型,直到找到给定名称的属性为止,直到到达原型链的顶部仍然没有找到指定的属性,就会返回undefined。

原型链总结:

1、构造函数就是普通函数 只不过通过new来调用

2、所有的函数都有prototype 这个属性指向一个对象 就是我们所说的原型。

3、所有的原型对象就是普通的对象,这个对象有一个constructor属性,这个属性值指向构造函数。

4、所有的对象都有__proto__属性,当对象通过构造函数实例化时,这个属性指向构造函数的原型。

5、原型对象也是对象 他也有__proto__属性。

6、对象A有__proto__属性,这个属性的值也是也个对象B,对象B依然是一个对象,所以对象B也有__proto__属性,他的值是对象C .......... 如此下去就串成了一条链路。这条链路就是原型链。

7、这条链路是有终点的 ,直到object 的原型对象 Object.prototype ,他的原型是null。原型链的终点是null倒数,第二层是object的原型。

8、基于原型链有一套读取对象属性(obj.a)的规则,规则就是访问对象属性时,会现在对象本身进行查找,如果找不到,就会沿着这条原型链去查找,直到找到为止。如果找到了就直接返回,如果没找到就返回undefined。

9、解决了JS构造函数无法实现数据共享的问题。 9.png

继承的方式

继承的根本是原型链,通过修改构造函数的原型进而实现数据共享,也就是继承。达到子类共享父类属性和方法的目的。

借用构造函数

function Person(name,age) { 
    this.name = name;
    this.age = age;
}
Person.prototype.country = 'china'
Person.prototype.say = function() { 
    console.log('hello,my name is',this.name)
}

const per_1 = new Person('张杰', 20)
const per_2 = new Person('苏醒', 18)
per_1.id = '87368129812739812'
console.log('000000',per_1.country, per_2.country)
per_1.say();
per_2.say();

function Students(name, age, className) { 
    Person.call(this, name, age); // 继承的一种实现方式 // 借用构造函数实现 强行绑定了this 
    this.className = className
}
Students.prototype.study = function () { 
    console.log('我在学习')
}
const stu = new Students('章三', 15, '一年级')
console.log(stu)
stu.study()
// console.log(stu.country) // undefined
// stu.say() // 报错   
// 缺点:这种方式会出现的问题是 我们没有办法继承父类原型上的属性

10.png 优点:避免实例之间共享一个原型实例,能向父类构造方法传参。

缺点:继承不到父类原型上的属性和方法。

利用原型对象

// 利用原型对象去继承父类
// 通过让子类的原型指向父类的实例.并让子类的原型的constructor属性指向子类构造函数  达到继承
// 父类
function Person(name,age) { 
    this.name = name;
    this.age = age;
}
Person.prototype.country = 'china'
Person.prototype.say = function () { 
    console.log('hello,my name is',this.name)
}
function Students(name,age, className) { 
   Person.call(this, name, age); // 借用构造函数
    this.className = className 
}

// 通过让子类的原型指向父类的实例.并让子类的原型的constructor属性指向子类构造函数  达到继承
Students.prototype = new Person(); // 原型对象继承
Students.prototype.constructor = Students;

// 再次给子类原型上添加方法时 需要保证原型链已经做了修改 也即是上边两行代码
Students.prototype.study = function () { 
    console.log('我在学习')
}

const stu = new Students('一年级')
console.log(stu)
stu.study()
console.log(stu.country) // china
stu.say() 
// 优点:
// 可以继承父类中所有的实例和方法
// 缺点:
// 没有办法动态的给父类的构造函数传参数 也就是没有实现super的功能

11.png 优点:

通过原型链继承的方式,原先存在父类型的实例中的所有属性和方法,现在也能存在于子类型的原型中了。

缺点:

1.由于所有子类实例原型都指向同一个父类实例, 因此对某个子类实例的父类引用类型变量修改会影响所有的子类实例。

2.在创建子类实例时无法向父类构造传参, 即没有实现super()的功能。

组合继承

组合继承实际上就是借用构造函数和原型对象的结合体。

// 利用原型对象去继承父类
// 通过让子类的原型指向父类的实例.并让子类的原型的constructor属性指向子类构造函数  达到继承
// 父类
function Person(name,age) { 
    this.name = name;
    this.age = age;
}
Person.prototype.country = 'china'
Person.prototype.say = function () { 
    console.log('hello,my name is',this.name)
}
function Students(className) { 
    this.className = className
}
// 通过让子类的原型指向父类的实例.并让子类的原型的constructor属性指向子类构造函数  达到继承
Students.prototype = new Person('张三',25);
Students.prototype.constructor = Students;

// 再次给子类原型上添加方法时 需要保证原型链已经做了修改 也即是上边两行代码
Students.prototype.study = function () { 
    console.log('我在学习')
}

const stu = new Students('一年级')
console.log(stu)
stu.study()
console.log(stu.country) // china
stu.say() 

缺点:每次创建子类实例都执行了两次构造函数(Father.call()和new Father())。

优点:组合继承拥有上面两种方法的优点。同时还能避免上面两种方法的缺点。

原型式继承

基于已有的对象(原型对象)创建新对象(实现Object.create())

function object(o) {
    function F() {}
    //F.prototype={name:'ccdida',friends:['shelly','Bob']}
    F.prototype=o
    // new F() 
    //F是个构造函数,返回F的实例:1.this此时用不上 2.将实例的__proto__指向F.prototype.
    //即返回了一个实例,其__proto__指向{name:'ccdida',friends:['shelly','Bob']}
    return new F()
}
var person = {
    name:'ccdida',
    friends:['shelly','Bob']
}
var person1=object(person)
var person2=object(person)
//object函数相当于实现了Object.Create的功能
console.log(person1.__proto__===person) //true 
person2.friends.push('shlimy')
console.log(person1.friends)// ["shelly", "Bob", "shlimy"]
// 基于已经有的对象创建新的对象
// 希望已有的对象可以作为新对象的原型 进而实现数据共享
const obj = {
    country: 'china',
    language: 'chinese',
    guoqi: 'star',
    sing() { 
        console.log('起来 不愿做奴隶的人们!')
    }
}
function object(obj, { name }) { 
    function Fun() { 
        this.name = name
    }
    Fun.prototype = obj;
    obj.constructor = Fun; // 可有可无
    return new Fun()
}
var per1 = object(obj, {name:'zhangjie'});
var per2 = object(obj,{name:'xiena'});
console.log(per1.__proto__ === obj, per2.__proto__ === obj)
console.log(per1.country, per2.language, per1.guoqi)
console.log(per1.name, per2.name)
per2.sing()

12.png Object.create()方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)。

因此上边代码也可以改写成:

    name:'ccdida',
    friends:['shelly','Bob']
  }
  var person1=Object.create(person)
  var person2=Object.create(person)

  console.log(person1.__proto__===person) //true 
  person2.friends.push('shlimy')
  person2.name = '12'
  console.log(person1.name,person2.name)
  console.log(person1.friends)// ["shelly", "Bob", "shlimy"]

缺点:引用类型值会共享,值类型不会共享,因为在改变值类型时,相当于给自己添加了属性。当去修改引用类型的某个值时,是在修改__proto__中的对象。但如果直接给引用类型赋值,那也和值类型一样,是给自己增加了属性

寄生式继承

创建一个用于封装继承过程的函数(实现Object.create()),同时以某种方式增强对象(比如添加方法)

var person = {
    name:'ccdida',
    friends:['shelly','Bob']
}
function createAnother(original) {
    //clone.__proto__===original
    var clone = Object.create(original)
    //增强对象,添加属于自己的方法
    clone.sayHi = function() {
      console.log('hi')
    }
    return clone
}
var person1 = createAnother(person)
var person2 = createAnother(person)
person1.friends.push('shmily')
console.log(person2.friends)//["shelly", "Bob","shmily"]
person1.sayHi() //hi
  

缺点:1、不能做到函数复用,2、引用类型数据依然共享

组合寄生式继承

定义:所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。

不需要为了指定子类型的原型而调用超类型的构造函数(就是不需要显示的new操作),通过上面的寄生式继承方式来继承超类型的原型即可,我们所需要的是超类型原型的一个副本。

// 寄生组合继承:这个过程既实现了继承,又没有去调用Super
function inheritPrototype(Sub,Super) {
    //subPrototype.__proto__=Super.prototype
    var subPrototype=Object.create(Super.prototype)
    // 修改原型链
    //subPrototype.constructor = Sub
    subPrototype.constructor = Sub
    //相当于subPrototype有__proto__和constructor两个属性
    //即:
    //Sub.prototype.__proto__===Super.prototype
    //Sub.prototype.constructor = Sub
    Sub.prototype=subPrototype  
  }
function Super(name) {
    this.name=name
}
Super.prototype.sayHi=function() {
    console.log(this.name)//ccdida
}
function Sub(name) {
    Super.call(this,name)
}
inheritPrototype(Sub,Super)

Sub.prototype.sayHello=function() {
    console.log('sayHello')
}
var instance1=new Sub('ccdida')
// instance1.sayHi()
console.log(instance1.__proto__)
console.log(instance1.__proto__.__proto__)

实例通过Super.call(this,name)拿到Super中的属性(这些属性属于实例本身,不会被共享)。

子类通过Object.create,让子类的原型对象的隐式原型(proto)指向父类的原型对象,完成方法的继承(可复用)。