测试目录:
在html文件中引入js文件
以下所有文件都在base.js中玩~
原型(Prototype)-构造器(Constructor)
一说到原型,一定和对象直接相关。
以前我们这样声明一个对象
当对象是一个用户时
当我们又需要一个用户时
可以看到两个对象的结构无区别。
那么问题来了,如果我们需要创建100个用户时,也要这么做吗?当然不要
两个原因:
- 懒
- 用户的信息是动态的,我们不能在代码中把它们写死。
所以想到一种办法简化我们的工作:创建一个function专门为我们生成user
可以看出,打印的结果与前面的没有任何区别~
但是我们却少写了很多代码。
这个function的本质就是生成对象。
在这个function中,先新建一个空对象,然后将它填满,最后返回这个对象。
这样的函数我们称它为工厂函数(Factory Function),专门生产对象。
在原生JS中,有一种更加简便的生产对象的方式
这个function中的this就代表它即将生成的对象。
注意:一旦使用这种方式来定义一个工厂函数时,就要用new关键词来叫它。
打印结果依然与前面的没区别~
这种写法相当于原生JS帮我们省去了声明对象和返回对象的步骤。
像这样用new关键词叫的function通常我们称它为构造器/构造函数(Constructor),构造函数的第一个字母一般大写。
不大写也没关系,大写不过是为了让其他人知道这是一个构造函数。
Constructor的概念应该理解的差不多了,下面我们再造几个构造器作为练习。
实例一:
实例二:
原型-prototype和__proto__
生成一个对象的过程叫实例化(Instantistion)。不管在哪种语言里面都一样。
我们再给这个构造器加一些功能,比如greet和eat。
实例化一个对象whh
运行whh.greet();
当我们还需要一个用户
可见两位用户都可以greet。但是注意,它们两都实例了this.greet这个能力。也就是说,whh这个对象中有greet这个能力(方法),lsd这个对象中也有同样的方法。这两个方法是完全独立的,我们来验证这一点,在控制台中测试:
如图,说明它每实例化一次都会给自己创造一份与其它对象同样的方法。在本例中也就是greet和eat方法。这里可以理解为拷贝代码。将构造器中的方法拷贝到实例化的对象当中。
这个时候我们想 有必要一次次拷贝吗?能不能想一种办法把它们放到一个地方?当实例化的对象要用的时候用一个东西就可以了。这就引入了原型(prototype)。
创建一个function。
在控制台输入a.prototype
发现它是一个对象。
原生JS就有这样一个机制,在我们创建一个function的时候,它就首先给我们prototype这样一个对象,放到function下。
为什么要这么做呢?prototype有什么用呢?
prototype就是用于给它即将生成的对象继承下去的属性。也就是说我们一眼看不出来,但它实际拥有的能力。我们来举个栗子
它没有在自有的属性中显示name属性,而在继承的属性中显示。但当我们使用的时候是相当于自己的属性在用的。而且不管我们new了几个对象,它们中的__proto__都是一样的(其实是指向同一对象的)。下图可验证。
既然如此,我们在以下例子中就可以把greet方法放到prototype中了。
修改为
在控制台中
也就是说,现在的function只会存在一个地方,它不再需要每次都拷到实例的对象中去。
用原型的方式来指定一个方法还有一个好处,可以动态更新。
当我们想知道一个对象来自于那个工厂函数,可以在控制台中输入 对象.constructor
如图
那么就意味着,如果我们想复制一个对象的结构,可以这样做:
这就相当于
以上就是有关prototype和__proto__的逻辑。
原生对象的原型
学到这里也许我们会有个疑问,当我们就这样创建一个对象时,它的原型是什么呢?或者说它有没有父亲呢?是谁创建的它呢?
我们来打印一下就知道了~
它的constructor是一个叫Object的函数,也就是说,它是有父亲的。
也就是说,以下两种写法是等价的。
我们来测试一下以上说法是否正确。
声明两个对象
然后再控制台中测试
也就是说上面两种创建对象的方法是等价的~
还有一种情况,当我们想创建一个不继承任何东西的对象时该怎么做呢?
我们可以这样:
这样我们就创建了一个最干净的对象~
在参数中设置原型,比如
看到这里,我们应该不再恐惧对象这个东西了,它的继承关系我们已经搞的很清楚了。
下面我们做个练习:
a是一个数组,我们打印它
可以清楚的看到,它的constructor是一个叫Array的function
也就相当于
两种写法是等价的。
我们继续往下看
Array()也有继承的属性,它的constructor是Object()。
综上,如果Object()是爷爷,那么Array()就是爸爸,而我们声明的数组对象a就是儿子。
我们来试一下从爸爸那继承来的方法:(拿push方法来举例)
证明它从爸爸那继承下来的方法确实能用。
我们再来试一下从爷爷那继承来的方法:(拿toString方法来举例)
总结:在JS中只要我们不明确的用Object.create()来创建对象,其余都是继承Object.prototype的。
如何实现多级继承链
有这样一条继承链
这样的一条继承链在代码中如何体现? 我们可以应用到之前说过的构造器。
创建三个构造器,并且使用Person构造器创建一个叫lsd的人。
我们从最顶级开始看
实例两个对象并打印
前面我们提到过,实例出来的对象会各自拷一份能力。所以打印结果是false。
事实上这些纯逻辑(纯能力)的东西是没有必要拷贝的,那我们为什么不将它们放到原型中去呢?
刷新网页
此时的eat都是指向prototype的。相当于借了一个能力过来,而非拷贝一个能力过来。
接下来我们看Mammal这个构造器
那么此时实例化的对象m有没有eat和sleep这种能力呢?
可见是没有的。因为此时三个构造器是独立的,我们还没有指定它们之间的继承关系。那么如果指定这条继承链呢?
拿Mammal和Animal说,Mammal不仅要继承Animal的prototype,而且还要有自己的东西。
我们可以用到前面提到的create方法。
这种写法就相当于 {__proto__:Animal.prototype}
现在我们再来测试一下实例化的对象m
这样我们就可以说它继承成功了。
不过现在还有一个问题,我们在控制台调用一下m对象的constructor。
打印结果是Animal而非Mammal。而事实上Mammal才是m对象的constructor。这是为啥呢?
仔细看对象m的打印结果,发现并没有constructor。
这是因为我们在前面把它的prototype重写了,覆盖掉了。
所以才会继承了上一级的constructor。
补救方法是,我们再明确指定一下就可以啦。
大功告成~
接下来我们看Person
到这里我们就完成了原型的三级继承~~~~撒花!
现在还有一个问题,不同的动物毛色可能不一样,体重可能也不一样,等等等。也就是说,它们会共有一些属性,不过值不一样。
我们可以这样做:
此时实例化的对象中就有了我们用this指定的两个显性属性"color"和"weight"
但是当我们实例一个Mammal的对象并传入参数,然后打印。
我们发现,打印出来的是一个空对象。
我们希望的是,只要是动物(不管是哺乳动物还是人),都存在毛色,体重等等属性。
这个功能如何实现呢?
我们可以这样做:
我们希望Mammal中的this与Animal中的this绑定到一块。然后正常传入参数。
在Person中同理
我们来打印一个Mammal中实例化的对象m做测试,看看它是否继承Animal中的两个显性属性。
我们再来试一下Person
我们再new一个Person。
可见,显性属性是拷贝的。它们之间互不影响。这就是显性属性的继承方式。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。