前言:
es6中我们使用class关键字可以创建类,使用extends关键字实现对类的继承。其实es6的类也是语法糖,底层逻辑和es5是一样的,只不过做了封装。 然而在es5中,我们要如何实现类的定义以及继承呢? 一步一步的来看下
最简单的构造函数:
es5中我们写个函数 如
function Person(name) { this.name = name; this.sayName = function() { console.log(this.name) } };
然后使用 new 关键字可以实例化一个Person对象
var person1 = new Person('zhangsan');
我们使用new的时候就是在内存中创建了一个新对象,这个对象的类型是Person。
问题来了,怎么标识一个实例对象的类型是什么的呢?
上面的函数Person就相当于一种构造函数,它就相当于一个新类型了。由它实例化出来的对象和它肯定有某种关联。 答案是 实例对象person1上有constructor属性可以指向构造函数Person。所以constructor就是用来标识对象类型的。(我们也可以用instanceof来确定一个实例属不属于后面的对象)
person1.constructor == Person; // true
person1 instanceof Person; // true
person1 instanceof Object; // true
tips:构造函数也是普通的函数,使用new就是构造函数,不使用new就是普通函数。
这种模式的缺点: Person函数中的sayName是一个函数,每次实例化都声明的一个sayName的函数,这样不太好,都是一样的函数,应该只声明一次。 对,顺着这个逻辑,我们可以把sayName放到全局,然后在Person函数里引用就好了。类似:
function Person(name) { this.name = name; this.sayName = sayName; }
function sayName() { console.log(this.name) } ;
这样解决了重复声明的问题,但是引来了新的问题,污染全局变量。
既要把构造函数里面的函数放到内部,又不能重复声明。我们可以用原型链。
原型链
每个函数创建的时候都会创建一个prototype属性,这个属性是一个对象。包括实例化时需要的共享属性和方法。 然后这个原型对象上有个constructor属性指回与之关联的构造函数。即:
Person.prototype.constructor === Person // true
实例化出来的对象,上面有__proto__属性指向原型对象。即:
Person.prototype === person1.__proto__ // true
tips:在实例对象person1上调用属性,会现在实例对象person1上查找,如果没有会去原型链(Person.prototype)上查找; tips:使用hasOwnProperty();函数来确定这个属性在不在实例对象上。in 操作符,在原型链上也会返回true;
这个时候,我们在构造函数中定义属性,在原型链上定义公共方法
function Person(name) { this.name = name; }
Person.prototype.sayName = function() {
console.log(this.name)
} ;
这个时候我们定义了一个Person类,但是别忘了,还有继承要实现,我们使用原型链让另一个函数的prototype指向Person的实例对象,可以实现对Person的继承,如:
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function() {
console.log(this.name);
};
// 继承Person
function Student() {}
Student.prototype = new Person();
let s1 = new Student();
console.log("s1 name:", s1.name); // undefined
这个时候问题又来了,我们Student继承Person的时候没办法传参,因为name通常是在实例化的时候传过来的. 不仅如此还有一个问题是,对于非基本类型的数据,实例s1上修改后,实例s2上也会修改
function Person(name) {
this.name = name;
this.colors = ["red", "pink"];
}
Person.prototype.sayName = function() {
console.log(this.name);
};
// 继承Person
function Student() {}
Student.prototype = new Person();
let s1 = new Student();
s1.colors.push("black");
s1.name = "zzz";
console.log("s1 colors:", s1.colors, s1.name); //[ 'red', 'pink', 'black' ], zzz
let s2 = new Student();
console.log("s2 colors:", s2.colors, s2.name); //[ 'red', 'pink', 'black' ], undefined
所以为了解决上述两个缺点(1.子类实例化化时无法传参给父类构造函数, 2.子类实例共享一个原型对象上的属性) 我们可以使用组合继承
组合继承
组合继承是 盗用父类的构造函数来继承属性,使用原型链来继承方法.(使用父类的构造函数时,要使用call或apply或bind来改变this指向)
// 组合继承 综合原型链和盗用构造函数
function Person(name) {
this.name = name;
this.colors = ["red", "pink"];
}
Person.prototype.sayName = function () {
console.log("name is ", this.name);
};
// 继承Person
function Student(name, age) {
// 继承属性
Person.call(this, name); // 第二次调用Person()
this.age = age;
}
// 继承方法
Student.prototype = new Person(); // 第一次调用Person()
Student.prototype.sayAge = function () {
console.log("age is ", this.age);
};
let s1 = new Student("zzz", 18);
s1.colors.push("black");
console.log("s1:", s1); // Person { name: 'zzz', colors: [ 'red', 'pink', 'black' ], age: 18 }
let s2 = new Student("ccc", 17);
console.log("s2:", s2); // Person { name: 'ccc', colors: [ 'red', 'pink' ], age: 17 }
这个时候我们已经基本实现了我们的需求,定义了类,并实现了继承.但还不够完美,因为我们调用了两次父类构造函数,造成了内存空间的浪费. 所以有种更好的方式叫做 寄生式组合继承
寄生组合继承
我们定义一个方法 inheritPrototype ,传入两个参数,第一个是子类,第二个是父类. 然后在函数中通过Object.create()方法,以父类的原型为原型 创建一个新对象赋值给子类的原型
// 寄生组合继承
function Person(name) {
this.name = name;
this.colors = ["red", "pink"];
}
Person.prototype.sayName = function () {
console.log("name is ", this.name);
};
// 继承Person
function Student(name, age) {
// 继承属性
Person.call(this, name);
this.age = age;
}
// 继承方法
inheritPrototype(Student, Person);
Student.prototype.sayAge = function () {
console.log("age is ", this.age);
};
function inheritPrototype(son, father) {
let prototype = Object.create(father.prototype);
prototype.constructor = son;
son.prototype = prototype;
}
let s1 = new Student("zzz", 18);
s1.colors.push("black");
console.log("s1:", s1); // Person { name: 'zzz', colors: [ 'red', 'pink', 'black' ], age: 18 }
let s2 = new Student("ccc", 17);
console.log("s2:", s2); // Person { name: 'ccc', colors: [ 'red', 'pink' ], age: 17 }
结果和组合继承是一样的,但是只调用了一次父类构造函数(即实例化时调用的).实现实例几次就调用几次构造函数. 至此我们就利用原型链和盗用构造函数的机制完美的用函数实现了类的定义和继承。
结语: 本文从红宝书整理而来.详情请参见js红宝书 P238 - P248