理解JavaScript函数,类继承
前言:本篇博客偏向理解总结,初学者不友好。
一、JavaScript函数
- js函数函数的声明:
- 一般的函数声明
function f(){
}
- 匿名函数声明
var f = function(){
}
- 函数的调用
- 直接使用函数名调用(这种调用方法是最直接也是最直观的调用方法,适合用作普通的方法函数的调用)
f();
- 使用new关键字进行函数调用(这种调用方式适合用于创建对象,进行对象构造时使用)
var fun = new f();
这里声明了一个f对象,并将fun指向了f对象。
JavaScript对构造函数和普通方法函数的划分是逻辑上的,并没有对两者进行显示的区分,这种划分需要编码者根据实际需求进行运用。
Function = 类(或者叫类构造器) + 普通函数
类 = 变量 + 方法
这些内容在一开始困扰了我好久,因为习惯了Java的OOP,对于JS这种并没有严格划分类,方法和普通函数的用法感觉比较绕,但是缕清之后就好了,因为JS对这些内容的划分是逻辑上的。但是也遵循一些格式规范以便增加可读性,比如用作类的function首字母大写,不然就真的傻傻分不清楚了。
二、函数的类属性和实例属性
1. 实例属性(可以类比Java的实例属性进行理解):
在方法中,尤其是被用作构造方法的方法会经常使用this关键字,this关键字表示该属性是当前函数实例范围内的实例属性,该属性属于当前所在的function。若要获得某个对象的实例属性,需要用对象名.属性名的方式进行访问。
2. 类属性(可以类比Java的静态域进行理解):
类属性属于当前本function,也就是本类,不属于某一个特定的实例。因此对类属性的访问需要采用 类名(function名).类属性 的方式对其进行访问。
注意:因为JavaScript并不严格限制变量先声明后使用,也不限制变量必须先显式的声明,所以在编码时要对实例属性和类属性进行区分,避免出现undefine的情况。类属性和实例属性完全是两个完全不同的概念,就像Java静态变量的实例变量是完全不同的概念
三、区分一个很重要的问题:
使用 new 和不new 的区别:
function doAdd(iNum) {
alert(iNum + 20);
}
function doAdd(iNum) {
alert(iNum + 10);
}
doAdd(10); //输出 "20"
如你所知,第二个函数重载了第一个函数,使 doAdd(10) 输出了 “20”,而不是 “30”。
如果以下面的形式重写该代码块,这个概念就清楚了:
请观察这段代码,很显然,doAdd 的值被改成了指向不同对象的指针。函数名只是指向函数对象的引用值,行为就像其他对象一样。甚至可以使两个变量指向同一个函数:
var doAdd = new Function("iNum", "alert(iNum + 20)");
var doAdd = new Function("iNum", "alert(iNum + 10)");
doAdd(10);
四、继承机制的实现
直接函数调用(多用于普通函数的调用)
就是我们最常见,直接进行声明,并可以直接调用的函数,从属于window
call方式和apply方式(用于继承机制的实现)
因为Js继承的存在,一些方法是属于父类的,子类进行父类的调用时直接调用就会存在一些问题,call和apply很好的解决了这个问题。
继承机制的实现:
- 对象冒充
function ClassA(sColor) {
this.color = sColor;
this.sayColor = function () {
alert(this.color);
};
}
function ClassB(sColor, sName) {
this.newMethod = ClassA;
this.newMethod(sColor);
delete this.newMethod;
this.name = sName;
this.sayName = function () {
alert(this.name);
};
}
var objA = new ClassA("blue");
var objB = new ClassB("red", "John");
objA.sayColor(); //输出 "blue"
objB.sayColor(); //输出 "red"
objB.sayName(); //输出 "John"
多重继承:
function ClassZ() {
this.newMethod = ClassX;
this.newMethod();
delete this.newMethod;
this.newMethod = ClassY;
this.newMethod();
delete this.newMethod;
}
对象冒充的方式直接将ClassA的内容拿到ClassB中,并调用A的构造方法进行了B的初始化操作。
- call调用
function ClassB(sColor, sName) {
//this.newMethod = ClassA;
//this.newMethod(color);
//delete this.newMethod;
ClassA.call(this, sColor);
this.name = sName;
this.sayName = function () {
alert(this.name);
};
}
applly调用和call类似,只是将调用的参数列表换为了数组。
- 原型链
prototype 对象是个模板,要实例化的对象都以这个模板为基础。总而言之,prototype 对象的任何属性和方法都被传递给那个类的所有实例。原型链利用这种功能来实现继承机制。
如果用原型方式重定义前面例子中的类,它们将变为下列形式:
function ClassA() {
}
ClassA.prototype.color = "blue";
ClassA.prototype.sayColor = function () {
alert(this.color);
};
function ClassB() {
}
ClassB.prototype = new ClassA();
这里,把 ClassB 的 prototype 属性设置成 ClassA 的实例。这很有意思,因为想要 ClassA 的所有属性和方法,但又不想逐个将它们 ClassB 的 prototype 属性。还有比把 ClassA 的实例赋予 prototype 属性更好的方法吗?
与对象冒充相似,子类的所有属性和方法都必须出现在 prototype 属性被赋值后,因为在它之前赋值的所有方法都会被删除。为什么?因为 prototype 属性被替换成了新对象,添加了新方法的原始对象将被销毁。所以,为 ClassB 类添加 name 属性和 sayName() 方法的代码如下:
function ClassB() {
}
ClassB.prototype = new ClassA();
ClassB.prototype.name = "";
ClassB.prototype.sayName = function () {
alert(this.name);
};
此外,在原型链中,instanceof 运算符的运行方式也很独特。对 ClassB 的所有实例,instanceof 为 ClassA 和 ClassB 都返回 true。
var objB = new ClassB();
alert(objB instanceof ClassA); //输出 "true"
alert(objB instanceof ClassB); //输出 "true"
所以仅用prototype实现的继承是伪继承。
- 混合方式
function ClassA(sColor) {
this.color = sColor;
}
ClassA.prototype.sayColor = function () {
alert(this.color);
};
function ClassB(sColor, sName) {
ClassA.call(this, sColor);
this.name = sName;
}
ClassB.prototype = new ClassA();
ClassB.prototype.sayName = function () {
alert(this.name);
};
var objA = new ClassA("blue");
var objB = new ClassB("red", "John");
objA.sayColor(); //输出 "blue"
objB.sayColor(); //输出 "red"
objB.sayName(); //输出 "John"
在此例子中,继承机制由两行
ClassA.call(this, sColor);
ClassB.prototype = new ClassA();
在第一行中,在 ClassB 构造函数中,用对象冒充继承 ClassA 类的 sColor 属性。在第二行中,用原型链继承 ClassA 类的方法。由于这种混合方式使用了原型链,所以 instanceof 运算符仍能正确运行。
五、总结
我们要深入理解Js函数之间几种使用方法的差异,普通函数调用和对象方法调用,以及用于类构造器的函数的使用,理清他们之间使用上的差异。JavaScript的类继承方式总结来说是在努力向Java的OOP来靠拢,从(四)混合方式实现类继承的混合方式可以看出,用Java的思想去理解该种实现方式就会很容易理解。