首先需要明确,JavaScript并不是传统意义上的OO语言,它并没有 class
的概念,
而是包含了另一套异常强大的原型机制。它的类型体系、继承体系都建立在原型基础之上。
为了迎合传统的OO开发者,JavaScript语言的设计者通过这套原型体系模拟了传统面向对象语言的编码风格。
简单来说,建立一个自定义类型只需要编写类型的构造函数即可:
javascript
function Person(name, age) { this.name = name; this.age = age; } // 实例化 Person 类型 Person person = new Person("John", 34);
Person
构造函数与一般的函数没有任何区别,只是调用方式不太一样,
通过使用 new
关键字,改变了一般函数调用的行为,有点类似于下面这样:
Object obj = new Object();
Person.call(obj, "John", 34);
obj.__proto__ = Person.prototype;
4. return obj;
序号 2
中 call
函数调用的作用是改变执行Person函数时的 this
为 obj
,
序号 3
的作用是设置新建实例的原型(一个实例的 __proto__
属性是这个实例的原型)。
记住,所有函数(如这里的Person)的 prototype
属性默认都是一个 Object
实例。
所以序号 3
执行后, person.__proto__
正是 Person.prototype
,
这解释了为什么所有的引用类型都派生自 Object
。
除此之外,从上面的介绍还应该意识到 Person
的所有实例的 __proto__
属性都是 Person.prototype
。
上面定义的属性都是实例的属性,也可以直接为某个类型添加类型的属性(记住,方法也是一个对象),而这个属性无法通过类型的实例访问到,如下面的代码:
javascript
Person.country = "Canada";
另一个例子是ECMAScript 5引入的 Object
类型的 getPrototypeOf
方法,它可以获得一个实例的原型变量:
javascript
Object.getPrototypeOf = function(instance) { // some code.. };
如果想定义同一个类型所有实例共享的属性(比如方法),可以定义在类型的原型中:
javascript
Person.prototype.logName = function() { console.log(this.name); };
需要注意,通过 Person
的实例只能读取原型中的属性,而不能重写;
如果尝试重写,实际上是在实例中定义了一个同名的属性,从而屏蔽了原型中的属性:
javascript
// 并没有改变 Person.prototype.logName的值 person.logName = function() { // some code.. }
造成屏蔽的原因是当使用 对象.属性
时,
是从 对象
开始查找 属性
,如果没有找到再在其原型中查找,
如果还没有找到,再查找其原型的原型,以此类推在原型链上不断向上查找,
第一次查找到 属性
后查找过程就结束了。
如果想恢复被屏蔽的原型属性,可以使用 delete
操作符:
javascript
delete person.logName;
最佳的实践是将实例的属性定义在构造函数中,将方法定义在原型中。这样每个实例独享自己的属性,并和其他同类型的实例共享方法:
javascript
// 构造函数 function Person(name, age) { this.name = name; this.age = age; } // 原型 Person.prototype.logName = function() { console.log(this.name); }
以上这种方式定义的 Person
类型,可以通过 instanceof
来判断一个实例是否是 Person
类型的:
javascript
Person person = new Person("John", 34); console.log(john instanceof Person); // true
实际上 instanceof
是通过实例的原型链来判断一个对象是否某个类型的实例的,具体的细节后面会详细介绍。
这里首先介绍一下如何获得一个实例的原型对象:
1. isPrototypeOf()
你可以判断一个对象是否在另一个对象的原型链上出现:
Person person = new Person("John", 34); console.log(Person.prototype.isPrototypeOf(person)); // true
2. Object.getPrototypeOf()
你可以得到一个对象的原型。
这个方法是ECMAScript 5引入的,某些IE浏览器并不支持:
// get the prototype of person instance Object.getPrototypeOf(person);
3. __proto__
JavaScript中 每个 对象都有一个指向其原型的内部属性,
在某些浏览器(如Chrome)中可以使用它们。
既然可以将属性定义在实例本身或它的原型链中,那么可不可以判断某个属性具体是在哪里定义呢?当然可以:
1. hasOwnProperty()
如果属性在实例本身出现,则返回 true
:
console.log(person.hasOwnProperty("name")); // true console.log(person.hasOwnProperty("logName")); // false
2. in
操作符,如果属性在实例或其原型链中出现,则返回true
alert("logName" in person); // true
利用 in
操作符,我们还可以枚举出一个实例和它原型链中所有可枚举的属性:
for (var propertyName in person) { // log all property name and its value console.log(propertyName + "/t/t" + person[propertyName]); }
最后来介绍一下 instanceof
的原理,假设执行下面的代码:
javascript
function Person(name, age) { this.name = name; this.age = age; } function Student(name, age, school) { Person.call(this, name, age); this.school = school; } // Student 继承了 Person Student.prototype = new Person(); Student student = new Student("Adam", 30, " School");
关于继承的细节之后再详细讨论,这里只需要明确我们将 Student
类型的原型赋值为一个 Person
实例。
此时 student
的原型链可以表示为:
student.__proto__
==> Person
实例(假设为 person
)
person.__proto__
==> Object
实例(假设为 object
)
object.__proto__
==> null
(到顶了)
也许会有人疑惑 person
的原型为什么是 Object
实例,
这是因为所有的方法(比如这里的 Person
、 Student
)的prototype属性默认都是一个Object类型的实例,
这也证明了为什么所有的内置类型和自定义类型无一例外全部都派生自 Object
类型。
当执行下面的代码时:
javascript
console.log(student instanceof Student); // true
实际上是判断Student.prototype是否在student的原型链中出现,如果出现了则返回true。
这里 Student.prototype
是 person
,是student的原型,所以返回true。
再看:
javascript
console.log(student instanceof Person); // true
同理因为存在 Person.prototype === student.__proto__.__proto__
,所以返回true。
看到这里相信你应该对 prototype
和 __proto__
的关系有了比较清楚的理解了。
它们之间的关系可以总结为:
__proto__
:
__proto__
is the actual object that is used in the lookup chain to resolve methods.
It is a property that all objects have.
This is the property which is used by the JavaScript engine for inheritance.
According to ECMA specifications it is supposed to be an internal property,
however most vendors allow it to be accessed and modified.
prototype
:
prototype
is a property belonging only to functions.
It is used to build __proto__
when the function
happens to be used as a constructor with the new keyword.
可以说JavaScript是Python的另一个极端
——There’s always more than one way to do it.
实现继承也不例外,不同的实现模式有不同的使用场景,
各有优势和不足,这里只介绍一个最常被使用的模式——组合继承模式,直接看例子:
javascript
/* * 基类,定义属性 */ function Person(name, age) { this.name = name; this.age = age; } /* * 基类,定义方法 */ Person.prototype.selfIntroduce = function () { console.log("name: " + this.name); console.log("age: " + this.age); } /* * 子类,定义子类的属性 */ function Student(name, age, school) { // 调用基类的构造函数 Person.call(this, name, age); this.school = school; } // 使子类继承基类 Student.prototype = new Person(); /* * 定义子类的方法 */ Student.prototype.goToSchool = function() { // some code.. } /* * 扩展并调用了超类的方法 */ Student.prototype.selfIntroduce = function () { Student.prototype.__proto__.selfIntroduce.call(this); console.log("school: " + this.school); } var student = new Student("John", 22, "My School"); student.selfIntroduce();