Web前端的知识之旅哟——继承

this的一些问题

前面我们知道函数内部的this默认的指向是全局对象window,那么我们有什么办法可以改变this的指向吗?

现在我们就可以使用call/apply改变this指向

call/apply

作用:改变this指向

区别:后面传的参数形式不同

我们现在写一个函数


1. function person () {
2.       this.name = ‘scarlett’;
3.       console.log(this);
4. }
5. person(); // window
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

这个时候this指向window,name属性自然也就是window上面的全局属性。

现在我们来尝试使用一下call,看看有什么效果。


1. var obj = {};
2. person.call(obj); // Object {name: “scarlett”}
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

我们可以看到,当使用了call方法之后,person函数内部的this就指向了我们传递的这个对象。现在这个函数就可以为obj来赋属性值了。

如果这个函数还有参数的话,我们只要把实参的值写在call后面并且用逗号间隔开就可以了。


1. function person(name, age) {
2.       this.name = name;
3.       this.age = age;
4. }
5. var obj = {};
6. person.call(obj, ‘scarlett’, 18);
7. console.log(obj.name); // scarlett
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

apply和call基本没有区别,唯一的区别是call后面的参数是一个一个传的,而apply后面的参数是放进一个数组里面然后传进去的。

还是上面那个例子,如果用apply来传递实参的话,将是下面这种形式。


1. person.apply(obj, [‘scarett’, 17]);
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

继承

继承是这篇文章的重点部分。

首先介绍一下继承发展史。

第一阶段:传统形式

这个阶段使用的继承方式就是我们上一篇文章介绍的那一种原型链式的继承。

但是这种继承有一个缺点就是它会继承过多没有用的属性,造成大量的浪费。

第二阶段:借用构造函数


1. function Foo(name, age) {
2.       this.name = name;
3.       this.age = age;
4. }
5. function Son(name, age) {
6.       Foo.call(this, name, age);
7. }
8. var son = new Son(‘son’, 123);
9. console.log(son.name); // son
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

这种方式就是利用了call和apply可以改变this指向的特点,通过构造函数来间接的构造子对象。

但是这种方式有两个缺点:

1.严格来说,这种方式不属于继承,也访问不了原型的原型

2.每次构造一个对象都要走两个构造函数,效率很低。

第三阶段:共享原型


1. Son.prototype = Foo.prototype;
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

这种方式就是让父子构造函数的原型都一样,虽然这种方法让子构造函数可以访问原型链,也不用走两个构造函数了,但是缺点也很明显:没办法改变子类的原型,一改就两个一起改了。

第四阶段:圣杯模式

这个阶段也是最终阶段,当然也是我们目前使用的方式。


1. function inherit (C, P) {
2.     function F () {}
3.     F.prototype = P.prototype;
4.     C.prototype = new F();
5. }
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

这里我们利用了一个中间函数F来沟通P和C的原型,这样我们改变C的原型的时候只会影响F而不会影响P,而F这里并没有任何的用处。


1. function Child () {}
2. function Parent () {}
3. Parent.prototype.name = ‘scarlett’;
4. inherit(Child, Parent);
5. console.log(new Child().name); //scarlett
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

但是现在我们还存在一个问题,当我们想查看Child构造出来的对象的构造函数的时候,它并不打印Child函数而是Parent函数。


1. console.log(new Child().constructor); //function Parent () {}
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

所以我们前面的工作还不完善,还需要记录子类的构造函数。


1. C.prototype.constructor = C;
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

同时我们为了保存一下它的父类,也用一个uber来记录一下父类。


1. C.prototype.uber = P; // 因为super是保留字我们不能使用,所以用了一个uber
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

所以我们最终的形式就变成了这个样子:


1. function inherit(C,P){
2. function F () {}
3.       F.prototype = P.prototype;
4.       C.prototype = new F();
5.       C.prototype.constructor = C;
6.       C.prototype.uber = P;
7. }
8. console.log(new Child().constructor);// Child
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

不过,这种写法虽然成功了,但是还是有一点点小瑕疵,这里面的F函数只是起到一个搭桥的功能,并没有什么实质性的作用,那么我们能不能舍弃掉这个东西呢?

虽然真的舍弃掉是不可能的,但是我们可以通过一定的方法来规避这个函数,让他不出现在我们的继承函数中。


1. var inherit = (function (){
2.       var F = function(){};
3.       return function (C,P){
4.             F.prototype = P.prototype;
5.             C.prototype = new F();
6.             C.prototype.constructor = C;
7.             C.prototype.uber = P;
8.       }
9. }());
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

这样通过立即执行函数和闭包,我们既可以使用F函数,又可以不让它出现在我们的真正的继承函数中(即返回的那个函数)。

这种方法很重要,希望大家都记这一种方法!