3

JavaScript面向对象编程

如何定义自定义类型

首先需要明确,JavaScript并不是传统意义上的OO语言,它并没有class的概念,
而是包含了另一套异常强大的原型机制。它的类型体系、继承体系都建立在原型基础之上。
为了迎合传统的OO开发者,JavaScript语言的设计者通过这套原型体系模拟了传统面向对象语言的编码风格。

简单来说,建立一个自定义类型只需要编写类型的构造函数即可:

javascriptfunction Person(name, age) {
  this.name = name;
  this.age = age;
}

// 实例化 Person 类型
Person person = new Person("John", 34);

Person构造函数与一般的函数没有任何区别,只是调用方式不太一样,
通过使用new关键字,改变了一般函数调用的行为,有点类似于下面这样:
1. Object obj = new Object();
2. Person.call(obj, "John", 34);
3. obj.__proto__ = Person.prototype;
4. return obj;

序号2call函数调用的作用是改变执行Person函数时的thisobj
序号3的作用是设置新建实例的原型(一个实例的__proto__属性是这个实例的原型)。
记住,所有函数(如这里的Person)的prototype属性默认都是一个Object实例。
所以序号3执行后,person.__proto__正是Person.prototype
这解释了为什么所有的引用类型都派生自Object

除此之外,从上面的介绍还应该意识到Person的所有实例的__proto__属性都是Person.prototype

上面定义的属性都是实例的属性,也可以直接为某个类型添加类型的属性(记住,方法也是一个对象),
而这个属性无法通过类型的实例访问到,如下面的代码:

javascriptPerson.country = "Canada";

另一个例子是ECMAScript 5引入的Object类型的getPrototypeOf方法,它可以获得一个实例的原型变量:

javascriptObject.getPrototypeOf = function(instance) {
  // some code..
};

如果想定义同一个类型所有实例共享的属性(比如方法),可以定义在类型的原型中:

javascriptPerson.prototype.logName = function() {
  console.log(this.name);
};

需要注意,通过Person的实例只能读取原型中的属性,而不能重写;
如果尝试重写,实际上是在实例中定义了一个同名的属性,从而屏蔽了原型中的属性:

javascript// 并没有改变 Person.prototype.logName的值
person.logName = function() {
  // some code..
}

造成屏蔽的原因是当使用对象.属性时,
是从对象开始查找属性,如果没有找到再在其原型中查找,
如果还没有找到,再查找其原型的原型,以此类推在原型链上不断向上查找,
第一次查找到属性后查找过程就结束了。
如果想恢复被屏蔽的原型属性,可以使用delete操作符:

javascriptdelete person.logName;

最佳的实践是将实例的属性定义在构造函数中,将方法定义在原型中。
这样每个实例独享自己的属性,并和其他同类型的实例共享方法:

javascript// 构造函数
function Person(name, age) {
    this.name = name;
    this.age = age;
}
// 原型
Person.prototype.logName = function() {
  console.log(this.name);
}

以上这种方式定义的Person类型,可以通过instanceof来判断一个实例是否是Person类型的:

javascriptPerson 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的原理,假设执行下面的代码:

javascriptfunction 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实例,
这是因为所有的方法(比如这里的PersonStudent)的prototype属性默认都是一个Object类型的实例,
这也证明了为什么所有的内置类型和自定义类型无一例外全部都派生自Object类型。

当执行下面的代码时:

javascriptconsole.log(student instanceof Student);    // true

实际上是判断Student.prototype是否在student的原型链中出现,如果出现了则返回true。
这里Student.prototypeperson,是student的原型,所以返回true。
再看:

javascriptconsole.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();

bedew
278 声望3 粉丝