昨天发现这篇自己初学半年时写的文章,当时被各种原型链的知识玩懵了,也算是写了这篇文章之后,才对原型链有了更深刻的理解和认识。搬运过来当做复习~
原型(Prototype)——构造器(Constructor)
当我们谈论原型,我们在谈论什么——对象。
过去,我们这样声明一个对象:
1 | var a = { |
当对象是一个用户时:
1 | var whh = { |
当我们需要多一个用户时
1 | var lsd = { |
两个对象的结构毫无区别。
那么问题来了,如果我们需要创建100个用户时,也要这么做吗?Of Course Not ✘
两个原因:
- 懒。
- 用户的信息是动态的,我们不能在代码中把它们写死。
工厂函数现身
所以伟大的先人就想到一种办法简化我们的工作:创建一个function专门为我们生成user
1 | function user(name, age, gender) { |
测试代码:
1 | console.log(user('王花花', 18, 'female')); |
所以,这个名为user的函数的本质就是生成对象。这样的函数被称为工厂函数(Factory Function),专门生产对象。
更加简便的方式:
1 | function user(name, age, gender) { |
this指代它即将生成的对象
工厂函数的使用方式:new
关键字。
1 | var whh = new user('王花花', 18, 'female'); |
通常我们称作使用new关键字叫的函数为构造器/构造函数(Constructor),构造函数的首字母一般大写。
原型————prototype和proto
生成一个对象的过程叫实例化,这在任何一种语言中都一样。
1 | var whh = new user('王花花', 18, 'female'); //实例化 |
接下来给这个构造器添加一些功能
1 | function User(name, age) { |
测试代码:
1 | var whh = new User('王花花', 19); |
可见两个实例化的user都实例了greet能力。也就是说,whh这个对象中有greet这个方法,lsd这个对象中也有同样的方法。这两个方法是完全独立的,可以通过如下方式验证:
1 | whh.greet === lsd.greet; //false |
以上说明,每一次实例化都会创造一份与其它对象同样的方法。这里可以理解为拷贝代码,将构造器中的方法拷贝到实例化的对象中。
那么问题来了,真的有必要每一次实例化都要拷贝吗?
这时伟大的先人出现了,就引入了原型(prototype)概念。
在控制台中随意创建一个function a() {}
,然后输入a.prototype
,我们可以发现它是一个对象。
原生JS就有这样一个机制,在我们创建一个function时,它首先将prototype对象放在function下。
prototype用于存放给它即将生产的对象继承下去的属性。也就是我们一眼看不出来,但它实际拥有的能力。举个栗子:
1 | function A() {}; |
可以看到,它没有在自有的属性中显示name属性,而是在继承的属性中显示。但使用时与自有属性无区别,而且不管我们new了几个对象,它们中的proto都是一样的(其实是指向同一对象的)。
测试代码:
1 | a.__proto__ === b.__proto__; //true |
知道这个原理,就可以优化上面的User构造器了。
1 | function User(name, age) { |
测试代码:
1 | whh.__proto__ === lsd.__proto__; //true |
也就是说,现在的function只会存在一个地方,它不再需要每次都拷到实例的对象中去。
当我们想知道一个对象来自于哪个工厂函数,可以直接对象.consturctor
。
那么就意味着,如果我们想复制一个对象的结构,可以new whh.constructor()
,也就相当于new User()
。
原生对象的原型
抛出一个问题,当我们这样创建一个对象的时候,它的原型是什么呢?它有没有父亲呢?
1 | var a = {}; |
我们打印这个对象后可以看到,它的constructor是一个叫Object的函数。也就是说,以下两种写法是等价的:
1 | var a = {}; |
测试代码:
1 | a.constructor === b.constructor; //true |
再次抛出问题:当我们想创建一个不继承任何东西的对象时该怎么办呢?
1 | var a = Object.create(null); |
可以在参数中设置原型,如:
1 | var a = Object.create({ |
测试代码:
1 | var a = []; |
控制台中可以看到a的constructor是一个叫做Array的function。
也就相当于:var a = new Array()
。两种写法等价。
继续,我们可以看到Array()也有继承的属性,它的constructor是Object()。
综上,如果Object()是爷爷,那么Array()就是爸爸,而我们声明的数组对象a就是儿子。
总结: 在JS中只要我们不明确的用Object.create()来创建对象,其余都是继承Object.prototype的。
如何实现多级继承琏
有这样一条继承链:动物-哺乳动物-人类-lsd
如何在代码中实现这样一条继承链?
1 | function Animal() {} |
从最顶层开始看,实例两个对象。
1 | function Animal() { |
测试代码:
1 | a.eat === b.eat; //false |
用原型优化构造器:
1 | function Animal() {} |
接下来看Mammal构造器
1 | function Mammal() {} |
那么此时实例化对象m是否有eat和sleep这种能力呢?
答案是:暂时还没有。
因为此时三个构造器是独立的,我们还没有指定它们之间的继承关系。那么如何指定这条继承琏?
拿Mammal和Animal来说,Mammal不仅要继承Animal的prototype,而且还要有自己的东西。
我们可以用到前面提到的create方法:
1 | Mammal.prototype = Object.create(Animal.prototype); |
测试代码:
1 | m.eat(); //mia mia mia... |
这样我们就可以说它继承成功了。
不过现在还有一个问题,我们可以查看一下m.constructor
。
打印结果为Animal而非Mammal。而事实上Mammal才是m对象的constructor。
仔细查看对象m的打印结果,发现并没有constructor。
这是因为我们在前面把它的prototype重写了,覆盖掉了。Mammal.prototype = Object.create(Animal.prototype);
所以才会继承上一级的constructor。
解决方案:重新明确指定即可。Mammal.prototype.constructor = Mammal;
]
接下来看Person。
1 | function Person() {} |
到这里就完成了原型的三级继承~~~
不过还有一个问题:不同的动物毛色可能不一样,体重也不一样,等等等等。也就是说,它们回宫有一些属性,不过值不一样。
测试代码:
1 | function Animal(color, weight) { |
此时实例化的对象中就有了我们用this指定的两个显性属性”color”和”weight”。
但是当我们实例一个Mammal的对象并传入参数:
1 | var m = new Mammal('black', 100); |
我们可以发现,是一个空对象。
我们希望的是,只要是动物(不管是哺乳动物还是人),都存在毛色、体重等等属性。如何实现?
1 | function Mammal(color, weight) { |
Person中同理
1 | function Person(color, weight) { |
测试代码:
1 | var m = new Mammal('black', 100); |
可见,显性属性是拷贝的。它们之间互不影响。这就是显性属性的继承方式。
Bye~👋
v1.5.2