JavaScript面向对象

JavaScript面向对象

JavaScript面向对象

在阅读本文前,我假设你已了解以下知识:(如果不清楚的,请自行查阅相关资料):

  • 对象
  • 函数
  • Chrome调试工具

引入

函数除了拿来调用外,还可以创建新的类型:

function People(name) {
	this.name = name;
	this.getName = function() {
		return this.name;
	}
}

var p = new People("Jack");
console.log(p instanceof People); // => true
console.log(p.getName()); // => "Jack"

那么,这个对象里都有什么呢?我们可以把它打印出来看看:

1

可以看到,除了我们放进去的name、getName,还有一个__proto__。它是什么,我先卖个关子。我们先看看“instanceof”,和其他语言类似:

var p = new People("Jack");
console.log(p instanceof People); // => true
console.log(p instanceof Object); // => true

我们可以笼统地认为,People继承于Object。不过,JS中的instanceof其实没有那么简单,MDN上是这样写的:The instanceof operator tests whether the prototype of a constructor appears anywhere in the prototype chain of an object.

接下来我们先看看prototype是什么,将上面的People打印一下:

2

它其实就是一个普普通通的对象,那么,它和new出来的对象(上面的p)有什么关系?可以用这张图表示:

3

这张图是什么意思呢?在内存中,People.prototype和它实例化出的p.__proto__同一个对象。在JS中,所有对象都是Object的子类,所以这个对象同时也是Object的一个实例(相当于new Object)。

这有什么意义?我们知道,JavaScript中会这样寻找属性、方法:

  • 在自己身上寻找
  • 在自己的__proto__上寻找
  • __proto____proto__上寻找……

所以,我们可以这样写:

function People(name) {
	this.name = name;
}
People.prototype.getName = function() {
	return this.name;
}

var p = new People("Jack");
console.log(p.getName());
var p2 = new People("Jason");
console.log(p2.getName());

这样一来,当你运行p.getName()的时候,JS首先会在p自己身上查找,因为自己身上没有找到,就会到自己的原型——People.prototype上面去查找。

再举个例子,我们从文档中可以查到hasOwnprototype的方法,MDN上有这样的描述:

所有继承了 Object 的对象都会继承到 hasOwnprototype 方法。这个方法可以用来检测一个对象是否含有特定的自身属性;

如果我们运行p.hasOwnprototype("name"),JavaScript怎么找到hasOwnprototype的呢?它会:

  • 在p自己身上寻找——没找到
  • 在自己的__proto__,也就是People.prototype上寻找——没找到
  • People.prototype__proto__上寻找。因为People.prototype是一个对象,直接父类是Object。所以,也就是Object.prototype上寻找

可以画出这样的示意图:

4

这就是为什么被称为“原型链”:原型(prototype)被一个一个地,“串”在了一条“链子”上面。

继承

上面介绍的内容其实已经非常像继承了。我们来实现一下。

我们要做这几件事:

  • 使其能找到相应的方法
  • 使其有正确的类型(instanceof)
  • 使其可以正常的实例化
  • 使其各方面都正常工作

如何使其能找到相应的方法,我们在上面已经说过了。接下来我们说说第二项:使其有正确的类型。

假设系统中有People(人)、Student(学生)、CollegeStudent(大学生)三个类:

function People(name) {
	this.name = name;
}
People.prototype.getName = function() {
	return this.name;
}

function Student() {
}

function CollegeStudent() {
}

我们要做的事有:

  • CollegeStudent.prototype.__proto__Student.prototype
  • Student.prototype.__proto__People.prototype

因此,我们编写如下代码:(Object.create的作用见此

function People(name) {
	this.name = name;
}
People.prototype.getName = function() {
	return this.name;
}

function Student() {
}
Student.prototype = Object.create(People.prototype);

function CollegeStudent() {
}
CollegeStudent.prototype = Object.create(Student.prototype);

这样它们就有了正确的类型:

var p = new CollegeStudent();
console.log(p instanceof CollegeStudent); // => true
console.log(p instanceof Student); // => true
console.log(p instanceof People); // => true

现在我们看看第三步:使其可以正常的实例化。在其他语言中,我们会这样写:

class People {
	private String name;
	public People(String name) {
		this.name = name;
	}
}

class Student extends People {
	public Student(String name) {
		super(name); // 这里
	}
}
class People {
	private $name;
	public function __construct($name) {
		$this->name = $name;
	}
}

class Student extends People {
	public function __construct($name) {
		parent::__construct($name); // 这里
	}
}

在JavaScript中也需要做类似的事。XXX.call的作用是改变其内部的this指向,例如下面我们在Student函数中调用了People.call(this, name),则在运行People函数的时候,People函数内部的this实际上是Student的实例。(相关文档

function People(name) {
	this.name = name;
}
People.prototype.getName = function() {
	return this.name;
}

function Student(name) {
	People.call(this, name);
}
Student.prototype = Object.create(People.prototype);

function CollegeStudent(name) {
	Student.call(this, name);
}
CollegeStudent.prototype = Object.create(Student.prototype);

最后,我们要恢复constructor

function People(name) {
	this.name = name;
}
People.prototype.getName = function() {
	return this.name;
}

function Student(name) {
	People.call(this, name);
}
Student.prototype = Object.create(People.prototype);
Student.prototype.constructor = Student;

function CollegeStudent(name) {
	Student.call(this, name);
}
CollegeStudent.prototype = Object.create(Student.prototype);
CollegeStudent.prototype.constructor = CollegeStudent;

完整Demo可以查看这里

ES6

如果你看完了上面的“长篇大论”,你一定会觉得这太麻烦了。所以,ES6中,可以很轻松的实现继承:

class People {
	constructor(name) {
		this.name = name;
	}
	getName() {
		return this.name;
	}
}

class Student extends People {
	constructor(name) {
		super(name);
		this.isStudent = true;
	}
}

class CollegeStudent extends Student {
	constructor(name, department) {
		super(name);
		this.department = department;
	}
}

不过,它只是一个“语法糖”。我们可以打印出来看看:

5

总的来说,它实际上做了两件事:方法上浮(至__proto__),属性下沉(至对象)

完整Demo可以查看这里

小结

这篇文章是用五笔打出来的……