JS-图解构造函数、原型、实例对象-看不懂来捶我🤣

326 阅读9分钟

面向对象系统中的继承方法

  1. 在Java语言中,有一套完整面向对象系统。类与类之间是通过extends关键词实现继承的。继承之后的效果是子类可以之间可以共享、覆盖父类的变量或方法。如果要使用方法,或者访问变量,就需要去实例化一个类,然后再去执行相关的动作。

  2. 在JavaScript中,对象与对象之间也有这样的继承关系,例如普通对象和Object之间的关系,就是子与父的关系。子对象之间共享Object的方法,像toString, getOwnProtopertyNames, getOwnProtopertySymbols,isPrototypeOf等属性。子对象本身并没有定义这样属性,但能够使用这些方法,是因为父对象Object定义了这些方法。

  3. OK,那Javascript是以什么样的方式来实现对象之间的继承关系呢?很遗憾我们并没有像Java这样的继承方式,但是我们有更简洁的继承方式,这就是原型继承

我们要注意,Java是基于类的编程。而在JavaScript中,更提倡程序员去关注一系列对象实例的行为,而后才去关系如何将这些对象按照使用方式的相似性来分类,而不是分成一个个的“类”

不要见到这么多文字就头疼啊,我的文字还是很好懂的😁

什么是原型

简单的来说,原型就是两点:

  1. 任何对象都有私有字段[[prototype]], 其指向的就是对象的原型
  2. 在对象上面查找属性的时候,没有查到,就会去原型对象上面找,直到原型对象为空,或者找到为止。

使用原型来实现下继承吧

我们先创建一个函数,用来被继承。并且在函数prototype上面挂上一个方法

function Animal(name){
  this.name = name;
}

Animal.prototype.logName = function(){
  console.log('i am Animal: '+ this.name);
}

var animal = new Animal('Cat');

animal.logName(); //i am Animal: Cat

通过Animal函数来创建一个animal对象,然后用让animal对象打印出自己的名字

prototype和私有[[prototype]],是两个不同字段哦

我们再来创建一个函数,用来继承上面创建Animal

function Tiger(name){
  this.name = name;
}

Tiger.prototype = new Animal();

var tiger = new Tiger('tiger');

tiger.logName(); //i am Animal: tiger

首先改变Tiger的prototype,让其指向一个Animal实例对象。

然后通过Tiger函数实例化一个tiger对象,然后用让tiger对象打印出自己的名字。我们知道,tiger并没有定义自己的logName方法,显然这个方法是从Animal中继承过来

注意我的单词大小哦,大写开头的一般是构造函数,小写开头的一般是实例对象

是不是有点懵

没关系,我们来一步一步拆解上面的代码

1. 构造函数,实例对象,对象的原型,这三者之间是什么关系

上面的例子中,Animal函数new了一个animal对象出来。这个过程,Animal函数就是充当构造函数,而animal对象就是实例对象。在Animal.prototype上面挂了一个函数,animal对象竟然可以访问到。

你心中是不是会想,Animal.prototype就是animal的原型对象啊,所以animal对象可以访问得到。

嗯,恭喜你,猜对了。哈哈哈哈 我们来看下面这张图

这幅图里面就是我们要讨论的三个东西,构造函数,实例化对象,原型对象

  1. 构造函数里面有一个prototype属性,指向原型对象。实例对象中也有一个属性指向原型对象,这个属性是__proto__,prototype和__proto__是两个含义完全不同的属性
  2. __proto__是所有对象都有的一个隐藏属性。作用是,在对象上查找属性查找不到的时候,就会在__proto__所指向的对象里面接着找。这也是为什么animal能够调用logName方法。

你在浏览器控制台里面可能会看到[[prototype]]。 别怕,这个就是__proto__属性

  1. prototype是构造函数特有的属性,非隐藏属性。作用是,实例化出来的对象,它的__proto__属性会指向prototype所指向的对象
  2. 是不是有点绕?没关系,我刚开始学也是

把这个图背下来吧,这三者形成一个三角形

2. 构造函数实例化对象的过程

我们先看实例化出来的对象,和构造函数的写法有什么联系

var animal = new Animal('Cat');
console.log(animal);

我把实例对象animal打印出来了,可以看到其中有个name属性,还有一个[[prototype]]属性

再打开[[prototype]]看看

[[prototype]]里面有个logName

到这里,你应该知道点什么了吧

我再具体化一下构造函数实例化对象的过程:

  1. 首先,JS引擎会创建一个对象A,并且A对象的__proto__指向构造函数prototype所指向的对象。该过程类似于 A = Object.create(Animal.prototype);,如果不清楚create的作用,可以查查文档

  2. 调用构造函数,并且将构造函数里面的this指向刚创建的A对。该过程类似于,Animal.call(A, name);

    call的作用就是改变Animal函数执行过程中的this指向。关于this指向,讲清楚要花点时间,我会专门放在一篇文章里讲清楚。这里你只要知道会这么做就行了

  3. 调用了Animal函数之后,A对象就有name属性。构造函数执行结束后,我们就得到了最终的A对象,也就是Animal函数实例化的对象

  4. 如果在构造函数中,return了另外一个对象B,那用户得到的就是这个被return的B对象,否则得到的就是上面的A对象

function Animal(){
    return {
        name: 'dog'
    }
}

var dog = new Animal();

这样得到的dog对象,和Animal原型对象一点关系也没有

console.log( dog.__proto__ === Animal.prototype );   // false
  1. 我把实例化对象的过程,用代码总结一下
function initialAnimal(name){

    var A = Object.create(Animal.prototype);
    var B = Animal.call(A, name);
    
    if(typeof B === 'object'){
        return B;
    }
    
    return A;
}

围绕着原型对象再说一说

1. constructor

每个原型对象都有一个constructor的属性,指向自己的构造函数。

这是Animal.prototype的打印结果:

我们可以看到,除了logName的属性,还是有个不可枚举属性--constructor。这个constructor指向Animal构造函数

这样的话,上面的三角图就有了变化:

这里我实例了两个对象,animal1、animal2,它们的__proto__都指向了同一个对象,也就是Animal.prototype指向的对象

我们可以看到原型对象中有个constructor属性,指向了构造函数。这个属性有什么用呢,当你拿到一个原型对象的时候,就可以通过constructor来判断该原型对象是哪个构造函数的原型对象。到后面讲到原型链尽头的时候,非常有用

console.log(Animal.prototype.constructor === Animal);	//true

2. instanceof

instanceof的用处是:用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

额,TMD好学术啊

不过,别慌。理解这句话有两个角度

  • 站在构造函数的角度去看,如果你是Animal构造函数,你的prototype所指的对象在另一个对象的原型链上,那么instanceof的操作结果就是true

  • 站在实例对象的角度去看。我们已经知道JS引擎在查找属性时,会顺着__proto__属性查找一个又一个原型对象地找。如果你是animal实例对象,在查找属性的过程,有个构造函数的prototype属性,指向了你可能要查找的原型对象,那么instanceof的操作结果就是true

    假设你要调用logName这个属性。这个过程就是,首先会在自己身上找,如果找不到,就去__proto__所指向的对象去找,正好你只找一个原型对象就找到了logName。

    假设你要调用toString这个属性。这个过程是,首先会在自己身上找,如果找不到,就去__proto__所指向的原型对象去找;如果在原型对象还找不到,就去原型对象的__proto__所指向的原型对象去找,也就是原型的原型;就这样一直找啊找啊,到最后,在Object的原型对象上找到了这个方法

我们很容易就感知到,查找属性的过程,像顺着一个链条查找。我们这个链条叫做原型链

我们来看用法

console.log(tiger instanceof Tiger);  	//true
console.log(tiger instanceof Animal); 	//true
console.log(tiger instanceof Object); 	// true

现在,你再理解这句话:“instanceof的用处是:用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上” ,是不是很清晰了

理解了instanceof,那对下面的这个结果就不难理解了

var tiger2 = new Tiger('tiger2');

console.log(tiger2 instanceof Tiger);  	//true
console.log(tiger2 instanceof Object); 	// true
console.log(Tiger.prototype instanceof Object);		//true	

3. isPrototypeOf

这是一个确定一个对象是否为另一个对象的原型的方法,很好理解的一个方法,没有instanceof这么绕

我们来看具体用法

var tiger1 = new Tiger('tiger');
var tiger2 = new Tiger('tiger');

console.log(Tiger.prototype.isPrototypeOf(tiger1)); // true
console.log(Tiger.prototype.isPrototypeOf(tiger2)); //true

4. Object.getPrototypeOf,Object.setPrototypeOf()

如果我们要判断一个对象是否为另一个对象的原型,方法不止isPrototypeOf()

Object.getPrototypeOf()的作用是返回一个对象的__proto__属性指向的对象,也就是直接获取一个对象的原型对象

我们来看具体用法

console.log(Object.getPrototypeOf(tiger1) === Tiger.prototype);  //true
console.log(Object.getPrototypeOf(tiger2) === Tiger.prototype)   // true

是不是超简单😁

Object.setPrototypeOf()的作用是设置一个对象的属性

我们来看具体用法

Object.setPrototypeOf(tiger, Animal.prototype);

console.log(Object.getPrototypeOf(tiger) === Tiger.prototype);   //false
console.log(Object.getPrototypeOf(tiger) === Animal.prototype);  //true

我们知道,“=== ”比较对象的时候,是比较引用的。如果比较结果是true,那就说明是同一个对象

下一讲,我们再来研究下原型链

好吧,这一讲的东西有点多,多看几遍,不难😁

总结:

  1. 不同编程语言对象系统的继承方法
  2. 用两句话概括原型
  3. 手动用原型实现一个继承
  4. 构造函数、原型对象、实例对象的关系
  5. 再说一说和原型对象有关的细节和API
  6. 如果有哪里讲的不明白,留言告诉我,谢谢🙏