什么是原型链
一个新的对象例如:
|
|
通过打印可以发现,对象在生成后自带了一个__proto__属性。
例如当我们调用obj.toString时,JS引擎会一次做以下步骤:
- 看看obj本身是否有toString属性。
- 看看obj.__proto__是否有toString属性,如果有则返回此内容。
- 查看obj.__proto__.__proto__ …直到找到toString属性或者proto\为null
这样一个链式的搜索过程,即为原型链。
this的值到底是什么
一个简单的面试题:
|
|
从函数调用的角度来看,JS中有三种函数调用形式:
|
|
不过前两类其实都可以归为第三类:
|
|
这样,this就好定义了,就是第三类调用方法中的context
回到题目:
|
|
小插曲:
|
|
JS的new到底是干什么的
先来看看不用new来制造一个对象:
|
|
如果我们要制造一百个这样的对象,循环一百次即可。
但是这样的方式存在内存大量浪费的问题,比如prop4和prop5,在一百个对象中其实都是同样的函数。
通过前面的原型链可以知道,我们可以通过原型链来解决重复创建的问题:我们可以先创建一个对象原型,然后让obj的proto指向对象原型。
|
|
把创建一个对象的代码写在两个地方,可能不太优雅,于是我们可以封装一个方法:
|
|
所以js内置了new这个关键字,它做了以下几件事:
- 不用创建对象,因为new会帮你做。(使用this就可以访问到临时对象)
- 不用绑定原型,new为了知道原型在哪,所以固定了原型的名字为prototype。
- 不用return对象,new会自动帮你做。
综上所述,new是一个js中的语法糖,所做的就是上面的几件事,下面用new来完成刚刚的操作:
|
|
这里要注意一下Obj从一个对象变成了一个函数,这里其实是因为new关键字需要记录这个新对象是由哪个函数来创建,所以prototype里面其实有一个隐藏的属性constructor,它被默认的指定为了Obj;
|
|
再谈继承
JS的继承,本质上就是原型链的传递。
1、借助构造函数实现继承
|
|
构造函数实现继承的原理是在子类的构造函数中调用父类的构造函数,这样父类的自有属性就可以继承给子类,但是缺点是子类没有父类prototype中的属性,比如上例中实例出来的Child子类就没有say方法。
2、借助原型链实现继承
|
|
这里有趣的一点是,child1给name赋值,没有影响child2.name原因是,当给一个对象本身的属性赋值的时候,是会覆盖掉原型上的属性,而不是修改原型上的属性。
而给child1.arr push一个值,却影响了child2.arr,原因是子类的原型都指向同一个对象(一个地址),而js是地址引用,所以child1.arr和child2.arr在没有被子类覆盖之前,其实是同一个地址,所以一个子类修改后其他子类也受影响。
3、组合方式继承
|
|
用这种组合的方式,能够解决1、2种方法的缺点。(1)行代码在每次执行Child的构造函数的时候,都执行了一遍父类的构造函数,因此每一个子类都会复制一遍父类的私有属性(arr也有了新的地址),所以修改时相互之间不受影响。(2)行代码可以让child通过两次原型链上的查找获得say方法。
但是这个方法的缺点在于性能不高,因为每建立一个子类,Parent 都new了两次,一次是在Child构造函数种调用了Parent.call(this);一次是将子类原型指向 new Parent() 的时候。
4、组合继承的优化1
|
|
将这一句修改后,能达到上述同样的效果,但随之而来的问题是:
|
|
我们无法区分一个对象到底是由谁实例化的,出现这种情况是因为我们说过,原型中会默认一个constructor的属性指向构造函数,因为我们将Child的原型指向了Parent的原型,所以出现了上述情况。
5、组合继承的优化2
|
|
经过上述改良后,其实并没有实质性的改变,所以我们最终还是需要手动的给原型指定constructor
|
|
最终代码:
|
|
6、延申:多重继承
|
|