我第一次接触面向对象时,使用的是 Python,运用 def __init__(self, *args):
这种 magic method 来完成自身参数的初始化。 JS 当中也有类似的构造器 constructor 。
我先用 ES6 中新引入的 class 语法来定义一个类,因为这样容易理解。
class Student {
constructor(name) {
this.name = name;
}
hello() {
return 'Hello, ' + this.name + '!';
}
}
再定义一个类来继承
class PrimaryStudent extends Student {
constructor(name, grade) {
super(name); // 调用父类的构造函数
this.grade = grade;
}
myGrade() {
return 'I am at grade ' + this.grade;
}
}
看似与 Python 极为相似,难怪我直接用 React 写应用的时候并没有发现不对。这种新的语法简化了 JS 原本复杂的原型 (prototype) 继承,也让直接了解新语法的我忽略了它的本质及特性。
(为了学习)回归原本的面貌
function Student(name) {
this.name = name;
}
Student.prototype.hello = function() {
return 'Hello, ' + this.name + '!';
}
var kitty = new Student('kitty');
var daisy = new Student('daisy');
可以观察到我们并不在 Student 中直接写 hello 函数,而是写在了它的 prototype 中。只要我们给 prototype 定义了函数,那么 Student 生成的对象就都可以调用这个函数,省去每个对象再独自生成函数的操作。
我们把 Student.prototype 想象成一根树枝,kitty 和 daisy 就是生出的绿叶了,它们共用树枝上的器官,不必自己再生出相同的器官来消耗资源。此时 kitty 和 daisy 的原型指向 Student.prototype。
Javascript规定,每一个构造函数都有一个
prototype
属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。
进一步,为了避免生成对象的时候漏写 new,可以封装一下。
function Student(props) {
this.name = props.name || '匿名'; // 默认值为'匿名'
this.grade = props.grade || 1; // 默认值为1
}
Student.prototype.hello = function () {
alert('Hello, ' + this.name + '!');
};
function createStudent(props) {
return new Student(props || {}); // 兼顾没有参数的情况
}
然后直接调用 createStudent() 就可以了。
Student.prototype 的继承应该如何写呢?
function PrimaryStudent(props) {
// 调用Student constructor构造函数,绑定this变量
Student.call(this, props);
this.grade = props.grade || 1;
}
别以为这就结束了。
Student.call(this, props);
这句代码,确实和 Student 搭上边了,但此时 PrimaryStudent.prototype 是谁?
我们的目标是构造一个 PrimaryStudent.prototype ,令它指向上层的 Student.prototype。
有谁是这种关系?kitty 和 daisy 的原型好像就是。
我就不想了,前辈的方法是这样的。
function F() {
}
F.prototype = Student.prototype;
PrimaryStudent.prototype = new F();
// 把PrimaryStudent原型的构造函数修复为PrimaryStudent(因为上一句修改了constructor)
PrimaryStudent.prototype.constructor = PrimaryStudent;
- 构造一个函数 F ,令 F.prototype = Student.prototype
- 生成一个中间对象,那么这个对象的 prototype 就可以指向 Student.prototype
这不就是我们想要的吗?我们把该对象赋给 PrimaryStudent.prototype ,那么就可以实现 PrimaryStudent.prototype —> Student.prototype 的指向连接了。
// 继续在PrimaryStudent原型(就是new F()对象)上定义方法:
PrimaryStudent.prototype.getGrade = function () {
return this.grade;
};
// 创建xiaoming:
var xiaoming = new PrimaryStudent({
name: '小明',
grade: 2
});
xiaoming.name; // '小明'
xiaoming.grade; // 2
// 验证原型:
xiaoming.__proto__ === PrimaryStudent.prototype; // true
xiaoming.__proto__.__proto__ === Student.prototype; // true
// 验证继承关系:
xiaoming instanceof PrimaryStudent; // true
xiaoming instanceof Student; // true
封装它
function extend(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}
Child.uber = Parent.prototype;
意思是为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。(uber是一个德语词,意思是"向上"、"上一层"。)这等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。
参考资料
廖雪峰的三篇文章(个人感觉有些晦涩,最好再去看看别的资料)
推荐阮一峰的三篇文章,由简入深,非常不错。
http://www.ruanyifeng.com/blog/2010/05/object-oriented_javascript_encapsulation.html
http://www.ruanyifeng.com/blog/2010/05/object-oriented_javascript_inheritance.html
http://www.ruanyifeng.com/blog/2010/05/object-oriented_javascript_inheritance_continued.html