类和实例是大多数面向对象编程语言的基本概念,不过在JS中不区分类和实例的概念,而是通过原型(prototype)来实现面向对象编程。原型是指当我们想要创建xiaoming这个具体的学生时,我们并没有一个Student类型可用。那怎么办?
var Student = {
name: 'Robot',
height: 1.2,
run: function () {
console.log(this.name + ' is running...');
}
};
var xiaoming = {
name: '小明'
};
// 把xiaoming的原型指向了对象Student
xiaoming.__proto__ = Student;
xiaoming.name; // '小明'
xiaoming.run(); // 小明 is running...
JS没有类的概念,所有对象都是实例,所谓继承关系就是把一个对象的原型指向另一个对象而已,如果你把xiaoming的原型指向其他对象:
var Bird = {
fly: function () {
console.log(this.name + ' is flying...');
}
};
xiaoming.__proto__ = Bird;
xiaoming.fly(); // 小明 is flying...
请注意,在JS中不要直接用obj.proto去改变一个对象的原型,并且低版本的IE也无法使用proto。Object.create()方法可以传入一个原型对象,并创建一个基于该原型的新对象,但是新对象什么属性都没有,因此我们可以编写一个函数来创建xiaoming:
// 原型对象:
var Student = {
name: 'Robot',
height: 1.2,
run: function () {
console.log(this.name + ' is running...');
}
};
function createStudent(name) {
// 基于Student原型创建一个新对象:
var s = Object.create(Student);
// 初始化新对象:
s.name = name;
return s;
}
var xiaoming = createStudent('小明');
xiaoming.run(); // 小明 is running...
xiaoming.__proto__ === Student; // true
实例对象
JS可以用构造函数的方法来创建对象,它的用法是先定义一个构造函数:
function Student(name) {
this.name = name;
this.hello = function () {
alert('Hello, ' + this.name + '!');
}
}
// 在JS中用关键字new调用函数返回对象:
var xiaoming = new Student('小明');
xiaoming.name; // '小明'
xiaoming.hello(); // Hello, 小明!
这里请注意,如果不写new,这就是一个普通函数并返回undefined。但是如果写了new,它就变成了一个构造函数,它绑定的this指向新创建的对象并默认返回this,也就是说不需要在最后return this。用new创建的对象还从原型上获得了一个constructor属性,它指向函数Student本身:
xiaoming.constructor === Student.prototype.constructor; // true
Student.prototype.constructor === Student; // true
Object.getPrototypeOf(xiaoming) === Student.prototype; // true
xiaoming instanceof Student; // true
Student.prototype指向的对象就是xiaoming、xiaohong的原型对象,这个原型对象有个属性constructor指向Student函数本身,但是xiaoming、xiaohong这些对象没有prototype属性,不过可以用proto这个非标准用法来查看,现在我们就认为xiaoming、xiaohong这些对象“继承”自Student。
xiaoming.name; // '小明'
xiaohong.name; // '小红'
xiaoming.hello; // function: Student.hello()
xiaohong.hello; // function: Student.hello()
xiaoming.hello === xiaohong.hello; // false
xiaoming和xiaohong调用的hello是一个函数,但它们是两个不同的函数,虽然函数名称和代码都是相同的。要让创建的对象共享一个hello函数,根据对象的属性查找原则只要把函数移动到对象原型上就可以了,也就是Student.prototype:
function Student(name) {
this.name = name;
}
Student.prototype.hello = function () {
alert('Hello, ' + this.name + '!');
};
如果创建对象时忘记写new,在strict模式下,this.name = name将报错,因为this绑定为undefined,在非strict模式下,this.name = name不报错,因为this绑定为window,于是无意间创建了全局变量name并返回undefined,这个结果更糟糕。为了区分普通函数和构造函数,按照规定构造函数首字母应当大写,而普通函数首字母小写:
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 || {})
}
var xiaoming = createStudent({
name: '小明'
});
xiaoming.grade; // 1
如果创建的对象有很多属性,我们只需要传递需要的某些属性,剩下的属性可以用默认值。
原型继承
在传统的基于类和实例的语言中,继承的本质是类型的拓展,由于JS采用了原型继承,所以不存在类。我们想要在JS中实现继承可以借助于中间函数,用inherits()函数进行封装还可以隐藏中间函数的定义,并简化代码:
function inherits(Child, Parent) {
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
}
function Student(props) {
this.name = props.name || 'Unnamed';
}
Student.prototype.hello = function () {
alert('Hello, ' + this.name + '!');
}
function PrimaryStudent(props) {
Student.call(this, props);
this.grade = props.grade || 1;
}
// 实现原型继承链:
inherits(PrimaryStudent, Student);
// 绑定其他方法到PrimaryStudent原型:
PrimaryStudent.prototype.getGrade = function () {
return this.grade;
};
JS的原型继承实现方式如下:
- 定义新的构造函数,并在内部用call()调用希望“继承”的构造函数,并绑定this;
- 借助中间函数实现原型链继承,最好通过封装的inherits函数完成;
- 继续在新的构造函数的原型上定义新方法;
类继承
我们知道JS的对象模型是基于原型实现的,特点是简单,但是实现继承需要大量代码。ES6引入了新的关键字class来定义类:
class Student {
constructor(name) {
this.name = name;
}
hello() {
alert('Hello, ' + this.name + '!');
}
}
var xiaoming = new Student('小明');
xiaoming.hello();
用class直接通过extends来实现继承:
class PrimaryStudent extends Student {
constructor(name, grade) {
super(name); // 记得用super调用父类的构造方法!
this.grade = grade;
}
myGrade() {
alert('I am at grade ' + this.grade);
}
}
注意,因为不是所有的主流浏览器都支持ES6的class,现在使用还不太方便,不过可以用Babel这个工具把class代码转换为传统的prototype代码。