javascript

js的继承/函数式编程/编程范式

js的继承

OO语言的继承

  1. 接口继承:继承方法签名
  2. 实现继承:继承实际的方法
  • js只支持实现继承,继承主要依靠原型链

  • 基本思想:利用原型让一个引用类型的继承另一个引用类型的属性和方法。

  • 构造函数、原型、实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

那么,假如我们让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例和原型的链条。这就是原型链。

1. 原型链

原型链:原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的。

继承是通过创建父类实例,将该实例赋给子类.prototype实现的

实现的本质是重写原型对象,代之以一个新类型的实例

要注意的地方:通过原型链实现继承的时候,不能使用对象字面量创建原型方法,因为这样做会重写原型链P135

function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
}

function SubType(){
    this.subproperty = false;
}

//继承了SuperType
SubType.prototype = new SuperType();

//attention!
//使用字面量添加新方法,会导致上一行代码无效!
Subtype.prototype = {
    getSubValue : function (){
        return this.subproperty;
    },
    someOtherMethod : function(){
        return false;
    }
};
var instance = new SubType();
alert(instance.getSuperValue());//error!

把SuperType的实例赋值给原型,紧接着又将原型替换成了一个对象字面量而导致了问题。由于现在的原型包含的是一个Object的实例,而非SuperType的实例,因此我们设想中的原型链已经被切断,SuperType和SubType之间已经没有关系了。

原型链的问题:

  • 最主要的问题来自包含引用类型值的原型,(包含引用类型值的原型属性会被所有实例共享——这也是为什么要在构造函数中而不是在原型对象中定义属性的原因)。在通过原型来实现继承的时候,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章的变成了现在的原型属性了。P135

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function SuperType(){
    this.colors = ["red", "blue", "green"];
    }
    function SubType(){
    }
    //继承了SuperType
    Subtype.prototype = new SuperType();
    var instance1 = new SubType();
    instance1.colors.push("black");
    alert(instance1.colors);//"red,blue,green,black"
    var instance2 = new SubType();
    alert(instance1.colors);//"red,blue,green,black"
  • 在创建子类型的实例的时候,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数

2. 借用构造函数(类式继承)

为了解决原型中包含引用类型值所带来的问题,开发人员开始使用一种叫做借用构造函数的技术。(有时候也伪造对象或者经典继承)

基本思想:在子类型构造函数的内部调用超类型的构造函数。

常用方法: apply()/call()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SuperType(){
this.colors = ["red", "blue", "green"];
}
function SubType(){
//继承了SuperType
SuperType.call(this);
}
var instance1 = new SubType();
instance1.color.push("black");
alert(instance1.colors);//"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors);//"red,blue,green"

这里在新创建的SubType实例的环境下调用了SuperType构造函数,这样一来,就会在新SubType对象上执行SuperType()函数中定义的所有对象初始化代码。结果,SubType的每个实例就都会有自己的colors属性的副本了。

借用构造函数虽然解决了刚才两种问题,但没有原型,则复用无从谈起,这种继承方式是非常少见的。

所以我们需要原型链+借用构造函数的模式,这种模式称为组合继承

3. 组合继承

组合式继承是比较常用的一种继承方法,其背后的思路是 使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又保证每个实例都有它自己的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function SuperType(name){
this.name = name;
this.color = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name, age){
// 借用构造函数继承属性
SuperType.call(this.name);
this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function(){
alert(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("balck");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName();//"Nicholas"
instance1.sayAge();//29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green“
instance2.sayName();//"Greg"
instance2.sayAge();//27

最常用的方法

最大的不足是无论什么情况下,都会调用两次超类型构造函数:

  • 一次是在创建子类型原型的时候
  • 另一次是在子类型构造函数内部

解决:开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式

确定原型和实例的关系

可以通过两种方式来确定原型和实例之间的关系。操作符instanceof和isPrototypeof()方法.

只要是原型链中出现过的原型,都可以说是该原型链派生的实例的原型,因此,isPrototypeof()方法也会返回true

call()/apply()/bind()

在 JS 中,这三者都是用来改变函数的 this 对象的指向的。

相似之处:

  1. 都是用来改变函数的 this 对象的指向的。
  2. 第一个参数都是 this 要指向的对象。
  3. 都可以利用后续参数传参。

那么他们的区别在哪里的,先看一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var xw = {
name : "小王",
gender : "男",
age : 24,
say : function() {
alert(this.name + " , " + this.gender + " ,今年" + this.age);
}
}
var xh = {
name : "小红",
gender : "女",
age : 18
}
xw.say();

本身没什么好说的,显示的肯定是小王 , 男 , 今年 24。

那么如何用 xw 的 say 方法来显示 xh 的数据呢?
对于 call 可以这样:

1
xw.say.call(xh);

对于 apply 可以这样:

1
xw.say.apply(xh);

而对于 bind 来说需要这样:

1
xw.say.bind(xh)();

如果直接写 xw.say.bind(xh) 是不会有任何结果的,看到区别了吗?call 和 apply 都是对函数的直接调用,而 bind 方法返回的仍然是一个函数,因此后面还需要 () 来进行调用才可以。
那么 call 和 apply 有什么区别呢?我们把例子稍微改写一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
var xw = {
name : "小王",
gender : "男",
age : 24,
say : function(school,grade) {
alert(this.name + " , " + this.gender + " ,今年" + this.age + " ,在" + school + "上" + grade);
}
}
var xh = {
name : "小红",
gender : "女",
age : 18
}

可以看到 say 方法多了两个参数,我们通过 call/apply 的参数进行传参。
对于 call 来说是这样的

xw.say.call(xh,"实验小学","六年级");   

而对于 apply 来说是这样的

xw.say.apply(xh,["实验小学","六年级"]);

看到区别了吗,call 后面的参数与 say 方法中是一一对应的,而 apply 的第二个参数是一个数组,数组中的元素是和 say 方法中一一对应的,这就是两者最大的区别。

那么 bind 怎么传参呢?它可以像 call 那样传参。

xw.say.bind(xh,"实验小学","六年级")();

但是由于 bind 返回的仍然是一个函数,所以我们还可以在调用的时候再进行传参。

xw.say.bind(xh)("实验小学","六年级");

reference