理解JavaScript函数与原型
在这里最重要的是理解一点,JavaScript里面一切都是一个值,换句话说,object是一个值,function也是一个值。这些值与基本类型的区别就在于,他们只是一个变量标签,存储的是一个指向其内容的指针。
函数相对来说就稍微复杂一点,因为默认情况下函数没有确定的运行环境,需要调用者提供运行环境(this)。但实际上函数本身就是一个变量,也是一个值,一个变量标签,这就是函数对象。通过函数对象可以构造函数对象的实例,本质上是通过一定规则对函数对象的一个拷贝(可以先不理解这句话)。
先不管函数对象,使用函数分两种情况,作为普通函数而言,它只是一系列过程的封装,我们使用的也只是这一系列的过程,也因此没有体现出函数的面向对象特性,此时函数的运行环境是调用者,在全局环境下是全局对象。作为构造函数而言,即new function(),其中function()里面一般没有返回值。它除了封装了一系列方法,还将自身实例化为一个对象实例,自己为自己提供了运行环境。我们看下面这个例子
如果函数内部有返回值,使用函数时前面加个new,会怎么样呢?事实上,当我们自定义返回值时,则我们返回的值取代了函数默认生成的对象实例。因此返回的将是return后面的值。
现在来说原型,说到原型时,我们就要考虑到函数对象了。函数是一个对象,是一个值,除了自身的“构造方法”之外,实际上还会有一个默认的属性:对于JavaScript而言,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性是一个引用,其指向函数的原型对象。此外,这个原型对象又会有一个属性constructor,也是一个引用,指向函数对象。从而形成一个圆环。此外,函数对象的实例实际上也有一个隐藏的属性__proto__指向函数的原型对象。
总结一下,一个函数代表着两个对象,应该是函数对象,一个是函数的原型对象。此外可以通过函数对象调用构造函数生成函数对象的实例,其属性__proto__指向函数的原型对象。
function test(){
console.log("test "+this);
return this;
}
var b = new test();
console.log(b == test);//false
console.log(b instanceof test);//true
console.log(test);//[Function: test]
console.log(test.prototype);//test {}
console.log(test.prototype.constructor);//[Function: test]
默认的原型对象只自己生成了constructor,其余的属性实际上都是从Object继承而来的。
对象属性搜索机制与原型链
事实上,任何一个对象实例都有__proto__属性,其指向其所在的原型,我们使用instanceof检查对象类型时,实际上也是通过这个检查的。进一步讲,原型也是一个对象实例,那么原型对象也有自己的__proto__属性,从而指向原型的对象类型。事实上,所有的对象最终都指向Object类型。
我们看到,通过__proto__属性,任何一个对象通过原型都形成了一个通往Object的链条,这就是原型链。因此使用instanceof检查对象类型时,所有对象都是Object对象,只要是在原型链上的类型,instanceof都会返回true。
那么当我们访问一个实例的属性时,会发生什么呢?举个例子A的原型是B,B的原型是Object,现在用A生成一个实例a,代码如下:
这里我直接给__proto__属性赋值,但实际不要这样做,逻辑上这个属性是不应该被修改的,如果用在实际中,鬼知道会出现什么问题。但是从这个代码中我们就看到了属性搜索的过程:首先搜索当前对象的所有属性,如果找到了(例如A.name)那就返回,如果没有找到,就在它的原型对象中搜索(例如A.age),如果找到了就返回,如果还是没有找到,继续沿着原型链搜索。如果始终没有找到就返回undefined。
继承
有了原型链的机制,再谈继承的时候,我们是不是只需要给将父类对象加给子类对象的原型就可以了,这样一方面子类可以访问父类的属性和方法,另一方面父类自己也有原型,这样就形成了继承链。
但是有一个问题,如果是这样的话,所有子类都会共享父类的属性,而没有自己的属性。当然这也好办,子类重写这些属性就可以了,这样一来,子类的属性就可以屏蔽掉父类的属性。但如何优雅地实现这一切呢?
组合继承
//组合继承
function SuperType(name){
this.name = name;
this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
};
function SubType(name,age){
//继承属性
SuperType.call(this,name);
this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
//指定子类对象类型
SubType.prototype.constructor = SubType;
//添加子类方法
SubType.prototype.sayAge = function(){
console.log(this.age);
};
var instance1 = new SubType("jack",22);
instance1.colors.push("black");
console.log(instance1.colors);
instance1.sayName();
instance1.sayAge();
var instance2 = new SubType("greg",12);
console.log(instance2.colors);
instance2.sayName();
instance2.sayAge();
console.log(instance1 instanceof SubType);//true
console.log(instance1 instanceof SuperType);//true
// [Running] node "c:\Users\Administrator\Desktop\JavaScript\继承.js"
// [ 'red', 'blue', 'green', 'black' ]
// jack
// 22
// [ 'red', 'blue', 'green' ]
// greg
// 12
可以看到,子类的构造函数通过call(用apply也可以),调用父类的构造函数,相当于是使用父类对象作为函数的用法,将子类对象作为环境变量输入到父类对象的构造函数当中,从而借用父类构造函数为子类对象做了一份属性的拷贝。之后将子类的原型赋值为父类的实例,从而让子类拥有了父类的所有方法。由于子类中用父类构造函数重写了属性,因此父类中对应的属性会被屏蔽掉。
如果直接使用父类实例作为原型,那么使用instanceof只能检测到子类对象类型为父类,所以要将SubType.prototype.constructor设置为SubType,同时因为子类原型为父类实例,父类实例的原型为父类对象,因此子类也属于父类。
注意:该方法不可在子类的构造函数内为子类的prototype重新复制,这样会导致父类无效。
寄生式继承
寄生式继承式根据已有的一个对象创建一个新的对象类型,并增强这个对象的属性或方法。
/**
* 该方法实际上是创建了一个空的函数对象,
* 并把这个函数对象的原型设置为已有的对象,
* 从而拥有该对象的特性
*/
function object(o){
function F(){}
F.prototype = o;
return new F();
}
var person = {
name:"Jack",
friends:["job","van"]
}
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("rob");
var anotherPerson2 = object(person);
anotherPerson2.name = "linda";
anotherPerson2.friends.push("barbie");
console.log(anotherPerson.name);
console.log(anotherPerson2.friends);
console.log(person.friends);
// [Running] node "c:\Users\Administrator\Desktop\JavaScript\tempCodeRunnerFile.js"
// Greg
// [ 'job', 'van', 'rob', 'barbie' ]
// [ 'job', 'van', 'rob', 'barbie' ]
/**
* 这个是通过上述方法创建已有对象的副本,
* 然后添加属性增强的一个示例
*/
function createAnother(original) {
var clone = object(original);
clone.sayHi = function () {
console.log("hi");
};
return clone;
}
一般来说这种方式用处不大,但借用这种方式,我们可以对组合式继承进一步升级,得到寄生组合式继承的方式。
寄生组合式继承
组合模式有一个局限性,那就是要调用两次父类构造函数,并且专门为子类创建了一个父类实例。但实际上,所有需要共享的函数(或属性)实际上都会放在父类的原型之中,而我们要继承的父类的属性都不需要他们共享。通过组合式继承,我们多创建了一份父类的实例,相当于就多创建了一份父类属性的副本,但实际上并不会用到。
寄生组合式继承就解决了这一问题,下面看代码:
//寄生组合式继承
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType) {
//创建空函数对象,其原型为父类原型
//(注意这里并没有调用父类构造函数,而是将父类原型直接赋值)
var prototype = object(superType.prototype);
//增强对象
prototype.constructor = subType;
//指定对象:将子类原型设置为该对象,相当于之拷贝父类原型
subType.prototype = prototype;
}
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function () {
console.log(this.name);
};
function SubType(name, age) {
//借用构造函数(唯一的一次调用父类构造函数)
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
//添加子类方法
SubType.prototype.sayAge = function () {
console.log(this.age);
};
这里object()函数通过直接将父类原型赋值过来,从而避免了创建无用的父类实例,而子类调用SuperType.call(this, name);复制了所有的父类属性。所以十分巧妙。