Reference : JavaScript教程 - 廖雪峰的官方网站
JavaScript哲学:万物皆对象
JavaScript没有类 (class) 或实例 (instance) 的概念。JavaScript实现面向对象编程的工具是原型 (prototype)。
原型 prototype
以下内容展示JavaScript面向对象编程的原理。
我们定义一个对象robot
:
var robot = {
name: 'Robot',
height: 1.6,
run: function () {
console.log (this.name + 'is running ...');
}
};
我们将这个对象作为模板对象,为了方便理解,重新用Student
命名它。
var Student = {
name: 'Robot',
height: 1.6,
run: function () {
console.log (this.name + 'is running ...');
}
};
现在想要创建对象xiaoming
,同时让这个新的对象获得Student
对象相同的属性和方法。
var xiaoming = {
name: '小明'
};
xiaoming.__proto__ = Student;
最后一行代码把xiaoming
的原型指向了对象Student
,看上去xiaoming
仿佛继承自Student
对象。
xiaoming.name; // '小明'
xiaoming.run(); // 小明 is running ...
如果现在定义一个新的对象Bird
,然后让xiaoming
的原型指向Bird
。
var Bird = {
fly: function () {
console.log (this.name + 'is flying ...');
}
};
xiaoming.__proto__ = Bird;
这时xiaoming
已经无法run()
了,他已经变成了一只鸟:
xiaoming.fly(); // 小明 is flying ...
注意上面直接修改obj.__proto__
的做法在开发时不可取,而且低版本的IE不支持这种写法。现在我们理解了面向对象编程的原理,接下来介绍推荐的面向对象编程方法。
面向对象编程
原型链
// 原型对象:
var Student = {
name: 'Robot',
height: 1.2,
run: function () {
console.log(this.name + ' is running...');
}
};
Object.create()
方法可以传入一个原型对象,并创建一个基于该原型的新对象,但是新对象什么属性都没有。
var new_student = Object.create(Student);
new_student; //{}
new_student.height; // 1.6
new_student.name; // 'robot'
可以看出,尽管新的对象没有自己的属性,但实际上具有了Student
对象的所有属性。基于此我们可以猜想,在获取对象属性时,先在对象内部查找,然后顺着原型依次向上查找。实际就是这样,而且如果访问到最顶层的Object.prototype
对象并且还是找不到这个属性,就会返回undefined
。
这里引入原型链的概念。正如上面所说,JavaScript的每一个对象,其__proto__
属性仍是一个对象,因此可以形成一条原型链。以内置的Array
对象为例,我们可以用[]
创建一个Array
对象。
比如,
var arr = [1, 2, 3];
其原型链是:
arr ----> Array.prototype ----> Object.prototype ----> null
Array.prototype
定义了indexOf()
、shift()
等方法,因此我们可以在所有的Array
对象上直接调用这些方法。
再举一个例子,我们可以用function
关键字创建函数。
function foo () {
return 0;
}
函数也是一个对象,它的原型链是:
foo ----> Function.prototype ----> Object.prototype ----> null
由于Function.prototype
定义了apply()
等方法,因此所有函数都可以调用apply()
方法。
很容易想到,如果原型链很长,那么访问一个对象的属性就会因为花更多的时间查找而变得更慢,因此要注意不要把原型链搞得太长。
当然,我们可以把Object.create
这个方法包装成一个创建新对象的函数。
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
构造函数
除了直接用{...}
创建一个对象外,JavaScript还可以用一种构造函数的方法来创建对象。用法是定义一个构造函数,比如:
function Student (name) {
this.name = name;
this.hello = function () {
alert ('Hello, ' + this.name + '!');
}
}
这个函数虽然看上去和普通函数一样,但只要用关键字new
调用这个函数,它就默认是构造函数,且在函数结束后一定返回this
,无论在函数中是否写有return
返回语句。
调用的写法如下:
var xiaoming = new Student ('小明');
xiaoming.name; // '小明'
xiaoming.hello(); // Hello, 小明!
值得注意的是,这个函数如果不写new
并调用,则成为了一个返回undefined
的普通函数。
这样新建的xiaoming
的原型链是:
xiaoming ----> Student.prototype ----> Object.prototype ----> null
此外,用new Student()
类似语句创建的对象还从原型上获得了一个constructor
属性,它指向Student
本身。这段话用代码表示如下。
xiaoming.constructor === Student.prototype.constructor; // true
Student.prototype.constructor === Student; // true
Object.getPrototypeOf(xiaoming) === Student.prototype; // true
xiaoming instanceof Student; // true
用图片表示如下,其中红色代表原型链:
对象共享方法
现在这么写,有一个问题:
xiaoming = new Student ('小明');
xiaohong = new Student ('小红');
xiaoming.hello === xiaohong.hello; // false
两个对象的方法不相等,显然浪费了内存空间,因为对于函数而言,我们只需要保存一份就可以了。存在两份的原因是每次调用new Student()
,都会在构造时执行var hello
一句。对这个问题,一个可行的优化是把hello
的定义放在xiaoming
和xiaohong
公共的原型上,而不是构造函数里,这样在调用hello
时,就会通过原型链查找到hello
。具体的代码如下:
function Student (name) {
this.name = name;
}
Student.prototype.hello = function () {
alert ('Hello, ' + this.name + '!');
};
优化的原理通过上面关于原型链的内容很容易理解,即用new Student()
语法创建的对象,其原型都指向Student.prototype
。
当然了,即使有了方便的构造函数工具,我们仍然建议将new
的过程封装在函数里完成。原因有二,一是不需要new
来调用,避免了漏写new
的可能;二是参数更灵活,参数可以不用完整地传递。
原型继承
function inherits( Child, Parent) {
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
}
关于这个函数的内容,请在原文中寻找解释。
这里需要增加的解释是为什么不直接用
PrimaryStudent.prototype = Student.prototype
可以从图中看出,prototype
实际指向一个对象,如果用了上面的语句,那么PrimaryStudent
就会和Student
共享同一个原型对象,这样绑定到PrimaryStudent.prototype
上的属性(特别是对象共享的方法)也会被绑定到Student.prototype
上,因为它们实际是同一个对象,这样就不符合子类和父类之间的关系。因此可以看出,我们创建的new F()
对象,就是为了独立地绑定PrimaryStudent
对象共享的方法。
class关键字 [ES6]
在上面的章节中我们看到了JavaScript的对象模型是基于原型实现的,特点是简单,缺点是理解起来比传统的类-实例模型要困难,最大的缺点是继承的实现需要编写大量代码,并且需要正确实现原型链。
新的关键字class
正是为了简化类的定义而引入。对于下面这个用构造函数实现的Student
:
function Student (name) {
this.name = name;
}
Student.prototype.hello = function () {
alert ('Hello, ' + this.name + '!');
};
如果用新的关键字class
来实现,可以这样写:
class Student {
constructor(name) {
this.name = name;
}
hello() {
alert('Hello, ' + this.name + '!');
}
}
显然用class
的代码简介明了,既包含了构造函数constructor
的定义,也包含了原先定义在原型对象上的函数hello()
(注意没有function
关键字),这样就避免了代码分散可能造成的理解障碍。
最后,这样定义的Student
也是用new
关键字调用,得到的对象与之前得到的对象用发法相同。
var xiaoming = new Student ('小明');
class继承 [ES6]
不需要考虑桥接的原型对象,直接用extends
关键字完成继承。
class PrimaryStudent extends Student {
constructor(name, grade) {
super(name); // 记得用super调用父类的构造方法!
this.grade = grade;
}
myGrade() {
alert('I am at grade ' + this.grade);
}
}
几个要点:
- 用
class
关键字引导 - 用
extends
关键字引出父类 - 在
constructor
定义开头用super()
调用父类的构造方法
由于现在很多浏览器还不支持ES6的所有新特性,特别是class
,在这里介绍一个小工具,用于将下一代的JavaScript代码转换为同义的较低版本代码:Babel - The compiler for next generation JavaScript。注:这个工具为原文推荐,而本文写作的时间比参考的文章晚3年,现在的主流数浏览器已经支持了ES6标准。
网友评论