JavaScript的原型及原型链

javascript王国的一次旅行,一个没有类的世界怎么玩转面向对象? 一文中我们提到了在没有类概念的 JavaScript 语言中是通过原型来实现面向对象的继承特性。只要运用得好,这种基于原型的 JavaScript 继承模型比传统的类继承还要强大。所以我们还需详细学习一下原型的知识

JavaScript 的对象

JavaScript 是基于对象的面向对象语言。因此在这里的“对象”既可以是普通对象(Object),也可以是函数对象(Function)。JS 抛弃了 Java 的类概念,而 Java 的继承恰恰是通过类来实现的。那么 JS 没有类的概念,就使用了“原型”的概念来实现继承。

字面量原型及原型链

JS 可通过字面量构造对象。为了实现继承,对象里面有个_proto_属性可以指向该对象的父对象。这个父对象就是所谓的“原型”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var animal = {
name: '动物',
eat: function(){
console.log(this.name + " is eating");
}
};

animal.eat(); // animal is eating

var dog = {
name: '狗',
_proto_: animal
};

var cat = {
name: '猫',
_proto_: animal
};

dog.eat(); // 狗 is eating
cat.eat(); // 猫 is eating

由上面代码我们可以看出:dog 和 cat 对象的原型都是 animal。但是 dog 和 cat 对象都没有定义 eat()方法,那怎么可以调用呢?其实当eat方法被调用的时候,先在自己的方法列表中寻找, 如果找不到,就去找原型中的方法, 如果原型中找不到, 就去原型的原型中去寻找…… 最后找到Object那里, 如果还找不到, 那就是未定义了。这几个对象通过_proto_属性建立一个原型链!

原型链

构造函数原型及原型链

但是为了迁就 C++、Java、C# 程序员,让 JavaScript 可以像 Java 那样 new (构造)出一个对象出来,于是这里做了一个变通,也提供了构造函数。例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Student(name,grade){
this.name = name;
this.grade = grade;
this.sayHello = function(){
console.log('Hi,I'm ' + this.name);
}
}
andy = new Student('Andy',5);
lisa = new Student('Lisa',5);

andy.sayHello(); //Hi,I'm Andy
lisa.sayHello(); //Hi,I'm Lisa

//假设 andy 和 lisa 是同班同学,同时即将升入六年级
andy.grade = 6;
console.log(andy.grade); // 6
console.log(lisa.grade); // 5

由上面的代码所示,我们可以看出这个所谓的构造函数已经很有 Java 类的感觉了。但是这里面存在两个问题:在对象里面定义方法,这样每创建一个对象都会一个sayHello()函数,这样来说显得对象臃肿,浪费资源;同时每个对象各自保有自己的属性和函数的副本,无法做到属性和方法共享。因此,这里有一个更加高效的办法就是把对象共享的属性和方法可以放到 Student.prototype 这个对象当中。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function Student(name){
this.name = name;
}

Student.prototype = {
grade: 5,
sayHello: function(){
console.log("Hi, I'm "+this.name);
}
};

/*也可以写成
Student.prototype.grade = 5;
Student.prototype.sayHello = function(){
console.log("Hi, I'm "+this.name);
};
*/

var andy = new Student("Andy");
var lisa = new Student("Lisa");

andy.sayHello(); //Hi, I'm Andy
lisa.sayHello(); //Hi, I'm Lisa

Student.prototype.grade = 6;
console.log("Andy's grade: "+ andy.grade); // 6
console.log("Lisa's grade: "+ Lisa.grade); // 6

构造函数原型链

由上面的代码和示意图可看出这个所谓的构造函数 Student 其实就是一个幌子啊, 每次去new Student的时候,确实会创建一个对象出来( andy 或者 lisa ) , 并且把这个对象的原型指针(_proto_)指向 Student.prototype 这个对象,这样一来就能找到sayHello()方法了。我们应该还知道上面的构造函数Student()对象(JS 中函数也是对象)会创建一个 prototype 对象(Student.prototype),而 new 出来的实例对象例如 andy 和 lisa 是没有这个 prototype 对象,但是他会有个 proto 属性(_proto_)指向这个构造函数对象的 prototype 对象,从而构成原型链。实例对象其实是通过原型对象与构造函数取得联系的。为了让 Java、C#、C++ 程序员降低学习成本,JavaScript 提供了语法糖:

1
2
3
4
5
6
7
8
9
10
11
class Student {
constructor(name){
this.name = name;
}
sayHello(){
console.log("Hi, I'm "+this.name);
}
}

var andy = new Student("andy");
andy.sayHello(); //Hi, I'm andy

Object.create()

1
2
3
4
5
6
var a = Object.create(null);
console.log(a); //{}
a.name = 'Zhiyu';
var b = Object.create(a);
console.log(b); //{}
console.log(b.name); //Zhiyu

上面我们可以看出该方法是创建一个空对象,空对象的原型是create()参数。此时创建的空对象会有个(_proto_)属性指向方法参数,这样也可以构成一个原型链。

总结

  1. JS 在创建对象(不论是普通对象还是函数对象)的时候,都有一个叫做 proto 的内置属性,用于指向创建它的函数对象的原型对象 prototype
  2. 原型和原型链是 JS 实现继承的一种模型
  3. 原型链是靠 proto 形成的,而不是 prototype
  4. 所有的原型对象都有 constructor 属性,该属性对应创建所有指向该原型的实例构造函数
  5. 函数对象和原型对象通过 prototype 和 constructor 属性进行相互关联

参考

本文作者:刘志宇

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!

Donate comment here