深入剖析js的原型、Function和Object之间的关系并简单实现继承

我看了很多网上有关于原型的一些说明,总觉得说的不够清楚,而有一句话很有意思“在js里万物皆对象”,我承认事实确实是这样,但是都说的模棱两可,现在我们就深入探究一下这里面的门道(我使用的火狐浏览器开发版,不同浏览器可能控制台输出不一样)

js里的对象,属性等说法,其实就是指针,它们指向自己的实例空间(除了基本类型)

先看一个简单的function变量

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

console.log("fun1", fun1)

 

 从结果可以看到定义一个function,它里边所含有的内容这六个属性是每个function所必有的,直接看第五个prototype(注意prototype是一个对象)就是传说中的原型(本文只称它为prototype),第六个属性是灰色的并且用尖括号括起来,它这么不显眼是因为js就不想让程序员去用它,在以前版本的浏览器它有另外一个名字叫__proto__(为了方便区分,下文就以__proto__来称呼它,而且在以前的版本里名字是__proto__,以下划线开头还是两个说明是js坚决拒绝程序员去修改它的,但是下面为了剖析其内部原理我会对其做一些粗暴的改变,大家注意在工程中尽量避免)。

如果大家去实验一下就会发现,每个对象都会有__proto__这个属性,但一般情况下只有声明function的变量(例如上图中的fun1)才会有(自动生成)prototype这个属性,而function通过在它的名字前加new 可以创建出属于它的实例,因此我认为js里的function有三个角色:函数,对象和类(类似于Java里的对象可以通过类产生,有人说js和java没有半毛钱关系,我想说它们都是面向对象的语言,都有对象这个概念,那么伴随的也就有类,function声明的变量就是类)。而prototype这个就体现了function类的概念

可以看到在 prototype里有两个属性constructor和__proto__,在前面我们说过prototype是一个对象和每个对象都会有__proto__这个属性,因此prototype也是有__proto__这个属性;constructor(构造方法)这个属性是在生成prototype时自动生成的属性,其指向函数本身(在申明函数时,js自动创建该函数的peototype属性)。

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

console.log("fun1", fun1)
console.log(fun1.prototype.constructor === fun1)

一个对象__proto__指向产生它的类的prototype(就是指向new 是后边所跟的那个东西,严格来讲是它作为左值时等号右边的东西,function fun1(){}等价于var fun1 = function(){},fun1的__proto__指向Function的prototype)。

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

var temp = new fun1("");
var obj = new Object;
console.log(temp.__proto__ === fun1.prototype)
console.log(fun1.__proto__ === Function.prototype)
console.log(obj.__proto__ === Object.prototype)

 

总结一下:

  1. 所有对象都有__proto__属性,是用来通过__proto__找到它的原型即prototype,function声明的变量的__proto__指向Function的prototype,其它对象的__proto__指向Object的prototype
  2. function声明的变量、Function和Object都有prototype, 有prototype的东西可以产生实例(即可以作为new 后边的值)。 
  3. prototype它是一个对象(在声明函数变量是在函数内部由js自动创建),因此它也有__proto__,并且指向Object的prototype。

 

Function和Object非常特殊,我们来具体分析

首先看一下Function里的东西

console.log("Function", Function);
console.log("Function.__proto__", Function.__proto__)

 从控制台的打印可以明显的看出Function的__proto__指向了它自己的prototype

 

接下里看Object(Object里有很多其它的东西,为了简洁我直接打印Object的prototype)

console.log("Object.prototype", Object.prototype)
console.log("Object.__proto__", Object.__proto__)

Object的prototype和Function的prototype的__proro__指向是相同的如下图:

 

综上可以看出Object的__proto__指向Function的prototype,而Object的prototype并没有灰色的<prototype>即__proto__,即它是一切之源。

console.log("Object.prototype.__proto__", Object.prototype.__proto__)

 我将Object的prototype称为源型,下面我给出我个人对这些现象的解释:

源型是js的内置的一段代码,所有所有通过这个源型创造出的都是object,第一步先创造出Function的prototype,因此这个prototype的__proto__指向源型,然后再通过这个prototype造出Function,因此Function的__proto__指向它自己的prototype,然后用Function造出Object,因此Object的__proto__指向Function的prototype,然后js直接将Object的prototype替换为源型。

并且我认为js里判断继承(即A instanceof B)是沿着__proto__往下走,首先要求B必须有prototype属性且必须是一个对象(否则会浏览器会报 'prototype' property of B is not an object),判断时先判断A的__proto__是否和B的prototype指向是否相同(即===结果为true),若相同则返回true,若不相同则判断A的__proto__指向属性里是否还有__proto__属性,若有则进行再次进行判断指向是否相同,直到找到源型,它的__protot__为null,返回false

为了证明只要A的__protot__和B的prototype指向相同就返回true,给了如下测试:

var obj = new Object;
function fun () {};
console.log(obj instanceof fun);
var temp = new Object
fun.prototype = temp
obj.__proto__ = temp;
console.log(obj instanceof fun)

有点颠覆三观不过习惯就好。

下面用我的结论来解释下边四个现象:

console.log(Function instanceof Function)
console.log(Function instanceof Object)
console.log(Object instanceof Function)
console.log(Object instanceof Object)

  1. Function的__proto__和Function的prototype指向相同,因此返回true,
  2. Function的__proto__和Function的prototype指向指向相同,Function的prototype的__protot__和Object的prototype指向相同,因此返回true。
  3. Object的__proto__和Function的prototype指向相同(因为Object就是以Function为模板创造的),因此返回true。
  4. Object的__proto__指向Function的prototype,Function的prototype的__proto__指向Object的prototype,这个prototype是属于Object(饶了一圈),因此返回true。

只要高清内部原理,理解instanceof就非常简单

下面再来一个小测试:

var obj = new Object;
obj.__proto__ = Function.prototype;
console.log(obj instanceof Function)

 

 

总结:peototype是原型,__proto__所指向的以及其后的所有peototype称为原型链。“js里一切皆对象”倒不如所是js里的所有对象都是由“源型”生成

 

简单(较优雅的方式)实现继承

在上篇博文里讲过function声明的对象(比如fun)有三个角色:函数、对象和类,作为类js会自动给它添加prototype属性,prototype是一个对象,它里面第一个属性就是constructor(构造函数),按道理说通过它(fun)创建的实例时有三步(这和java的new的过程很像)

  1. 在内存里分配好所需的空间
  2. 执行构造函数,完成对成员的初始化
  3. 将空间的首地址赋值给等号有边的变量

但问题是在第二步的时候,执行的构造函数是prototype里的constructor吗

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

animal.prototype.constructor = null;

console.log("animal", animal)
var a = new animal("狗子") 
console.log("a", a)

我将animal的prototype的constructor属性赋值为null,但是a依然被正确的构造出来了,并且a的<prototype>也就是__proto__中的constructor也是null,我认为在构造实例的时候js调用构造函数时是直接调用animal本身的代码,而不是通过animal的prototype访问其constructor。那么constructor是不是就没有用了呢?在这里我觉得prototype里的constructor属性只是给一个对象提供找到它的够造函数来用的,给它一个追根溯源的机会(可能我见识浅薄暂时看不到它的用处)。

什么是继承:继承要实现的目的就是子类的对象可以调用父类提供的一些公共的属性和方法,子类对象是通过__proto__来寻找的。当子类对象要访问某一个方法或属性,js会先在子类对象里寻找是否有该属性若没有则沿着__proto__指向的prototype空间找,找到了则返回,子类对象只能访问而不能修改,若要试图给父类的属性赋值,则js只会在该对象创建一个名称一样的变量而不会修改父类的prototype空间里的值。

那么如上文所说直接修改类的prototype的__protot__属性,让它指向另一个类的prototype不就可以实现继承吗,但我们坚决拒绝直接修改__proto__。先讲一下思路:首先我们需要让一个类的prototype的__proto__指向另一个类的prototype,但是不能直接修改prototype的__proto__属性,但我们可以修改该类的prototype属性,也就是我们可以创建一个对象,让该对象的__proto__指向父类的prototype并且让这个对象作为子类的prototype。

具体做法:先声明一个function变量,让其的prototype指向父类的prototype,并new 它,产生的对象的__proto__就指向了父类的prototype,然后将它作为子类的prototype就完成了继承(那种直接new 父类并将产生的对象作为子类的方法是不可取的,因为在new父类时会调用父类的构造函数生成很多垃圾属性,这些属性不应该存在在prototype中)

function ExtendClass(base, klass) {
	if (base == undefined ||  base == null || klass == undefined || klass == null || !base instanceof Object || !klass instanceof Function) {
		return;
	}

	function fun() {};
	fun.prototype = base.prototype;
	klass.prototype = new fun();
	klass.prototype.constructor = klass;
}

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

function Dog(name, type) {
	Anaimal.call(this, name)
	this.type = type;
}
ExtendClass(Anaimal, Dog);

var dog = new Dog("小白", "京巴");
console.log("Anaimal", Anaimal)
console.log("Dog", Dog)
console.log("dog", dog)

我编写了一个继承的函数,继承函数必须在类变量声明后在prototype修改前调用,否则以前的修改无效。

从上图可以得知我们成功让Dog继承Animal,即让Dog的prototype的__proto__指向Animal的prototype属性。这时dog就可以调用它的原型链里面的所有方法和属性。

一般在new的过程中我们是先调用父类的构造方法,然后依次执行,所以我们在new Dog时要在内部先调用Animal的构造方法(因为哪个类继承与哪个类是程序员所知道的,因此可以直接指定调用哪个方法作为父类构造方法),但是如果你在Dog代码块内部直接调用Animal(“”)方法它的默认执行对象是window,即最终的结果不是给dog里增加一个name属性,而是用Animal类创建了一个匿名对象,因此使用call函数,它的第一个参数是执行的对象,之后是形参,我将this传进去就相当于我将执行对象变为dog,这样dog就成功执行了父类的构造方法。

 

  • 14
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值