这是一系列对于初学者的 javascript 文章,包括很多的基础知识,由于我也是初学者,所以,翻译过来,大家一起学习,这一系列就叫做 javascript 101 吧,嘿嘿,当然,一般情况下,每篇文章,基本不会超过十分钟。

经过多年对面向对象编程语言的学习,类(Class)的概念已经深深的在脑海中扎根,但是对于 javascript 却有点让人沮丧,因为 javascript 没有"类"的概念,但是,对于继承来说,javascript 有它自己的一套方式,甚至有时候比基于类的语言更具灵活性。

首先要认识到的就是明白基于对象的和基于类的语言的区别,而 javascript 给了我们使用类似“类”的语言能做的工作的大部分功能,也就是说,javascript 的继承方式,更类似与基于类的语言。

首先我们简单看一下 prototype 属性,看看它如何使我们更深入理解 javascript 。

prototype 属性

prototype 属性是一个内部属性,它的作用就是被设计用来实现继承。这里说的继承,有一个具体的形式,就是说,某个对象(例如 对象 b)的状态和方法都是由另一个对象(例如对象 a)产生的,因此,这里(对象 b)的结构,行为和状态,都是被继承的(继承于对象 a)。(ES5: Objects),这也是基于类的语言所有的,实例(instance)具有状态,而类具有方法。

构造函数(constructor)也是一个函数,有一个名为原型(prototype)的属性:

1
2
3
4
function Animal() {
}
 
console.log(Animal.prototype);

在这里会显示 {} 或者 object ,证明 Animal 对象具有 prototype 属性。但是,我们还没有在函数体内定义任何东西,然后增加一些代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Animal() {
}
 
Animal.prototype.type = 'Unknown';
Animal.prototype.weight = 0;
Animal.prototype.weightUnits = 'kg';
 
Animal.prototype.toString = function() {
  return this.type + ', ' + this.weight + this.weightUnits;
};
 
var molly = new Animal();
molly.type = 'Dog';
molly.weight = 28;
 
console.log(molly.toString());

此时,将会显示 "Dog, 28kg",同样的,此代码可以被改写,将其写到一起:

1
2
3
4
5
6
7
8
9
10
11
12
function Animal() {
}
 
Animal.prototype = {
  type: 'Unknown',
  weight: 0,
  weightUnits: 'kg',
 
  toString: function() {
    return this.type + ', ' + this.weight + this.weightUnits;
  }
};

这些和已经熟悉的基于类的语言,看起来应该没有什么太大的不同。

动态原型

可以动态的添加属性,只要赋值即可。

1
2
3
4
5
6
7
8
9
10
11
var molly = new Animal(), harley = new Animal();
 
molly.type = 'Dog';
molly.weight = 28;
 
harley.type = 'Dog';
harley.weight = 38;
harley.name = 'Harley';
 
console.log(molly);// { type: 'Dog', weight: 28 }
console.log(harley);// { type: 'Dog', weight: 38, name: 'Harley' }

此处添加的 "name" 属性,只影响了,(当前的)实例,但是,构造函数(constructor)的原型(prototype),同样可以被改变, 但是这样的话,就会影响所有继承此原型的对象了。例如:

1
2
3
4
5
6
7
8
var molly = new Animal();
 
Animal.prototype.weightUnits = 'oz';
 
molly.type = 'Dog';
molly.weight = 28;
 
console.log(molly.toString());//'Dog, 28oz'

不过,这样做(改变原型)有可能带来不好的结果。这也是很多类库不会这么做的原因,因为这么做可能更改本地内建函数的功能(浏览器自身),例如:

1
2
3
4
5
String.prototype.match = function() {
  return true;
};
 
console.log('alex'.match(/1234/));

此时,不管我怎么使用 match() 方法,其都会返回 true ,因此虽然成功的改写了此函数,但是也破坏了浏览器自身(原生的) javascript 的功能。

但是,这并不一定是坏事,因为我们总需要改写或者扩展一些原生的 javascript 功能,例如:使旧浏览器支持新的 javascript 标准。

如果改写了原生的 prototype 属性,会发生什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var molly = new Animal(), harley;
 
molly.type = 'Dog';
molly.weight = 28;
 
Animal.prototype = {
  toString: function() { // 改写原生的 toString 方法。
    return '...';
  }
};
 
harley = new Animal;
harley.type = 'Dog';
harley.weight = 38;
 
console.log(molly.toString());// Dog, 28kg
console.log(harley.toString());// ...

在 molly 时,并没有改写原生的 toString 方法,所以其正常输出,但是到 harley 时,由于改写了其方法,所以,其输出 "..." 省略号。

尽管在更改了 prototype 的情况下,应该影响所有的实例,但是,更改(重写)了构造函数的 prototype 却并不影响到所有的实例。这是因为,所有的实例都引用自原型,而不是原型的一份拷贝,也可以这么说:使用 new 关键字,创建的实例,都会连接到原始的原型。